mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:25:29 +08:00
268 lines
8.1 KiB
JavaScript
268 lines
8.1 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { DatabaseSync } = require('node:sqlite');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
ToolExecutionEvidenceConflictError,
|
|
ToolExecutionEvidenceUnavailableError,
|
|
createToolExecutionEvidenceBundle,
|
|
} = require('@qinglong/runtime-core/tool-execution-evidence');
|
|
const {
|
|
createStepRunMutation,
|
|
} = require('@qinglong/runtime-core/step-run');
|
|
const {
|
|
LocalSqliteOperationAuthority,
|
|
} = require('../dist/authority/operationAuthority');
|
|
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
|
|
const {
|
|
LocalSqliteStepRunRepository,
|
|
} = require('../dist/run/stepRunRepository');
|
|
const {
|
|
LocalSqliteToolExecutionEvidenceRepository,
|
|
} = require('../dist/tool-execution/toolExecutionEvidenceRepository');
|
|
|
|
const DIGEST_A = 'a'.repeat(64);
|
|
const DIGEST_B = 'b'.repeat(64);
|
|
const DIGEST_C = 'c'.repeat(64);
|
|
const DIGEST_D = 'd'.repeat(64);
|
|
const DIGEST_E = 'e'.repeat(64);
|
|
|
|
async function harness() {
|
|
const client = new DatabaseSync(':memory:');
|
|
client.exec('PRAGMA foreign_keys = ON');
|
|
await migrateLocalSqliteDatabase(client);
|
|
client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3Projects" (
|
|
id, name, slug, status, version, created_at_ms, updated_at_ms
|
|
) VALUES ('project-001', 'Project', 'project-001', 'active', 1, 1, 1)`,
|
|
)
|
|
.run();
|
|
client
|
|
.prepare(
|
|
`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-001', 'project-001', 'task-001', 'revision-001', 'manual',
|
|
'manual', 'runtime', 'running', 0, 0, 0, 1
|
|
)`,
|
|
)
|
|
.run();
|
|
const authority = new LocalSqliteOperationAuthority(client);
|
|
return {
|
|
client,
|
|
authority,
|
|
stepRuns: new LocalSqliteStepRunRepository(authority),
|
|
evidence: new LocalSqliteToolExecutionEvidenceRepository(authority),
|
|
close: () => authority.close(),
|
|
};
|
|
}
|
|
|
|
function stepMutation(index, expectedRunVersion) {
|
|
const suffix = String(index).padStart(3, '0');
|
|
return createStepRunMutation(
|
|
{
|
|
id: `step-run-${suffix}`,
|
|
runId: 'run-001',
|
|
stepKey: `workflow.tool-${suffix}`,
|
|
kind: 'tool',
|
|
definitionRef: `tool:demo.tool-${suffix}@1.0.0`,
|
|
definitionDigest: DIGEST_A,
|
|
required: true,
|
|
initialStatus: 'ready',
|
|
mutationId: `step-create-${suffix}`,
|
|
createdAtMs: 900 + index,
|
|
},
|
|
{
|
|
expectedRunVersion,
|
|
expectedRunEventSequence: expectedRunVersion,
|
|
eventId: `50000000-0000-4000-8000-${suffix.padStart(12, '0')}`,
|
|
dedupeKey: `step-create:step-run-${suffix}`,
|
|
actor: { type: 'agent', id: 'agent-001' },
|
|
},
|
|
);
|
|
}
|
|
|
|
function evidence(index, overrides = {}) {
|
|
const suffix = String(index).padStart(3, '0');
|
|
const createdAtMs = 1_000 + index;
|
|
return createToolExecutionEvidenceBundle({
|
|
traceId: index.toString(16).padStart(32, '0'),
|
|
spanId: (index + 16).toString(16).padStart(16, '0'),
|
|
projectId: 'project-001',
|
|
runId: 'run-001',
|
|
stepRunId: `step-run-${suffix}`,
|
|
invocationPlanDigest: DIGEST_A,
|
|
bindingDigest: DIGEST_B,
|
|
adapterDigest: DIGEST_C,
|
|
redactionContractDigest: DIGEST_D,
|
|
auditContractDigest: DIGEST_E,
|
|
audit: {
|
|
eventId: `60000000-0000-4000-8000-${suffix.padStart(12, '0')}`,
|
|
requestId: `tool-request-${suffix}`,
|
|
operationId: 'tool.invoke.start',
|
|
projectId: 'project-001',
|
|
subject: { type: 'agent', id: 'agent-001' },
|
|
authenticationId: 'auth-agent-001',
|
|
outcome: 'allowed',
|
|
reasons: ['tool_execution_start'],
|
|
fence: { projectVersion: 1, bindingVersion: 1 },
|
|
occurredAtMs: createdAtMs,
|
|
},
|
|
createdAtMs,
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
test('atomically prepares and exactly replays durable Trace and Audit evidence', async (t) => {
|
|
const value = await harness();
|
|
t.after(() => value.close());
|
|
await value.stepRuns.apply(stepMutation(1, 0));
|
|
const bundle = evidence(1);
|
|
|
|
assert.deepEqual(await value.evidence.prepare(bundle), {
|
|
status: 'created',
|
|
bundle,
|
|
});
|
|
assert.deepEqual(await value.evidence.prepare(bundle), {
|
|
status: 'existing',
|
|
bundle,
|
|
});
|
|
assert.deepEqual(
|
|
await value.evidence.findByTrace(
|
|
bundle.trace.traceId,
|
|
bundle.trace.spanId,
|
|
),
|
|
bundle,
|
|
);
|
|
assert.deepEqual(
|
|
await value.evidence.findByAuditEventId(bundle.audit.eventId),
|
|
bundle,
|
|
);
|
|
assert.deepEqual(
|
|
{
|
|
...value.client
|
|
.prepare(
|
|
`SELECT
|
|
(SELECT COUNT(*) FROM "ToolExecutionTraceAnchors") AS traces,
|
|
(SELECT COUNT(*) FROM "ToolExecutionAuditReceipts") AS receipts,
|
|
(SELECT COUNT(*) FROM "QingLong3SecurityAuditEvents"
|
|
WHERE operation_id = 'tool.invoke.start') AS audits`,
|
|
)
|
|
.get(),
|
|
},
|
|
{ traces: 1, receipts: 1, audits: 1 },
|
|
);
|
|
});
|
|
|
|
test('rejects reused Trace or Audit identity with different content', async (t) => {
|
|
const value = await harness();
|
|
t.after(() => value.close());
|
|
await value.stepRuns.apply(stepMutation(1, 0));
|
|
const first = evidence(1);
|
|
await value.evidence.prepare(first);
|
|
|
|
const reusedTrace = evidence(1, {
|
|
audit: {
|
|
...first.audit,
|
|
eventId: '60000000-0000-4000-8000-000000000099',
|
|
requestId: 'tool-request-reused',
|
|
},
|
|
});
|
|
await assert.rejects(
|
|
value.evidence.prepare(reusedTrace),
|
|
ToolExecutionEvidenceConflictError,
|
|
);
|
|
|
|
const reusedAudit = evidence(1, {
|
|
traceId: 'f'.repeat(32),
|
|
spanId: 'e'.repeat(16),
|
|
audit: first.audit,
|
|
});
|
|
await assert.rejects(
|
|
value.evidence.prepare(reusedAudit),
|
|
ToolExecutionEvidenceConflictError,
|
|
);
|
|
});
|
|
|
|
test('requires one ready Tool StepRun in the same Project and rolls back audit', async (t) => {
|
|
const value = await harness();
|
|
t.after(() => value.close());
|
|
|
|
await assert.rejects(
|
|
value.evidence.prepare(evidence(1)),
|
|
ToolExecutionEvidenceConflictError,
|
|
);
|
|
assert.equal(
|
|
value.client
|
|
.prepare(
|
|
`SELECT COUNT(*) AS count
|
|
FROM "QingLong3SecurityAuditEvents"
|
|
WHERE operation_id = 'tool.invoke.start'`,
|
|
)
|
|
.get().count,
|
|
0,
|
|
);
|
|
});
|
|
|
|
test('lists evidence with stable bounded keyset pagination', async (t) => {
|
|
const value = await harness();
|
|
t.after(() => value.close());
|
|
for (let index = 1; index <= 3; index += 1) {
|
|
await value.stepRuns.apply(stepMutation(index, index - 1));
|
|
await value.evidence.prepare(evidence(index));
|
|
}
|
|
const first = await value.evidence.listByRun({
|
|
runId: 'run-001',
|
|
limit: 2,
|
|
});
|
|
assert.equal(first.bundles.length, 2);
|
|
assert.equal(first.truncated, true);
|
|
assert.deepEqual(first.next, {
|
|
createdAtMs: first.bundles[1].trace.createdAtMs,
|
|
traceId: first.bundles[1].trace.traceId,
|
|
spanId: first.bundles[1].trace.spanId,
|
|
});
|
|
const second = await value.evidence.listByRun({
|
|
runId: 'run-001',
|
|
limit: 2,
|
|
after: first.next,
|
|
});
|
|
assert.equal(second.bundles.length, 1);
|
|
assert.equal(second.truncated, false);
|
|
assert.equal(second.next, undefined);
|
|
});
|
|
|
|
test('fails closed when durable JSON no longer matches mirrored columns', async (t) => {
|
|
const value = await harness();
|
|
t.after(() => value.close());
|
|
await value.stepRuns.apply(stepMutation(1, 0));
|
|
const bundle = evidence(1);
|
|
await value.evidence.prepare(bundle);
|
|
value.client.exec('PRAGMA ignore_check_constraints = ON');
|
|
value.client
|
|
.prepare(
|
|
`UPDATE "ToolExecutionTraceAnchors"
|
|
SET trace_json = json_set(trace_json, '$.projectId', 'project-other')`,
|
|
)
|
|
.run();
|
|
await assert.rejects(
|
|
value.evidence.findByTrace(bundle.trace.traceId, bundle.trace.spanId),
|
|
ToolExecutionEvidenceUnavailableError,
|
|
);
|
|
});
|
|
|
|
test('publishes evidence authority only through its explicit subpath', () => {
|
|
const root = require('@qinglong/local-sqlite');
|
|
const runtime = require('@qinglong/local-sqlite/runtime');
|
|
const authority = require('@qinglong/local-sqlite/tool-execution-evidence');
|
|
assert.equal(root.LocalSqliteToolExecutionEvidenceRepository, undefined);
|
|
assert.equal(runtime.LocalSqliteToolExecutionEvidenceRepository, undefined);
|
|
assert.equal(
|
|
typeof authority.LocalSqliteToolExecutionEvidenceRepository,
|
|
'function',
|
|
);
|
|
});
|