mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
312 lines
9.1 KiB
JavaScript
312 lines
9.1 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
const {
|
|
createToolExecutionEvidenceBundle,
|
|
ToolExecutionEvidenceConflictError,
|
|
ToolExecutionEvidenceUnavailableError,
|
|
} = require('@qinglong/runtime-core/tool-execution-evidence');
|
|
const {
|
|
PostgresToolExecutionEvidenceRepository,
|
|
} = require('@qinglong/cluster-postgres/tool-execution-evidence');
|
|
|
|
function evidence(overrides = {}) {
|
|
const createdAtMs = overrides.createdAtMs ?? 1700000000000;
|
|
return createToolExecutionEvidenceBundle({
|
|
traceId: overrides.traceId ?? '1'.repeat(32),
|
|
spanId: overrides.spanId ?? '2'.repeat(16),
|
|
projectId: 'project-1',
|
|
runId: 'run-1',
|
|
stepRunId: 'step-1',
|
|
invocationPlanDigest: '3'.repeat(64),
|
|
bindingDigest: '4'.repeat(64),
|
|
adapterDigest: '5'.repeat(64),
|
|
redactionContractDigest: '6'.repeat(64),
|
|
auditContractDigest: '7'.repeat(64),
|
|
audit: {
|
|
eventId:
|
|
overrides.eventId ?? '12345678-1234-4234-9234-123456789abc',
|
|
requestId: 'request-1',
|
|
operationId: 'tool.invoke.start',
|
|
projectId: 'project-1',
|
|
subject: { type: 'user', id: 'user-1' },
|
|
authenticationId: 'authentication-1',
|
|
outcome: 'allowed',
|
|
reasons: ['policy_allowed'],
|
|
fence: { projectVersion: 4, bindingVersion: 7 },
|
|
occurredAtMs: createdAtMs,
|
|
},
|
|
createdAtMs,
|
|
});
|
|
}
|
|
|
|
function row(bundle, overrides = {}) {
|
|
return {
|
|
traceJson: bundle.trace,
|
|
auditJson: bundle.audit,
|
|
receiptJson: bundle.receipt,
|
|
storedTraceId: bundle.trace.traceId,
|
|
storedSpanId: bundle.trace.spanId,
|
|
storedParentSpanId: bundle.trace.parentSpanId,
|
|
traceProjectId: bundle.trace.projectId,
|
|
traceRunId: bundle.trace.runId,
|
|
traceStepRunId: bundle.trace.stepRunId,
|
|
traceInvocationPlanDigest: bundle.trace.invocationPlanDigest,
|
|
traceBindingDigest: bundle.trace.bindingDigest,
|
|
storedAdapterDigest: bundle.trace.adapterDigest,
|
|
storedRedactionContractDigest: bundle.trace.redactionContractDigest,
|
|
storedAuditContractDigest: bundle.trace.auditContractDigest,
|
|
traceCreatedAtMs: String(bundle.trace.createdAtMs),
|
|
storedTraceDigest: bundle.trace.traceDigest,
|
|
storedEventId: bundle.receipt.eventId,
|
|
receiptProjectId: bundle.receipt.projectId,
|
|
receiptRunId: bundle.receipt.runId,
|
|
receiptStepRunId: bundle.receipt.stepRunId,
|
|
receiptTraceId: bundle.receipt.traceId,
|
|
receiptSpanId: bundle.receipt.spanId,
|
|
receiptTraceDigest: bundle.receipt.traceDigest,
|
|
receiptInvocationPlanDigest: bundle.receipt.invocationPlanDigest,
|
|
receiptBindingDigest: bundle.receipt.bindingDigest,
|
|
storedAuditRecordDigest: bundle.receipt.auditRecordDigest,
|
|
receiptCreatedAtMs: String(bundle.receipt.createdAtMs),
|
|
storedReceiptDigest: bundle.receipt.receiptDigest,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function clientWith(handler) {
|
|
const calls = [];
|
|
let released = false;
|
|
return {
|
|
calls,
|
|
get released() {
|
|
return released;
|
|
},
|
|
async query(text, values = []) {
|
|
calls.push({ text, values });
|
|
if (
|
|
text.startsWith('BEGIN') ||
|
|
text.includes("set_config('") ||
|
|
text === 'COMMIT' ||
|
|
text === 'ROLLBACK'
|
|
) {
|
|
return { rows: [] };
|
|
}
|
|
return handler(text, values, calls);
|
|
},
|
|
release() {
|
|
released = true;
|
|
},
|
|
};
|
|
}
|
|
|
|
test('prepares audit, trace and receipt in one serializable transaction', async () => {
|
|
const bundle = evidence();
|
|
const client = clientWith(async (sql) => {
|
|
if (sql.includes('WHERE receipt.event_id')) return { rows: [] };
|
|
if (sql.includes('FROM "ql3"."step_runs" AS step')) {
|
|
return {
|
|
rows: [{ kind: 'tool', status: 'ready', projectId: 'project-1' }],
|
|
};
|
|
}
|
|
if (sql.startsWith('INSERT INTO')) return { rows: [], rowCount: 1 };
|
|
throw new Error(`unexpected SQL: ${sql}`);
|
|
});
|
|
const pool = {
|
|
async query() {
|
|
throw new Error('pool query is not used by prepare');
|
|
},
|
|
async connect() {
|
|
return client;
|
|
},
|
|
};
|
|
|
|
const repository = new PostgresToolExecutionEvidenceRepository(pool);
|
|
assert.deepEqual(await repository.prepare(bundle), {
|
|
status: 'created',
|
|
bundle,
|
|
});
|
|
assert.equal(client.released, true);
|
|
assert.deepEqual(
|
|
client.calls
|
|
.map(({ text }) => text)
|
|
.filter((sql) => sql.startsWith('INSERT INTO'))
|
|
.map((sql) => sql.match(/"ql3"\."([^"]+)"/)[1]),
|
|
[
|
|
'security_audit_events',
|
|
'tool_execution_trace_anchors',
|
|
'tool_execution_audit_receipts',
|
|
],
|
|
);
|
|
assert.equal(
|
|
client.calls.some(({ text }) => text === 'COMMIT'),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
client.calls.some(({ text }) => text === 'ROLLBACK'),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test('replays exact evidence and rejects identity reuse', async () => {
|
|
const bundle = evidence();
|
|
const stored = row(bundle);
|
|
const exactClient = clientWith(async (sql) => {
|
|
if (sql.includes('WHERE receipt.event_id')) return { rows: [stored] };
|
|
throw new Error(`unexpected SQL: ${sql}`);
|
|
});
|
|
const exact = new PostgresToolExecutionEvidenceRepository({
|
|
async query() {
|
|
throw new Error('unused');
|
|
},
|
|
async connect() {
|
|
return exactClient;
|
|
},
|
|
});
|
|
assert.deepEqual(await exact.prepare(bundle), {
|
|
status: 'existing',
|
|
bundle,
|
|
});
|
|
|
|
const changed = evidence({
|
|
createdAtMs: bundle.trace.createdAtMs + 1,
|
|
traceId: bundle.trace.traceId,
|
|
spanId: bundle.trace.spanId,
|
|
eventId: bundle.audit.eventId,
|
|
});
|
|
const conflictClient = clientWith(async (sql) => {
|
|
if (sql.includes('WHERE receipt.event_id')) return { rows: [stored] };
|
|
throw new Error(`unexpected SQL: ${sql}`);
|
|
});
|
|
const conflict = new PostgresToolExecutionEvidenceRepository({
|
|
async query() {
|
|
throw new Error('unused');
|
|
},
|
|
async connect() {
|
|
return conflictClient;
|
|
},
|
|
});
|
|
await assert.rejects(
|
|
conflict.prepare(changed),
|
|
ToolExecutionEvidenceConflictError,
|
|
);
|
|
assert.equal(
|
|
conflictClient.calls.some(({ text }) => text === 'ROLLBACK'),
|
|
true,
|
|
);
|
|
assert.equal(conflictClient.released, true);
|
|
});
|
|
|
|
test('reads bounded evidence without SELECT authority on the audit table', async () => {
|
|
const first = evidence();
|
|
const second = evidence({
|
|
createdAtMs: first.trace.createdAtMs + 1,
|
|
traceId: '8'.repeat(32),
|
|
spanId: '9'.repeat(16),
|
|
eventId: '87654321-4321-4321-8321-cba987654321',
|
|
});
|
|
const calls = [];
|
|
const repository = new PostgresToolExecutionEvidenceRepository({
|
|
async query(text, values) {
|
|
calls.push({ text, values });
|
|
if (text.includes('WHERE trace.trace_id')) {
|
|
return { rows: [row(first)] };
|
|
}
|
|
if (text.includes('WHERE receipt.event_id')) {
|
|
return { rows: [row(first)] };
|
|
}
|
|
if (text.includes('WHERE trace.run_id')) {
|
|
return { rows: [row(first), row(second)] };
|
|
}
|
|
throw new Error(`unexpected SQL: ${text}`);
|
|
},
|
|
async connect() {
|
|
throw new Error('unused');
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(
|
|
await repository.findByTrace(first.trace.traceId, first.trace.spanId),
|
|
first,
|
|
);
|
|
assert.deepEqual(
|
|
await repository.findByAuditEventId(first.audit.eventId),
|
|
first,
|
|
);
|
|
assert.deepEqual(
|
|
await repository.listByRun({ runId: 'run-1', limit: 1 }),
|
|
{
|
|
bundles: [first],
|
|
truncated: true,
|
|
next: {
|
|
createdAtMs: first.trace.createdAtMs,
|
|
traceId: first.trace.traceId,
|
|
spanId: first.trace.spanId,
|
|
},
|
|
},
|
|
);
|
|
assert.equal(
|
|
calls.some(({ text }) =>
|
|
text.includes('JOIN "ql3"."security_audit_events"'),
|
|
),
|
|
false,
|
|
);
|
|
assert.deepEqual(calls.at(-1).values, ['run-1', null, 0, '', 2]);
|
|
});
|
|
|
|
test('rejects invalid lookups before SQL and fails closed on corrupt rows', async () => {
|
|
let queries = 0;
|
|
const repository = new PostgresToolExecutionEvidenceRepository({
|
|
async query() {
|
|
queries += 1;
|
|
return {
|
|
rows: [row(evidence(), { storedTraceDigest: '0'.repeat(64) })],
|
|
};
|
|
},
|
|
async connect() {
|
|
throw new Error('unused');
|
|
},
|
|
});
|
|
await assert.rejects(repository.findByTrace('bad', 'also-bad'), {
|
|
code: 'TOOL_EXECUTION_EVIDENCE_INVALID',
|
|
});
|
|
assert.equal(queries, 0);
|
|
await assert.rejects(
|
|
repository.findByTrace('1'.repeat(32), '2'.repeat(16)),
|
|
ToolExecutionEvidenceUnavailableError,
|
|
);
|
|
});
|
|
|
|
test('rolls back when the StepRun is not an admitted same-Project Tool step', async () => {
|
|
const bundle = evidence();
|
|
const client = clientWith(async (sql) => {
|
|
if (sql.includes('WHERE receipt.event_id')) return { rows: [] };
|
|
if (sql.includes('FROM "ql3"."step_runs" AS step')) {
|
|
return {
|
|
rows: [{ kind: 'task', status: 'ready', projectId: 'project-1' }],
|
|
};
|
|
}
|
|
throw new Error(`unexpected SQL: ${sql}`);
|
|
});
|
|
const repository = new PostgresToolExecutionEvidenceRepository({
|
|
async query() {
|
|
throw new Error('unused');
|
|
},
|
|
async connect() {
|
|
return client;
|
|
},
|
|
});
|
|
await assert.rejects(
|
|
repository.prepare(bundle),
|
|
ToolExecutionEvidenceConflictError,
|
|
);
|
|
assert.equal(
|
|
client.calls.some(({ text }) => text.startsWith('INSERT INTO')),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
client.calls.some(({ text }) => text === 'ROLLBACK'),
|
|
true,
|
|
);
|
|
assert.equal(client.released, true);
|
|
});
|