mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:21:50 +08:00
958 lines
28 KiB
JavaScript
958 lines
28 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { createHash } = require('node:crypto');
|
|
const fs = require('node:fs');
|
|
|
|
const {
|
|
createPluginPackageResourceGenerationFromReferences,
|
|
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
|
|
const {
|
|
createProjectToolDefinitionSnapshot,
|
|
projectToolDefinitionRegistry,
|
|
} = require('@qinglong/runtime-core/project-tool-definition-snapshot');
|
|
const {
|
|
createStepRunMutation,
|
|
transitionStepRunMutation,
|
|
} = require('@qinglong/runtime-core/step-run');
|
|
const {
|
|
createToolExecutionCompletionCommand,
|
|
createToolExecutionResultArtifact,
|
|
toolExecutionResultKeyBinding,
|
|
} = require('@qinglong/runtime-core/tool-execution-completion');
|
|
const {
|
|
createToolExecutionEvidenceBundle,
|
|
toolExecutionAdmissionEvidence,
|
|
TOOL_EXECUTION_START_AUDIT_OPERATION,
|
|
} = require('@qinglong/runtime-core/tool-execution-evidence');
|
|
const {
|
|
createToolExecutionStartCommand,
|
|
} = require('@qinglong/runtime-core/tool-execution-start-barrier');
|
|
const {
|
|
TrustedToolHandlerBindingRegistry,
|
|
admitTrustedToolExecution,
|
|
createTrustedToolHandlerBinding,
|
|
createTrustedToolInvocationPlan,
|
|
trustedToolContractIdentityDigest,
|
|
} = require('@qinglong/runtime-core/trusted-tool-invocation');
|
|
const {
|
|
TRUSTED_TOOL_EXECUTION_RESULT_SCHEMA,
|
|
} = require('@qinglong/runtime-core/trusted-tool-execution');
|
|
const {
|
|
prepareToolInvocation,
|
|
} = require('@qinglong/runtime-core/tool-registry');
|
|
const {
|
|
createToolResultKeyCatalogBootstrapCommand,
|
|
createToolResultKeyRetirementCommand,
|
|
createToolResultKeyRotationCommand,
|
|
requireActiveToolResultKey,
|
|
toolResultKeyCatalogFence,
|
|
toolResultKeyMaterialProof,
|
|
} = require('@qinglong/runtime-core/tool-result-key-catalog');
|
|
const {
|
|
createToolExecutionResultRekeyCommand,
|
|
createToolResultKeyRetirementReceiptCommand,
|
|
} = require('@qinglong/runtime-core/tool-result-rekey');
|
|
|
|
const { openLocalSqliteClient } = require('../../dist/storage/config');
|
|
const {
|
|
migrateLocalSqliteDatabase,
|
|
} = require('../../dist/migration/migration');
|
|
const {
|
|
LocalSqliteOperationAuthority,
|
|
} = require('../../dist/authority/operationAuthority');
|
|
const {
|
|
LocalSqliteStepRunRepository,
|
|
} = require('../../dist/run/stepRunRepository');
|
|
const {
|
|
LocalSqliteToolExecutionCompletionRepository,
|
|
} = require('../../dist/tool-execution/toolExecutionCompletionRepository');
|
|
const {
|
|
LocalSqliteToolExecutionStartBarrierRepository,
|
|
} = require('../../dist/tool-execution/toolExecutionStartBarrierRepository');
|
|
const {
|
|
LocalSqliteToolInvocationArtifactRepository,
|
|
} = require('../../dist/tool-execution/toolInvocationArtifactRepository');
|
|
const {
|
|
LocalSqliteToolResultKeyCatalogRepository,
|
|
} = require('../../dist/tool-execution/toolResultKeyCatalogRepository');
|
|
const {
|
|
LocalSqliteToolResultRekeyRepository,
|
|
} = require('../../dist/tool-execution/toolResultRekeyRepository');
|
|
|
|
const PROJECT_ID = 'crash-tool-result-project';
|
|
const RUN_ID = 'crash-tool-result-run';
|
|
const STEP_RUN_ID = 'crash-tool-result-step';
|
|
const START_ID = 'crash-tool-result-start';
|
|
const RESULT_KEY_A_ID = 'crash-result-key-a';
|
|
const RESULT_KEY_B_ID = 'crash-result-key-b';
|
|
const RESULT_KEY_A = Buffer.alloc(32, 41);
|
|
const RESULT_KEY_B = Buffer.alloc(32, 42);
|
|
const INVOCATION_KEY = Buffer.alloc(32, 43);
|
|
const SUBJECT = Object.freeze({
|
|
type: 'user',
|
|
id: 'usr-crash-tool-result',
|
|
});
|
|
const POLICY_FENCE = Object.freeze({
|
|
projectVersion: 1,
|
|
bindingVersion: 1,
|
|
});
|
|
const TOOL = Object.freeze({
|
|
name: 'crash.result.read',
|
|
version: '1.0.0',
|
|
});
|
|
const OUTPUT = Object.freeze({
|
|
summary: 'SQLite crash matrix durable result',
|
|
});
|
|
const OUTPUT_DIGEST_DOMAIN = Buffer.from(
|
|
'qinglong/trusted-tool-execution-output-digest@v1\0',
|
|
'utf8',
|
|
);
|
|
const RESULT_DIGEST_DOMAIN = Buffer.from(
|
|
'qinglong/trusted-tool-execution-result-digest@v1\0',
|
|
'utf8',
|
|
);
|
|
|
|
const CRASH_POINTS = Object.freeze({
|
|
completion_before_begin: Object.freeze({
|
|
operation: 'completion',
|
|
timing: 'beforeExec',
|
|
sql: 'BEGIN IMMEDIATE',
|
|
durable: false,
|
|
}),
|
|
completion_after_binding: Object.freeze({
|
|
operation: 'completion',
|
|
timing: 'afterRun',
|
|
sql: 'INSERT INTO "ToolExecutionResultKeyBindings"',
|
|
durable: false,
|
|
}),
|
|
completion_after_commit: Object.freeze({
|
|
operation: 'completion',
|
|
timing: 'afterExec',
|
|
sql: 'COMMIT',
|
|
durable: true,
|
|
}),
|
|
rekey_after_overlay: Object.freeze({
|
|
operation: 'rekey',
|
|
timing: 'afterRun',
|
|
sql: 'INSERT INTO "ToolExecutionResultRekeyOverlays"',
|
|
durable: false,
|
|
}),
|
|
rekey_after_head: Object.freeze({
|
|
operation: 'rekey',
|
|
timing: 'afterRun',
|
|
sql: 'INSERT INTO "ToolExecutionResultRekeyHeads"',
|
|
durable: false,
|
|
}),
|
|
rekey_after_commit: Object.freeze({
|
|
operation: 'rekey',
|
|
timing: 'afterExec',
|
|
sql: 'COMMIT',
|
|
durable: true,
|
|
}),
|
|
receipt_after_insert: Object.freeze({
|
|
operation: 'receipt',
|
|
timing: 'afterRun',
|
|
sql: 'INSERT INTO "ToolResultKeyRetirementReceipts"',
|
|
durable: false,
|
|
}),
|
|
receipt_after_commit: Object.freeze({
|
|
operation: 'receipt',
|
|
timing: 'afterExec',
|
|
sql: 'COMMIT',
|
|
durable: true,
|
|
}),
|
|
retire_after_insert: Object.freeze({
|
|
operation: 'retire',
|
|
timing: 'afterRun',
|
|
sql: 'INSERT INTO "ToolResultKeyCatalogGenerations"',
|
|
durable: false,
|
|
}),
|
|
retire_after_commit: Object.freeze({
|
|
operation: 'retire',
|
|
timing: 'afterExec',
|
|
sql: 'COMMIT',
|
|
durable: true,
|
|
}),
|
|
});
|
|
|
|
function hash(domain, value) {
|
|
return createHash('sha256')
|
|
.update(domain)
|
|
.update(JSON.stringify(value))
|
|
.digest('hex');
|
|
}
|
|
|
|
function snapshot() {
|
|
const generation = createPluginPackageResourceGenerationFromReferences({
|
|
installationId: 'install-crash-tool-result',
|
|
projectId: PROJECT_ID,
|
|
packageName: 'crash',
|
|
lockDigest: 'a'.repeat(64),
|
|
generation: 1,
|
|
previousActiveLockDigest: null,
|
|
contentDigest: 'b'.repeat(64),
|
|
resources: [],
|
|
});
|
|
return createProjectToolDefinitionSnapshot({
|
|
projectId: PROJECT_ID,
|
|
contributions: [
|
|
{
|
|
generation,
|
|
revisionDigest: 'c'.repeat(64),
|
|
definitions: [
|
|
{
|
|
...TOOL,
|
|
description: 'Read one SQLite crash matrix fixture',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
runId: { type: 'string', minLength: 1, maxLength: 64 },
|
|
},
|
|
required: ['runId'],
|
|
additionalProperties: false,
|
|
},
|
|
outputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
summary: { type: 'string', maxLength: 1024 },
|
|
},
|
|
required: ['summary'],
|
|
additionalProperties: false,
|
|
},
|
|
effect: 'read',
|
|
risk: 'low',
|
|
requiredPermissions: ['run.read'],
|
|
timeoutSeconds: 30,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
function principal() {
|
|
return Object.freeze({
|
|
subject: SUBJECT,
|
|
authenticationId: 'auth-crash-tool-result',
|
|
authenticatedAtMs: 800,
|
|
expiresAtMs: 10_000,
|
|
assurance: 'local_console',
|
|
});
|
|
}
|
|
|
|
function authorizer() {
|
|
return Object.freeze({
|
|
async authorize() {
|
|
return Object.freeze({
|
|
effect: 'allow',
|
|
reasons: Object.freeze(['role_grant']),
|
|
fence: POLICY_FENCE,
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
function bindingRegistry(definitionSnapshot) {
|
|
const binding = createTrustedToolHandlerBinding(definitionSnapshot, {
|
|
tool: TOOL,
|
|
adapter: {
|
|
id: 'builtin.crash-result-read',
|
|
version: '1.0.0',
|
|
},
|
|
executionClass: 'builtin_in_process',
|
|
profiles: ['edge', 'standalone'],
|
|
authorities: ['database.read'],
|
|
timeoutSeconds: 20,
|
|
redactionContract: {
|
|
id: 'redaction.crash-result-read',
|
|
version: '1.0.0',
|
|
},
|
|
auditContract: {
|
|
id: 'audit.tool-call',
|
|
version: '1.0.0',
|
|
},
|
|
});
|
|
return Object.freeze({
|
|
binding,
|
|
bindings: new TrustedToolHandlerBindingRegistry(definitionSnapshot, [
|
|
binding,
|
|
]),
|
|
});
|
|
}
|
|
|
|
function openClient(databasePath, profile) {
|
|
return openLocalSqliteClient(
|
|
{
|
|
databasePath,
|
|
profile,
|
|
busyTimeoutMs: 5_000,
|
|
},
|
|
false,
|
|
);
|
|
}
|
|
|
|
async function prepareCompletionFixture(client, authority, profile) {
|
|
client.exec(`
|
|
INSERT INTO "QingLong3Projects" (
|
|
id, name, slug, status, version, created_at_ms, updated_at_ms
|
|
) VALUES (
|
|
'${PROJECT_ID}', 'Crash Tool Result', 'crash-tool-result',
|
|
'active', 1, 1, 1
|
|
);
|
|
INSERT INTO "Runs" (
|
|
id, project_id, task_id, task_revision, trigger_type,
|
|
execution_origin, execution_owner, status, version,
|
|
event_sequence, priority, created_at_ms
|
|
) VALUES (
|
|
'${RUN_ID}', '${PROJECT_ID}', 'crash-tool-result-task', 'v1',
|
|
'manual', 'manual', 'runtime', 'running', 0, 0, 0, 1
|
|
);
|
|
`);
|
|
const definitionSnapshot = snapshot();
|
|
const { binding, bindings } = bindingRegistry(definitionSnapshot);
|
|
const stepRuns = new LocalSqliteStepRunRepository(authority);
|
|
const creation = createStepRunMutation(
|
|
{
|
|
id: STEP_RUN_ID,
|
|
runId: RUN_ID,
|
|
stepKey: 'workflow.crash-result-read',
|
|
kind: 'tool',
|
|
definitionRef: `tool:${TOOL.name}@${TOOL.version}`,
|
|
definitionDigest: definitionSnapshot.definitions[0].definitionDigest,
|
|
required: true,
|
|
initialStatus: 'ready',
|
|
inputRef: 'artifact:crash-tool-result-input',
|
|
mutationId: 'crash-tool-result-create',
|
|
createdAtMs: 1_000,
|
|
},
|
|
{
|
|
expectedRunVersion: 0,
|
|
expectedRunEventSequence: 0,
|
|
eventId: '51000000-0000-4000-8000-000000000001',
|
|
dedupeKey: 'crash-tool-result:create',
|
|
actor: SUBJECT,
|
|
},
|
|
);
|
|
assert.equal((await stepRuns.apply(creation)).status, 'applied');
|
|
|
|
const invocation = await prepareToolInvocation(
|
|
projectToolDefinitionRegistry(definitionSnapshot),
|
|
{
|
|
projectId: PROJECT_ID,
|
|
principal: principal(),
|
|
nowMs: 900,
|
|
tool: TOOL,
|
|
input: { runId: RUN_ID },
|
|
},
|
|
authorizer(),
|
|
);
|
|
const planBundle = createTrustedToolInvocationPlan(bindings, invocation, {
|
|
actionRef: `tool-plan:${RUN_ID}`,
|
|
inputArtifactId: 'crash-tool-result-input-artifact',
|
|
previewArtifactId: 'crash-tool-result-preview-artifact',
|
|
artifactKeyId: 'crash-tool-invocation-key',
|
|
artifactKey: INVOCATION_KEY,
|
|
artifactNonce: Buffer.alloc(12, 44),
|
|
profile,
|
|
preview: {
|
|
title: 'SQLite Crash Tool Result',
|
|
summary: 'Creates one encrypted crash-matrix completion',
|
|
fields: [
|
|
{
|
|
kind: 'identifier',
|
|
label: 'Run',
|
|
value: RUN_ID,
|
|
},
|
|
],
|
|
warnings: [],
|
|
},
|
|
sealedAtMs: 1_100,
|
|
});
|
|
assert.deepEqual(
|
|
await new LocalSqliteToolInvocationArtifactRepository(authority).put(
|
|
planBundle.inputArtifact,
|
|
planBundle.previewArtifact,
|
|
),
|
|
{ status: 'inserted' },
|
|
);
|
|
const startedAtMs = 1_200;
|
|
const evidence = createToolExecutionEvidenceBundle({
|
|
traceId: '1'.repeat(32),
|
|
spanId: '2'.repeat(16),
|
|
projectId: PROJECT_ID,
|
|
runId: RUN_ID,
|
|
stepRunId: STEP_RUN_ID,
|
|
invocationPlanDigest: planBundle.plan.planDigest,
|
|
bindingDigest: binding.bindingDigest,
|
|
adapterDigest: trustedToolContractIdentityDigest(binding.adapter),
|
|
redactionContractDigest: trustedToolContractIdentityDigest(
|
|
binding.redactionContract,
|
|
),
|
|
auditContractDigest: trustedToolContractIdentityDigest(
|
|
binding.auditContract,
|
|
),
|
|
audit: {
|
|
eventId: '41000000-0000-4000-8000-000000000001',
|
|
requestId: 'crash-tool-result-request',
|
|
operationId: TOOL_EXECUTION_START_AUDIT_OPERATION,
|
|
projectId: PROJECT_ID,
|
|
subject: SUBJECT,
|
|
authenticationId: 'auth-crash-tool-result',
|
|
outcome: 'allowed',
|
|
reasons: ['tool_execution_start'],
|
|
fence: POLICY_FENCE,
|
|
occurredAtMs: startedAtMs,
|
|
},
|
|
createdAtMs: startedAtMs,
|
|
});
|
|
const admission = await admitTrustedToolExecution(
|
|
bindings,
|
|
planBundle.plan,
|
|
{
|
|
principal: principal(),
|
|
profile,
|
|
nowMs: startedAtMs,
|
|
authorizer: authorizer(),
|
|
evidence: {
|
|
stepRun: {
|
|
id: creation.stepRun.id,
|
|
version: creation.stepRun.version,
|
|
digest: creation.stepRun.stepRunDigest,
|
|
},
|
|
...toolExecutionAdmissionEvidence(evidence),
|
|
},
|
|
},
|
|
);
|
|
const runningMutation = transitionStepRunMutation(
|
|
creation.stepRun,
|
|
{
|
|
expectedVersion: creation.stepRun.version,
|
|
expectedDigest: creation.stepRun.stepRunDigest,
|
|
mutationId: 'crash-tool-result-running',
|
|
to: 'running',
|
|
atMs: startedAtMs,
|
|
},
|
|
{
|
|
expectedRunVersion: 1,
|
|
expectedRunEventSequence: 1,
|
|
eventId: '51000000-0000-4000-8000-000000000002',
|
|
dedupeKey: 'crash-tool-result:running',
|
|
actor: SUBJECT,
|
|
},
|
|
);
|
|
const start = await new LocalSqliteToolExecutionStartBarrierRepository(
|
|
authority,
|
|
).prepare(
|
|
createToolExecutionStartCommand({
|
|
startId: START_ID,
|
|
admission,
|
|
evidence,
|
|
stepRunMutation: runningMutation,
|
|
}),
|
|
);
|
|
assert.equal(start.status, 'created');
|
|
|
|
const catalogRepository =
|
|
new LocalSqliteToolResultKeyCatalogRepository(authority);
|
|
const catalog = await catalogRepository.append(
|
|
createToolResultKeyCatalogBootstrapCommand({
|
|
keyId: RESULT_KEY_A_ID,
|
|
materialProof: toolResultKeyMaterialProof(
|
|
RESULT_KEY_A_ID,
|
|
RESULT_KEY_A,
|
|
),
|
|
mutationId: 'crash-result-key-bootstrap-a',
|
|
}),
|
|
);
|
|
const executionResultUnsigned = Object.freeze({
|
|
schema: TRUSTED_TOOL_EXECUTION_RESULT_SCHEMA,
|
|
startId: START_ID,
|
|
barrierDigest: start.barrier.barrierDigest,
|
|
adapterDigest: start.barrier.adapterDigest,
|
|
output: OUTPUT,
|
|
outputDigest: hash(OUTPUT_DIGEST_DOMAIN, OUTPUT),
|
|
completedAtMs: 1_300,
|
|
});
|
|
const executionResult = Object.freeze({
|
|
...executionResultUnsigned,
|
|
resultDigest: hash(RESULT_DIGEST_DOMAIN, executionResultUnsigned),
|
|
});
|
|
const resultArtifact = createToolExecutionResultArtifact(
|
|
{
|
|
artifactId: 'crash-tool-result-output-artifact',
|
|
projectId: PROJECT_ID,
|
|
runId: RUN_ID,
|
|
stepRunId: STEP_RUN_ID,
|
|
tool: TOOL,
|
|
executionResult,
|
|
keyId: RESULT_KEY_A_ID,
|
|
key: RESULT_KEY_A,
|
|
},
|
|
projectToolDefinitionRegistry(definitionSnapshot),
|
|
() => Buffer.alloc(12, 45),
|
|
);
|
|
const running = await stepRuns.findById(STEP_RUN_ID);
|
|
assert.ok(running);
|
|
const succeededMutation = transitionStepRunMutation(
|
|
running,
|
|
{
|
|
expectedVersion: running.version,
|
|
expectedDigest: running.stepRunDigest,
|
|
mutationId: 'crash-tool-result-succeeded',
|
|
to: 'succeeded',
|
|
atMs: executionResult.completedAtMs,
|
|
outputRef: resultArtifact.artifactId,
|
|
},
|
|
{
|
|
expectedRunVersion: 2,
|
|
expectedRunEventSequence: 2,
|
|
eventId: '51000000-0000-4000-8000-000000000003',
|
|
dedupeKey: 'crash-tool-result:succeeded',
|
|
actor: SUBJECT,
|
|
},
|
|
);
|
|
return Object.freeze({
|
|
catalogRepository,
|
|
definitionSnapshot,
|
|
completionCommand: createToolExecutionCompletionCommand({
|
|
barrier: start.barrier,
|
|
executionResult,
|
|
resultArtifact,
|
|
resultKeyCatalogFence: toolResultKeyCatalogFence(
|
|
catalog.catalog,
|
|
requireActiveToolResultKey(catalog.catalog),
|
|
),
|
|
stepRunMutation: succeededMutation,
|
|
}),
|
|
});
|
|
}
|
|
|
|
async function setupScenario({
|
|
databasePath,
|
|
statePath,
|
|
profile,
|
|
operation,
|
|
}) {
|
|
const client = openClient(databasePath, profile);
|
|
await migrateLocalSqliteDatabase(client);
|
|
const authority = new LocalSqliteOperationAuthority(client);
|
|
try {
|
|
const prepared = await prepareCompletionFixture(
|
|
client,
|
|
authority,
|
|
profile,
|
|
);
|
|
const state = {
|
|
profile,
|
|
operation,
|
|
completionCommand: prepared.completionCommand,
|
|
};
|
|
if (operation !== 'completion') {
|
|
const completed =
|
|
await new LocalSqliteToolExecutionCompletionRepository(
|
|
authority,
|
|
).commit(prepared.completionCommand);
|
|
assert.equal(completed.status, 'created');
|
|
const rotated = await prepared.catalogRepository.append(
|
|
createToolResultKeyRotationCommand(
|
|
await prepared.catalogRepository.findCurrent(),
|
|
{
|
|
keyId: RESULT_KEY_B_ID,
|
|
materialProof: toolResultKeyMaterialProof(
|
|
RESULT_KEY_B_ID,
|
|
RESULT_KEY_B,
|
|
),
|
|
mutationId: 'crash-result-key-rotate-b',
|
|
},
|
|
),
|
|
);
|
|
const rekeyCommand = createToolExecutionResultRekeyCommand({
|
|
artifact: prepared.completionCommand.resultArtifact,
|
|
binding: toolExecutionResultKeyBinding(
|
|
prepared.completionCommand,
|
|
),
|
|
previousOverlay: null,
|
|
overlayId: 'crash-tool-result-rekey-overlay',
|
|
mutationId: 'crash-tool-result-rekey',
|
|
targetCatalogFence: toolResultKeyCatalogFence(
|
|
rotated.catalog,
|
|
requireActiveToolResultKey(rotated.catalog),
|
|
),
|
|
targetKey: RESULT_KEY_B,
|
|
output: OUTPUT,
|
|
rekeyedAtMs: 1_400,
|
|
registry: projectToolDefinitionRegistry(
|
|
prepared.definitionSnapshot,
|
|
),
|
|
nonceFactory: () => Buffer.alloc(12, 46),
|
|
});
|
|
state.rekeyCommand = rekeyCommand;
|
|
state.receiptCommand =
|
|
createToolResultKeyRetirementReceiptCommand({
|
|
expectedCatalogGeneration: rotated.catalog.generation,
|
|
expectedCatalogDigest: rotated.catalog.catalogDigest,
|
|
keyId: RESULT_KEY_A_ID,
|
|
mutationId: 'crash-tool-result-retirement-receipt',
|
|
});
|
|
if (operation !== 'rekey') {
|
|
const rekeyed =
|
|
await new LocalSqliteToolResultRekeyRepository(
|
|
authority,
|
|
).append(rekeyCommand);
|
|
assert.equal(rekeyed.status, 'created');
|
|
}
|
|
if (operation === 'retire') {
|
|
const receipt =
|
|
await new LocalSqliteToolResultRekeyRepository(
|
|
authority,
|
|
).create(state.receiptCommand);
|
|
assert.equal(receipt.status, 'created');
|
|
state.retireCommand = createToolResultKeyRetirementCommand(
|
|
rotated.catalog,
|
|
{
|
|
keyId: RESULT_KEY_A_ID,
|
|
retirementReceiptDigest: receipt.receipt.receiptDigest,
|
|
mutationId: 'crash-result-key-retire-a',
|
|
},
|
|
);
|
|
}
|
|
}
|
|
fs.writeFileSync(statePath, JSON.stringify(state), {
|
|
encoding: 'utf8',
|
|
flag: 'wx',
|
|
mode: 0o600,
|
|
});
|
|
} finally {
|
|
await authority.close();
|
|
}
|
|
}
|
|
|
|
function writeCrashMarker(markerPath, pointName) {
|
|
const file = fs.openSync(markerPath, 'wx', 0o600);
|
|
try {
|
|
fs.writeSync(
|
|
file,
|
|
JSON.stringify({
|
|
schema: 'qinglong/sqlite-tool-result-crash-marker@v1',
|
|
point: pointName,
|
|
pid: process.pid,
|
|
}),
|
|
);
|
|
fs.fsyncSync(file);
|
|
} finally {
|
|
fs.closeSync(file);
|
|
}
|
|
}
|
|
|
|
function crashClient(client, pointName, markerPath) {
|
|
const point = CRASH_POINTS[pointName];
|
|
if (!point) throw new Error(`unknown crash point ${pointName}`);
|
|
let triggered = false;
|
|
const crash = () => {
|
|
if (triggered) return;
|
|
triggered = true;
|
|
writeCrashMarker(markerPath, pointName);
|
|
process.kill(process.pid, 'SIGKILL');
|
|
throw new Error(`SIGKILL did not terminate ${pointName}`);
|
|
};
|
|
const matches = (timing, sql) =>
|
|
!triggered &&
|
|
point.timing === timing &&
|
|
sql.trim().includes(point.sql);
|
|
return new Proxy(client, {
|
|
get(target, property) {
|
|
if (property === 'exec') {
|
|
return (sql) => {
|
|
if (matches('beforeExec', sql)) crash();
|
|
const result = target.exec(sql);
|
|
if (matches('afterExec', sql)) crash();
|
|
return result;
|
|
};
|
|
}
|
|
if (property === 'prepare') {
|
|
return (sql) => {
|
|
const statement = target.prepare(sql);
|
|
return new Proxy(statement, {
|
|
get(statementTarget, statementProperty) {
|
|
const value = Reflect.get(
|
|
statementTarget,
|
|
statementProperty,
|
|
statementTarget,
|
|
);
|
|
if (statementProperty === 'run') {
|
|
return (...values) => {
|
|
const result = value.apply(statementTarget, values);
|
|
if (matches('afterRun', sql)) crash();
|
|
return result;
|
|
};
|
|
}
|
|
return typeof value === 'function'
|
|
? value.bind(statementTarget)
|
|
: value;
|
|
},
|
|
});
|
|
};
|
|
}
|
|
const value = Reflect.get(target, property, target);
|
|
return typeof value === 'function' ? value.bind(target) : value;
|
|
},
|
|
});
|
|
}
|
|
|
|
async function runCrashScenario({
|
|
databasePath,
|
|
statePath,
|
|
markerPath,
|
|
pointName,
|
|
}) {
|
|
const point = CRASH_POINTS[pointName];
|
|
if (!point) throw new Error(`unknown crash point ${pointName}`);
|
|
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
if (state.operation !== point.operation) {
|
|
throw new Error(
|
|
`crash point ${pointName} does not match ${state.operation}`,
|
|
);
|
|
}
|
|
const client = openClient(databasePath, state.profile);
|
|
const authority = new LocalSqliteOperationAuthority(
|
|
crashClient(client, pointName, markerPath),
|
|
);
|
|
if (point.operation === 'completion') {
|
|
await new LocalSqliteToolExecutionCompletionRepository(
|
|
authority,
|
|
).commit(state.completionCommand);
|
|
} else if (point.operation === 'rekey') {
|
|
await new LocalSqliteToolResultRekeyRepository(authority).append(
|
|
state.rekeyCommand,
|
|
);
|
|
} else if (point.operation === 'receipt') {
|
|
await new LocalSqliteToolResultRekeyRepository(authority).create(
|
|
state.receiptCommand,
|
|
);
|
|
} else {
|
|
await new LocalSqliteToolResultKeyCatalogRepository(authority).append(
|
|
state.retireCommand,
|
|
);
|
|
}
|
|
throw new Error(`crash point ${pointName} was not reached`);
|
|
}
|
|
|
|
function completionFacts(client) {
|
|
return {
|
|
...client
|
|
.prepare(
|
|
`SELECT
|
|
step.status AS "stepStatus",
|
|
step.version AS "stepVersion",
|
|
run.version AS "runVersion",
|
|
run.event_sequence AS "runEventSequence",
|
|
(SELECT count(*) FROM "ToolExecutionCompletions"
|
|
WHERE start_id = ?) AS "completionCount",
|
|
(SELECT count(*) FROM "ToolExecutionResultKeyBindings"
|
|
WHERE start_id = ?) AS "bindingCount",
|
|
(SELECT count(*) FROM "StepRunMutations"
|
|
WHERE mutation_id = 'crash-tool-result-succeeded')
|
|
AS "completionMutationCount",
|
|
(SELECT count(*) FROM "RunEvents"
|
|
WHERE id = '51000000-0000-4000-8000-000000000003')
|
|
AS "completionEventCount"
|
|
FROM "StepRuns" AS step
|
|
JOIN "Runs" AS run ON run.id = step.run_id
|
|
WHERE step.id = ? AND run.id = ?`,
|
|
)
|
|
.get(START_ID, START_ID, STEP_RUN_ID, RUN_ID),
|
|
};
|
|
}
|
|
|
|
function rekeyFacts(client) {
|
|
return {
|
|
...client
|
|
.prepare(
|
|
`SELECT
|
|
(SELECT count(*) FROM "ToolExecutionResultRekeyOverlays"
|
|
WHERE overlay_id = 'crash-tool-result-rekey-overlay')
|
|
AS "overlayCount",
|
|
(SELECT count(*) FROM "ToolExecutionResultRekeyHeads"
|
|
WHERE artifact_id = 'crash-tool-result-output-artifact')
|
|
AS "headCount"`,
|
|
)
|
|
.get(),
|
|
};
|
|
}
|
|
|
|
function receiptCount(client) {
|
|
return client
|
|
.prepare(
|
|
`SELECT count(*) AS count
|
|
FROM "ToolResultKeyRetirementReceipts"
|
|
WHERE mutation_id = 'crash-tool-result-retirement-receipt'`,
|
|
)
|
|
.get().count;
|
|
}
|
|
|
|
async function verifyScenario({
|
|
databasePath,
|
|
statePath,
|
|
pointName,
|
|
}) {
|
|
const point = CRASH_POINTS[pointName];
|
|
if (!point) throw new Error(`unknown crash point ${pointName}`);
|
|
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
const client = openClient(databasePath, state.profile);
|
|
const authority = new LocalSqliteOperationAuthority(client);
|
|
try {
|
|
assert.equal(
|
|
client.prepare('PRAGMA integrity_check').get().integrity_check,
|
|
'ok',
|
|
);
|
|
assert.equal(
|
|
client.prepare('PRAGMA journal_mode').get().journal_mode,
|
|
state.profile === 'edge' ? 'delete' : 'wal',
|
|
);
|
|
let replayStatus;
|
|
if (point.operation === 'completion') {
|
|
assert.deepEqual(
|
|
completionFacts(client),
|
|
point.durable
|
|
? {
|
|
stepStatus: 'succeeded',
|
|
stepVersion: 3,
|
|
runVersion: 3,
|
|
runEventSequence: 3,
|
|
completionCount: 1,
|
|
bindingCount: 1,
|
|
completionMutationCount: 1,
|
|
completionEventCount: 1,
|
|
}
|
|
: {
|
|
stepStatus: 'running',
|
|
stepVersion: 2,
|
|
runVersion: 2,
|
|
runEventSequence: 2,
|
|
completionCount: 0,
|
|
bindingCount: 0,
|
|
completionMutationCount: 0,
|
|
completionEventCount: 0,
|
|
},
|
|
);
|
|
const repository =
|
|
new LocalSqliteToolExecutionCompletionRepository(authority);
|
|
replayStatus = (
|
|
await repository.commit(state.completionCommand)
|
|
).status;
|
|
assert.equal(
|
|
(
|
|
await repository.commit(state.completionCommand)
|
|
).status,
|
|
'existing',
|
|
);
|
|
assert.deepEqual(completionFacts(client), {
|
|
stepStatus: 'succeeded',
|
|
stepVersion: 3,
|
|
runVersion: 3,
|
|
runEventSequence: 3,
|
|
completionCount: 1,
|
|
bindingCount: 1,
|
|
completionMutationCount: 1,
|
|
completionEventCount: 1,
|
|
});
|
|
} else if (point.operation === 'rekey') {
|
|
assert.deepEqual(
|
|
rekeyFacts(client),
|
|
point.durable
|
|
? { overlayCount: 1, headCount: 1 }
|
|
: { overlayCount: 0, headCount: 0 },
|
|
);
|
|
const repository =
|
|
new LocalSqliteToolResultRekeyRepository(authority);
|
|
replayStatus = (
|
|
await repository.append(state.rekeyCommand)
|
|
).status;
|
|
assert.equal(
|
|
(await repository.append(state.rekeyCommand)).status,
|
|
'existing',
|
|
);
|
|
assert.deepEqual(rekeyFacts(client), {
|
|
overlayCount: 1,
|
|
headCount: 1,
|
|
});
|
|
} else if (point.operation === 'receipt') {
|
|
assert.equal(receiptCount(client), point.durable ? 1 : 0);
|
|
const repository =
|
|
new LocalSqliteToolResultRekeyRepository(authority);
|
|
replayStatus = (
|
|
await repository.create(state.receiptCommand)
|
|
).status;
|
|
assert.equal(
|
|
(await repository.create(state.receiptCommand)).status,
|
|
'existing',
|
|
);
|
|
assert.equal(receiptCount(client), 1);
|
|
} else {
|
|
const catalogRepository =
|
|
new LocalSqliteToolResultKeyCatalogRepository(authority);
|
|
const before = await catalogRepository.findCurrent();
|
|
assert.ok(before);
|
|
assert.equal(before.generation, point.durable ? 3 : 2);
|
|
assert.equal(
|
|
before.keys.find((entry) => entry.keyId === RESULT_KEY_A_ID)
|
|
.state,
|
|
point.durable ? 'retired' : 'decrypt_only',
|
|
);
|
|
replayStatus = (
|
|
await catalogRepository.append(state.retireCommand)
|
|
).status;
|
|
assert.equal(
|
|
(
|
|
await catalogRepository.append(state.retireCommand)
|
|
).status,
|
|
'existing',
|
|
);
|
|
const after = await catalogRepository.findCurrent();
|
|
assert.ok(after);
|
|
assert.equal(after.generation, 3);
|
|
assert.equal(
|
|
after.keys.find((entry) => entry.keyId === RESULT_KEY_A_ID)
|
|
.state,
|
|
'retired',
|
|
);
|
|
}
|
|
assert.equal(replayStatus, point.durable ? 'existing' : 'created');
|
|
return Object.freeze({
|
|
point: pointName,
|
|
operation: point.operation,
|
|
crashBeforeCommit: !point.durable,
|
|
durableAfterCrash: point.durable,
|
|
replayStatus,
|
|
integrityCheck: 'ok',
|
|
journalMode: state.profile === 'edge' ? 'delete' : 'wal',
|
|
});
|
|
} finally {
|
|
await authority.close();
|
|
}
|
|
}
|
|
|
|
if (require.main === module) {
|
|
const [, , action, databasePath, statePath, markerPath, pointName] =
|
|
process.argv;
|
|
if (action !== 'crash') {
|
|
throw new Error('fixture action must be crash');
|
|
}
|
|
runCrashScenario({
|
|
databasePath,
|
|
statePath,
|
|
markerPath,
|
|
pointName,
|
|
}).catch((error) => {
|
|
process.stderr.write(
|
|
`${error instanceof Error ? error.stack : String(error)}\n`,
|
|
);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
CRASH_POINTS,
|
|
setupScenario,
|
|
verifyScenario,
|
|
};
|