feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,266 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const { migrateLocalSqlitePath } = require('../dist/migration/migration');
const {
LocalLegacyAdoptionAuthorizationFenceConflictError,
LocalLegacyAdoptionConflictError,
openLocalSqliteAdoptionDatabase,
} = require('@qinglong/local-sqlite/adoption');
const MUTATION_ID = '12345678-1234-4123-8123-123456789abc';
const DECISION_ID = '019f7200-0000-7000-8000-000000000001';
const SUBJECT = Object.freeze({ type: 'user', id: 'local-owner' });
const DIGEST = 'a'.repeat(64);
function fixture(t) {
const directory = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-adoption-publisher-'),
);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
return path.join(directory, 'qinglong3.sqlite');
}
async function preparedDatabase(t) {
const databasePath = fixture(t);
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const client = new DatabaseSync(databasePath);
client
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings" (
"project_id", "subject_type", "subject_id", "version", "state",
"role", "mutation_id", "changed_by_type", "changed_by_id",
"created_at_ms"
) VALUES ('default', 'user', 'local-owner', 1, 'active', 'owner',
'test-owner-binding', 'user', 'local-owner', 1)`,
)
.run();
client.close();
return databasePath;
}
function candidate(index, taskId = `legacy-cron:${index}`) {
return Object.freeze({
rowOrdinal: index,
sourceDigest: String(index).padStart(64, '0'),
task: Object.freeze({
taskId,
name: `Legacy Task ${index}`,
kind: 'command',
spec: Object.freeze({
schema: 'qinglong/command@v1',
config: Object.freeze({
command: Object.freeze({
kind: 'argv',
file: '/bin/echo',
args: Object.freeze([String(index)]),
}),
}),
}),
labels: Object.freeze({ source: 'legacy-adoption' }),
enabled: true,
}),
triggers: Object.freeze([
Object.freeze({
triggerId: `${taskId}:cron:1`,
spec: Object.freeze({
schema: 'qinglong/cron@v1',
config: Object.freeze({
expression: `${index} 0 * * *`,
timezone: 'UTC',
misfirePolicy: 'skip',
}),
}),
enabled: true,
}),
]),
});
}
function command(candidates, overrides = {}) {
return {
mutationId: MUTATION_ID,
decisionId: DECISION_ID,
projectId: 'default',
profile: 'edge',
planDigest: DIGEST,
inventoryDigest: 'b'.repeat(64),
decisionDigest: 'c'.repeat(64),
receiptDigest: 'd'.repeat(64),
authorizationFileDigest: 'e'.repeat(64),
rowCount: candidates.length,
skippedCount: 0,
subject: SUBJECT,
fence: Object.freeze({ projectVersion: 1, bindingVersion: 1 }),
audit: Object.freeze({
eventId: MUTATION_ID,
requestId: 'legacy-adoption-test',
operationId: 'task.adopt',
projectId: 'default',
subject: SUBJECT,
authenticationId: 'local-console:adoption-review',
outcome: 'allowed',
reasons: Object.freeze(['project_role_allowed']),
fence: Object.freeze({ projectVersion: 1, bindingVersion: 1 }),
occurredAtMs: 100,
}),
candidates,
confirmExternalAuthority() {},
createdAtMs: 100,
...overrides,
};
}
test('publishes tasks, execution facts, triggers, audit and ledger atomically', async (t) => {
const databasePath = await preparedDatabase(t);
const adoption = await openLocalSqliteAdoptionDatabase({
databasePath,
profile: 'edge',
});
const input = command([candidate(1), candidate(2)]);
const inserted = await adoption.publisher.publish(input);
assert.equal(inserted.status, 'inserted');
assert.equal(inserted.adoption.adoptedTaskCount, 2);
assert.equal(inserted.adoption.adoptedTriggerCount, 2);
assert.match(inserted.adoption.publicationDigest, /^[0-9a-f]{64}$/);
assert.equal((await adoption.publisher.publish(input)).status, 'existing');
await adoption.close();
const client = new DatabaseSync(databasePath, { readOnly: true });
assert.equal(
client
.prepare('SELECT COUNT(*) AS count FROM "QingLong3TaskDefinitions"')
.get().count,
2,
);
assert.equal(
client.prepare('SELECT COUNT(*) AS count FROM "QingLong3Triggers"').get()
.count,
2,
);
assert.equal(
client
.prepare(
'SELECT COUNT(*) AS count FROM "QingLong3LocalTriggerSchedules" WHERE "next_fire_at_ms" IS NULL',
)
.get().count,
2,
);
assert.equal(
client
.prepare(
'SELECT COUNT(*) AS count FROM "QingLong3LocalTaskExecutionRevisions"',
)
.get().count,
2,
);
assert.deepEqual(
{
...client
.prepare(
`SELECT "operation_id" AS operationId, "outcome" AS outcome
FROM "QingLong3SecurityAuditEvents" WHERE "event_id" = ?`,
)
.get(MUTATION_ID),
},
{ operationId: 'task.adopt', outcome: 'allowed' },
);
client.close();
});
test('rolls the complete publication back on a later candidate conflict', async (t) => {
const databasePath = await preparedDatabase(t);
const adoption = await openLocalSqliteAdoptionDatabase({
databasePath,
profile: 'edge',
});
await assert.rejects(
adoption.publisher.publish(
command([candidate(1, 'duplicate-task'), candidate(2, 'duplicate-task')]),
),
LocalLegacyAdoptionConflictError,
);
await adoption.close();
const client = new DatabaseSync(databasePath, { readOnly: true });
for (const table of [
'QingLong3TaskDefinitions',
'QingLong3Triggers',
'QingLong3LocalTriggerSchedules',
'QingLong3LegacyAdoptions',
'QingLong3SecurityAuditEvents',
]) {
assert.equal(
client.prepare(`SELECT COUNT(*) AS count FROM "${table}"`).get().count,
0,
table,
);
}
client.close();
});
test('awaits the final external authority check and rolls back on rejection', async (t) => {
const databasePath = await preparedDatabase(t);
const adoption = await openLocalSqliteAdoptionDatabase({
databasePath,
profile: 'edge',
});
let checked = false;
const authorityFailure = new Error('external authority changed');
await assert.rejects(
adoption.publisher.publish(
command([candidate(1)], {
async confirmExternalAuthority() {
await Promise.resolve();
checked = true;
throw authorityFailure;
},
}),
),
{
name: 'LocalLegacyAdoptionUnavailableError',
code: 'LOCAL_LEGACY_ADOPTION_UNAVAILABLE',
cause: authorityFailure,
},
);
assert.equal(checked, true);
await adoption.close();
const client = new DatabaseSync(databasePath, { readOnly: true });
for (const table of [
'QingLong3TaskDefinitions',
'QingLong3Triggers',
'QingLong3LocalTriggerSchedules',
'QingLong3LegacyAdoptions',
'QingLong3SecurityAuditEvents',
]) {
assert.equal(
client.prepare(`SELECT COUNT(*) AS count FROM "${table}"`).get().count,
0,
table,
);
}
client.close();
});
test('rejects stale authorization fences before any adoption mutation', async (t) => {
const databasePath = await preparedDatabase(t);
const client = new DatabaseSync(databasePath);
client.exec(
`UPDATE "QingLong3Projects" SET "version" = 2, "updated_at_ms" = 2
WHERE "id" = 'default'`,
);
client.close();
const adoption = await openLocalSqliteAdoptionDatabase({
databasePath,
profile: 'edge',
});
await assert.rejects(
adoption.publisher.publish(command([candidate(1)])),
LocalLegacyAdoptionAuthorizationFenceConflictError,
);
await adoption.close();
});
@@ -0,0 +1,305 @@
const assert = require('node:assert/strict');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
ApprovalMutationConflictError,
ApprovalPolicyFenceConflictError,
ApprovalUnavailableError,
createApprovalRequest,
} = require('@qinglong/runtime-core/approved-action');
const {
LocalSqliteApprovalRequestRepository,
} = require('@qinglong/local-sqlite/approved-action');
const {
migrateLocalSqliteDatabase,
} = require('@qinglong/local-sqlite/migration');
const DIGEST_A = 'a'.repeat(64);
const DIGEST_B = 'b'.repeat(64);
const REQUESTER = Object.freeze({ type: 'user', id: 'usr_owner' });
const SYSTEM = Object.freeze({ type: 'system', id: 'approved-dispatcher' });
const FENCE = Object.freeze({ projectVersion: 1, bindingVersion: 1 });
function action(overrides = {}) {
return {
permission: 'package.manage',
actionType: 'plugin_package.install',
actionRef: 'proposal:pkg-demo-v1',
actionDigest: DIGEST_A,
previewDigest: DIGEST_B,
...overrides,
};
}
function request(id = 'approval-1') {
return createApprovalRequest({
id,
projectId: 'default',
action: action(),
risk: 'high',
decisionMode: 'human_confirmation',
requestedBy: REQUESTER,
requestedAtMs: 1_000,
expiresAtMs: 61_000,
requestFence: FENCE,
});
}
function audit(eventId, operationId, subject, authenticationId, outcome, atMs) {
return {
eventId,
requestId: 'request-http-1',
operationId,
projectId: 'default',
subject,
authenticationId,
outcome,
reasons: [outcome === 'approval_required' ? 'package_review' : 'role_grant'],
fence: FENCE,
occurredAtMs: atMs,
};
}
function createCommand(overrides = {}) {
return {
request: request(),
audit: audit(
'10000000-0000-4000-8000-000000000001',
'approval.request',
REQUESTER,
'auth-requester-1',
'approval_required',
1_000,
),
...overrides,
};
}
function decideCommand(overrides = {}) {
return {
requestId: 'approval-1',
expectedVersion: 1,
decisionId: 'decision-1',
decision: 'approved',
reasonCode: 'reviewed',
principal: {
subject: REQUESTER,
authenticationId: 'auth-step-up-1',
authenticatedAtMs: 1_500,
expiresAtMs: 10_000,
assurance: 'local_console',
},
decidedAtMs: 2_000,
authorizationFence: FENCE,
audit: audit(
'10000000-0000-4000-8000-000000000002',
'approval.decide',
REQUESTER,
'auth-step-up-1',
'allowed',
2_000,
),
...overrides,
};
}
function consumeCommand(overrides = {}) {
return {
requestId: 'approval-1',
expectedVersion: 2,
consumptionId: 'consume-1',
dispatchId: 'dispatch-1',
action: action(),
requestedBy: REQUESTER,
consumedBy: SYSTEM,
consumedAtMs: 3_000,
authorizationFence: FENCE,
audit: audit(
'10000000-0000-4000-8000-000000000003',
'approval.consume',
SYSTEM,
'auth-dispatcher-1',
'allowed',
3_000,
),
...overrides,
};
}
async function fixture(t) {
const client = new DatabaseSync(':memory:');
t.after(() => client.close());
client.exec('PRAGMA foreign_keys = ON');
await migrateLocalSqliteDatabase(client);
client
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings"
("project_id","subject_type","subject_id","version","state","role",
"mutation_id","changed_by_type","changed_by_id","created_at_ms")
VALUES ('default','user','usr_owner',1,'active','owner',
'grant-owner-1','user','usr_owner',0)`,
)
.run();
return {
client,
repository: new LocalSqliteApprovalRequestRepository(client),
};
}
test('persists request, strong decision, dispatch and audit with exact replay', async (t) => {
const { client, repository } = await fixture(t);
assert.equal((await repository.create(createCommand())).status, 'created');
assert.equal((await repository.create(createCommand())).status, 'existing');
const decided = await repository.decide(decideCommand());
assert.equal(decided.status, 'decided');
assert.equal(decided.request.state, 'approved');
assert.equal((await repository.decide(decideCommand())).status, 'existing');
const consumed = await repository.consume(consumeCommand());
assert.equal(consumed.status, 'consumed');
assert.equal(consumed.request.state, 'consumed');
assert.equal(consumed.dispatch.approvedBy.id, 'usr_owner');
assert.equal(
(await repository.consume(consumeCommand())).status,
'existing',
);
assert.deepEqual(
await repository.findDispatchById('dispatch-1'),
consumed.dispatch,
);
assert.equal(
client
.prepare(
`SELECT count(*) AS count FROM "QingLong3SecurityAuditEvents"
WHERE "operation_id" LIKE 'approval.%'`,
)
.get().count,
3,
);
});
test('rejects replay drift and rolls request plus audit back together', async (t) => {
const { client, repository } = await fixture(t);
await repository.create(createCommand());
await assert.rejects(
repository.create(
createCommand({
audit: {
...createCommand().audit,
reasons: ['changed'],
},
}),
),
ApprovalMutationConflictError,
);
await assert.rejects(
repository.decide(
decideCommand({
audit: {
...decideCommand().audit,
operationId: 'approval.consume',
},
}),
),
ApprovalMutationConflictError,
);
assert.equal((await repository.findById('approval-1')).state, 'pending');
assert.equal(
client
.prepare(
`SELECT count(*) AS count FROM "QingLong3SecurityAuditEvents"
WHERE "operation_id" = 'approval.decide'`,
)
.get().count,
0,
);
});
test('fences a role change before decision without partial audit', async (t) => {
const { client, repository } = await fixture(t);
await repository.create(createCommand());
client
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings"
("project_id","subject_type","subject_id","version","state","role",
"mutation_id","changed_by_type","changed_by_id","created_at_ms")
VALUES ('default','user','usr_owner',2,'active','owner',
'grant-owner-2','user','usr_owner',1500)`,
)
.run();
await assert.rejects(
repository.decide(decideCommand()),
ApprovalPolicyFenceConflictError,
);
assert.equal((await repository.findById('approval-1')).state, 'pending');
assert.equal(
client
.prepare(
`SELECT count(*) AS count FROM "QingLong3SecurityAuditEvents"
WHERE "event_id" = '10000000-0000-4000-8000-000000000002'`,
)
.get().count,
0,
);
});
test('fails closed when stored canonical request or dispatch JSON drifts', async (t) => {
const { client, repository } = await fixture(t);
await repository.create(createCommand());
client
.prepare(
`UPDATE "QingLong3ApprovalRequests"
SET "request_json" = json_set("request_json", '$.risk', 'low')
WHERE "request_id" = 'approval-1'`,
)
.run();
await assert.rejects(
repository.findById('approval-1'),
ApprovalUnavailableError,
);
});
test('runs an optional authentication guard inside every mutation transaction', async (t) => {
const client = new DatabaseSync(':memory:');
t.after(() => client.close());
client.exec('PRAGMA foreign_keys = ON');
await migrateLocalSqliteDatabase(client);
client
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings"
("project_id","subject_type","subject_id","version","state","role",
"mutation_id","changed_by_type","changed_by_id","created_at_ms")
VALUES ('default','user','usr_owner',1,'active','owner',
'10000000-0000-4000-8000-000000000099','user','usr_owner',1)`
)
.run();
let admitted = false;
let guardCalls = 0;
const repository = new LocalSqliteApprovalRequestRepository(client, () => {
guardCalls += 1;
if (!admitted) throw new Error('credential fence rejected');
});
await assert.rejects(
repository.create(createCommand()),
ApprovalUnavailableError,
);
assert.equal(guardCalls, 1);
assert.equal(await repository.findById('approval-1'), null);
admitted = true;
await repository.create(createCommand());
await repository.decide(decideCommand());
await repository.consume(consumeCommand());
assert.equal(guardCalls, 4);
});
test('exports the authority only through the approved-action subpath', () => {
const root = require('@qinglong/local-sqlite');
const subpath = require('@qinglong/local-sqlite/approved-action');
assert.equal(root.LocalSqliteApprovalRequestRepository, undefined);
assert.equal(
subpath.LocalSqliteApprovalRequestRepository,
LocalSqliteApprovalRequestRepository,
);
});
@@ -0,0 +1,218 @@
const assert = require('node:assert/strict');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
ApprovalUnavailableError,
createApprovalRequest,
} = require('@qinglong/runtime-core/approved-action');
const {
createToolInvocationPreviewArtifact,
} = require('@qinglong/runtime-core/tool-invocation-artifact');
const {
LocalSqliteApprovalRequestRepository,
} = require('@qinglong/local-sqlite/approved-action');
const {
LocalSqliteApprovalRequestSource,
} = require('@qinglong/local-sqlite/approval-discovery');
const {
LocalSqliteOperationAuthority,
} = require('@qinglong/local-sqlite/operation-authority');
const {
migrateLocalSqliteDatabase,
} = require('@qinglong/local-sqlite/migration');
const FENCE = Object.freeze({ projectVersion: 1, bindingVersion: 1 });
const REQUESTER = Object.freeze({ type: 'user', id: 'usr_owner' });
function request(id, requestedAtMs) {
return createApprovalRequest({
id,
projectId: 'default',
action: {
permission: 'run.start',
actionType: 'tool.invoke',
actionRef: `tool:${id}`,
actionDigest: 'a'.repeat(64),
previewDigest: 'b'.repeat(64),
},
risk: 'medium',
decisionMode: 'human_confirmation',
requestedBy: REQUESTER,
requestedAtMs,
expiresAtMs: requestedAtMs + 60_000,
requestFence: FENCE,
});
}
function audit(id, atMs) {
return {
eventId: id,
requestId: `command-${id}`,
operationId: 'approval.request',
projectId: 'default',
subject: REQUESTER,
authenticationId: 'auth-owner',
outcome: 'approval_required',
reasons: ['agent_action_requires_approval'],
fence: FENCE,
occurredAtMs: atMs,
};
}
async function fixture(t) {
const client = new DatabaseSync(':memory:');
client.exec('PRAGMA foreign_keys = ON');
await migrateLocalSqliteDatabase(client);
client.exec(`INSERT INTO "QingLong3ProjectRoleBindings"
("project_id","subject_type","subject_id","version","state","role",
"mutation_id","changed_by_type","changed_by_id","created_at_ms")
VALUES ('default','user','usr_owner',1,'active','owner','grant-owner',
'user','usr_owner',0)`);
const authority = new LocalSqliteOperationAuthority(client);
t.after(() => authority.close());
const writer = new LocalSqliteApprovalRequestRepository(authority);
for (const [index, atMs] of [1_000, 2_000, 3_000].entries()) {
const id = `approval-${index + 1}`;
await writer.create({
request: request(id, atMs),
audit: audit(`10000000-0000-4000-8000-00000000000${index + 1}`, atMs),
});
}
return {
authority,
client,
source: new LocalSqliteApprovalRequestSource(authority),
};
}
test('lists one Project newest-first with a stable keyset cursor', async (t) => {
const { source } = await fixture(t);
const first = await source.listApprovalRequests({
projectId: 'default',
limit: 2,
});
assert.deepEqual(
first.requests.map(({ id }) => id),
['approval-3', 'approval-2'],
);
assert.equal(first.truncated, true);
assert.deepEqual(first.next, {
updatedAtMs: 2_000,
requestId: 'approval-2',
});
const second = await source.listApprovalRequests({
projectId: 'default',
limit: 2,
after: first.next,
});
assert.deepEqual(second.requests.map(({ id }) => id), ['approval-1']);
assert.equal(second.truncated, false);
assert.equal(second.next, undefined);
});
test('rejects widened input and fails closed on row mirror drift', async (t) => {
const { client, source } = await fixture(t);
assert.throws(
() => source.listApprovalRequests({ projectId: 'default', limit: 65 }),
TypeError,
);
client.exec(`UPDATE "QingLong3ApprovalRequests"
SET "updated_at_ms" = "updated_at_ms" + 1
WHERE "request_id" = 'approval-3'`);
await assert.rejects(
source.listApprovalRequests({ projectId: 'default', limit: 2 }),
ApprovalUnavailableError,
);
});
test('reads one Project-scoped Approval with an exactly bound redacted preview', async (t) => {
const { authority, client, source } = await fixture(t);
const previewArtifact = createToolInvocationPreviewArtifact({
artifactId: 'preview-approval',
projectId: 'default',
actionRef: 'tool:approval-preview',
actionDigest: 'c'.repeat(64),
redactionContractDigest: 'd'.repeat(64),
sealedAtMs: 4_000,
preview: {
title: 'Run task',
summary: 'Runs one selected task.',
fields: [{ kind: 'redacted', label: 'Token', value: null }],
warnings: ['external_effect'],
},
});
const approval = createApprovalRequest({
id: 'approval-preview',
projectId: 'default',
action: {
permission: 'run.start',
actionType: 'tool.invoke',
actionRef: previewArtifact.actionRef,
actionDigest: previewArtifact.actionDigest,
previewDigest: previewArtifact.previewDigest,
},
risk: 'medium',
decisionMode: 'human_confirmation',
requestedBy: REQUESTER,
requestedAtMs: 4_000,
expiresAtMs: 64_000,
requestFence: FENCE,
});
await new LocalSqliteApprovalRequestRepository(authority).create({
request: approval,
audit: audit('10000000-0000-4000-8000-000000000009', 4_000),
});
client.prepare(`INSERT INTO "ToolInvocationPreviewArtifacts" (
artifact_id, project_id, action_ref, action_digest, preview_digest,
redaction_contract_digest, artifact_digest, byte_length, sealed_at_ms,
artifact_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
previewArtifact.artifactId,
previewArtifact.projectId,
previewArtifact.actionRef,
previewArtifact.actionDigest,
previewArtifact.previewDigest,
previewArtifact.redactionContractDigest,
previewArtifact.artifactDigest,
previewArtifact.byteLength,
previewArtifact.sealedAtMs,
JSON.stringify(previewArtifact),
);
const detail = await source.getApprovalRequestDetail({
projectId: 'default',
requestId: 'approval-preview',
});
assert.equal(detail.request.id, 'approval-preview');
assert.equal(detail.preview.title, 'Run task');
assert.equal(
await source.getApprovalRequestDetail({
projectId: 'other',
requestId: 'approval-preview',
}),
null,
);
client.exec('PRAGMA ignore_check_constraints = ON');
client.exec(`UPDATE "ToolInvocationPreviewArtifacts"
SET byte_length = byte_length + 1
WHERE artifact_id = 'preview-approval'`);
await assert.rejects(
source.getApprovalRequestDetail({
projectId: 'default',
requestId: 'approval-preview',
}),
ApprovalUnavailableError,
);
});
test('exports discovery separately from Approval mutation authority', () => {
const root = require('@qinglong/local-sqlite');
const mutation = require('@qinglong/local-sqlite/approved-action');
const discovery = require('@qinglong/local-sqlite/approval-discovery');
assert.equal(root.LocalSqliteApprovalRequestSource, undefined);
assert.equal(mutation.LocalSqliteApprovalRequestSource, undefined);
assert.equal(
discovery.LocalSqliteApprovalRequestSource,
LocalSqliteApprovalRequestSource,
);
});
@@ -0,0 +1,159 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
migrateLocalSqlitePath,
openLocalSqliteRuntimeDatabase,
} = require('../dist');
const RUN_1 = '019f70d0-0000-7000-8000-000000000001';
const RUN_2 = '019f70d0-0000-7000-8000-000000000002';
const ATTEMPT_1 = '019f70d0-0000-7000-8000-000000000011';
const ATTEMPT_2 = '019f70d0-0000-7000-8000-000000000012';
async function database(t) {
const directory = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-receipt-journal-'),
);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const databasePath = path.join(directory, 'qinglong3.sqlite');
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const opened = await openLocalSqliteRuntimeDatabase({
databasePath,
profile: 'edge',
});
t.after(() => opened.close());
return opened;
}
function run(id) {
return {
id,
projectId: 'default',
taskId: `task-${id}`,
taskRevision: `revision-${id}`,
triggerType: 'manual',
executionOrigin: 'manual',
executionOwner: 'runtime',
status: 'running',
version: 0,
eventSequence: 0,
priority: 0,
createdAtMs: 1,
};
}
function attempt(id, runId, status = 'running', finishedAtMs) {
return {
id,
runId,
attempt: 1,
status,
executorType: 'local_process',
callbackSequence: 0,
createdAtMs: 1,
...(finishedAtMs === undefined ? {} : { finishedAtMs }),
};
}
test('registers one exact pre-spawn receipt barrier idempotently', async (t) => {
const opened = await database(t);
await opened.runRepository.transaction(async (transaction) => {
await transaction.insertRun(run(RUN_1));
await transaction.insertAttempt(attempt(ATTEMPT_1, RUN_1));
});
const command = { runId: RUN_1, attemptId: ATTEMPT_1, registeredAtMs: 10 };
await opened.completionReceipts.register(command);
await opened.completionReceipts.register(command);
await assert.rejects(
opened.completionReceipts.register({ ...command, registeredAtMs: 11 }),
/registration conflicts/,
);
assert.deepEqual(
await opened.completionReceipts.listCandidates({ observedAtMs: 20 }),
{
candidates: [
{
...command,
state: 'pending',
updatedAtMs: 10,
attemptStatus: 'running',
executorType: 'local_process',
},
],
truncated: false,
nextCursor: { updatedAtMs: 10, attemptId: ATTEMPT_1 },
},
);
});
test('pages deterministically and exposes quarantines only when purge is due', async (t) => {
const opened = await database(t);
await opened.runRepository.transaction(async (transaction) => {
await transaction.insertRun(run(RUN_1));
await transaction.insertAttempt(attempt(ATTEMPT_1, RUN_1, 'failed', 5));
await transaction.insertRun(run(RUN_2));
await transaction.insertAttempt(attempt(ATTEMPT_2, RUN_2));
});
await opened.completionReceipts.register({
runId: RUN_1,
attemptId: ATTEMPT_1,
registeredAtMs: 10,
});
await opened.completionReceipts.register({
runId: RUN_2,
attemptId: ATTEMPT_2,
registeredAtMs: 11,
});
await opened.completionReceipts.markQuarantined({
attemptId: ATTEMPT_1,
quarantineRef: `.quarantine/${ATTEMPT_1.slice(0, 2)}/${ATTEMPT_1}.json`,
updatedAtMs: 20,
purgeAfterMs: 30,
});
const beforePurge = await opened.completionReceipts.listCandidates({
observedAtMs: 29,
limit: 1,
});
assert.deepEqual(
beforePurge.candidates.map(({ attemptId }) => attemptId),
[ATTEMPT_2],
);
assert.equal(beforePurge.truncated, false);
const due = await opened.completionReceipts.listCandidates({
observedAtMs: 30,
limit: 1,
});
assert.deepEqual(
due.candidates.map(({ attemptId }) => attemptId),
[ATTEMPT_2],
);
assert.equal(due.truncated, true);
const next = await opened.completionReceipts.listCandidates({
observedAtMs: 30,
limit: 1,
cursor: due.nextCursor,
});
assert.equal(next.candidates[0].attemptId, ATTEMPT_1);
assert.equal(next.candidates[0].state, 'quarantined');
assert.equal(await opened.completionReceipts.resolve(ATTEMPT_1), true);
assert.equal(await opened.completionReceipts.resolve(ATTEMPT_1), false);
});
test('rejects a receipt registration that is not bound to a local Attempt', async (t) => {
const opened = await database(t);
await assert.rejects(
opened.completionReceipts.register({
runId: RUN_1,
attemptId: ATTEMPT_1,
registeredAtMs: 1,
}),
/does not match a local Attempt/,
);
});
@@ -0,0 +1,744 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
runMigrationStream,
} = require('@qinglong/runtime-core/migration-stream');
const {
createLocalExecutionContextRecipe,
createLocalTaskExecutionRevision,
} = require('@qinglong/runtime-core/local-dispatch');
const {
createTaskDefinitionRecord,
} = require('@qinglong/runtime-core/task-definition');
const {
createBuiltInTaskSpecSemanticRegistry,
} = require('@qinglong/runtime-core/task-spec-semantic');
const {
compileLocalCommandTaskDefinition,
} = require('@qinglong/runtime-core/task-definition-execution-compiler');
const {
LocalSqliteConfigurationError,
LocalSqliteReadinessError,
auditLocalSqlitePath,
migrateLocalSqlitePath,
openLocalSqliteRuntimeDatabase,
} = require('../dist');
const {
localSqliteMigrationDefinition,
} = require('../dist/migration/migration');
const {
LocalSqliteMigrationStreamStore,
} = require('../dist/migration/migrationStreamStore');
function fixture(t) {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-local-sqlite-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
return {
directory,
databasePath: path.join(directory, 'qinglong3.sqlite'),
};
}
test('creates a reviewed edge database and opens runtime only after readiness', async (t) => {
const { databasePath } = fixture(t);
const options = { databasePath, profile: 'edge' };
const migrated = await migrateLocalSqlitePath(options);
assert.deepEqual(migrated.readiness.migrationIds, [
'0001-run-core',
'0002-capability',
'0003-completion-receipt-journal',
'0004-capability-v2',
'0005-local-dispatch-plan',
'0006-capability-v3',
'0007-local-secret-envelopes',
'0008-capability-v4',
'0009-local-project-policy-audit',
'0010-capability-v5',
'0011-local-identity-credential',
'0012-capability-v6',
'0013-local-owner-bootstrap',
'0014-capability-v7',
'0015-local-owner-delivery-acknowledgements',
'0016-capability-v8',
'0017-api-credential-pepper-bindings',
'0018-capability-v9',
'0019-local-owner-pepper-catalog',
'0020-capability-v10',
'0021-local-owner-credential-recovery',
'0022-capability-v11',
'0023-local-owner-pepper-material-gc',
'0024-capability-v12',
'0025-local-owner-delivery-acknowledgement-gc',
'0026-capability-v13',
'0027-task-definitions',
'0028-capability-v14',
'0029-local-execution-revision-digest',
'0030-capability-v15',
'0031-trigger-definitions',
'0032-capability-v16',
'0033-legacy-adoption-ledger',
'0034-capability-v17',
'0035-local-scheduler',
'0036-capability-v18',
'0037-plugin-package-installs',
'0038-capability-v19',
'0039-approved-actions',
'0040-capability-v20',
'0041-plugin-package-admission-receipts',
'0042-capability-v21',
'0043-approved-action-executions-and-package-proposals',
'0044-capability-v22',
'0045-plugin-package-materialized-revisions',
'0046-capability-v23',
'0047-plugin-package-task-reconciliations',
'0048-capability-v24',
'0049-project-tool-definition-snapshots',
'0050-capability-v25',
'0051-step-runs',
'0052-capability-v26',
'0053-tool-execution-evidence',
'0054-capability-v27',
'0055-tool-execution-start-barriers',
'0056-capability-v28',
'0057-tool-invocation-artifacts',
'0058-capability-v29',
'0059-tool-execution-artifact-bindings',
'0060-capability-v30',
'0061-tool-execution-completions',
'0062-capability-v31',
'0063-tool-execution-failure-completions',
'0064-capability-v32',
'0065-tool-result-key-catalog',
'0066-capability-v33',
'0067-tool-result-rekey-overlays',
'0068-capability-v34',
'0069-plugin-package-quarantine',
'0070-capability-v35',
'0071-local-identity-credential-administration',
'0072-capability-v36',
'0073-local-project-administration',
'0074-capability-v37',
'0075-security-audit-compactions',
'0076-capability-v38',
'0077-plugin-package-lifecycle',
'0078-capability-v39',
'0079-plugin-package-automation-publications',
'0080-capability-v40',
'0081-plugin-package-workflow-admissions',
'0082-capability-v41',
'0083-plugin-package-workflow-task-attempt-admissions',
'0084-capability-v42',
'0085-plugin-package-workflow-run-list-index',
'0086-capability-v43',
]);
assert.equal(migrated.readiness.contractName, 'local-control-core');
assert.equal(migrated.readiness.contractVersion, 43);
assert.equal(migrated.readiness.journalMode, 'delete');
assert.equal(fs.statSync(databasePath).mode & 0o777, 0o600);
assert.deepEqual(await auditLocalSqlitePath(options), migrated.readiness);
const runtime = await openLocalSqliteRuntimeDatabase(options);
assert.equal(runtime.profile, 'edge');
assert.deepEqual(runtime.readiness, migrated.readiness);
assert.deepEqual(Object.keys(runtime.localDispatch).sort(), [
'appendLocalExecutionContextRecipe',
'appendLocalTaskExecutionRevision',
'listLocalDispatchCandidates',
'resolveLocalExecutionContextRecipe',
'resolveLocalTaskExecutionRevision',
]);
assert.deepEqual(Object.keys(runtime.executionControl).sort(), [
'listLocalActiveExecutions',
'listLocalExecutionControlCandidates',
]);
assert.deepEqual(Object.keys(runtime.startupRecovery), ['inspectCandidates']);
assert.deepEqual(Object.keys(runtime.completionReceipts).sort(), [
'listCandidates',
'markQuarantined',
'register',
'resolve',
]);
for (const capability of [
runtime.localDispatch,
runtime.executionControl,
runtime.startupRecovery,
runtime.completionReceipts,
]) {
assert.equal(Object.isFrozen(capability), true);
assert.notEqual(capability, runtime.runRepository);
}
assert.equal(runtime.runRepository.register, undefined);
assert.equal(runtime.runRepository.listLocalDispatchCandidates, undefined);
assert.equal(
typeof runtime.localSecrets.resolveLocalSecretEnvelopes,
'function',
);
assert.equal(typeof runtime.projectPolicy.resolve, 'function');
assert.equal(
typeof runtime.localSecretAdministration
.appendAuthorizedLocalSecretEnvelope,
'function',
);
assert.equal(typeof runtime.securityAudit.record, 'function');
assert.equal(typeof runtime.apiCredentials.resolve, 'function');
assert.equal(
typeof runtime.taskDefinitions.findCurrentTaskDefinition,
'function',
);
assert.equal(typeof runtime.triggers.findCurrentTrigger, 'function');
assert.equal(
typeof (await runtime.projectToolDefinitionSnapshots()).findCurrent,
'function',
);
assert.equal(
typeof (await runtime.pluginPackageAutomationPublications())
.listPendingPage,
'function',
);
await Promise.all([runtime.close(), runtime.close()]);
for (const operation of [
runtime.runRepository.findRunById('closed-run'),
runtime.localDispatch.listLocalDispatchCandidates({ limit: 1 }),
runtime.executionControl.listLocalExecutionControlCandidates({
observedAtMs: 1,
limit: 1,
}),
runtime.startupRecovery.inspectCandidates({ limit: 1 }),
runtime.completionReceipts.listCandidates({
observedAtMs: 1,
limit: 1,
}),
]) {
await assert.rejects(
operation,
(error) => error?.name === 'RunRepositoryOperationError',
);
}
});
test('standalone opts into bounded WAL while edge keeps rollback journal', async (t) => {
const { databasePath } = fixture(t);
const options = { databasePath, profile: 'standalone' };
const migrated = await migrateLocalSqlitePath(options);
assert.equal(migrated.readiness.journalMode, 'wal');
const runtime = await openLocalSqliteRuntimeDatabase(options);
assert.equal(runtime.readiness.journalMode, 'wal');
await runtime.close();
});
test('loads one shared trusted Tool storage bundle only when requested', async (t) => {
const { databasePath } = fixture(t);
const options = { databasePath, profile: 'edge' };
await migrateLocalSqlitePath(options);
const runtime = await openLocalSqliteRuntimeDatabase(options);
const first = await runtime.trustedToolStorage();
const second = await runtime.trustedToolStorage();
assert.equal(first, second);
assert.equal(Object.isFrozen(first), true);
assert.equal(typeof first.invocationArtifacts.findInput, 'function');
assert.equal(typeof first.stepRuns.findById, 'function');
assert.equal(typeof first.startBarriers.findByStartId, 'function');
assert.equal(typeof first.completions.findByStartId, 'function');
assert.equal(typeof first.failureCompletions.findByStartId, 'function');
assert.equal(typeof first.resultKeyCatalog.findCurrent, 'function');
assert.equal(first.resultKeyCatalog.append, undefined);
assert.equal(typeof first.resultRekeys.findHeadByArtifactId, 'function');
assert.equal(first.resultRekeys.append, undefined);
assert.equal(typeof first.toolDefinitionSnapshots.findCurrent, 'function');
assert.equal(
first.toolDefinitionSnapshots,
await runtime.projectToolDefinitionSnapshots(),
);
await runtime.close();
});
test('backfills legacy credential provenance and recovery-required catalog state', async () => {
const client = new DatabaseSync(':memory:');
try {
client.exec('PRAGMA foreign_keys = ON');
const legacyStream = {
...localSqliteMigrationDefinition,
migrations: localSqliteMigrationDefinition.migrations.slice(0, 16),
};
await runMigrationStream({
stream: legacyStream,
store: new LocalSqliteMigrationStreamStore(client),
});
client.exec(`
INSERT INTO "QingLong3IdentitySubjects" (
"subject_type", "subject_id", "status", "version",
"created_at_ms", "updated_at_ms"
) VALUES ('user', 'legacy-user', 'active', 1, 1, 1);
INSERT INTO "QingLong3ApiCredentials" (
"credential_id", "version", "state", "subject_type", "subject_id",
"secret_digest", "created_at_ms", "not_before_at_ms", "expires_at_ms"
) VALUES (
'legacy-credential', 1, 'active', 'user', 'legacy-user',
'${'a'.repeat(64)}', 1, 1, 2
);
`);
await localSqliteMigrationDefinition.migrations[16].up({ client });
client.exec('BEGIN IMMEDIATE');
try {
await localSqliteMigrationDefinition.migrations[18].up({ client });
client.exec('COMMIT');
} catch (error) {
if (client.isTransaction) client.exec('ROLLBACK');
throw error;
}
assert.deepEqual(
{
...client
.prepare(
`SELECT credential_id, credential_version, pepper_key_id
FROM "QingLong3ApiCredentialPepperBindings"`,
)
.get(),
},
{
credential_id: 'legacy-credential',
credential_version: 1,
pepper_key_id: 'legacy-v1',
},
);
assert.deepEqual(
{
...client
.prepare(
`SELECT pepper_key_id, state, version, material_digest
FROM "QingLong3LocalOwnerPepperKeys"`,
)
.get(),
},
{
pepper_key_id: 'legacy-v1',
state: 'recovery_required',
version: 1,
material_digest: null,
},
);
} finally {
client.close();
}
});
test('backfills v14 execution revisions with a verified independent digest', async () => {
const client = new DatabaseSync(':memory:');
try {
client.exec('PRAGMA foreign_keys = ON');
await runMigrationStream({
stream: {
...localSqliteMigrationDefinition,
migrations: localSqliteMigrationDefinition.migrations.slice(0, 28),
},
store: new LocalSqliteMigrationStreamStore(client),
});
const recipe = createLocalExecutionContextRecipe({
environment: [{ name: 'VALUE', kind: 'public', value: 'legacy' }],
createdAtMs: 10,
});
client
.prepare(
`INSERT INTO "QingLong3LocalExecutionContextRecipes" (
"context_ref", "environment_json", "content_digest", "created_at_ms"
) VALUES (?, ?, ?, ?)`,
)
.run(
recipe.contextRef,
JSON.stringify(recipe.environment),
recipe.contentDigest,
recipe.createdAtMs,
);
client
.prepare(
`INSERT INTO "QingLong3LocalTaskExecutionRevisions" (
"project_id", "task_id", "task_revision", "executor_type",
"command_json", "working_directory", "timeout_ms",
"context_ref", "created_at_ms"
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
'default',
'legacy-task',
'legacy-revision',
'local_process',
JSON.stringify({ kind: 'argv', file: '/bin/echo', args: ['legacy'] }),
'/tmp',
1000,
recipe.contextRef,
11,
);
const registry = createBuiltInTaskSpecSemanticRegistry();
const taskCommand = {
projectId: 'default',
taskId: 'task-definition-backfill',
expectedRevision: null,
mutationId: '019f7200-0000-7000-8000-000000000029',
name: 'Backfilled TaskDefinition',
kind: 'command',
spec: registry.normalize({
projectId: 'default',
taskId: 'task-definition-backfill',
kind: 'command',
spec: {
schema: 'qinglong/command@v1',
config: {
command: {
kind: 'argv',
file: '/bin/echo',
args: ['definition'],
},
},
},
}),
labels: {},
enabled: true,
occurredAtMs: 12,
};
const taskDefinition = createTaskDefinitionRecord(taskCommand, 12);
const taskPlan = compileLocalCommandTaskDefinition(
taskDefinition,
registry,
);
client
.prepare(
`INSERT INTO "QingLong3TaskDefinitions" (
"project_id", "task_id", "current_revision",
"created_at_ms", "updated_at_ms"
) VALUES (?, ?, ?, ?, ?)`,
)
.run(
taskDefinition.projectId,
taskDefinition.taskId,
taskDefinition.revision,
taskDefinition.createdAtMs,
taskDefinition.updatedAtMs,
);
client
.prepare(
`INSERT INTO "QingLong3TaskDefinitionRevisions" (
"project_id", "task_id", "revision", "mutation_id",
"name", "description", "kind", "spec_json", "labels_json",
"enabled", "content_digest", "created_at_ms"
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
taskDefinition.projectId,
taskDefinition.taskId,
taskDefinition.revision,
taskDefinition.mutationId,
taskDefinition.name,
null,
taskDefinition.kind,
JSON.stringify(taskDefinition.spec),
JSON.stringify(taskDefinition.labels),
1,
taskDefinition.contentDigest,
taskDefinition.updatedAtMs,
);
await runMigrationStream({
stream: localSqliteMigrationDefinition,
store: new LocalSqliteMigrationStreamStore(client),
});
const expected = createLocalTaskExecutionRevision({
projectId: 'default',
taskId: 'legacy-task',
taskRevision: 'legacy-revision',
executorType: 'local_process',
command: { kind: 'argv', file: '/bin/echo', args: ['legacy'] },
workingDirectory: '/tmp',
timeoutMs: 1000,
contextRef: recipe.contextRef,
createdAtMs: 11,
});
const stored = client
.prepare(
`SELECT "content_digest" AS "contentDigest", "command_json" AS "commandJson"
FROM "QingLong3LocalTaskExecutionRevisions"`,
)
.get();
assert.equal(stored.contentDigest, expected.contentDigest);
assert.equal(stored.commandJson, JSON.stringify(expected.command));
assert.deepEqual(
{
...client
.prepare(
`SELECT "content_digest" AS "contentDigest",
"context_ref" AS "contextRef"
FROM "QingLong3LocalTaskExecutionRevisions"
WHERE "project_id" = ? AND "task_id" = ?
AND "task_revision" = ?`,
)
.get(
taskDefinition.projectId,
taskDefinition.taskId,
taskPlan.executionRevision.taskRevision,
),
},
{
contentDigest: taskPlan.executionRevision.contentDigest,
contextRef: taskPlan.contextRecipe.contextRef,
},
);
assert.deepEqual(
{
...client
.prepare(
`SELECT contract_version, migration_id
FROM "QingLong3SchemaCapabilities"
WHERE contract_name = 'local-control-core'`,
)
.get(),
},
{
contract_version: 43,
migration_id: '0085-plugin-package-workflow-run-list-index',
},
);
} finally {
client.close();
}
});
test('rolls the digest migration back when a legacy revision is not canonical', async () => {
const client = new DatabaseSync(':memory:');
try {
client.exec('PRAGMA foreign_keys = ON');
await runMigrationStream({
stream: {
...localSqliteMigrationDefinition,
migrations: localSqliteMigrationDefinition.migrations.slice(0, 28),
},
store: new LocalSqliteMigrationStreamStore(client),
});
const recipe = createLocalExecutionContextRecipe({
environment: [],
createdAtMs: 1,
});
client
.prepare(
`INSERT INTO "QingLong3LocalExecutionContextRecipes" (
"context_ref", "environment_json", "content_digest", "created_at_ms"
) VALUES (?, ?, ?, ?)`,
)
.run(recipe.contextRef, '[]', recipe.contentDigest, 1);
client
.prepare(
`INSERT INTO "QingLong3LocalTaskExecutionRevisions" (
"project_id", "task_id", "task_revision", "executor_type",
"command_json", "working_directory", "timeout_ms",
"context_ref", "created_at_ms"
) VALUES (?, ?, ?, ?, ?, NULL, NULL, ?, ?)`,
)
.run(
'default',
'corrupt-task',
'corrupt-revision',
'local_process',
'{}',
recipe.contextRef,
1,
);
await assert.rejects(
runMigrationStream({
stream: localSqliteMigrationDefinition,
store: new LocalSqliteMigrationStreamStore(client),
}),
/command kind is invalid/,
);
assert.equal(
client
.prepare(
`SELECT COUNT(*) AS count FROM pragma_table_info(
'QingLong3LocalTaskExecutionRevisions'
) WHERE name = 'content_digest'`,
)
.get().count,
0,
);
assert.equal(
client
.prepare(`SELECT COUNT(*) AS count FROM "QingLong3SchemaMigrations"`)
.get().count,
28,
);
} finally {
client.close();
}
});
test('runtime entrypoint keeps migration, compiler and Plugin Package adapter lazy', () => {
const script = `
require('@qinglong/local-sqlite/runtime');
const loaded = Object.keys(require.cache)
.filter((entry) =>
/[\\/]migrations[\\/]|[\\/]migration\\.js$|taskDefinitionExecutionCompiler\\.js$|pluginPackageInstallRepository\\.js$|approvedActionExecutionRepository\\.js$|pluginPackageProposalRepository\\.js$/.test(entry),
);
process.stdout.write(JSON.stringify(loaded));
`;
const result = spawnSync(process.execPath, ['-e', script], {
encoding: 'utf8',
});
assert.equal(result.status, 0, result.stderr);
assert.deepEqual(JSON.parse(result.stdout), []);
});
test('readiness inspection subpath excludes DDL and mutable repositories', () => {
const script = `
const inspection = require(${JSON.stringify(
path.resolve(__dirname, '../dist/readiness/readinessInspection.js'),
)});
const loaded = Object.keys(require.cache)
.filter((entry) =>
/[\\/]migrations[\\/]|[\\/]migration\\.js$|runRepository\\.js$|pluginPackageInstallRepository\\.js$/.test(entry),
);
process.stdout.write(JSON.stringify({
inspection: typeof inspection.inspectLocalSqliteReadinessPath,
loaded,
}));
`;
const result = spawnSync(process.execPath, ['-e', script], {
encoding: 'utf8',
});
assert.equal(result.status, 0, result.stderr);
assert.deepEqual(JSON.parse(result.stdout), {
inspection: 'function',
loaded: [],
});
});
test('Approved Action execution and Package proposal authorities require explicit subpaths', () => {
const root = require('@qinglong/local-sqlite');
const execution = require('@qinglong/local-sqlite/approved-action-execution');
const proposal = require('@qinglong/local-sqlite/plugin-package-proposal');
assert.equal(root.LocalSqliteApprovedActionExecutionRepository, undefined);
assert.equal(
root.LocalSqlitePluginPackageInstallProposalRepository,
undefined,
);
assert.equal(
typeof execution.LocalSqliteApprovedActionExecutionRepository,
'function',
);
assert.equal(
typeof proposal.LocalSqlitePluginPackageInstallProposalRepository,
'function',
);
});
test('runtime bootstrap never auto-migrates an unprepared database', async (t) => {
const { databasePath } = fixture(t);
new DatabaseSync(databasePath).close();
await assert.rejects(
openLocalSqliteRuntimeDatabase({ databasePath, profile: 'standalone' }),
LocalSqliteReadinessError,
);
const client = new DatabaseSync(databasePath, { readOnly: true });
try {
assert.equal(
client
.prepare(
`SELECT 1 FROM sqlite_schema
WHERE type = 'table' AND name = 'QingLong3SchemaMigrations'`,
)
.get(),
undefined,
);
} finally {
client.close();
}
});
test('fails closed for schema drift and migration checksum drift', async (t) => {
const { databasePath } = fixture(t);
const options = { databasePath, profile: 'standalone' };
await migrateLocalSqlitePath(options);
const client = new DatabaseSync(databasePath);
client.exec('DROP INDEX ql3_local_events_run_sequence_uidx');
client
.prepare(
`UPDATE "QingLong3SchemaMigrations"
SET checksum = ? WHERE migration_id = '0001-run-core'`,
)
.run('0'.repeat(64));
client.close();
await assert.rejects(
auditLocalSqlitePath(options),
(error) =>
error instanceof LocalSqliteReadinessError &&
/audit failed/.test(error.message),
);
});
test('excludes reviewed optional feature tables while preserving unknown table drift evidence', async (t) => {
const { databasePath } = fixture(t);
const options = { databasePath, profile: 'edge' };
await migrateLocalSqlitePath(options);
const client = new DatabaseSync(databasePath);
assert.equal((await auditLocalSqlitePath(options)).tableCount, 76);
client.exec(
'CREATE TABLE "ModelInvocationFeatureHead" (feature_id TEXT PRIMARY KEY)',
);
client.close();
assert.equal((await auditLocalSqlitePath(options)).tableCount, 76);
const unknownClient = new DatabaseSync(databasePath);
unknownClient.exec('CREATE TABLE "UserExtensionData" (id TEXT PRIMARY KEY)');
unknownClient.close();
assert.equal((await auditLocalSqlitePath(options)).tableCount, 77);
const triggerClient = new DatabaseSync(databasePath);
triggerClient.exec(`
CREATE TRIGGER unreviewed_run_trigger AFTER INSERT ON "Runs"
BEGIN
SELECT 1;
END
`);
triggerClient.close();
await assert.rejects(
auditLocalSqlitePath(options),
/reviewed trigger contract is incompatible/,
);
});
test('rejects wrong Profiles, relative paths and symlink targets before opening', async (t) => {
const { directory, databasePath } = fixture(t);
await assert.rejects(
() =>
migrateLocalSqlitePath({
databasePath,
profile: 'cluster-control',
}),
LocalSqliteConfigurationError,
);
await assert.rejects(
() =>
migrateLocalSqlitePath({ databasePath: 'relative.db', profile: 'edge' }),
LocalSqliteConfigurationError,
);
const target = path.join(directory, 'target.sqlite');
new DatabaseSync(target).close();
const link = path.join(directory, 'linked.sqlite');
fs.symlinkSync(target, link);
await assert.rejects(
migrateLocalSqlitePath({ databasePath: link, profile: 'edge' }),
LocalSqliteConfigurationError,
);
});
@@ -0,0 +1,667 @@
const fs = require('node:fs');
const { DatabaseSync } = require('node:sqlite');
const {
createApprovalRequest,
} = require('@qinglong/runtime-core/approved-action');
const {
createPluginPackageLifecycleEvent,
pluginPackageLifecycleActionDigest,
} = require('@qinglong/runtime-core/plugin-package-lifecycle');
const {
createProjectToolDefinitionSnapshot,
projectToolDefinitionSnapshotContribution,
} = require('@qinglong/runtime-core/project-tool-definition-snapshot');
const {
activateInstall,
pluginPackageTaskReconciliationFixture,
} = require('../../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
const {
LocalSqliteApprovalRequestRepository,
} = require('../../dist/approved-action/approvalRequestRepository');
const {
LocalSqliteOperationAuthority,
} = require('../../dist/authority/operationAuthority');
const {
LocalSqlitePluginPackageInstallRepository,
} = require('../../dist/plugin-package/pluginPackageInstallRepository');
const {
EDGE_PLUGIN_PACKAGE_LIFECYCLE_ACTIVE_SOURCE_LIMIT,
STANDALONE_PLUGIN_PACKAGE_LIFECYCLE_ACTIVE_SOURCE_LIMIT,
LocalSqlitePluginPackageLifecycleRepository,
} = require('../../dist/plugin-package/pluginPackageLifecycleRepository');
const {
LocalSqlitePluginPackageMaterializedRevisionRepository,
} = require('../../dist/plugin-package/pluginPackageMaterializedRevisionRepository');
const {
LocalSqlitePluginPackageTaskReconciliationRepository,
} = require('../../dist/plugin-package/pluginPackageTaskReconciliationRepository');
const {
LocalSqliteProjectToolDefinitionSnapshotRepository,
} = require('../../dist/tool-execution/projectToolDefinitionSnapshotRepository');
const { migrateLocalSqlitePath } = require('../../dist/migration/migration');
const { auditLocalSqliteReadiness } = require('../../dist/readiness/readiness');
const OWNER = Object.freeze({ type: 'user', id: 'owner-001' });
const SYSTEM = Object.freeze({ type: 'system', id: 'lifecycle-dispatcher' });
const FENCE = Object.freeze({ projectVersion: 1, bindingVersion: 1 });
const CRASH_POINTS = Object.freeze({
after_task_revision: Object.freeze({
timing: 'afterRun',
sql: 'INSERT INTO "QingLong3TaskDefinitionRevisions"',
durable: false,
}),
after_tool_snapshot: Object.freeze({
timing: 'afterRun',
sql: 'INSERT INTO "QingLong3ProjectToolDefinitionSnapshots"',
durable: false,
}),
after_lifecycle_event: Object.freeze({
timing: 'afterRun',
sql: 'INSERT INTO "QingLong3PluginPackageLifecycleEvents"',
durable: false,
}),
after_lifecycle_receipt: Object.freeze({
timing: 'afterRun',
sql: 'INSERT INTO "QingLong3PluginPackageLifecycleReceipts"',
durable: false,
}),
after_lifecycle_task: Object.freeze({
timing: 'afterRun',
sql: 'INSERT INTO "QingLong3PluginPackageLifecycleTasks"',
durable: false,
}),
after_lifecycle_head: Object.freeze({
timing: 'afterRun',
sql: 'INSERT INTO "QingLong3PluginPackageLifecycleHeads"',
durable: false,
}),
before_commit: Object.freeze({
timing: 'beforeExec',
sql: 'COMMIT',
durable: false,
}),
after_commit: Object.freeze({
timing: 'afterExec',
sql: 'COMMIT',
durable: true,
}),
});
function fixture(profile) {
return pluginPackageTaskReconciliationFixture(
`lifecycle-crash-${profile}`,
{ profile },
);
}
function client(databasePath) {
const database = new DatabaseSync(databasePath);
database.exec('PRAGMA foreign_keys = ON');
return database;
}
function activeSourceLimit(profile) {
return profile === 'edge'
? EDGE_PLUGIN_PACKAGE_LIFECYCLE_ACTIVE_SOURCE_LIMIT
: STANDALONE_PLUGIN_PACKAGE_LIFECYCLE_ACTIVE_SOURCE_LIMIT;
}
function repositories(authority, value, profile) {
return {
approval: new LocalSqliteApprovalRequestRepository(authority),
install: new LocalSqlitePluginPackageInstallRepository(authority),
lifecycle: new LocalSqlitePluginPackageLifecycleRepository(authority, {
registry: value.registry,
activeSourceLimit: activeSourceLimit(profile),
}),
materialized:
new LocalSqlitePluginPackageMaterializedRevisionRepository(
authority,
value.registry,
),
reconciliation:
new LocalSqlitePluginPackageTaskReconciliationRepository(
authority,
value.registry,
),
snapshots: new LocalSqliteProjectToolDefinitionSnapshotRepository(
authority,
),
};
}
function audit(
eventId,
requestId,
operationId,
subject,
authenticationId,
outcome,
projectId,
occurredAtMs,
) {
return {
eventId,
requestId,
operationId,
projectId,
subject,
authenticationId,
outcome,
reasons: [outcome === 'approval_required' ? 'package_review' : 'role_grant'],
fence: FENCE,
occurredAtMs,
};
}
function auditId(sequence, offset) {
return `91000000-0000-4000-8000-${String(sequence * 10 + offset).padStart(
12,
'0',
)}`;
}
function approvalSequence(action) {
return action === 'enable' ? 2 : 1;
}
async function approveLifecycleImpact(
approval,
value,
impact,
sequence,
) {
const requestId = `lifecycle-crash-approval-${sequence}`;
const dispatchId = `lifecycle-crash-dispatch-${sequence}`;
const requestedAtMs = 10_000 * sequence + 1;
const decidedAtMs = requestedAtMs + 1;
const consumedAtMs = requestedAtMs + 2;
const expiresAtMs = requestedAtMs + 1_000;
const action = {
permission: 'package.manage',
actionType: `plugin_package.lifecycle.${impact.action}`,
actionRef: `lifecycle:${impact.impactDigest}`,
actionDigest: pluginPackageLifecycleActionDigest(impact),
previewDigest: impact.impactDigest,
};
await approval.create({
request: createApprovalRequest({
id: requestId,
projectId: value.projectId,
action,
risk: 'high',
decisionMode: 'human_confirmation',
requestedBy: OWNER,
requestedAtMs,
expiresAtMs,
requestFence: FENCE,
}),
audit: audit(
auditId(sequence, 1),
`lifecycle-crash-http-${sequence}`,
'approval.request',
OWNER,
`auth-request-${sequence}`,
'approval_required',
value.projectId,
requestedAtMs,
),
});
await approval.decide({
requestId,
expectedVersion: 1,
decisionId: `lifecycle-crash-decision-${sequence}`,
decision: 'approved',
reasonCode: 'reviewed',
principal: {
subject: OWNER,
authenticationId: `auth-approve-${sequence}`,
authenticatedAtMs: decidedAtMs - 1,
expiresAtMs,
assurance: 'local_console',
},
decidedAtMs,
authorizationFence: FENCE,
audit: audit(
auditId(sequence, 2),
`lifecycle-crash-http-${sequence}`,
'approval.decide',
OWNER,
`auth-approve-${sequence}`,
'allowed',
value.projectId,
decidedAtMs,
),
});
return approval.consume({
requestId,
expectedVersion: 2,
consumptionId: `lifecycle-crash-consume-${sequence}`,
dispatchId,
action,
requestedBy: OWNER,
consumedBy: SYSTEM,
consumedAtMs,
authorizationFence: FENCE,
audit: audit(
auditId(sequence, 3),
`lifecycle-crash-dispatch-cycle-${sequence}`,
'approval.consume',
SYSTEM,
`auth-dispatch-${sequence}`,
'allowed',
value.projectId,
consumedAtMs,
),
});
}
function lifecycleEvent(impact, action) {
const sequence = approvalSequence(action);
return createPluginPackageLifecycleEvent({
dispatchId: `lifecycle-crash-dispatch-${sequence}`,
impact,
requestedBy: OWNER,
approvedBy: OWNER,
authorizationMode: 'human_confirmation',
occurredAtMs: 10_000 * sequence + 4,
});
}
async function publishActivePackage(repositoriesValue, value) {
await activateInstall(repositoriesValue.install, value);
await repositoriesValue.materialized.publish(value.revision);
await repositoriesValue.reconciliation.reconcile(value.revision, {
async findActiveResourceGeneration() {
return value.revision.generation;
},
});
await repositoriesValue.snapshots.publish(
createProjectToolDefinitionSnapshot({
projectId: value.projectId,
contributions: [
projectToolDefinitionSnapshotContribution(
value.revision,
value.registry,
),
],
}),
);
}
async function setupScenario({ action, databasePath, profile }) {
await migrateLocalSqlitePath({ databasePath, profile });
const value = fixture(profile);
const database = client(databasePath);
database
.prepare(
`INSERT INTO "QingLong3Projects"
(id, name, slug, status, version, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, 'active', 1, 1, 1)`,
)
.run(value.projectId, value.projectId, value.projectId);
database
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings" (
project_id, subject_type, subject_id, version, state, role,
mutation_id, changed_by_type, changed_by_id, created_at_ms
) VALUES (?, 'user', ?, 1, 'active', 'owner', ?, 'user', ?, 1)`,
)
.run(
value.projectId,
OWNER.id,
`grant-lifecycle-crash-${profile}`,
OWNER.id,
);
const authority = new LocalSqliteOperationAuthority(database);
try {
const repository = repositories(authority, value, profile);
await publishActivePackage(repository, value);
if (action === 'enable') {
const disableImpact = await repository.lifecycle.plan(
'disable',
value.projectId,
value.packageName,
);
await approveLifecycleImpact(
repository.approval,
value,
disableImpact,
1,
);
await repository.lifecycle.transition(
lifecycleEvent(disableImpact, 'disable'),
() => {},
);
}
const impact = await repository.lifecycle.plan(
action,
value.projectId,
value.packageName,
);
await approveLifecycleImpact(
repository.approval,
value,
impact,
approvalSequence(action),
);
return lifecycleEvent(impact, action);
} finally {
await authority.close();
}
}
function writeCrashMarker(markerPath, pointName, action) {
const descriptor = fs.openSync(markerPath, 'wx', 0o600);
try {
fs.writeSync(
descriptor,
JSON.stringify({
schema: 'qinglong/sqlite-plugin-package-lifecycle-crash-marker@v1',
action,
point: pointName,
pid: process.pid,
}),
);
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
function crashClient(database, pointName, markerPath, action) {
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, action);
process.kill(process.pid, 'SIGKILL');
throw new Error(`SIGKILL did not terminate ${action}/${pointName}`);
};
const matches = (timing, sql) =>
!triggered && point.timing === timing && sql.trim().includes(point.sql);
return new Proxy(database, {
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({
action,
databasePath,
markerPath,
pointName,
profile,
}) {
const value = fixture(profile);
const planningAuthority = new LocalSqliteOperationAuthority(
client(databasePath),
);
const impact = await repositories(
planningAuthority,
value,
profile,
).lifecycle.plan(action, value.projectId, value.packageName);
await planningAuthority.close();
const database = client(databasePath);
const authority = new LocalSqliteOperationAuthority(
crashClient(database, pointName, markerPath, action),
);
const repository = repositories(authority, value, profile);
await repository.lifecycle.transition(lifecycleEvent(impact, action), () => {});
throw new Error(`crash point ${action}/${pointName} was not reached`);
}
function eventFacts(database, eventDigest) {
return {
events: database
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3PluginPackageLifecycleEvents"
WHERE event_digest = ?`,
)
.get(eventDigest).count,
receipts: database
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3PluginPackageLifecycleReceipts"
WHERE event_digest = ?`,
)
.get(eventDigest).count,
tasks: database
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3PluginPackageLifecycleTasks"
WHERE event_digest = ?`,
)
.get(eventDigest).count,
heads: database
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3PluginPackageLifecycleHeads"
WHERE event_digest = ?`,
)
.get(eventDigest).count,
};
}
function taskFacts(database, projectId) {
return database
.prepare(
`SELECT revision.enabled,
head.current_revision AS "currentRevision"
FROM "QingLong3TaskDefinitions" AS head
JOIN "QingLong3TaskDefinitionRevisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
AND revision.revision = head.current_revision
WHERE head.project_id = ?
ORDER BY head.task_id`,
)
.all(projectId);
}
function assertTaskState(facts, revision, enabled, label) {
if (
facts.length !== 2 ||
facts.some(
(fact) =>
fact.enabled !== enabled || fact.currentRevision !== revision,
)
) {
throw new Error(`${label} Task state is incomplete`);
}
}
async function verifyScenario({
action,
databasePath,
event,
pointName,
profile,
}) {
const point = CRASH_POINTS[pointName];
const value = fixture(profile);
const database = client(databasePath);
const authority = new LocalSqliteOperationAuthority(database);
try {
const repository = repositories(authority, value, profile);
const beforeRecovery = await repository.lifecycle.findByEventDigest(
event.eventDigest,
);
if (point.durable !== (beforeRecovery !== null)) {
throw new Error(
`${profile}/${action}/${pointName} durability is inconsistent`,
);
}
const beforeEventFacts = eventFacts(database, event.eventDigest);
const expectedBeforeFacts = point.durable
? { events: 1, receipts: 1, tasks: 2, heads: 1 }
: { events: 0, receipts: 0, tasks: 0, heads: 0 };
if (JSON.stringify(beforeEventFacts) !== JSON.stringify(expectedBeforeFacts)) {
throw new Error(
`${profile}/${action}/${pointName} left partial lifecycle facts`,
);
}
const beforeRevision = action === 'disable' ? 1 : 2;
const beforeEnabled = action === 'disable' ? 1 : 0;
const finalRevision = action === 'disable' ? 2 : 3;
const finalEnabled = action === 'disable' ? 0 : 1;
assertTaskState(
taskFacts(database, value.projectId),
point.durable ? finalRevision : beforeRevision,
point.durable ? finalEnabled : beforeEnabled,
`${profile}/${action}/${pointName} pre-recovery`,
);
const beforeSnapshot = await repository.snapshots.findCurrent(
value.projectId,
);
const expectedBeforeSourceCount = point.durable
? action === 'enable'
? 1
: 0
: action === 'enable'
? 0
: 1;
if (beforeSnapshot.snapshot.sources.length !== expectedBeforeSourceCount) {
throw new Error(
`${profile}/${action}/${pointName} Tool snapshot is partial`,
);
}
const recovered = await repository.lifecycle.transition(event, () => {});
if (recovered.status !== (point.durable ? 'existing' : 'created')) {
throw new Error(
`${profile}/${action}/${pointName} replay status is inconsistent`,
);
}
assertTaskState(
taskFacts(database, value.projectId),
finalRevision,
finalEnabled,
`${profile}/${action}/${pointName} recovered`,
);
const recoveredSnapshot = await repository.snapshots.findCurrent(
value.projectId,
);
const finalSourceCount = action === 'enable' ? 1 : 0;
if (recoveredSnapshot.snapshot.sources.length !== finalSourceCount) {
throw new Error(
`${profile}/${action}/${pointName} recovered Tool snapshot is invalid`,
);
}
const recoveredFacts = eventFacts(database, event.eventDigest);
if (
JSON.stringify(recoveredFacts) !==
JSON.stringify({ events: 1, receipts: 1, tasks: 2, heads: 1 })
) {
throw new Error(
`${profile}/${action}/${pointName} replay is not exactly once`,
);
}
const head = await repository.lifecycle.findHead(
value.projectId,
value.packageName,
);
const expectedDisposition = action === 'enable' ? 'active' : 'disabled';
if (
!head ||
head.disposition !== expectedDisposition ||
head.eventDigest !== event.eventDigest
) {
throw new Error(
`${profile}/${action}/${pointName} lifecycle head is invalid`,
);
}
await auditLocalSqliteReadiness(database);
const integrity = database.prepare('PRAGMA integrity_check').get();
const foreignKey = database
.prepare('SELECT * FROM pragma_foreign_key_check LIMIT 1')
.get();
const journal = database.prepare('PRAGMA journal_mode').get();
const synchronous = database.prepare('PRAGMA synchronous').get();
return Object.freeze({
profile,
action,
pointName,
crashBeforeCommit: !point.durable,
durableAfterCrash: point.durable,
exactReplay: true,
integrityCheck: Object.values(integrity)[0],
foreignKeyCheck: foreignKey === undefined ? 'ok' : 'failed',
journalMode: journal.journal_mode,
synchronous: synchronous.synchronous,
});
} finally {
await authority.close();
}
}
module.exports = {
CRASH_POINTS,
setupScenario,
verifyScenario,
};
if (require.main === module) {
const [
,
,
command,
databasePath,
markerPath,
pointName,
profile,
action,
] = process.argv;
if (command !== 'crash') {
throw new Error('fixture command must be crash');
}
runCrashScenario({
action,
databasePath,
markerPath,
pointName,
profile,
}).catch((error) => {
process.stderr.write(`${error.stack ?? error}\n`);
process.exitCode = 1;
});
}
@@ -0,0 +1,303 @@
const fs = require('node:fs');
const { DatabaseSync } = require('node:sqlite');
const {
createPluginPackageQuarantineEvent,
} = require('@qinglong/runtime-core/plugin-package-quarantine');
const {
activateInstall,
pluginPackageTaskReconciliationFixture,
} = require('../../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
const {
LocalSqliteOperationAuthority,
} = require('../../dist/authority/operationAuthority');
const {
LocalSqlitePluginPackageInstallRepository,
} = require('../../dist/plugin-package/pluginPackageInstallRepository');
const {
LocalSqlitePluginPackageMaterializedRevisionRepository,
} = require('../../dist/plugin-package/pluginPackageMaterializedRevisionRepository');
const {
LocalSqlitePluginPackageQuarantineRepository,
} = require('../../dist/plugin-package/pluginPackageQuarantineRepository');
const {
LocalSqlitePluginPackageTaskReconciliationRepository,
} = require('../../dist/plugin-package/pluginPackageTaskReconciliationRepository');
const { auditLocalSqliteReadiness } = require('../../dist/readiness/readiness');
const { migrateLocalSqlitePath } = require('../../dist/migration/migration');
const DIGEST_D = 'd'.repeat(64);
const DIGEST_E = 'e'.repeat(64);
const CRASH_POINTS = Object.freeze({
after_task_disable: Object.freeze({
timing: 'afterRun',
sql: 'INSERT INTO "QingLong3TaskDefinitionRevisions"',
durable: false,
}),
after_quarantine_event: Object.freeze({
timing: 'afterRun',
sql: 'INSERT INTO "QingLong3PluginPackageQuarantineEvents"',
durable: false,
}),
after_withdrawal_receipt: Object.freeze({
timing: 'afterRun',
sql: 'INSERT INTO "QingLong3PluginPackageWithdrawalReceipts"',
durable: false,
}),
before_commit: Object.freeze({
timing: 'beforeExec',
sql: 'COMMIT',
durable: false,
}),
after_commit: Object.freeze({
timing: 'afterExec',
sql: 'COMMIT',
durable: true,
}),
});
function fixture(profile) {
return pluginPackageTaskReconciliationFixture(`quarantine-crash-${profile}`, {
profile,
});
}
function event(value) {
const record = value.install.active;
return createPluginPackageQuarantineEvent({
mutationId: `quarantine-crash-${value.profile}`,
revocationReceiptDigest: DIGEST_D,
impactDigest: DIGEST_E,
target: {
projectId: record.projectId,
packageName: record.packageName,
installationId: record.installationId,
lockDigest: record.lockDigest,
installState: record.state,
installVersion: record.version,
installRecordDigest: record.recordDigest,
activeLockDigest: record.activeLockDigest,
},
proposer: { type: 'user', id: 'owner-a' },
confirmer: { type: 'user', id: 'owner-b' },
authorizationMode: 'dual_control',
reasonCode: 'confirmed_key_compromise',
occurredAtMs: record.updatedAtMs + 1,
});
}
function client(databasePath) {
const database = new DatabaseSync(databasePath);
database.exec('PRAGMA foreign_keys = ON');
return database;
}
async function setupScenario({ databasePath, profile }) {
await migrateLocalSqlitePath({ databasePath, profile });
const value = fixture(profile);
const database = client(databasePath);
database
.prepare(
`INSERT INTO "QingLong3Projects"
(id, name, slug, status, version, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, 'active', 1, 1, 1)`,
)
.run(value.projectId, value.projectId, value.projectId);
const authority = new LocalSqliteOperationAuthority(database);
try {
const install = new LocalSqlitePluginPackageInstallRepository(authority);
const materialized =
new LocalSqlitePluginPackageMaterializedRevisionRepository(
authority,
value.registry,
);
const reconciliation =
new LocalSqlitePluginPackageTaskReconciliationRepository(
authority,
value.registry,
);
await activateInstall(install, value);
await materialized.publish(value.revision);
await reconciliation.reconcile(value.revision, {
async findActiveResourceGeneration() {
return value.revision.generation;
},
});
} finally {
await authority.close();
}
}
function writeCrashMarker(markerPath, pointName) {
const descriptor = fs.openSync(markerPath, 'wx', 0o600);
try {
fs.writeSync(
descriptor,
JSON.stringify({
schema: 'qinglong/sqlite-plugin-package-quarantine-crash-marker@v1',
point: pointName,
pid: process.pid,
}),
);
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
function crashClient(database, 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(database, {
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,
markerPath,
pointName,
profile,
}) {
const database = client(databasePath);
const authority = new LocalSqliteOperationAuthority(
crashClient(database, pointName, markerPath),
);
const value = fixture(profile);
await new LocalSqlitePluginPackageQuarantineRepository(authority, {
registry: value.registry,
activeSourceLimit: profile === 'edge' ? 4 : 16,
}).quarantine(event(value), () => {});
throw new Error(`crash point ${pointName} was not reached`);
}
async function verifyScenario({ databasePath, pointName, profile }) {
const point = CRASH_POINTS[pointName];
const value = fixture(profile);
const quarantineEvent = event(value);
const database = client(databasePath);
const authority = new LocalSqliteOperationAuthority(database);
try {
const repository = new LocalSqlitePluginPackageQuarantineRepository(
authority,
{
registry: value.registry,
activeSourceLimit: profile === 'edge' ? 4 : 16,
},
);
const beforeRecovery = await repository.findByEventDigest(
quarantineEvent.eventDigest,
);
if (point.durable !== (beforeRecovery !== null)) {
throw new Error(`${profile}/${pointName} durability is inconsistent`);
}
const recovered = await repository.quarantine(quarantineEvent, () => {});
if (recovered.status !== (point.durable ? 'existing' : 'created')) {
throw new Error(`${profile}/${pointName} replay status is inconsistent`);
}
const taskFacts = database
.prepare(
`SELECT revision.enabled, head.current_revision AS "currentRevision"
FROM "QingLong3TaskDefinitions" AS head
JOIN "QingLong3TaskDefinitionRevisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
AND revision.revision = head.current_revision
WHERE head.project_id = ?
ORDER BY head.task_id`,
)
.all(value.projectId);
if (
taskFacts.length !== 2 ||
taskFacts.some((fact) => fact.enabled !== 0 || fact.currentRevision !== 2)
) {
throw new Error(`${profile}/${pointName} Task withdrawal is incomplete`);
}
await auditLocalSqliteReadiness(database);
const integrity = database.prepare('PRAGMA integrity_check').get();
const foreignKey = database
.prepare('SELECT * FROM pragma_foreign_key_check LIMIT 1')
.get();
const journal = database.prepare('PRAGMA journal_mode').get();
return Object.freeze({
profile,
pointName,
crashBeforeCommit: !point.durable,
durableAfterCrash: point.durable,
integrityCheck: Object.values(integrity)[0],
foreignKeyCheck: foreignKey === undefined ? 'ok' : 'failed',
journalMode: journal.journal_mode,
});
} finally {
await authority.close();
}
}
module.exports = {
CRASH_POINTS,
setupScenario,
verifyScenario,
};
if (require.main === module) {
const [, , action, databasePath, markerPath, pointName, profile] =
process.argv;
if (action !== 'crash') {
throw new Error('fixture action must be crash');
}
runCrashScenario({
databasePath,
markerPath,
pointName,
profile,
}).catch((error) => {
process.stderr.write(`${error.stack ?? error}\n`);
process.exitCode = 1;
});
}
@@ -0,0 +1,488 @@
const fs = require('node:fs');
const { DatabaseSync } = require('node:sqlite');
const { performance } = require('node:perf_hooks');
const {
createInitialPluginPackageAutomationPublication,
} = require('@qinglong/runtime-core/plugin-package-automation-publication');
const {
createPluginPackageWorkflowExecutionPlan,
} = require('@qinglong/runtime-core/plugin-package-workflow-execution-plan');
const {
activateInstall,
pluginPackageTaskReconciliationFixture,
} = require('../../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
const {
LocalSqliteOperationAuthority,
} = require('../../dist/authority/operationAuthority');
const {
LocalSqlitePluginPackageAutomationPublicationRepository,
} = require('../../dist/plugin-package/pluginPackageAutomationPublicationRepository');
const {
LocalSqlitePluginPackageInstallRepository,
} = require('../../dist/plugin-package/pluginPackageInstallRepository');
const {
LocalSqlitePluginPackageMaterializedRevisionRepository,
} = require('../../dist/plugin-package/pluginPackageMaterializedRevisionRepository');
const {
LocalSqlitePluginPackageWorkflowAdmissionRepository,
} = require('../../dist/plugin-package/workflow/pluginPackageWorkflowAdmissionRepository');
const { auditLocalSqliteReadiness } = require('../../dist/readiness/readiness');
const { migrateLocalSqlitePath } = require('../../dist/migration/migration');
const CRASH_POINTS = Object.freeze({
after_run: Object.freeze({
timing: 'afterRun',
sql: 'INSERT INTO "Runs"',
durable: false,
}),
after_admission_event: Object.freeze({
timing: 'afterRun',
sql: 'INSERT INTO "RunEvents"',
durable: false,
}),
after_first_step_run: Object.freeze({
timing: 'afterRun',
sql: 'INSERT INTO "StepRuns"',
durable: false,
}),
after_first_step_mutation: Object.freeze({
timing: 'afterRun',
sql: 'INSERT INTO "StepRunMutations"',
durable: false,
}),
after_admission: Object.freeze({
timing: 'afterRun',
sql: 'INSERT INTO "QingLong3PluginPackageWorkflowAdmissions"',
durable: false,
}),
after_first_admission_step: Object.freeze({
timing: 'afterRun',
sql: 'INSERT INTO "QingLong3PluginPackageWorkflowAdmissionSteps"',
durable: false,
}),
before_commit: Object.freeze({
timing: 'beforeExec',
sql: 'COMMIT',
durable: false,
}),
after_commit: Object.freeze({
timing: 'afterExec',
sql: 'COMMIT',
durable: true,
}),
});
function fixture(profile) {
const value = pluginPackageTaskReconciliationFixture(
`workflow-admission-crash-${profile}`,
{
profile,
workflows: [
{
schema: 'qinglong/plugin-package-workflow-resource@v1',
id: 'daily',
name: 'Daily workflow',
enabled: true,
steps: [
{ id: 'collect', task: 'alpha', needs: [] },
{ id: 'summarize', task: 'beta', needs: ['collect'] },
],
},
],
},
);
return {
...value,
publication: createInitialPluginPackageAutomationPublication(
value.revision,
value.registry,
2_000,
),
};
}
function executionPlan(value) {
return createPluginPackageWorkflowExecutionPlan({
planId: `workflow-admission-crash-plan-${value.profile}`,
runId: `wfa-crash-run-${value.profile}`,
workflowId: 'daily',
stepRunIds: {
collect: `workflow-admission-crash-collect-${value.profile}`,
summarize: `workflow-admission-crash-summary-${value.profile}`,
},
publication: value.publication,
revision: value.revision,
taskSpecSemanticRegistry: value.registry,
plannedAtMs: 3_000,
});
}
function client(databasePath) {
const database = new DatabaseSync(databasePath);
database.exec('PRAGMA foreign_keys = ON');
return database;
}
async function setupScenario({ databasePath, profile }) {
await migrateLocalSqlitePath({ databasePath, profile });
const value = fixture(profile);
const database = client(databasePath);
database
.prepare(
`INSERT INTO "QingLong3Projects"
(id, name, slug, status, version, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, 'active', 1, 1, 1)`,
)
.run(value.projectId, value.projectId, value.projectId);
const authority = new LocalSqliteOperationAuthority(database);
try {
await activateInstall(
new LocalSqlitePluginPackageInstallRepository(authority),
value,
);
await new LocalSqlitePluginPackageMaterializedRevisionRepository(
authority,
value.registry,
).publish(value.revision);
await new LocalSqlitePluginPackageAutomationPublicationRepository(
authority,
).publish(value.publication);
} finally {
await authority.close();
}
}
function writeCrashMarker(markerPath, pointName) {
const descriptor = fs.openSync(markerPath, 'wx', 0o600);
try {
fs.writeSync(
descriptor,
JSON.stringify({
schema:
'qinglong/sqlite-plugin-package-workflow-admission-crash-marker@v1',
point: pointName,
pid: process.pid,
}),
);
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
function crashClient(database, 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(database, {
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,
markerPath,
pointName,
profile,
}) {
const database = client(databasePath);
const authority = new LocalSqliteOperationAuthority(
crashClient(database, pointName, markerPath),
);
const value = fixture(profile);
await new LocalSqlitePluginPackageWorkflowAdmissionRepository(
authority,
).admit(executionPlan(value));
throw new Error(`crash point ${pointName} was not reached`);
}
async function verifyScenario({ databasePath, pointName, profile }) {
const point = CRASH_POINTS[pointName];
const value = fixture(profile);
const plan = executionPlan(value);
const database = client(databasePath);
const authority = new LocalSqliteOperationAuthority(database);
try {
const repository = new LocalSqlitePluginPackageWorkflowAdmissionRepository(
authority,
);
const beforeRecovery = await repository.findByPlanId(plan.planId);
if (point.durable !== (beforeRecovery !== null)) {
throw new Error(`${profile}/${pointName} durability is inconsistent`);
}
const recovered = await repository.admit(plan);
if (recovered.status !== (point.durable ? 'existing' : 'created')) {
throw new Error(`${profile}/${pointName} replay status is inconsistent`);
}
const counts = database
.prepare(
`SELECT
(SELECT COUNT(*) FROM "Runs") AS runs,
(SELECT COUNT(*) FROM "StepRuns") AS steps,
(SELECT COUNT(*) FROM "RunEvents") AS events,
(SELECT COUNT(*) FROM "StepRunMutations") AS mutations,
(SELECT COUNT(*)
FROM "QingLong3PluginPackageWorkflowAdmissions") AS admissions,
(SELECT COUNT(*)
FROM "QingLong3PluginPackageWorkflowAdmissionSteps")
AS admissionSteps`,
)
.get();
if (
counts.runs !== 1 ||
counts.steps !== 2 ||
counts.events !== 3 ||
counts.mutations !== 2 ||
counts.admissions !== 1 ||
counts.admissionSteps !== 2
) {
throw new Error(`${profile}/${pointName} evidence is incomplete`);
}
await auditLocalSqliteReadiness(database);
const integrity = database.prepare('PRAGMA integrity_check').get();
const foreignKey = database
.prepare('SELECT * FROM pragma_foreign_key_check LIMIT 1')
.get();
const journal = database.prepare('PRAGMA journal_mode').get();
const synchronous = database.prepare('PRAGMA synchronous').get();
return Object.freeze({
profile,
pointName,
crashBeforeCommit: !point.durable,
durableAfterCrash: point.durable,
exactReplay:
recovered.status === (point.durable ? 'existing' : 'created'),
integrityCheck: Object.values(integrity)[0],
foreignKeyCheck: foreignKey === undefined ? 'ok' : 'failed',
journalMode: journal.journal_mode,
synchronous: synchronous.synchronous,
});
} finally {
await authority.close();
}
}
function measuredClient(database, measurement) {
let lockStartedAt;
return new Proxy(database, {
get(target, property) {
if (property === 'exec') {
return (sql) => {
const normalized = sql.trim().toUpperCase();
const result = target.exec(sql);
if (normalized === 'BEGIN IMMEDIATE') {
if (lockStartedAt !== undefined) {
throw new Error('nested Workflow admission write lock');
}
measurement.beginImmediateCount += 1;
lockStartedAt = performance.now();
} else if (normalized === 'COMMIT') {
if (lockStartedAt === undefined) {
throw new Error(
'Workflow admission committed without a write lock',
);
}
measurement.commitCount += 1;
measurement.lockDurationsMs.push(performance.now() - lockStartedAt);
lockStartedAt = undefined;
} else if (normalized === 'ROLLBACK') {
measurement.rollbackCount += 1;
lockStartedAt = undefined;
}
return result;
};
}
const value = Reflect.get(target, property, target);
return typeof value === 'function' ? value.bind(target) : value;
},
});
}
function measuredExecutionPlan(value, sequence) {
const suffix = String(sequence).padStart(4, '0');
return createPluginPackageWorkflowExecutionPlan({
planId: `wfl-plan-${value.profile}-${suffix}`,
runId: `wfl-run-${value.profile}-${suffix}`,
workflowId: 'daily',
stepRunIds: {
collect: `wfl-collect-${value.profile}-${suffix}`,
summarize: `wfl-summary-${value.profile}-${suffix}`,
},
publication: value.publication,
revision: value.revision,
taskSpecSemanticRegistry: value.registry,
plannedAtMs: 4_000 + sequence,
});
}
function rounded(value) {
return Math.round(value * 1_000) / 1_000;
}
function percentile(sortedValues, percentileValue) {
const index = Math.min(
sortedValues.length - 1,
Math.ceil((percentileValue / 100) * sortedValues.length) - 1,
);
return sortedValues[Math.max(0, index)];
}
async function measureWorkflowAdmissionTransactions({
databasePath,
profile,
samples,
}) {
if (!Number.isSafeInteger(samples) || samples < 1 || samples > 1_000) {
throw new RangeError('Workflow admission samples are out of range');
}
const value = fixture(profile);
const database = client(databasePath);
const measurement = {
beginImmediateCount: 0,
commitCount: 0,
rollbackCount: 0,
lockDurationsMs: [],
};
const authority = new LocalSqliteOperationAuthority(
measuredClient(database, measurement),
);
try {
const repository = new LocalSqlitePluginPackageWorkflowAdmissionRepository(
authority,
);
for (let sequence = 1; sequence <= samples; sequence += 1) {
const result = await repository.admit(
measuredExecutionPlan(value, sequence),
);
if (result.status !== 'created') {
throw new Error(
`Workflow admission sample ${sequence} was not newly committed`,
);
}
}
const counts = database
.prepare(
`SELECT
(SELECT COUNT(*) FROM "Runs") AS runs,
(SELECT COUNT(*) FROM "StepRuns") AS steps,
(SELECT COUNT(*)
FROM "QingLong3PluginPackageWorkflowAdmissions") AS admissions,
(SELECT COUNT(*)
FROM "QingLong3PluginPackageWorkflowAdmissionSteps")
AS admissionSteps`,
)
.get();
if (
counts.runs !== samples ||
counts.steps !== samples * 2 ||
counts.admissions !== samples ||
counts.admissionSteps !== samples * 2
) {
throw new Error('Workflow admission measurement facts are incomplete');
}
await auditLocalSqliteReadiness(database);
const integrity = database.prepare('PRAGMA integrity_check').get();
const foreignKey = database
.prepare('SELECT * FROM pragma_foreign_key_check LIMIT 1')
.get();
const journal = database.prepare('PRAGMA journal_mode').get();
const synchronous = database.prepare('PRAGMA synchronous').get();
const sorted = [...measurement.lockDurationsMs].sort(
(left, right) => left - right,
);
return Object.freeze({
profile,
samples,
beginImmediateCount: measurement.beginImmediateCount,
commitCount: measurement.commitCount,
rollbackCount: measurement.rollbackCount,
oneWriteTransactionPerWorkflow:
measurement.beginImmediateCount === samples &&
measurement.commitCount === samples &&
measurement.rollbackCount === 0,
lockDurationMs: Object.freeze({
p50: rounded(percentile(sorted, 50)),
p95: rounded(percentile(sorted, 95)),
p99: rounded(percentile(sorted, 99)),
max: rounded(sorted.at(-1)),
}),
integrityCheck: Object.values(integrity)[0],
foreignKeyCheck: foreignKey === undefined ? 'ok' : 'failed',
journalMode: journal.journal_mode,
synchronous: synchronous.synchronous,
});
} finally {
await authority.close();
}
}
module.exports = {
CRASH_POINTS,
executionPlan,
fixture,
measureWorkflowAdmissionTransactions,
setupScenario,
verifyScenario,
};
if (require.main === module) {
const [, , action, databasePath, markerPath, pointName, profile] =
process.argv;
if (action !== 'crash') {
throw new Error('fixture action must be crash');
}
runCrashScenario({
databasePath,
markerPath,
pointName,
profile,
}).catch((error) => {
process.stderr.write(`${error.stack ?? error}\n`);
process.exitCode = 1;
});
}
@@ -0,0 +1,407 @@
const fs = require('node:fs');
const { createHash } = require('node:crypto');
const { DatabaseSync } = require('node:sqlite');
const {
LocalSqliteOperationAuthority,
} = require('../../dist/authority/operationAuthority');
const {
LocalSqlitePluginPackageTaskReconciliationRepository,
} = require('../../dist/plugin-package/pluginPackageTaskReconciliationRepository');
const {
LocalSqlitePluginPackageWorkflowAdmissionRepository,
} = require('../../dist/plugin-package/workflow/pluginPackageWorkflowAdmissionRepository');
const {
LocalSqlitePluginPackageWorkflowTaskAttemptAdmissionRepository,
} = require('../../dist/plugin-package/workflow/pluginPackageWorkflowTaskAttemptAdmissionRepository');
const {
LocalSqlitePluginPackageWorkflowCancellationConvergenceRepository,
} = require('../../dist/plugin-package/workflow/pluginPackageWorkflowCancellationConvergenceRepository');
const { LocalSqliteRunRepository } = require('../../dist/run/runRepository');
const {
LocalSqliteWorkflowTaskExecutionRepository,
} = require('../../dist/plugin-package/workflow/workflowTaskExecutionRepository');
const { auditLocalSqliteReadiness } = require('../../dist/readiness/readiness');
const {
executionPlan,
fixture,
setupScenario: setupWorkflowAdmissionScenario,
} = require('./pluginPackageWorkflowAdmissionCrashMatrixFixture.cjs');
const CRASH_POINTS = Object.freeze({
after_conclusive_stop_before_begin: Object.freeze({
timing: 'beforeExec',
sql: 'BEGIN IMMEDIATE',
durable: false,
}),
after_attempt_terminal: Object.freeze({
timing: 'afterRun',
sql: 'UPDATE "RunAttempts"',
durable: false,
}),
after_run_cas: Object.freeze({
timing: 'afterRun',
sql: 'UPDATE "Runs"',
durable: false,
}),
after_step_terminal: Object.freeze({
timing: 'afterRun',
sql: 'UPDATE "StepRuns"',
durable: false,
}),
after_attempt_event: Object.freeze({
timing: 'afterRun',
sql: 'INSERT INTO "RunEvents"',
durable: false,
}),
after_step_mutation: Object.freeze({
timing: 'afterRun',
sql: 'INSERT INTO "StepRunMutations"',
durable: false,
}),
before_commit: Object.freeze({
timing: 'beforeExec',
sql: 'COMMIT',
durable: false,
}),
after_commit: Object.freeze({
timing: 'afterExec',
sql: 'COMMIT',
durable: true,
}),
});
function client(databasePath) {
const database = new DatabaseSync(databasePath);
database.exec('PRAGMA foreign_keys = ON');
return database;
}
function writeCrashMarker(markerPath, pointName) {
const descriptor = fs.openSync(markerPath, 'wx', 0o600);
try {
fs.writeSync(
descriptor,
JSON.stringify({
schema:
'qinglong/sqlite-plugin-package-workflow-task-control-crash-marker@v1',
point: pointName,
conclusiveStopObserved: true,
pid: process.pid,
}),
);
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
function crashClient(database, 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(database, {
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;
},
});
}
function command(run, attempt, pointName, profile) {
const identity = createHash('sha256')
.update(profile)
.update('\0')
.update(pointName)
.digest('hex')
.slice(0, 16);
return {
run,
attempt,
reason: 'user',
terminalStatus: 'cancelled',
errorCode: 'EXECUTION_CANCELLED',
errorSummary: 'Execution was cancelled',
finishedAtMs: Math.max(
Date.now(),
run.updatedAtMs ?? 0,
attempt.startedAtMs ?? 0,
),
attemptEventId: `wfc-a-${identity}`,
stepMutationId: `wfc-s-${identity}`,
};
}
async function setupScenario({ databasePath, profile }) {
await setupWorkflowAdmissionScenario({ databasePath, profile });
const value = fixture(profile);
const plan = executionPlan(value);
const database = client(databasePath);
const authority = new LocalSqliteOperationAuthority(database);
try {
await new LocalSqlitePluginPackageTaskReconciliationRepository(
authority,
value.registry,
).reconcile(value.revision, {
async findActiveResourceGeneration() {
return value.revision.generation;
},
});
await new LocalSqlitePluginPackageWorkflowAdmissionRepository(
authority,
).admit(plan);
const collect = plan.steps.find(({ stepKey }) => stepKey === 'collect');
if (!collect) throw new Error('Workflow collect Step is missing');
const admitted =
await new LocalSqlitePluginPackageWorkflowTaskAttemptAdmissionRepository(
authority,
).admit(plan.runId, collect.stepRunId);
const runs = new LocalSqliteRunRepository(database);
const execution = new LocalSqliteWorkflowTaskExecutionRepository(authority);
const callbackTokenHash = 'c'.repeat(64);
const startingAtMs = admitted.receipt.admittedAtMs + 1;
const runningAtMs = startingAtMs + 1;
const prepared = await execution.prepare({
runId: plan.runId,
attemptId: admitted.receipt.attemptId,
stepRunId: collect.stepRunId,
callbackTokenHash,
deadlineAtMs: startingAtMs + 60_000,
logArtifactId: 'local-0123456789abcdef0123456789abcd',
atMs: startingAtMs,
eventId: `wfc-start-${profile}`,
});
if (prepared.status !== 'applied') {
throw new Error('Workflow Task did not enter starting');
}
const run = await runs.findRunById(plan.runId);
const attempt = await runs.findAttemptById(admitted.receipt.attemptId);
if (!run || !attempt) {
throw new Error('Workflow Task starting authority is missing');
}
const running = await execution.recordRunning({
run,
attempt,
callbackTokenHash,
executorHandle: `qlp:v1:workflow-control-${profile}`,
pid: 321,
startedAtMs: runningAtMs,
attemptEventId: `wfc-running-a-${profile}`,
stepMutationId: `wfc-running-s-${profile}`,
});
if (running.status !== 'applied') {
throw new Error('Workflow Task did not enter running');
}
const cancellation = database
.prepare(
`UPDATE "Runs"
SET cancel_requested_at_ms = ?,
cancel_reason = 'user'
WHERE id = ? AND status = 'running'
AND cancel_requested_at_ms IS NULL`,
)
.run(runningAtMs + 1, plan.runId);
if (cancellation.changes !== 1) {
throw new Error('Workflow cancellation intent was not recorded');
}
return Object.freeze({
runId: plan.runId,
attemptId: admitted.receipt.attemptId,
stepRunId: collect.stepRunId,
});
} finally {
await authority.close();
}
}
async function runCrashScenario({
databasePath,
markerPath,
pointName,
profile,
}) {
const database = client(databasePath);
const runs = new LocalSqliteRunRepository(database);
const value = fixture(profile);
const plan = executionPlan(value);
const run = await runs.findRunById(plan.runId);
const attempt = await runs.findLatestAttemptByRunId(plan.runId);
if (!run || !attempt) {
throw new Error('Workflow Task control authority is missing');
}
const authority = new LocalSqliteOperationAuthority(
crashClient(database, pointName, markerPath),
);
await new LocalSqliteWorkflowTaskExecutionRepository(
authority,
).recordControlTerminal(command(run, attempt, pointName, profile));
throw new Error(`crash point ${pointName} was not reached`);
}
async function verifyScenario({ databasePath, pointName, profile }) {
const point = CRASH_POINTS[pointName];
if (!point) throw new Error(`unknown crash point ${pointName}`);
const database = client(databasePath);
const authority = new LocalSqliteOperationAuthority(database);
try {
const value = fixture(profile);
const plan = executionPlan(value);
const runs = new LocalSqliteRunRepository(database);
const execution = new LocalSqliteWorkflowTaskExecutionRepository(authority);
let run = await runs.findRunById(plan.runId);
let attempt = await runs.findLatestAttemptByRunId(plan.runId);
if (!run || !attempt) {
throw new Error('Workflow Task recovery authority is missing');
}
if (point.durable !== (attempt.status === 'cancelled')) {
throw new Error(`${profile}/${pointName} durability is inconsistent`);
}
const recovered = await execution.recordControlTerminal(
command(run, attempt, pointName, profile),
);
if (recovered !== (point.durable ? 'already_terminal' : 'terminal')) {
throw new Error(`${profile}/${pointName} replay is inconsistent`);
}
const cancellation =
new LocalSqlitePluginPackageWorkflowCancellationConvergenceRepository(
authority,
);
const converged = await cancellation.convergePage({ limit: 8 });
if (
converged.settledRuns !== 1 ||
converged.settledAttempts !== 0 ||
converged.blocked !== 0
) {
throw new Error(`${profile}/${pointName} parent did not converge`);
}
run = await runs.findRunById(plan.runId);
attempt = await runs.findLatestAttemptByRunId(plan.runId);
const facts = database
.prepare(
`SELECT
(SELECT COUNT(*) FROM "RunAttempts"
WHERE run_id = ?) AS attempts,
(SELECT COUNT(*) FROM "RunEvents"
WHERE run_id = ? AND type =
'workflow.task_attempt.cancelled') AS attemptEvents,
(SELECT COUNT(*) FROM "RunEvents"
WHERE run_id = ? AND type = 'step.cancelled') AS stepEvents,
(SELECT COUNT(*) FROM "RunEvents"
WHERE run_id = ? AND type =
'workflow.cancelled') AS workflowEvents,
(SELECT COUNT(*) FROM "StepRuns"
WHERE run_id = ? AND status = 'cancelled') AS cancelledSteps`,
)
.get(plan.runId, plan.runId, plan.runId, plan.runId, plan.runId);
if (
run?.status !== 'cancelled' ||
run.version !== run.eventSequence ||
attempt?.status !== 'cancelled' ||
facts.attempts !== 1 ||
facts.attemptEvents !== 1 ||
facts.stepEvents !== 2 ||
facts.workflowEvents !== 1 ||
facts.cancelledSteps !== 2
) {
throw new Error(`${profile}/${pointName} terminal facts are incomplete`);
}
const replay = await execution.recordControlTerminal(
command(run, attempt, pointName, profile),
);
if (replay !== 'already_terminal') {
throw new Error(`${profile}/${pointName} terminal replay drifted`);
}
const empty = await cancellation.convergePage({ limit: 8 });
if (
empty.scanned !== 0 ||
empty.settledRuns !== 0 ||
empty.settledAttempts !== 0
) {
throw new Error(`${profile}/${pointName} cancellation replay drifted`);
}
await auditLocalSqliteReadiness(database);
const integrity = database.prepare('PRAGMA integrity_check').get();
const foreignKey = database
.prepare('SELECT * FROM pragma_foreign_key_check LIMIT 1')
.get();
const journal = database.prepare('PRAGMA journal_mode').get();
return Object.freeze({
profile,
pointName,
crashAfterConclusiveStop: true,
crashBeforeCommit: !point.durable,
durableAfterCrash: point.durable,
exactTerminalReplay: replay === 'already_terminal',
parentConverged: run.status === 'cancelled',
integrityCheck: Object.values(integrity)[0],
foreignKeyCheck: foreignKey === undefined ? 'ok' : 'failed',
journalMode: journal.journal_mode,
});
} finally {
await authority.close();
}
}
module.exports = {
CRASH_POINTS,
setupScenario,
verifyScenario,
};
if (require.main === module) {
const [, , action, databasePath, markerPath, pointName, profile] =
process.argv;
if (action !== 'crash') {
throw new Error('fixture action must be crash');
}
runCrashScenario({
databasePath,
markerPath,
pointName,
profile,
}).catch((error) => {
process.stderr.write(`${error.stack ?? error}\n`);
process.exitCode = 1;
});
}
@@ -0,0 +1,957 @@
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,
};
@@ -0,0 +1,150 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
createLocalExecutionContextRecipe,
createLocalTaskExecutionRevision,
} = require('@qinglong/runtime-core/local-dispatch');
const {
RunRepositoryConstraintError,
} = require('@qinglong/runtime-core/run-repository');
const {
migrateLocalSqlitePath,
openLocalSqliteRuntimeDatabase,
} = require('../dist');
async function fixture(t) {
const directory = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-local-dispatch-store-'),
);
const databasePath = path.join(directory, 'qinglong3.sqlite');
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const runtime = await openLocalSqliteRuntimeDatabase({
databasePath,
profile: 'edge',
});
t.after(async () => {
await runtime.close();
fs.rmSync(directory, { recursive: true, force: true });
});
return runtime;
}
function run(id, priority, queuedAtMs, cancelled = false) {
return {
id: `run-${id}`,
projectId: 'default',
taskId: 'task-1',
taskRevision: 'revision-1',
triggerType: 'manual',
executionOrigin: 'manual',
executionOwner: 'runtime',
status: 'queued',
version: 0,
eventSequence: 0,
priority,
createdAtMs: 1,
queuedAtMs,
...(cancelled ? { cancelRequestedAtMs: 2, cancelReason: 'user' } : {}),
};
}
function attempt(id, createdAtMs) {
return {
id: `attempt-${id}`,
runId: `run-${id}`,
attempt: 1,
status: 'claimed',
executorType: 'local_process',
callbackSequence: 0,
createdAtMs,
};
}
test('append-only definitions replay by content and reject identity conflicts', async (t) => {
const runtime = await fixture(t);
const recipe = createLocalExecutionContextRecipe({
environment: [{ name: 'VALUE', kind: 'public', value: 'one' }],
createdAtMs: 1,
});
assert.equal(
await runtime.localDispatch.appendLocalExecutionContextRecipe(recipe),
'inserted',
);
assert.equal(
await runtime.localDispatch.appendLocalExecutionContextRecipe({
...recipe,
createdAtMs: 2,
}),
'existing',
);
const revision = createLocalTaskExecutionRevision({
projectId: 'default',
taskId: 'task-1',
taskRevision: 'revision-1',
executorType: 'local_process',
command: { kind: 'argv', file: '/bin/echo', args: ['one'] },
contextRef: recipe.contextRef,
createdAtMs: 1,
});
assert.equal(
await runtime.localDispatch.appendLocalTaskExecutionRevision(revision),
'inserted',
);
assert.equal(
await runtime.localDispatch.appendLocalTaskExecutionRevision({
...revision,
createdAtMs: 2,
}),
'existing',
);
await assert.rejects(
runtime.localDispatch.appendLocalTaskExecutionRevision(
createLocalTaskExecutionRevision({
...revision,
command: { kind: 'argv', file: '/bin/echo', args: ['different'] },
}),
),
RunRepositoryConstraintError,
);
});
test('candidate pages are bounded, ordered and exclude cancellation intent', async (t) => {
const runtime = await fixture(t);
await runtime.runRepository.transaction(async (transaction) => {
for (const value of [
['a', 10, 2, 2, false],
['b', 10, 1, 3, false],
['c', 5, 1, 1, false],
['cancelled', 100, 1, 1, true],
]) {
await transaction.insertRun(run(value[0], value[1], value[2], value[4]));
await transaction.insertAttempt(attempt(value[0], value[3]));
}
});
const first = await runtime.localDispatch.listLocalDispatchCandidates({
limit: 2,
});
assert.deepEqual(
first.candidates.map(({ runId }) => runId),
['run-b', 'run-a'],
);
assert.equal(first.truncated, true);
const last = first.candidates.at(-1);
const second = await runtime.localDispatch.listLocalDispatchCandidates({
limit: 2,
after: {
priority: last.priority,
queuedAtMs: last.queuedAtMs,
attemptCreatedAtMs: last.attemptCreatedAtMs,
attemptId: last.attemptId,
},
});
assert.deepEqual(
second.candidates.map(({ runId }) => runId),
['run-c'],
);
assert.equal(second.truncated, false);
});
@@ -0,0 +1,80 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
const {
bootstrapLocalProfileStorage,
} = require('../dist/profile/localProfile');
function fixture(t) {
const directory = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-local-profile-'),
);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
return path.join(directory, 'qinglong3.sqlite');
}
test('disabled local Profile does not inspect or create the database path', async () => {
const records = [];
const databasePath = path.join(
os.tmpdir(),
'ql3-local-profile-parent-does-not-exist',
'database.sqlite',
);
const result = await bootstrapLocalProfileStorage({
enabled: false,
profile: 'edge',
databasePath,
audit: (record) => records.push(record),
});
assert.equal(result.status, 'disabled');
assert.equal(await result.stop(), 'stopped');
assert.equal(fs.existsSync(databasePath), false);
assert.deepEqual(records, [{ profile: 'edge', state: 'disabled' }]);
});
test('enabled local Profile owns one ready repository and idempotent stop', async (t) => {
const databasePath = fixture(t);
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const records = [];
const result = await bootstrapLocalProfileStorage({
enabled: true,
profile: 'edge',
databasePath,
audit: (record) => records.push(record),
});
assert.equal(result.status, 'storage_ready');
assert.equal(result.profile, 'edge');
assert.equal(result.evidence.journalMode, 'delete');
assert.equal(await result.runs.findRunById('missing'), null);
assert.deepEqual(await result.startupRecovery.inspectCandidates(), {
candidates: [],
truncated: false,
});
assert.deepEqual(await Promise.all([result.stop(), result.stop()]), [
'stopped',
'stopped',
]);
assert.deepEqual(
records.map(({ state }) => state),
['storage_ready', 'stopped'],
);
});
test('unprepared storage fails closed without auto-migration', async (t) => {
const databasePath = fixture(t);
new (require('node:sqlite').DatabaseSync)(databasePath).close();
const records = [];
await assert.rejects(
bootstrapLocalProfileStorage({
enabled: true,
profile: 'standalone',
databasePath,
audit: (record) => records.push(record),
}),
/not ready/,
);
assert.deepEqual(records, [{ profile: 'standalone', state: 'failed' }]);
});
@@ -0,0 +1,34 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
bootstrapEdgeStorage,
} = require('@qinglong/local-sqlite/profile/edge');
const {
bootstrapStandaloneStorage,
} = require('@qinglong/local-sqlite/profile/standalone');
for (const [profile, bootstrap] of [
['edge', bootstrapEdgeStorage],
['standalone', bootstrapStandaloneStorage],
]) {
test(`${profile} subpath fixes the Profile and leaves disabled storage untouched`, async () => {
const records = [];
const databasePath = path.join(
os.tmpdir(),
`ql3-${profile}-missing`,
'db.sqlite',
);
const result = await bootstrap({
enabled: false,
databasePath,
audit: (record) => records.push(record),
});
assert.equal(result.profile, profile);
assert.equal(result.status, 'disabled');
assert.equal(fs.existsSync(databasePath), false);
assert.deepEqual(records, [{ profile, state: 'disabled' }]);
});
}
@@ -0,0 +1,373 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
REVOKED_API_CREDENTIAL_DIGEST,
} = require('@qinglong/runtime-core/api-credential-administration');
const {
LocalOwnerCredentialRecoveryInProgressError,
LocalOwnerCredentialRecoveryMutationConflictError,
LocalOwnerCredentialRecoveryNotAcknowledgedError,
} = require('@qinglong/runtime-core/local-owner-credential-recovery');
const { openLocalSqliteBootstrapDatabase } = require('../dist/storage/bootstrap');
const { migrateLocalSqlitePath } = require('../dist/migration/migration');
const SUBJECT_ID = `usr_${'a'.repeat(22)}`;
const PREVIOUS_CREDENTIAL_ID = `own_${'b'.repeat(22)}`;
const PEPPER_KEY_ID = 'owner-key-1';
function fixture(t) {
const directory = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-owner-recovery-'),
);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
return path.join(directory, 'qinglong3.sqlite');
}
async function prepared(t) {
const databasePath = fixture(t);
const options = { databasePath, profile: 'edge' };
await migrateLocalSqlitePath(options);
const client = new DatabaseSync(databasePath);
client.exec('PRAGMA foreign_keys = ON');
client
.prepare(
`INSERT INTO "QingLong3IdentitySubjects" (
subject_type, subject_id, status, version, created_at_ms, updated_at_ms
) VALUES ('user', ?, 'active', 1, 100, 100)`,
)
.run(SUBJECT_ID);
client
.prepare(
`INSERT INTO "QingLong3LocalOwnerPepperKeys" (
pepper_key_id, material_digest, backup_digest, state, version,
register_mutation_id, activate_mutation_id, registered_at_ms,
activated_at_ms
) VALUES (?, ?, ?, 'active', 2, ?, ?, 90, 95)`,
)
.run(
PEPPER_KEY_ID,
'1'.repeat(64),
'2'.repeat(64),
'00000000-0000-4000-8000-000000000091',
'00000000-0000-4000-8000-000000000092',
);
client
.prepare(
`INSERT INTO "QingLong3LocalOwnerPepperActivations" (
generation, mutation_id, expected_generation, previous_pepper_key_id,
active_pepper_key_id, material_digest, backup_digest, activated_at_ms
) VALUES (1, ?, 0, NULL, ?, ?, ?, 95)`,
)
.run(
'00000000-0000-4000-8000-000000000092',
PEPPER_KEY_ID,
'1'.repeat(64),
'2'.repeat(64),
);
client
.prepare(
`INSERT INTO "QingLong3ApiCredentials" (
credential_id, version, state, subject_type, subject_id,
secret_digest, created_at_ms, not_before_at_ms, expires_at_ms
) VALUES (?, 1, 'active', 'user', ?, ?, 100, 100, 100000)`,
)
.run(PREVIOUS_CREDENTIAL_ID, SUBJECT_ID, '3'.repeat(64));
client
.prepare(
`INSERT INTO "QingLong3ApiCredentialPepperBindings" (
credential_id, credential_version, pepper_key_id
) VALUES (?, 1, ?)`,
)
.run(PREVIOUS_CREDENTIAL_ID, PEPPER_KEY_ID);
client.close();
return { options, databasePath };
}
function audit(eventId, requestId, operationId, occurredAtMs) {
return {
eventId,
requestId,
operationId,
projectId: null,
subject: { type: 'system', id: 'owner-credential-recovery' },
authenticationId: 'local-owner-console',
outcome: 'allowed',
reasons: ['credential_recovery'],
fence: null,
occurredAtMs,
};
}
function issueCommand(index = 1) {
const mutationId = `00000000-0000-4000-8000-0000000001${String(
index,
).padStart(2, '0')}`;
const requestId = `recover-issue-${index}`;
const replacementCredential = {
credentialId: `own_${String(index).repeat(22)}`,
version: 1,
pepperKeyId: PEPPER_KEY_ID,
state: 'active',
subject: { type: 'user', id: SUBJECT_ID },
subjectStatus: 'active',
secretDigest: String(index).repeat(64),
createdAtMs: 1000 + index,
notBeforeAtMs: 1000 + index,
expiresAtMs: 10000 + index,
};
return {
mutationId,
requestId,
previousCredentialId: PREVIOUS_CREDENTIAL_ID,
expectedPreviousVersion: 1,
replacementCredential,
mutation: {
mutationId,
operation: 'issue',
credentialId: replacementCredential.credentialId,
credentialVersion: 1,
expectedPreviousVersion: 0,
changedBy: { type: 'system', id: 'owner-credential-recovery' },
createdAtMs: replacementCredential.createdAtMs,
},
audit: audit(
mutationId,
requestId,
'credential.issue',
replacementCredential.createdAtMs,
),
};
}
function acknowledgement(command, overrides = {}) {
return {
issueMutationId: command.mutationId,
requestId: command.requestId,
credentialId: command.replacementCredential.credentialId,
factDigest: command.replacementCredential.secretDigest,
deliveryDigest: 'd'.repeat(64),
acknowledgedAtMs: 1100,
...overrides,
};
}
function completeCommand(issue, index = 1) {
const mutationId = `00000000-0000-4000-8000-0000000002${String(
index,
).padStart(2, '0')}`;
const requestId = `recover-complete-${index}`;
const revokedCredential = {
credentialId: PREVIOUS_CREDENTIAL_ID,
version: 2,
pepperKeyId: PEPPER_KEY_ID,
state: 'revoked',
subject: { type: 'user', id: SUBJECT_ID },
subjectStatus: 'active',
secretDigest: REVOKED_API_CREDENTIAL_DIGEST,
createdAtMs: 1200,
notBeforeAtMs: 1200,
expiresAtMs: 1201,
};
return {
issueMutationId: issue.mutationId,
mutationId,
requestId,
expectedPreviousVersion: 1,
revokedCredential,
mutation: {
mutationId,
operation: 'revoke',
credentialId: PREVIOUS_CREDENTIAL_ID,
credentialVersion: 2,
expectedPreviousVersion: 1,
changedBy: { type: 'system', id: 'owner-credential-recovery' },
createdAtMs: 1200,
},
audit: audit(mutationId, requestId, 'credential.revoke', 1200),
};
}
test('keeps the old credential active until exact delivery acknowledgement', async (t) => {
const { options } = await prepared(t);
const database = await openLocalSqliteBootstrapDatabase(options);
t.after(() => database.close());
const issue = issueCommand();
assert.equal(
(await database.ownerCredentialRecovery.issue(issue)).status,
'inserted',
);
assert.equal(
(await database.ownerCredentialRecovery.issue(issue)).status,
'existing',
);
assert.equal(
(await database.apiCredentials.resolve(PREVIOUS_CREDENTIAL_ID)).state,
'active',
);
await assert.rejects(
database.ownerCredentialRecovery.complete(completeCommand(issue)),
LocalOwnerCredentialRecoveryNotAcknowledgedError,
);
assert.equal(
(await database.apiCredentials.resolve(PREVIOUS_CREDENTIAL_ID)).state,
'active',
);
const ack = acknowledgement(issue);
assert.equal(
(await database.ownerCredentialRecovery.acknowledge(ack)).recovery.state,
'acknowledged',
);
assert.equal(
(await database.ownerCredentialRecovery.acknowledge(ack)).status,
'existing',
);
assert.equal(
(await database.apiCredentials.resolve(PREVIOUS_CREDENTIAL_ID)).state,
'active',
);
const completed = await database.ownerCredentialRecovery.complete(
completeCommand(issue),
);
assert.equal(completed.recovery.state, 'completed');
assert.equal(
(await database.apiCredentials.resolve(PREVIOUS_CREDENTIAL_ID)).state,
'revoked',
);
assert.equal(
(
await database.apiCredentials.resolve(
issue.replacementCredential.credentialId,
)
).state,
'active',
);
assert.equal(
(await database.ownerPepper.inspectReferences(PEPPER_KEY_ID, 1300))
.currentCredentialReferences,
1,
);
await database.ownerPepper.register({
mutationId: '00000000-0000-4000-8000-000000000301',
pepperKeyId: 'owner-key-2',
materialDigest: '4'.repeat(64),
backupDigest: '5'.repeat(64),
registeredAtMs: 1301,
});
await database.ownerPepper.activate({
mutationId: '00000000-0000-4000-8000-000000000302',
pepperKeyId: 'owner-key-2',
expectedGeneration: 1,
expectedActivePepperKeyId: PEPPER_KEY_ID,
activatedAtMs: 1302,
});
assert.deepEqual(
await database.ownerPepper.inspectReferences(PEPPER_KEY_ID, 20000),
{
pepperKeyId: PEPPER_KEY_ID,
inspectedAtMs: 20000,
currentCredentialReferences: 0,
inFlightRecoveryReferences: 0,
historicalCredentialReferences: 3,
runtimeReferencesClear: true,
},
);
});
test('serializes concurrent recovery and fails closed on acknowledgement drift', async (t) => {
const { options } = await prepared(t);
const first = await openLocalSqliteBootstrapDatabase(options);
const second = await openLocalSqliteBootstrapDatabase(options);
t.after(() => Promise.all([first.close(), second.close()]));
const candidates = [issueCommand(1), issueCommand(2)];
const settled = await Promise.allSettled([
first.ownerCredentialRecovery.issue(candidates[0]),
second.ownerCredentialRecovery.issue(candidates[1]),
]);
assert.equal(
settled.filter(({ status }) => status === 'fulfilled').length,
1,
);
assert.ok(
settled.find(({ status }) => status === 'rejected').reason instanceof
LocalOwnerCredentialRecoveryInProgressError,
);
const winnerIndex = settled.findIndex(({ status }) => status === 'fulfilled');
const winner = candidates[winnerIndex];
await assert.rejects(
first.ownerCredentialRecovery.acknowledge(
acknowledgement(winner, {
deliveryDigest: 'e'.repeat(64),
factDigest: 'f'.repeat(64),
}),
),
LocalOwnerCredentialRecoveryMutationConflictError,
);
assert.equal(
(await first.apiCredentials.resolve(PREVIOUS_CREDENTIAL_ID)).state,
'active',
);
});
test('keeps future active credentials as runtime pepper references', async (t) => {
const { options, databasePath } = await prepared(t);
const futureCredentialId = `own_${'f'.repeat(22)}`;
const client = new DatabaseSync(databasePath);
client.exec('PRAGMA foreign_keys = ON');
client
.prepare(
`INSERT INTO "QingLong3ApiCredentials" (
credential_id, version, state, subject_type, subject_id,
secret_digest, created_at_ms, not_before_at_ms, expires_at_ms
) VALUES (?, 1, 'active', 'user', ?, ?, 200, 300000, 400000)`,
)
.run(futureCredentialId, SUBJECT_ID, '6'.repeat(64));
client
.prepare(
`INSERT INTO "QingLong3ApiCredentialPepperBindings" (
credential_id, credential_version, pepper_key_id
) VALUES (?, 1, ?)`,
)
.run(futureCredentialId, PEPPER_KEY_ID);
client.close();
const database = await openLocalSqliteBootstrapDatabase(options);
t.after(() => database.close());
await database.ownerPepper.register({
mutationId: '00000000-0000-4000-8000-000000000401',
pepperKeyId: 'owner-key-2',
materialDigest: '7'.repeat(64),
backupDigest: '8'.repeat(64),
registeredAtMs: 300,
});
await database.ownerPepper.activate({
mutationId: '00000000-0000-4000-8000-000000000402',
pepperKeyId: 'owner-key-2',
expectedGeneration: 1,
expectedActivePepperKeyId: PEPPER_KEY_ID,
activatedAtMs: 301,
});
assert.deepEqual(
await database.ownerPepper.inspectReferences(PEPPER_KEY_ID, 200000),
{
pepperKeyId: PEPPER_KEY_ID,
inspectedAtMs: 200000,
currentCredentialReferences: 1,
inFlightRecoveryReferences: 0,
historicalCredentialReferences: 2,
runtimeReferencesClear: false,
},
);
assert.equal(
(await database.ownerPepper.inspectReferences(PEPPER_KEY_ID, 500000))
.runtimeReferencesClear,
true,
);
});
@@ -0,0 +1,225 @@
const assert = require('node:assert/strict');
const { randomUUID } = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
LocalOwnerDeliveryAcknowledgementGcMutationConflictError,
LocalOwnerDeliveryAcknowledgementGcReferenceConflictError,
MIN_LOCAL_OWNER_ACKNOWLEDGEMENT_AUDIT_RETENTION_MS,
MIN_LOCAL_OWNER_ACKNOWLEDGEMENT_REPLAY_RETENTION_MS,
} = require('@qinglong/runtime-core/local-owner-delivery-acknowledgement-gc');
const {
openLocalSqliteAcknowledgementGcDatabase,
} = require('../dist/maintenance/acknowledgementGc');
const { openLocalSqliteBootstrapDatabase } = require('../dist/storage/bootstrap');
const { migrateLocalSqlitePath } = require('../dist/migration/migration');
const NOW = 1_760_000_000_000;
const CREDENTIAL_TTL_MS = 600_000;
const SUBJECT_ID = `usr_${Buffer.alloc(16, 31).toString('base64url')}`;
const CREDENTIAL_ID = `own_${Buffer.alloc(16, 32).toString('base64url')}`;
const ACK_MUTATION_ID = '00000000-0000-4000-8000-000000000d01';
const DELIVERY_DIGEST = 'd'.repeat(64);
function fixture(t) {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-ack-gc-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
return {
databasePath: path.join(directory, 'qinglong3.sqlite'),
profile: 'edge',
};
}
function issuer() {
return {
subject: { type: 'system', id: 'owner-bootstrap' },
authenticationId: 'local-console-test',
authenticatedAtMs: NOW - 1_000,
expiresAtMs: NOW + 60_000,
assurance: 'local_console',
};
}
function gcCommand(compactedAtMs, overrides = {}) {
const mutationId = overrides.mutationId ?? randomUUID();
const requestId = overrides.requestId ?? `ack-gc-${mutationId}`;
return {
mutationId,
requestId,
acknowledgementMutationId: ACK_MUTATION_ID,
expectedKind: 'credential',
expectedDeliveryDigest: DELIVERY_DIGEST,
bridgeClearEvidence: {
kind: 'credential',
acknowledgementMutationId: ACK_MUTATION_ID,
inspectedAtMs: compactedAtMs,
evidenceDigest: 'e'.repeat(64),
},
retentionPolicy: {
version: 1,
replayRetentionMs: MIN_LOCAL_OWNER_ACKNOWLEDGEMENT_REPLAY_RETENTION_MS,
auditRetentionMs: MIN_LOCAL_OWNER_ACKNOWLEDGEMENT_AUDIT_RETENTION_MS,
},
compactedAtMs,
audit: {
eventId: mutationId,
requestId,
operationId: 'owner.delivery_acknowledgement.gc',
projectId: null,
subject: { type: 'system', id: 'owner-acknowledgement-gc' },
authenticationId: 'local-owner-console',
outcome: 'allowed',
reasons: ['delivery_acknowledgement_gc'],
fence: null,
occurredAtMs: compactedAtMs,
},
};
}
async function readyAcknowledgement(t) {
const options = fixture(t);
await migrateLocalSqlitePath(options);
const database = await openLocalSqliteBootstrapDatabase(options);
await database.ownerPepper.register({
mutationId: '00000000-0000-4000-8000-000000000d91',
pepperKeyId: 'legacy-v1',
materialDigest: 'a'.repeat(64),
backupDigest: 'b'.repeat(64),
registeredAtMs: NOW - 2_000,
});
await database.ownerPepper.activate({
mutationId: '00000000-0000-4000-8000-000000000d92',
pepperKeyId: 'legacy-v1',
expectedGeneration: 0,
activatedAtMs: NOW - 1_000,
});
await database.ownerBootstrap.provision({
mutationId: ACK_MUTATION_ID,
requestId: 'provision-d01',
identity: {
subject: { type: 'user', id: SUBJECT_ID },
status: 'active',
version: 1,
createdAtMs: NOW,
updatedAtMs: NOW,
},
credential: {
credentialId: CREDENTIAL_ID,
version: 1,
pepperKeyId: 'legacy-v1',
state: 'active',
subject: { type: 'user', id: SUBJECT_ID },
subjectStatus: 'active',
secretDigest: 'c'.repeat(64),
createdAtMs: NOW,
notBeforeAtMs: NOW,
expiresAtMs: NOW + CREDENTIAL_TTL_MS,
},
issuer: issuer(),
audit: {
eventId: ACK_MUTATION_ID,
requestId: 'provision-d01',
operationId: 'identity.bootstrap_provision',
projectId: null,
subject: issuer().subject,
authenticationId: issuer().authenticationId,
outcome: 'allowed',
reasons: ['local_console_provisioning'],
fence: null,
occurredAtMs: NOW,
},
createdAtMs: NOW,
});
const acknowledgement = {
kind: 'credential',
mutationId: ACK_MUTATION_ID,
requestId: 'provision-d01',
subjectId: SUBJECT_ID,
credentialId: CREDENTIAL_ID,
factDigest: 'c'.repeat(64),
deliveryDigest: DELIVERY_DIGEST,
ttlMs: CREDENTIAL_TTL_MS,
acknowledgedAtMs: NOW + 1,
};
await database.ownerBootstrap.recordDeliveryAcknowledgement(acknowledgement);
await database.close();
return { options, acknowledgement };
}
test('compacts one expired acknowledgement and reconstructs exact replay', async (t) => {
const { options, acknowledgement } = await readyAcknowledgement(t);
const compactedAtMs =
NOW + MIN_LOCAL_OWNER_ACKNOWLEDGEMENT_AUDIT_RETENTION_MS + 1_000;
const first = await openLocalSqliteAcknowledgementGcDatabase(options);
const second = await openLocalSqliteAcknowledgementGcDatabase(options);
t.after(() => Promise.all([first.close(), second.close()]));
const command = gcCommand(compactedAtMs);
const results = await Promise.all([
first.acknowledgementGc.compact(command),
second.acknowledgementGc.compact(command),
]);
assert.deepEqual(results.map((result) => result.status).sort(), [
'existing',
'inserted',
]);
assert.deepEqual(
await first.ownerBootstrap.resolveDeliveryAcknowledgement(ACK_MUTATION_ID),
acknowledgement,
);
assert.equal(
(await first.acknowledgementGc.resolveByAcknowledgement(ACK_MUTATION_ID))
.acknowledgementSemanticDigest.length,
64,
);
const client = new DatabaseSync(options.databasePath, { readOnly: true });
assert.equal(
client
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3LocalOwnerDeliveryAcknowledgements"`,
)
.get().count,
0,
);
assert.equal(
client
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3LocalOwnerDeliveryAcknowledgementGc"`,
)
.get().count,
1,
);
client.close();
await assert.rejects(
first.acknowledgementGc.compact(
gcCommand(compactedAtMs, { mutationId: randomUUID() }),
),
LocalOwnerDeliveryAcknowledgementGcMutationConflictError,
);
});
test('rejects compaction while the credential is still active', async (t) => {
const { options } = await readyAcknowledgement(t);
const database = await openLocalSqliteAcknowledgementGcDatabase(options);
t.after(() => database.close());
await assert.rejects(
database.acknowledgementGc.compact(gcCommand(NOW + 2)),
LocalOwnerDeliveryAcknowledgementGcReferenceConflictError,
);
assert.equal(
await database.acknowledgementGc.resolveByAcknowledgement(ACK_MUTATION_ID),
null,
);
assert.equal(
(
await database.ownerBootstrap.resolveDeliveryAcknowledgement(
ACK_MUTATION_ID,
)
).deliveryDigest,
DELIVERY_DIGEST,
);
});
@@ -0,0 +1,257 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
LocalOwnerPepperMaterialGcInProgressError,
LocalOwnerPepperMaterialGcMutationConflictError,
LocalOwnerPepperMaterialGcReferenceConflictError,
LocalOwnerPepperMaterialGcRetentionPendingError,
MIN_LOCAL_OWNER_PEPPER_ACK_RETENTION_MS,
MIN_LOCAL_OWNER_PEPPER_AUDIT_RETENTION_MS,
MIN_LOCAL_OWNER_PEPPER_BACKUP_RETENTION_MS,
} = require('@qinglong/runtime-core/local-owner-pepper-material-gc');
const { openLocalSqlitePepperGcDatabase } = require('../dist/maintenance/pepperGc');
const { migrateLocalSqlitePath } = require('../dist/migration/migration');
const RETIRED_KEY_ID = 'owner-key-retired';
const ACTIVE_KEY_ID = 'owner-key-active';
const RETIRED_DIGEST = '1'.repeat(64);
const ACTIVE_DIGEST = '2'.repeat(64);
const PREPARED_AT_MS = 3_000_000_000;
function fixture(t) {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-pepper-gc-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
return path.join(directory, 'qinglong3.sqlite');
}
async function preparedDatabase(t, futureReference = false) {
const databasePath = fixture(t);
const options = { databasePath, profile: 'edge' };
await migrateLocalSqlitePath(options);
const client = new DatabaseSync(databasePath);
client.exec('PRAGMA foreign_keys = ON');
client
.prepare(
`INSERT INTO "QingLong3LocalOwnerPepperKeys" (
pepper_key_id, material_digest, backup_digest, state, version,
register_mutation_id, activate_mutation_id, retire_mutation_id,
registered_at_ms, activated_at_ms, retired_at_ms
) VALUES (?, ?, ?, 'retired', 3, ?, ?, ?, 10, 50, 100)`,
)
.run(
RETIRED_KEY_ID,
RETIRED_DIGEST,
RETIRED_DIGEST,
'00000000-0000-4000-8000-000000000011',
'00000000-0000-4000-8000-000000000012',
'00000000-0000-4000-8000-000000000014',
);
client
.prepare(
`INSERT INTO "QingLong3LocalOwnerPepperKeys" (
pepper_key_id, material_digest, backup_digest, state, version,
register_mutation_id, activate_mutation_id, registered_at_ms,
activated_at_ms
) VALUES (?, ?, ?, 'active', 2, ?, ?, 60, 100)`,
)
.run(
ACTIVE_KEY_ID,
ACTIVE_DIGEST,
ACTIVE_DIGEST,
'00000000-0000-4000-8000-000000000021',
'00000000-0000-4000-8000-000000000022',
);
client
.prepare(
`INSERT INTO "QingLong3LocalOwnerPepperActivations" (
generation, mutation_id, expected_generation,
previous_pepper_key_id, active_pepper_key_id,
material_digest, backup_digest, activated_at_ms
) VALUES (1, ?, 0, NULL, ?, ?, ?, 50)`,
)
.run(
'00000000-0000-4000-8000-000000000012',
RETIRED_KEY_ID,
RETIRED_DIGEST,
RETIRED_DIGEST,
);
client
.prepare(
`INSERT INTO "QingLong3LocalOwnerPepperActivations" (
generation, mutation_id, expected_generation,
previous_pepper_key_id, active_pepper_key_id,
material_digest, backup_digest, activated_at_ms
) VALUES (2, ?, 1, ?, ?, ?, ?, 100)`,
)
.run(
'00000000-0000-4000-8000-000000000022',
RETIRED_KEY_ID,
ACTIVE_KEY_ID,
ACTIVE_DIGEST,
ACTIVE_DIGEST,
);
if (futureReference) {
const subjectId = `usr_${'g'.repeat(22)}`;
const credentialId = `own_${'h'.repeat(22)}`;
client
.prepare(
`INSERT INTO "QingLong3IdentitySubjects" (
subject_type, subject_id, status, version, created_at_ms, updated_at_ms
) VALUES ('user', ?, 'active', 1, 200, 200)`,
)
.run(subjectId);
client
.prepare(
`INSERT INTO "QingLong3ApiCredentials" (
credential_id, version, state, subject_type, subject_id,
secret_digest, created_at_ms, not_before_at_ms, expires_at_ms
) VALUES (?, 1, 'active', 'user', ?, ?, 200, 3500000000, 4000000000)`,
)
.run(credentialId, subjectId, '3'.repeat(64));
client
.prepare(
`INSERT INTO "QingLong3ApiCredentialPepperBindings" (
credential_id, credential_version, pepper_key_id
) VALUES (?, 1, ?)`,
)
.run(credentialId, RETIRED_KEY_ID);
}
client.close();
return options;
}
function policy() {
return {
version: 1,
acknowledgementRetentionMs: MIN_LOCAL_OWNER_PEPPER_ACK_RETENTION_MS,
auditRetentionMs: MIN_LOCAL_OWNER_PEPPER_AUDIT_RETENTION_MS,
backupRetentionMs: MIN_LOCAL_OWNER_PEPPER_BACKUP_RETENTION_MS,
};
}
function audit(mutationId, requestId, operation, occurredAtMs) {
return {
eventId: mutationId,
requestId,
operationId: `owner.pepper.material_gc.${operation}`,
projectId: null,
subject: { type: 'system', id: 'owner-pepper-gc' },
authenticationId: 'local-owner-console',
outcome: 'allowed',
reasons: ['pepper_material_gc'],
fence: null,
occurredAtMs,
};
}
function prepareCommand(index = 1, preparedAtMs = PREPARED_AT_MS) {
const mutationId = `00000000-0000-4000-8000-0000000006${String(
index,
).padStart(2, '0')}`;
const requestId = `pepper-gc-prepare-${index}`;
return {
mutationId,
requestId,
pepperKeyId: RETIRED_KEY_ID,
expectedMaterialDigest: RETIRED_DIGEST,
expectedBackupMaterialDigest: RETIRED_DIGEST,
expectedActivePepperKeyId: ACTIVE_KEY_ID,
expectedActiveGeneration: 2,
expectedActiveMaterialDigest: ACTIVE_DIGEST,
retentionPolicy: policy(),
preparedAtMs,
audit: audit(mutationId, requestId, 'prepare', preparedAtMs),
};
}
function completeCommand(prepare) {
const mutationId = '00000000-0000-4000-8000-000000000701';
const requestId = 'pepper-gc-complete-1';
const completedAtMs = PREPARED_AT_MS + 1;
return {
prepareMutationId: prepare.mutationId,
mutationId,
requestId,
destructionProofDigest: '4'.repeat(64),
completedAtMs,
audit: audit(mutationId, requestId, 'complete', completedAtMs),
};
}
test('prepares and completes one exact idempotent GC ledger', async (t) => {
const options = await preparedDatabase(t);
const database = await openLocalSqlitePepperGcDatabase(options);
t.after(() => database.close());
const prepare = prepareCommand();
const inserted = await database.materialGc.prepare(prepare);
assert.equal(inserted.status, 'inserted');
assert.equal(inserted.record.state, 'prepared');
assert.equal(
inserted.record.retentionEligibleAtMs,
100 + MIN_LOCAL_OWNER_PEPPER_BACKUP_RETENTION_MS,
);
assert.equal((await database.materialGc.prepare(prepare)).status, 'existing');
const complete = completeCommand(prepare);
assert.equal(
(await database.materialGc.complete(complete)).record.state,
'completed',
);
assert.equal(
(await database.materialGc.complete(complete)).status,
'existing',
);
});
test('rejects retention, future references and concurrent open GC', async (t) => {
const retentionOptions = await preparedDatabase(t);
const retentionDatabase = await openLocalSqlitePepperGcDatabase(
retentionOptions,
);
t.after(() => retentionDatabase.close());
await assert.rejects(
retentionDatabase.materialGc.prepare(prepareCommand(1, 1_000)),
LocalOwnerPepperMaterialGcRetentionPendingError,
);
const referenceOptions = await preparedDatabase(t, true);
const referenceDatabase = await openLocalSqlitePepperGcDatabase(
referenceOptions,
);
t.after(() => referenceDatabase.close());
await assert.rejects(
referenceDatabase.materialGc.prepare(prepareCommand()),
LocalOwnerPepperMaterialGcReferenceConflictError,
);
const concurrentOptions = await preparedDatabase(t);
const first = await openLocalSqlitePepperGcDatabase(concurrentOptions);
const second = await openLocalSqlitePepperGcDatabase(concurrentOptions);
t.after(() => Promise.all([first.close(), second.close()]));
await first.materialGc.prepare(prepareCommand(1));
await assert.rejects(
second.materialGc.prepare(prepareCommand(2)),
LocalOwnerPepperMaterialGcInProgressError,
);
});
test('rejects a backup digest that is not bound to the retired catalog row', async (t) => {
const options = await preparedDatabase(t);
const database = await openLocalSqlitePepperGcDatabase(options);
t.after(() => database.close());
await assert.rejects(
database.materialGc.prepare({
...prepareCommand(),
expectedBackupMaterialDigest: '9'.repeat(64),
}),
LocalOwnerPepperMaterialGcMutationConflictError,
);
assert.equal(
await database.materialGc.resolve(prepareCommand().mutationId),
null,
);
});
@@ -0,0 +1,106 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
LocalOwnerPepperCatalogFullError,
LocalOwnerPepperGenerationConflictError,
LocalOwnerPepperMutationConflictError,
} = require('@qinglong/runtime-core/local-owner-pepper');
const {
openLocalSqliteBootstrapDatabase,
} = require('../dist/storage/bootstrap');
const { migrateLocalSqlitePath } = require('../dist/migration/migration');
function fixture(t) {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-pepper-catalog-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
return path.join(directory, 'qinglong3.sqlite');
}
function registration(index, overrides = {}) {
return {
mutationId: `018f4f58-7d5a-4d82-8f7d-5da12f05c0${String(index).padStart(2, '0')}`,
pepperKeyId: `owner-key-${index}`,
materialDigest: index.toString(16).padStart(64, '0'),
backupDigest: (index + 16).toString(16).padStart(64, '0'),
registeredAtMs: 100 + index,
...overrides,
};
}
test('registers, activates and rotates with one append-only generation winner', async (t) => {
const databasePath = fixture(t);
const options = { databasePath, profile: 'edge' };
await migrateLocalSqlitePath(options);
const first = await openLocalSqliteBootstrapDatabase(options);
const second = await openLocalSqliteBootstrapDatabase(options);
t.after(() => Promise.all([first.close(), second.close()]));
const key1 = registration(1);
assert.equal((await first.ownerPepper.register(key1)).status, 'inserted');
assert.equal((await second.ownerPepper.register(key1)).status, 'existing');
const activation1 = {
mutationId: '018f4f58-7d5a-4d82-8f7d-5da12f05d001',
pepperKeyId: key1.pepperKeyId,
expectedGeneration: 0,
activatedAtMs: 200,
};
assert.equal(
(await first.ownerPepper.activate(activation1)).activation.generation,
1,
);
assert.equal(
(await second.ownerPepper.activate(activation1)).status,
'existing',
);
const key2 = registration(2);
const key3 = registration(3);
await first.ownerPepper.register(key2);
await first.ownerPepper.register(key3);
const contenders = [key2, key3].map((key, index) =>
[first, second][index].ownerPepper.activate({
mutationId: `018f4f58-7d5a-4d82-8f7d-5da12f05d00${index + 2}`,
pepperKeyId: key.pepperKeyId,
expectedGeneration: 1,
expectedActivePepperKeyId: key1.pepperKeyId,
activatedAtMs: 300 + index,
}),
);
const settled = await Promise.allSettled(contenders);
assert.equal(settled.filter(({ status }) => status === 'fulfilled').length, 1);
const rejection = settled.find(({ status }) => status === 'rejected');
assert.ok(rejection.reason instanceof LocalOwnerPepperGenerationConflictError);
const active = await first.ownerPepper.resolveActive();
assert.equal(active.generation, 2);
assert.equal((await first.ownerPepper.resolveKey(key1.pepperKeyId)).state, 'retired');
assert.equal(
(await first.ownerPepper.resolveKey(active.activePepperKeyId)).state,
'active',
);
});
test('rejects semantic mutation drift and caps the catalog at eight keys', async (t) => {
const databasePath = fixture(t);
const options = { databasePath, profile: 'standalone' };
await migrateLocalSqlitePath(options);
const database = await openLocalSqliteBootstrapDatabase(options);
t.after(() => database.close());
const first = registration(1);
await database.ownerPepper.register(first);
await assert.rejects(
database.ownerPepper.register({ ...first, backupDigest: 'f'.repeat(64) }),
LocalOwnerPepperMutationConflictError,
);
for (let index = 2; index <= 8; index += 1) {
await database.ownerPepper.register(registration(index));
}
await assert.rejects(
database.ownerPepper.register(registration(9)),
LocalOwnerPepperCatalogFullError,
);
});
@@ -0,0 +1,423 @@
const assert = require('node:assert/strict');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
ApprovalPolicyFenceConflictError,
createApprovalRequest,
} = require('@qinglong/runtime-core/approved-action');
const {
PLUGIN_PACKAGE_INSTALL_ACTION_TYPE,
PluginPackageAdmissionBindingConflictError,
PluginPackageAdmissionReceiptConflictError,
} = require('@qinglong/runtime-core/plugin-package-admission');
const {
createPluginPackageInstallProposal,
resolvePluginPackageInstallProposal,
} = require('@qinglong/runtime-core/plugin-package-proposal');
const {
pluginPackageInstallActionDigest,
pluginPackageInstallPlanDigest,
} = require('@qinglong/runtime-core/plugin-package-install');
const {
PLUGIN_PACKAGE_API_VERSION,
PLUGIN_PACKAGE_KIND,
planPluginPackageInstall,
} = require('@qinglong/runtime-core/plugin-package');
const {
LocalSqliteApprovalRequestRepository,
} = require('@qinglong/local-sqlite/approved-action');
const {
LocalSqliteApprovedActionExecutionRepository,
} = require('@qinglong/local-sqlite/approved-action-execution');
const {
migrateLocalSqliteDatabase,
} = require('@qinglong/local-sqlite/migration');
const {
LocalSqlitePluginPackageInstallRepository,
} = require('@qinglong/local-sqlite/plugin-package-install');
const {
LocalSqlitePluginPackageInstallProposalRepository,
} = require('@qinglong/local-sqlite/plugin-package-proposal');
const REQUESTER = Object.freeze({ type: 'user', id: 'usr_owner' });
const SYSTEM = Object.freeze({ type: 'system', id: 'package_dispatcher' });
const FENCE = Object.freeze({ projectVersion: 1, bindingVersion: 1 });
function packageAction() {
const manifest = {
apiVersion: PLUGIN_PACKAGE_API_VERSION,
kind: PLUGIN_PACKAGE_KIND,
metadata: {
name: 'example-monitor',
displayName: 'Example Monitor',
version: '1.2.0',
description: 'One bounded package',
license: 'Apache-2.0',
},
spec: {
compatibility: {
qinglong: '>=3.0.0-0 <4.0.0',
architectures: ['arm64'],
deploymentProfiles: ['edge'],
},
runtimes: [],
resources: {
memory: { recommended: '16Mi' },
disk: { install: '4Mi', working: '16Mi' },
},
permissions: {
network: { allowedHosts: [] },
secrets: [],
tools: [],
},
contents: { tasks: [], workflows: [], prompts: [], tools: [] },
},
};
const environment = {
qinglongVersion: '3.0.0-alpha.0',
architecture: 'arm64',
deploymentProfile: 'edge',
runtimes: [],
availableMemoryBytes: 128 * 1024 * 1024,
availableDiskBytes: 256 * 1024 * 1024,
};
const plan = planPluginPackageInstall(manifest, environment);
return {
input: {
lockId: 'proposal-monitor-v1',
projectId: 'default',
manifest,
plan,
environment,
source: {
kind: 'offline',
locator: `offline:sha256:${'a'.repeat(64)}`,
artifactDigest: 'a'.repeat(64),
artifactBytes: 2048,
contentDigest: 'b'.repeat(64),
},
architecture: 'arm64',
deploymentProfile: 'edge',
targetGeneration: 1,
},
plan,
};
}
function audit(
eventId,
requestId,
operationId,
subject,
authenticationId,
outcome,
reasons,
occurredAtMs,
) {
return {
eventId,
requestId,
operationId,
projectId: 'default',
subject,
authenticationId,
outcome,
reasons,
fence: FENCE,
occurredAtMs,
};
}
async function fixture(
t,
{ admittedAtMs = Date.now(), leaseDurationMs = 60_000 } = {},
) {
const proposedAtMs = admittedAtMs - 40;
const requestedAtMs = admittedAtMs - 30;
const decidedAtMs = admittedAtMs - 20;
const consumedAtMs = admittedAtMs - 10;
const claimedAtMs = admittedAtMs - 5;
const expiresAtMs = admittedAtMs + 60_000;
const client = new DatabaseSync(':memory:');
t.after(() => client.close());
client.exec('PRAGMA foreign_keys = ON');
await migrateLocalSqliteDatabase(client);
client
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings"
("project_id","subject_type","subject_id","version","state","role",
"mutation_id","changed_by_type","changed_by_id","created_at_ms")
VALUES ('default','user','usr_owner',1,'active','owner',
'grant-owner-1','user','usr_owner',0)`,
)
.run();
const action = packageAction();
const binding = {
permission: 'package.manage',
actionType: PLUGIN_PACKAGE_INSTALL_ACTION_TYPE,
actionRef: 'proposal:monitor-v1',
actionDigest: pluginPackageInstallActionDigest(action.input),
previewDigest: pluginPackageInstallPlanDigest(action.plan),
};
const proposal = createPluginPackageInstallProposal({
actionRef: binding.actionRef,
actionInput: action.input,
proposedBy: REQUESTER,
proposalFence: FENCE,
createdAtMs: proposedAtMs,
});
await new LocalSqlitePluginPackageInstallProposalRepository(
client,
).createProposal({
proposal,
audit: audit(
'10000000-0000-4000-8000-000000000100',
binding.actionRef,
'plugin_package.propose',
REQUESTER,
'auth-owner',
'allowed',
['package_proposal'],
proposedAtMs,
),
});
const approval = new LocalSqliteApprovalRequestRepository(client);
await approval.create({
request: createApprovalRequest({
id: 'approval-monitor-v1',
projectId: 'default',
action: binding,
risk: 'high',
decisionMode: 'human_confirmation',
requestedBy: REQUESTER,
requestedAtMs,
expiresAtMs,
requestFence: FENCE,
}),
audit: audit(
'10000000-0000-4000-8000-000000000101',
'http-request-1',
'approval.request',
REQUESTER,
'auth-owner',
'approval_required',
['package_review'],
requestedAtMs,
),
});
await approval.decide({
requestId: 'approval-monitor-v1',
expectedVersion: 1,
decisionId: 'decision-monitor-v1',
decision: 'approved',
reasonCode: 'reviewed',
principal: {
subject: REQUESTER,
authenticationId: 'auth-owner-step-up',
authenticatedAtMs: requestedAtMs,
expiresAtMs,
assurance: 'local_console',
},
decidedAtMs,
authorizationFence: FENCE,
audit: audit(
'10000000-0000-4000-8000-000000000102',
'http-request-1',
'approval.decide',
REQUESTER,
'auth-owner-step-up',
'allowed',
['role_grant'],
decidedAtMs,
),
});
const consumed = await approval.consume({
requestId: 'approval-monitor-v1',
expectedVersion: 2,
consumptionId: 'consume-monitor-v1',
dispatchId: 'dispatch-monitor-v1',
action: binding,
requestedBy: REQUESTER,
consumedBy: SYSTEM,
consumedAtMs,
authorizationFence: FENCE,
audit: audit(
'10000000-0000-4000-8000-000000000103',
'dispatch-cycle-1',
'approval.consume',
SYSTEM,
'auth-package-dispatcher',
'allowed',
['role_grant'],
consumedAtMs,
),
});
const executions = new LocalSqliteApprovedActionExecutionRepository(client);
const claimed = await executions.claimExecution({
dispatchId: consumed.dispatch.id,
owner: 'package_dispatcher',
leaseToken: 'lease-monitor-v1',
nowMs: claimedAtMs,
leaseDurationMs,
});
assert.equal(claimed.status, 'claimed');
const started = await executions.startExecution({
dispatchId: consumed.dispatch.id,
approvalRequestId: consumed.dispatch.approvalRequestId,
actionDigest: consumed.dispatch.action.actionDigest,
owner: 'package_dispatcher',
leaseToken: 'lease-monitor-v1',
expectedVersion: claimed.snapshot.execution.version,
startedAtMs: admittedAtMs,
});
const lock = resolvePluginPackageInstallProposal(
proposal,
consumed.dispatch,
admittedAtMs,
);
return {
client,
executions,
repository: new LocalSqlitePluginPackageInstallRepository(client),
request: {
lock,
proposalDigest: proposal.proposalDigest,
execution: started.execution,
installationId: 'install-monitor-v1',
mutationId: 'admit-monitor-v1',
admittedAtMs,
audit: audit(
'10000000-0000-4000-8000-000000000104',
consumed.dispatch.id,
'plugin_package.admit',
SYSTEM,
'auth-package-dispatcher',
'allowed',
['approved_action'],
admittedAtMs,
),
},
};
}
test('admits one approved Package atomically and exactly replays its receipt', async (t) => {
const { client, repository, request } = await fixture(t);
const admitted = await repository.admit(request);
assert.equal(admitted.status, 'admitted');
assert.equal(admitted.record.state, 'queued');
assert.equal(admitted.receipt.dispatchId, 'dispatch-monitor-v1');
assert.deepEqual(
await repository.findAdmissionReceipt('dispatch-monitor-v1'),
admitted.receipt,
);
const replay = await repository.admit(request);
assert.equal(replay.status, 'existing');
assert.deepEqual(replay, { ...admitted, status: 'existing' });
assert.deepEqual(
{
...client
.prepare(
`SELECT
(SELECT count(*) FROM "QingLong3PluginPackageInstalls") AS installs,
(SELECT count(*) FROM "QingLong3PluginPackageInstallMutations") AS mutations,
(SELECT count(*) FROM "QingLong3PluginPackageAdmissionReceipts") AS receipts,
(SELECT count(*) FROM "QingLong3SecurityAuditEvents"
WHERE "operation_id" = 'plugin_package.admit') AS audits`,
)
.get(),
},
{ installs: 1, mutations: 1, receipts: 1, audits: 1 },
);
await assert.rejects(
repository.admit({
...request,
audit: { ...request.audit, authenticationId: 'auth-drift' },
}),
PluginPackageAdmissionReceiptConflictError,
);
});
test('rejects proposal and execution fence drift before admission', async (t) => {
const { client, executions, repository, request } = await fixture(t);
await assert.rejects(
repository.admit({
...request,
proposalDigest: 'f'.repeat(64),
}),
PluginPackageAdmissionBindingConflictError,
);
await executions.renewExecution({
dispatchId: request.execution.dispatchId,
owner: request.execution.leaseOwner,
leaseToken: request.execution.leaseToken,
expectedVersion: request.execution.version,
nowMs: request.admittedAtMs + 5,
leaseDurationMs: 60_000,
});
await assert.rejects(
repository.admit(request),
PluginPackageAdmissionBindingConflictError,
);
assert.equal(
client
.prepare(
`SELECT count(*) AS count
FROM "QingLong3PluginPackageAdmissionReceipts"`,
)
.get().count,
0,
);
});
test('rejects an admission observed after its durable execution lease expired', async (t) => {
const { client, repository, request } = await fixture(t, {
admittedAtMs: Date.now() - 100,
leaseDurationMs: 20,
});
await assert.rejects(
repository.admit(request),
PluginPackageAdmissionBindingConflictError,
);
assert.equal(
client
.prepare(
`SELECT count(*) AS count
FROM "QingLong3PluginPackageAdmissionReceipts"`,
)
.get().count,
0,
);
});
test('rolls the full admission back when the requester Policy fence changed', async (t) => {
const { client, repository, request } = await fixture(t);
client
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings"
("project_id","subject_type","subject_id","version","state","role",
"mutation_id","changed_by_type","changed_by_id","created_at_ms")
VALUES ('default','user','usr_owner',2,'revoked',NULL,
'revoke-owner-1','user','usr_owner',?)`,
)
.run(request.admittedAtMs + 5);
await assert.rejects(
repository.admit(request),
ApprovalPolicyFenceConflictError,
);
assert.deepEqual(
{
...client
.prepare(
`SELECT
(SELECT count(*) FROM "QingLong3PluginPackageInstalls") AS installs,
(SELECT count(*) FROM "QingLong3PluginPackageAdmissionReceipts") AS receipts,
(SELECT count(*) FROM "QingLong3SecurityAuditEvents"
WHERE "operation_id" = 'plugin_package.admit') AS audits`,
)
.get(),
},
{ installs: 0, receipts: 0, audits: 0 },
);
});
@@ -0,0 +1,281 @@
const assert = require('node:assert/strict');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
PluginPackageAutomationPublicationConflictError,
PluginPackageAutomationPublicationUnavailableError,
createInitialPluginPackageAutomationPublication,
} = require('@qinglong/runtime-core/plugin-package-automation-publication');
const {
createPluginPackageQuarantineEvent,
} = require('@qinglong/runtime-core/plugin-package-quarantine');
const {
pluginPackageAutomationPublicationFixture,
registerPluginPackageAutomationPublicationRepositoryContract,
} = require('../../../test/contracts/pluginPackageAutomationPublicationRepositoryContract.cjs');
const {
activateInstall,
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
const {
LocalSqlitePluginPackageAutomationPublicationRepository,
} = require('../dist/plugin-package/pluginPackageAutomationPublicationRepository');
const {
LocalSqlitePluginPackageInstallRepository,
} = require('../dist/plugin-package/pluginPackageInstallRepository');
const {
LocalSqlitePluginPackageMaterializedRevisionRepository,
} = require('../dist/plugin-package/pluginPackageMaterializedRevisionRepository');
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
const digest = (value) => value.repeat(64);
function insertQuarantine(client, fixture) {
const record = fixture.install.active;
const event = createPluginPackageQuarantineEvent({
mutationId: `quarantine-${fixture.namespace}`,
revocationReceiptDigest: digest('d'),
impactDigest: digest('e'),
target: {
projectId: record.projectId,
packageName: record.packageName,
installationId: record.installationId,
lockDigest: record.lockDigest,
installState: record.state,
installVersion: record.version,
installRecordDigest: record.recordDigest,
activeLockDigest: record.activeLockDigest,
},
proposer: { type: 'user', id: 'owner-a' },
confirmer: { type: 'user', id: 'owner-b' },
authorizationMode: 'dual_control',
reasonCode: 'confirmed_key_compromise',
occurredAtMs: record.updatedAtMs + 1,
});
client
.prepare(
`INSERT INTO "QingLong3PluginPackageQuarantineEvents" (
event_digest, mutation_id, revocation_receipt_digest, impact_digest,
project_id, package_name, installation_id, lock_digest,
install_state, install_version, install_record_digest,
active_lock_digest, proposer_type, proposer_id, confirmer_type,
confirmer_id, authorization_mode, reason_code, occurred_at_ms,
event_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
event.eventDigest,
event.mutationId,
event.revocationReceiptDigest,
event.impactDigest,
event.target.projectId,
event.target.packageName,
event.target.installationId,
event.target.lockDigest,
event.target.installState,
event.target.installVersion,
event.target.installRecordDigest,
event.target.activeLockDigest,
event.proposer.type,
event.proposer.id,
event.confirmer.type,
event.confirmer.id,
event.authorizationMode,
event.reasonCode,
event.occurredAtMs,
JSON.stringify(event),
);
}
async function createRepository(_t, fixture) {
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 (?, ?, ?, 'active', 1, 1, 1)`,
)
.run(fixture.projectId, fixture.projectId, fixture.projectId);
return {
client,
repository:
new LocalSqlitePluginPackageAutomationPublicationRepository(client),
materializedRepository:
new LocalSqlitePluginPackageMaterializedRevisionRepository(
client,
fixture.registry,
),
close: () => client.close(),
};
}
registerPluginPackageAutomationPublicationRepositoryContract({
name: 'SQLite Plugin Package automation publication repository',
namespace: 'sqlite-automation-publication',
profile: 'edge',
createRepository,
});
test('fails closed when publication JSON is changed in place', async (t) => {
const fixture = pluginPackageAutomationPublicationFixture(
'sqlite-automation-corrupt',
{ profile: 'edge', name: 'daily' },
);
const harness = await createRepository(t, fixture);
t.after(() => harness.close());
await harness.materializedRepository.publish(fixture.revision);
const publication = createInitialPluginPackageAutomationPublication(
fixture.revision,
fixture.registry,
1_000,
);
await harness.repository.publish(publication);
harness.client.exec('PRAGMA ignore_check_constraints = ON');
harness.client
.prepare(
`UPDATE "QingLong3PluginPackageAutomationPublications"
SET publication_json =
json_set(publication_json, '$.definitions.workflows[0].name', 'Drift')
WHERE publication_digest = ?`,
)
.run(publication.publicationDigest);
await assert.rejects(
harness.repository.findByDigest(publication.publicationDigest),
PluginPackageAutomationPublicationUnavailableError,
);
});
test('lists only materialized active generations whose automation head is stale', async (t) => {
const fixture = pluginPackageAutomationPublicationFixture(
'sqlite-automation-pending',
{ profile: 'edge', name: 'daily' },
);
const harness = await createRepository(t, fixture);
t.after(() => harness.close());
const installRepository =
new LocalSqlitePluginPackageInstallRepository(harness.client);
await activateInstall(installRepository, fixture);
assert.deepEqual(await harness.repository.listPendingPage({ limit: 1 }), {
candidates: [],
truncated: false,
});
await harness.materializedRepository.publish(fixture.revision);
assert.deepEqual(await harness.repository.listPendingPage({ limit: 1 }), {
candidates: [
{
projectId: fixture.projectId,
packageName: fixture.packageName,
},
],
truncated: false,
});
const publication = createInitialPluginPackageAutomationPublication(
fixture.revision,
fixture.registry,
1_000,
);
await harness.repository.publish(publication);
assert.deepEqual(await harness.repository.listPendingPage({ limit: 1 }), {
candidates: [],
truncated: false,
});
});
test('admits only the exact active current automation publication', async (t) => {
const fixture = pluginPackageAutomationPublicationFixture(
'sqlite-automation-start-guard',
{ profile: 'edge', name: 'daily' },
);
const harness = await createRepository(t, fixture);
t.after(() => harness.close());
await activateInstall(
new LocalSqlitePluginPackageInstallRepository(harness.client),
fixture,
);
await harness.materializedRepository.publish(fixture.revision);
const publication = createInitialPluginPackageAutomationPublication(
fixture.revision,
fixture.registry,
1_000,
);
await harness.repository.publish(publication);
assert.equal(
await harness.repository.isStartAllowed(
fixture.projectId,
fixture.packageName,
publication.publicationDigest,
),
true,
);
assert.equal(
await harness.repository.isStartAllowed(
fixture.projectId,
fixture.packageName,
digest('f'),
),
false,
);
});
test('quarantine atomically removes pending work and rejects publication', async (t) => {
const fixture = pluginPackageAutomationPublicationFixture(
'sqlite-automation-quarantine',
{ profile: 'edge', name: 'daily' },
);
const harness = await createRepository(t, fixture);
t.after(() => harness.close());
await activateInstall(
new LocalSqlitePluginPackageInstallRepository(harness.client),
fixture,
);
await harness.materializedRepository.publish(fixture.revision);
assert.deepEqual(await harness.repository.listPendingPage({ limit: 1 }), {
candidates: [
{
projectId: fixture.projectId,
packageName: fixture.packageName,
},
],
truncated: false,
});
insertQuarantine(harness.client, fixture);
assert.deepEqual(await harness.repository.listPendingPage({ limit: 1 }), {
candidates: [],
truncated: false,
});
const publication = createInitialPluginPackageAutomationPublication(
fixture.revision,
fixture.registry,
1_000,
);
await assert.rejects(
harness.repository.publish(publication),
PluginPackageAutomationPublicationConflictError,
);
assert.equal(
await harness.repository.isStartAllowed(
fixture.projectId,
fixture.packageName,
publication.publicationDigest,
),
false,
);
});
test('publishes storage only through the explicit subpath', () => {
const entrypoint = require('@qinglong/local-sqlite/plugin-package-automation-publication');
assert.equal(
entrypoint.LocalSqlitePluginPackageAutomationPublicationRepository,
LocalSqlitePluginPackageAutomationPublicationRepository,
);
assert.equal(
require('../dist')
.LocalSqlitePluginPackageAutomationPublicationRepository,
undefined,
);
});
@@ -0,0 +1,449 @@
const assert = require('node:assert/strict');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
registerPluginPackageInstallRepositoryContract,
} = require('../../../test/contracts/pluginPackageInstallRepositoryContract.cjs');
const {
PluginPackageInstallMutationConflictError,
PluginPackageInstallTransitionConflictError,
PluginPackageInstallUnavailableError,
createPluginPackageInstall,
createPluginPackageLock,
pluginPackageInstallActionDigest,
pluginPackageInstallCommit,
pluginPackageInstallCreate,
pluginPackageInstallPlanDigest,
transitionPluginPackageInstall,
} = require('@qinglong/runtime-core/plugin-package-install');
const {
PLUGIN_PACKAGE_API_VERSION,
PLUGIN_PACKAGE_KIND,
planPluginPackageInstall,
} = require('@qinglong/runtime-core/plugin-package');
const {
LocalSqlitePluginPackageInstallRepository,
} = require('../dist/plugin-package/pluginPackageInstallRepository');
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
const ARTIFACT_DIGEST = 'a'.repeat(64);
const CONTENT_DIGEST = 'b'.repeat(64);
function manifest(packageName = 'example-monitor') {
return {
apiVersion: PLUGIN_PACKAGE_API_VERSION,
kind: PLUGIN_PACKAGE_KIND,
metadata: {
name: packageName,
displayName: packageName,
version: '1.2.0',
description: 'One bounded package',
license: 'Apache-2.0',
},
spec: {
compatibility: {
qinglong: '>=3.0.0-0 <4.0.0',
architectures: ['arm64'],
deploymentProfiles: ['edge'],
},
runtimes: [],
resources: {
memory: { recommended: '16Mi' },
disk: { install: '4Mi', working: '16Mi' },
},
permissions: {
network: { allowedHosts: [] },
secrets: [],
tools: [],
},
contents: {
tasks: [],
workflows: [],
prompts: [],
tools: [],
},
},
};
}
function environment() {
return {
qinglongVersion: '3.0.0-alpha.0',
architecture: 'arm64',
deploymentProfile: 'edge',
runtimes: [],
availableMemoryBytes: 128 * 1024 * 1024,
availableDiskBytes: 256 * 1024 * 1024,
};
}
function fixture(overrides = {}) {
const packageManifest = manifest(overrides.packageName ?? 'example-monitor');
const installEnvironment = environment();
const plan = planPluginPackageInstall(packageManifest, installEnvironment);
const source = {
kind: 'offline',
locator: `offline:sha256:${ARTIFACT_DIGEST}`,
artifactDigest: ARTIFACT_DIGEST,
artifactBytes: 2048,
contentDigest: CONTENT_DIGEST,
};
const actionInput = {
lockId: overrides.lockId ?? 'lock-001',
projectId: 'default',
manifest: packageManifest,
plan,
environment: installEnvironment,
source,
architecture: 'arm64',
deploymentProfile: 'edge',
targetGeneration: 1,
};
const lock = createPluginPackageLock({
...actionInput,
approval: {
requestId: `approval-${overrides.lockId ?? '001'}`,
requestVersion: 1,
dispatchId: `dispatch-${overrides.lockId ?? '001'}`,
actionDigest: pluginPackageInstallActionDigest(actionInput),
previewDigest: pluginPackageInstallPlanDigest(plan),
approvedBy: { type: 'user', id: 'owner-001' },
approvedAtMs: 100,
expiresAtMs: 10_000,
fence: { projectVersion: 1, bindingVersion: 1 },
},
createdAtMs: 200,
});
const install = createPluginPackageInstall(lock, {
installationId: overrides.installationId ?? 'install-001',
mutationId: overrides.mutationId ?? 'mutation-create',
occurredAtMs: overrides.occurredAtMs ?? 201,
});
return { lock, install };
}
function stage(lock, install, overrides = {}) {
return transitionPluginPackageInstall(lock, install, {
type: 'stage_completed',
mutationId: overrides.mutationId ?? 'mutation-stage',
occurredAtMs: overrides.occurredAtMs ?? install.updatedAtMs + 1,
stageRef: `local-stage:${lock.lockDigest}`,
artifactDigest: lock.source.artifactDigest,
manifestDigest: lock.manifestDigest,
contentDigest: lock.source.contentDigest,
evidenceDigest: 'e'.repeat(64),
});
}
async function repository(t) {
const client = new DatabaseSync(':memory:');
client.exec('PRAGMA foreign_keys = ON');
await migrateLocalSqliteDatabase(client);
t.after(() => client.close());
return {
client,
repository: new LocalSqlitePluginPackageInstallRepository(client),
};
}
registerPluginPackageInstallRepositoryContract({
name: 'SQLite Plugin Package install repository',
async createRepository() {
const client = new DatabaseSync(':memory:');
client.exec('PRAGMA foreign_keys = ON');
await migrateLocalSqliteDatabase(client);
return {
repository: new LocalSqlitePluginPackageInstallRepository(client),
close: () => client.close(),
};
},
});
test('creates and finds one queued install with a durable head and mutation', async (t) => {
const { client, repository: store } = await repository(t);
const value = fixture();
const result = await store.create(
pluginPackageInstallCreate(value.lock, value.install, null),
);
assert.equal(result.status, 'created');
assert.deepEqual(
await store.find('default', 'example-monitor'),
value.install,
);
assert.deepEqual(await store.findLock(value.lock.lockDigest), value.lock);
assert.deepEqual(
{
...client
.prepare(
`SELECT
(SELECT count(*) FROM "QingLong3PluginPackageInstalls") AS installs,
(SELECT count(*) FROM "QingLong3PluginPackageInstallHeads") AS heads,
(SELECT count(*) FROM "QingLong3PluginPackageInstallMutations") AS mutations`,
)
.get(),
},
{ installs: 1, heads: 1, mutations: 1 },
);
});
test('replays an exact create and rejects reuse with different locked facts', async (t) => {
const { repository: store } = await repository(t);
const value = fixture();
const command = pluginPackageInstallCreate(value.lock, value.install, null);
await store.create(command);
const replay = await store.create(command);
assert.equal(replay.status, 'existing');
assert.deepEqual(replay.record, value.install);
const drift = fixture({
lockId: 'lock-drift',
installationId: value.install.installationId,
mutationId: value.install.lastMutationId,
});
await assert.rejects(
store.create(pluginPackageInstallCreate(drift.lock, drift.install, null)),
PluginPackageInstallMutationConflictError,
);
});
test('commits exact CAS transitions and rejects stale durable state', async (t) => {
const { repository: store } = await repository(t);
const value = fixture();
await store.create(
pluginPackageInstallCreate(value.lock, value.install, null),
);
const staged = stage(value.lock, value.install);
const command = pluginPackageInstallCommit(value.install, staged);
const committed = await store.commit(command);
assert.equal(committed.status, 'committed');
assert.deepEqual(await store.find('default', 'example-monitor'), staged);
assert.equal((await store.commit(command)).status, 'existing');
const competing = stage(value.lock, value.install, {
mutationId: 'mutation-competing-stage',
});
await assert.rejects(
store.commit(pluginPackageInstallCommit(value.install, competing)),
PluginPackageInstallTransitionConflictError,
);
});
test('keeps old mutation replay idempotent after the record advances', async (t) => {
const { repository: store } = await repository(t);
const value = fixture();
const create = pluginPackageInstallCreate(value.lock, value.install, null);
await store.create(create);
const staged = stage(value.lock, value.install);
await store.commit(pluginPackageInstallCommit(value.install, staged));
const activating = transitionPluginPackageInstall(value.lock, staged, {
type: 'activation_started',
mutationId: 'mutation-activate',
occurredAtMs: 203,
});
await store.commit(pluginPackageInstallCommit(staged, activating));
const replay = await store.create(create);
assert.equal(replay.status, 'existing');
assert.deepEqual(replay.record, activating);
});
test('replaces only an exact terminal head and preserves install history', async (t) => {
const { client, repository: store } = await repository(t);
const first = fixture();
await store.create(
pluginPackageInstallCreate(first.lock, first.install, null),
);
const failed = transitionPluginPackageInstall(first.lock, first.install, {
type: 'failed',
mutationId: 'mutation-fail',
occurredAtMs: 202,
reason: 'stage_failed',
});
await store.commit(pluginPackageInstallCommit(first.install, failed));
const retry = fixture({
lockId: 'lock-retry',
installationId: 'install-002',
mutationId: 'mutation-retry',
occurredAtMs: 203,
});
await store.create(
pluginPackageInstallCreate(retry.lock, retry.install, failed),
);
assert.deepEqual(
await store.find('default', 'example-monitor'),
retry.install,
);
assert.equal(
client
.prepare(`SELECT count(*) AS count FROM "QingLong3PluginPackageInstalls"`)
.get().count,
2,
);
const stale = fixture({
lockId: 'lock-stale',
installationId: 'install-003',
mutationId: 'mutation-stale',
occurredAtMs: 204,
});
await assert.rejects(
store.create(pluginPackageInstallCreate(stale.lock, stale.install, failed)),
PluginPackageInstallTransitionConflictError,
);
});
test('paginates only current recoverable heads with a stable cursor', async (t) => {
const { repository: store } = await repository(t);
const alpha = fixture({
packageName: 'alpha',
lockId: 'lock-alpha',
installationId: 'install-alpha',
mutationId: 'mutation-alpha',
});
const beta = fixture({
packageName: 'beta',
lockId: 'lock-beta',
installationId: 'install-beta',
mutationId: 'mutation-beta',
});
await store.create(
pluginPackageInstallCreate(alpha.lock, alpha.install, null),
);
await store.create(pluginPackageInstallCreate(beta.lock, beta.install, null));
const first = await store.listRecoveryPage({ limit: 1 });
assert.equal(first.truncated, true);
assert.deepEqual(first.records, [alpha.install]);
assert.deepEqual(first.next, {
packageName: 'alpha',
installationId: 'install-alpha',
});
const second = await store.listRecoveryPage({
limit: 1,
after: first.next,
});
assert.deepEqual(second.records, [beta.install]);
assert.equal(second.truncated, false);
const failed = transitionPluginPackageInstall(alpha.lock, alpha.install, {
type: 'failed',
mutationId: 'mutation-alpha-fail',
occurredAtMs: 202,
reason: 'source_unavailable',
});
await store.commit(pluginPackageInstallCommit(alpha.install, failed));
assert.deepEqual((await store.listRecoveryPage({ limit: 64 })).records, [
beta.install,
]);
});
test('lists every current installation head by project with a bounded cursor', async (t) => {
const { repository: store } = await repository(t);
const alpha = fixture({
packageName: 'alpha',
lockId: 'inventory-alpha',
installationId: 'inventory-alpha',
mutationId: 'inventory-alpha-create',
});
const beta = fixture({
packageName: 'beta',
lockId: 'inventory-beta',
installationId: 'inventory-beta',
mutationId: 'inventory-beta-create',
});
await store.create(
pluginPackageInstallCreate(alpha.lock, alpha.install, null),
);
await store.create(pluginPackageInstallCreate(beta.lock, beta.install, null));
const failed = transitionPluginPackageInstall(alpha.lock, alpha.install, {
type: 'failed',
mutationId: 'inventory-alpha-failed',
occurredAtMs: 202,
reason: 'source_unavailable',
});
await store.commit(pluginPackageInstallCommit(alpha.install, failed));
const first = await store.listCurrentPage({
projectId: 'default',
limit: 1,
});
assert.equal(first.truncated, true);
assert.deepEqual(first.items, [{ record: failed, quarantine: null }]);
assert.deepEqual(first.next, { packageName: 'alpha' });
const second = await store.listCurrentPage({
projectId: 'default',
limit: 1,
after: first.next,
});
assert.deepEqual(second.items, [{ record: beta.install, quarantine: null }]);
assert.equal(second.truncated, false);
assert.equal(second.next, undefined);
assert.deepEqual(
await store.listCurrentPage({ projectId: 'missing', limit: 64 }),
{ items: [], truncated: false },
);
});
test('fails closed for archived projects and corrupt persisted JSON', async (t) => {
const { client, repository: store } = await repository(t);
client
.prepare(
`UPDATE "QingLong3Projects" SET "status" = 'archived' WHERE "id" = 'default'`,
)
.run();
const value = fixture();
await assert.rejects(
store.create(pluginPackageInstallCreate(value.lock, value.install, null)),
PluginPackageInstallTransitionConflictError,
);
client
.prepare(
`UPDATE "QingLong3Projects" SET "status" = 'active' WHERE "id" = 'default'`,
)
.run();
await store.create(
pluginPackageInstallCreate(value.lock, value.install, null),
);
client.exec('PRAGMA ignore_check_constraints = ON');
client
.prepare(
`UPDATE "QingLong3PluginPackageInstalls"
SET "lock_json" = '{"schema":"corrupt"}'
WHERE "installation_id" = ?`,
)
.run(value.install.installationId);
await assert.rejects(
store.findLock(value.lock.lockDigest),
PluginPackageInstallUnavailableError,
);
client
.prepare(
`UPDATE "QingLong3PluginPackageInstalls"
SET "lock_json" = ?
WHERE "installation_id" = ?`,
)
.run(JSON.stringify(value.lock), value.install.installationId);
client
.prepare(
`UPDATE "QingLong3PluginPackageInstalls"
SET "record_json" = '{"schema":"corrupt"}'
WHERE "installation_id" = ?`,
)
.run(value.install.installationId);
await assert.rejects(
store.find('default', 'example-monitor'),
PluginPackageInstallUnavailableError,
);
});
test('publishes the repository only through its explicit SQLite subpath', () => {
const root = require('../dist');
const subpath = require('@qinglong/local-sqlite/plugin-package-install');
assert.equal(root.LocalSqlitePluginPackageInstallRepository, undefined);
assert.equal(
subpath.LocalSqlitePluginPackageInstallRepository,
LocalSqlitePluginPackageInstallRepository,
);
});
@@ -0,0 +1,107 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { test } = require('node:test');
const {
CRASH_POINTS,
setupScenario,
verifyScenario,
} = require('./fixtures/pluginPackageLifecycleCrashMatrixFixture.cjs');
const FIXTURE_PATH = path.join(
__dirname,
'fixtures',
'pluginPackageLifecycleCrashMatrixFixture.cjs',
);
test(
'survives the Plugin Package lifecycle crash matrix',
{ timeout: 240_000 },
async (context) => {
const reports = [];
for (const profile of ['edge', 'standalone']) {
for (const action of ['disable', 'enable']) {
for (const [pointName, point] of Object.entries(CRASH_POINTS)) {
const directory = fs.mkdtempSync(
path.join(
os.tmpdir(),
`ql3-package-lifecycle-${profile}-${action}-`,
),
);
context.after(() => {
fs.rmSync(directory, { recursive: true, force: true });
});
const databasePath = path.join(directory, 'runtime.sqlite');
const markerPath = path.join(directory, 'crash-marker.json');
const event = await setupScenario({ action, databasePath, profile });
const crashed = spawnSync(
process.execPath,
[
FIXTURE_PATH,
'crash',
databasePath,
markerPath,
pointName,
profile,
action,
],
{ encoding: 'utf8', timeout: 30_000 },
);
assert.equal(
crashed.error,
undefined,
`${profile}/${action}/${pointName}: ${crashed.error?.message}`,
);
assert.equal(
crashed.signal,
'SIGKILL',
`${profile}/${action}/${pointName}: status=${crashed.status}, stderr=${crashed.stderr}`,
);
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
assert.deepEqual(marker, {
schema:
'qinglong/sqlite-plugin-package-lifecycle-crash-marker@v1',
action,
point: pointName,
pid: marker.pid,
});
reports.push(
await verifyScenario({
action,
databasePath,
event,
pointName,
profile,
}),
);
assert.equal(reports.at(-1).durableAfterCrash, point.durable);
}
}
}
assert.equal(reports.length, 32);
assert.equal(
reports.filter(({ crashBeforeCommit }) => crashBeforeCommit).length,
28,
);
assert.equal(
reports.filter(({ durableAfterCrash }) => durableAfterCrash).length,
4,
);
assert.deepEqual(
[...new Set(reports.map(({ journalMode }) => journalMode))].sort(),
['delete', 'wal'],
);
assert.ok(reports.every(({ synchronous }) => synchronous === 2));
assert.ok(
reports.every(
({ exactReplay, integrityCheck, foreignKeyCheck }) =>
exactReplay &&
integrityCheck === 'ok' &&
foreignKeyCheck === 'ok',
),
);
},
);
@@ -0,0 +1,672 @@
const assert = require('node:assert/strict');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
createApprovalRequest,
} = require('@qinglong/runtime-core/approved-action');
const {
createInitialPluginPackageAutomationPublication,
pluginPackageAutomationDefinitionsFromRevision,
} = require('@qinglong/runtime-core/plugin-package-automation-publication');
const {
InvalidPluginPackageLifecycleError,
PluginPackageLifecycleConflictError,
PluginPackageLifecycleUnavailableError,
createPluginPackageLifecycleEvent,
} = require('@qinglong/runtime-core/plugin-package-lifecycle');
const {
createProjectToolDefinitionSnapshot,
projectToolDefinitionSnapshotContribution,
} = require('@qinglong/runtime-core/project-tool-definition-snapshot');
const {
activateInstall,
pluginPackageTaskReconciliationFixture,
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
const {
LocalSqliteApprovalRequestRepository,
} = require('../dist/approved-action/approvalRequestRepository');
const { LocalSqliteOperationAuthority } = require('../dist/authority/operationAuthority');
const {
LocalSqlitePluginPackageInstallRepository,
} = require('../dist/plugin-package/pluginPackageInstallRepository');
const {
LocalSqlitePluginPackageAutomationPublicationRepository,
} = require('../dist/plugin-package/pluginPackageAutomationPublicationRepository');
const {
EDGE_PLUGIN_PACKAGE_LIFECYCLE_ACTIVE_SOURCE_LIMIT,
LocalSqlitePluginPackageLifecycleRepository,
} = require('../dist/plugin-package/pluginPackageLifecycleRepository');
const {
LocalSqlitePluginPackageMaterializedRevisionRepository,
} = require('../dist/plugin-package/pluginPackageMaterializedRevisionRepository');
const {
LocalSqlitePluginPackageTaskReconciliationRepository,
} = require('../dist/plugin-package/pluginPackageTaskReconciliationRepository');
const {
LocalSqliteProjectToolDefinitionSnapshotRepository,
} = require('../dist/tool-execution/projectToolDefinitionSnapshotRepository');
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
const {
LocalSqliteReadinessError,
auditLocalSqliteReadiness,
} = require('../dist/readiness/readiness');
const OWNER = Object.freeze({ type: 'user', id: 'owner-001' });
const OTHER_OWNER = Object.freeze({ type: 'user', id: 'owner-002' });
const SYSTEM = Object.freeze({ type: 'system', id: 'lifecycle-dispatcher' });
const FENCE = Object.freeze({ projectVersion: 1, bindingVersion: 1 });
function audit(
eventId,
requestId,
operationId,
subject,
authenticationId,
outcome,
projectId,
occurredAtMs,
) {
return {
eventId,
requestId,
operationId,
projectId,
subject,
authenticationId,
outcome,
reasons: [outcome === 'approval_required' ? 'package_review' : 'role_grant'],
fence: FENCE,
occurredAtMs,
};
}
function auditId(sequence, offset) {
return `90000000-0000-4000-8000-${String(sequence * 10 + offset).padStart(
12,
'0',
)}`;
}
async function harness(t, namespace, fixtureOptions = {}) {
const fixture = pluginPackageTaskReconciliationFixture(namespace, {
profile: 'edge',
...fixtureOptions,
});
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 (?, ?, ?, 'active', 1, 1, 1)`,
)
.run(fixture.projectId, fixture.projectId, fixture.projectId);
client
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings" (
project_id, subject_type, subject_id, version, state, role,
mutation_id, changed_by_type, changed_by_id, created_at_ms
) VALUES (?, 'user', ?, 1, 'active', 'owner', ?, 'user', ?, 1)`,
)
.run(
fixture.projectId,
OWNER.id,
`grant-${namespace}`,
OWNER.id,
);
const authority = new LocalSqliteOperationAuthority(client);
t.after(() => authority.close());
return {
fixture,
client,
authority,
approval: new LocalSqliteApprovalRequestRepository(authority),
automations:
new LocalSqlitePluginPackageAutomationPublicationRepository(authority),
install: new LocalSqlitePluginPackageInstallRepository(authority),
materialized: new LocalSqlitePluginPackageMaterializedRevisionRepository(
authority,
fixture.registry,
),
reconciliation: new LocalSqlitePluginPackageTaskReconciliationRepository(
authority,
fixture.registry,
),
snapshots: new LocalSqliteProjectToolDefinitionSnapshotRepository(
authority,
),
lifecycle: new LocalSqlitePluginPackageLifecycleRepository(authority, {
registry: fixture.registry,
activeSourceLimit:
EDGE_PLUGIN_PACKAGE_LIFECYCLE_ACTIVE_SOURCE_LIMIT,
}),
approvalSequence: 0,
};
}
async function publishActivePackage(value) {
await activateInstall(value.install, value.fixture);
await value.materialized.publish(value.fixture.revision);
if (
pluginPackageAutomationDefinitionsFromRevision(
value.fixture.revision,
value.fixture.registry,
)
) {
await value.automations.publish(
createInitialPluginPackageAutomationPublication(
value.fixture.revision,
value.fixture.registry,
1_000,
),
);
}
await value.reconciliation.reconcile(value.fixture.revision, {
async findActiveResourceGeneration() {
return value.fixture.revision.generation;
},
});
await value.snapshots.publish(
createProjectToolDefinitionSnapshot({
projectId: value.fixture.projectId,
contributions: [
projectToolDefinitionSnapshotContribution(
value.fixture.revision,
value.fixture.registry,
),
],
}),
);
}
async function approveLifecycleEvent(
value,
impact,
subjects = Object.freeze({
requestedBy: OWNER,
approvedBy: OWNER,
}),
) {
value.approvalSequence += 1;
const sequence = value.approvalSequence;
const requestId = `lifecycle-approval-${sequence}`;
const dispatchId = `lifecycle-dispatch-${sequence}`;
const requestedAtMs = 10_000 * sequence + 1;
const decidedAtMs = requestedAtMs + 1;
const consumedAtMs = requestedAtMs + 2;
const occurredAtMs = requestedAtMs + 3;
const expiresAtMs = requestedAtMs + 1_000;
const action = {
permission: 'package.manage',
actionType: `plugin_package.lifecycle.${impact.action}`,
actionRef: `lifecycle:${impact.impactDigest}`,
actionDigest: require('@qinglong/runtime-core/plugin-package-lifecycle')
.pluginPackageLifecycleActionDigest(impact),
previewDigest: impact.impactDigest,
};
await value.approval.create({
request: createApprovalRequest({
id: requestId,
projectId: value.fixture.projectId,
action,
risk: 'high',
decisionMode: 'human_confirmation',
requestedBy: subjects.requestedBy,
requestedAtMs,
expiresAtMs,
requestFence: FENCE,
}),
audit: audit(
auditId(sequence, 1),
`lifecycle-http-${sequence}`,
'approval.request',
subjects.requestedBy,
`auth-request-${sequence}`,
'approval_required',
value.fixture.projectId,
requestedAtMs,
),
});
await value.approval.decide({
requestId,
expectedVersion: 1,
decisionId: `lifecycle-decision-${sequence}`,
decision: 'approved',
reasonCode: 'reviewed',
principal: {
subject: subjects.approvedBy,
authenticationId: `auth-approve-${sequence}`,
authenticatedAtMs: decidedAtMs - 1,
expiresAtMs,
assurance: 'local_console',
},
decidedAtMs,
authorizationFence: FENCE,
audit: audit(
auditId(sequence, 2),
`lifecycle-http-${sequence}`,
'approval.decide',
subjects.approvedBy,
`auth-approve-${sequence}`,
'allowed',
value.fixture.projectId,
decidedAtMs,
),
});
const consumed = await value.approval.consume({
requestId,
expectedVersion: 2,
consumptionId: `lifecycle-consume-${sequence}`,
dispatchId,
action,
requestedBy: subjects.requestedBy,
consumedBy: SYSTEM,
consumedAtMs,
authorizationFence: FENCE,
audit: audit(
auditId(sequence, 3),
`lifecycle-dispatch-cycle-${sequence}`,
'approval.consume',
SYSTEM,
`auth-dispatch-${sequence}`,
'allowed',
value.fixture.projectId,
consumedAtMs,
),
});
return createPluginPackageLifecycleEvent({
dispatchId: consumed.dispatch.id,
impact,
requestedBy: subjects.requestedBy,
approvedBy: subjects.approvedBy,
authorizationMode: 'human_confirmation',
occurredAtMs,
});
}
function taskHeads(value) {
return value.client
.prepare(
`SELECT head.task_id AS "taskId",
revision.revision,
revision.enabled
FROM "QingLong3TaskDefinitions" AS head
JOIN "QingLong3TaskDefinitionRevisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
AND revision.revision = head.current_revision
WHERE head.project_id = ?
ORDER BY head.task_id`,
)
.all(value.fixture.projectId)
.map((row) => ({ ...row }));
}
test('atomically disables, exactly replays and restores only lifecycle Tasks', async (t) => {
const value = await harness(t, 'sqlite-lifecycle-roundtrip');
await publishActivePackage(value);
const before = await value.snapshots.findCurrent(value.fixture.projectId);
assert.equal(before.snapshot.sources.length, 1);
const disableImpact = await value.lifecycle.plan(
'disable',
value.fixture.projectId,
value.fixture.packageName,
);
assert.equal(disableImpact.expected.disposition, 'active');
assert.deepEqual(disableImpact.taskIds, [
`pkg:${value.fixture.packageName}:alpha`,
`pkg:${value.fixture.packageName}:beta`,
]);
const disableEvent = await approveLifecycleEvent(value, disableImpact);
let authorizationChecks = 0;
const disabled = await value.lifecycle.transition(disableEvent, () => {
authorizationChecks += 1;
});
assert.equal(disabled.status, 'created');
assert.equal(authorizationChecks, 2);
assert.equal(disabled.receipt.lifecycle.disposition, 'disabled');
assert.equal(disabled.receipt.capability.status, 'withdrawn');
assert.equal(disabled.receipt.capability.retainedSourceCount, 0);
assert.deepEqual(taskHeads(value), [
{
taskId: `pkg:${value.fixture.packageName}:alpha`,
revision: 2,
enabled: 0,
},
{
taskId: `pkg:${value.fixture.packageName}:beta`,
revision: 2,
enabled: 0,
},
]);
assert.equal(
(await value.snapshots.findCurrent(value.fixture.projectId)).snapshot.sources
.length,
0,
);
const replay = await value.lifecycle.transition(disableEvent, () => {
authorizationChecks += 1;
});
assert.equal(replay.status, 'existing');
assert.equal(authorizationChecks, 4);
assert.deepEqual(replay.receipt, disabled.receipt);
const enableImpact = await value.lifecycle.plan(
'enable',
value.fixture.projectId,
value.fixture.packageName,
);
assert.deepEqual(enableImpact.taskIds, disableImpact.taskIds);
const enableEvent = await approveLifecycleEvent(value, enableImpact);
const enabled = await value.lifecycle.transition(enableEvent, () => {});
assert.equal(enabled.receipt.lifecycle.disposition, 'active');
assert.equal(enabled.receipt.capability.status, 'restored');
assert.equal(enabled.receipt.capability.retainedSourceCount, 1);
assert.deepEqual(taskHeads(value), [
{
taskId: `pkg:${value.fixture.packageName}:alpha`,
revision: 3,
enabled: 1,
},
{
taskId: `pkg:${value.fixture.packageName}:beta`,
revision: 3,
enabled: 1,
},
]);
assert.equal(
(await value.snapshots.findCurrent(value.fixture.projectId)).snapshot.sources
.length,
1,
);
assert.deepEqual(
await value.lifecycle.findByEventDigest(enableEvent.eventDigest),
enabled.receipt,
);
await auditLocalSqliteReadiness(value.client);
});
test('atomically withdraws and restores Workflow and Prompt publications', async (t) => {
const value = await harness(t, 'sqlite-lifecycle-automation', {
workflows: [
{
schema: 'qinglong/plugin-package-workflow-resource@v1',
id: 'daily',
name: 'Daily workflow',
enabled: true,
steps: [{ id: 'run', task: 'alpha', needs: [] }],
},
],
prompts: [
{
schema: 'qinglong/plugin-package-prompt-resource@v1',
id: 'greeting',
name: 'Greeting prompt',
template: 'Hello {{name}}',
parameters: [{ name: 'name', required: true }],
},
],
});
await publishActivePackage(value);
const initial = await value.automations.findCurrent(
value.fixture.projectId,
value.fixture.packageName,
);
assert.equal(initial.state, 'active');
assert.equal(initial.version, 1);
const disableImpact = await value.lifecycle.plan(
'disable',
value.fixture.projectId,
value.fixture.packageName,
);
const disableEvent = await approveLifecycleEvent(value, disableImpact);
let checks = 0;
await assert.rejects(
value.lifecycle.transition(disableEvent, () => {
checks += 1;
if (checks === 2) throw new Error('authorization expired');
}),
(error) =>
error instanceof PluginPackageLifecycleUnavailableError &&
error.cause?.message === 'authorization expired',
);
assert.deepEqual(
await value.automations.findCurrent(
value.fixture.projectId,
value.fixture.packageName,
),
initial,
);
assert.deepEqual(
taskHeads(value).map(({ revision, enabled }) => ({ revision, enabled })),
[
{ revision: 1, enabled: 1 },
{ revision: 1, enabled: 1 },
],
);
const disabled = await value.lifecycle.transition(disableEvent, () => {});
const withdrawn = await value.automations.findCurrent(
value.fixture.projectId,
value.fixture.packageName,
);
assert.equal(disabled.status, 'created');
assert.equal(withdrawn.state, 'withdrawn');
assert.equal(withdrawn.version, 2);
assert.equal(withdrawn.lifecycleEventDigest, disableEvent.eventDigest);
assert.equal(withdrawn.previousPublicationDigest, initial.publicationDigest);
assert.deepEqual(withdrawn.definitions, initial.definitions);
const enableImpact = await value.lifecycle.plan(
'enable',
value.fixture.projectId,
value.fixture.packageName,
);
const enableEvent = await approveLifecycleEvent(value, enableImpact);
await value.lifecycle.transition(enableEvent, () => {});
const restored = await value.automations.findCurrent(
value.fixture.projectId,
value.fixture.packageName,
);
assert.equal(restored.state, 'active');
assert.equal(restored.version, 3);
assert.equal(restored.lifecycleEventDigest, enableEvent.eventDigest);
assert.equal(
restored.previousPublicationDigest,
withdrawn.publicationDigest,
);
assert.deepEqual(restored.definitions, initial.definitions);
await auditLocalSqliteReadiness(value.client);
});
test('diagnoses live Run blockers and retires history only after they clear', async (t) => {
const value = await harness(t, 'sqlite-lifecycle-uninstall');
await publishActivePackage(value);
const disableImpact = await value.lifecycle.plan(
'disable',
value.fixture.projectId,
value.fixture.packageName,
);
await value.lifecycle.transition(
await approveLifecycleEvent(value, disableImpact),
() => {},
);
const task = taskHeads(value)[0];
value.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 (?, ?, ?, ?, 'manual', 'manual', 'runtime', 'queued',
0, 0, 0, 1)`,
)
.run(
'019f9b00-0000-4000-a000-000000000001',
value.fixture.projectId,
task.taskId,
String(task.revision),
);
const blocked = await value.lifecycle.plan(
'uninstall',
value.fixture.projectId,
value.fixture.packageName,
);
assert.deepEqual(
blocked.blockingReferences.map(({ kind, ownerId }) => ({ kind, ownerId })),
[
{
kind: 'execution_recovery',
ownerId: '019f9b00-0000-4000-a000-000000000001',
},
],
);
assert.throws(
() =>
createPluginPackageLifecycleEvent({
dispatchId: 'blocked-dispatch',
impact: blocked,
requestedBy: OWNER,
approvedBy: OWNER,
authorizationMode: 'human_confirmation',
occurredAtMs: 1,
}),
InvalidPluginPackageLifecycleError,
);
value.client
.prepare(`UPDATE "Runs" SET status = 'succeeded' WHERE id = ?`)
.run('019f9b00-0000-4000-a000-000000000001');
const impact = await value.lifecycle.plan(
'uninstall',
value.fixture.projectId,
value.fixture.packageName,
);
assert.deepEqual(impact.blockingReferences, []);
const retired = await value.lifecycle.transition(
await approveLifecycleEvent(value, impact),
() => {},
);
assert.equal(retired.receipt.lifecycle.disposition, 'uninstalled');
assert.equal(retired.receipt.capability.status, 'retired');
assert.deepEqual(retired.receipt.capability.taskTransitions, []);
assert.equal(
value.client
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3PluginPackageLifecycleEvents"
WHERE project_id = ? AND package_name = ?`,
)
.get(value.fixture.projectId, value.fixture.packageName).count,
2,
);
await auditLocalSqliteReadiness(value.client);
});
test('rejects stale approved impact without leaving Task or lifecycle facts', async (t) => {
const value = await harness(t, 'sqlite-lifecycle-stale');
await publishActivePackage(value);
const impact = await value.lifecycle.plan(
'disable',
value.fixture.projectId,
value.fixture.packageName,
);
const event = await approveLifecycleEvent(value, impact);
const task = taskHeads(value)[0];
value.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 (?, ?, ?, ?, 'manual', 'manual', 'runtime', 'queued',
0, 0, 0, 1)`,
)
.run(
'019f9b00-0000-4000-a000-000000000002',
value.fixture.projectId,
task.taskId,
String(task.revision),
);
await assert.rejects(
value.lifecycle.transition(event, () => {}),
PluginPackageLifecycleConflictError,
);
assert.deepEqual(taskHeads(value).map(({ revision, enabled }) => ({
revision,
enabled,
})), [
{ revision: 1, enabled: 1 },
{ revision: 1, enabled: 1 },
]);
assert.equal(
value.client
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3PluginPackageLifecycleEvents"`,
)
.get().count,
0,
);
});
test('fails closed on dispatch subject drift and incomplete Task evidence', async (t) => {
const value = await harness(t, 'sqlite-lifecycle-corrupt');
await publishActivePackage(value);
const impact = await value.lifecycle.plan(
'disable',
value.fixture.projectId,
value.fixture.packageName,
);
const approved = await approveLifecycleEvent(value, impact);
const drifted = createPluginPackageLifecycleEvent({
dispatchId: approved.dispatchId,
impact,
requestedBy: OTHER_OWNER,
approvedBy: OTHER_OWNER,
authorizationMode: 'human_confirmation',
occurredAtMs: approved.occurredAtMs,
});
await assert.rejects(
value.lifecycle.transition(drifted, () => {}),
PluginPackageLifecycleConflictError,
);
const created = await value.lifecycle.transition(approved, () => {});
value.client.exec('PRAGMA foreign_keys = OFF');
value.client
.prepare(
`DELETE FROM "QingLong3PluginPackageLifecycleTasks"
WHERE event_digest = ? AND task_id = ?`,
)
.run(
approved.eventDigest,
`pkg:${value.fixture.packageName}:alpha`,
);
await assert.rejects(
value.lifecycle.findByEventDigest(approved.eventDigest),
PluginPackageLifecycleUnavailableError,
);
await assert.rejects(
auditLocalSqliteReadiness(value.client),
LocalSqliteReadinessError,
);
assert.equal(created.receipt.capability.taskTransitions.length, 2);
});
test('publishes lifecycle storage only through its explicit subpath', () => {
const entrypoint = require('@qinglong/local-sqlite/plugin-package-lifecycle');
assert.equal(
entrypoint.LocalSqlitePluginPackageLifecycleRepository,
LocalSqlitePluginPackageLifecycleRepository,
);
assert.equal(
require('../dist').LocalSqlitePluginPackageLifecycleRepository,
undefined,
);
});
@@ -0,0 +1,75 @@
const assert = require('node:assert/strict');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
materializedRevisionFixture,
registerPluginPackageMaterializedRevisionRepositoryContract,
} = require('../../../test/contracts/pluginPackageMaterializedRevisionRepositoryContract.cjs');
const {
PluginPackageResourceMaterializationUnavailableError,
} = require('@qinglong/runtime-core/plugin-package-resource-materialization');
const {
LocalSqlitePluginPackageMaterializedRevisionRepository,
} = require('../dist/plugin-package/pluginPackageMaterializedRevisionRepository');
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
async function createRepository(_t, fixture) {
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 (?, ?, ?, 'active', 1, 1, 1)`,
)
.run(fixture.projectId, fixture.projectId, fixture.projectId);
return {
client,
repository: new LocalSqlitePluginPackageMaterializedRevisionRepository(
client,
fixture.registry,
),
close: () => client.close(),
};
}
registerPluginPackageMaterializedRevisionRepositoryContract({
name: 'SQLite Plugin Package materialized revision repository',
namespace: 'sqlite-materialized',
profile: 'edge',
createRepository,
});
test('fails closed when durable semantic JSON is changed in place', async (t) => {
const fixture = materializedRevisionFixture('sqlite-corrupt');
const harness = await createRepository(t, fixture);
t.after(() => harness.close());
await harness.repository.publish(fixture.revision);
harness.client.exec('PRAGMA ignore_check_constraints = ON');
harness.client
.prepare(
`UPDATE "QingLong3PluginPackageMaterializedRevisions"
SET revision_json =
json_set(revision_json, '$.resources[0].value.name', 'Changed')
WHERE generation_digest = ?`,
)
.run(fixture.revision.generation.generationDigest);
await assert.rejects(
harness.repository.find(fixture.revision.generation.generationDigest),
PluginPackageResourceMaterializationUnavailableError,
);
});
test('publishes storage only through the explicit subpath', () => {
const entrypoint = require('@qinglong/local-sqlite/plugin-package-materialized-revision');
assert.equal(
entrypoint.LocalSqlitePluginPackageMaterializedRevisionRepository,
LocalSqlitePluginPackageMaterializedRevisionRepository,
);
assert.equal(
require('../dist').LocalSqlitePluginPackageMaterializedRevisionRepository,
undefined,
);
});
@@ -0,0 +1,83 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { test } = require('node:test');
const {
CRASH_POINTS,
setupScenario,
verifyScenario,
} = require('./fixtures/pluginPackageQuarantineCrashMatrixFixture.cjs');
const FIXTURE_PATH = path.join(
__dirname,
'fixtures',
'pluginPackageQuarantineCrashMatrixFixture.cjs',
);
test(
'survives the Plugin Package quarantine withdrawal crash matrix',
{ timeout: 120_000 },
async (context) => {
const reports = [];
for (const profile of ['edge', 'standalone']) {
for (const [pointName, point] of Object.entries(CRASH_POINTS)) {
const directory = fs.mkdtempSync(
path.join(os.tmpdir(), `ql3-package-quarantine-${profile}-`),
);
context.after(() => {
fs.rmSync(directory, { recursive: true, force: true });
});
const databasePath = path.join(directory, 'runtime.sqlite');
const markerPath = path.join(directory, 'crash-marker.json');
await setupScenario({ databasePath, profile });
const crashed = spawnSync(
process.execPath,
[FIXTURE_PATH, 'crash', databasePath, markerPath, pointName, profile],
{ encoding: 'utf8', timeout: 30_000 },
);
assert.equal(
crashed.error,
undefined,
`${profile}/${pointName}: ${crashed.error?.message}`,
);
assert.equal(
crashed.signal,
'SIGKILL',
`${profile}/${pointName}: status=${crashed.status}, stderr=${crashed.stderr}`,
);
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
assert.deepEqual(marker, {
schema: 'qinglong/sqlite-plugin-package-quarantine-crash-marker@v1',
point: pointName,
pid: marker.pid,
});
reports.push(
await verifyScenario({ databasePath, pointName, profile }),
);
assert.equal(reports.at(-1).durableAfterCrash, point.durable);
}
}
assert.equal(reports.length, 10);
assert.equal(
reports.filter(({ crashBeforeCommit }) => crashBeforeCommit).length,
8,
);
assert.equal(
reports.filter(({ durableAfterCrash }) => durableAfterCrash).length,
2,
);
assert.deepEqual(
[...new Set(reports.map(({ journalMode }) => journalMode))].sort(),
['delete', 'wal'],
);
assert.ok(
reports.every(
({ integrityCheck, foreignKeyCheck }) =>
integrityCheck === 'ok' && foreignKeyCheck === 'ok',
),
);
},
);
@@ -0,0 +1,470 @@
const assert = require('node:assert/strict');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
PluginPackageQuarantineConflictError,
PluginPackageQuarantineUnavailableError,
createPluginPackageQuarantineEvent,
} = require('@qinglong/runtime-core/plugin-package-quarantine');
const {
RunRepositoryConstraintError,
} = require('@qinglong/runtime-core/run-repository');
const {
createTaskDefinitionRevisionRef,
} = require('@qinglong/runtime-core/task-definition-execution-compiler');
const {
activateInstall,
pluginPackageTaskReconciliationFixture,
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
const { LocalSqliteOperationAuthority } = require('../dist/authority/operationAuthority');
const {
LocalSqlitePluginPackageInstallRepository,
} = require('../dist/plugin-package/pluginPackageInstallRepository');
const {
LocalSqlitePluginPackageMaterializedRevisionRepository,
} = require('../dist/plugin-package/pluginPackageMaterializedRevisionRepository');
const {
LocalSqlitePluginPackageTaskReconciliationRepository,
} = require('../dist/plugin-package/pluginPackageTaskReconciliationRepository');
const {
LocalSqliteProjectToolDefinitionSnapshotRepository,
} = require('../dist/tool-execution/projectToolDefinitionSnapshotRepository');
const { LocalSqliteRunRepository } = require('../dist/run/runRepository');
const {
EDGE_PLUGIN_PACKAGE_QUARANTINE_ACTIVE_SOURCE_LIMIT,
LocalSqlitePluginPackageQuarantineRepository,
} = require('../dist/plugin-package/pluginPackageQuarantineRepository');
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
const {
LocalSqliteReadinessError,
auditLocalSqliteReadiness,
} = require('../dist/readiness/readiness');
const digest = (value) => value.repeat(64);
async function harness(t, namespace) {
const fixture = pluginPackageTaskReconciliationFixture(namespace, {
profile: 'edge',
});
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 (?, ?, ?, 'active', 1, 1, 1)`,
)
.run(fixture.projectId, fixture.projectId, fixture.projectId);
const authority = new LocalSqliteOperationAuthority(client);
t.after(() => authority.close());
return {
fixture,
client,
install: new LocalSqlitePluginPackageInstallRepository(authority),
materialized: new LocalSqlitePluginPackageMaterializedRevisionRepository(
authority,
fixture.registry,
),
reconciliation: new LocalSqlitePluginPackageTaskReconciliationRepository(
authority,
fixture.registry,
),
snapshots: new LocalSqliteProjectToolDefinitionSnapshotRepository(
authority,
),
runs: new LocalSqliteRunRepository(authority),
quarantine: new LocalSqlitePluginPackageQuarantineRepository(authority, {
registry: fixture.registry,
activeSourceLimit: EDGE_PLUGIN_PACKAGE_QUARANTINE_ACTIVE_SOURCE_LIMIT,
}),
};
}
function quarantineEvent(fixture, record = fixture.install.active) {
return createPluginPackageQuarantineEvent({
mutationId: `quarantine-${fixture.namespace}`,
revocationReceiptDigest: digest('d'),
impactDigest: digest('e'),
target: {
projectId: record.projectId,
packageName: record.packageName,
installationId: record.installationId,
lockDigest: record.lockDigest,
installState: record.state,
installVersion: record.version,
installRecordDigest: record.recordDigest,
activeLockDigest: record.activeLockDigest,
},
proposer: { type: 'user', id: 'owner-a' },
confirmer: { type: 'user', id: 'owner-b' },
authorizationMode: 'dual_control',
reasonCode: 'confirmed_key_compromise',
occurredAtMs: record.updatedAtMs + 1,
});
}
async function publishActivePackage(value) {
await activateInstall(value.install, value.fixture);
await value.materialized.publish(value.fixture.revision);
await value.reconciliation.reconcile(value.fixture.revision, {
async findActiveResourceGeneration() {
return value.fixture.revision.generation;
},
});
}
test('withdraws active Package Tasks and Tool source in one exact replayable transaction', async (t) => {
const value = await harness(t, 'sqlite-quarantine-active');
await publishActivePackage(value);
const event = quarantineEvent(value.fixture);
assert.deepEqual(
await value.quarantine.findTargetsByLockDigest(event.target.lockDigest),
[event.target],
);
let authorizationChecks = 0;
const created = await value.quarantine.quarantine(event, () => {
authorizationChecks += 1;
});
assert.equal(authorizationChecks, 2);
assert.equal(created.status, 'created');
assert.equal(created.receipt.capability.status, 'withdrawn');
assert.deepEqual(
created.receipt.capability.taskWithdrawals.map(
({ taskId, previousRevision, disabledRevision }) => ({
taskId,
previousRevision,
disabledRevision,
}),
),
[
{
taskId: `pkg:${value.fixture.packageName}:alpha`,
previousRevision: 1,
disabledRevision: 2,
},
{
taskId: `pkg:${value.fixture.packageName}:beta`,
previousRevision: 1,
disabledRevision: 2,
},
],
);
assert.equal(created.receipt.capability.retainedSourceCount, 0);
assert.notEqual(
created.receipt.capability.previousActiveVectorDigest,
created.receipt.capability.currentActiveVectorDigest,
);
assert.deepEqual(
value.client
.prepare(
`SELECT task_id AS "taskId", current_revision AS "revision"
FROM "QingLong3TaskDefinitions"
WHERE project_id = ? ORDER BY task_id`,
)
.all(value.fixture.projectId)
.map((row) => ({ ...row })),
[
{ taskId: `pkg:${value.fixture.packageName}:alpha`, revision: 2 },
{ taskId: `pkg:${value.fixture.packageName}:beta`, revision: 2 },
],
);
assert.equal(
value.client
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3TaskDefinitionRevisions"
WHERE project_id = ? AND enabled = 0`,
)
.get(value.fixture.projectId).count,
2,
);
assert.equal(
value.client
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3ProjectToolDefinitionSnapshotSources"
WHERE project_id = ? AND active_vector_digest = ?`,
)
.get(
value.fixture.projectId,
created.receipt.capability.currentActiveVectorDigest,
).count,
0,
);
assert.equal(
(await value.snapshots.findCurrent(value.fixture.projectId)).snapshot
.snapshotDigest,
created.receipt.capability.currentToolSnapshotDigest,
);
const replay = await value.quarantine.quarantine(event, () => {
authorizationChecks += 1;
});
assert.equal(authorizationChecks, 4);
assert.equal(replay.status, 'existing');
assert.deepEqual(replay.receipt, created.receipt);
assert.deepEqual(
await value.quarantine.findByEventDigest(event.eventDigest),
created.receipt,
);
});
test('records a queued target without inventing Task or Tool withdrawal', async (t) => {
const value = await harness(t, 'sqlite-quarantine-queued');
await value.install.create(value.fixture.install.create);
const event = quarantineEvent(value.fixture, value.fixture.install.queued);
const created = await value.quarantine.quarantine(event, () => {});
assert.equal(created.receipt.capability.status, 'not_active');
assert.deepEqual(created.receipt.capability.taskWithdrawals, []);
assert.deepEqual(await value.install.listRecoveryPage({ limit: 1 }), {
records: [],
truncated: false,
});
assert.equal(
value.client
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3ProjectToolDefinitionSnapshots"`,
)
.get().count,
0,
);
});
test('rolls back every withdrawal fact when the target install advanced', async (t) => {
const value = await harness(t, 'sqlite-quarantine-stale');
await publishActivePackage(value);
const stale = quarantineEvent(value.fixture, {
...value.fixture.install.active,
version: value.fixture.install.active.version - 1,
});
await assert.rejects(
value.quarantine.quarantine(stale, () => {}),
PluginPackageQuarantineConflictError,
);
assert.equal(
value.client
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3PluginPackageQuarantineEvents"`,
)
.get().count,
0,
);
assert.equal(
value.client
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3TaskDefinitionRevisions"
WHERE project_id = ? AND enabled = 0`,
)
.get(value.fixture.projectId).count,
0,
);
});
test('rolls back withdrawal when the in-transaction Owner fence changes before commit', async (t) => {
const value = await harness(t, 'sqlite-quarantine-owner-fence');
await publishActivePackage(value);
let checks = 0;
await assert.rejects(
value.quarantine.quarantine(quarantineEvent(value.fixture), () => {
checks += 1;
if (checks === 2) throw new Error('Owner fence changed');
}),
(error) =>
error instanceof PluginPackageQuarantineUnavailableError &&
error.cause?.message === 'Owner fence changed',
);
assert.equal(checks, 2);
assert.equal(
value.client
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3PluginPackageQuarantineEvents"`,
)
.get().count,
0,
);
assert.equal(
value.client
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3TaskDefinitionRevisions"
WHERE project_id = ? AND enabled = 0`,
)
.get(value.fixture.projectId).count,
0,
);
});
test('fails closed when durable withdrawal task evidence is incomplete', async (t) => {
const value = await harness(t, 'sqlite-quarantine-corrupt');
await publishActivePackage(value);
const event = quarantineEvent(value.fixture);
await value.quarantine.quarantine(event, () => {});
value.client.exec('PRAGMA foreign_keys = OFF');
value.client
.prepare(
`DELETE FROM "QingLong3PluginPackageWithdrawalTasks"
WHERE event_digest = ? AND task_id = ?`,
)
.run(event.eventDigest, `pkg:${value.fixture.packageName}:alpha`);
await assert.rejects(
value.quarantine.findByEventDigest(event.eventDigest),
PluginPackageQuarantineUnavailableError,
);
await assert.rejects(
auditLocalSqliteReadiness(value.client),
LocalSqliteReadinessError,
);
});
test('rejects dispatch of a Run pinned to the quarantined Package Task revision', async (t) => {
const value = await harness(t, 'sqlite-quarantine-run-fence');
await publishActivePackage(value);
const task = value.client
.prepare(
`SELECT head.task_id AS "taskId", revision.revision,
revision.content_digest AS "contentDigest"
FROM "QingLong3TaskDefinitions" AS head
JOIN "QingLong3TaskDefinitionRevisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
AND revision.revision = head.current_revision
WHERE head.project_id = ?
ORDER BY head.task_id LIMIT 1`,
)
.get(value.fixture.projectId);
const run = {
id: '019f9a00-0000-4000-a000-000000000001',
projectId: value.fixture.projectId,
taskId: task.taskId,
taskRevision: createTaskDefinitionRevisionRef({
revision: task.revision,
contentDigest: task.contentDigest,
}),
taskName: 'quarantine fence',
triggerType: 'manual',
executionOrigin: 'manual',
executionOwner: 'runtime',
status: 'queued',
version: 0,
eventSequence: 0,
priority: 0,
createdAtMs: 1_000,
queuedAtMs: 1_001,
};
await value.runs.transaction((transaction) => transaction.insertRun(run));
await value.quarantine.quarantine(quarantineEvent(value.fixture), () => {});
await assert.rejects(
value.runs.transaction((transaction) =>
transaction.compareAndSetRun(
{ ...run, status: 'dispatching', version: 1 },
0,
),
),
RunRepositoryConstraintError,
);
assert.equal((await value.runs.findRunById(run.id)).status, 'queued');
});
test('rejects a Package Run while lifecycle is disabled and admits it after enable', async (t) => {
const value = await harness(t, 'sqlite-lifecycle-run-fence');
await publishActivePackage(value);
const task = value.client
.prepare(
`SELECT head.task_id AS "taskId", revision.revision,
revision.content_digest AS "contentDigest"
FROM "QingLong3TaskDefinitions" AS head
JOIN "QingLong3TaskDefinitionRevisions" AS revision
ON revision.project_id = head.project_id
AND revision.task_id = head.task_id
AND revision.revision = head.current_revision
WHERE head.project_id = ?
ORDER BY head.task_id LIMIT 1`,
)
.get(value.fixture.projectId);
const run = {
id: '019f9a00-0000-4000-a000-000000000002',
projectId: value.fixture.projectId,
taskId: task.taskId,
taskRevision: createTaskDefinitionRevisionRef({
revision: task.revision,
contentDigest: task.contentDigest,
}),
taskName: 'lifecycle fence',
triggerType: 'manual',
executionOrigin: 'manual',
executionOwner: 'runtime',
status: 'queued',
version: 0,
eventSequence: 0,
priority: 0,
createdAtMs: 1_000,
queuedAtMs: 1_001,
};
await value.runs.transaction((transaction) => transaction.insertRun(run));
value.client.exec('PRAGMA foreign_keys = OFF');
value.client
.prepare(
`INSERT INTO "QingLong3PluginPackageLifecycleHeads" (
project_id, package_name, installation_id, lock_digest,
install_record_digest, version, disposition, event_digest,
updated_at_ms
) VALUES (?, ?, ?, ?, ?, 1, 'disabled', ?, 2)`,
)
.run(
value.fixture.projectId,
value.fixture.packageName,
value.fixture.install.active.installationId,
value.fixture.install.active.lockDigest,
value.fixture.install.active.recordDigest,
digest('f'),
);
value.client.exec('PRAGMA foreign_keys = ON');
await assert.rejects(
value.runs.transaction((transaction) =>
transaction.compareAndSetRun(
{ ...run, status: 'dispatching', version: 1 },
0,
),
),
RunRepositoryConstraintError,
);
assert.equal((await value.runs.findRunById(run.id)).status, 'queued');
value.client
.prepare(
`UPDATE "QingLong3PluginPackageLifecycleHeads"
SET disposition = 'active', version = 2, updated_at_ms = 3
WHERE project_id = ? AND package_name = ?`,
)
.run(value.fixture.projectId, value.fixture.packageName);
assert.equal(
await value.runs.transaction((transaction) =>
transaction.compareAndSetRun(
{ ...run, status: 'dispatching', version: 1 },
0,
),
),
true,
);
assert.equal((await value.runs.findRunById(run.id)).status, 'dispatching');
});
test('publishes quarantine storage only through its explicit subpath', () => {
const entrypoint = require('@qinglong/local-sqlite/plugin-package-quarantine');
assert.equal(
entrypoint.LocalSqlitePluginPackageQuarantineRepository,
LocalSqlitePluginPackageQuarantineRepository,
);
assert.equal(
require('../dist').LocalSqlitePluginPackageQuarantineRepository,
undefined,
);
});
@@ -0,0 +1,151 @@
const assert = require('node:assert/strict');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
PluginPackageTaskReconciliationConflictError,
} = require('@qinglong/runtime-core/plugin-package-task-reconciliation');
const {
TaskDefinitionConflictError,
} = require('@qinglong/runtime-core/task-definition');
const {
registerPluginPackageTaskReconciliationRepositoryContract,
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
const {
LocalSqlitePluginPackageTaskReconciliationRepository,
} = require('../dist/plugin-package/pluginPackageTaskReconciliationRepository');
const {
LocalSqlitePluginPackageMaterializedRevisionRepository,
} = require('../dist/plugin-package/pluginPackageMaterializedRevisionRepository');
const {
LocalSqlitePluginPackageInstallRepository,
} = require('../dist/plugin-package/pluginPackageInstallRepository');
const {
LocalSqliteTaskDefinitionRepository,
} = require('../dist/task-definition/taskDefinitionRepository');
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
async function createRepository(_t, fixture) {
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 (?, ?, ?, 'active', 1, 1, 1)`,
)
.run(fixture.projectId, fixture.projectId, fixture.projectId);
return {
client,
repository:
new LocalSqlitePluginPackageTaskReconciliationRepository(
client,
fixture.registry,
),
materializedRepository:
new LocalSqlitePluginPackageMaterializedRevisionRepository(
client,
fixture.registry,
),
installRepository: new LocalSqlitePluginPackageInstallRepository(client),
taskRepository: new LocalSqliteTaskDefinitionRepository(
client,
fixture.registry,
),
close: () => client.close(),
};
}
registerPluginPackageTaskReconciliationRepositoryContract({
name: 'SQLite Plugin Package Task reconciliation repository',
namespace: 'sqlite-task-reconcile',
profile: 'edge',
createRepository,
async assertGenericWriteRejected(harness, fixture) {
const task = await harness.taskRepository.findCurrentTaskDefinition(
fixture.projectId,
`pkg:${fixture.packageName}:alpha`,
);
await assert.rejects(
harness.taskRepository.appendTaskDefinitionRevision({
projectId: task.projectId,
taskId: task.taskId,
expectedRevision: task.revision,
mutationId: '019f9000-0000-4000-a000-000000000001',
name: 'Bypass',
kind: task.kind,
spec: task.spec,
labels: task.labels,
enabled: task.enabled,
occurredAtMs: task.updatedAtMs + 1,
}),
TaskDefinitionConflictError,
);
},
async assertDurableUpgrade(harness, fixture) {
const beta = await harness.taskRepository.findCurrentTaskDefinition(
fixture.projectId,
`pkg:${fixture.packageName}:beta`,
);
assert.equal(beta.revision, 2);
assert.equal(beta.enabled, false);
assert.equal(
harness.client
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3PluginPackageTaskOwnerships"
WHERE project_id = ? AND package_name = ?`,
)
.get(fixture.projectId, fixture.packageName).count,
3,
);
},
});
test('rolls back when the external generation fence changes', async (t) => {
const {
pluginPackageTaskReconciliationFixture,
activateInstall,
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
const fixture = pluginPackageTaskReconciliationFixture(
'sqlite-task-reconcile-fence',
);
const harness = await createRepository(t, fixture);
t.after(() => harness.close());
await activateInstall(harness.installRepository, fixture);
await harness.materializedRepository.publish(fixture.revision);
await assert.rejects(
harness.repository.reconcile(fixture.revision, {
async findActiveResourceGeneration() {
return null;
},
}),
PluginPackageTaskReconciliationConflictError,
);
assert.equal(
harness.client
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3TaskDefinitions" WHERE project_id = ?`,
)
.get(fixture.projectId).count,
0,
);
assert.equal(
await harness.repository.find(fixture.revision.generation.generationDigest),
null,
);
});
test('publishes storage only through the explicit subpath', () => {
const entrypoint = require('@qinglong/local-sqlite/plugin-package-task-reconciliation');
assert.equal(
entrypoint.LocalSqlitePluginPackageTaskReconciliationRepository,
LocalSqlitePluginPackageTaskReconciliationRepository,
);
assert.equal(
require('../dist').LocalSqlitePluginPackageTaskReconciliationRepository,
undefined,
);
});
@@ -0,0 +1,91 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { test } = require('node:test');
const {
CRASH_POINTS,
setupScenario,
verifyScenario,
} = require('./fixtures/pluginPackageWorkflowAdmissionCrashMatrixFixture.cjs');
const FIXTURE_PATH = path.join(
__dirname,
'fixtures',
'pluginPackageWorkflowAdmissionCrashMatrixFixture.cjs',
);
test(
'survives the Plugin Package Workflow admission crash matrix',
{ timeout: 180_000 },
async (context) => {
const reports = [];
for (const profile of ['edge', 'standalone']) {
for (const [pointName, point] of Object.entries(CRASH_POINTS)) {
const directory = fs.mkdtempSync(
path.join(
os.tmpdir(),
`ql3-workflow-admission-${profile}-${pointName}-`,
),
);
context.after(() => {
fs.rmSync(directory, { recursive: true, force: true });
});
const databasePath = path.join(directory, 'runtime.sqlite');
const markerPath = path.join(directory, 'crash-marker.json');
await setupScenario({ databasePath, profile });
const crashed = spawnSync(
process.execPath,
[FIXTURE_PATH, 'crash', databasePath, markerPath, pointName, profile],
{ encoding: 'utf8', timeout: 30_000 },
);
assert.equal(
crashed.error,
undefined,
`${profile}/${pointName}: ${crashed.error?.message}`,
);
assert.equal(
crashed.signal,
'SIGKILL',
`${profile}/${pointName}: status=${crashed.status}, stderr=${crashed.stderr}`,
);
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
assert.deepEqual(marker, {
schema:
'qinglong/sqlite-plugin-package-workflow-admission-crash-marker@v1',
point: pointName,
pid: marker.pid,
});
const report = await verifyScenario({
databasePath,
pointName,
profile,
});
assert.equal(report.durableAfterCrash, point.durable);
reports.push(report);
}
}
assert.equal(reports.length, 16);
assert.equal(
reports.filter(({ crashBeforeCommit }) => crashBeforeCommit).length,
14,
);
assert.equal(
reports.filter(({ durableAfterCrash }) => durableAfterCrash).length,
2,
);
assert.deepEqual(
[...new Set(reports.map(({ journalMode }) => journalMode))].sort(),
['delete', 'wal'],
);
assert.ok(reports.every(({ synchronous }) => synchronous === 2));
assert.ok(
reports.every(
({ exactReplay, integrityCheck, foreignKeyCheck }) =>
exactReplay && integrityCheck === 'ok' && foreignKeyCheck === 'ok',
),
);
},
);
@@ -0,0 +1,397 @@
const assert = require('node:assert/strict');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
createInitialPluginPackageAutomationPublication,
} = require('@qinglong/runtime-core/plugin-package-automation-publication');
const {
createPluginPackageWorkflowExecutionPlan,
PluginPackageWorkflowAdmissionConflictError,
PluginPackageWorkflowAdmissionNotAllowedError,
PluginPackageWorkflowAdmissionUnavailableError,
} = require('@qinglong/runtime-core/plugin-package-workflow-execution-plan');
const {
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError,
} = require('@qinglong/runtime-core/plugin-package-workflow-administration');
const {
transitionStepRunMutation,
} = require('@qinglong/runtime-core/step-run');
const {
activateInstall,
pluginPackageTaskReconciliationFixture,
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
const {
LocalSqlitePluginPackageAutomationPublicationRepository,
} = require('../dist/plugin-package/pluginPackageAutomationPublicationRepository');
const {
LocalSqlitePluginPackageInstallRepository,
} = require('../dist/plugin-package/pluginPackageInstallRepository');
const {
LocalSqlitePluginPackageMaterializedRevisionRepository,
} = require('../dist/plugin-package/pluginPackageMaterializedRevisionRepository');
const {
LocalSqlitePluginPackageWorkflowAdmissionRepository,
} = require('../dist/plugin-package/workflow/pluginPackageWorkflowAdmissionRepository');
const { LocalSqliteStepRunRepository } = require('../dist/run/stepRunRepository');
const { auditLocalSqliteReadiness } = require('../dist/readiness/readiness');
function fixture(namespace) {
const value = pluginPackageTaskReconciliationFixture(namespace, {
workflows: [
{
schema: 'qinglong/plugin-package-workflow-resource@v1',
id: 'daily',
name: 'Daily workflow',
enabled: true,
steps: [
{ id: 'collect', task: 'alpha', needs: [] },
{ id: 'summarize', task: 'beta', needs: ['collect'] },
],
},
],
});
return {
...value,
publication: createInitialPluginPackageAutomationPublication(
value.revision,
value.registry,
2_000,
),
};
}
function plan(value, overrides = {}) {
return createPluginPackageWorkflowExecutionPlan({
planId: `workflow-plan-${value.namespace}`,
runId: `run-${value.namespace}`,
workflowId: 'daily',
stepRunIds: {
collect: `step-collect-${value.namespace}`,
summarize: `step-summarize-${value.namespace}`,
},
publication: value.publication,
revision: value.revision,
taskSpecSemanticRegistry: value.registry,
plannedAtMs: 3_000,
...overrides,
});
}
async function harness(t, namespace, { active = true } = {}) {
const value = fixture(namespace);
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 (?, ?, ?, 'active', 1, 1, 1)`,
)
.run(value.projectId, value.projectId, value.projectId);
if (active) {
await activateInstall(
new LocalSqlitePluginPackageInstallRepository(client),
value,
);
}
await new LocalSqlitePluginPackageMaterializedRevisionRepository(
client,
value.registry,
).publish(value.revision);
await new LocalSqlitePluginPackageAutomationPublicationRepository(
client,
).publish(value.publication);
t.after(() => client.close());
return {
client,
value,
repository: new LocalSqlitePluginPackageWorkflowAdmissionRepository(client),
};
}
test('atomically admits one generation-bound Workflow Run and exactly replays it', async (t) => {
const { client, value, repository } = await harness(
t,
'sqlite-workflow-admit',
);
const executionPlan = plan(value);
const created = await repository.admit(executionPlan);
assert.equal(created.status, 'created');
assert.equal(created.receipt.finalRunVersion, 3);
assert.equal(created.receipt.finalRunEventSequence, 3);
const replay = await repository.admit(
JSON.parse(JSON.stringify(executionPlan)),
);
assert.deepEqual(replay, {
status: 'existing',
receipt: created.receipt,
});
assert.deepEqual(
await repository.findByPlanId(executionPlan.planId),
created.receipt,
);
assert.deepEqual(
await repository.findByRunId(executionPlan.runId),
created.receipt,
);
assert.deepEqual(
{
...client
.prepare(
`SELECT
(SELECT COUNT(*) FROM "Runs") AS runs,
(SELECT COUNT(*) FROM "StepRuns") AS steps,
(SELECT COUNT(*) FROM "RunEvents") AS events,
(SELECT COUNT(*) FROM "StepRunMutations") AS mutations,
(SELECT COUNT(*)
FROM "QingLong3PluginPackageWorkflowAdmissions") AS admissions`,
)
.get(),
},
{ runs: 1, steps: 2, events: 3, mutations: 2, admissions: 1 },
);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 43);
});
test('runs an optional authorization guard inside new and replay transactions', async (t) => {
const { value, repository } = await harness(t, 'tx-guard');
const executionPlan = plan(value);
const observations = [];
const created = await repository.admit(executionPlan, (context) => {
observations.push({
replay: context.replay,
planId: context.plan.planId,
receiptDigest: context.receipt.receiptDigest,
});
});
assert.deepEqual(
await repository.findPlanByPlanId(executionPlan.planId),
executionPlan,
);
await repository.admit(executionPlan, (context) => {
observations.push({
replay: context.replay,
planId: context.plan.planId,
receiptDigest: context.receipt.receiptDigest,
});
});
assert.deepEqual(observations, [
{
replay: false,
planId: executionPlan.planId,
receiptDigest: created.receipt.receiptDigest,
},
{
replay: true,
planId: executionPlan.planId,
receiptDigest: created.receipt.receiptDigest,
},
]);
});
test('rolls back Workflow admission when the transaction authorization guard rejects', async (t) => {
const { client, value, repository } = await harness(t, 'tx-reject');
const executionPlan = plan(value);
await assert.rejects(
repository.admit(executionPlan, () => {
throw new PluginPackageWorkflowAdministrationAuthorizationFenceConflictError();
}),
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError,
);
assert.deepEqual(
{
...client
.prepare(
`SELECT
(SELECT COUNT(*) FROM "Runs") AS runs,
(SELECT COUNT(*) FROM "StepRuns") AS steps,
(SELECT COUNT(*) FROM "QingLong3PluginPackageWorkflowAdmissions") AS admissions`,
)
.get(),
},
{ runs: 0, steps: 0, admissions: 0 },
);
});
test('exactly replays immutable admission after the Workflow StepRun advances', async (t) => {
const { client, value, repository } = await harness(
t,
'sqlite-workflow-progress-replay',
);
const executionPlan = plan(value);
const created = await repository.admit(executionPlan);
const stepRuns = new LocalSqliteStepRunRepository(client);
const collect = await stepRuns.findByRunAndStepKey(
executionPlan.runId,
'collect',
);
assert.ok(collect);
const running = transitionStepRunMutation(
collect,
{
expectedVersion: collect.version,
expectedDigest: collect.stepRunDigest,
mutationId: 'workflow-progress-running',
to: 'running',
atMs: 4_000,
},
{
expectedRunVersion: created.receipt.finalRunVersion,
expectedRunEventSequence: created.receipt.finalRunEventSequence,
eventId: 'workflow-progress-running-event',
dedupeKey: 'workflow-progress-running-event',
actor: { type: 'executor' },
},
);
await stepRuns.apply(running);
const succeeded = transitionStepRunMutation(
running.stepRun,
{
expectedVersion: running.stepRun.version,
expectedDigest: running.stepRun.stepRunDigest,
mutationId: 'workflow-progress-succeeded',
to: 'succeeded',
atMs: 5_000,
},
{
expectedRunVersion: created.receipt.finalRunVersion + 1,
expectedRunEventSequence: created.receipt.finalRunEventSequence + 1,
eventId: 'workflow-progress-succeeded-event',
dedupeKey: 'workflow-progress-succeeded-event',
actor: { type: 'executor' },
},
);
await stepRuns.apply(succeeded);
assert.deepEqual(await repository.admit(executionPlan), {
status: 'existing',
receipt: created.receipt,
});
assert.deepEqual(
await repository.findByRunId(executionPlan.runId),
created.receipt,
);
assert.deepEqual(
{
...client
.prepare(
`SELECT status, version, event_sequence AS "eventSequence"
FROM "Runs" WHERE id = ?`,
)
.get(executionPlan.runId),
},
{ status: 'running', version: 5, eventSequence: 5 },
);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 43);
});
test('fails closed before writing when the exact installation is not active', async (t) => {
const { client, value, repository } = await harness(
t,
'sqlite-workflow-not-allowed',
{ active: false },
);
await assert.rejects(
repository.admit(plan(value)),
PluginPackageWorkflowAdmissionNotAllowedError,
);
assert.deepEqual(
{
...client
.prepare(
`SELECT
(SELECT COUNT(*) FROM "Runs") AS runs,
(SELECT COUNT(*)
FROM "QingLong3PluginPackageWorkflowAdmissions") AS admissions`,
)
.get(),
},
{ runs: 0, admissions: 0 },
);
});
test('rolls back every Run artifact on identity collision or Task drift', async (t) => {
const first = await harness(t, 'sqlite-workflow-collision');
const firstPlan = plan(first.value);
await first.repository.admit(firstPlan);
const colliding = plan(first.value, {
planId: `${firstPlan.planId}-other`,
});
await assert.rejects(
first.repository.admit(colliding),
PluginPackageWorkflowAdmissionConflictError,
);
assert.deepEqual(
{
...first.client
.prepare(
`SELECT
(SELECT COUNT(*) FROM "Runs") AS runs,
(SELECT COUNT(*) FROM "StepRuns") AS steps,
(SELECT COUNT(*) FROM "RunEvents") AS events,
(SELECT COUNT(*)
FROM "QingLong3PluginPackageWorkflowAdmissions") AS admissions`,
)
.get(),
},
{ runs: 1, steps: 2, events: 3, admissions: 1 },
);
const drift = await harness(t, 'sqlite-workflow-task-drift');
drift.client.exec('PRAGMA ignore_check_constraints = ON');
drift.client
.prepare(
`UPDATE "QingLong3PluginPackageMaterializedRevisions"
SET revision_json = json_set(
revision_json, '$.resources[0].value.enabled', json('false')
)
WHERE generation_digest = ?`,
)
.run(drift.value.revision.generation.generationDigest);
await assert.rejects(
drift.repository.admit(plan(drift.value)),
PluginPackageWorkflowAdmissionConflictError,
);
assert.equal(
drift.client.prepare(`SELECT COUNT(*) AS count FROM "Runs"`).get().count,
0,
);
});
test('detects durable Workflow admission evidence changed in place', async (t) => {
const { client, value, repository } = await harness(
t,
'sqlite-workflow-corrupt',
);
const executionPlan = plan(value);
await repository.admit(executionPlan);
client.exec('PRAGMA ignore_check_constraints = ON');
client
.prepare(
`UPDATE "QingLong3PluginPackageWorkflowAdmissions"
SET receipt_json = json_set(receipt_json, '$.workflowId', 'changed')
WHERE plan_digest = ?`,
)
.run(executionPlan.planDigest);
await assert.rejects(
repository.findByPlanId(executionPlan.planId),
PluginPackageWorkflowAdmissionUnavailableError,
);
});
test('publishes Workflow admission only through an explicit local SQLite subpath', () => {
const subpath = require('@qinglong/local-sqlite/plugin-package-workflow-admission');
const root = require('../dist');
assert.equal(
subpath.LocalSqlitePluginPackageWorkflowAdmissionRepository,
LocalSqlitePluginPackageWorkflowAdmissionRepository,
);
assert.equal(
root.LocalSqlitePluginPackageWorkflowAdmissionRepository,
undefined,
);
});
@@ -0,0 +1,315 @@
const assert = require('node:assert/strict');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
createInitialPluginPackageAutomationPublication,
} = require('@qinglong/runtime-core/plugin-package-automation-publication');
const {
createPluginPackageWorkflowExecutionPlan,
} = require('@qinglong/runtime-core/plugin-package-workflow-execution-plan');
const {
InvalidPluginPackageWorkflowFrontierError,
PluginPackageWorkflowFrontierUnavailableError,
} = require('@qinglong/runtime-core/plugin-package-workflow-frontier');
const {
transitionStepRunMutation,
} = require('@qinglong/runtime-core/step-run');
const {
activateInstall,
pluginPackageTaskReconciliationFixture,
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
const {
LocalSqlitePluginPackageAutomationPublicationRepository,
} = require('../dist/plugin-package/pluginPackageAutomationPublicationRepository');
const {
LocalSqlitePluginPackageInstallRepository,
} = require('../dist/plugin-package/pluginPackageInstallRepository');
const {
LocalSqlitePluginPackageMaterializedRevisionRepository,
} = require('../dist/plugin-package/pluginPackageMaterializedRevisionRepository');
const {
LocalSqlitePluginPackageWorkflowAdmissionRepository,
} = require('../dist/plugin-package/workflow/pluginPackageWorkflowAdmissionRepository');
const {
LocalSqlitePluginPackageWorkflowFrontierRepository,
} = require('../dist/plugin-package/workflow/pluginPackageWorkflowFrontierRepository');
const {
LocalSqliteStepRunRepository,
} = require('../dist/run/stepRunRepository');
function fixture(namespace) {
const value = pluginPackageTaskReconciliationFixture(namespace, {
workflows: [
{
schema: 'qinglong/plugin-package-workflow-resource@v1',
id: 'daily',
name: 'Daily workflow',
enabled: true,
steps: [
{ id: 'collect', task: 'alpha', needs: [] },
{ id: 'summarize', task: 'beta', needs: ['collect'] },
],
},
],
});
const publication = createInitialPluginPackageAutomationPublication(
value.revision,
value.registry,
2_000,
);
const plan = createPluginPackageWorkflowExecutionPlan({
planId: `wf-plan-${namespace}`,
runId: `wf-run-${namespace}`,
workflowId: 'daily',
stepRunIds: {
collect: `wf-collect-${namespace}`,
summarize: `wf-summary-${namespace}`,
},
publication,
revision: value.revision,
taskSpecSemanticRegistry: value.registry,
plannedAtMs: 3_000,
});
return { ...value, publication, plan };
}
async function harness(t, namespace) {
const value = fixture(namespace);
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 (?, ?, ?, 'active', 1, 1, 1)`,
)
.run(value.projectId, value.projectId, value.projectId);
await activateInstall(
new LocalSqlitePluginPackageInstallRepository(client),
value,
);
await new LocalSqlitePluginPackageMaterializedRevisionRepository(
client,
value.registry,
).publish(value.revision);
await new LocalSqlitePluginPackageAutomationPublicationRepository(
client,
).publish(value.publication);
const admission =
new LocalSqlitePluginPackageWorkflowAdmissionRepository(client);
const admitted = await admission.admit(value.plan);
t.after(() => client.close());
return {
client,
value,
admission,
admitted,
frontier:
new LocalSqlitePluginPackageWorkflowFrontierRepository(client),
stepRuns: new LocalSqliteStepRunRepository(client),
};
}
async function transitionStep(
harnessValue,
stepKey,
to,
runVersion,
atMs,
) {
const current = await harnessValue.stepRuns.findByRunAndStepKey(
harnessValue.value.plan.runId,
stepKey,
);
assert.ok(current);
const mutation = transitionStepRunMutation(
current,
{
expectedVersion: current.version,
expectedDigest: current.stepRunDigest,
mutationId: `${stepKey}-${to}-${current.version}`,
to,
atMs,
...(to === 'failed' ? { resultCode: 'task_failed' } : {}),
},
{
expectedRunVersion: runVersion,
expectedRunEventSequence: runVersion,
eventId: `${stepKey}-${to}-event-${current.version}`,
dedupeKey: `${stepKey}-${to}-event-${current.version}`,
actor: { type: 'executor' },
},
);
return harnessValue.stepRuns.apply(mutation);
}
test('advances only an actionable dependency and terminalizes exactly once', async (t) => {
const value = await harness(t, 'frontier-ok');
assert.deepEqual(await value.frontier.listCandidates({ limit: 8 }), {
candidates: [],
truncated: false,
});
await transitionStep(value, 'collect', 'running', 3, 4_000);
await transitionStep(value, 'collect', 'succeeded', 4, 5_000);
const actionable = await value.frontier.listCandidates({ limit: 8 });
assert.deepEqual(actionable, {
candidates: [
{
runId: value.value.plan.runId,
planDigest: value.value.plan.planDigest,
admittedAtMs: 3_000,
},
],
truncated: false,
});
const advanced = await value.frontier.advance(value.value.plan.runId);
assert.equal(advanced.status, 'advanced');
assert.equal(advanced.stepMutationCount, 1);
assert.deepEqual(advanced.readyStepRunIds, [
value.value.plan.steps.find(({ stepKey }) => stepKey === 'summarize')
.stepRunId,
]);
assert.equal(advanced.runVersion, 6);
assert.deepEqual(await value.frontier.listCandidates({ limit: 8 }), {
candidates: [],
truncated: false,
});
await transitionStep(
value,
'summarize',
'running',
6,
advanced.observedAtMs + 1,
);
await transitionStep(
value,
'summarize',
'succeeded',
7,
advanced.observedAtMs + 2,
);
assert.equal(
(await value.frontier.listCandidates({ limit: 8 })).candidates.length,
1,
);
const terminal = await value.frontier.advance(value.value.plan.runId);
assert.equal(terminal.status, 'terminal');
assert.equal(terminal.terminalStatus, 'succeeded');
assert.equal(terminal.runVersion, 9);
assert.deepEqual(
{
...value.client
.prepare(
`SELECT status, version, event_sequence AS "eventSequence",
finished_at_ms IS NOT NULL AS finished,
error_code AS "errorCode"
FROM "Runs" WHERE id = ?`,
)
.get(value.value.plan.runId),
},
{
status: 'succeeded',
version: 9,
eventSequence: 9,
finished: 1,
errorCode: null,
},
);
const replay = await value.frontier.advance(value.value.plan.runId);
assert.equal(replay.status, 'settled');
assert.equal(replay.terminalStatus, 'succeeded');
assert.deepEqual(await value.admission.admit(value.value.plan), {
status: 'existing',
receipt: value.admitted.receipt,
});
});
test('skips downstream work and fails the aggregate atomically', async (t) => {
const value = await harness(t, 'frontier-fail');
await transitionStep(value, 'collect', 'running', 3, 4_000);
await transitionStep(value, 'collect', 'failed', 4, 5_000);
const result = await value.frontier.advance(value.value.plan.runId);
assert.equal(result.status, 'terminal');
assert.equal(result.stepMutationCount, 1);
assert.equal(result.terminalStatus, 'failed');
assert.equal(result.runVersion, 7);
assert.deepEqual(
{
...value.client
.prepare(
`SELECT run.status, run.version,
run.event_sequence AS "eventSequence",
run.error_code AS "errorCode",
step.status AS "stepStatus",
step.result_code AS "resultCode",
(SELECT COUNT(*) FROM "RunEvents"
WHERE run_id = run.id) AS events
FROM "Runs" AS run
JOIN "StepRuns" AS step
ON step.run_id = run.id AND step.step_key = 'summarize'
WHERE run.id = ?`,
)
.get(value.value.plan.runId),
},
{
status: 'failed',
version: 7,
eventSequence: 7,
errorCode: 'workflow_step_failed',
stepStatus: 'skipped',
resultCode: 'dependency_not_succeeded',
events: 7,
},
);
});
test('bounds paging before SQL and fails closed on current StepRun drift', async (t) => {
const value = await harness(t, 'frontier-drift');
assert.throws(
() => value.frontier.listCandidates({ limit: 65 }),
InvalidPluginPackageWorkflowFrontierError,
);
await transitionStep(value, 'collect', 'running', 3, 4_000);
await transitionStep(value, 'collect', 'succeeded', 4, 5_000);
value.client.exec('PRAGMA ignore_check_constraints = ON');
value.client
.prepare(
`UPDATE "StepRuns"
SET step_run_json = json_set(step_run_json, '$.definitionRef', 'drift')
WHERE run_id = ? AND step_key = 'summarize'`,
)
.run(value.value.plan.runId);
await assert.rejects(
value.frontier.advance(value.value.plan.runId),
PluginPackageWorkflowFrontierUnavailableError,
);
assert.deepEqual(
{
...value.client
.prepare(
`SELECT status, version, event_sequence AS "eventSequence"
FROM "Runs" WHERE id = ?`,
)
.get(value.value.plan.runId),
},
{ status: 'running', version: 5, eventSequence: 5 },
);
});
test('publishes Workflow frontier only through its explicit SQLite subpath', () => {
const subpath = require('@qinglong/local-sqlite/plugin-package-workflow-frontier');
const root = require('../dist');
assert.equal(
subpath.LocalSqlitePluginPackageWorkflowFrontierRepository,
LocalSqlitePluginPackageWorkflowFrontierRepository,
);
assert.equal(
root.LocalSqlitePluginPackageWorkflowFrontierRepository,
undefined,
);
});
@@ -0,0 +1,644 @@
const assert = require('node:assert/strict');
const { createHash } = require('node:crypto');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
createInitialPluginPackageAutomationPublication,
} = require('@qinglong/runtime-core/plugin-package-automation-publication');
const {
createPluginPackageWorkflowExecutionPlan,
} = require('@qinglong/runtime-core/plugin-package-workflow-execution-plan');
const {
InvalidPluginPackageWorkflowTaskAttemptAdmissionError,
PluginPackageWorkflowTaskAttemptAdmissionConflictError,
PluginPackageWorkflowTaskAttemptAdmissionUnavailableError,
} = require('@qinglong/runtime-core/plugin-package-workflow-task-attempt-admission');
const {
activateInstall,
pluginPackageTaskReconciliationFixture,
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
const {
LocalSqlitePluginPackageAutomationPublicationRepository,
} = require('../dist/plugin-package/pluginPackageAutomationPublicationRepository');
const {
LocalSqlitePluginPackageInstallRepository,
} = require('../dist/plugin-package/pluginPackageInstallRepository');
const {
LocalSqlitePluginPackageMaterializedRevisionRepository,
} = require('../dist/plugin-package/pluginPackageMaterializedRevisionRepository');
const {
LocalSqlitePluginPackageTaskReconciliationRepository,
} = require('../dist/plugin-package/pluginPackageTaskReconciliationRepository');
const {
LocalSqlitePluginPackageWorkflowAdmissionRepository,
} = require('../dist/plugin-package/workflow/pluginPackageWorkflowAdmissionRepository');
const {
LocalSqlitePluginPackageWorkflowTaskAttemptAdmissionRepository,
} = require('../dist/plugin-package/workflow/pluginPackageWorkflowTaskAttemptAdmissionRepository');
const { LocalSqliteRunRepository } = require('../dist/run/runRepository');
const {
LocalSqliteOperationAuthority,
} = require('../dist/authority/operationAuthority');
const {
createLocalSqliteRunRuntimeCapabilities,
} = require('../dist/run/runRuntimeCapabilities');
const {
LocalSqliteWorkflowTaskExecutionRepository,
} = require('../dist/plugin-package/workflow/workflowTaskExecutionRepository');
const {
LocalSqlitePluginPackageWorkflowCancellationConvergenceRepository,
} = require('../dist/plugin-package/workflow/pluginPackageWorkflowCancellationConvergenceRepository');
const { auditLocalSqliteReadiness } = require('../dist/readiness/readiness');
function fixture(namespace) {
const identity = createHash('sha256')
.update(namespace)
.digest('hex')
.slice(0, 16);
const value = pluginPackageTaskReconciliationFixture(namespace, {
workflows: [
{
schema: 'qinglong/plugin-package-workflow-resource@v1',
id: 'daily',
name: 'Daily workflow',
enabled: true,
steps: [
{ id: 'collect', task: 'alpha', needs: [] },
{ id: 'summarize', task: 'beta', needs: ['collect'] },
],
},
],
});
const publication = createInitialPluginPackageAutomationPublication(
value.revision,
value.registry,
2_000,
);
const plan = createPluginPackageWorkflowExecutionPlan({
planId: `task-attempt-plan-${namespace}`,
runId: `wta-run-${identity}`,
workflowId: 'daily',
stepRunIds: {
collect: `wta-collect-${identity}`,
summarize: `wta-summary-${identity}`,
},
publication,
revision: value.revision,
taskSpecSemanticRegistry: value.registry,
plannedAtMs: 3_000,
});
return { ...value, publication, plan };
}
async function harness(t, namespace, { reconcile = true } = {}) {
const value = fixture(namespace);
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 (?, ?, ?, 'active', 1, 1, 1)`,
)
.run(value.projectId, value.projectId, value.projectId);
await activateInstall(
new LocalSqlitePluginPackageInstallRepository(client),
value,
);
await new LocalSqlitePluginPackageMaterializedRevisionRepository(
client,
value.registry,
).publish(value.revision);
if (reconcile) {
await new LocalSqlitePluginPackageTaskReconciliationRepository(
client,
value.registry,
).reconcile(value.revision, {
async findActiveResourceGeneration() {
return value.revision.generation;
},
});
}
await new LocalSqlitePluginPackageAutomationPublicationRepository(
client,
).publish(value.publication);
await new LocalSqlitePluginPackageWorkflowAdmissionRepository(client).admit(
value.plan,
);
t.after(() => client.close());
return {
client,
value,
repository:
new LocalSqlitePluginPackageWorkflowTaskAttemptAdmissionRepository(
client,
),
};
}
test('atomically admits the exact reconciled local Task revision and replays it', async (t) => {
const { client, value, repository } = await harness(
t,
'sqlite-workflow-task-attempt',
);
const collect = value.plan.steps.find(({ stepKey }) => stepKey === 'collect');
assert.ok(collect);
assert.deepEqual(await repository.listCandidates({ limit: 8 }), {
candidates: [
{
runId: value.plan.runId,
stepRunId: collect.stepRunId,
readyAtMs: 3_000,
planDigest: value.plan.planDigest,
},
],
truncated: false,
});
const created = await repository.admit(value.plan.runId, collect.stepRunId);
assert.equal(created.status, 'created');
assert.equal(created.receipt.resourceTaskId, 'alpha');
assert.equal(created.receipt.taskId, `pkg:${value.packageName}:alpha`);
assert.match(created.receipt.taskRevision, /^qltd:v1:1:[0-9a-f]{64}$/);
assert.equal(created.receipt.executorType, 'local_process');
assert.equal(created.receipt.attemptNumber, 1);
assert.equal(created.receipt.runVersion, 4);
assert.deepEqual(
await createLocalSqliteRunRuntimeCapabilities(
new LocalSqliteOperationAuthority(client),
).dispatch.listLocalDispatchCandidates({ limit: 8 }),
{
candidates: [
{
runId: value.plan.runId,
stepRunId: collect.stepRunId,
projectId: value.projectId,
taskId: created.receipt.taskId,
taskRevision: created.receipt.taskRevision,
attemptId: created.receipt.attemptId,
attemptNumber: 1,
executorType: 'local_process',
priority: 0,
queuedAtMs: 3_000,
attemptCreatedAtMs: created.receipt.admittedAtMs,
},
],
truncated: false,
},
);
assert.deepEqual(
await repository.admit(value.plan.runId, collect.stepRunId),
{
status: 'existing',
receipt: created.receipt,
},
);
assert.deepEqual(await repository.listCandidates({ limit: 8 }), {
candidates: [],
truncated: false,
});
assert.deepEqual(
{
...client
.prepare(
`SELECT
(SELECT COUNT(*) FROM "RunAttempts") AS attempts,
(SELECT COUNT(*) FROM "RunEvents") AS events,
(SELECT COUNT(*) FROM
"QingLong3PluginPackageWorkflowTaskAttemptAdmissions")
AS admissions,
run.version AS "runVersion",
run.event_sequence AS "eventSequence",
step.status AS "stepStatus",
step.attempt_count AS "stepAttemptCount"
FROM "Runs" AS run
JOIN "StepRuns" AS step
ON step.run_id = run.id AND step.id = ?
WHERE run.id = ?`,
)
.get(collect.stepRunId, value.plan.runId),
},
{
attempts: 1,
events: 4,
admissions: 1,
runVersion: 4,
eventSequence: 4,
stepStatus: 'ready',
stepAttemptCount: 0,
},
);
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 43);
});
test('bounds candidate paging before SQL and fences cancellation', async (t) => {
const { client, value, repository } = await harness(
t,
'sqlite-workflow-task-attempt-fence',
);
assert.throws(
() => repository.listCandidates({ limit: 65 }),
InvalidPluginPackageWorkflowTaskAttemptAdmissionError,
);
client
.prepare(
`UPDATE "Runs"
SET cancel_requested_at_ms = ?, cancel_reason = 'user'
WHERE id = ?`,
)
.run(4_000, value.plan.runId);
assert.deepEqual(await repository.listCandidates({ limit: 8 }), {
candidates: [],
truncated: false,
});
await assert.rejects(
repository.admit(value.plan.runId, value.plan.steps[0].stepRunId),
InvalidPluginPackageWorkflowTaskAttemptAdmissionError,
);
assert.equal(
client.prepare(`SELECT COUNT(*) AS count FROM "RunAttempts"`).get().count,
0,
);
});
test('keeps the parent Workflow running while one local Task starts and completes', async (t) => {
const { client, value, repository } = await harness(
t,
'sqlite-workflow-task-local-execution',
);
const collect = value.plan.steps.find(({ stepKey }) => stepKey === 'collect');
assert.ok(collect);
const admitted = await repository.admit(value.plan.runId, collect.stepRunId);
const runs = new LocalSqliteRunRepository(client);
const execution = new LocalSqliteWorkflowTaskExecutionRepository(client);
const callbackTokenHash = 'a'.repeat(64);
const startingAtMs = admitted.receipt.admittedAtMs + 1;
const runningAtMs = startingAtMs + 1;
const finishedAtMs = runningAtMs + 1;
assert.equal(
(
await execution.prepare({
runId: value.plan.runId,
attemptId: admitted.receipt.attemptId,
stepRunId: collect.stepRunId,
callbackTokenHash,
deadlineAtMs: startingAtMs + 1_000,
logArtifactId: 'local-0123456789abcdef0123456789abcd',
atMs: startingAtMs,
eventId: 'local-workflow-starting-event',
})
).status,
'applied',
);
const startingRun = await runs.findRunById(value.plan.runId);
const startingAttempt = await runs.findAttemptById(
admitted.receipt.attemptId,
);
assert.ok(startingRun);
assert.ok(startingAttempt);
assert.equal(
(
await execution.recordRunning({
run: startingRun,
attempt: startingAttempt,
callbackTokenHash,
executorHandle: 'qlp:v1:durable-local-workflow-handle',
pid: 123,
startedAtMs: runningAtMs,
attemptEventId: 'local-workflow-running-attempt',
stepMutationId: 'local-workflow-running-step',
})
).status,
'applied',
);
const runningRun = await runs.findRunById(value.plan.runId);
const runningAttempt = await runs.findAttemptById(admitted.receipt.attemptId);
assert.ok(runningRun);
assert.ok(runningAttempt);
assert.equal(
await execution.complete({
run: runningRun,
attempt: runningAttempt,
callbackSequence: 1,
startedAtMs: runningAtMs,
finishedAtMs,
exitCode: 0,
terminalStatus: 'succeeded',
attemptEventId: 'local-workflow-completed-attempt',
syntheticStartMutationId: 'unused-synthetic-start',
terminalStepMutationId: 'local-workflow-completed-step',
}),
'completed',
);
assert.deepEqual(
{
...client
.prepare(
`SELECT run.status AS "runStatus",
attempt.status AS "attemptStatus",
attempt.callback_sequence AS "callbackSequence",
step.status AS "stepStatus",
step.attempt_count AS "stepAttemptCount"
FROM "Runs" AS run
JOIN "RunAttempts" AS attempt
ON attempt.run_id = run.id AND attempt.id = ?
JOIN "StepRuns" AS step
ON step.run_id = run.id AND step.id = ?
WHERE run.id = ?`,
)
.get(admitted.receipt.attemptId, collect.stepRunId, value.plan.runId),
},
{
runStatus: 'running',
attemptStatus: 'succeeded',
callbackSequence: 1,
stepStatus: 'succeeded',
stepAttemptCount: 1,
},
);
});
test('times out one local Workflow Task without cancelling its parent Run', async (t) => {
const { client, value, repository } = await harness(
t,
'sqlite-workflow-task-local-timeout',
);
const collect = value.plan.steps[0];
const admitted = await repository.admit(value.plan.runId, collect.stepRunId);
const runs = new LocalSqliteRunRepository(client);
const execution = new LocalSqliteWorkflowTaskExecutionRepository(client);
const callbackTokenHash = 'b'.repeat(64);
const startingAtMs = admitted.receipt.admittedAtMs + 1;
const deadlineAtMs = startingAtMs + 10;
await execution.prepare({
runId: value.plan.runId,
attemptId: admitted.receipt.attemptId,
stepRunId: collect.stepRunId,
callbackTokenHash,
deadlineAtMs,
atMs: startingAtMs,
eventId: 'local-workflow-timeout-starting',
});
let run = await runs.findRunById(value.plan.runId);
let attempt = await runs.findAttemptById(admitted.receipt.attemptId);
assert.ok(run);
assert.ok(attempt);
await execution.recordRunning({
run,
attempt,
callbackTokenHash,
executorHandle: 'qlp:v1:timeout-workflow-handle',
pid: 124,
startedAtMs: startingAtMs + 1,
attemptEventId: 'local-workflow-timeout-running-attempt',
stepMutationId: 'local-workflow-timeout-running-step',
});
run = await runs.findRunById(value.plan.runId);
attempt = await runs.findAttemptById(admitted.receipt.attemptId);
assert.ok(run);
assert.ok(attempt);
assert.equal(
await execution.requestTimeout({
run,
attempt,
dueAtMs: deadlineAtMs,
eventId: 'local-workflow-timeout-requested',
}),
'requested',
);
run = await runs.findRunById(value.plan.runId);
attempt = await runs.findAttemptById(admitted.receipt.attemptId);
assert.ok(run);
assert.ok(attempt);
assert.equal(
await execution.recordControlTerminal({
run,
attempt,
reason: 'timeout',
terminalStatus: 'timed_out',
errorCode: 'EXECUTION_TIMED_OUT',
errorSummary: 'Execution exceeded its configured timeout',
finishedAtMs: deadlineAtMs + 1,
attemptEventId: 'local-workflow-timeout-terminal-attempt',
stepMutationId: 'local-workflow-timeout-terminal-step',
}),
'terminal',
);
assert.deepEqual(
{
...client
.prepare(
`SELECT run.status AS "runStatus",
run.cancel_requested_at_ms AS "cancelRequestedAtMs",
attempt.status AS "attemptStatus",
step.status AS "stepStatus"
FROM "Runs" AS run
JOIN "RunAttempts" AS attempt
ON attempt.run_id = run.id AND attempt.id = ?
JOIN "StepRuns" AS step
ON step.run_id = run.id AND step.id = ?
WHERE run.id = ?`,
)
.get(admitted.receipt.attemptId, collect.stepRunId, value.plan.runId),
},
{
runStatus: 'running',
cancelRequestedAtMs: null,
attemptStatus: 'timed_out',
stepStatus: 'timed_out',
},
);
});
test('requeues one orphaned claimed Workflow Task at a fresh Step epoch', async (t) => {
const { client, value, repository } = await harness(
t,
'sqlite-workflow-task-local-recovery',
);
const collect = value.plan.steps[0];
const admitted = await repository.admit(value.plan.runId, collect.stepRunId);
const runs = new LocalSqliteRunRepository(client);
const recovery = new LocalSqliteWorkflowTaskExecutionRepository(client);
assert.deepEqual(await recovery.listRecoveryCandidates({ limit: 8 }), {
candidates: [
{
runId: value.plan.runId,
attemptId: admitted.receipt.attemptId,
attemptCreatedAtMs: admitted.receipt.admittedAtMs,
},
],
truncated: false,
});
const run = await runs.findRunById(value.plan.runId);
const attempt = await runs.findAttemptById(admitted.receipt.attemptId);
assert.ok(run);
assert.ok(attempt);
assert.equal(
await recovery.recover({
run,
attempt,
reason: 'unstarted_claim_expired',
observedAtMs: admitted.receipt.admittedAtMs + 1,
}),
'requeued',
);
assert.equal((await runs.findRunById(value.plan.runId)).status, 'running');
assert.equal(
(await runs.findAttemptById(admitted.receipt.attemptId)).status,
'lost',
);
const refreshed = await repository.listCandidates({ limit: 8 });
assert.equal(refreshed.candidates.length, 1);
assert.equal(refreshed.candidates[0].stepRunId, collect.stepRunId);
const second = await repository.admit(value.plan.runId, collect.stepRunId);
assert.equal(second.status, 'created');
assert.equal(second.receipt.attemptNumber, 2);
assert.notEqual(second.receipt.attemptId, admitted.receipt.attemptId);
});
test('rolls back when the exact generation has no reconciled execution revision', async (t) => {
const { client, value, repository } = await harness(
t,
'sqlite-workflow-task-attempt-unreconciled',
{ reconcile: false },
);
await assert.rejects(
repository.admit(value.plan.runId, value.plan.steps[0].stepRunId),
PluginPackageWorkflowTaskAttemptAdmissionConflictError,
);
assert.deepEqual(
{
...client
.prepare(
`SELECT version, event_sequence AS "eventSequence",
(SELECT COUNT(*) FROM "RunAttempts") AS attempts
FROM "Runs" WHERE id = ?`,
)
.get(value.plan.runId),
},
{ version: 3, eventSequence: 3, attempts: 0 },
);
});
test('fails closed on receipt drift and publishes only an explicit subpath', async (t) => {
const { client, value, repository } = await harness(
t,
'sqlite-workflow-task-attempt-drift',
);
const stepRunId = value.plan.steps[0].stepRunId;
await repository.admit(value.plan.runId, stepRunId);
client.exec('PRAGMA ignore_check_constraints = ON');
client
.prepare(
`UPDATE "QingLong3PluginPackageWorkflowTaskAttemptAdmissions"
SET receipt_json = json_set(receipt_json, '$.taskId', 'drift')
WHERE run_id = ? AND step_run_id = ?`,
)
.run(value.plan.runId, stepRunId);
await assert.rejects(
repository.admit(value.plan.runId, stepRunId),
PluginPackageWorkflowTaskAttemptAdmissionUnavailableError,
);
const subpath = require('@qinglong/local-sqlite/plugin-package-workflow-task-attempt-admission');
const root = require('../dist');
assert.equal(
subpath.LocalSqlitePluginPackageWorkflowTaskAttemptAdmissionRepository,
LocalSqlitePluginPackageWorkflowTaskAttemptAdmissionRepository,
);
assert.equal(
root.LocalSqlitePluginPackageWorkflowTaskAttemptAdmissionRepository,
undefined,
);
});
test('converges a cancelling local Workflow in bounded per-Run transactions', async (t) => {
const { client, value, repository } = await harness(
t,
'sqlite-workflow-cancellation',
);
const collect = value.plan.steps.find(({ stepKey }) => stepKey === 'collect');
assert.ok(collect);
await repository.admit(value.plan.runId, collect.stepRunId);
client
.prepare(
`UPDATE "Runs"
SET cancel_requested_at_ms = ?, cancel_reason = 'user'
WHERE id = ? AND status = 'running'`,
)
.run(4_000, value.plan.runId);
const cancellation =
new LocalSqlitePluginPackageWorkflowCancellationConvergenceRepository(
client,
);
assert.deepEqual(await cancellation.convergePage({ limit: 8 }), {
scanned: 1,
settledRuns: 1,
settledAttempts: 1,
blocked: 0,
hasMore: false,
});
assert.deepEqual(
{
...client
.prepare(
`SELECT status, version,
event_sequence AS "eventSequence",
(SELECT group_concat(status, ',')
FROM (
SELECT DISTINCT status FROM "RunAttempts"
WHERE run_id = run.id ORDER BY status
)) AS "attemptStatuses",
(SELECT group_concat(status, ',')
FROM (
SELECT DISTINCT status FROM "StepRuns"
WHERE run_id = run.id ORDER BY status
)) AS "stepStatuses",
(SELECT group_concat(type, ',')
FROM (
SELECT type FROM "RunEvents"
WHERE run_id = run.id AND sequence >= 4
ORDER BY sequence
)) AS events
FROM "Runs" AS run WHERE id = ?`,
)
.get(value.plan.runId),
},
{
status: 'cancelled',
version: 8,
eventSequence: 8,
attemptStatuses: 'cancelled',
stepStatuses: 'cancelled',
events: [
'workflow.task_attempt_admitted',
'workflow.task_attempt.cancelled',
'step.cancelled',
'step.cancelled',
'workflow.cancelled',
].join(','),
},
);
assert.deepEqual(await cancellation.convergePage({ limit: 8 }), {
scanned: 0,
settledRuns: 0,
settledAttempts: 0,
blocked: 0,
hasMore: false,
});
const subpath = require('@qinglong/local-sqlite/plugin-package-workflow-cancellation-convergence');
const root = require('../dist');
assert.equal(
subpath.LocalSqlitePluginPackageWorkflowCancellationConvergenceRepository,
LocalSqlitePluginPackageWorkflowCancellationConvergenceRepository,
);
assert.equal(
root.LocalSqlitePluginPackageWorkflowCancellationConvergenceRepository,
undefined,
);
});
@@ -0,0 +1,101 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { test } = require('node:test');
const {
CRASH_POINTS,
setupScenario,
verifyScenario,
} = require('./fixtures/pluginPackageWorkflowTaskControlCrashMatrixFixture.cjs');
const FIXTURE_PATH = path.join(
__dirname,
'fixtures',
'pluginPackageWorkflowTaskControlCrashMatrixFixture.cjs',
);
test(
'survives Workflow Task conclusive-stop and control-terminal crash windows',
{ timeout: 180_000 },
async (context) => {
const reports = [];
for (const profile of ['edge', 'standalone']) {
for (const [pointName, point] of Object.entries(CRASH_POINTS)) {
const directory = fs.mkdtempSync(
path.join(
os.tmpdir(),
`ql3-workflow-control-${profile}-${pointName}-`,
),
);
context.after(() => {
fs.rmSync(directory, { recursive: true, force: true });
});
const databasePath = path.join(directory, 'runtime.sqlite');
const markerPath = path.join(directory, 'crash-marker.json');
await setupScenario({ databasePath, profile });
const crashed = spawnSync(
process.execPath,
[FIXTURE_PATH, 'crash', databasePath, markerPath, pointName, profile],
{ encoding: 'utf8', timeout: 30_000 },
);
assert.equal(
crashed.error,
undefined,
`${profile}/${pointName}: ${crashed.error?.message}`,
);
assert.equal(
crashed.signal,
'SIGKILL',
`${profile}/${pointName}: status=${crashed.status}, stderr=${crashed.stderr}`,
);
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
assert.deepEqual(marker, {
schema:
'qinglong/sqlite-plugin-package-workflow-task-control-crash-marker@v1',
point: pointName,
conclusiveStopObserved: true,
pid: marker.pid,
});
const report = await verifyScenario({
databasePath,
pointName,
profile,
});
assert.equal(report.durableAfterCrash, point.durable);
reports.push(report);
}
}
assert.equal(reports.length, 16);
assert.equal(
reports.filter(({ crashBeforeCommit }) => crashBeforeCommit).length,
14,
);
assert.equal(
reports.filter(({ durableAfterCrash }) => durableAfterCrash).length,
2,
);
assert.deepEqual(
[...new Set(reports.map(({ journalMode }) => journalMode))].sort(),
['delete', 'wal'],
);
assert.ok(
reports.every(
({
crashAfterConclusiveStop,
exactTerminalReplay,
parentConverged,
integrityCheck,
foreignKeyCheck,
}) =>
crashAfterConclusiveStop &&
exactTerminalReplay &&
parentConverged &&
integrityCheck === 'ok' &&
foreignKeyCheck === 'ok',
),
);
},
);
@@ -0,0 +1,83 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
migrateLocalSqlitePath,
openLocalSqliteRuntimeDatabase,
} = require('../dist');
function run(id, projectId, createdAtMs) {
return {
id,
projectId,
taskId: `task-${id}`,
taskRevision: 'revision-1',
taskName: id,
triggerType: 'manual',
executionOrigin: 'manual',
executionOwner: 'runtime',
triggeredBy: 'test',
status: 'created',
version: 0,
eventSequence: 0,
priority: 0,
createdAtMs,
};
}
test('lists only one Project with descending keyset pagination', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-run-list-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const options = {
databasePath: path.join(directory, 'qinglong3.sqlite'),
profile: 'edge',
};
await migrateLocalSqlitePath(options);
const runtime = await openLocalSqliteRuntimeDatabase(options);
t.after(() => runtime.close());
await runtime.runRepository.transaction(async (transaction) => {
for (const value of [
run('run-a', 'default', 10),
run('run-b', 'default', 20),
run('run-c', 'default', 20),
run('run-other', 'other', 30),
]) {
await transaction.insertRun(value);
}
});
const first = await runtime.runRepository.listRunsByProject({
projectId: 'default',
limit: 2,
});
assert.deepEqual(
first.map(({ id }) => id),
['run-c', 'run-b'],
);
const second = await runtime.runRepository.listRunsByProject({
projectId: 'default',
limit: 2,
after: { createdAtMs: 20, runId: 'run-b' },
});
assert.deepEqual(
second.map(({ id }) => id),
['run-a'],
);
await assert.rejects(
runtime.runRepository.listRunsByProject({
projectId: 'default',
limit: 66,
}),
TypeError,
);
await assert.rejects(
runtime.runRepository.listRunsByProject({
projectId: 'default',
limit: 1,
after: { createdAtMs: 20, runId: 'run-b', extra: true },
}),
TypeError,
);
});
@@ -0,0 +1,118 @@
const assert = require('node:assert/strict');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
ProjectToolDefinitionSnapshotUnavailableError,
} = require('@qinglong/runtime-core/project-tool-definition-snapshot');
const {
registerProjectToolDefinitionSnapshotRepositoryContract,
projectToolDefinitionSnapshotForFixture,
} = require('../../../test/contracts/projectToolDefinitionSnapshotRepositoryContract.cjs');
const {
LocalSqliteOperationAuthority,
} = require('../dist/authority/operationAuthority');
const {
LocalSqlitePluginPackageInstallRepository,
} = require('../dist/plugin-package/pluginPackageInstallRepository');
const {
LocalSqlitePluginPackageMaterializedRevisionRepository,
} = require('../dist/plugin-package/pluginPackageMaterializedRevisionRepository');
const {
LocalSqliteProjectToolDefinitionSnapshotRepository,
} = require('../dist/tool-execution/projectToolDefinitionSnapshotRepository');
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
const {
activateInstall,
pluginPackageTaskReconciliationFixture,
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
async function createRepository(_t, fixture) {
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 (?, ?, ?, 'active', 1, 1, 1)`,
)
.run(fixture.projectId, fixture.projectId, fixture.projectId);
const authority = new LocalSqliteOperationAuthority(client);
return {
client,
repository:
new LocalSqliteProjectToolDefinitionSnapshotRepository(authority),
installRepository:
new LocalSqlitePluginPackageInstallRepository(authority),
materializedRepository:
new LocalSqlitePluginPackageMaterializedRevisionRepository(
authority,
fixture.registry,
),
close: () => authority.close(),
};
}
registerProjectToolDefinitionSnapshotRepositoryContract({
name: 'SQLite Project Tool Definition snapshot repository',
namespace: 'sqlite-tool-snapshot',
profile: 'edge',
createRepository,
assertDurableSource(harness, value) {
assert.deepEqual(
{
...harness.client
.prepare(
`SELECT installation_id AS "installationId",
generation_digest AS "generationDigest",
revision_digest AS "revisionDigest"
FROM "QingLong3ProjectToolDefinitionSnapshotSources"`,
)
.get(),
},
{
installationId: value.install.active.installationId,
generationDigest: value.revision.generation.generationDigest,
revisionDigest: value.revision.revisionDigest,
},
);
},
});
test('SQLite Project Tool Definition snapshot fails closed on source loss', async (t) => {
const value = pluginPackageTaskReconciliationFixture(
'sqlite-tool-snapshot-corrupt',
{ profile: 'edge' },
);
const harness = await createRepository(t, value);
t.after(() => harness.close());
await activateInstall(harness.installRepository, value);
await harness.materializedRepository.publish(value.revision);
await harness.repository.publish(
projectToolDefinitionSnapshotForFixture(value),
);
harness.client.exec('PRAGMA foreign_keys = OFF');
harness.client
.prepare(
`DELETE FROM "QingLong3ProjectToolDefinitionSnapshotSources"
WHERE project_id = ?`,
)
.run(value.projectId);
await assert.rejects(
harness.repository.findCurrent(value.projectId),
ProjectToolDefinitionSnapshotUnavailableError,
);
});
test('publishes snapshot storage only through the explicit subpath', () => {
const entrypoint = require('@qinglong/local-sqlite/project-tool-definition-snapshot');
assert.equal(
entrypoint.LocalSqliteProjectToolDefinitionSnapshotRepository,
LocalSqliteProjectToolDefinitionSnapshotRepository,
);
assert.equal(
require('../dist').LocalSqliteProjectToolDefinitionSnapshotRepository,
undefined,
);
});
@@ -0,0 +1,241 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const { migrateLocalSqlitePath } = require('../dist/migration/migration.js');
const {
checkpointLocalSqliteForRestore,
createLocalSqliteRolloutBackup,
inspectLocalSqliteRolloutBackup,
inspectLocalSqliteSnapshot,
LOCAL_SQLITE_WRITE_CONTRACT_VERSION,
openLocalSqliteChangeObserver,
restoreLocalSqliteSnapshot,
} = require('../dist/readiness/rolloutSafety.js');
function fixture(t, profile = 'edge') {
const root = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-rollout-safety-')),
);
fs.chmodSync(root, 0o700);
const backupRoot = path.join(root, 'backups');
fs.mkdirSync(backupRoot, { mode: 0o700 });
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
return {
root,
profile,
databasePath: path.join(root, 'qinglong3.sqlite'),
backupPath: path.join(backupRoot, 'rollout.sqlite'),
};
}
test('creates and exactly replays a reviewed rollout backup', async (t) => {
const state = fixture(t);
await migrateLocalSqlitePath(state);
const prepared = await createLocalSqliteRolloutBackup(state);
assert.equal(prepared.status, 'prepared');
assert.equal(prepared.contractVersion, 43);
assert.equal(prepared.writeContractVersion, 43);
assert.equal(LOCAL_SQLITE_WRITE_CONTRACT_VERSION, 43);
assert.match(prepared.sha256, /^[0-9a-f]{64}$/);
assert.equal(prepared.bytes > 0, true);
assert.equal(prepared.pageCount > 0, true);
assert.equal(prepared.pageSize >= 512, true);
assert.equal(fs.statSync(state.backupPath).mode & 0o777, 0o600);
const replay = await createLocalSqliteRolloutBackup(state);
assert.deepEqual(replay, { ...prepared, status: 'existing' });
assert.deepEqual(await inspectLocalSqliteRolloutBackup(state), replay);
const linkedStagePath = path.join(
path.dirname(state.backupPath),
`.${path.basename(state.backupPath)}.ql3-backup-stage`,
);
fs.linkSync(state.backupPath, linkedStagePath);
assert.equal(fs.statSync(state.backupPath).nlink, 2);
assert.deepEqual(await createLocalSqliteRolloutBackup(state), replay);
assert.equal(fs.existsSync(linkedStagePath), false);
assert.equal(fs.statSync(state.backupPath).nlink, 1);
const source = new DatabaseSync(state.databasePath);
source.exec('PRAGMA user_version = 7');
source.close();
assert.deepEqual(await inspectLocalSqliteRolloutBackup(state), replay);
});
test('observes external commits across an online standalone WAL backup', async (t) => {
const state = fixture(t, 'standalone');
await migrateLocalSqlitePath(state);
const observer = openLocalSqliteChangeObserver(state);
assert.equal(observer.changed(), false);
const writer = new DatabaseSync(state.databasePath);
writer.exec('PRAGMA user_version = 9');
writer.close();
assert.equal(observer.changed(), true);
const backup = await createLocalSqliteRolloutBackup(state);
assert.equal(backup.status, 'prepared');
observer.close();
observer.close();
assert.throws(() => observer.changed(), /observer is closed/);
});
test('recovers an incomplete stage and cleans a failed backup attempt', async (t) => {
const state = fixture(t);
await migrateLocalSqlitePath(state);
const stagePath = path.join(
path.dirname(state.backupPath),
`.${path.basename(state.backupPath)}.ql3-backup-stage`,
);
fs.writeFileSync(stagePath, 'incomplete', { mode: 0o600 });
const recovered = await createLocalSqliteRolloutBackup(state);
assert.equal(recovered.status, 'prepared');
assert.equal(fs.existsSync(stagePath), false);
const failed = fixture(t);
await migrateLocalSqlitePath(failed);
const failedStagePath = path.join(
path.dirname(failed.backupPath),
`.${path.basename(failed.backupPath)}.ql3-backup-stage`,
);
await assert.rejects(
createLocalSqliteRolloutBackup(failed, {
async performBackup(_source, target) {
fs.writeFileSync(target, 'partial', { mode: 0o600 });
throw Object.assign(new Error('no space left'), { code: 'ENOSPC' });
},
}),
/could not be created/,
);
assert.equal(fs.existsSync(failed.backupPath), false);
assert.equal(fs.existsSync(failedStagePath), false);
});
for (const profile of ['edge', 'standalone']) {
test(`checkpoints and restores one exact ${profile} snapshot`, async (t) => {
const state = fixture(t, profile);
await migrateLocalSqlitePath(state);
const source = await createLocalSqliteRolloutBackup(state);
const writer = new DatabaseSync(state.databasePath);
writer.exec('PRAGMA user_version = 19');
writer.close();
const current = await checkpointLocalSqliteForRestore(state);
assert.notEqual(current.sha256, source.sha256);
assert.deepEqual(await checkpointLocalSqliteForRestore(state), current);
assert.equal(fs.existsSync(`${state.databasePath}-wal`), false);
assert.equal(fs.existsSync(`${state.databasePath}-shm`), false);
const restoreStagePath = path.join(
state.root,
`.qinglong3.${profile}.restore-stage`,
);
const replacedDatabasePath = path.join(
path.dirname(state.backupPath),
`${profile}.replaced.sqlite`,
);
const restoreOptions = {
databasePath: state.databasePath,
profile,
sourceSnapshotPath: state.backupPath,
restoreStagePath,
replacedDatabasePath,
expectedCurrentSha256: current.sha256,
expectedSourceSha256: source.sha256,
};
const restored = await restoreLocalSqliteSnapshot(restoreOptions);
assert.equal(restored.status, 'restored');
assert.equal(restored.sha256, source.sha256);
assert.equal(fs.existsSync(restoreStagePath), false);
assert.equal(fs.existsSync(replacedDatabasePath), false);
assert.equal(
(await inspectLocalSqliteSnapshot(state)).sha256,
source.sha256,
);
assert.equal(
(await restoreLocalSqliteSnapshot(restoreOptions)).status,
'existing',
);
});
}
test('converges the moved-current restore window and cleans ENOSPC stage', async (t) => {
const state = fixture(t);
await migrateLocalSqlitePath(state);
const source = await createLocalSqliteRolloutBackup(state);
const writer = new DatabaseSync(state.databasePath);
writer.exec('PRAGMA user_version = 23');
writer.close();
const current = await checkpointLocalSqliteForRestore(state);
const restoreStagePath = path.join(state.root, '.restore-stage');
const replacedDatabasePath = path.join(
path.dirname(state.backupPath),
'replaced.sqlite',
);
const restoreOptions = {
databasePath: state.databasePath,
profile: state.profile,
sourceSnapshotPath: state.backupPath,
restoreStagePath,
replacedDatabasePath,
expectedCurrentSha256: current.sha256,
expectedSourceSha256: source.sha256,
};
await assert.rejects(
restoreLocalSqliteSnapshot(restoreOptions, {
copySnapshot(_sourcePath, targetPath) {
fs.writeFileSync(targetPath, 'partial', { mode: 0o600 });
throw Object.assign(new Error('injected restore ENOSPC'), {
code: 'ENOSPC',
});
},
}),
/restore stage could not be created/,
);
assert.equal(fs.existsSync(restoreStagePath), false);
assert.equal(
(await inspectLocalSqliteSnapshot(state)).sha256,
current.sha256,
);
fs.copyFileSync(state.backupPath, restoreStagePath);
fs.chmodSync(restoreStagePath, 0o600);
fs.renameSync(state.databasePath, replacedDatabasePath);
const recovered = await restoreLocalSqliteSnapshot(restoreOptions);
assert.equal(recovered.status, 'restored');
assert.equal(recovered.sha256, source.sha256);
assert.equal(fs.existsSync(replacedDatabasePath), false);
});
test('rollout safety subpath excludes DDL and mutable repositories', () => {
const script = `
const safety = require(${JSON.stringify(
path.resolve(__dirname, '../dist/readiness/rolloutSafety.js'),
)});
const loaded = Object.keys(require.cache)
.filter((entry) =>
/[\\/]migrations[\\/]|[\\/]migration\\.js$|runRepository\\.js$|pluginPackageInstallRepository\\.js$/.test(entry),
);
process.stdout.write(JSON.stringify({
backup: typeof safety.createLocalSqliteRolloutBackup,
checkpoint: typeof safety.checkpointLocalSqliteForRestore,
observer: typeof safety.openLocalSqliteChangeObserver,
restore: typeof safety.restoreLocalSqliteSnapshot,
loaded,
}));
`;
const result = spawnSync(process.execPath, ['-e', script], {
encoding: 'utf8',
});
assert.equal(result.status, 0, result.stderr);
assert.deepEqual(JSON.parse(result.stdout), {
backup: 'function',
checkpoint: 'function',
observer: 'function',
restore: 'function',
loaded: [],
});
});
@@ -0,0 +1,183 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
RunCancellationFenceRejectedError,
RunCancellationNotFoundError,
RunCancellationUnavailableError,
} = require('@qinglong/runtime-core/run-cancellation');
const {
LocalSqliteOperationAuthority,
} = require('../dist/authority/operationAuthority.js');
const {
LocalSqliteRunCancellationRepository,
} = require('../dist/run/runCancellationRepository.js');
const { migrateLocalSqlitePath } = require('../dist/migration/migration.js');
const NOW = 1_800_000_000_000;
const EVENT_ID = '018f0000-0000-7000-8000-000000000001';
function seed(client, runId = 'run-1', status = 'running') {
client
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings" (
"project_id", "subject_type", "subject_id", "version", "state",
"role", "mutation_id", "changed_by_type", "changed_by_id",
"created_at_ms"
) VALUES ('default', 'user', 'user-1', 1, 'active', 'operator',
'grant-operator', 'user', 'user-1', ?)`,
)
.run(NOW - 1_000);
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 (?, 'default', 'task-1', 'revision-1', 'manual', 'manual',
'runtime', ?, 1, 0, 0, ?)`,
)
.run(runId, status, NOW - 500);
}
function command(overrides = {}) {
return {
projectId: 'default',
runId: 'run-1',
mutationId: 'mutation-1',
eventId: EVENT_ID,
subject: { type: 'user', id: 'user-1' },
policyFence: { projectVersion: 1, bindingVersion: 1 },
...overrides,
};
}
async function fixture(t, status = 'running') {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-run-cancel-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const databasePath = path.join(root, 'qinglong3.sqlite');
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const client = new DatabaseSync(databasePath);
client.exec('PRAGMA foreign_keys = ON');
seed(client, 'run-1', status);
const authority = new LocalSqliteOperationAuthority(client);
t.after(() => authority.close());
return {
client,
repository: new LocalSqliteRunCancellationRepository(authority, () => NOW),
};
}
test('atomically publishes one durable cancellation intent and exact replay', async (t) => {
const { client, repository } = await fixture(t);
assert.deepEqual(await repository.requestUserCancellation(command()), {
status: 'accepted',
projectId: 'default',
runId: 'run-1',
runStatus: 'running',
runVersion: 2,
eventSequence: 1,
cancelRequestedAtMs: NOW,
cancelReason: 'user',
});
assert.equal(
(await repository.requestUserCancellation(
command({ eventId: '018f0000-0000-7000-8000-000000000002' }),
)).status,
'already_requested',
);
assert.deepEqual(
client
.prepare(
`SELECT "type", "dedupe_key" AS "dedupeKey", "actor_type" AS "actorType"
FROM "RunEvents" WHERE "run_id" = 'run-1'`,
)
.all()
.map((row) => ({ ...row })),
[
{
type: 'run.cancel_requested',
dedupeKey: 'user-cancel:mutation-1',
actorType: 'user',
},
],
);
});
test('returns terminal and masks missing or cross-Project Runs', async (t) => {
const { repository } = await fixture(t, 'succeeded');
assert.deepEqual(await repository.requestUserCancellation(command()), {
status: 'already_terminal',
projectId: 'default',
runId: 'run-1',
runStatus: 'succeeded',
runVersion: 1,
eventSequence: 0,
});
await assert.rejects(
repository.requestUserCancellation(command({ runId: 'missing' })),
RunCancellationNotFoundError,
);
await assert.rejects(
repository.requestUserCancellation(command({ projectId: 'other' })),
RunCancellationNotFoundError,
);
});
test('revalidates the latest RoleBinding inside the mutation transaction', async (t) => {
const { client, repository } = await fixture(t);
client
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings" (
"project_id", "subject_type", "subject_id", "version", "state",
"role", "mutation_id", "changed_by_type", "changed_by_id",
"created_at_ms"
) VALUES ('default', 'user', 'user-1', 2, 'revoked', NULL,
'revoke-operator', 'user', 'user-1', ?)`,
)
.run(NOW - 100);
await assert.rejects(
repository.requestUserCancellation(command()),
(error) =>
error instanceof RunCancellationFenceRejectedError &&
error.reason === 'authorization_changed',
);
assert.equal(
client
.prepare(`SELECT "cancel_requested_at_ms" AS value FROM "Runs" WHERE "id" = 'run-1'`)
.get().value,
null,
);
});
test('rolls back the Run mutation when Event persistence fails', async (t) => {
const { client, repository } = await fixture(t);
client
.prepare(
`INSERT INTO "RunEvents" (
"id", "run_id", "sequence", "type", "dedupe_key", "actor_type",
"payload", "created_at_ms"
) VALUES (?, 'run-1', 1, 'run.started', 'existing', 'system', '{}', ?)`,
)
.run(EVENT_ID, NOW - 1);
await assert.rejects(
repository.requestUserCancellation(command()),
RunCancellationUnavailableError,
);
assert.deepEqual(
{
...client
.prepare(
`SELECT "version", "event_sequence" AS "eventSequence",
"cancel_requested_at_ms" AS "cancelRequestedAtMs"
FROM "Runs" WHERE "id" = 'run-1'`,
)
.get(),
},
{ version: 1, eventSequence: 0, cancelRequestedAtMs: null },
);
});
@@ -0,0 +1,56 @@
require('ts-node/register/transpile-only');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const {
registerRunRepositoryContract,
} = require('../../../test/contracts/runRepositoryContract.cjs');
const {
DuplicateIdempotencyKeyError,
DuplicateRunAttemptError,
DuplicateRunEventError,
MAX_CANCELLATION_RECOVERY_PAGE_SIZE,
MAX_RUN_EVENT_PAGE_SIZE,
MAX_RUN_EVENT_PAYLOAD_BYTES,
RunEventPayloadTooLargeError,
} = require('@qinglong/runtime-core');
const {
migrateLocalSqlitePath,
openLocalSqliteRuntimeDatabase,
} = require('../dist');
registerRunRepositoryContract({
name: 'Node SQLite local Profile binding',
defaultExecutionOwner: 'runtime',
contract: {
DuplicateIdempotencyKeyError,
DuplicateRunAttemptError,
DuplicateRunEventError,
RunEventPayloadTooLargeError,
MAX_CANCELLATION_RECOVERY_PAGE_SIZE,
MAX_RUN_EVENT_PAGE_SIZE,
MAX_RUN_EVENT_PAYLOAD_BYTES,
},
async createRepository() {
const directory = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-local-repository-contract-'),
);
const options = {
databasePath: path.join(directory, 'qinglong3.sqlite'),
profile: 'edge',
};
await migrateLocalSqlitePath(options);
const runtime = await openLocalSqliteRuntimeDatabase(options);
return {
repository: runtime.runRepository,
async close() {
try {
await runtime.close();
} finally {
fs.rmSync(directory, { recursive: true, force: true });
}
},
};
},
});
@@ -0,0 +1,85 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const { LocalSqliteRunRepository } = require('../dist');
const FORBIDDEN_SECURITY_METHODS = Object.freeze([
'resolveProjectPolicy',
'appendProjectRoleBinding',
'record',
'resolveLocalSecretAdministrationMutation',
'appendAuthorizedLocalSecretEnvelope',
'appendLocalSecretEnvelope',
'findLocalSecretEnvelopeByMutation',
'resolveLocalSecretEnvelopes',
]);
const FORBIDDEN_RUNTIME_CAPABILITY_METHODS = Object.freeze([
'inspectCandidates',
'listLocalDispatchCandidates',
'listLocalExecutionControlCandidates',
'listLocalActiveExecutions',
'resolveLocalTaskExecutionRevision',
'resolveLocalExecutionContextRecipe',
'appendLocalExecutionContextRecipe',
'appendLocalTaskExecutionRevision',
'register',
'markQuarantined',
'resolve',
'listCandidates',
]);
test('Run facade excludes Policy, Audit and Secret authorities', () => {
for (const method of FORBIDDEN_SECURITY_METHODS) {
assert.equal(
Object.hasOwn(LocalSqliteRunRepository.prototype, method),
false,
`${method} must remain owned by the Security authority`,
);
}
const declaration = fs.readFileSync(
path.join(__dirname, '../dist/run/runRepository.d.ts'),
'utf8',
);
for (const method of FORBIDDEN_SECURITY_METHODS) {
assert.doesNotMatch(declaration, new RegExp(`\\b${method}\\b`, 'u'));
}
assert.doesNotMatch(
declaration,
/local-secret|project-policy|security-audit|SecurityAuthorityStore/u,
);
const runtime = fs.readFileSync(
path.join(__dirname, '../dist/run/runRepository.js'),
'utf8',
);
assert.doesNotMatch(
runtime,
/securityAuthorityStore|local-secret|project-policy|security-audit/u,
);
});
test('Run repository excludes Dispatch, Control, Recovery and Receipt capabilities', () => {
for (const method of FORBIDDEN_RUNTIME_CAPABILITY_METHODS) {
assert.equal(
Object.hasOwn(LocalSqliteRunRepository.prototype, method),
false,
`${method} must remain on its least-authority runtime capability`,
);
}
const declaration = fs.readFileSync(
path.join(__dirname, '../dist/run/runRepository.d.ts'),
'utf8',
);
for (const method of FORBIDDEN_RUNTIME_CAPABILITY_METHODS) {
assert.doesNotMatch(declaration, new RegExp(`\\b${method}\\b`, 'u'));
}
assert.doesNotMatch(
declaration,
/LocalDispatchStore|LocalExecutionControlSource|LocalRunStartupRecoverySource|LocalCompletionReceiptJournal/u,
);
});
@@ -0,0 +1,257 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
resolveLocalScheduleDecision,
} = require('@qinglong/runtime-core/local-scheduler');
const {
migrateLocalSqlitePath,
openLocalSqliteRuntimeDatabase,
} = require('../dist');
function nextMinute(schedule, afterMs) {
if (schedule.expression !== '* * * * *' || schedule.timezone !== 'UTC') {
throw new Error('unsupported test schedule');
}
return Math.floor(afterMs / 60_000 + 1) * 60_000;
}
function id(value) {
return `019f7400-0000-4000-8000-${String(value).padStart(12, '0')}`;
}
async function fixture(t) {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-schedule-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const databasePath = path.join(directory, 'qinglong3.sqlite');
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const runtime = await openLocalSqliteRuntimeDatabase({
databasePath,
profile: 'edge',
});
t.after(() => runtime.close());
const task = (
await runtime.taskDefinitions.appendTaskDefinitionRevision({
projectId: 'default',
taskId: 'task-1',
expectedRevision: null,
mutationId: '019f7410-0000-7000-8000-000000000001',
name: 'Scheduled task',
kind: 'command',
spec: {
schema: 'qinglong/command@v1',
config: {
command: { kind: 'argv', file: '/bin/echo', args: ['scheduled'] },
},
},
labels: {},
enabled: true,
occurredAtMs: 1,
})
).definition;
await runtime.triggers.appendTriggerRevision({
projectId: 'default',
triggerId: 'trigger-1',
expectedRevision: null,
mutationId: '019f7420-0000-7000-8000-000000000001',
taskId: task.taskId,
taskRevision: task.revision,
taskContentDigest: task.contentDigest,
spec: {
schema: 'qinglong/cron@v1',
config: {
expression: '* * * * *',
timezone: 'UTC',
misfirePolicy: 'skip',
},
},
enabled: true,
occurredAtMs: 1,
});
return runtime;
}
test('atomically advances one due Trigger into a queued Run aggregate', async (t) => {
const runtime = await fixture(t);
const page = await runtime.schedules.listLocalScheduleCandidates({
observedAtMs: 61_000,
limit: 4,
});
assert.equal(page.candidates.length, 1);
assert.equal(page.truncated, false);
assert.equal(page.candidates[0].nextFireAtMs, null);
const decision = resolveLocalScheduleDecision(
page.candidates[0],
61_000,
5_000,
nextMinute,
);
const admitted = await runtime.schedules.commitLocalScheduleDecision({
decision,
runId: id(1),
attemptId: id(2),
createdEventId: id(3),
queuedEventId: id(4),
});
assert.deepEqual(admitted, {
status: 'admitted',
disposition: 'admit',
runId: id(1),
attemptId: id(2),
});
const run = await runtime.runRepository.findRunById(id(1));
assert.equal(run.status, 'queued');
assert.equal(run.executionOwner, 'runtime');
assert.equal(run.executionOrigin, 'scheduled_system');
assert.equal(run.triggerId, 'trigger-1');
assert.equal(run.scheduledForMs, 60_000);
assert.match(run.taskRevision, /^qltd:v1:1:[a-f0-9]{64}$/);
assert.equal(
(await runtime.runRepository.findAttemptById(id(2))).status,
'claimed',
);
assert.deepEqual(
(await runtime.runRepository.listEvents(id(1))).map((event) => event.type),
['run.created', 'run.queued'],
);
assert.equal(
(
await runtime.schedules.listLocalScheduleCandidates({
observedAtMs: 61_000,
limit: 4,
})
).candidates.length,
0,
);
assert.deepEqual(
await runtime.schedules.commitLocalScheduleDecision({
decision,
runId: id(5),
attemptId: id(6),
createdEventId: id(7),
queuedEventId: id(8),
}),
{ status: 'raced' },
);
});
test('fences a stale Task head before schedule discovery and final Run commit', async (t) => {
const runtime = await fixture(t);
const page = await runtime.schedules.listLocalScheduleCandidates({
observedAtMs: 61_000,
limit: 4,
});
const decision = resolveLocalScheduleDecision(
page.candidates[0],
61_000,
5_000,
nextMinute,
);
const task = await runtime.taskDefinitions.findCurrentTaskDefinition(
'default',
'task-1',
);
const disabled = (
await runtime.taskDefinitions.appendTaskDefinitionRevision({
projectId: task.projectId,
taskId: task.taskId,
expectedRevision: task.revision,
mutationId: '019f7410-0000-7000-8000-000000000002',
name: task.name,
kind: task.kind,
spec: task.spec,
labels: task.labels,
enabled: false,
occurredAtMs: 2,
})
).definition;
assert.equal(
(
await runtime.schedules.listLocalScheduleCandidates({
observedAtMs: 61_000,
limit: 4,
})
).candidates.length,
0,
);
assert.deepEqual(
await runtime.schedules.commitLocalScheduleDecision({
decision,
runId: id(21),
attemptId: id(22),
createdEventId: id(23),
queuedEventId: id(24),
}),
{ status: 'raced' },
);
assert.equal(await runtime.runRepository.findRunById(id(21)), null);
await assert.rejects(
runtime.triggers.appendTriggerRevision({
projectId: 'default',
triggerId: 'trigger-1',
expectedRevision: 1,
mutationId: '019f7420-0000-7000-8000-000000000002',
taskId: task.taskId,
taskRevision: task.revision,
taskContentDigest: task.contentDigest,
spec: {
schema: 'qinglong/cron@v1',
config: {
expression: '* * * * *',
timezone: 'UTC',
misfirePolicy: 'skip',
},
},
enabled: true,
occurredAtMs: 3,
}),
{ code: 'TRIGGER_CONFLICT' },
);
const enabled = (
await runtime.taskDefinitions.appendTaskDefinitionRevision({
projectId: disabled.projectId,
taskId: disabled.taskId,
expectedRevision: disabled.revision,
mutationId: '019f7410-0000-7000-8000-000000000003',
name: disabled.name,
kind: disabled.kind,
spec: disabled.spec,
labels: disabled.labels,
enabled: true,
occurredAtMs: 4,
})
).definition;
await runtime.triggers.appendTriggerRevision({
projectId: 'default',
triggerId: 'trigger-1',
expectedRevision: 1,
mutationId: '019f7420-0000-7000-8000-000000000003',
taskId: enabled.taskId,
taskRevision: enabled.revision,
taskContentDigest: enabled.contentDigest,
spec: {
schema: 'qinglong/cron@v1',
config: {
expression: '* * * * *',
timezone: 'UTC',
misfirePolicy: 'skip',
},
},
enabled: true,
occurredAtMs: 5,
});
assert.equal(
(
await runtime.schedules.listLocalScheduleCandidates({
observedAtMs: 61_000,
limit: 4,
})
).candidates.length,
1,
);
});
@@ -0,0 +1,122 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const { getTableName } = require('drizzle-orm');
const { getTableConfig } = require('drizzle-orm/sqlite-core');
const { migrateLocalSqlitePath } = require('../dist/migration/migration');
const { localSqliteSchema } = require('../dist/storage/schema');
function sorted(values) {
return [...values].sort((left, right) =>
JSON.stringify(left).localeCompare(JSON.stringify(right)),
);
}
function tableSql(client, tableName) {
const row = client
.prepare(
`SELECT sql FROM sqlite_schema WHERE type = 'table' AND name = ?`,
)
.get(tableName);
assert.equal(typeof row?.sql, 'string');
return row.sql;
}
function catalogChecks(sql) {
return [...sql.matchAll(/CONSTRAINT\s+([A-Za-z0-9_]+)\s+CHECK\b/gi)].map(
(match) => match[1],
);
}
function catalogForeignKeys(client, tableName) {
const grouped = new Map();
for (const entry of client
.prepare(`PRAGMA foreign_key_list("${tableName}")`)
.all()) {
const current = grouped.get(entry.id) ?? {
columns: [],
foreignTable: entry.table,
foreignColumns: [],
onDelete: entry.on_delete,
onUpdate: entry.on_update,
};
current.columns[entry.seq] = entry.from;
current.foreignColumns[entry.seq] = entry.to;
grouped.set(entry.id, current);
}
return [...grouped.values()];
}
function drizzleForeignKeys(config) {
return config.foreignKeys.map((foreignKey) => {
const reference = foreignKey.reference();
return {
columns: reference.columns.map(({ name }) => name),
foreignTable: getTableName(reference.foreignTable),
foreignColumns: reference.foreignColumns.map(({ name }) => name),
onDelete: (foreignKey.onDelete ?? 'no action').toUpperCase(),
onUpdate: (foreignKey.onUpdate ?? 'no action').toUpperCase(),
};
});
}
test('typed SQLite schema matches every reviewed table, column, index, check and foreign key', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-schema-lockstep-'));
const databasePath = path.join(directory, 'qinglong3.sqlite');
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const client = new DatabaseSync(databasePath, { readOnly: true });
try {
const actualTables = client
.prepare(
`SELECT name FROM sqlite_schema
WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name`,
)
.all()
.map(({ name }) => name);
const drizzleTables = Object.values(localSqliteSchema).map(getTableConfig);
assert.deepEqual(
actualTables,
drizzleTables.map(({ name }) => name).sort(),
);
for (const config of drizzleTables) {
const columns = client
.prepare(`PRAGMA table_info("${config.name}")`)
.all()
.map(({ name }) => name);
assert.deepEqual(
columns,
config.columns.map(({ name }) => name),
`${config.name} columns`,
);
const indexes = client
.prepare(`PRAGMA index_list("${config.name}")`)
.all()
.map(({ name }) => name)
.filter((name) => !name.startsWith('sqlite_autoindex_'));
assert.deepEqual(
indexes.sort(),
config.indexes.map(({ config: value }) => value.name).sort(),
`${config.name} indexes`,
);
assert.deepEqual(
catalogChecks(tableSql(client, config.name)).sort(),
config.checks.map(({ name }) => name).sort(),
`${config.name} checks`,
);
assert.deepEqual(
sorted(catalogForeignKeys(client, config.name)),
sorted(drizzleForeignKeys(config)),
`${config.name} foreign keys`,
);
}
} finally {
client.close();
}
});
@@ -0,0 +1,192 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
LocalSqliteSecurityAuditQueryRepository,
} = require('@qinglong/local-sqlite/security-audit-query');
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
const {
LocalSqliteOperationAuthority,
} = require('@qinglong/local-sqlite/operation-authority');
const {
LocalSecurityAuditQueryAuthorizationFenceConflictError,
} = require('@qinglong/runtime-core/local-security-audit-query');
async function fixture(t) {
const directory = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-security-audit-query-'),
);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const databasePath = path.join(directory, 'qinglong3.sqlite');
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const client = new DatabaseSync(databasePath);
client.exec('PRAGMA foreign_keys = ON');
const now = 10_000;
client
.prepare(
`INSERT INTO "QingLong3Projects" (
"id", "name", "slug", "status", "version",
"created_at_ms", "updated_at_ms"
) VALUES ('project-alpha', 'Project Alpha', 'project-alpha',
'active', 1, ?, ?)`,
)
.run(now - 200, now - 200);
client
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings" (
"project_id", "subject_type", "subject_id", "version", "state",
"role", "mutation_id", "changed_by_type", "changed_by_id",
"created_at_ms"
) VALUES (
'default', 'user', 'owner-user', 1, 'active', 'owner', ?,
'user', 'owner-user', ?
)`,
)
.run('96000000-0000-4000-8000-000000000001', now - 100);
const insertAudit = client.prepare(
`INSERT INTO "QingLong3SecurityAuditEvents" (
"event_id", "request_id", "operation_id", "project_id",
"subject_type", "subject_id", "authentication_id", "outcome",
"reasons_json", "fence_project_version", "fence_binding_version",
"occurred_at_ms"
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
);
for (const [eventId, occurredAtMs, outcome, subjectId] of [
['97000000-0000-4000-8000-000000000003', 9_003, 'denied', 'planner'],
['97000000-0000-4000-8000-000000000002', 9_002, 'denied', 'planner'],
['97000000-0000-4000-8000-000000000001', 9_001, 'denied', 'planner'],
['97000000-0000-4000-8000-000000000000', 9_000, 'allowed', 'planner'],
]) {
insertAudit.run(
eventId,
`request-${occurredAtMs}`,
'tool.invoke',
'project-alpha',
'agent',
subjectId,
`private-auth-${occurredAtMs}`,
outcome,
'["policy_result"]',
1,
1,
occurredAtMs,
);
}
const authority = new LocalSqliteOperationAuthority(client);
t.after(() => authority.close());
return {
client,
repository: new LocalSqliteSecurityAuditQueryRepository(
authority,
() => {},
),
};
}
function authorization(overrides = {}) {
return {
authorityProjectId: 'default',
actor: { type: 'user', id: 'owner-user' },
fence: { projectVersion: 1, bindingVersion: 1 },
...overrides,
};
}
function queryAudit(eventId) {
return {
eventId,
requestId: `audit-query-${eventId.at(-1)}`,
operationId: 'security.audit.list',
projectId: 'default',
subject: { type: 'user', id: 'owner-user' },
authenticationId: 'local_security_audit:test',
outcome: 'allowed',
reasons: ['instance_authority_security_audit_query'],
fence: { projectVersion: 1, bindingVersion: 1 },
occurredAtMs: 10_000 + Number(eventId.at(-1)),
};
}
test('filters and keyset-pages a pre-audit snapshot with an exact has-more cursor', async (t) => {
const value = await fixture(t);
const filter = {
projectId: 'project-alpha',
subject: { type: 'agent', id: 'planner' },
outcome: 'denied',
};
const first = await value.repository.listAuthorized({
query: { limit: 2, filter },
authorization: authorization(),
audit: queryAudit('98000000-0000-4000-8000-000000000001'),
});
assert.deepEqual(
first.records.map((record) => record.eventId),
[
'97000000-0000-4000-8000-000000000003',
'97000000-0000-4000-8000-000000000002',
],
);
assert.deepEqual(first.nextCursor, {
occurredAtMs: 9_002,
eventId: '97000000-0000-4000-8000-000000000002',
});
assert.equal(
first.records.some(
(record) => record.eventId === '98000000-0000-4000-8000-000000000001',
),
false,
);
const second = await value.repository.listAuthorized({
query: { limit: 2, before: first.nextCursor, filter },
authorization: authorization(),
audit: queryAudit('98000000-0000-4000-8000-000000000002'),
});
assert.deepEqual(
second.records.map((record) => record.eventId),
['97000000-0000-4000-8000-000000000001'],
);
assert.equal(second.nextCursor, null);
assert.equal(
value.client
.prepare(
`SELECT count(*) AS "count"
FROM "QingLong3SecurityAuditEvents"
WHERE "operation_id" = 'security.audit.list'
AND "outcome" = 'allowed'`,
)
.get().count,
2,
);
});
test('rejects a foreign instance authority before reading or auditing rows', async (t) => {
const value = await fixture(t);
await assert.rejects(
value.repository.listAuthorized({
query: { limit: 1, filter: {} },
authorization: authorization({
authorityProjectId: 'project-alpha',
}),
audit: {
...queryAudit('99000000-0000-4000-8000-000000000001'),
projectId: 'project-alpha',
},
}),
LocalSecurityAuditQueryAuthorizationFenceConflictError,
);
assert.equal(
value.client
.prepare(
`SELECT count(*) AS "count"
FROM "QingLong3SecurityAuditEvents"
WHERE "event_id" = ?`,
)
.get('99000000-0000-4000-8000-000000000001').count,
0,
);
});
@@ -0,0 +1,322 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
const {
LocalSqliteOperationAuthority,
} = require('@qinglong/local-sqlite/operation-authority');
const {
LocalSqliteSecurityAuditRetentionRepository,
} = require('@qinglong/local-sqlite/security-audit-retention');
const {
LocalSecurityAuditCompactionMutationConflictError,
LocalSecurityAuditRetentionAuthorizationFenceConflictError,
MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS,
localSecurityAuditCompactionPayload,
} = require('@qinglong/runtime-core/local-security-audit-retention');
const NOW = 4_000_000_000;
const CUTOFF = NOW - MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS;
function audit(eventId, occurredAtMs, outcome, operationId = 'tool.invoke') {
return {
eventId,
requestId: `request-${eventId}`,
operationId,
projectId: 'default',
subject: { type: 'user', id: 'owner-user' },
authenticationId: 'local_security_audit:test',
outcome,
reasons: ['test_reason'],
fence: { projectVersion: 1, bindingVersion: 1 },
occurredAtMs,
};
}
function insertAudit(client, value) {
client
.prepare(
`INSERT INTO "QingLong3SecurityAuditEvents" (
"event_id", "request_id", "operation_id", "project_id",
"subject_type", "subject_id", "authentication_id", "outcome",
"reasons_json", "fence_project_version", "fence_binding_version",
"occurred_at_ms"
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
value.eventId,
value.requestId,
value.operationId,
value.projectId,
value.subject.type,
value.subject.id,
value.authenticationId,
value.outcome,
JSON.stringify(value.reasons),
value.fence.projectVersion,
value.fence.bindingVersion,
value.occurredAtMs,
);
}
async function fixture(t, profile = 'edge') {
const directory = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-security-audit-retention-'),
);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const databasePath = path.join(directory, 'qinglong3.sqlite');
await migrateLocalSqlitePath({ databasePath, profile });
const client = new DatabaseSync(databasePath);
client.exec('PRAGMA foreign_keys = ON');
client
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings" (
"project_id", "subject_type", "subject_id", "version", "state",
"role", "mutation_id", "changed_by_type", "changed_by_id",
"created_at_ms"
) VALUES (
'default', 'user', 'owner-user', 1, 'active', 'owner', ?,
'user', 'owner-user', ?
)`,
)
.run('b1000000-0000-4000-8000-000000000001', NOW - 1_000);
const authority = new LocalSqliteOperationAuthority(client);
t.after(() => authority.close());
return {
client,
repository: new LocalSqliteSecurityAuditRetentionRepository(
authority,
() => {},
profile === 'edge' ? 64 : 512,
),
};
}
function authorization(overrides = {}) {
return {
authorityProjectId: 'default',
actor: { type: 'user', id: 'owner-user' },
fence: { projectVersion: 1, bindingVersion: 1 },
...overrides,
};
}
function command(mutationId, overrides = {}) {
return {
mutationId,
requestId: `compact-${mutationId}`,
retentionMs: MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS,
eligibleBeforeMs: CUTOFF,
limit: 64,
authorization: authorization(),
audit: {
...audit(mutationId, NOW, 'allowed', 'security.audit.compact'),
requestId: `compact-${mutationId}`,
reasons: ['instance_authority_security_audit_compaction'],
},
...overrides,
};
}
function existingIds(client) {
return client
.prepare(
`SELECT "event_id" AS "eventId"
FROM "QingLong3SecurityAuditEvents"
ORDER BY "event_id"`,
)
.all()
.map((row) => row.eventId);
}
test('deletes only unreferenced expired diagnostic audit and keeps an immutable receipt', async (t) => {
const value = await fixture(t);
const denied = audit(
'b2000000-0000-4000-8000-000000000001',
CUTOFF - 4,
'denied',
);
const diagnostic = audit(
'b2000000-0000-4000-8000-000000000002',
CUTOFF - 3,
'allowed',
'security.audit.list',
);
const allowedMutation = audit(
'b2000000-0000-4000-8000-000000000003',
CUTOFF - 2,
'allowed',
'policy.project.create',
);
const referenced = audit(
'b2000000-0000-4000-8000-000000000004',
CUTOFF - 1,
'denied',
);
const recent = audit(
'b2000000-0000-4000-8000-000000000005',
CUTOFF,
'denied',
);
for (const record of [
denied,
diagnostic,
allowedMutation,
referenced,
recent,
]) {
insertAudit(value.client, record);
}
value.client
.prepare(
`INSERT INTO "QingLong3LegacyAdoptions" (
"mutation_id", "decision_id", "project_id", "profile",
"plan_digest", "inventory_digest", "decision_digest",
"receipt_digest", "authorization_file_digest",
"publication_digest", "row_count", "adopted_task_count",
"adopted_trigger_count", "skipped_count", "audit_event_id",
"created_at_ms"
) VALUES (?, ?, 'default', 'edge', ?, ?, ?, ?, ?, ?, 0, 0, 0, 0, ?, ?)`,
)
.run(
referenced.eventId,
'b2000000-0000-7000-8000-000000000004',
...Array(6).fill('a'.repeat(64)),
referenced.eventId,
referenced.occurredAtMs,
);
const mutationId = 'b3000000-0000-4000-8000-000000000001';
const result = await value.repository.compactAuthorized(command(mutationId));
assert.equal(result.status, 'inserted');
assert.equal(result.record.deletedCount, 2);
assert.deepEqual(result.record.first, {
occurredAtMs: denied.occurredAtMs,
eventId: denied.eventId,
});
assert.deepEqual(result.record.last, {
occurredAtMs: diagnostic.occurredAtMs,
eventId: diagnostic.eventId,
});
assert.deepEqual(
{
recordsDigest: result.record.recordsDigest,
payloadBytes: result.record.deletedPayloadBytes,
},
localSecurityAuditCompactionPayload([denied, diagnostic]),
);
assert.deepEqual(existingIds(value.client), [
allowedMutation.eventId,
referenced.eventId,
recent.eventId,
mutationId,
]);
assert.equal(
value.client
.prepare(
`SELECT count(*) AS "count"
FROM "QingLong3SecurityAuditCompactions"
WHERE "mutation_id" = ? AND "audit_event_id" = ?`,
)
.get(mutationId, mutationId).count,
1,
);
});
test('exactly replays a batch and rejects semantic drift', async (t) => {
const value = await fixture(t);
insertAudit(
value.client,
audit('b4000000-0000-4000-8000-000000000001', CUTOFF - 1, 'denied'),
);
const mutationId = 'b5000000-0000-4000-8000-000000000001';
const input = command(mutationId);
const inserted = await value.repository.compactAuthorized(input);
const replay = await value.repository.compactAuthorized(input);
assert.equal(inserted.status, 'inserted');
assert.equal(replay.status, 'existing');
assert.deepEqual(replay.record, inserted.record);
await assert.rejects(
value.repository.compactAuthorized({
...input,
requestId: 'compact-drift',
audit: { ...input.audit, requestId: 'compact-drift' },
}),
LocalSecurityAuditCompactionMutationConflictError,
);
});
test('honors the hard batch cap and advances only through fresh mutations', async (t) => {
const value = await fixture(t);
const firstCandidate = audit(
'b8000000-0000-4000-8000-000000000001',
CUTOFF - 2,
'denied',
);
const secondCandidate = audit(
'b8000000-0000-4000-8000-000000000002',
CUTOFF - 1,
'denied',
);
insertAudit(value.client, firstCandidate);
insertAudit(value.client, secondCandidate);
const first = await value.repository.compactAuthorized(
command('b9000000-0000-4000-8000-000000000001', { limit: 1 }),
);
assert.equal(first.record.deletedCount, 1);
assert.deepEqual(existingIds(value.client), [
secondCandidate.eventId,
first.record.mutationId,
]);
const second = await value.repository.compactAuthorized(
command('b9000000-0000-4000-8000-000000000002', { limit: 1 }),
);
assert.equal(second.record.deletedCount, 1);
assert.deepEqual(existingIds(value.client), [
first.record.mutationId,
second.record.mutationId,
]);
const empty = await value.repository.compactAuthorized(
command('b9000000-0000-4000-8000-000000000003', { limit: 1 }),
);
assert.equal(empty.record.deletedCount, 0);
assert.equal(empty.record.deletedPayloadBytes, 0);
assert.equal(empty.record.first, null);
assert.equal(empty.record.last, null);
});
test('rejects a foreign authority without deleting or writing a receipt', async (t) => {
const value = await fixture(t);
const candidate = audit(
'b6000000-0000-4000-8000-000000000001',
CUTOFF - 1,
'denied',
);
insertAudit(value.client, candidate);
const input = command('b7000000-0000-4000-8000-000000000001');
await assert.rejects(
value.repository.compactAuthorized({
...input,
authorization: authorization({ authorityProjectId: 'foreign' }),
audit: { ...input.audit, projectId: 'foreign' },
}),
LocalSecurityAuditRetentionAuthorizationFenceConflictError,
);
assert.deepEqual(existingIds(value.client), [candidate.eventId]);
assert.equal(
value.client
.prepare(
`SELECT count(*) AS "count"
FROM "QingLong3SecurityAuditCompactions"`,
)
.get().count,
0,
);
});
@@ -0,0 +1,172 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
createSqlitePersistencePrimitives,
isSqliteDriverError,
sqliteDriverErrorCode,
sqliteDriverErrorMessage,
sqliteDriverErrorNumber,
} = require('../dist/storage/sqlitePersistence');
const runPersistence = require('../dist/run/runPersistence');
const securityPersistence = require('../dist/security/securityPersistence');
class AdapterError extends Error {}
function primitives() {
return createSqlitePersistencePrimitives({
invalidRowValue: (property) => new AdapterError(`invalid:${property}`),
invalidJson: (property) => new AdapterError(`json:${property}`),
unsupportedRowValue: (property) =>
new AdapterError(`unsupported:${property}`),
duplicateIdentityRows: () => new AdapterError('duplicate'),
mapDriverError: (error) => new AdapterError('driver', { cause: error }),
});
}
test('domain-neutral row primitives preserve scalar, JSON and blob semantics', () => {
const persistence = primitives();
const source = Uint8Array.from([1, 2, 3]);
const row = {
text: 'value',
empty: '',
absent: null,
integer: 42,
unsafe: Number.MAX_SAFE_INTEGER + 1,
enabled: 1,
disabled: 0,
json: '{"ok":true}',
brokenJson: '{',
blob: source,
state: 'ready',
};
assert.equal(persistence.requiredString(row, 'text'), 'value');
assert.equal(persistence.optionalString(row, 'empty'), '');
assert.equal(persistence.optionalString(row, 'absent'), undefined);
assert.equal(persistence.requiredInteger(row, 'integer'), 42);
assert.equal(persistence.optionalInteger(row, 'absent'), undefined);
assert.equal(persistence.requiredBoolean(row, 'enabled'), true);
assert.equal(persistence.requiredBoolean(row, 'disabled'), false);
assert.deepEqual(persistence.requiredJson(row, 'json'), { ok: true });
assert.equal(
persistence.requiredEnum(row, 'state', ['ready', 'done']),
'ready',
);
const blob = persistence.requiredBlob(row, 'blob');
assert.deepEqual(blob, Buffer.from([1, 2, 3]));
source[0] = 9;
assert.deepEqual(blob, Buffer.from([1, 2, 3]));
assert.throws(
() => persistence.requiredString(row, 'empty'),
/invalid:empty/,
);
assert.throws(
() => persistence.requiredInteger(row, 'unsafe'),
/invalid:unsafe/,
);
assert.throws(
() => persistence.requiredJson(row, 'brokenJson'),
/json:brokenJson/,
);
assert.throws(
() => persistence.requiredEnum(row, 'state', ['done']),
/unsupported:state/,
);
});
test('domain-neutral query primitives delegate all errors to the boundary contract', () => {
const persistence = primitives();
const expected = [{ id: 'one' }];
const client = {
prepare(sql) {
assert.equal(sql, 'SELECT ?');
return {
all(...values) {
assert.deepEqual(values, ['one']);
return expected;
},
};
},
};
assert.deepEqual(
persistence.queryRows(client, 'SELECT ?', ['one']),
expected,
);
assert.equal(persistence.singleRow(expected), expected[0]);
assert.equal(persistence.singleRow([]), null);
assert.throws(
() => persistence.singleRow([expected[0], { id: 'two' }]),
/duplicate/,
);
const driverFailure = new Error('sqlite failure');
const failingClient = {
prepare() {
throw driverFailure;
},
};
assert.throws(
() => persistence.queryRows(failingClient, 'SELECT 1'),
(error) =>
error instanceof AdapterError &&
error.message === 'driver' &&
error.cause === driverFailure,
);
});
test('SQLite driver observation remains domain neutral', () => {
const error = Object.assign(new Error('busy'), {
code: 'ERR_SQLITE_ERROR',
errcode: 5,
});
assert.equal(sqliteDriverErrorCode(error), 'ERR_SQLITE_ERROR');
assert.equal(sqliteDriverErrorNumber(error), 5);
assert.equal(sqliteDriverErrorMessage(error), 'busy');
assert.equal(isSqliteDriverError(error), true);
assert.equal(isSqliteDriverError(new Error('other')), false);
assert.equal(sqliteDriverErrorMessage('other'), '');
});
test('Run and Security adapters freeze the existing corruption contract', () => {
for (const persistence of [runPersistence, securityPersistence]) {
assert.throws(
() => persistence.requiredString({ value: '' }, 'value'),
(error) =>
error.name === 'RunRepositoryConstraintError' &&
error.message === 'Local SQLite Run row has an invalid value',
);
assert.throws(
() => persistence.requiredBlob({ value: 'not-a-blob' }, 'value'),
(error) =>
error.name === 'RunRepositoryConstraintError' &&
error.message === 'Local SQLite row has an invalid value',
);
assert.throws(
() => persistence.singleRow([{ id: 'one' }, { id: 'two' }]),
(error) =>
error.name === 'RunRepositoryConstraintError' &&
error.message ===
'Local SQLite Run repository returned duplicate identity rows',
);
const constraint = persistence.mapSqliteError({
code: 'ERR_SQLITE_CONSTRAINT',
});
assert.equal(constraint.name, 'RunRepositoryConstraintError');
assert.equal(
constraint.message,
'Local SQLite Run repository constraint violation',
);
assert.equal(
persistence.mapSqliteError({ errcode: 5 }).name,
'RunRepositoryBusyError',
);
assert.equal(
persistence.mapSqliteError(new Error('other')).name,
'RunRepositoryOperationError',
);
}
});
@@ -0,0 +1,133 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
MAX_LOCAL_RUN_STARTUP_RECOVERY_CANDIDATES,
migrateLocalSqlitePath,
openLocalSqliteRuntimeDatabase,
} = require('../dist');
async function database(t) {
const directory = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-local-startup-recovery-'),
);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const databasePath = path.join(directory, 'qinglong3.sqlite');
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const opened = await openLocalSqliteRuntimeDatabase({
databasePath,
profile: 'edge',
});
t.after(() => opened.close());
return opened;
}
function run(id, status, executionOwner = 'runtime') {
return {
id,
projectId: 'default',
taskId: `task-${id}`,
taskRevision: `revision-${id}`,
triggerType: 'manual',
executionOrigin: 'manual',
executionOwner,
status,
version: 0,
eventSequence: 0,
priority: 0,
createdAtMs: 1,
};
}
function attempt(id, runId, status = 'running') {
return {
id,
runId,
attempt: 1,
status,
executorType: 'local_process',
callbackSequence: 0,
createdAtMs: 1,
};
}
test('finds only runtime-owned dispatching or running Runs', async (t) => {
const opened = await database(t);
await opened.runRepository.transaction(async (transaction) => {
await transaction.insertRun(run('run-1', 'running'));
await transaction.insertAttempt(attempt('attempt-1', 'run-1'));
await transaction.insertRun(run('run-2', 'dispatching'));
await transaction.insertRun(run('run-3', 'succeeded'));
await transaction.insertRun(run('run-4', 'running', 'legacy'));
});
const page = await opened.startupRecovery.inspectCandidates();
assert.deepEqual(page, {
candidates: [
{
runId: 'run-1',
runStatus: 'running',
activeAttemptCount: 1,
},
{
runId: 'run-2',
runStatus: 'dispatching',
activeAttemptCount: 0,
},
],
truncated: false,
});
});
test('returns a bounded deterministic page and explicit truncation', async (t) => {
const opened = await database(t);
await opened.runRepository.transaction(async (transaction) => {
await transaction.insertRun(run('run-3', 'running'));
await transaction.insertRun(run('run-1', 'running'));
await transaction.insertRun(run('run-2', 'running'));
});
assert.deepEqual(
await opened.startupRecovery.inspectCandidates({ limit: 2 }),
{
candidates: [
{
runId: 'run-1',
runStatus: 'running',
activeAttemptCount: 0,
},
{
runId: 'run-2',
runStatus: 'running',
activeAttemptCount: 0,
},
],
truncated: true,
},
);
});
test('rejects invalid limits and shares repository close fencing', async (t) => {
const opened = await database(t);
await assert.rejects(
opened.startupRecovery.inspectCandidates({ limit: 0 }),
/limit must be between 1/,
);
await assert.rejects(
opened.startupRecovery.inspectCandidates({
limit: MAX_LOCAL_RUN_STARTUP_RECOVERY_CANDIDATES + 1,
}),
/limit must be between 1/,
);
await opened.close();
await assert.rejects(
opened.startupRecovery.inspectCandidates(),
(error) =>
error &&
error.name === 'RunRepositoryOperationError' &&
error.cause instanceof Error &&
error.cause.message === 'Local SQLite Run repository is closed',
);
});
@@ -0,0 +1,413 @@
const assert = require('node:assert/strict');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
StepRunFenceConflictError,
StepRunMutationConflictError,
StepRunStateConflictError,
createStepRunMutation,
transitionStepRunMutation,
} = 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 DEFINITION_DIGEST = 'a'.repeat(64);
async function harness() {
const client = new DatabaseSync(':memory:');
client.exec('PRAGMA foreign_keys = ON');
await migrateLocalSqliteDatabase(client);
const authority = new LocalSqliteOperationAuthority(client);
return {
client,
authority,
repository: new LocalSqliteStepRunRepository(authority),
close: () => authority.close(),
};
}
function insertRun(client, id, version = 0, eventSequence = 0) {
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 (?, 'project-001', 'task-001', 'revision-001', 'manual',
'manual', 'runtime', 'running', ?, ?, 0, 1)`,
)
.run(id, version, eventSequence);
}
function createMutation(options = {}) {
const {
id = 'step-run-001',
runId = 'run-001',
stepKey = 'workflow.fetch',
mutationId = 'step-create-001',
eventId = 'event-step-001',
dedupeKey = `step-create:${id}`,
expectedRunVersion = 0,
expectedRunEventSequence = 0,
parentStepRunId,
initialStatus = 'pending',
createdAtMs = 1_000,
} = options;
return createStepRunMutation(
{
id,
runId,
...(parentStepRunId === undefined ? {} : { parentStepRunId }),
stepKey,
kind: 'tool',
definitionRef: 'tool:demo.compare@1.0.0',
definitionDigest: DEFINITION_DIGEST,
required: true,
initialStatus,
inputRef: `artifact:${id}:input`,
mutationId,
createdAtMs,
},
{
expectedRunVersion,
expectedRunEventSequence,
eventId,
dedupeKey,
actor: { type: 'agent', id: 'agent-001' },
},
);
}
function transitionMutation(current, to, runVersion, eventSequence, options = {}) {
const mutationId =
options.mutationId ?? `step-${to}-${current.version + 1}`;
return transitionStepRunMutation(
current,
{
expectedVersion: current.version,
expectedDigest: current.stepRunDigest,
mutationId,
to,
atMs: options.atMs ?? current.updatedAtMs + 100,
...(options.approvalRequestId === undefined
? {}
: { approvalRequestId: options.approvalRequestId }),
...(options.outputRef === undefined
? {}
: { outputRef: options.outputRef }),
...(options.resultCode === undefined
? {}
: { resultCode: options.resultCode }),
...(options.errorSummary === undefined
? {}
: { errorSummary: options.errorSummary }),
},
{
expectedRunVersion: runVersion,
expectedRunEventSequence: eventSequence,
eventId: options.eventId ?? `event-${to}-${current.version + 1}`,
dedupeKey:
options.dedupeKey ?? `step-${to}:${current.id}:${current.version + 1}`,
actor: { type: 'agent', id: 'agent-001' },
},
);
}
test('atomically creates, transitions and replays historical StepRun mutations', async (t) => {
const value = await harness();
t.after(() => value.close());
insertRun(value.client, 'run-001');
const created = createMutation();
assert.deepEqual(await value.repository.apply(created), {
status: 'applied',
stepRun: created.stepRun,
runVersion: 1,
runEventSequence: 1,
});
assert.deepEqual(await value.repository.findById(created.stepRun.id), created.stepRun);
assert.deepEqual(
await value.repository.findByRunAndStepKey('run-001', 'workflow.fetch'),
created.stepRun,
);
assert.deepEqual(await value.repository.apply(created), {
status: 'existing',
stepRun: created.stepRun,
runVersion: 1,
runEventSequence: 1,
});
const ready = transitionMutation(created.stepRun, 'ready', 1, 1);
assert.deepEqual(await value.repository.apply(ready), {
status: 'applied',
stepRun: ready.stepRun,
runVersion: 2,
runEventSequence: 2,
});
assert.deepEqual(await value.repository.findById(created.stepRun.id), ready.stepRun);
assert.deepEqual(await value.repository.apply(created), {
status: 'existing',
stepRun: created.stepRun,
runVersion: 1,
runEventSequence: 1,
});
assert.deepEqual(
{
...value.client
.prepare(
`SELECT version, event_sequence AS "eventSequence"
FROM "Runs" WHERE id = 'run-001'`,
)
.get(),
},
{ version: 2, eventSequence: 2 },
);
assert.deepEqual(
{
...value.client
.prepare(
`SELECT COUNT(*) AS events,
COUNT(DISTINCT dedupe_key) AS dedupeKeys
FROM "RunEvents" WHERE run_id = 'run-001'`,
)
.get(),
},
{ events: 2, dedupeKeys: 2 },
);
assert.equal(
value.client
.prepare('SELECT COUNT(*) AS count FROM "StepRunMutations"')
.get().count,
2,
);
});
test('rolls the whole aggregate back on a stale Run fence', async (t) => {
const value = await harness();
t.after(() => value.close());
insertRun(value.client, 'run-001', 3, 4);
const stale = createMutation({
expectedRunVersion: 2,
expectedRunEventSequence: 4,
});
await assert.rejects(
value.repository.apply(stale),
StepRunFenceConflictError,
);
assert.deepEqual(
{
...value.client
.prepare(
`SELECT
(SELECT COUNT(*) FROM "StepRuns") AS stepRuns,
(SELECT COUNT(*) FROM "RunEvents") AS events,
(SELECT COUNT(*) FROM "StepRunMutations") AS mutations,
(SELECT version FROM "Runs" WHERE id = 'run-001') AS runVersion`,
)
.get(),
},
{ stepRuns: 0, events: 0, mutations: 0, runVersion: 3 },
);
});
test('rejects StepRun mutation after the Run aggregate is terminal', async (t) => {
const value = await harness();
t.after(() => value.close());
insertRun(value.client, 'run-001');
value.client
.prepare(`UPDATE "Runs" SET status = 'succeeded' WHERE id = 'run-001'`)
.run();
await assert.rejects(
value.repository.apply(createMutation()),
StepRunStateConflictError,
);
assert.equal(
value.client
.prepare('SELECT COUNT(*) AS count FROM "StepRuns"')
.get().count,
0,
);
});
test('rejects step-key collisions, missing parents and mutation identity reuse', async (t) => {
const value = await harness();
t.after(() => value.close());
insertRun(value.client, 'run-001');
const first = createMutation();
await value.repository.apply(first);
const collision = createMutation({
id: 'step-run-002',
stepKey: first.stepRun.stepKey,
mutationId: 'step-create-002',
eventId: 'event-step-002',
expectedRunVersion: 1,
expectedRunEventSequence: 1,
});
await assert.rejects(
value.repository.apply(collision),
StepRunStateConflictError,
);
const missingParent = createMutation({
id: 'step-run-003',
stepKey: 'workflow.child',
parentStepRunId: 'step-run-missing',
mutationId: 'step-create-003',
eventId: 'event-step-003',
expectedRunVersion: 1,
expectedRunEventSequence: 1,
});
await assert.rejects(
value.repository.apply(missingParent),
StepRunStateConflictError,
);
const reused = createMutation({
id: 'step-run-reused',
stepKey: 'workflow.reused',
mutationId: first.mutationId,
eventId: 'event-step-reused',
dedupeKey: 'step-create:step-run-reused',
expectedRunVersion: 1,
expectedRunEventSequence: 1,
});
await assert.rejects(
value.repository.apply(reused),
StepRunMutationConflictError,
);
assert.equal(
value.client
.prepare('SELECT COUNT(*) AS count FROM "StepRuns"')
.get().count,
1,
);
});
test('lists StepRuns with stable keyset pagination', async (t) => {
const value = await harness();
t.after(() => value.close());
insertRun(value.client, 'run-001');
let runVersion = 0;
let eventSequence = 0;
for (const [index, stepKey] of ['workflow.c', 'workflow.a', 'workflow.b'].entries()) {
const mutation = createMutation({
id: `step-run-00${index + 1}`,
stepKey,
mutationId: `step-create-00${index + 1}`,
eventId: `event-step-00${index + 1}`,
dedupeKey: `step-create:00${index + 1}`,
expectedRunVersion: runVersion,
expectedRunEventSequence: eventSequence,
createdAtMs: 1_000 + index,
});
await value.repository.apply(mutation);
runVersion += 1;
eventSequence += 1;
}
const first = await value.repository.listByRun({
runId: 'run-001',
limit: 2,
});
assert.deepEqual(first.stepRuns.map((item) => item.stepKey), [
'workflow.a',
'workflow.b',
]);
assert.equal(first.truncated, true);
assert.deepEqual(first.next, {
stepKey: 'workflow.b',
id: 'step-run-003',
});
const second = await value.repository.listByRun({
runId: 'run-001',
limit: 2,
after: first.next,
});
assert.deepEqual(second.stepRuns.map((item) => item.stepKey), [
'workflow.c',
]);
assert.equal(second.truncated, false);
assert.equal(second.next, undefined);
});
test('reviewed guards bind Attempt and Event StepRun references to the same Run', async (t) => {
const value = await harness();
t.after(() => value.close());
insertRun(value.client, 'run-001');
insertRun(value.client, 'run-002');
const created = createMutation();
await value.repository.apply(created);
assert.throws(
() =>
value.client
.prepare(
`INSERT INTO "RunAttempts" (
id, run_id, step_run_id, attempt, status, executor_type,
callback_sequence, created_at_ms
) VALUES (
'attempt-cross-run', 'run-002', 'step-run-001', 1, 'claimed',
'local_process', 0, 1
)`,
)
.run(),
/StepRun reference mismatch/,
);
assert.throws(
() =>
value.client
.prepare(
`INSERT INTO "RunEvents" (
id, run_id, sequence, type, dedupe_key, actor_type, step_run_id,
payload, created_at_ms
) VALUES (
'event-cross-run', 'run-002', 1, 'step.test',
'event-cross-run', 'system', 'step-run-001', '{}', 1
)`,
)
.run(),
/StepRun reference mismatch/,
);
});
test('binds every mutable StepRun mirror column to its digested JSON record', async (t) => {
const value = await harness();
t.after(() => value.close());
insertRun(value.client, 'run-001');
const created = createMutation();
await value.repository.apply(created);
assert.throws(
() =>
value.client
.prepare(
`UPDATE "StepRuns"
SET definition_ref = 'tool:tampered@1.0.0'
WHERE id = 'step-run-001'`,
)
.run(),
/CHECK constraint failed/,
);
assert.deepEqual(
await value.repository.findById('step-run-001'),
created.stepRun,
);
});
test('publishes the repository only through the explicit step-run subpath', () => {
const entrypoint = require('@qinglong/local-sqlite/step-run');
assert.equal(
entrypoint.LocalSqliteStepRunRepository,
LocalSqliteStepRunRepository,
);
assert.equal(require('../dist').LocalSqliteStepRunRepository, undefined);
});
@@ -0,0 +1,475 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
createTaskDefinitionRecord,
TaskDefinitionConflictError,
TaskDefinitionUnavailableError,
} = require('@qinglong/runtime-core/task-definition');
const {
UnsupportedTaskSpecError,
createBuiltInTaskSpecSemanticRegistry,
createTaskSpecSemanticRegistry,
} = require('@qinglong/runtime-core/task-spec-semantic');
const {
compileLocalCommandTaskDefinition,
} = require('@qinglong/runtime-core/task-definition-execution-compiler');
const {
createLocalTaskExecutionRevision,
} = require('@qinglong/runtime-core/local-dispatch');
const {
migrateLocalSqlitePath,
openLocalSqliteRuntimeDatabase,
} = require('../dist');
function fixture(t) {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-task-def-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
return path.join(directory, 'qinglong3.sqlite');
}
function command(index, overrides = {}) {
const suffix = String(index).padStart(12, '0');
return {
projectId: 'default',
taskId: `task-${String(index).padStart(5, '0')}`,
expectedRevision: null,
mutationId: `019f7200-0000-7000-8000-${suffix}`,
name: `Task ${index}`,
kind: 'command',
spec: {
schema: 'qinglong/command@v1',
config: {
command: { kind: 'argv', file: '/bin/echo', args: [String(index)] },
},
},
labels: { source: 'contract-test' },
enabled: true,
occurredAtMs: 100 + index,
...overrides,
};
}
test('creates, versions and resolves immutable TaskDefinitions', async (t) => {
const databasePath = fixture(t);
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const runtime = await openLocalSqliteRuntimeDatabase({
databasePath,
profile: 'edge',
});
t.after(() => runtime.close());
const created = await runtime.taskDefinitions.appendTaskDefinitionRevision(
command(1),
);
assert.equal(created.status, 'created');
assert.equal(created.definition.revision, 1);
const createdTaskRevision = `qltd:v1:1:${created.definition.contentDigest}`;
const executionRevision =
await runtime.localDispatch.resolveLocalTaskExecutionRevision({
projectId: created.definition.projectId,
taskId: created.definition.taskId,
taskRevision: createdTaskRevision,
});
assert.ok(executionRevision);
assert.match(executionRevision.contentDigest, /^[a-f0-9]{64}$/);
assert.equal(
(
await runtime.localDispatch.resolveLocalExecutionContextRecipe(
executionRevision.contextRef,
)
).contextRef,
executionRevision.contextRef,
);
assert.equal(
(await runtime.taskDefinitions.appendTaskDefinitionRevision(command(1)))
.status,
'existing',
);
const updatedCommand = command(1, {
expectedRevision: 1,
mutationId: '019f7200-0000-7000-8000-000000010001',
name: 'Task 1 updated',
enabled: false,
occurredAtMs: 200,
});
const updated = await runtime.taskDefinitions.appendTaskDefinitionRevision(
updatedCommand,
);
assert.equal(updated.status, 'updated');
assert.equal(updated.definition.revision, 2);
assert.equal(updated.definition.createdAtMs, 101);
assert.equal(updated.definition.updatedAtMs, 200);
assert.equal(
await runtime.localDispatch.resolveLocalTaskExecutionRevision({
projectId: updated.definition.projectId,
taskId: updated.definition.taskId,
taskRevision: `qltd:v1:2:${updated.definition.contentDigest}`,
}),
null,
);
assert.equal(
(
await runtime.taskDefinitions.findCurrentTaskDefinition(
'default',
'task-00001',
)
).name,
'Task 1 updated',
);
assert.equal(
(
await runtime.taskDefinitions.findTaskDefinitionRevision(
'default',
'task-00001',
1,
)
).name,
'Task 1',
);
});
test('rejects unknown semantics before mutation and accepts explicit composition', async (t) => {
const builtInPath = fixture(t);
await migrateLocalSqlitePath({ databasePath: builtInPath, profile: 'edge' });
const builtIn = await openLocalSqliteRuntimeDatabase({
databasePath: builtInPath,
profile: 'edge',
});
t.after(() => builtIn.close());
const customCommand = command(1, {
kind: 'tool',
spec: { schema: 'example/tool@v1', config: { entrypoint: 'probe' } },
});
await assert.rejects(
async () =>
builtIn.taskDefinitions.appendTaskDefinitionRevision(customCommand),
UnsupportedTaskSpecError,
);
assert.equal(
await builtIn.taskDefinitions.findCurrentTaskDefinition(
customCommand.projectId,
customCommand.taskId,
),
null,
);
const customPath = fixture(t);
await migrateLocalSqlitePath({ databasePath: customPath, profile: 'edge' });
const registry = createTaskSpecSemanticRegistry([
{
schema: 'example/tool@v1',
kind: 'tool',
normalizeConfig(config) {
return Object.freeze({
entrypoint: String(config.entrypoint).toUpperCase(),
});
},
},
]);
const custom = await openLocalSqliteRuntimeDatabase(
{ databasePath: customPath, profile: 'edge' },
{ taskSpecSemanticRegistry: registry },
);
t.after(() => custom.close());
const created = await custom.taskDefinitions.appendTaskDefinitionRevision(
customCommand,
);
assert.equal(created.status, 'created');
assert.equal(created.definition.spec.config.entrypoint, 'PROBE');
await custom.close();
const historicalReader = await openLocalSqliteRuntimeDatabase({
databasePath: customPath,
profile: 'edge',
});
t.after(() => historicalReader.close());
assert.equal(
(
await historicalReader.taskDefinitions.findCurrentTaskDefinition(
customCommand.projectId,
customCommand.taskId,
)
).spec.config.entrypoint,
'PROBE',
);
await assert.rejects(
async () =>
historicalReader.taskDefinitions.appendTaskDefinitionRevision(
customCommand,
),
UnsupportedTaskSpecError,
);
});
test('publishes TaskDefinition and local execution facts in one transaction', async (t) => {
const databasePath = fixture(t);
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const runtime = await openLocalSqliteRuntimeDatabase({
databasePath,
profile: 'edge',
});
t.after(() => runtime.close());
const input = command(20);
const registry = createBuiltInTaskSpecSemanticRegistry();
const canonicalInput = {
...input,
spec: registry.normalize({
projectId: input.projectId,
taskId: input.taskId,
kind: input.kind,
spec: input.spec,
}),
};
const definition = createTaskDefinitionRecord(
canonicalInput,
input.occurredAtMs,
);
const plan = compileLocalCommandTaskDefinition(definition, registry);
assert.equal(
await runtime.localDispatch.appendLocalExecutionContextRecipe(
plan.contextRecipe,
),
'inserted',
);
const conflictingRevision = createLocalTaskExecutionRevision({
...plan.executionRevision,
command: { kind: 'argv', file: '/bin/echo', args: ['conflict'] },
});
assert.equal(
await runtime.localDispatch.appendLocalTaskExecutionRevision(
conflictingRevision,
),
'inserted',
);
await assert.rejects(
runtime.taskDefinitions.appendTaskDefinitionRevision(input),
TaskDefinitionConflictError,
);
assert.equal(
await runtime.taskDefinitions.findCurrentTaskDefinition(
input.projectId,
input.taskId,
),
null,
);
const client = new DatabaseSync(databasePath, { readOnly: true });
try {
assert.equal(
client
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3TaskDefinitionRevisions"
WHERE "project_id" = ? AND "task_id" = ?`,
)
.get(input.projectId, input.taskId).count,
0,
);
} finally {
client.close();
}
});
test('exact TaskDefinition replay fails closed when a published execution fact is missing', async (t) => {
const databasePath = fixture(t);
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const runtime = await openLocalSqliteRuntimeDatabase({
databasePath,
profile: 'edge',
});
t.after(() => runtime.close());
const input = command(21);
const created = await runtime.taskDefinitions.appendTaskDefinitionRevision(
input,
);
const taskRevision = `qltd:v1:1:${created.definition.contentDigest}`;
const client = new DatabaseSync(databasePath);
try {
client
.prepare(
`DELETE FROM "QingLong3LocalTaskExecutionRevisions"
WHERE "project_id" = ? AND "task_id" = ? AND "task_revision" = ?`,
)
.run(input.projectId, input.taskId, taskRevision);
} finally {
client.close();
}
await assert.rejects(
runtime.taskDefinitions.appendTaskDefinitionRevision(input),
TaskDefinitionUnavailableError,
);
assert.equal(
await runtime.localDispatch.resolveLocalTaskExecutionRevision({
projectId: input.projectId,
taskId: input.taskId,
taskRevision,
}),
null,
);
});
test('fences stale revisions, mutation drift and archived Projects', async (t) => {
const databasePath = fixture(t);
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const runtime = await openLocalSqliteRuntimeDatabase({
databasePath,
profile: 'edge',
});
t.after(() => runtime.close());
await runtime.taskDefinitions.appendTaskDefinitionRevision(command(1));
await assert.rejects(
runtime.taskDefinitions.appendTaskDefinitionRevision(
command(1, { name: 'drifted replay' }),
),
TaskDefinitionConflictError,
);
await assert.rejects(
runtime.taskDefinitions.appendTaskDefinitionRevision(
command(1, {
expectedRevision: 2,
mutationId: '019f7200-0000-7000-8000-000000010002',
}),
),
TaskDefinitionConflictError,
);
const client = new DatabaseSync(databasePath);
client.exec(
`UPDATE "QingLong3Projects" SET "status" = 'archived', "version" = 2,
"updated_at_ms" = 300 WHERE "id" = 'default'`,
);
client.close();
await assert.rejects(
runtime.taskDefinitions.appendTaskDefinitionRevision(command(2)),
TaskDefinitionConflictError,
);
});
test('paginates current definitions and allows only one concurrent revision', async (t) => {
const databasePath = fixture(t);
await migrateLocalSqlitePath({ databasePath, profile: 'standalone' });
const first = await openLocalSqliteRuntimeDatabase({
databasePath,
profile: 'standalone',
});
const second = await openLocalSqliteRuntimeDatabase({
databasePath,
profile: 'standalone',
});
t.after(() => Promise.all([first.close(), second.close()]));
for (let index = 1; index <= 3; index += 1) {
await first.taskDefinitions.appendTaskDefinitionRevision(command(index));
}
const page = await first.taskDefinitions.listTaskDefinitions({
projectId: 'default',
limit: 2,
});
assert.deepEqual(
page.definitions.map(({ taskId }) => taskId),
['task-00001', 'task-00002'],
);
assert.equal(page.truncated, true);
assert.deepEqual(
(
await first.taskDefinitions.listTaskDefinitions({
projectId: 'default',
limit: 2,
after: page.next,
})
).definitions.map(({ taskId }) => taskId),
['task-00003'],
);
const results = await Promise.allSettled([
first.taskDefinitions.appendTaskDefinitionRevision(
command(1, {
expectedRevision: 1,
mutationId: '019f7200-0000-7000-8000-000000020001',
name: 'winner-a',
occurredAtMs: 500,
}),
),
second.taskDefinitions.appendTaskDefinitionRevision(
command(1, {
expectedRevision: 1,
mutationId: '019f7200-0000-7000-8000-000000020002',
name: 'winner-b',
occurredAtMs: 500,
}),
),
]);
assert.equal(
results.filter(({ status }) => status === 'fulfilled').length,
1,
);
assert.equal(results.filter(({ status }) => status === 'rejected').length, 1);
assert.ok(
results.find(({ status }) => status === 'rejected').reason instanceof
TaskDefinitionConflictError,
);
});
test('fails closed when a durable revision digest is corrupt', async (t) => {
const databasePath = fixture(t);
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const runtime = await openLocalSqliteRuntimeDatabase({
databasePath,
profile: 'edge',
});
t.after(() => runtime.close());
await runtime.taskDefinitions.appendTaskDefinitionRevision(command(1));
const client = new DatabaseSync(databasePath);
client.exec(
`UPDATE "QingLong3TaskDefinitionRevisions"
SET "content_digest" = '${'0'.repeat(64)}'`,
);
client.close();
await assert.rejects(
runtime.taskDefinitions.findCurrentTaskDefinition('default', 'task-00001'),
TaskDefinitionUnavailableError,
);
});
test('fails closed when a durable execution revision digest is corrupt', async (t) => {
const databasePath = fixture(t);
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const runtime = await openLocalSqliteRuntimeDatabase({
databasePath,
profile: 'edge',
});
t.after(() => runtime.close());
const input = command(22);
const created = await runtime.taskDefinitions.appendTaskDefinitionRevision(
input,
);
const taskRevision = `qltd:v1:1:${created.definition.contentDigest}`;
const client = new DatabaseSync(databasePath);
client
.prepare(
`UPDATE "QingLong3LocalTaskExecutionRevisions"
SET "content_digest" = ?
WHERE "project_id" = ? AND "task_id" = ? AND "task_revision" = ?`,
)
.run('0'.repeat(64), input.projectId, input.taskId, taskRevision);
client.close();
await assert.rejects(
runtime.localDispatch.resolveLocalTaskExecutionRevision({
projectId: input.projectId,
taskId: input.taskId,
taskRevision,
}),
/digest does not match/,
);
await assert.rejects(
runtime.taskDefinitions.appendTaskDefinitionRevision(input),
TaskDefinitionUnavailableError,
);
});
@@ -0,0 +1,242 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
TaskStartFenceRejectedError,
TaskStartNotFoundError,
TaskStartUnavailableError,
} = require('@qinglong/runtime-core/task-start');
const {
migrateLocalSqlitePath,
openLocalSqliteRuntimeDatabase,
} = require('../dist');
const NOW = 1_800_000_000_000;
const IDS = [
'019f7300-0000-7000-8000-000000000101',
'019f7300-0000-7000-8000-000000000102',
'019f7300-0000-7000-8000-000000000103',
'019f7300-0000-7000-8000-000000000104',
];
function definition(index = 1, overrides = {}) {
return {
projectId: 'default',
taskId: `task-${index}`,
expectedRevision: null,
mutationId: `019f7300-0000-7000-8000-${String(index).padStart(12, '0')}`,
name: `Task ${index}`,
kind: 'command',
spec: {
schema: 'qinglong/command@v1',
config: {
command: { kind: 'argv', file: '/bin/echo', args: [String(index)] },
},
},
labels: {},
enabled: true,
occurredAtMs: 100 + index,
...overrides,
};
}
function command(record, overrides = {}) {
return {
projectId: 'default',
taskId: record.taskId,
mutationId: '019f7300-0000-7000-8000-000000000100',
expectedRevision: record.revision,
expectedContentDigest: record.contentDigest,
runId: IDS[0],
attemptId: IDS[1],
createdEventId: IDS[2],
queuedEventId: IDS[3],
subject: { type: 'user', id: 'user-1' },
policyFence: { projectVersion: 1, bindingVersion: 1 },
...overrides,
};
}
async function fixture(t) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-task-start-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const databasePath = path.join(root, 'qinglong3.sqlite');
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const client = new DatabaseSync(databasePath);
client.prepare(`
INSERT INTO "QingLong3ProjectRoleBindings" (
"project_id", "subject_type", "subject_id", "version", "state",
"role", "mutation_id", "changed_by_type", "changed_by_id",
"created_at_ms"
) VALUES ('default', 'user', 'user-1', 1, 'active', 'operator',
'grant-operator', 'user', 'user-1', ?)
`).run(NOW - 1_000);
client.close();
const runtime = await openLocalSqliteRuntimeDatabase({
databasePath,
profile: 'edge',
});
t.after(() => runtime.close());
const repository = await runtime.taskStartRepository();
return { databasePath, runtime, repository };
}
test('atomically creates queued Run, claimed Attempt and two Events, then replays', async (t) => {
const { databasePath, runtime, repository } = await fixture(t);
const record = (
await runtime.taskDefinitions.appendTaskDefinitionRevision(definition())
).definition;
const accepted = await repository.startTask(command(record));
assert.deepEqual(accepted, {
status: 'accepted',
projectId: 'default',
taskId: 'task-1',
taskRevision: 1,
taskContentDigest: record.contentDigest,
runId: IDS[0],
attemptId: IDS[1],
runStatus: 'queued',
runVersion: 2,
eventSequence: 2,
executorType: 'local_process',
executionRevisionDigest: accepted.executionRevisionDigest,
createdAtMs: accepted.createdAtMs,
});
assert.match(accepted.executionRevisionDigest, /^[0-9a-f]{64}$/);
const replay = await repository.startTask(command(record, {
runId: '019f7300-0000-7000-8000-000000000201',
attemptId: '019f7300-0000-7000-8000-000000000202',
createdEventId: '019f7300-0000-7000-8000-000000000203',
queuedEventId: '019f7300-0000-7000-8000-000000000204',
}));
assert.equal(replay.status, 'existing');
assert.equal(replay.runId, IDS[0]);
assert.equal(replay.attemptId, IDS[1]);
const client = new DatabaseSync(databasePath, { readOnly: true });
t.after(() => client.close());
assert.deepEqual({ ...client.prepare(`
SELECT "status", "version", "event_sequence" AS "eventSequence",
"execution_origin" AS "executionOrigin",
"trigger_type" AS "triggerType"
FROM "Runs" WHERE "id" = ?
`).get(IDS[0]) }, {
status: 'queued',
version: 2,
eventSequence: 2,
executionOrigin: 'manual',
triggerType: 'task_start',
});
assert.equal(client.prepare(
`SELECT COUNT(*) AS count FROM "RunEvents" WHERE "run_id" = ?`,
).get(IDS[0]).count, 2);
});
test('serializes concurrent retries to one durable Run', async (t) => {
const { runtime, repository } = await fixture(t);
const record = (
await runtime.taskDefinitions.appendTaskDefinitionRevision(definition())
).definition;
const [left, right] = await Promise.all([
repository.startTask(command(record)),
repository.startTask(command(record, {
runId: '019f7300-0000-7000-8000-000000000301',
attemptId: '019f7300-0000-7000-8000-000000000302',
createdEventId: '019f7300-0000-7000-8000-000000000303',
queuedEventId: '019f7300-0000-7000-8000-000000000304',
})),
]);
assert.deepEqual([left.status, right.status].sort(), ['accepted', 'existing']);
assert.equal(left.runId, right.runId);
});
test('rejects missing, changed, disabled and conflicting Task fences', async (t) => {
const { runtime, repository } = await fixture(t);
const record = (
await runtime.taskDefinitions.appendTaskDefinitionRevision(definition())
).definition;
await assert.rejects(
repository.startTask(command({ ...record, taskId: 'missing' })),
TaskStartNotFoundError,
);
await assert.rejects(
repository.startTask(command(record, { expectedRevision: 2 })),
(error) =>
error instanceof TaskStartFenceRejectedError &&
error.reason === 'definition_changed',
);
const disabled = (
await runtime.taskDefinitions.appendTaskDefinitionRevision(
definition(2, { enabled: false }),
)
).definition;
await assert.rejects(
repository.startTask(command(disabled, {
taskId: disabled.taskId,
mutationId: '019f7300-0000-7000-8000-000000000400',
})),
(error) =>
error instanceof TaskStartFenceRejectedError &&
error.reason === 'task_disabled',
);
await repository.startTask(command(record));
await assert.rejects(
repository.startTask(command(record, {
expectedContentDigest: 'f'.repeat(64),
runId: '019f7300-0000-7000-8000-000000000401',
attemptId: '019f7300-0000-7000-8000-000000000402',
createdEventId: '019f7300-0000-7000-8000-000000000403',
queuedEventId: '019f7300-0000-7000-8000-000000000404',
})),
(error) =>
error instanceof TaskStartFenceRejectedError &&
error.reason === 'mutation_conflict',
);
});
test('fails closed after authorization revocation or execution revision loss', async (t) => {
const { databasePath, runtime, repository } = await fixture(t);
const first = (
await runtime.taskDefinitions.appendTaskDefinitionRevision(definition())
).definition;
const client = new DatabaseSync(databasePath);
client.prepare(`
INSERT INTO "QingLong3ProjectRoleBindings" (
"project_id", "subject_type", "subject_id", "version", "state",
"role", "mutation_id", "changed_by_type", "changed_by_id",
"created_at_ms"
) VALUES ('default', 'user', 'user-1', 2, 'revoked', NULL,
'revoke-operator', 'user', 'user-1', ?)
`).run(NOW - 100);
await assert.rejects(
repository.startTask(command(first)),
(error) =>
error instanceof TaskStartFenceRejectedError &&
error.reason === 'authorization_changed',
);
client.prepare(
`DELETE FROM "QingLong3ProjectRoleBindings" WHERE "version" = 2`,
).run();
const second = (
await runtime.taskDefinitions.appendTaskDefinitionRevision(definition(2))
).definition;
client.prepare(`
DELETE FROM "QingLong3LocalTaskExecutionRevisions"
WHERE "project_id" = ? AND "task_id" = ?
`).run('default', second.taskId);
client.close();
await assert.rejects(
repository.startTask(command(second, {
taskId: second.taskId,
mutationId: '019f7300-0000-7000-8000-000000000500',
})),
TaskStartUnavailableError,
);
});
@@ -0,0 +1,267 @@
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',
);
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,190 @@
const assert = require('node:assert/strict');
const { createHash } = require('node:crypto');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
ToolInvocationArtifactConflictError,
ToolInvocationArtifactUnavailableError,
createToolInvocationInputArtifact,
createToolInvocationPreviewArtifact,
} = require('@qinglong/runtime-core/tool-invocation-artifact');
const {
LocalSqliteOperationAuthority,
} = require('../dist/authority/operationAuthority');
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
const {
LocalSqliteToolInvocationArtifactRepository,
} = require('../dist/tool-execution/toolInvocationArtifactRepository');
const KEY = Buffer.alloc(32, 7);
const NONCE = Buffer.alloc(12, 9);
const INVOCATION_ACTION_DIGEST = 'a'.repeat(64);
const ACTION_DIGEST = 'b'.repeat(64);
const REDACTION_DIGEST = 'c'.repeat(64);
function digest(value) {
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
}
function artifacts(index = 1, overrides = {}) {
const input = {
runId: `run-${String(index).padStart(3, '0')}`,
token: 'secret-value',
};
const common = {
projectId: 'project-001',
actionRef: `tool-plan:${input.runId}`,
sealedAtMs: 1_000 + index,
...overrides.common,
};
const inputArtifact = createToolInvocationInputArtifact(
{
artifactId: `artifact-input-${index}`,
requestedBy: { type: 'user', id: 'usr-owner' },
tool: { name: 'demo.compare', version: '1.0.0' },
input,
inputDigest: digest(input),
invocationActionDigest: INVOCATION_ACTION_DIGEST,
keyId: 'tool-key-test',
key: KEY,
...common,
...overrides.input,
},
() => NONCE,
);
const previewArtifact = createToolInvocationPreviewArtifact({
artifactId: `artifact-preview-${index}`,
actionDigest: ACTION_DIGEST,
redactionContractDigest: REDACTION_DIGEST,
preview: {
title: 'Compare Run',
summary: 'Reads one bounded Run projection',
fields: [
{ kind: 'identifier', label: 'Run', value: input.runId },
{ kind: 'redacted', label: 'Credential', value: null },
],
warnings: [],
},
...common,
...overrides.preview,
});
return { inputArtifact, previewArtifact };
}
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();
const authority = new LocalSqliteOperationAuthority(client);
return {
client,
repository: new LocalSqliteToolInvocationArtifactRepository(authority),
close: () => authority.close(),
};
}
test('atomically inserts and exactly replays one Artifact pair', async (t) => {
const current = await harness();
t.after(current.close);
const pair = artifacts();
assert.deepEqual(
await current.repository.put(pair.inputArtifact, pair.previewArtifact),
{ status: 'inserted' },
);
assert.deepEqual(
await current.repository.put(pair.inputArtifact, pair.previewArtifact),
{ status: 'existing' },
);
assert.deepEqual(
await current.repository.findInput(pair.inputArtifact.artifactId),
pair.inputArtifact,
);
assert.deepEqual(
await current.repository.findPreview(pair.previewArtifact.artifactId),
pair.previewArtifact,
);
assert.equal(
JSON.stringify(
current.client
.prepare(
`SELECT artifact_json FROM "ToolInvocationInputArtifacts"`,
)
.get(),
).includes('secret-value'),
false,
);
});
test('rejects pair drift and rolls back both rows', async (t) => {
const current = await harness();
t.after(current.close);
const pair = artifacts();
const detached = artifacts(2, {
common: {
projectId: 'project-001',
actionRef: 'tool-plan:detached',
sealedAtMs: 1_002,
},
});
await assert.rejects(
current.repository.put(pair.inputArtifact, detached.previewArtifact),
ToolInvocationArtifactConflictError,
);
assert.equal(
current.client
.prepare(
`SELECT count(*) AS count FROM "ToolInvocationInputArtifacts"`,
)
.get().count,
0,
);
assert.equal(
current.client
.prepare(
`SELECT count(*) AS count FROM "ToolInvocationPreviewArtifacts"`,
)
.get().count,
0,
);
});
test('fails closed when a stored projection is corrupted', async (t) => {
const current = await harness();
t.after(current.close);
const pair = artifacts();
await current.repository.put(pair.inputArtifact, pair.previewArtifact);
current.client.exec('PRAGMA ignore_check_constraints = ON');
current.client
.prepare(
`UPDATE "ToolInvocationInputArtifacts"
SET input_digest = ?
WHERE artifact_id = ?`,
)
.run('d'.repeat(64), pair.inputArtifact.artifactId);
current.client.exec('PRAGMA ignore_check_constraints = OFF');
await assert.rejects(
current.repository.findInput(pair.inputArtifact.artifactId),
ToolInvocationArtifactUnavailableError,
);
});
test('publishes only the explicit adapter subpath', () => {
const root = require('../dist');
const subpath = require('@qinglong/local-sqlite/tool-invocation-artifact');
assert.equal(
root.LocalSqliteToolInvocationArtifactRepository,
LocalSqliteToolInvocationArtifactRepository,
);
assert.equal(
subpath.LocalSqliteToolInvocationArtifactRepository,
LocalSqliteToolInvocationArtifactRepository,
);
});
@@ -0,0 +1,103 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { test } = require('node:test');
const {
CRASH_POINTS,
setupScenario,
verifyScenario,
} = require('./fixtures/toolResultCrashMatrixFixture.cjs');
const FIXTURE_PATH = path.join(
__dirname,
'fixtures',
'toolResultCrashMatrixFixture.cjs',
);
test(
'survives the Tool Result completion and key lifecycle crash matrix',
{ timeout: 180_000 },
async (context) => {
const reports = [];
for (const profile of ['edge', 'standalone']) {
for (const [pointName, point] of Object.entries(CRASH_POINTS)) {
const directory = fs.mkdtempSync(
path.join(os.tmpdir(), `ql3-tool-result-${profile}-`),
);
context.after(() => {
fs.rmSync(directory, { recursive: true, force: true });
});
const databasePath = path.join(directory, 'runtime.sqlite');
const statePath = path.join(directory, 'state.json');
const markerPath = path.join(directory, 'crash-marker.json');
await setupScenario({
databasePath,
statePath,
profile,
operation: point.operation,
});
const crashed = spawnSync(
process.execPath,
[
FIXTURE_PATH,
'crash',
databasePath,
statePath,
markerPath,
pointName,
],
{
encoding: 'utf8',
timeout: 30_000,
},
);
assert.equal(
crashed.error,
undefined,
`${profile}/${pointName}: ${crashed.error?.message}`,
);
assert.equal(
crashed.signal,
'SIGKILL',
`${profile}/${pointName}: status=${crashed.status}, stderr=${crashed.stderr}`,
);
assert.deepEqual(
JSON.parse(fs.readFileSync(markerPath, 'utf8')),
{
schema: 'qinglong/sqlite-tool-result-crash-marker@v1',
point: pointName,
pid: JSON.parse(
fs.readFileSync(markerPath, 'utf8'),
).pid,
},
);
reports.push(
await verifyScenario({
databasePath,
statePath,
pointName,
}),
);
}
}
assert.equal(reports.length, 20);
assert.equal(
reports.filter((report) => report.crashBeforeCommit).length,
12,
);
assert.equal(
reports.filter((report) => report.durableAfterCrash).length,
8,
);
assert.deepEqual(
[...new Set(reports.map((report) => report.journalMode))].sort(),
['delete', 'wal'],
);
assert.ok(
reports.every((report) => report.integrityCheck === 'ok'),
);
},
);
@@ -0,0 +1,124 @@
const assert = require('node:assert/strict');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
ToolResultKeyCatalogConflictError,
ToolResultKeyCatalogUnavailableError,
createToolResultKeyCatalogBootstrapCommand,
createToolResultKeyRotationCommand,
toolResultKeyMaterialProof,
} = require('@qinglong/runtime-core/tool-result-key-catalog');
const {
LocalSqliteToolResultKeyCatalogRepository,
} = require('@qinglong/local-sqlite/tool-result-key-catalog');
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
const { LocalSqliteOperationAuthority } = require('../dist/authority/operationAuthority');
async function harness() {
const client = new DatabaseSync(':memory:');
client.exec('PRAGMA foreign_keys = ON');
await migrateLocalSqliteDatabase(client);
const authority = new LocalSqliteOperationAuthority(client);
return {
client,
repository: new LocalSqliteToolResultKeyCatalogRepository(authority),
close: () => authority.close(),
};
}
function bootstrapCommand() {
return createToolResultKeyCatalogBootstrapCommand({
keyId: 'tool-result-key-001',
materialProof: toolResultKeyMaterialProof(
'tool-result-key-001',
Buffer.alloc(32, 1),
),
mutationId: 'tool-result-key-bootstrap-001',
});
}
test('appends, exactly replays and reads the latest SQLite key generation', async () => {
const current = await harness();
try {
assert.equal(await current.repository.findCurrent(), null);
const bootstrap = bootstrapCommand();
const first = await current.repository.append(bootstrap);
assert.equal(first.status, 'created');
assert.equal(first.catalog.generation, 1);
assert.deepEqual(await current.repository.append(bootstrap), {
status: 'existing',
catalog: first.catalog,
});
const rotation = createToolResultKeyRotationCommand(first.catalog, {
keyId: 'tool-result-key-002',
materialProof: toolResultKeyMaterialProof(
'tool-result-key-002',
Buffer.alloc(32, 2),
),
mutationId: 'tool-result-key-rotate-002',
});
const second = await current.repository.append(rotation);
assert.equal(second.status, 'created');
assert.equal(second.catalog.generation, 2);
assert.deepEqual(await current.repository.findCurrent(), second.catalog);
assert.equal(
current.client
.prepare(
`SELECT COUNT(*) AS count
FROM "ToolResultKeyCatalogGenerations"`,
)
.get().count,
2,
);
await assert.rejects(
current.repository.append(
createToolResultKeyRotationCommand(first.catalog, {
keyId: 'tool-result-key-stale',
materialProof: toolResultKeyMaterialProof(
'tool-result-key-stale',
Buffer.alloc(32, 3),
),
mutationId: 'tool-result-key-stale-003',
}),
),
ToolResultKeyCatalogConflictError,
);
} finally {
await current.close();
}
});
test('fails closed when the durable SQLite catalog projection drifts', async () => {
const current = await harness();
try {
await current.repository.append(bootstrapCommand());
current.client
.prepare(
`UPDATE "ToolResultKeyCatalogGenerations"
SET command_digest = ?
WHERE generation = 1`,
)
.run('c'.repeat(64));
await assert.rejects(
current.repository.findCurrent(),
ToolResultKeyCatalogUnavailableError,
);
} finally {
await current.close();
}
});
test('publishes SQLite catalog mutation 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-result-key-catalog');
assert.equal(root.LocalSqliteToolResultKeyCatalogRepository, undefined);
assert.equal(runtime.LocalSqliteToolResultKeyCatalogRepository, undefined);
assert.equal(
typeof authority.LocalSqliteToolResultKeyCatalogRepository,
'function',
);
});
@@ -0,0 +1,277 @@
const assert = require('node:assert/strict');
const { createHash } = require('node:crypto');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
BUILTIN_RUN_READ_TOOL,
BUILTIN_RUN_READ_TOOL_DEFINITION,
} = require('@qinglong/runtime-core/builtin-run-read-tool');
const {
TRUSTED_TOOL_EXECUTION_RESULT_SCHEMA,
} = require('@qinglong/runtime-core/trusted-tool-execution');
const {
TOOL_EXECUTION_RESULT_KEY_BINDING_SCHEMA,
createToolExecutionResultArtifact,
normalizeToolExecutionResultKeyBinding,
} = require('@qinglong/runtime-core/tool-execution-completion');
const {
createToolResultKeyCatalogBootstrapCommand,
createToolResultKeyRetirementCommand,
createToolResultKeyRotationCommand,
requireActiveToolResultKey,
toolResultKeyCatalogFence,
toolResultKeyMaterialProof,
} = require('@qinglong/runtime-core/tool-result-key-catalog');
const {
ToolExecutionResultRekeyConflictError,
createToolExecutionResultRekeyCommand,
createToolResultKeyRetirementReceiptCommand,
} = require('@qinglong/runtime-core/tool-result-rekey');
const {
ToolDefinitionRegistry,
} = require('@qinglong/runtime-core/tool-registry');
const {
LocalSqliteToolResultKeyCatalogRepository,
} = require('@qinglong/local-sqlite/tool-result-key-catalog');
const {
LocalSqliteToolResultRekeyRepository,
} = require('@qinglong/local-sqlite/tool-result-rekey');
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
const { LocalSqliteOperationAuthority } = require('../dist/authority/operationAuthority');
const KEY_A = Buffer.alloc(32, 1);
const KEY_B = Buffer.alloc(32, 2);
const OUTPUT_DIGEST_DOMAIN = Buffer.from(
'qinglong/trusted-tool-execution-output-digest@v1\0',
);
const RESULT_DIGEST_DOMAIN = Buffer.from(
'qinglong/trusted-tool-execution-result-digest@v1\0',
);
const BINDING_DIGEST_DOMAIN = Buffer.from(
'qinglong/tool-execution-result-key-binding-digest@v1\0',
);
function hash(domain, value) {
return createHash('sha256')
.update(domain)
.update(JSON.stringify(value))
.digest('hex');
}
function output() {
return {
createdAtMs: 1_000,
eventSequence: 3,
executionOrigin: 'manual',
executionOwner: 'runtime',
found: true,
id: 'run-rekey-local-001',
priority: 10,
queuedAtMs: 1_100,
startedAtMs: 1_200,
status: 'succeeded',
taskId: 'task-rekey-local-001',
taskRevision: 'task-rekey-local-001@1',
version: 2,
};
}
function registry() {
return new ToolDefinitionRegistry([BUILTIN_RUN_READ_TOOL_DEFINITION]);
}
function sourceArtifact() {
const value = output();
const unsigned = {
schema: TRUSTED_TOOL_EXECUTION_RESULT_SCHEMA,
startId: 'tool-start-rekey-local-001',
barrierDigest: 'a'.repeat(64),
adapterDigest: 'b'.repeat(64),
output: value,
outputDigest: hash(OUTPUT_DIGEST_DOMAIN, value),
completedAtMs: 1_500,
};
const result = {
...unsigned,
resultDigest: hash(RESULT_DIGEST_DOMAIN, unsigned),
};
return createToolExecutionResultArtifact(
{
artifactId: 'artifact-result-rekey-local-001',
projectId: 'project-rekey-local-001',
runId: 'run-host-rekey-local-001',
stepRunId: 'step-run-rekey-local-001',
tool: BUILTIN_RUN_READ_TOOL,
executionResult: result,
keyId: 'result-key-a',
key: KEY_A,
},
registry(),
() => Buffer.alloc(12, 4),
);
}
function sourceBinding(artifact, catalog) {
const unsigned = {
schema: TOOL_EXECUTION_RESULT_KEY_BINDING_SCHEMA,
startId: artifact.startId,
artifactId: artifact.artifactId,
artifactDigest: artifact.artifactDigest,
catalogGeneration: catalog.generation,
catalogDigest: catalog.catalogDigest,
keyId: artifact.keyId,
materialProof: toolResultKeyMaterialProof(artifact.keyId, KEY_A),
};
return normalizeToolExecutionResultKeyBinding({
...unsigned,
bindingDigest: hash(BINDING_DIGEST_DOMAIN, unsigned),
});
}
async function harness() {
const client = new DatabaseSync(':memory:');
client.exec('PRAGMA foreign_keys = ON');
await migrateLocalSqliteDatabase(client);
const authority = new LocalSqliteOperationAuthority(client);
return {
client,
catalog: new LocalSqliteToolResultKeyCatalogRepository(authority),
rekey: new LocalSqliteToolResultRekeyRepository(authority),
close: () => authority.close(),
};
}
function insertBinding(client, binding) {
client.exec('PRAGMA foreign_keys = OFF');
try {
client
.prepare(
`INSERT INTO "ToolExecutionResultKeyBindings" (
start_id, artifact_id, artifact_digest, catalog_authority,
catalog_generation, catalog_digest, key_id, material_proof,
binding_digest
) VALUES (?, ?, ?, 'trusted-tool-results', ?, ?, ?, ?, ?)`,
)
.run(
binding.startId,
binding.artifactId,
binding.artifactDigest,
binding.catalogGeneration,
binding.catalogDigest,
binding.keyId,
binding.materialProof,
binding.bindingDigest,
);
} finally {
client.exec('PRAGMA foreign_keys = ON');
}
}
test('appends one SQLite rekey head and creates coverage-derived retirement evidence', async () => {
const current = await harness();
try {
const first = await current.catalog.append(
createToolResultKeyCatalogBootstrapCommand({
keyId: 'result-key-a',
materialProof: toolResultKeyMaterialProof('result-key-a', KEY_A),
mutationId: 'result-key-bootstrap-local-a',
}),
);
const artifact = sourceArtifact();
const binding = sourceBinding(artifact, first.catalog);
insertBinding(current.client, binding);
const second = await current.catalog.append(
createToolResultKeyRotationCommand(first.catalog, {
keyId: 'result-key-b',
materialProof: toolResultKeyMaterialProof('result-key-b', KEY_B),
mutationId: 'result-key-rotate-local-b',
}),
);
const receiptCommand = createToolResultKeyRetirementReceiptCommand({
expectedCatalogGeneration: second.catalog.generation,
expectedCatalogDigest: second.catalog.catalogDigest,
keyId: 'result-key-a',
mutationId: 'result-key-retirement-local-a',
});
await assert.rejects(
current.rekey.create(receiptCommand),
ToolExecutionResultRekeyConflictError,
);
await assert.rejects(
current.catalog.append(
createToolResultKeyRetirementCommand(second.catalog, {
keyId: 'result-key-a',
retirementReceiptDigest: 'f'.repeat(64),
mutationId: 'result-key-retire-local-forged',
}),
),
/conflicts with durable state/,
);
const overlayCommand = createToolExecutionResultRekeyCommand({
artifact,
binding,
previousOverlay: null,
overlayId: 'result-rekey-overlay-local-001',
mutationId: 'result-rekey-mutation-local-001',
targetCatalogFence: toolResultKeyCatalogFence(
second.catalog,
requireActiveToolResultKey(second.catalog),
),
targetKey: KEY_B,
output: output(),
rekeyedAtMs: 1_700,
registry: registry(),
nonceFactory: () => Buffer.alloc(12, 5),
});
const appended = await current.rekey.append(overlayCommand);
assert.equal(appended.status, 'created');
assert.deepEqual(await current.rekey.append(overlayCommand), {
status: 'existing',
overlay: appended.overlay,
});
assert.deepEqual(
await current.rekey.findHeadByArtifactId(artifact.artifactId),
appended.overlay,
);
const receipt = await current.rekey.create(receiptCommand);
assert.equal(receipt.status, 'created');
assert.equal(receipt.receipt.bindingCount, 1);
assert.equal(receipt.receipt.overlayHeadCount, 1);
assert.deepEqual(await current.rekey.create(receiptCommand), {
status: 'existing',
receipt: receipt.receipt,
});
assert.deepEqual(
await current.rekey.findByDigest(receipt.receipt.receiptDigest),
receipt.receipt,
);
const retired = await current.catalog.append(
createToolResultKeyRetirementCommand(second.catalog, {
keyId: 'result-key-a',
retirementReceiptDigest: receipt.receipt.receiptDigest,
mutationId: 'result-key-retire-local-a',
}),
);
assert.equal(retired.status, 'created');
assert.equal(
retired.catalog.keys.find((entry) => entry.keyId === 'result-key-a')
.state,
'retired',
);
} finally {
await current.close();
}
});
test('keeps SQLite rekey authority behind its explicit subpath', () => {
const root = require('../dist');
const authority = require('@qinglong/local-sqlite/tool-result-rekey');
assert.equal(root.LocalSqliteToolResultRekeyRepository, undefined);
assert.equal(
authority.LocalSqliteToolResultRekeyRepository,
LocalSqliteToolResultRekeyRepository,
);
});
@@ -0,0 +1,342 @@
const assert = require('node:assert/strict');
const { createHash } = require('node:crypto');
const { mkdtemp, rm } = require('node:fs/promises');
const { tmpdir } = require('node:os');
const { join } = require('node:path');
const { test } = require('node:test');
const {
BUILTIN_RUN_READ_TOOL,
BUILTIN_RUN_READ_TOOL_DEFINITION,
} = require('@qinglong/runtime-core/builtin-run-read-tool');
const {
TRUSTED_TOOL_EXECUTION_RESULT_SCHEMA,
} = require('@qinglong/runtime-core/trusted-tool-execution');
const {
TOOL_EXECUTION_RESULT_KEY_BINDING_SCHEMA,
createToolExecutionResultArtifact,
normalizeToolExecutionResultKeyBinding,
} = require('@qinglong/runtime-core/tool-execution-completion');
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 {
ToolDefinitionRegistry,
} = require('@qinglong/runtime-core/tool-registry');
const {
LocalSqliteToolResultKeyCatalogRepository,
} = require('@qinglong/local-sqlite/tool-result-key-catalog');
const {
LocalSqliteToolResultRekeyRepository,
} = require('@qinglong/local-sqlite/tool-result-rekey');
const { openLocalSqliteClient } = require('../dist/storage/config');
const { migrateLocalSqlitePath } = require('../dist/migration/migration');
const { LocalSqliteOperationAuthority } = require('../dist/authority/operationAuthority');
const COVERAGE_COUNT = 129;
const CONCURRENT_REPLAYS = 8;
const KEY_A = Buffer.alloc(32, 1);
const KEY_B = Buffer.alloc(32, 2);
const OUTPUT_DIGEST_DOMAIN = Buffer.from(
'qinglong/trusted-tool-execution-output-digest@v1\0',
);
const RESULT_DIGEST_DOMAIN = Buffer.from(
'qinglong/trusted-tool-execution-result-digest@v1\0',
);
const BINDING_DIGEST_DOMAIN = Buffer.from(
'qinglong/tool-execution-result-key-binding-digest@v1\0',
);
function hash(domain, value) {
return createHash('sha256')
.update(domain)
.update(JSON.stringify(value))
.digest('hex');
}
function output(index) {
const suffix = String(index).padStart(3, '0');
return {
createdAtMs: 1_000 + index,
eventSequence: 3,
executionOrigin: 'manual',
executionOwner: 'runtime',
found: true,
id: `run-coverage-pressure-${suffix}`,
priority: 10,
queuedAtMs: 1_100 + index,
startedAtMs: 1_200 + index,
status: 'succeeded',
taskId: `task-coverage-pressure-${suffix}`,
taskRevision: `task-coverage-pressure-${suffix}@1`,
version: 2,
};
}
function registry() {
return new ToolDefinitionRegistry([BUILTIN_RUN_READ_TOOL_DEFINITION]);
}
function nonce(index, marker) {
const value = Buffer.alloc(12, marker);
value.writeUInt32BE(index, 8);
return value;
}
function sourceArtifact(index, definitions) {
const suffix = String(index).padStart(3, '0');
const value = output(index);
const unsigned = {
schema: TRUSTED_TOOL_EXECUTION_RESULT_SCHEMA,
startId: `tool-start-coverage-pressure-${suffix}`,
barrierDigest: 'a'.repeat(64),
adapterDigest: 'b'.repeat(64),
output: value,
outputDigest: hash(OUTPUT_DIGEST_DOMAIN, value),
completedAtMs: 1_500 + index,
};
const result = {
...unsigned,
resultDigest: hash(RESULT_DIGEST_DOMAIN, unsigned),
};
return createToolExecutionResultArtifact(
{
artifactId: `artifact-coverage-pressure-${suffix}`,
projectId: 'project-coverage-pressure',
runId: `run-host-coverage-pressure-${suffix}`,
stepRunId: `step-run-coverage-pressure-${suffix}`,
tool: BUILTIN_RUN_READ_TOOL,
executionResult: result,
keyId: 'result-key-a',
key: KEY_A,
},
definitions,
() => nonce(index, 4),
);
}
function sourceBinding(artifact, catalog) {
const unsigned = {
schema: TOOL_EXECUTION_RESULT_KEY_BINDING_SCHEMA,
startId: artifact.startId,
artifactId: artifact.artifactId,
artifactDigest: artifact.artifactDigest,
catalogGeneration: catalog.generation,
catalogDigest: catalog.catalogDigest,
keyId: artifact.keyId,
materialProof: toolResultKeyMaterialProof(artifact.keyId, KEY_A),
};
return normalizeToolExecutionResultKeyBinding({
...unsigned,
bindingDigest: hash(BINDING_DIGEST_DOMAIN, unsigned),
});
}
function insertBindings(client, bindings) {
client.exec('PRAGMA foreign_keys = OFF');
try {
const insert = client.prepare(
`INSERT INTO "ToolExecutionResultKeyBindings" (
start_id, artifact_id, artifact_digest, catalog_authority,
catalog_generation, catalog_digest, key_id, material_proof,
binding_digest
) VALUES (?, ?, ?, 'trusted-tool-results', ?, ?, ?, ?, ?)`,
);
for (const binding of bindings) {
insert.run(
binding.startId,
binding.artifactId,
binding.artifactDigest,
binding.catalogGeneration,
binding.catalogDigest,
binding.keyId,
binding.materialProof,
binding.bindingDigest,
);
}
} finally {
client.exec('PRAGMA foreign_keys = ON');
}
}
async function exerciseProfile(profile) {
const directory = await mkdtemp(
join(tmpdir(), `ql3-result-coverage-${profile}-`),
);
const databasePath = join(directory, 'coverage.sqlite');
let authority;
try {
await migrateLocalSqlitePath({ databasePath, profile });
const client = openLocalSqliteClient(
{ databasePath, profile, busyTimeoutMs: 5_000 },
false,
);
authority = new LocalSqliteOperationAuthority(client);
const catalogs =
new LocalSqliteToolResultKeyCatalogRepository(authority);
const rekeys = new LocalSqliteToolResultRekeyRepository(authority);
const first = await catalogs.append(
createToolResultKeyCatalogBootstrapCommand({
keyId: 'result-key-a',
materialProof: toolResultKeyMaterialProof('result-key-a', KEY_A),
mutationId: `coverage-pressure-${profile}-bootstrap-a`,
}),
);
const definitions = registry();
const artifacts = Array.from(
{ length: COVERAGE_COUNT },
(_, index) => sourceArtifact(index, definitions),
);
const bindings = artifacts.map((artifact) =>
sourceBinding(artifact, first.catalog),
);
insertBindings(client, bindings);
const second = await catalogs.append(
createToolResultKeyRotationCommand(first.catalog, {
keyId: 'result-key-b',
materialProof: toolResultKeyMaterialProof('result-key-b', KEY_B),
mutationId: `coverage-pressure-${profile}-rotate-b`,
}),
);
const fence = toolResultKeyCatalogFence(
second.catalog,
requireActiveToolResultKey(second.catalog),
);
const commands = artifacts.map((artifact, index) =>
createToolExecutionResultRekeyCommand({
artifact,
binding: bindings[index],
previousOverlay: null,
overlayId: `coverage-pressure-${profile}-overlay-${String(
index,
).padStart(3, '0')}`,
mutationId: `coverage-pressure-${profile}-rekey-${String(
index,
).padStart(3, '0')}`,
targetCatalogFence: fence,
targetKey: KEY_B,
output: output(index),
rekeyedAtMs: 2_000 + index,
registry: definitions,
nonceFactory: () => nonce(index, 5),
}),
);
const appended = await Promise.all(
commands.map((command) => rekeys.append(command)),
);
assert.equal(
appended.filter((result) => result.status === 'created').length,
COVERAGE_COUNT,
);
const replayed = await Promise.all(
Array.from({ length: CONCURRENT_REPLAYS }, () =>
rekeys.append(commands[0]),
),
);
assert.equal(
replayed.every((result) => result.status === 'existing'),
true,
);
const receiptCommand = createToolResultKeyRetirementReceiptCommand({
expectedCatalogGeneration: second.catalog.generation,
expectedCatalogDigest: second.catalog.catalogDigest,
keyId: 'result-key-a',
mutationId: `coverage-pressure-${profile}-receipt`,
});
const receipts = await Promise.all(
Array.from({ length: CONCURRENT_REPLAYS }, () =>
rekeys.create(receiptCommand),
),
);
assert.equal(
receipts.filter((result) => result.status === 'created').length,
1,
);
assert.equal(
receipts.filter((result) => result.status === 'existing').length,
CONCURRENT_REPLAYS - 1,
);
const receipt = receipts[0].receipt;
assert.equal(receipt.bindingCount, COVERAGE_COUNT);
assert.equal(receipt.overlayHeadCount, COVERAGE_COUNT);
assert.equal(receipt.uncoveredBindingCount, 0);
assert.equal(receipt.uncoveredOverlayHeadCount, 0);
assert.equal(
receipts.every(
(result) =>
result.receipt.receiptDigest === receipt.receiptDigest &&
result.receipt.coverageDigest === receipt.coverageDigest,
),
true,
);
const retireCommand = createToolResultKeyRetirementCommand(
second.catalog,
{
keyId: 'result-key-a',
retirementReceiptDigest: receipt.receiptDigest,
mutationId: `coverage-pressure-${profile}-retire-a`,
},
);
const retired = await Promise.all(
Array.from({ length: CONCURRENT_REPLAYS }, () =>
catalogs.append(retireCommand),
),
);
assert.equal(
retired.filter((result) => result.status === 'created').length,
1,
);
assert.equal(
retired.filter((result) => result.status === 'existing').length,
CONCURRENT_REPLAYS - 1,
);
assert.equal(
retired.every(
(result) =>
result.catalog.catalogDigest === retired[0].catalog.catalogDigest &&
result.catalog.keys.find((entry) => entry.keyId === 'result-key-a')
.state === 'retired',
),
true,
);
const facts = client
.prepare(
`SELECT
(SELECT count(*) FROM "ToolExecutionResultKeyBindings")
AS bindings,
(SELECT count(*) FROM "ToolExecutionResultRekeyHeads")
AS heads,
(SELECT count(*) FROM "ToolResultKeyRetirementReceipts")
AS receipts`,
)
.get();
assert.equal(facts.bindings, COVERAGE_COUNT);
assert.equal(facts.heads, COVERAGE_COUNT);
assert.equal(facts.receipts, 1);
assert.equal(
client.prepare('PRAGMA integrity_check').get().integrity_check,
'ok',
);
} finally {
if (authority) await authority.close();
await rm(directory, { force: true, recursive: true });
}
}
for (const profile of ['edge', 'standalone']) {
test(`converges ${profile} 129-row rekey and retirement pressure exactly once`, async () => {
await exerciseProfile(profile);
});
}
@@ -0,0 +1,340 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
TriggerConflictError,
TriggerUnavailableError,
UnsupportedTriggerSpecError,
createTriggerSpecSemanticRegistry,
} = require('@qinglong/runtime-core/trigger');
const {
migrateLocalSqlitePath,
openLocalSqliteRuntimeDatabase,
} = require('../dist');
function fixture(t) {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-trigger-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
return path.join(directory, 'qinglong3.sqlite');
}
function taskCommand(index, overrides = {}) {
const suffix = String(index).padStart(12, '0');
return {
projectId: 'default',
taskId: `task-${String(index).padStart(5, '0')}`,
expectedRevision: null,
mutationId: `019f7310-0000-7000-8000-${suffix}`,
name: `Task ${index}`,
kind: 'command',
spec: {
schema: 'qinglong/command@v1',
config: {
command: { kind: 'argv', file: '/bin/echo', args: [String(index)] },
},
},
labels: { source: 'trigger-test' },
enabled: true,
occurredAtMs: 100 + index,
...overrides,
};
}
function triggerCommand(index, task, overrides = {}) {
const suffix = String(index).padStart(12, '0');
return {
projectId: task.projectId,
triggerId: `trigger-${String(index).padStart(5, '0')}`,
expectedRevision: null,
mutationId: `019f7320-0000-7000-8000-${suffix}`,
taskId: task.taskId,
taskRevision: task.revision,
taskContentDigest: task.contentDigest,
spec: {
schema: 'qinglong/cron@v1',
config: {
expression: '*/5 * * * *',
timezone: 'Etc/UTC',
misfirePolicy: 'skip',
},
},
enabled: true,
occurredAtMs: 200 + index,
...overrides,
};
}
async function createRuntime(t, options = {}) {
const databasePath = fixture(t);
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const runtime = await openLocalSqliteRuntimeDatabase(
{ databasePath, profile: 'edge' },
options,
);
t.after(() => runtime.close());
return { databasePath, runtime };
}
test('creates, replays and versions a Trigger pinned to immutable task revisions', async (t) => {
const { runtime } = await createRuntime(t);
const firstTask = (
await runtime.taskDefinitions.appendTaskDefinitionRevision(taskCommand(1))
).definition;
const firstInput = triggerCommand(1, firstTask);
const created = await runtime.triggers.appendTriggerRevision(firstInput);
assert.equal(created.status, 'created');
assert.equal(created.trigger.revision, 1);
assert.deepEqual({ ...created.trigger.spec.config }, {
expression: '*/5 * * * *',
misfirePolicy: 'skip',
timezone: 'UTC',
});
assert.equal(
(await runtime.triggers.appendTriggerRevision(firstInput)).status,
'existing',
);
const secondTask = (
await runtime.taskDefinitions.appendTaskDefinitionRevision(
taskCommand(1, {
expectedRevision: 1,
mutationId: '019f7310-0000-7000-8000-000000010001',
name: 'Task 1 revision 2',
occurredAtMs: 300,
}),
)
).definition;
const updated = await runtime.triggers.appendTriggerRevision(
triggerCommand(1, secondTask, {
expectedRevision: 1,
mutationId: '019f7320-0000-7000-8000-000000010001',
occurredAtMs: 400,
}),
);
assert.equal(updated.status, 'updated');
assert.equal(updated.trigger.taskRevision, 2);
assert.equal(updated.trigger.createdAtMs, created.trigger.createdAtMs);
assert.equal(
(
await runtime.triggers.findTriggerRevision(
'default',
firstInput.triggerId,
1,
)
).taskRevision,
1,
);
assert.equal(
(
await runtime.triggers.findCurrentTrigger(
'default',
firstInput.triggerId,
)
).taskRevision,
2,
);
});
test('rejects digest drift, disabled targets, stale CAS and task rebinding', async (t) => {
const { runtime } = await createRuntime(t);
const task1 = (
await runtime.taskDefinitions.appendTaskDefinitionRevision(taskCommand(1))
).definition;
await assert.rejects(
runtime.triggers.appendTriggerRevision(
triggerCommand(1, task1, { taskContentDigest: 'b'.repeat(64) }),
),
TriggerConflictError,
);
const disabledTask = (
await runtime.taskDefinitions.appendTaskDefinitionRevision(
taskCommand(2, { enabled: false }),
)
).definition;
await assert.rejects(
runtime.triggers.appendTriggerRevision(triggerCommand(2, disabledTask)),
TriggerConflictError,
);
const disabledTrigger = await runtime.triggers.appendTriggerRevision(
triggerCommand(2, disabledTask, {
enabled: false,
mutationId: '019f7320-0000-7000-8000-000000020001',
}),
);
assert.equal(disabledTrigger.trigger.enabled, false);
const first = await runtime.triggers.appendTriggerRevision(
triggerCommand(1, task1),
);
await assert.rejects(
runtime.triggers.appendTriggerRevision(
triggerCommand(1, task1, {
expectedRevision: null,
mutationId: '019f7320-0000-7000-8000-000000010010',
}),
),
TriggerConflictError,
);
await assert.rejects(
runtime.triggers.appendTriggerRevision(
triggerCommand(1, disabledTask, {
enabled: false,
expectedRevision: first.trigger.revision,
mutationId: '019f7320-0000-7000-8000-000000010011',
occurredAtMs: 500,
}),
),
TriggerConflictError,
);
});
test('paginates current Trigger heads and serializes competing revisions', async (t) => {
const { runtime } = await createRuntime(t);
const task = (
await runtime.taskDefinitions.appendTaskDefinitionRevision(taskCommand(1))
).definition;
for (const index of [1, 2, 3]) {
await runtime.triggers.appendTriggerRevision(triggerCommand(index, task));
}
const first = await runtime.triggers.listTriggers({
projectId: 'default',
limit: 2,
});
assert.equal(first.triggers.length, 2);
assert.equal(first.truncated, true);
const second = await runtime.triggers.listTriggers({
projectId: 'default',
limit: 2,
after: first.next,
});
assert.equal(second.triggers.length, 1);
assert.equal(second.truncated, false);
const base = first.triggers[0];
const results = await Promise.allSettled([
runtime.triggers.appendTriggerRevision(
triggerCommand(1, task, {
expectedRevision: base.revision,
mutationId: '019f7320-0000-7000-8000-000000010101',
occurredAtMs: 600,
}),
),
runtime.triggers.appendTriggerRevision(
triggerCommand(1, task, {
expectedRevision: base.revision,
mutationId: '019f7320-0000-7000-8000-000000010102',
occurredAtMs: 601,
}),
),
]);
assert.equal(results.filter(({ status }) => status === 'fulfilled').length, 1);
assert.equal(results.filter(({ status }) => status === 'rejected').length, 1);
});
test('fails closed when a durable Trigger or pinned task digest is corrupt', async (t) => {
const { databasePath, runtime } = await createRuntime(t);
const task = (
await runtime.taskDefinitions.appendTaskDefinitionRevision(taskCommand(1))
).definition;
const input = triggerCommand(1, task);
const trigger = (
await runtime.triggers.appendTriggerRevision(input)
).trigger;
await runtime.close();
const client = new DatabaseSync(databasePath);
client
.prepare(
`UPDATE "QingLong3TriggerRevisions" SET "content_digest" = ?
WHERE "project_id" = ? AND "trigger_id" = ? AND "revision" = ?`,
)
.run('b'.repeat(64), trigger.projectId, trigger.triggerId, trigger.revision);
client.close();
const reader = await openLocalSqliteRuntimeDatabase({
databasePath,
profile: 'edge',
});
t.after(() => reader.close());
await assert.rejects(
reader.triggers.findCurrentTrigger(trigger.projectId, trigger.triggerId),
TriggerUnavailableError,
);
await reader.close();
const taskClient = new DatabaseSync(databasePath);
taskClient
.prepare(
`UPDATE "QingLong3TriggerRevisions" SET "content_digest" = ?
WHERE "project_id" = ? AND "trigger_id" = ? AND "revision" = ?`,
)
.run(
trigger.contentDigest,
trigger.projectId,
trigger.triggerId,
trigger.revision,
);
taskClient
.prepare(
`UPDATE "QingLong3TaskDefinitionRevisions" SET "content_digest" = ?
WHERE "project_id" = ? AND "task_id" = ? AND "revision" = ?`,
)
.run('b'.repeat(64), task.projectId, task.taskId, task.revision);
taskClient.close();
const replayReader = await openLocalSqliteRuntimeDatabase({
databasePath,
profile: 'edge',
});
t.after(() => replayReader.close());
await assert.rejects(
replayReader.triggers.appendTriggerRevision(input),
TriggerUnavailableError,
);
});
test('reads historical extension specs without loading their write semantics', async (t) => {
const databasePath = fixture(t);
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
const registry = createTriggerSpecSemanticRegistry([
{
schema: 'example/event@v1',
normalizeConfig(config) {
return Object.freeze({ topic: String(config.topic).toLowerCase() });
},
},
]);
const writer = await openLocalSqliteRuntimeDatabase(
{ databasePath, profile: 'edge' },
{ triggerSpecSemanticRegistry: registry },
);
const task = (
await writer.taskDefinitions.appendTaskDefinitionRevision(taskCommand(1))
).definition;
const input = triggerCommand(1, task, {
spec: { schema: 'example/event@v1', config: { topic: 'BUILD' } },
});
await writer.triggers.appendTriggerRevision(input);
await writer.close();
const reader = await openLocalSqliteRuntimeDatabase({
databasePath,
profile: 'edge',
});
t.after(() => reader.close());
assert.equal(
(
await reader.triggers.findCurrentTrigger(
input.projectId,
input.triggerId,
)
).spec.config.topic,
'build',
);
await assert.rejects(
async () => reader.triggers.appendTriggerRevision(input),
UnsupportedTriggerSpecError,
);
});