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
+489
View File
@@ -0,0 +1,489 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const { Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const {
projectPolicyMigration,
} = require('../../back/migrations/0017-project-policy');
const {
APPROVAL_REQUEST_CONSUMPTION_ID_INDEX,
APPROVAL_REQUEST_DECISION_ID_INDEX,
APPROVAL_REQUEST_PENDING_INDEX,
APPROVAL_REQUEST_REQUESTER_INDEX,
APPROVAL_REQUEST_TABLE,
APPROVED_ACTION_DISPATCH_PENDING_INDEX,
APPROVED_ACTION_DISPATCH_REQUEST_INDEX,
APPROVED_ACTION_DISPATCH_TABLE,
approvalRequestMigration,
} = require('../../back/migrations/0020-approval-requests');
const {
approvedActionDispatchExecutionMigration,
} = require('../../back/migrations/0021-approved-action-dispatch-executions');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacySequelizeApprovalRequestRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/approvalRequestRepository');
const {
LegacySequelizeProjectPolicyRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/projectPolicyRepository');
const {
ApprovalRequestService,
} = require('../../back/runtime/application/approvalRequestService');
const {
ProjectPolicyEngine,
} = require('../../back/runtime/application/projectPolicyEngine');
const {
ApprovalHumanDecisionRequiredError,
ApprovalMutationConflictError,
ApprovalPolicyDeniedError,
ApprovalPolicyFenceConflictError,
ApprovalRequestExpiredError,
ApprovalRequestStateConflictError,
ApprovalRequestVersionConflictError,
} = require('../../back/runtime/domain/approvalRequest');
const PROJECT_ID = 'default';
const AGENT = Object.freeze({ type: 'agent', id: 'agent-1' });
const OWNER = Object.freeze({ type: 'user', id: 'owner-1' });
const VIEWER = Object.freeze({ type: 'user', id: 'viewer-1' });
const SYSTEM = Object.freeze({ type: 'system', id: 'approval-dispatcher' });
const NOW = 100_000;
const EXPIRES_AT = 200_000;
function action(overrides = {}) {
return {
permission: 'tool.call:filesystem.write',
actionType: 'tool_call',
actionRef: 'planned-action-1',
actionDigest: 'a'.repeat(64),
previewDigest: 'b'.repeat(64),
...overrides,
};
}
function createInput(overrides = {}) {
return {
id: 'approval-1',
projectId: PROJECT_ID,
action: action(),
risk: 'high',
requestedBy: AGENT,
requestedAtMs: NOW,
expiresAtMs: EXPIRES_AT,
...overrides,
};
}
function decisionInput(overrides = {}) {
return {
requestId: 'approval-1',
expectedVersion: 1,
decisionId: 'decision-1',
decision: 'approved',
reasonCode: 'reviewed_action',
decidedBy: OWNER,
decidedAtMs: NOW + 10,
...overrides,
};
}
function consumptionInput(overrides = {}) {
return {
requestId: 'approval-1',
expectedVersion: 2,
consumptionId: 'consumption-1',
dispatchId: 'dispatch-1',
action: action(),
requestedBy: AGENT,
consumedBy: SYSTEM,
consumedAtMs: NOW + 20,
...overrides,
};
}
async function migrate(database) {
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations: [
projectPolicyMigration,
approvalRequestMigration,
approvedActionDispatchExecutionMigration,
],
logger: { info() {} },
});
}
async function bind(policyRepository, subject, role, mutationId) {
await policyRepository.append({
expectedCurrentVersion: 0,
binding: {
projectId: PROJECT_ID,
subject,
version: 1,
state: 'active',
role,
mutationId,
changedBy: OWNER,
createdAtMs: NOW - 100,
},
});
}
async function setup(t, storage = ':memory:') {
const database = new Sequelize({
dialect: 'sqlite',
storage,
logging: false,
});
t.after(() => database.close());
await migrate(database);
const policyRepository = new LegacySequelizeProjectPolicyRepository(database);
await bind(policyRepository, OWNER, 'owner', 'bind-owner');
await bind(policyRepository, AGENT, 'operator', 'bind-agent');
await bind(policyRepository, VIEWER, 'viewer', 'bind-viewer');
const repository = new LegacySequelizeApprovalRequestRepository(database);
const policy = new ProjectPolicyEngine(policyRepository);
return {
database,
policyRepository,
repository,
policy,
service: new ApprovalRequestService(repository, policy),
};
}
test('migration owns bounded approval and durable dispatch indexes', async (t) => {
const { database, service } = await setup(t);
await service.create(createInput());
const rows = await database
.getQueryInterface()
.select(null, APPROVAL_REQUEST_TABLE);
assert.equal(rows.length, 1);
assert.equal(rows[0].action_digest, 'a'.repeat(64));
assert.equal(rows[0].preview_digest, 'b'.repeat(64));
assert.equal(JSON.stringify(rows).includes('filesystem.write'), true);
assert.equal('preview' in rows[0], false);
assert.equal('arguments' in rows[0], false);
assert.equal('secret' in rows[0], false);
const requestIndexes = new Set(
(await database.getQueryInterface().showIndex(APPROVAL_REQUEST_TABLE)).map(
(index) => index.name,
),
);
for (const name of [
APPROVAL_REQUEST_DECISION_ID_INDEX,
APPROVAL_REQUEST_CONSUMPTION_ID_INDEX,
APPROVAL_REQUEST_PENDING_INDEX,
APPROVAL_REQUEST_REQUESTER_INDEX,
]) {
assert.ok(requestIndexes.has(name));
}
const dispatchIndexes = new Set(
(
await database
.getQueryInterface()
.showIndex(APPROVED_ACTION_DISPATCH_TABLE)
).map((index) => index.name),
);
assert.ok(dispatchIndexes.has(APPROVED_ACTION_DISPATCH_REQUEST_INDEX));
assert.ok(dispatchIndexes.has(APPROVED_ACTION_DISPATCH_PENDING_INDEX));
});
test('creates, approves and atomically emits a one-time durable dispatch', async (t) => {
const { database, service } = await setup(t);
const pending = await service.create(createInput());
assert.equal(pending.state, 'pending');
assert.equal(pending.version, 1);
const approved = await service.decide(decisionInput());
assert.equal(approved.state, 'approved');
assert.equal(approved.version, 2);
assert.deepEqual(approved.decidedBy, OWNER);
const consumed = await service.consume(consumptionInput());
assert.equal(consumed.request.state, 'consumed');
assert.equal(consumed.request.version, 3);
assert.equal(consumed.dispatch.state, 'pending');
assert.equal(consumed.dispatch.approvalRequestId, pending.id);
assert.equal(
consumed.dispatch.action.actionDigest,
pending.action.actionDigest,
);
assert.equal(
(
await database
.getQueryInterface()
.select(null, APPROVED_ACTION_DISPATCH_TABLE)
).length,
1,
);
});
test('only require-approval actions can create requests', async (t) => {
const { service } = await setup(t);
await assert.rejects(
service.create(createInput({ requestedBy: OWNER })),
ApprovalPolicyDeniedError,
);
await assert.rejects(
service.create(
createInput({ requestedBy: { type: 'agent', id: 'unbound-agent' } }),
),
ApprovalPolicyDeniedError,
);
});
test('decisions require a bound human user with approval.decide', async (t) => {
const { service } = await setup(t);
await service.create(createInput());
await assert.rejects(
service.decide(decisionInput({ decidedBy: AGENT })),
ApprovalHumanDecisionRequiredError,
);
await assert.rejects(
service.decide(decisionInput({ decidedBy: VIEWER })),
ApprovalPolicyDeniedError,
);
const rejected = await service.decide(
decisionInput({ decision: 'rejected', reasonCode: 'unsafe_action' }),
);
assert.equal(rejected.state, 'rejected');
await assert.rejects(
service.consume(consumptionInput()),
ApprovalRequestStateConflictError,
);
});
test('expiry is exact and does not require background timers', async (t) => {
const { service } = await setup(t);
await service.create(createInput());
assert.equal(
(await service.get('approval-1', EXPIRES_AT - 1)).effectiveStatus,
'pending',
);
assert.equal(
(await service.get('approval-1', EXPIRES_AT)).effectiveStatus,
'expired',
);
await assert.rejects(
service.decide(decisionInput({ decidedAtMs: EXPIRES_AT })),
ApprovalRequestExpiredError,
);
await service.create(createInput({ id: 'approval-2' }));
await service.decide(
decisionInput({
requestId: 'approval-2',
decisionId: 'decision-2',
decidedAtMs: EXPIRES_AT - 2,
}),
);
await assert.rejects(
service.consume(
consumptionInput({
requestId: 'approval-2',
consumptionId: 'consumption-2',
dispatchId: 'dispatch-2',
consumedAtMs: EXPIRES_AT,
}),
),
ApprovalRequestExpiredError,
);
});
test('exact mutation replays are idempotent and drift conflicts', async (t) => {
const { service } = await setup(t);
const pending = await service.create(createInput());
assert.deepEqual(await service.create(createInput()), pending);
await assert.rejects(
service.create(createInput({ risk: 'critical' })),
ApprovalMutationConflictError,
);
const approved = await service.decide(decisionInput());
assert.deepEqual(await service.decide(decisionInput()), approved);
await assert.rejects(
service.decide(decisionInput({ reasonCode: 'different_review' })),
ApprovalMutationConflictError,
);
const consumed = await service.consume(consumptionInput());
assert.deepEqual(await service.consume(consumptionInput()), consumed);
assert.deepEqual(await service.create(createInput()), consumed.request);
await assert.rejects(
service.consume(
consumptionInput({ action: action({ actionDigest: 'c'.repeat(64) }) }),
),
ApprovalMutationConflictError,
);
});
test('policy versions fence every mutation against revocation races', async (t) => {
const { policyRepository, policy, repository } = await setup(t);
const authorization = await policy.decideWithFence({
projectId: PROJECT_ID,
subject: AGENT,
permission: action().permission,
});
assert.equal(authorization.decision.effect, 'require_approval');
await policyRepository.append({
expectedCurrentVersion: 1,
binding: {
projectId: PROJECT_ID,
subject: AGENT,
version: 2,
state: 'revoked',
mutationId: 'revoke-agent',
changedBy: OWNER,
createdAtMs: NOW,
},
});
const request = {
...createInput(),
version: 1,
state: 'pending',
decisionId: null,
decision: null,
decisionReasonCode: null,
decidedBy: null,
decidedAtMs: null,
consumptionId: null,
dispatchId: null,
consumedBy: null,
consumedAtMs: null,
};
await assert.rejects(
repository.create({
request,
authorizationFence: authorization.fence,
}),
ApprovalPolicyFenceConflictError,
);
});
test('two SQLite connections allow only one decision and one consumption', async (t) => {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-approval-'));
t.after(() => fs.rm(directory, { recursive: true, force: true }));
const storage = path.join(directory, 'database.sqlite');
const first = await setup(t, storage);
const secondDatabase = new Sequelize({
dialect: 'sqlite',
storage,
logging: false,
});
t.after(() => secondDatabase.close());
const secondPolicyRepository = new LegacySequelizeProjectPolicyRepository(
secondDatabase,
);
const secondRepository = new LegacySequelizeApprovalRequestRepository(
secondDatabase,
);
const secondService = new ApprovalRequestService(
secondRepository,
new ProjectPolicyEngine(secondPolicyRepository),
);
await first.service.create(createInput());
const decisions = await Promise.allSettled([
first.service.decide(decisionInput()),
secondService.decide(
decisionInput({ decisionId: 'decision-racer', reasonCode: 'racer' }),
),
]);
assert.equal(
decisions.filter((entry) => entry.status === 'fulfilled').length,
1,
);
assert.equal(
decisions.filter((entry) => entry.status === 'rejected').length,
1,
);
assert.ok(
decisions
.filter((entry) => entry.status === 'rejected')
.every(
(entry) =>
entry.reason instanceof ApprovalRequestVersionConflictError ||
entry.reason instanceof ApprovalRequestStateConflictError,
),
);
const approved = await first.repository.findById('approval-1');
const winnerDecisionId = approved.decisionId;
const winnerDecision =
winnerDecisionId === 'decision-1'
? decisionInput()
: decisionInput({ decisionId: 'decision-racer', reasonCode: 'racer' });
assert.deepEqual(await first.service.decide(winnerDecision), approved);
const consumptions = await Promise.allSettled([
first.service.consume(consumptionInput()),
secondService.consume(
consumptionInput({
consumptionId: 'consumption-racer',
dispatchId: 'dispatch-racer',
}),
),
]);
assert.equal(
consumptions.filter((entry) => entry.status === 'fulfilled').length,
1,
);
assert.equal(
consumptions.filter((entry) => entry.status === 'rejected').length,
1,
);
assert.equal(
(
await first.database
.getQueryInterface()
.select(null, APPROVED_ACTION_DISPATCH_TABLE)
).length,
1,
);
});
test('dispatch collisions roll back consumption and leave approval reusable', async (t) => {
const { database, service, repository } = await setup(t);
await service.create(createInput());
await service.decide(decisionInput());
await service.create(
createInput({
id: 'approval-2',
action: action({ actionRef: 'planned-action-2' }),
}),
);
await service.decide(
decisionInput({ requestId: 'approval-2', decisionId: 'decision-2' }),
);
await service.consume(consumptionInput());
await assert.rejects(
service.consume(
consumptionInput({
requestId: 'approval-2',
consumptionId: 'consumption-2',
dispatchId: 'dispatch-1',
action: action({ actionRef: 'planned-action-2' }),
}),
),
ApprovalMutationConflictError,
);
assert.equal((await repository.findById('approval-2')).state, 'approved');
assert.equal(
(
await database
.getQueryInterface()
.select(null, APPROVED_ACTION_DISPATCH_TABLE)
).length,
1,
);
});
+645
View File
@@ -0,0 +1,645 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const { QueryTypes, Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const {
projectPolicyMigration,
} = require('../../back/migrations/0017-project-policy');
const {
APPROVAL_REQUEST_TABLE,
APPROVED_ACTION_DISPATCH_TABLE,
approvalRequestMigration,
} = require('../../back/migrations/0020-approval-requests');
const {
APPROVED_ACTION_DISPATCH_EXECUTION_DUE_INDEX,
APPROVED_ACTION_DISPATCH_EXECUTION_LEASE_INDEX,
APPROVED_ACTION_DISPATCH_EXECUTION_PROJECT_INDEX,
APPROVED_ACTION_DISPATCH_EXECUTION_TABLE,
approvedActionDispatchExecutionMigration,
} = require('../../back/migrations/0021-approved-action-dispatch-executions');
const {
approvedActionRecoveryMigration,
} = require('../../back/migrations/0022-approved-action-recovery');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacySequelizeApprovedActionDispatchRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/approvedActionDispatchRepository');
const {
LegacySequelizeApprovalRequestRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/approvalRequestRepository');
const {
LegacySequelizeProjectPolicyRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/projectPolicyRepository');
const {
ApprovalRequestService,
} = require('../../back/runtime/application/approvalRequestService');
const {
ProjectPolicyEngine,
} = require('../../back/runtime/application/projectPolicyEngine');
const {
ApprovedActionDispatchBindingConflictError,
ApprovedActionDispatchFenceRejectedError,
ApprovedActionDispatchRepositoryError,
} = require('../../back/runtime/domain/approvedActionDispatchExecution');
const {
ApprovalUnavailableError,
} = require('../../back/runtime/domain/approvalRequest');
const PROJECT_ID = 'default';
const AGENT = Object.freeze({ type: 'agent', id: 'agent-1' });
const OWNER = Object.freeze({ type: 'user', id: 'owner-1' });
const SYSTEM = Object.freeze({ type: 'system', id: 'approval-dispatcher' });
const BASE_TIME = 100_000;
function action(name, digestCharacter = 'a') {
return {
permission: 'tool.call:filesystem.write',
actionType: 'tool_call',
actionRef: `planned-${name}`,
actionDigest: digestCharacter.repeat(64),
previewDigest: 'f'.repeat(64),
};
}
async function migrate(database, migrations) {
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations,
logger: { info() {} },
});
}
async function bind(policyRepository, subject, role, mutationId) {
await policyRepository.append({
expectedCurrentVersion: 0,
binding: {
projectId: PROJECT_ID,
subject,
version: 1,
state: 'active',
role,
mutationId,
changedBy: OWNER,
createdAtMs: BASE_TIME - 100,
},
});
}
async function setup(t, storage = ':memory:') {
const database = new Sequelize({
dialect: 'sqlite',
storage,
logging: false,
});
t.after(() => database.close());
await migrate(database, [
projectPolicyMigration,
approvalRequestMigration,
approvedActionDispatchExecutionMigration,
approvedActionRecoveryMigration,
]);
const policyRepository = new LegacySequelizeProjectPolicyRepository(database);
await bind(policyRepository, OWNER, 'owner', 'bind-owner');
await bind(policyRepository, AGENT, 'operator', 'bind-agent');
const approvalRepository = new LegacySequelizeApprovalRequestRepository(
database,
);
return {
database,
approvalRepository,
approvalService: new ApprovalRequestService(
approvalRepository,
new ProjectPolicyEngine(policyRepository),
),
executionRepository: new LegacySequelizeApprovedActionDispatchRepository(
database,
),
};
}
async function prepareDispatch(service, name, offset = 0, digest = 'a') {
const requestedAtMs = BASE_TIME + offset;
const binding = action(name, digest);
await service.create({
id: `approval-${name}`,
projectId: PROJECT_ID,
action: binding,
risk: 'high',
requestedBy: AGENT,
requestedAtMs,
expiresAtMs: requestedAtMs + 60_000,
});
await service.decide({
requestId: `approval-${name}`,
expectedVersion: 1,
decisionId: `decision-${name}`,
decision: 'approved',
reasonCode: 'reviewed_action',
decidedBy: OWNER,
decidedAtMs: requestedAtMs + 10,
});
const consumed = await service.consume({
requestId: `approval-${name}`,
expectedVersion: 2,
consumptionId: `consumption-${name}`,
dispatchId: `dispatch-${name}`,
action: binding,
requestedBy: AGENT,
consumedBy: SYSTEM,
consumedAtMs: requestedAtMs + 20,
});
return consumed.dispatch;
}
function claimInput(dispatchId, overrides = {}) {
return {
dispatchId,
owner: 'dispatcher-1',
leaseToken: 'lease-1',
nowMs: BASE_TIME + 1_000,
leaseDurationMs: 1_000,
...overrides,
};
}
test('0021 backfills immutable dispatches and owns bounded execution indexes', async (t) => {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
t.after(() => database.close());
await migrate(database, [projectPolicyMigration, approvalRequestMigration]);
const queryInterface = database.getQueryInterface();
await queryInterface.bulkInsert(APPROVAL_REQUEST_TABLE, [
{
id: 'approval-backfill',
project_id: PROJECT_ID,
version: 3,
state: 'consumed',
permission: 'tool.call:filesystem.write',
action_type: 'tool_call',
action_ref: 'planned-backfill',
action_digest: 'a'.repeat(64),
preview_digest: 'f'.repeat(64),
risk: 'high',
requested_by_type: 'agent',
requested_by_id: AGENT.id,
requested_at_ms: BASE_TIME,
expires_at_ms: BASE_TIME + 60_000,
decision_id: 'decision-backfill',
decision: 'approved',
decision_reason_code: 'reviewed_action',
decided_by_type: 'user',
decided_by_id: OWNER.id,
decided_at_ms: BASE_TIME + 10,
consumption_id: 'consumption-backfill',
dispatch_id: 'dispatch-backfill',
consumed_by_type: 'system',
consumed_by_id: SYSTEM.id,
consumed_at_ms: BASE_TIME + 20,
},
]);
await queryInterface.bulkInsert(APPROVED_ACTION_DISPATCH_TABLE, [
{
id: 'dispatch-backfill',
approval_request_id: 'approval-backfill',
approval_request_version: 3,
project_id: PROJECT_ID,
state: 'pending',
permission: 'tool.call:filesystem.write',
action_type: 'tool_call',
action_ref: 'planned-backfill',
action_digest: 'a'.repeat(64),
preview_digest: 'f'.repeat(64),
requested_by_type: 'agent',
requested_by_id: AGENT.id,
consumed_by_type: 'system',
consumed_by_id: SYSTEM.id,
created_at_ms: BASE_TIME + 20,
},
]);
await migrate(database, [approvedActionDispatchExecutionMigration]);
const rows = await queryInterface.select(
null,
APPROVED_ACTION_DISPATCH_EXECUTION_TABLE,
);
assert.equal(rows.length, 1);
assert.equal(rows[0].dispatch_id, 'dispatch-backfill');
assert.equal(rows[0].status, 'pending');
assert.equal(rows[0].version, 0);
assert.equal(rows[0].eligible_at_ms, BASE_TIME + 20);
const indexes = new Set(
(
await queryInterface.showIndex(APPROVED_ACTION_DISPATCH_EXECUTION_TABLE)
).map((index) => index.name),
);
for (const name of [
APPROVED_ACTION_DISPATCH_EXECUTION_DUE_INDEX,
APPROVED_ACTION_DISPATCH_EXECUTION_PROJECT_INDEX,
APPROVED_ACTION_DISPATCH_EXECUTION_LEASE_INDEX,
]) {
assert.ok(indexes.has(name));
}
});
test('approval consumption atomically creates the immutable dispatch and execution baseline', async (t) => {
const { database, approvalService, executionRepository } = await setup(t);
const dispatch = await prepareDispatch(approvalService, 'atomic');
const snapshot = await executionRepository.findById(dispatch.id);
assert.equal(snapshot.execution.status, 'pending');
assert.equal(snapshot.execution.version, 0);
assert.equal(snapshot.execution.attemptCount, 0);
assert.equal(snapshot.execution.eligibleAtMs, dispatch.createdAtMs);
await approvalService.create({
id: 'approval-rollback',
projectId: PROJECT_ID,
action: action('rollback', 'b'),
risk: 'high',
requestedBy: AGENT,
requestedAtMs: BASE_TIME + 100,
expiresAtMs: BASE_TIME + 60_100,
});
await approvalService.decide({
requestId: 'approval-rollback',
expectedVersion: 1,
decisionId: 'decision-rollback',
decision: 'approved',
reasonCode: 'reviewed_action',
decidedBy: OWNER,
decidedAtMs: BASE_TIME + 110,
});
await database.query(
`CREATE TRIGGER fail_approved_execution_insert
BEFORE INSERT ON "${APPROVED_ACTION_DISPATCH_EXECUTION_TABLE}"
BEGIN SELECT RAISE(ABORT, 'forced execution insert failure'); END`,
);
await assert.rejects(
approvalService.consume({
requestId: 'approval-rollback',
expectedVersion: 2,
consumptionId: 'consumption-rollback',
dispatchId: 'dispatch-rollback',
action: action('rollback', 'b'),
requestedBy: AGENT,
consumedBy: SYSTEM,
consumedAtMs: BASE_TIME + 120,
}),
ApprovalUnavailableError,
);
assert.equal(await executionRepository.findById('dispatch-rollback'), null);
assert.equal(
(
await database.query(
`SELECT state FROM "${APPROVAL_REQUEST_TABLE}"
WHERE id = 'approval-rollback'`,
{ type: QueryTypes.SELECT },
)
)[0].state,
'approved',
);
});
test('lists a bounded stable due page without loading executing or future work', async (t) => {
const { approvalService, executionRepository } = await setup(t);
await prepareDispatch(approvalService, 'page-a', 0, 'a');
await prepareDispatch(approvalService, 'page-b', 10, 'b');
await prepareDispatch(approvalService, 'page-c', 20, 'c');
const first = await executionRepository.listDue({
nowMs: BASE_TIME + 1_000,
limit: 2,
});
assert.deepEqual(
first.dispatches.map((entry) => entry.dispatch.id),
['dispatch-page-a', 'dispatch-page-b'],
);
assert.equal(first.truncated, true);
const second = await executionRepository.listDue({
nowMs: BASE_TIME + 1_000,
limit: 2,
cursor: first.nextCursor,
});
assert.deepEqual(
second.dispatches.map((entry) => entry.dispatch.id),
['dispatch-page-c'],
);
assert.equal(second.truncated, false);
});
test('claims idempotently and lets only an expired pre-start lease be taken over', async (t) => {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-action-'));
t.after(() => fs.rm(directory, { recursive: true, force: true }));
const storage = path.join(directory, 'database.sqlite');
const first = await setup(t, storage);
await prepareDispatch(first.approvalService, 'race');
const secondDatabase = new Sequelize({
dialect: 'sqlite',
storage,
logging: false,
});
t.after(() => secondDatabase.close());
const secondRepository = new LegacySequelizeApprovedActionDispatchRepository(
secondDatabase,
);
const results = await Promise.all([
first.executionRepository.claim(claimInput('dispatch-race')),
secondRepository.claim(
claimInput('dispatch-race', {
owner: 'dispatcher-2',
leaseToken: 'lease-2',
}),
),
]);
assert.equal(
results.filter((result) => result.status === 'claimed').length,
1,
);
assert.equal(
results.filter((result) => result.status === 'leased').length,
1,
);
const winner = results.find((result) => result.status === 'claimed');
assert.equal(
(
await first.executionRepository.claim(
claimInput('dispatch-race', {
owner: winner.snapshot.execution.leaseOwner,
leaseToken: winner.snapshot.execution.leaseToken,
}),
)
).status,
'claimed',
);
const takeover = await secondRepository.claim(
claimInput('dispatch-race', {
owner: 'dispatcher-3',
leaseToken: 'lease-3',
nowMs: BASE_TIME + 2_000,
}),
);
assert.equal(takeover.status, 'claimed');
assert.equal(takeover.snapshot.execution.attemptCount, 2);
assert.equal(takeover.snapshot.execution.leaseOwner, 'dispatcher-3');
});
test('start barrier binds the exact approval and action digest before side effects', async (t) => {
const { approvalService, executionRepository } = await setup(t);
const dispatch = await prepareDispatch(approvalService, 'start');
const claimed = await executionRepository.claim(
claimInput(dispatch.id, { nowMs: BASE_TIME + 1_000 }),
);
await assert.rejects(
executionRepository.start({
dispatchId: dispatch.id,
approvalRequestId: dispatch.approvalRequestId,
actionDigest: 'e'.repeat(64),
owner: 'dispatcher-1',
leaseToken: 'lease-1',
expectedVersion: claimed.snapshot.execution.version,
startedAtMs: BASE_TIME + 1_100,
}),
ApprovedActionDispatchBindingConflictError,
);
const started = await executionRepository.start({
dispatchId: dispatch.id,
approvalRequestId: dispatch.approvalRequestId,
actionDigest: dispatch.action.actionDigest,
owner: 'dispatcher-1',
leaseToken: 'lease-1',
expectedVersion: claimed.snapshot.execution.version,
startedAtMs: BASE_TIME + 1_100,
});
assert.equal(started.execution.status, 'executing');
assert.equal(started.execution.eligibleAtMs, null);
assert.deepEqual(
await executionRepository.start({
dispatchId: dispatch.id,
approvalRequestId: dispatch.approvalRequestId,
actionDigest: dispatch.action.actionDigest,
owner: 'dispatcher-1',
leaseToken: 'lease-1',
expectedVersion: claimed.snapshot.execution.version,
startedAtMs: BASE_TIME + 1_100,
}),
started,
);
});
test('executing lease expiry requires recovery and can never auto-take over', async (t) => {
const { approvalService, executionRepository } = await setup(t);
const dispatch = await prepareDispatch(approvalService, 'recovery');
const claimed = await executionRepository.claim(
claimInput(dispatch.id, {
nowMs: BASE_TIME + 1_000,
leaseDurationMs: 100,
}),
);
const started = await executionRepository.start({
dispatchId: dispatch.id,
approvalRequestId: dispatch.approvalRequestId,
actionDigest: dispatch.action.actionDigest,
owner: 'dispatcher-1',
leaseToken: 'lease-1',
expectedVersion: claimed.snapshot.execution.version,
startedAtMs: BASE_TIME + 1_010,
});
const takeover = await executionRepository.claim(
claimInput(dispatch.id, {
owner: 'dispatcher-2',
leaseToken: 'lease-2',
nowMs: BASE_TIME + 1_100,
}),
);
assert.equal(takeover.status, 'recovery_required');
assert.equal(takeover.snapshot.execution.leaseOwner, 'dispatcher-1');
assert.equal(
(
await executionRepository.listDue({
nowMs: BASE_TIME + 10_000,
limit: 64,
})
).dispatches.length,
0,
);
const renewed = await executionRepository.renew({
dispatchId: dispatch.id,
owner: 'dispatcher-1',
leaseToken: 'lease-1',
expectedVersion: started.execution.version,
nowMs: BASE_TIME + 1_200,
leaseDurationMs: 1_000,
});
assert.equal(renewed.execution.status, 'executing');
});
test('pre-effect failures retry safely but exhaust into a terminal block', async (t) => {
const { database, approvalService, executionRepository } = await setup(t);
const dispatch = await prepareDispatch(approvalService, 'retry');
await database.query(
`UPDATE "${APPROVED_ACTION_DISPATCH_EXECUTION_TABLE}"
SET max_attempts = 2
WHERE dispatch_id = :dispatchId`,
{ replacements: { dispatchId: dispatch.id } },
);
const first = await executionRepository.claim(claimInput(dispatch.id));
const retry = await executionRepository.releaseBeforeStart({
dispatchId: dispatch.id,
owner: 'dispatcher-1',
leaseToken: 'lease-1',
expectedVersion: first.snapshot.execution.version,
resultMutationId: 'preflight-result-1',
resultCode: 'executor_unavailable',
atMs: BASE_TIME + 1_010,
retryAtMs: BASE_TIME + 1_020,
});
assert.equal(retry.execution.status, 'retry_wait');
assert.deepEqual(
await executionRepository.releaseBeforeStart({
dispatchId: dispatch.id,
owner: 'dispatcher-1',
leaseToken: 'lease-1',
expectedVersion: first.snapshot.execution.version,
resultMutationId: 'preflight-result-1',
resultCode: 'executor_unavailable',
atMs: BASE_TIME + 1_010,
retryAtMs: BASE_TIME + 1_020,
}),
retry,
);
assert.equal(
(
await executionRepository.claim(
claimInput(dispatch.id, { nowMs: BASE_TIME + 1_019 }),
)
).status,
'not_due',
);
const second = await executionRepository.claim(
claimInput(dispatch.id, {
leaseToken: 'lease-2',
nowMs: BASE_TIME + 1_020,
}),
);
const blocked = await executionRepository.releaseBeforeStart({
dispatchId: dispatch.id,
owner: 'dispatcher-1',
leaseToken: 'lease-2',
expectedVersion: second.snapshot.execution.version,
resultMutationId: 'preflight-result-2',
resultCode: 'executor_unavailable',
atMs: BASE_TIME + 1_030,
retryAtMs: BASE_TIME + 1_040,
});
assert.equal(blocked.execution.status, 'blocked');
assert.equal(blocked.execution.completedAtMs, BASE_TIME + 1_030);
});
test('completion is fenced, idempotent, and indeterminate evidence blocks replay', async (t) => {
const { approvalService, executionRepository } = await setup(t);
const dispatch = await prepareDispatch(approvalService, 'complete');
const claimed = await executionRepository.claim(claimInput(dispatch.id));
const started = await executionRepository.start({
dispatchId: dispatch.id,
approvalRequestId: dispatch.approvalRequestId,
actionDigest: dispatch.action.actionDigest,
owner: 'dispatcher-1',
leaseToken: 'lease-1',
expectedVersion: claimed.snapshot.execution.version,
startedAtMs: BASE_TIME + 1_010,
});
await assert.rejects(
executionRepository.complete({
dispatchId: dispatch.id,
owner: 'dispatcher-2',
leaseToken: 'lease-2',
expectedVersion: started.execution.version,
resultMutationId: 'completion-wrong',
outcome: 'succeeded',
resultCode: 'ok',
completedAtMs: BASE_TIME + 1_100,
}),
ApprovedActionDispatchFenceRejectedError,
);
const command = {
dispatchId: dispatch.id,
owner: 'dispatcher-1',
leaseToken: 'lease-1',
expectedVersion: started.execution.version,
resultMutationId: 'completion-1',
outcome: 'indeterminate',
resultCode: 'transport_lost_after_start',
completedAtMs: BASE_TIME + 2_100,
};
const completed = await executionRepository.complete(command);
assert.equal(completed.execution.status, 'blocked');
assert.deepEqual(await executionRepository.complete(command), completed);
assert.equal(
(await executionRepository.claim(claimInput(dispatch.id))).status,
'blocked',
);
});
test('a successful result can arrive after lease expiry without re-executing', async (t) => {
const { approvalService, executionRepository } = await setup(t);
const dispatch = await prepareDispatch(approvalService, 'success');
const claimed = await executionRepository.claim(
claimInput(dispatch.id, {
nowMs: BASE_TIME + 1_000,
leaseDurationMs: 100,
}),
);
const started = await executionRepository.start({
dispatchId: dispatch.id,
approvalRequestId: dispatch.approvalRequestId,
actionDigest: dispatch.action.actionDigest,
owner: 'dispatcher-1',
leaseToken: 'lease-1',
expectedVersion: claimed.snapshot.execution.version,
startedAtMs: BASE_TIME + 1_010,
});
const command = {
dispatchId: dispatch.id,
owner: 'dispatcher-1',
leaseToken: 'lease-1',
expectedVersion: started.execution.version,
resultMutationId: 'completion-success',
outcome: 'succeeded',
resultCode: 'ok',
completedAtMs: BASE_TIME + 2_000,
};
const completed = await executionRepository.complete(command);
assert.equal(completed.execution.status, 'succeeded');
assert.deepEqual(await executionRepository.complete(command), completed);
assert.equal(
(await executionRepository.claim(claimInput(dispatch.id))).status,
'succeeded',
);
});
test('missing execution state is corruption rather than an absent dispatch', async (t) => {
const { database, approvalService, executionRepository } = await setup(t);
const dispatch = await prepareDispatch(approvalService, 'corrupt');
await database.query('PRAGMA foreign_keys = OFF');
await database.query(
`DELETE FROM "${APPROVED_ACTION_DISPATCH_EXECUTION_TABLE}"
WHERE dispatch_id = :dispatchId`,
{ replacements: { dispatchId: dispatch.id } },
);
await database.query('PRAGMA foreign_keys = ON');
await assert.rejects(
executionRepository.findById(dispatch.id),
ApprovedActionDispatchRepositoryError,
);
});
+349
View File
@@ -0,0 +1,349 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
ApprovedActionDispatcher,
} = require('../../back/runtime/application/approvedActionDispatcher');
function pendingSnapshot(name = 'one') {
return {
dispatch: {
id: `dispatch-${name}`,
approvalRequestId: `approval-${name}`,
approvalRequestVersion: 3,
projectId: 'default',
state: 'pending',
action: {
permission: 'tool.call:filesystem.write',
actionType: 'tool_call',
actionRef: `planned-${name}`,
actionDigest: 'a'.repeat(64),
previewDigest: 'f'.repeat(64),
},
requestedBy: { type: 'agent', id: 'agent-1' },
consumedBy: { type: 'system', id: 'approval-dispatcher' },
createdAtMs: 100,
},
execution: {
dispatchId: `dispatch-${name}`,
projectId: 'default',
status: 'pending',
version: 0,
attemptCount: 0,
maxAttempts: 5,
eligibleAtMs: 100,
nextAttemptAtMs: null,
leaseOwner: null,
leaseToken: null,
leaseExpiresAtMs: null,
startedAtMs: null,
resultMutationId: null,
lastResultCode: null,
completedAtMs: null,
createdAtMs: 100,
updatedAtMs: 100,
},
};
}
function fakeRepository(options = {}) {
const calls = [];
const initial = pendingSnapshot();
return {
calls,
async listDue(query) {
calls.push(['list', query]);
return {
dispatches: [initial],
truncated: false,
};
},
async claim(command) {
calls.push(['claim', command]);
return {
status: 'claimed',
snapshot: {
dispatch: initial.dispatch,
execution: {
...initial.execution,
status: 'leased',
version: 1,
attemptCount: 1,
eligibleAtMs: command.nowMs + command.leaseDurationMs,
leaseOwner: command.owner,
leaseToken: command.leaseToken,
leaseExpiresAtMs: command.nowMs + command.leaseDurationMs,
updatedAtMs: command.nowMs,
},
},
};
},
async start(command) {
calls.push(['start', command]);
if (options.startError) throw new Error('start unavailable');
const claim = calls.find((entry) => entry[0] === 'claim')[1];
return {
dispatch: initial.dispatch,
execution: {
...initial.execution,
status: 'executing',
version: 2,
attemptCount: 1,
eligibleAtMs: null,
leaseOwner: claim.owner,
leaseToken: claim.leaseToken,
leaseExpiresAtMs: claim.nowMs + claim.leaseDurationMs,
startedAtMs: command.startedAtMs,
updatedAtMs: command.startedAtMs,
},
};
},
async releaseBeforeStart(command) {
calls.push(['release', command]);
return {
dispatch: initial.dispatch,
execution: {
...initial.execution,
status: command.retryAtMs === undefined ? 'blocked' : 'retry_wait',
version: 2,
attemptCount: 1,
eligibleAtMs: command.retryAtMs ?? null,
nextAttemptAtMs: command.retryAtMs ?? null,
resultMutationId: command.resultMutationId,
lastResultCode: command.resultCode,
completedAtMs: command.retryAtMs === undefined ? command.atMs : null,
updatedAtMs: command.atMs,
},
};
},
async complete(command) {
calls.push(['complete', command]);
if (options.completeError) throw new Error('completion unavailable');
return {
dispatch: initial.dispatch,
execution: {
...initial.execution,
status:
command.outcome === 'indeterminate' ? 'blocked' : command.outcome,
version: 3,
attemptCount: 1,
eligibleAtMs: null,
startedAtMs: 103,
resultMutationId: command.resultMutationId,
lastResultCode: command.resultCode,
completedAtMs: command.completedAtMs,
updatedAtMs: command.completedAtMs,
},
};
},
};
}
function deterministicOptions() {
let now = 100;
let id = 0;
return {
owner: 'dispatcher-1',
leaseDurationMs: 1_000,
retryBaseMs: 10,
retryMaxMs: 100,
clock: () => ++now,
createId: () => `mutation-${++id}`,
};
}
function handler(overrides = {}) {
return {
actionType: 'tool_call',
async inspect(dispatch) {
return { status: 'ready', actionDigest: dispatch.action.actionDigest };
},
async execute() {
return { outcome: 'succeeded', resultCode: 'ok' };
},
...overrides,
};
}
test('persists the start barrier before invoking a successful handler', async () => {
const repository = fakeRepository();
const observed = [];
const dispatcher = new ApprovedActionDispatcher(
repository,
[
handler({
async inspect(dispatch) {
observed.push(['inspect', dispatch.id]);
return {
status: 'ready',
actionDigest: dispatch.action.actionDigest,
};
},
async execute(context) {
observed.push(['execute', context.dispatch.id]);
assert.equal(context.idempotencyKey, 'dispatch-one');
assert.equal(context.execution.status, 'executing');
assert.equal(context.fence.owner, 'dispatcher-1');
assert.equal(context.fence.version, 2);
return { outcome: 'succeeded', resultCode: 'ok' };
},
}),
],
deterministicOptions(),
);
const summary = await dispatcher.dispatchBatch({ limit: 1 });
assert.deepEqual(
repository.calls.map((entry) => entry[0]),
['list', 'claim', 'start', 'complete'],
);
assert.deepEqual(observed, [
['inspect', 'dispatch-one'],
['execute', 'dispatch-one'],
]);
assert.equal(summary.claimed, 1);
assert.equal(summary.started, 1);
assert.equal(summary.succeeded, 1);
assert.equal(summary.blocked, 0);
});
test('missing handlers and inspection failures retry only before start', async () => {
for (const handlers of [
[],
[
handler({
async inspect() {
throw new Error('temporary inspection failure');
},
}),
],
]) {
const repository = fakeRepository();
const dispatcher = new ApprovedActionDispatcher(
repository,
handlers,
deterministicOptions(),
);
const summary = await dispatcher.dispatchBatch();
assert.deepEqual(
repository.calls.map((entry) => entry[0]),
['list', 'claim', 'release'],
);
assert.equal(summary.retrying, 1);
assert.equal(summary.started, 0);
}
});
test('digest drift and explicit inspection blocks never invoke execute', async () => {
for (const inspection of [
{ status: 'ready', actionDigest: 'b'.repeat(64) },
{ status: 'blocked', resultCode: 'plan_revoked' },
]) {
let executed = false;
const repository = fakeRepository();
const dispatcher = new ApprovedActionDispatcher(
repository,
[
handler({
async inspect() {
return inspection;
},
async execute() {
executed = true;
return { outcome: 'succeeded', resultCode: 'ok' };
},
}),
],
deterministicOptions(),
);
const summary = await dispatcher.dispatchBatch();
assert.equal(executed, false);
assert.equal(summary.blocked, 1);
assert.deepEqual(
repository.calls.map((entry) => entry[0]),
['list', 'claim', 'release'],
);
}
});
test('handler exceptions after start become indeterminate terminal evidence', async () => {
const repository = fakeRepository();
const dispatcher = new ApprovedActionDispatcher(
repository,
[
handler({
async execute() {
throw new Error('transport disappeared after invocation');
},
}),
],
deterministicOptions(),
);
const summary = await dispatcher.dispatchBatch();
const completion = repository.calls.find(
(entry) => entry[0] === 'complete',
)[1];
assert.equal(completion.outcome, 'indeterminate');
assert.equal(completion.resultCode, 'handler_failed_after_start');
assert.equal(summary.blocked, 1);
assert.equal(summary.retrying, 0);
});
test('a completion persistence failure reports recovery instead of retrying execute', async () => {
let executions = 0;
const repository = fakeRepository({ completeError: true });
const dispatcher = new ApprovedActionDispatcher(
repository,
[
handler({
async execute() {
executions += 1;
return { outcome: 'succeeded', resultCode: 'ok' };
},
}),
],
deterministicOptions(),
);
const summary = await dispatcher.dispatchBatch();
assert.equal(executions, 1);
assert.equal(summary.unavailable, 1);
assert.equal(summary.recoveryRequired, 1);
assert.equal(
repository.calls.some((entry) => entry[0] === 'release'),
false,
);
});
test('rejects duplicate handlers and extensible handler results', async () => {
assert.throws(
() =>
new ApprovedActionDispatcher(
fakeRepository(),
[handler(), handler()],
deterministicOptions(),
),
/Duplicate approved action handler/,
);
const repository = fakeRepository();
const dispatcher = new ApprovedActionDispatcher(
repository,
[
handler({
async inspect(dispatch) {
return {
status: 'ready',
actionDigest: dispatch.action.actionDigest,
injected: true,
};
},
}),
],
deterministicOptions(),
);
const summary = await dispatcher.dispatchBatch();
assert.equal(summary.retrying, 1);
assert.equal(
repository.calls.some((entry) => entry[0] === 'start'),
false,
);
});
@@ -0,0 +1,394 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const { QueryTypes, Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const {
projectPolicyMigration,
} = require('../../back/migrations/0017-project-policy');
const {
approvalRequestMigration,
} = require('../../back/migrations/0020-approval-requests');
const {
approvedActionDispatchExecutionMigration,
} = require('../../back/migrations/0021-approved-action-dispatch-executions');
const {
approvedActionRecoveryMigration,
} = require('../../back/migrations/0022-approved-action-recovery');
const {
APPROVED_ACTION_RECOVERY_AUTHORIZATION_AUTH_INDEX,
APPROVED_ACTION_RECOVERY_AUTHORIZATION_MUTATION_INDEX,
APPROVED_ACTION_RECOVERY_AUTHORIZATION_PROJECT_INDEX,
APPROVED_ACTION_RECOVERY_AUTHORIZATION_TABLE,
approvedActionRecoveryAuthorizationMigration,
} = require('../../back/migrations/0024-approved-action-recovery-authorization');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacySequelizeApprovedActionDispatchRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/approvedActionDispatchRepository');
const {
LegacySequelizeApprovedActionRecoveryRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/approvedActionRecoveryRepository');
const {
LegacySequelizeApprovalRequestRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/approvalRequestRepository');
const {
LegacySequelizeProjectPolicyRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/projectPolicyRepository');
const {
ApprovedActionManualRecoveryService,
} = require('../../back/runtime/application/approvedActionManualRecoveryService');
const {
ApprovalRequestService,
} = require('../../back/runtime/application/approvalRequestService');
const {
ProjectPolicyEngine,
} = require('../../back/runtime/application/projectPolicyEngine');
const {
ApprovedActionRecoveryRepositoryError,
ApprovedActionRecoveryFenceRejectedError,
} = require('../../back/runtime/domain/approvedActionRecovery');
const {
ApprovedActionRecoveryAuthorizationDeniedError,
ApprovedActionRecoveryHumanRequiredError,
ApprovedActionRecoveryStrongAuthenticationRequiredError,
} = require('../../back/runtime/domain/approvedActionRecoveryAuthorization');
const PROJECT_ID = 'default';
const AGENT = Object.freeze({ type: 'agent', id: 'agent-1' });
const OWNER = Object.freeze({ type: 'user', id: 'owner-1' });
const SYSTEM = Object.freeze({ type: 'system', id: 'approval-dispatcher' });
const BASE_TIME = 300_000;
const RESOLUTION_TIME = BASE_TIME + 2_000;
async function migrate(database) {
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations: [
projectPolicyMigration,
approvalRequestMigration,
approvedActionDispatchExecutionMigration,
approvedActionRecoveryMigration,
approvedActionRecoveryAuthorizationMigration,
],
logger: { info() {} },
});
}
async function bind(repository, subject, role, mutationId) {
await repository.append({
expectedCurrentVersion: 0,
binding: {
projectId: PROJECT_ID,
subject,
version: 1,
state: 'active',
role,
mutationId,
changedBy: OWNER,
createdAtMs: BASE_TIME - 100,
},
});
}
async function setup(t) {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
t.after(() => database.close());
await migrate(database);
const policyRepository = new LegacySequelizeProjectPolicyRepository(database);
await bind(policyRepository, OWNER, 'owner', 'bind-owner');
await bind(policyRepository, AGENT, 'operator', 'bind-agent');
const policy = new ProjectPolicyEngine(policyRepository);
const approvals = new ApprovalRequestService(
new LegacySequelizeApprovalRequestRepository(database),
policy,
);
const executions = new LegacySequelizeApprovedActionDispatchRepository(
database,
);
const recovery = new LegacySequelizeApprovedActionRecoveryRepository(
database,
);
const action = {
permission: 'tool.call:filesystem.write',
actionType: 'tool_call',
actionRef: 'manual-recovery-plan',
actionDigest: 'a'.repeat(64),
previewDigest: 'f'.repeat(64),
};
await approvals.create({
id: 'approval-manual-recovery',
projectId: PROJECT_ID,
action,
risk: 'critical',
requestedBy: AGENT,
requestedAtMs: BASE_TIME,
expiresAtMs: BASE_TIME + 60_000,
});
await approvals.decide({
requestId: 'approval-manual-recovery',
expectedVersion: 1,
decisionId: 'decision-manual-recovery',
decision: 'approved',
reasonCode: 'reviewed_action',
decidedBy: OWNER,
decidedAtMs: BASE_TIME + 10,
});
const dispatch = (
await approvals.consume({
requestId: 'approval-manual-recovery',
expectedVersion: 2,
consumptionId: 'consumption-manual-recovery',
dispatchId: 'dispatch-manual-recovery',
action,
requestedBy: AGENT,
consumedBy: SYSTEM,
consumedAtMs: BASE_TIME + 20,
})
).dispatch;
const claimed = await executions.claim({
dispatchId: dispatch.id,
owner: 'dispatcher-1',
leaseToken: 'execution-lease-1',
nowMs: BASE_TIME + 100,
leaseDurationMs: 100,
});
const started = await executions.start({
dispatchId: dispatch.id,
approvalRequestId: dispatch.approvalRequestId,
actionDigest: dispatch.action.actionDigest,
owner: 'dispatcher-1',
leaseToken: 'execution-lease-1',
expectedVersion: claimed.snapshot.execution.version,
startedAtMs: BASE_TIME + 110,
});
return {
database,
policyRepository,
policy,
recovery,
dispatch,
started,
};
}
function principal(overrides = {}) {
return {
subject: OWNER,
authenticationId: 'mfa-session-1',
authenticatedAtMs: RESOLUTION_TIME - 1_000,
expiresAtMs: RESOLUTION_TIME + 60_000,
assurance: 'multi_factor',
...overrides,
};
}
function input(state, overrides = {}) {
return {
dispatchId: state.dispatch.id,
expectedExecutionVersion: state.started.execution.version,
expectedRecoveryVersion: 0,
mutationId: 'manual-resolution-1',
decision: 'abandon_unknown',
reasonCode: 'operator_abandoned_unknown',
principal: principal(),
...overrides,
};
}
test('0024 stores a strong-auth and Policy-fenced fact with manual resolution', async (t) => {
const state = await setup(t);
const service = new ApprovedActionManualRecoveryService(
state.recovery,
state.policy,
() => RESOLUTION_TIME,
);
const result = await service.resolve(input(state));
assert.equal(result.status, 'resolved');
assert.equal(result.snapshot.action.execution.status, 'blocked');
assert.equal(result.snapshot.resolution.resolvedBy.id, OWNER.id);
assert.deepEqual(await service.resolve(input(state)), result);
const [fact] = await state.database.query(
`SELECT * FROM "${APPROVED_ACTION_RECOVERY_AUTHORIZATION_TABLE}"`,
{ type: QueryTypes.SELECT },
);
assert.equal(fact.dispatch_id, state.dispatch.id);
assert.equal(fact.mutation_id, 'manual-resolution-1');
assert.equal(fact.resolved_by_id, OWNER.id);
assert.equal(fact.authentication_id, 'mfa-session-1');
assert.equal(fact.assurance, 'multi_factor');
assert.equal(fact.project_version, 1);
assert.equal(fact.binding_version, 1);
assert.match(fact.fact_digest, /^[0-9a-f]{64}$/);
const indexes = new Set(
(
await state.database
.getQueryInterface()
.showIndex(APPROVED_ACTION_RECOVERY_AUTHORIZATION_TABLE)
).map((index) => index.name),
);
for (const name of [
APPROVED_ACTION_RECOVERY_AUTHORIZATION_MUTATION_INDEX,
APPROVED_ACTION_RECOVERY_AUTHORIZATION_PROJECT_INDEX,
APPROVED_ACTION_RECOVERY_AUTHORIZATION_AUTH_INDEX,
]) {
assert.ok(indexes.has(name));
}
});
test('requires a stable User and recent strong authentication before storage reads', async () => {
let reads = 0;
const repository = {
async findById() {
reads += 1;
return null;
},
};
const policy = {
async decideWithFence() {
throw new Error('unreachable');
},
};
const service = new ApprovedActionManualRecoveryService(
repository,
policy,
() => RESOLUTION_TIME,
);
const base = {
dispatchId: 'dispatch-auth-check',
expectedExecutionVersion: 2,
expectedRecoveryVersion: 0,
mutationId: 'manual-auth-check',
decision: 'confirm_failed',
reasonCode: 'human_confirmed_failure',
};
await assert.rejects(
service.resolve({
...base,
principal: principal({
subject: { type: 'agent', id: 'agent-1' },
}),
}),
ApprovedActionRecoveryHumanRequiredError,
);
await assert.rejects(
service.resolve({
...base,
principal: principal({ assurance: 'single_factor' }),
}),
ApprovedActionRecoveryStrongAuthenticationRequiredError,
);
await assert.rejects(
service.resolve({
...base,
principal: principal({ authenticatedAtMs: RESOLUTION_TIME - 300_001 }),
}),
ApprovedActionRecoveryStrongAuthenticationRequiredError,
);
assert.equal(reads, 0);
});
test('operator Policy is denied and an authorization revocation race is fenced', async (t) => {
const denied = await setup(t);
await denied.policyRepository.append({
expectedCurrentVersion: 0,
binding: {
projectId: PROJECT_ID,
subject: { type: 'user', id: 'operator-1' },
version: 1,
state: 'active',
role: 'operator',
mutationId: 'bind-operator',
changedBy: OWNER,
createdAtMs: BASE_TIME,
},
});
const deniedService = new ApprovedActionManualRecoveryService(
denied.recovery,
denied.policy,
() => RESOLUTION_TIME,
);
await assert.rejects(
deniedService.resolve(
input(denied, {
principal: principal({
subject: { type: 'user', id: 'operator-1' },
}),
}),
),
ApprovedActionRecoveryAuthorizationDeniedError,
);
const raced = await setup(t);
const racingPolicy = {
async decideWithFence(request) {
const decision = await raced.policy.decideWithFence(request);
await raced.policyRepository.append({
expectedCurrentVersion: 1,
binding: {
projectId: PROJECT_ID,
subject: OWNER,
version: 2,
state: 'revoked',
mutationId: 'revoke-owner-during-recovery',
changedBy: OWNER,
createdAtMs: RESOLUTION_TIME,
},
});
return decision;
},
};
const racedService = new ApprovedActionManualRecoveryService(
raced.recovery,
racingPolicy,
() => RESOLUTION_TIME,
);
await assert.rejects(
racedService.resolve(input(raced)),
ApprovedActionRecoveryFenceRejectedError,
);
const after = await raced.recovery.findById(raced.dispatch.id);
assert.equal(after.action.execution.status, 'executing');
assert.equal(after.recovery.status, 'armed');
});
test('authorization fact failure rolls the manual terminal transition back', async (t) => {
const state = await setup(t);
await state.database.query(
`CREATE TRIGGER reject_recovery_authorization
BEFORE INSERT ON "${APPROVED_ACTION_RECOVERY_AUTHORIZATION_TABLE}"
BEGIN SELECT RAISE(ABORT, 'authorization rejected'); END`,
);
const service = new ApprovedActionManualRecoveryService(
state.recovery,
state.policy,
() => RESOLUTION_TIME,
);
await assert.rejects(
service.resolve(input(state)),
ApprovedActionRecoveryRepositoryError,
);
const after = await state.recovery.findById(state.dispatch.id);
assert.equal(after.action.execution.status, 'executing');
assert.equal(after.recovery.status, 'armed');
assert.equal(after.resolution, null);
assert.equal(
(
await state.database.query(
`SELECT dispatch_id FROM "${APPROVED_ACTION_RECOVERY_AUTHORIZATION_TABLE}"`,
{ type: QueryTypes.SELECT },
)
).length,
0,
);
});
+686
View File
@@ -0,0 +1,686 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const { QueryTypes, Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const {
projectPolicyMigration,
} = require('../../back/migrations/0017-project-policy');
const {
approvalRequestMigration,
} = require('../../back/migrations/0020-approval-requests');
const {
APPROVED_ACTION_DISPATCH_EXECUTION_TABLE,
approvedActionDispatchExecutionMigration,
} = require('../../back/migrations/0021-approved-action-dispatch-executions');
const {
APPROVED_ACTION_RECOVERY_CONTROL_TABLE,
APPROVED_ACTION_RECOVERY_DUE_INDEX,
APPROVED_ACTION_RECOVERY_LEASE_INDEX,
APPROVED_ACTION_RECOVERY_PROJECT_INDEX,
APPROVED_ACTION_RECOVERY_RESOLUTION_MUTATION_INDEX,
APPROVED_ACTION_RECOVERY_RESOLUTION_PROJECT_INDEX,
APPROVED_ACTION_RECOVERY_RESOLUTION_TABLE,
approvedActionRecoveryMigration,
} = require('../../back/migrations/0022-approved-action-recovery');
const {
approvedActionRecoveryAuthorizationMigration,
} = require('../../back/migrations/0024-approved-action-recovery-authorization');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacySequelizeApprovedActionDispatchRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/approvedActionDispatchRepository');
const {
LegacySequelizeApprovedActionRecoveryRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/approvedActionRecoveryRepository');
const {
LegacySequelizeApprovalRequestRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/approvalRequestRepository');
const {
LegacySequelizeProjectPolicyRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/projectPolicyRepository');
const {
ApprovalRequestService,
} = require('../../back/runtime/application/approvalRequestService');
const {
ProjectPolicyEngine,
} = require('../../back/runtime/application/projectPolicyEngine');
const {
ApprovedActionDispatchRepositoryError,
} = require('../../back/runtime/domain/approvedActionDispatchExecution');
const {
ApprovedActionRecoveryFenceRejectedError,
} = require('../../back/runtime/domain/approvedActionRecovery');
const {
createApprovedActionRecoveryAuthorizationFact,
} = require('../../back/runtime/domain/approvedActionRecoveryAuthorization');
const PROJECT_ID = 'default';
const AGENT = Object.freeze({ type: 'agent', id: 'agent-1' });
const OWNER = Object.freeze({ type: 'user', id: 'owner-1' });
const SYSTEM = Object.freeze({ type: 'system', id: 'approval-dispatcher' });
const BASE_TIME = 100_000;
function action(name, digestCharacter = 'a') {
return {
permission: 'tool.call:filesystem.write',
actionType: 'tool_call',
actionRef: `planned-${name}`,
actionDigest: digestCharacter.repeat(64),
previewDigest: 'f'.repeat(64),
};
}
async function migrate(database, migrations) {
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations,
logger: { info() {} },
});
}
async function bind(policyRepository, subject, role, mutationId) {
await policyRepository.append({
expectedCurrentVersion: 0,
binding: {
projectId: PROJECT_ID,
subject,
version: 1,
state: 'active',
role,
mutationId,
changedBy: OWNER,
createdAtMs: BASE_TIME - 100,
},
});
}
async function setup(t, storage = ':memory:', includeRecovery = true) {
const database = new Sequelize({
dialect: 'sqlite',
storage,
logging: false,
});
t.after(() => database.close());
await migrate(database, [
projectPolicyMigration,
approvalRequestMigration,
approvedActionDispatchExecutionMigration,
...(includeRecovery
? [
approvedActionRecoveryMigration,
approvedActionRecoveryAuthorizationMigration,
]
: []),
]);
const policyRepository = new LegacySequelizeProjectPolicyRepository(database);
await bind(policyRepository, OWNER, 'owner', 'bind-owner');
await bind(policyRepository, AGENT, 'operator', 'bind-agent');
const approvalRepository = new LegacySequelizeApprovalRequestRepository(
database,
);
return {
database,
approvalService: new ApprovalRequestService(
approvalRepository,
new ProjectPolicyEngine(policyRepository),
),
actionRepository: new LegacySequelizeApprovedActionDispatchRepository(
database,
),
recoveryRepository: includeRecovery
? new LegacySequelizeApprovedActionRecoveryRepository(database)
: null,
};
}
async function prepareDispatch(service, name, offset = 0, digest = 'a') {
const requestedAtMs = BASE_TIME + offset;
const binding = action(name, digest);
await service.create({
id: `approval-${name}`,
projectId: PROJECT_ID,
action: binding,
risk: 'high',
requestedBy: AGENT,
requestedAtMs,
expiresAtMs: requestedAtMs + 60_000,
});
await service.decide({
requestId: `approval-${name}`,
expectedVersion: 1,
decisionId: `decision-${name}`,
decision: 'approved',
reasonCode: 'reviewed_action',
decidedBy: OWNER,
decidedAtMs: requestedAtMs + 10,
});
return (
await service.consume({
requestId: `approval-${name}`,
expectedVersion: 2,
consumptionId: `consumption-${name}`,
dispatchId: `dispatch-${name}`,
action: binding,
requestedBy: AGENT,
consumedBy: SYSTEM,
consumedAtMs: requestedAtMs + 20,
})
).dispatch;
}
async function startAction(actionRepository, dispatch, options = {}) {
const nowMs = options.nowMs ?? BASE_TIME + 1_000;
const leaseDurationMs = options.leaseDurationMs ?? 100;
const owner = options.owner ?? 'dispatcher-1';
const leaseToken = options.leaseToken ?? 'execution-lease-1';
const claimed = await actionRepository.claim({
dispatchId: dispatch.id,
owner,
leaseToken,
nowMs,
leaseDurationMs,
});
assert.equal(claimed.status, 'claimed');
const started = await actionRepository.start({
dispatchId: dispatch.id,
approvalRequestId: dispatch.approvalRequestId,
actionDigest: dispatch.action.actionDigest,
owner,
leaseToken,
expectedVersion: claimed.snapshot.execution.version,
startedAtMs: nowMs + 10,
});
return {
claimed,
started,
owner,
leaseToken,
leaseExpiresAtMs: nowMs + leaseDurationMs,
};
}
async function claimRecovery(recoveryRepository, dispatchId, overrides = {}) {
return recoveryRepository.claim({
dispatchId,
owner: 'resolver-1',
leaseToken: 'recovery-lease-1',
nowMs: BASE_TIME + 1_100,
leaseDurationMs: 1_000,
...overrides,
});
}
test('0022 backfills executing controls and owns bounded recovery indexes', async (t) => {
const context = await setup(t, ':memory:', false);
const dispatch = await prepareDispatch(context.approvalService, 'backfill');
await context.database.query(
`UPDATE "${APPROVED_ACTION_DISPATCH_EXECUTION_TABLE}"
SET status = 'executing', version = 2, attempt_count = 1,
eligible_at_ms = NULL, lease_owner = 'dispatcher-1',
lease_token = 'execution-lease-1',
lease_expires_at_ms = :leaseExpiresAtMs,
started_at_ms = :startedAtMs, updated_at_ms = :startedAtMs
WHERE dispatch_id = :dispatchId`,
{
replacements: {
dispatchId: dispatch.id,
startedAtMs: BASE_TIME + 1_010,
leaseExpiresAtMs: BASE_TIME + 1_100,
},
},
);
await migrate(context.database, [approvedActionRecoveryMigration]);
const rows = await context.database.query(
`SELECT * FROM "${APPROVED_ACTION_RECOVERY_CONTROL_TABLE}"`,
{ type: QueryTypes.SELECT },
);
assert.equal(rows.length, 1);
assert.equal(rows[0].dispatch_id, dispatch.id);
assert.equal(rows[0].execution_version, 2);
assert.equal(rows[0].status, 'armed');
assert.equal(rows[0].next_scan_at_ms, BASE_TIME + 1_100);
const controlIndexes = new Set(
(
await context.database
.getQueryInterface()
.showIndex(APPROVED_ACTION_RECOVERY_CONTROL_TABLE)
).map((index) => index.name),
);
for (const name of [
APPROVED_ACTION_RECOVERY_DUE_INDEX,
APPROVED_ACTION_RECOVERY_PROJECT_INDEX,
APPROVED_ACTION_RECOVERY_LEASE_INDEX,
]) {
assert.ok(controlIndexes.has(name));
}
const resolutionIndexes = new Set(
(
await context.database
.getQueryInterface()
.showIndex(APPROVED_ACTION_RECOVERY_RESOLUTION_TABLE)
).map((index) => index.name),
);
assert.ok(
resolutionIndexes.has(APPROVED_ACTION_RECOVERY_RESOLUTION_MUTATION_INDEX),
);
assert.ok(
resolutionIndexes.has(APPROVED_ACTION_RECOVERY_RESOLUTION_PROJECT_INDEX),
);
});
test('start barrier atomically arms recovery and rolls back when control insert fails', async (t) => {
const context = await setup(t);
const dispatch = await prepareDispatch(
context.approvalService,
'start-atomic',
);
const claimed = await context.actionRepository.claim({
dispatchId: dispatch.id,
owner: 'dispatcher-1',
leaseToken: 'execution-lease-1',
nowMs: BASE_TIME + 1_000,
leaseDurationMs: 100,
});
await context.database.query(
`CREATE TRIGGER fail_recovery_control_insert
BEFORE INSERT ON "${APPROVED_ACTION_RECOVERY_CONTROL_TABLE}"
BEGIN SELECT RAISE(ABORT, 'forced recovery insert failure'); END`,
);
await assert.rejects(
context.actionRepository.start({
dispatchId: dispatch.id,
approvalRequestId: dispatch.approvalRequestId,
actionDigest: dispatch.action.actionDigest,
owner: 'dispatcher-1',
leaseToken: 'execution-lease-1',
expectedVersion: claimed.snapshot.execution.version,
startedAtMs: BASE_TIME + 1_010,
}),
ApprovedActionDispatchRepositoryError,
);
assert.equal(
(await context.actionRepository.findById(dispatch.id)).execution.status,
'leased',
);
assert.equal(await context.recoveryRepository.findById(dispatch.id), null);
});
test('normal completion closes the recovery control without creating a recovery resolution', async (t) => {
const context = await setup(t);
const dispatch = await prepareDispatch(context.approvalService, 'complete');
const execution = await startAction(context.actionRepository, dispatch);
const armed = await context.recoveryRepository.findById(dispatch.id);
assert.equal(armed.recovery.status, 'armed');
assert.equal(
armed.recovery.executionVersion,
execution.started.execution.version,
);
assert.equal(armed.recovery.nextScanAtMs, execution.leaseExpiresAtMs);
const completed = await context.actionRepository.complete({
dispatchId: dispatch.id,
owner: execution.owner,
leaseToken: execution.leaseToken,
expectedVersion: execution.started.execution.version,
resultMutationId: 'normal-completion-1',
outcome: 'succeeded',
resultCode: 'ok',
completedAtMs: BASE_TIME + 1_120,
});
assert.equal(completed.execution.status, 'succeeded');
const closed = await context.recoveryRepository.findById(dispatch.id);
assert.equal(closed.recovery.status, 'resolved');
assert.equal(closed.recovery.resolutionMutationId, 'normal-completion-1');
assert.equal(closed.resolution, null);
assert.equal(
(await claimRecovery(context.recoveryRepository, dispatch.id)).status,
'resolved',
);
});
test('a late execution renew re-arms recovery and fences the stale resolver lease', async (t) => {
const context = await setup(t);
const dispatch = await prepareDispatch(context.approvalService, 'renew');
const execution = await startAction(context.actionRepository, dispatch);
const claimed = await claimRecovery(context.recoveryRepository, dispatch.id);
assert.equal(claimed.status, 'claimed');
const renewed = await context.actionRepository.renew({
dispatchId: dispatch.id,
owner: execution.owner,
leaseToken: execution.leaseToken,
expectedVersion: execution.started.execution.version,
nowMs: BASE_TIME + 1_150,
leaseDurationMs: 1_000,
});
assert.equal(
renewed.execution.version,
execution.started.execution.version + 1,
);
const rearmed = await context.recoveryRepository.findById(dispatch.id);
assert.equal(rearmed.recovery.status, 'armed');
assert.equal(rearmed.recovery.executionVersion, renewed.execution.version);
assert.equal(rearmed.recovery.nextScanAtMs, BASE_TIME + 2_150);
assert.equal(rearmed.recovery.leaseOwner, null);
await assert.rejects(
context.recoveryRepository.recordFinding({
dispatchId: dispatch.id,
expectedExecutionVersion: claimed.snapshot.action.execution.version,
expectedRecoveryVersion: claimed.snapshot.recovery.version,
owner: 'resolver-1',
leaseToken: 'recovery-lease-1',
findingMutationId: 'stale-finding-1',
finding: 'missing',
resultCode: 'receipt_missing',
observedAtMs: BASE_TIME + 1_160,
retryAtMs: BASE_TIME + 1_200,
}),
ApprovedActionRecoveryFenceRejectedError,
);
});
test('lists a bounded stable recovery page and excludes live executions', async (t) => {
const context = await setup(t);
const firstDispatch = await prepareDispatch(
context.approvalService,
'due-a',
0,
'a',
);
const secondDispatch = await prepareDispatch(
context.approvalService,
'due-b',
10,
'b',
);
const liveDispatch = await prepareDispatch(
context.approvalService,
'live',
20,
'c',
);
await startAction(context.actionRepository, firstDispatch);
await startAction(context.actionRepository, secondDispatch);
await startAction(context.actionRepository, liveDispatch, {
leaseDurationMs: 1_000,
});
const first = await context.recoveryRepository.listDue({
nowMs: BASE_TIME + 1_100,
limit: 1,
});
assert.deepEqual(
first.recoveries.map((entry) => entry.action.dispatch.id),
['dispatch-due-a'],
);
assert.equal(first.truncated, true);
const second = await context.recoveryRepository.listDue({
nowMs: BASE_TIME + 1_100,
limit: 1,
cursor: first.nextCursor,
});
assert.deepEqual(
second.recoveries.map((entry) => entry.action.dispatch.id),
['dispatch-due-b'],
);
assert.equal(second.truncated, false);
});
test('two SQLite resolvers claim once and only an expired recovery lease is taken over', async (t) => {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-recovery-'));
t.after(() => fs.rm(directory, { recursive: true, force: true }));
const storage = path.join(directory, 'database.sqlite');
const first = await setup(t, storage);
const dispatch = await prepareDispatch(first.approvalService, 'claim-race');
await startAction(first.actionRepository, dispatch);
const secondDatabase = new Sequelize({
dialect: 'sqlite',
storage,
logging: false,
});
t.after(() => secondDatabase.close());
const secondRepository = new LegacySequelizeApprovedActionRecoveryRepository(
secondDatabase,
);
const results = await Promise.all([
claimRecovery(first.recoveryRepository, dispatch.id, {
leaseDurationMs: 100,
}),
claimRecovery(secondRepository, dispatch.id, {
owner: 'resolver-2',
leaseToken: 'recovery-lease-2',
leaseDurationMs: 100,
}),
]);
assert.equal(
results.filter((result) => result.status === 'claimed').length,
1,
);
assert.equal(
results.filter((result) => result.status === 'leased').length,
1,
);
const winner = results.find((result) => result.status === 'claimed');
assert.equal(
(
await claimRecovery(first.recoveryRepository, dispatch.id, {
owner: winner.snapshot.recovery.leaseOwner,
leaseToken: winner.snapshot.recovery.leaseToken,
leaseDurationMs: 100,
})
).status,
'claimed',
);
const takeover = await claimRecovery(secondRepository, dispatch.id, {
owner: 'resolver-3',
leaseToken: 'recovery-lease-3',
nowMs: BASE_TIME + 1_200,
leaseDurationMs: 100,
});
assert.equal(takeover.status, 'claimed');
assert.equal(takeover.snapshot.recovery.leaseOwner, 'resolver-3');
});
test('recovery finding replay is exact and unsupported evidence becomes manual-only', async (t) => {
const context = await setup(t);
const dispatch = await prepareDispatch(context.approvalService, 'finding');
await startAction(context.actionRepository, dispatch);
const claimed = await claimRecovery(context.recoveryRepository, dispatch.id);
assert.equal(claimed.status, 'claimed');
const finding = {
dispatchId: dispatch.id,
expectedExecutionVersion: claimed.snapshot.action.execution.version,
expectedRecoveryVersion: claimed.snapshot.recovery.version,
owner: 'resolver-1',
leaseToken: 'recovery-lease-1',
findingMutationId: 'finding-missing-1',
finding: 'missing',
resultCode: 'receipt_missing',
observedAtMs: BASE_TIME + 1_110,
retryAtMs: BASE_TIME + 1_200,
};
const deferred = await context.recoveryRepository.recordFinding(finding);
assert.equal(deferred.recovery.status, 'armed');
assert.equal(deferred.recovery.findingCount, 1);
assert.deepEqual(
await context.recoveryRepository.recordFinding(finding),
deferred,
);
assert.equal(
(
await claimRecovery(context.recoveryRepository, dispatch.id, {
nowMs: BASE_TIME + 1_199,
})
).status,
'not_due',
);
const second = await claimRecovery(context.recoveryRepository, dispatch.id, {
owner: 'resolver-2',
leaseToken: 'recovery-lease-2',
nowMs: BASE_TIME + 1_200,
});
assert.equal(second.status, 'claimed');
const manual = await context.recoveryRepository.recordFinding({
dispatchId: dispatch.id,
expectedExecutionVersion: second.snapshot.action.execution.version,
expectedRecoveryVersion: second.snapshot.recovery.version,
owner: 'resolver-2',
leaseToken: 'recovery-lease-2',
findingMutationId: 'finding-unsupported-1',
finding: 'unsupported',
resultCode: 'automatic_recovery_unsupported',
observedAtMs: BASE_TIME + 1_210,
});
assert.equal(manual.recovery.status, 'manual_required');
assert.equal(manual.recovery.nextScanAtMs, null);
assert.equal(
(
await context.recoveryRepository.listDue({
nowMs: BASE_TIME + 10_000,
limit: 64,
})
).recoveries.length,
0,
);
});
test('verified evidence resolves atomically and exact mutation replay returns the same resolution', async (t) => {
const context = await setup(t);
const dispatch = await prepareDispatch(context.approvalService, 'verified');
await startAction(context.actionRepository, dispatch);
const claimed = await claimRecovery(context.recoveryRepository, dispatch.id);
const command = {
dispatchId: dispatch.id,
expectedExecutionVersion: claimed.snapshot.action.execution.version,
expectedRecoveryVersion: claimed.snapshot.recovery.version,
owner: 'resolver-1',
leaseToken: 'recovery-lease-1',
mutationId: 'resolution-verified-1',
source: 'automatic_evidence',
decision: 'confirm_succeeded',
evidenceDigest: 'e'.repeat(64),
reasonCode: 'provider_receipt_verified',
resolvedAtMs: BASE_TIME + 1_120,
};
const resolved = await context.recoveryRepository.resolve(command);
assert.equal(resolved.status, 'resolved');
assert.equal(resolved.snapshot.action.execution.status, 'succeeded');
assert.equal(resolved.snapshot.recovery.status, 'resolved');
assert.equal(resolved.snapshot.resolution.source, 'automatic_evidence');
assert.equal(resolved.snapshot.resolution.evidenceDigest, 'e'.repeat(64));
assert.deepEqual(await context.recoveryRepository.resolve(command), resolved);
});
test('manual resolution cannot preempt a live execution and never resets it for retry', async (t) => {
const context = await setup(t);
const dispatch = await prepareDispatch(context.approvalService, 'manual');
const execution = await startAction(context.actionRepository, dispatch, {
leaseDurationMs: 500,
});
const armed = await context.recoveryRepository.findById(dispatch.id);
const commandAt = (resolvedAtMs) => {
const command = {
dispatchId: dispatch.id,
expectedExecutionVersion: armed.action.execution.version,
expectedRecoveryVersion: armed.recovery.version,
mutationId: 'resolution-manual-1',
source: 'human',
decision: 'abandon_unknown',
reasonCode: 'operator_abandoned_unknown',
resolvedBy: OWNER,
resolvedAtMs,
};
return {
...command,
authorizationFact: createApprovedActionRecoveryAuthorizationFact({
dispatchId: dispatch.id,
projectId: PROJECT_ID,
mutationId: command.mutationId,
resolvedBy: OWNER,
authenticationId: 'mfa-session-1',
assurance: 'multi_factor',
authenticatedAtMs: resolvedAtMs - 100,
projectVersion: 1,
bindingVersion: 1,
authorizedAtMs: resolvedAtMs,
}),
};
};
await assert.rejects(
context.recoveryRepository.resolve(
commandAt(execution.leaseExpiresAtMs - 1),
),
ApprovedActionRecoveryFenceRejectedError,
);
const resolved = await context.recoveryRepository.resolve(
commandAt(execution.leaseExpiresAtMs),
);
assert.equal(resolved.snapshot.action.execution.status, 'blocked');
assert.equal(resolved.snapshot.resolution.decision, 'abandon_unknown');
assert.equal(resolved.snapshot.resolution.resolvedBy.type, 'user');
assert.equal(resolved.snapshot.action.execution.eligibleAtMs, null);
});
test('late completion and recovery resolution have one durable terminal winner', async (t) => {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-recovery-'));
t.after(() => fs.rm(directory, { recursive: true, force: true }));
const storage = path.join(directory, 'database.sqlite');
const first = await setup(t, storage);
const dispatch = await prepareDispatch(first.approvalService, 'race');
const execution = await startAction(first.actionRepository, dispatch);
const secondDatabase = new Sequelize({
dialect: 'sqlite',
storage,
logging: false,
});
t.after(() => secondDatabase.close());
const secondRecovery = new LegacySequelizeApprovedActionRecoveryRepository(
secondDatabase,
);
const claimed = await claimRecovery(secondRecovery, dispatch.id);
const recoveryCommand = {
dispatchId: dispatch.id,
expectedExecutionVersion: claimed.snapshot.action.execution.version,
expectedRecoveryVersion: claimed.snapshot.recovery.version,
owner: 'resolver-1',
leaseToken: 'recovery-lease-1',
mutationId: 'resolution-race-1',
source: 'automatic_evidence',
decision: 'confirm_failed',
evidenceDigest: 'd'.repeat(64),
reasonCode: 'provider_failed',
resolvedAtMs: BASE_TIME + 1_120,
};
await Promise.allSettled([
first.actionRepository.complete({
dispatchId: dispatch.id,
owner: execution.owner,
leaseToken: execution.leaseToken,
expectedVersion: execution.started.execution.version,
resultMutationId: 'normal-race-1',
outcome: 'succeeded',
resultCode: 'ok',
completedAtMs: BASE_TIME + 1_120,
}),
secondRecovery.resolve(recoveryCommand),
]);
const final = await secondRecovery.findById(dispatch.id);
assert.ok(['succeeded', 'failed'].includes(final.action.execution.status));
assert.equal(final.recovery.status, 'resolved');
assert.equal(
final.recovery.resolutionMutationId,
final.action.execution.resultMutationId,
);
if (final.resolution) {
assert.equal(final.resolution.mutationId, 'resolution-race-1');
assert.equal(final.action.execution.status, 'failed');
} else {
assert.equal(final.action.execution.resultMutationId, 'normal-race-1');
assert.equal(final.action.execution.status, 'succeeded');
}
});
@@ -0,0 +1,312 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
ApprovedActionRecoveryReconciler,
} = require('../../back/runtime/application/approvedActionRecoveryReconciler');
function recoverySnapshot() {
return {
action: {
dispatch: {
id: 'dispatch-one',
approvalRequestId: 'approval-one',
approvalRequestVersion: 3,
projectId: 'default',
state: 'pending',
action: {
permission: 'tool.call:filesystem.write',
actionType: 'tool_call',
actionRef: 'planned-one',
actionDigest: 'a'.repeat(64),
previewDigest: 'f'.repeat(64),
},
requestedBy: { type: 'agent', id: 'agent-1' },
consumedBy: { type: 'system', id: 'approval-dispatcher' },
createdAtMs: 100,
},
execution: {
dispatchId: 'dispatch-one',
projectId: 'default',
status: 'executing',
version: 2,
attemptCount: 1,
maxAttempts: 5,
eligibleAtMs: null,
nextAttemptAtMs: null,
leaseOwner: 'dispatcher-1',
leaseToken: 'execution-lease-1',
leaseExpiresAtMs: 200,
startedAtMs: 110,
resultMutationId: null,
lastResultCode: null,
completedAtMs: null,
createdAtMs: 100,
updatedAtMs: 110,
},
},
recovery: {
dispatchId: 'dispatch-one',
projectId: 'default',
executionVersion: 2,
status: 'armed',
version: 0,
nextScanAtMs: 200,
leaseOwner: null,
leaseToken: null,
leaseExpiresAtMs: null,
findingCount: 0,
lastFindingMutationId: null,
lastFinding: null,
lastResultCode: null,
lastEvidenceDigest: null,
resolutionMutationId: null,
createdAtMs: 110,
updatedAtMs: 110,
},
resolution: null,
};
}
function fakeRepository() {
const calls = [];
const initial = recoverySnapshot();
return {
calls,
async listDue(query) {
calls.push(['list', query]);
return { recoveries: [initial], truncated: false };
},
async claim(command) {
calls.push(['claim', command]);
return {
status: 'claimed',
snapshot: {
...initial,
recovery: {
...initial.recovery,
status: 'leased',
version: 1,
nextScanAtMs: command.nowMs + command.leaseDurationMs,
leaseOwner: command.owner,
leaseToken: command.leaseToken,
leaseExpiresAtMs: command.nowMs + command.leaseDurationMs,
updatedAtMs: command.nowMs,
},
},
};
},
async recordFinding(command) {
calls.push(['finding', command]);
return {
...initial,
recovery: {
...initial.recovery,
status: command.retryAtMs ? 'armed' : 'manual_required',
version: command.expectedRecoveryVersion + 1,
nextScanAtMs: command.retryAtMs ?? null,
findingCount: 1,
lastFindingMutationId: command.findingMutationId,
lastFinding: command.finding,
lastResultCode: command.resultCode,
lastEvidenceDigest: command.evidenceDigest ?? null,
updatedAtMs: command.observedAtMs,
},
};
},
async resolve(command) {
calls.push(['resolve', command]);
const status =
command.decision === 'confirm_succeeded' ? 'succeeded' : 'failed';
return {
status: 'resolved',
snapshot: {
...initial,
action: {
...initial.action,
execution: {
...initial.action.execution,
status,
version: command.expectedExecutionVersion + 1,
leaseOwner: null,
leaseToken: null,
leaseExpiresAtMs: null,
resultMutationId: command.mutationId,
lastResultCode: command.reasonCode,
completedAtMs: command.resolvedAtMs,
updatedAtMs: command.resolvedAtMs,
},
},
recovery: {
...initial.recovery,
status: 'resolved',
version: command.expectedRecoveryVersion + 1,
executionVersion: command.expectedExecutionVersion + 1,
nextScanAtMs: null,
resolutionMutationId: command.mutationId,
updatedAtMs: command.resolvedAtMs,
},
},
};
},
};
}
function options() {
let now = 200;
let id = 0;
return {
owner: 'resolver-1',
leaseDurationMs: 1_000,
retryBaseMs: 10,
retryMaxMs: 100,
clock: () => ++now,
createId: () => `recovery-mutation-${++id}`,
};
}
function provider(overrides = {}) {
return {
actionType: 'tool_call',
capability: 'automatic',
async inspect() {
return {
finding: 'verified_succeeded',
resultCode: 'provider_receipt_verified',
evidenceDigest: 'e'.repeat(64),
};
},
...overrides,
};
}
test('verified evidence resolves without exposing an execute capability', async () => {
const repository = fakeRepository();
let inspected = 0;
const reconciler = new ApprovedActionRecoveryReconciler(
repository,
[
provider({
async inspect(context) {
inspected += 1;
assert.equal(context.idempotencyKey, 'dispatch-one');
assert.equal(context.snapshot.action.execution.status, 'executing');
assert.equal('execute' in context, false);
return {
finding: 'verified_succeeded',
resultCode: 'provider_receipt_verified',
evidenceDigest: 'e'.repeat(64),
};
},
}),
],
options(),
);
const summary = await reconciler.reconcileBatch({ limit: 1 });
assert.equal(inspected, 1);
assert.deepEqual(
repository.calls.map((entry) => entry[0]),
['list', 'claim', 'resolve'],
);
assert.equal(summary.verifiedSucceeded, 1);
assert.equal(summary.manualRequired, 0);
});
test('manual-only providers are never inspected and move to manual-required', async () => {
const repository = fakeRepository();
let inspected = false;
const reconciler = new ApprovedActionRecoveryReconciler(
repository,
[
provider({
capability: 'manual_only',
async inspect() {
inspected = true;
throw new Error('must not be called');
},
}),
],
options(),
);
const summary = await reconciler.reconcileBatch();
assert.equal(inspected, false);
const finding = repository.calls.find((entry) => entry[0] === 'finding')[1];
assert.equal(finding.finding, 'unsupported');
assert.equal('retryAtMs' in finding, false);
assert.equal(summary.manualRequired, 1);
});
test('missing and unavailable evidence defer with bounded retry instead of resolving', async () => {
for (const inspect of [
async () => ({ finding: 'missing', resultCode: 'receipt_missing' }),
async () => {
throw new Error('provider unavailable');
},
]) {
const repository = fakeRepository();
const reconciler = new ApprovedActionRecoveryReconciler(
repository,
[provider({ inspect })],
options(),
);
const summary = await reconciler.reconcileBatch();
const finding = repository.calls.find((entry) => entry[0] === 'finding')[1];
assert.ok(['missing', 'unavailable'].includes(finding.finding));
assert.ok(finding.retryAtMs > finding.observedAtMs);
assert.equal(
repository.calls.some((entry) => entry[0] === 'resolve'),
false,
);
assert.equal(summary.deferred, 1);
}
});
test('extensible or malformed evidence fails closed into manual review', async () => {
const repository = fakeRepository();
const reconciler = new ApprovedActionRecoveryReconciler(
repository,
[
provider({
async inspect() {
return {
finding: 'verified_succeeded',
resultCode: 'ok',
evidenceDigest: 'e'.repeat(64),
injected: true,
};
},
}),
],
options(),
);
const summary = await reconciler.reconcileBatch();
const finding = repository.calls.find((entry) => entry[0] === 'finding')[1];
assert.equal(finding.finding, 'conflict');
assert.equal(finding.resultCode, 'recovery_evidence_invalid');
assert.equal('retryAtMs' in finding, false);
assert.equal(summary.manualRequired, 1);
});
test('rejects duplicate providers and keeps each batch to one bounded page', async () => {
assert.throws(
() =>
new ApprovedActionRecoveryReconciler(
fakeRepository(),
[provider(), provider()],
options(),
),
/Duplicate approved action recovery provider/,
);
const repository = fakeRepository();
const reconciler = new ApprovedActionRecoveryReconciler(
repository,
[provider()],
options(),
);
await reconciler.reconcileBatch({ limit: 1 });
assert.equal(
repository.calls.filter((entry) => entry[0] === 'list').length,
1,
);
});
@@ -0,0 +1,353 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
ApprovedActionRuntimeLifecycle,
} = require('../../back/runtime/application/approvedActionRuntimeLifecycle');
const {
ApprovedActionRuntimeSupervisor,
} = require('../../back/runtime/application/approvedActionRuntimeSupervisor');
const {
localPrimaryResourcePolicy,
} = require('../../back/runtime/domain/deploymentProfile');
function dispatchPage(overrides = {}) {
return {
scanned: 1,
claimed: 1,
started: 1,
succeeded: 1,
failed: 0,
blocked: 0,
retrying: 0,
deferred: 0,
recoveryRequired: 0,
alreadyTerminal: 0,
unavailable: 0,
truncated: false,
...overrides,
};
}
function recoveryPage(overrides = {}) {
return {
scanned: 1,
claimed: 1,
verifiedSucceeded: 1,
verifiedFailed: 0,
deferred: 0,
manualRequired: 0,
executionActive: 0,
alreadyResolved: 0,
unavailable: 0,
truncated: false,
...overrides,
};
}
function runtimeSummary(overrides = {}) {
return {
recovery: {
pages: 1,
...recoveryPage(),
stopReason: 'complete',
remaining: false,
...overrides.recovery,
},
dispatch: {
pages: 1,
...dispatchPage(),
stopReason: 'complete',
remaining: false,
...overrides.dispatch,
},
};
}
function deferred() {
let resolve;
const promise = new Promise((resolvePromise) => {
resolve = resolvePromise;
});
return { promise, resolve };
}
function fakeScheduler() {
let id = 0;
const pending = new Map();
const cleared = [];
return {
pending,
cleared,
scheduler: {
setTimeout(callback, delayMs) {
const timer = {
id: ++id,
delayMs,
unrefCalls: 0,
unref() {
this.unrefCalls += 1;
},
};
pending.set(timer.id, { timer, callback });
return timer;
},
clearTimeout(timer) {
cleared.push(timer.id);
pending.delete(timer.id);
},
},
fireNext() {
const next = pending.values().next().value;
assert.ok(next, 'expected a scheduled timer');
pending.delete(next.timer.id);
next.callback();
return next.timer;
},
};
}
async function flush() {
await new Promise((resolve) => setImmediate(resolve));
}
test('supervisor runs recovery first and aggregates bounded dispatch pages', async () => {
const calls = [];
let dispatchPageNumber = 0;
const supervisor = new ApprovedActionRuntimeSupervisor(
{
async dispatchBatch(options) {
calls.push(['dispatch', options]);
dispatchPageNumber += 1;
if (dispatchPageNumber === 1) {
return dispatchPage({
truncated: true,
nextCursor: { eligibleAtMs: 11, dispatchId: 'dispatch-1' },
});
}
return dispatchPage({ claimed: 0, succeeded: 0, unavailable: 1 });
},
},
{
async reconcileBatch(options) {
calls.push(['recovery', options]);
return recoveryPage();
},
},
);
const summary = await supervisor.runCycle({
recovery: { pageSize: 4, maxPages: 1 },
dispatch: { pageSize: 8, maxPages: 2 },
});
assert.deepEqual(calls, [
['recovery', { limit: 4 }],
['dispatch', { limit: 8 }],
[
'dispatch',
{
cursor: { eligibleAtMs: 11, dispatchId: 'dispatch-1' },
limit: 8,
},
],
]);
assert.equal(summary.recovery.verifiedSucceeded, 1);
assert.equal(summary.dispatch.pages, 2);
assert.equal(summary.dispatch.scanned, 2);
assert.equal(summary.dispatch.claimed, 1);
assert.equal(summary.dispatch.succeeded, 1);
assert.equal(summary.dispatch.unavailable, 1);
assert.equal(summary.dispatch.remaining, false);
});
test('supervisor exposes page-limit cursors and refuses stalled scans', async () => {
const dispatchCursor = { eligibleAtMs: 20, dispatchId: 'dispatch-2' };
const recoveryCursor = { nextScanAtMs: 30, dispatchId: 'dispatch-3' };
const limited = new ApprovedActionRuntimeSupervisor(
{
async dispatchBatch() {
return dispatchPage({ truncated: true, nextCursor: dispatchCursor });
},
},
{
async reconcileBatch() {
return recoveryPage({ truncated: true, nextCursor: recoveryCursor });
},
},
);
const limitedSummary = await limited.runCycle({
dispatch: { pageSize: 1, maxPages: 1 },
recovery: { pageSize: 1, maxPages: 1 },
});
assert.equal(limitedSummary.dispatch.stopReason, 'page_limit');
assert.deepEqual(limitedSummary.dispatch.nextCursor, dispatchCursor);
assert.equal(limitedSummary.recovery.stopReason, 'page_limit');
assert.deepEqual(limitedSummary.recovery.nextCursor, recoveryCursor);
const stalled = new ApprovedActionRuntimeSupervisor(
{
async dispatchBatch(options) {
return dispatchPage({ truncated: true, nextCursor: options.cursor });
},
},
{
async reconcileBatch() {
return recoveryPage();
},
},
);
const stalledSummary = await stalled.runCycle({
dispatch: { cursor: dispatchCursor, pageSize: 1, maxPages: 2 },
});
assert.equal(stalledSummary.dispatch.stopReason, 'cursor_stalled');
assert.equal(stalledSummary.dispatch.remaining, true);
});
test('recovery storage failure prevents new dispatch work in the same cycle', async () => {
let dispatchCalls = 0;
const supervisor = new ApprovedActionRuntimeSupervisor(
{
async dispatchBatch() {
dispatchCalls += 1;
return dispatchPage();
},
},
{
async reconcileBatch() {
throw new Error('recovery index unavailable');
},
},
);
await assert.rejects(supervisor.runCycle(), /recovery index unavailable/);
assert.equal(dispatchCalls, 0);
});
test('lifecycle uses one unref timer, never overlaps, and resumes both cursors', async () => {
const timers = fakeScheduler();
const first = deferred();
const calls = [];
const policy = localPrimaryResourcePolicy('edge').approvedAction;
const lifecycle = new ApprovedActionRuntimeLifecycle(
{
async runCycle(options) {
calls.push(options);
if (calls.length === 1) return first.promise;
return runtimeSummary();
},
},
{
intervalMs: policy.intervalMs,
initialDelayMs: policy.initialDelayMs,
stopTimeoutMs: policy.stopTimeoutMs,
cycle: {
dispatch: policy.dispatch,
recovery: policy.recovery,
},
scheduler: timers.scheduler,
},
);
assert.equal(timers.pending.size, 0);
assert.equal(lifecycle.start(), true);
assert.equal(lifecycle.start(), false);
const initial = timers.fireNext();
assert.equal(initial.delayMs, 0);
assert.equal(initial.unrefCalls, 1);
await flush();
assert.equal(calls.length, 1);
assert.equal(timers.pending.size, 0);
first.resolve(
runtimeSummary({
recovery: {
remaining: true,
stopReason: 'page_limit',
nextCursor: { nextScanAtMs: 40, dispatchId: 'recovery-resume' },
},
dispatch: {
remaining: true,
stopReason: 'page_limit',
nextCursor: { eligibleAtMs: 50, dispatchId: 'dispatch-resume' },
},
}),
);
await flush();
const next = timers.fireNext();
assert.equal(next.delayMs, 30_000);
assert.equal(next.unrefCalls, 1);
await flush();
assert.deepEqual(calls[1], {
recovery: {
pageSize: 8,
maxPages: 1,
cursor: { nextScanAtMs: 40, dispatchId: 'recovery-resume' },
},
dispatch: {
pageSize: 8,
maxPages: 1,
cursor: { eligibleAtMs: 50, dispatchId: 'dispatch-resume' },
},
});
assert.equal(await lifecycle.stop(), 'drained');
});
test('lifecycle isolates diagnostics and bounds an in-flight shutdown', async () => {
const timers = fakeScheduler();
const running = deferred();
const errors = [];
const lifecycle = new ApprovedActionRuntimeLifecycle(
{
async runCycle() {
return running.promise;
},
},
{
intervalMs: 500,
stopTimeoutMs: 5,
scheduler: timers.scheduler,
onError(error) {
errors.push(error.message);
},
},
);
lifecycle.start();
timers.fireNext();
await flush();
assert.equal(await lifecycle.stop(), 'timed_out');
assert.equal(timers.pending.size, 0);
assert.equal(lifecycle.start(), false);
running.resolve(runtimeSummary());
await flush();
assert.equal(lifecycle.start(), true);
assert.equal(await lifecycle.stop(), 'drained');
assert.deepEqual(errors, []);
});
test('supervisor and lifecycle reject hot loops and unbounded pages', async () => {
const supervisor = new ApprovedActionRuntimeSupervisor(
{
async dispatchBatch() {
return dispatchPage();
},
},
{
async reconcileBatch() {
return recoveryPage();
},
},
);
await assert.rejects(
supervisor.runCycle({ dispatch: { maxPages: 65 } }),
RangeError,
);
assert.throws(
() => new ApprovedActionRuntimeLifecycle(supervisor, { intervalMs: 249 }),
RangeError,
);
assert.throws(
() =>
new ApprovedActionRuntimeLifecycle(supervisor, {
intervalMs: 500,
stopTimeoutMs: 60_001,
}),
RangeError,
);
});
+618
View File
@@ -0,0 +1,618 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const { QueryTypes, Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const { runSchemaMigration } = require('../../back/migrations/0002-run-schema');
const {
runCancellationRequestMigration,
} = require('../../back/migrations/0004-run-cancellation-request');
const {
runAttemptDeadlineMigration,
} = require('../../back/migrations/0006-run-attempt-deadline');
const {
runRetryPolicyMigration,
} = require('../../back/migrations/0011-run-retry-policy');
const {
projectPolicyMigration,
} = require('../../back/migrations/0017-project-policy');
const {
approvalRequestMigration,
} = require('../../back/migrations/0020-approval-requests');
const {
approvedActionDispatchExecutionMigration,
} = require('../../back/migrations/0021-approved-action-dispatch-executions');
const {
approvedActionRecoveryMigration,
} = require('../../back/migrations/0022-approved-action-recovery');
const {
APPROVED_RUN_ACTION_RECEIPT_PROJECT_INDEX,
APPROVED_RUN_ACTION_RECEIPT_RESOURCE_UNIQUE_INDEX,
APPROVED_RUN_ACTION_RECEIPT_TABLE,
approvedRunActionReceiptMigration,
} = require('../../back/migrations/0023-approved-run-action-receipts');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacySequelizeApprovedActionDispatchRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/approvedActionDispatchRepository');
const {
LegacySequelizeApprovedActionRecoveryRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/approvedActionRecoveryRepository');
const {
LegacySequelizeApprovedRunActionRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/approvedRunActionRepository');
const {
LegacySequelizeApprovedRunRecoveryEvidenceProvider,
} = require('../../back/runtime/adapters/legacy-sequelize/approvedRunRecoveryEvidenceProvider');
const {
LegacySequelizeApprovalRequestRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/approvalRequestRepository');
const {
LegacySequelizeProjectPolicyRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/projectPolicyRepository');
const {
LegacySequelizeRunRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/runRepository');
const {
ApprovedActionDispatcher,
} = require('../../back/runtime/application/approvedActionDispatcher');
const {
ApprovedActionRecoveryReconciler,
} = require('../../back/runtime/application/approvedActionRecoveryReconciler');
const {
ApprovedRunActionHandler,
} = require('../../back/runtime/application/approvedRunActionHandler');
const {
ApprovalRequestService,
} = require('../../back/runtime/application/approvalRequestService');
const {
PrimaryRunCreator,
} = require('../../back/runtime/application/primaryRunCreator');
const {
ProjectPolicyEngine,
} = require('../../back/runtime/application/projectPolicyEngine');
const {
ApprovedRunActionBindingConflictError,
ApprovedRunActionRepositoryError,
InvalidApprovedRunActionError,
digestApprovedRunCreationPlan,
normalizeApprovedRunCreationPlan,
} = require('../../back/runtime/domain/approvedRunAction');
const PROJECT_ID = 'default';
const AGENT = Object.freeze({ type: 'agent', id: 'agent-1' });
const OWNER = Object.freeze({ type: 'user', id: 'owner-1' });
const SYSTEM = Object.freeze({ type: 'system', id: 'approval-dispatcher' });
const BASE_TIME = 200_000;
function plan(name = 'one') {
return {
schemaVersion: 1,
actionRef: `approved-run-${name}`,
projectId: PROJECT_ID,
taskId: `task-${name}`,
taskRevision: `revision-${name}`,
executorType: 'local_process',
priority: 3,
taskName: `Task ${name}`,
taskSnapshotRef: `task-snapshot:${name}`,
inputRef: `input:${name}`,
};
}
async function migrate(database) {
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations: [
runSchemaMigration,
runCancellationRequestMigration,
runAttemptDeadlineMigration,
runRetryPolicyMigration,
projectPolicyMigration,
approvalRequestMigration,
approvedActionDispatchExecutionMigration,
approvedActionRecoveryMigration,
approvedRunActionReceiptMigration,
],
logger: { info() {} },
});
}
async function bind(repository, subject, role, mutationId) {
await repository.append({
expectedCurrentVersion: 0,
binding: {
projectId: PROJECT_ID,
subject,
version: 1,
state: 'active',
role,
mutationId,
changedBy: OWNER,
createdAtMs: BASE_TIME - 100,
},
});
}
async function setup(t) {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
t.after(() => database.close());
await migrate(database);
const policy = new LegacySequelizeProjectPolicyRepository(database);
await bind(policy, OWNER, 'owner', 'bind-owner');
await bind(policy, AGENT, 'operator', 'bind-agent');
const approvals = new ApprovalRequestService(
new LegacySequelizeApprovalRequestRepository(database),
new ProjectPolicyEngine(policy),
);
return {
database,
approvals,
executions: new LegacySequelizeApprovedActionDispatchRepository(database),
};
}
async function prepareApprovedRun(
setupResult,
name,
approvedPlan = plan(name),
) {
const actionDigest = digestApprovedRunCreationPlan(approvedPlan);
const action = {
permission: 'run.start',
actionType: 'run.create',
actionRef: approvedPlan.actionRef,
actionDigest,
previewDigest: 'f'.repeat(64),
};
const requestedAtMs = BASE_TIME + 100;
await setupResult.approvals.create({
id: `approval-${name}`,
projectId: PROJECT_ID,
action,
risk: 'high',
requestedBy: AGENT,
requestedAtMs,
expiresAtMs: requestedAtMs + 60_000,
});
await setupResult.approvals.decide({
requestId: `approval-${name}`,
expectedVersion: 1,
decisionId: `decision-${name}`,
decision: 'approved',
reasonCode: 'reviewed_action',
decidedBy: OWNER,
decidedAtMs: requestedAtMs + 10,
});
const consumed = await setupResult.approvals.consume({
requestId: `approval-${name}`,
expectedVersion: 2,
consumptionId: `consumption-${name}`,
dispatchId: `dispatch-${name}`,
action,
requestedBy: AGENT,
consumedBy: SYSTEM,
consumedAtMs: requestedAtMs + 20,
});
return consumed.dispatch;
}
async function startApprovedRun(setupResult, name, approvedPlan = plan(name)) {
const dispatch = await prepareApprovedRun(setupResult, name, approvedPlan);
const claimed = await setupResult.executions.claim({
dispatchId: dispatch.id,
owner: 'dispatcher-1',
leaseToken: `lease-${name}`,
nowMs: BASE_TIME + 130,
leaseDurationMs: 1_000,
});
assert.equal(claimed.status, 'claimed');
return setupResult.executions.start({
dispatchId: dispatch.id,
approvalRequestId: dispatch.approvalRequestId,
actionDigest: dispatch.action.actionDigest,
owner: 'dispatcher-1',
leaseToken: `lease-${name}`,
expectedVersion: claimed.snapshot.execution.version,
startedAtMs: BASE_TIME + 140,
});
}
function recoveryContext(snapshot) {
return {
snapshot: { action: snapshot },
idempotencyKey: snapshot.dispatch.id,
observedAtMs: BASE_TIME + 10_000,
};
}
test('canonical Run action plans are exact-shape and digest-stable', () => {
const first = plan('canonical');
const reordered = {
inputRef: first.inputRef,
priority: first.priority,
executorType: first.executorType,
taskRevision: first.taskRevision,
taskId: first.taskId,
projectId: first.projectId,
actionRef: first.actionRef,
schemaVersion: first.schemaVersion,
taskSnapshotRef: first.taskSnapshotRef,
taskName: first.taskName,
};
assert.equal(
digestApprovedRunCreationPlan(first),
digestApprovedRunCreationPlan(reordered),
);
assert.throws(
() => normalizeApprovedRunCreationPlan({ ...first, hidden: true }),
InvalidApprovedRunActionError,
);
});
test('0023 owns bounded receipt indexes and enforces immutable tuple checks', async (t) => {
const state = await setup(t);
const { database } = state;
const indexes = await database
.getQueryInterface()
.showIndex(APPROVED_RUN_ACTION_RECEIPT_TABLE);
assert.ok(
indexes.some(
(index) => index.name === APPROVED_RUN_ACTION_RECEIPT_PROJECT_INDEX,
),
);
assert.ok(
indexes.some(
(index) =>
index.name === APPROVED_RUN_ACTION_RECEIPT_RESOURCE_UNIQUE_INDEX &&
index.unique,
),
);
const approvedPlan = plan('constraints');
const snapshot = await startApprovedRun(state, 'constraints', approvedPlan);
await new LegacySequelizeApprovedRunActionRepository(database, {
clock: () => snapshot.execution.startedAtMs + 5,
}).create({ snapshot, plan: approvedPlan });
await assert.rejects(
database.query(
`UPDATE "${APPROVED_RUN_ACTION_RECEIPT_TABLE}"
SET outcome = 'failed'
WHERE dispatch_id = :dispatchId`,
{ replacements: { dispatchId: snapshot.dispatch.id } },
),
);
await assert.rejects(
database.query(
`UPDATE "${APPROVED_RUN_ACTION_RECEIPT_TABLE}"
SET idempotency_key = 'different-dispatch'
WHERE dispatch_id = :dispatchId`,
{ replacements: { dispatchId: snapshot.dispatch.id } },
),
);
await assert.rejects(
database.query(
`UPDATE "${APPROVED_RUN_ACTION_RECEIPT_TABLE}"
SET created_at_ms = finished_at_ms + 1
WHERE dispatch_id = :dispatchId`,
{ replacements: { dispatchId: snapshot.dispatch.id } },
),
);
});
test('handler atomically creates one queued Run and a fully bound receipt', async (t) => {
const state = await setup(t);
const approvedPlan = plan('atomic');
const snapshot = await startApprovedRun(state, 'atomic', approvedPlan);
const repository = new LegacySequelizeApprovedRunActionRepository(
state.database,
{
clock: () => snapshot.execution.startedAtMs + 5,
createId: (() => {
const ids = [
'019f8000-0000-7000-8000-000000000001',
'019f8000-0000-7000-8000-000000000002',
'019f8000-0000-7000-8000-000000000003',
'019f8000-0000-7000-8000-000000000004',
];
return () => ids.shift();
})(),
},
);
const resolver = {
async resolve() {
return approvedPlan;
},
};
const handler = new ApprovedRunActionHandler(resolver, repository);
const inspection = await handler.inspect(snapshot.dispatch);
assert.deepEqual(inspection, {
status: 'ready',
actionDigest: snapshot.dispatch.action.actionDigest,
});
const context = {
dispatch: snapshot.dispatch,
execution: snapshot.execution,
idempotencyKey: snapshot.dispatch.id,
fence: {
owner: snapshot.execution.leaseOwner,
leaseToken: snapshot.execution.leaseToken,
version: snapshot.execution.version,
},
};
assert.deepEqual(await handler.execute(context), {
outcome: 'succeeded',
resultCode: 'approved_run_created',
});
assert.deepEqual(await handler.execute(context), {
outcome: 'succeeded',
resultCode: 'approved_run_created',
});
const runs = await state.database.query('SELECT * FROM "Runs"', {
type: QueryTypes.SELECT,
});
const receipts = await state.database.query(
`SELECT * FROM "${APPROVED_RUN_ACTION_RECEIPT_TABLE}"`,
{ type: QueryTypes.SELECT },
);
assert.equal(runs.length, 1);
assert.equal(receipts.length, 1);
assert.equal(runs[0].status, 'queued');
assert.equal(runs[0].idempotency_key, snapshot.dispatch.id);
assert.equal(runs[0].request_id, snapshot.dispatch.approvalRequestId);
assert.equal(receipts[0].resource_id, runs[0].id);
assert.equal(receipts[0].execution_attempt, snapshot.execution.attemptCount);
assert.equal(receipts[0].execution_version, snapshot.execution.version);
assert.equal(receipts[0].started_at_ms, snapshot.execution.startedAtMs);
});
test('receipt insertion failure rolls the Run aggregate back with it', async (t) => {
const state = await setup(t);
const approvedPlan = plan('rollback');
const snapshot = await startApprovedRun(state, 'rollback', approvedPlan);
await state.database.query(
`CREATE TRIGGER reject_approved_run_receipt
BEFORE INSERT ON "${APPROVED_RUN_ACTION_RECEIPT_TABLE}"
BEGIN SELECT RAISE(ABORT, 'receipt rejected'); END`,
);
const repository = new LegacySequelizeApprovedRunActionRepository(
state.database,
{ clock: () => snapshot.execution.startedAtMs + 5 },
);
await assert.rejects(
repository.create({ snapshot, plan: approvedPlan }),
ApprovedRunActionRepositoryError,
);
assert.equal(
(
await state.database.query('SELECT id FROM "Runs"', {
type: QueryTypes.SELECT,
})
).length,
0,
);
assert.equal(
(
await state.database.query(
'SELECT dispatch_id FROM "ApprovedRunActionReceipts"',
{
type: QueryTypes.SELECT,
},
)
).length,
0,
);
});
test('atomic creation accepts same-fence renew and records the current version', async (t) => {
const state = await setup(t);
const approvedPlan = plan('renewed');
const started = await startApprovedRun(state, 'renewed', approvedPlan);
const renewed = await state.executions.renew({
dispatchId: started.dispatch.id,
owner: started.execution.leaseOwner,
leaseToken: started.execution.leaseToken,
expectedVersion: started.execution.version,
nowMs: started.execution.startedAtMs + 10,
leaseDurationMs: 1_000,
});
const repository = new LegacySequelizeApprovedRunActionRepository(
state.database,
{ clock: () => started.execution.startedAtMs + 20 },
);
await repository.create({ snapshot: started, plan: approvedPlan });
const [receipt] = await state.database.query(
`SELECT execution_version FROM "${APPROVED_RUN_ACTION_RECEIPT_TABLE}"`,
{ type: QueryTypes.SELECT },
);
assert.equal(receipt.execution_version, renewed.execution.version);
const provider = new LegacySequelizeApprovedRunRecoveryEvidenceProvider(
state.database,
);
assert.equal(
(await provider.inspect(recoveryContext(renewed))).finding,
'verified_succeeded',
);
});
test('terminal resolution fences stale handler context before any Run write', async (t) => {
const state = await setup(t);
const approvedPlan = plan('fenced');
const started = await startApprovedRun(state, 'fenced', approvedPlan);
await state.executions.complete({
dispatchId: started.dispatch.id,
owner: started.execution.leaseOwner,
leaseToken: started.execution.leaseToken,
expectedVersion: started.execution.version,
resultMutationId: 'terminal-before-action',
outcome: 'failed',
resultCode: 'fenced_before_action',
completedAtMs: started.execution.startedAtMs + 10,
});
const repository = new LegacySequelizeApprovedRunActionRepository(
state.database,
{ clock: () => started.execution.startedAtMs + 20 },
);
await assert.rejects(
repository.create({ snapshot: started, plan: approvedPlan }),
ApprovedRunActionBindingConflictError,
);
assert.equal(
(
await state.database.query('SELECT id FROM "Runs"', {
type: QueryTypes.SELECT,
})
).length,
0,
);
});
test('evidence provider verifies only the atomic receipt and bound Run fact', async (t) => {
const state = await setup(t);
const approvedPlan = plan('evidence');
const snapshot = await startApprovedRun(state, 'evidence', approvedPlan);
const provider = new LegacySequelizeApprovedRunRecoveryEvidenceProvider(
state.database,
);
assert.deepEqual(await provider.inspect(recoveryContext(snapshot)), {
finding: 'missing',
resultCode: 'approved_run_receipt_missing',
});
const repository = new LegacySequelizeApprovedRunActionRepository(
state.database,
{ clock: () => snapshot.execution.startedAtMs + 5 },
);
await repository.create({ snapshot, plan: approvedPlan });
const evidence = await provider.inspect(recoveryContext(snapshot));
assert.equal(evidence.finding, 'verified_succeeded');
assert.equal(evidence.resultCode, 'approved_run_receipt_verified');
assert.match(evidence.evidenceDigest, /^[0-9a-f]{64}$/);
await state.database.query(
`UPDATE "${APPROVED_RUN_ACTION_RECEIPT_TABLE}"
SET action_digest = :digest
WHERE dispatch_id = :dispatchId`,
{
replacements: {
digest: '0'.repeat(64),
dispatchId: snapshot.dispatch.id,
},
},
);
assert.deepEqual(await provider.inspect(recoveryContext(snapshot)), {
finding: 'conflict',
resultCode: 'approved_run_receipt_conflict',
});
});
test('a Run idempotency collision without an atomic receipt is conflict, not success', async (t) => {
const state = await setup(t);
const approvedPlan = plan('collision');
const snapshot = await startApprovedRun(state, 'collision', approvedPlan);
const creator = new PrimaryRunCreator(
new LegacySequelizeRunRepository(state.database),
);
await creator.create(
{
projectId: PROJECT_ID,
taskId: approvedPlan.taskId,
taskRevision: approvedPlan.taskRevision,
triggerType: 'untrusted_collision',
executionOrigin: 'system',
requestId: snapshot.dispatch.approvalRequestId,
idempotencyKey: snapshot.dispatch.id,
acceptedAtMs: snapshot.execution.startedAtMs,
actor: { type: 'system' },
},
'local_process',
);
const provider = new LegacySequelizeApprovedRunRecoveryEvidenceProvider(
state.database,
);
assert.deepEqual(await provider.inspect(recoveryContext(snapshot)), {
finding: 'conflict',
resultCode: 'approved_run_receipt_conflict',
});
});
test('receipt recovers a dispatcher crash after the Run commit but before completion', async (t) => {
const state = await setup(t);
const approvedPlan = plan('crash-window');
const dispatch = await prepareApprovedRun(
state,
'crash-window',
approvedPlan,
);
const crashingRepository = {
findById: (...args) => state.executions.findById(...args),
listDue: (...args) => state.executions.listDue(...args),
claim: (...args) => state.executions.claim(...args),
start: (...args) => state.executions.start(...args),
renew: (...args) => state.executions.renew(...args),
releaseBeforeStart: (...args) =>
state.executions.releaseBeforeStart(...args),
async complete() {
throw new Error('simulated completion persistence outage');
},
};
const actionRepository = new LegacySequelizeApprovedRunActionRepository(
state.database,
{ clock: () => BASE_TIME + 1_010 },
);
const handler = new ApprovedRunActionHandler(
{
async resolve() {
return approvedPlan;
},
},
actionRepository,
);
let dispatchNow = BASE_TIME + 1_000;
let dispatchId = 0;
const dispatcher = new ApprovedActionDispatcher(
crashingRepository,
[handler],
{
owner: 'dispatcher-crash-test',
leaseDurationMs: 1_000,
clock: () => ++dispatchNow,
createId: () => `dispatch-mutation-${++dispatchId}`,
},
);
const dispatched = await dispatcher.dispatchBatch({ limit: 1 });
assert.equal(dispatched.started, 1);
assert.equal(dispatched.succeeded, 0);
assert.equal(dispatched.unavailable, 1);
assert.equal(dispatched.recoveryRequired, 1);
assert.equal(
(await state.executions.findById(dispatch.id)).execution.status,
'executing',
);
let recoveryNow = BASE_TIME + 3_000;
let recoveryId = 0;
const reconciler = new ApprovedActionRecoveryReconciler(
new LegacySequelizeApprovedActionRecoveryRepository(state.database),
[new LegacySequelizeApprovedRunRecoveryEvidenceProvider(state.database)],
{
owner: 'recovery-crash-test',
leaseDurationMs: 1_000,
clock: () => ++recoveryNow,
createId: () => `recovery-mutation-${++recoveryId}`,
},
);
const recovered = await reconciler.reconcileBatch({ limit: 1 });
assert.equal(recovered.verifiedSucceeded, 1);
assert.equal(
(await state.executions.findById(dispatch.id)).execution.status,
'succeeded',
);
});
@@ -0,0 +1,227 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
bootstrapDefaultManualPrimaryRuntime,
} = require('../../back/runtime/adapters/legacy/bootstrapDefaultManualPrimaryRuntime');
const {
RuntimeRolloutPolicy,
} = require('../../back/runtime/domain/runtimeRollout');
const NOW = 1_750_000_000_000;
function loadResult(status, mode = 'off') {
return {
status,
policy: new RuntimeRolloutPolicy({
defaultMode: 'off',
origins: mode === 'off' ? {} : { manual: mode },
allowLegacyFallbackBeforeStart: false,
}),
audit: {
event: 'runtime.rollout_config_evaluated',
evaluatedAtMs: NOW,
sourcePath: '/data/config/qinglong3-rollout.json',
status,
},
};
}
test('default-off bootstrap does not import or construct the Primary stack', async () => {
const calls = [];
const result = await bootstrapDefaultManualPrimaryRuntime({
load: async () => loadResult('missing'),
async loadStack() {
calls.push('load-stack');
throw new Error('must remain lazy');
},
install() {
calls.push('install');
return () => undefined;
},
audit(record) {
calls.push(`audit:${record.activation}`);
},
});
assert.equal(result.active, false);
assert.deepEqual(calls, ['audit:not_activated']);
});
test('accepted bootstrap lazily loads the stack and delegates activation', async () => {
const calls = [];
const result = await bootstrapDefaultManualPrimaryRuntime({
load: async () => loadResult('accepted', 'primary'),
async loadStack() {
calls.push('load-stack');
return {
createDefaultManualPrimaryActivationStack(_rollout, options) {
calls.push('create-stack');
calls.push(`profile:${options.deploymentProfile}`);
return {
router: {
ownsNewRuns: () => true,
async start() {
throw new Error('not used');
},
async stopCron() {
return { matched: 0, failed: 0 };
},
async stopAttempt() {
return { matched: 0, failed: 0 };
},
},
async reconcile() {
calls.push('reconcile');
return {
pages: 1,
scanned: 0,
verifiedRunning: 0,
recoveredRunning: 0,
completedFromReceipt: 0,
quarantinedReceipts: 0,
publishGraceWaits: 0,
markedLost: 0,
skipped: 0,
ambiguous: 0,
failed: 0,
stopReason: 'complete',
remaining: false,
};
},
startCompletion() {
calls.push('start-completion');
return true;
},
async stopCompletion() {
calls.push('stop-completion');
return 'drained';
},
startTimeout() {
calls.push('start-timeout');
return true;
},
async stopTimeout() {
calls.push('stop-timeout');
return 'drained';
},
startCancellation() {
calls.push('start-cancellation');
return true;
},
async stopCancellation() {
calls.push('stop-cancellation');
return 'drained';
},
};
},
};
},
install() {
calls.push('install');
return () => calls.push('dispose');
},
audit(record) {
calls.push(`audit:${record.activation}`);
},
});
assert.equal(result.active, true);
await result.stop();
assert.deepEqual(calls, [
'load-stack',
'audit:selected',
'create-stack',
'profile:standalone',
'reconcile',
'audit:reconciled',
'start-completion',
'start-timeout',
'start-cancellation',
'install',
'audit:activated',
'dispose',
'stop-timeout',
'stop-cancellation',
'stop-completion',
'audit:stopped',
]);
});
test('accepted bootstrap audits a lazy stack import failure without installing', async () => {
const calls = [];
await assert.rejects(
bootstrapDefaultManualPrimaryRuntime({
load: async () => loadResult('accepted', 'primary'),
async loadStack() {
calls.push('load-stack');
throw new Error('stack import failed');
},
install() {
calls.push('install');
return () => calls.push('dispose');
},
audit(record) {
calls.push(`audit:${record.activation}`);
},
}),
/stack import failed/,
);
assert.deepEqual(calls, ['load-stack', 'audit:failed']);
});
test('disabled bootstrap stays inert even with an invalid deployment profile', async () => {
const previous = process.env.QL_DEPLOYMENT_PROFILE;
process.env.QL_DEPLOYMENT_PROFILE = 'invalid-profile';
try {
const result = await bootstrapDefaultManualPrimaryRuntime({
load: async () => loadResult('disabled'),
async loadStack() {
throw new Error('must remain lazy');
},
install() {
throw new Error('must remain uninstalled');
},
audit() {},
});
assert.equal(result.active, false);
} finally {
if (previous === undefined) delete process.env.QL_DEPLOYMENT_PROFILE;
else process.env.QL_DEPLOYMENT_PROFILE = previous;
}
});
test('accepted bootstrap rejects and audits an invalid deployment profile', async () => {
const previous = process.env.QL_DEPLOYMENT_PROFILE;
process.env.QL_DEPLOYMENT_PROFILE = 'invalid-profile';
const calls = [];
try {
await assert.rejects(
bootstrapDefaultManualPrimaryRuntime({
load: async () => loadResult('accepted', 'primary'),
async loadStack() {
calls.push('load-stack');
return {
createDefaultManualPrimaryActivationStack() {
calls.push('create-stack');
throw new Error('profile must be rejected first');
},
};
},
install() {
calls.push('install');
return () => calls.push('dispose');
},
audit(record) {
calls.push(`audit:${record.activation}`);
},
}),
/QL_DEPLOYMENT_PROFILE is invalid/,
);
assert.deepEqual(calls, ['load-stack', 'audit:selected', 'audit:failed']);
} finally {
if (previous === undefined) delete process.env.QL_DEPLOYMENT_PROFILE;
else process.env.QL_DEPLOYMENT_PROFILE = previous;
}
});
@@ -0,0 +1,447 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { afterEach, test } = require('node:test');
const { QueryTypes, Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const {
RUN_ATTEMPT_TABLE,
RUN_EVENT_TABLE,
RUN_TABLE,
runSchemaMigration,
} = require('../../back/migrations/0002-run-schema');
const {
runCancellationRequestMigration,
} = require('../../back/migrations/0004-run-cancellation-request');
const {
runCancellationDispatchMigration,
RUN_CANCELLATION_DISPATCH_TABLE,
} = require('../../back/migrations/0005-run-cancellation-dispatch');
const {
runAttemptDeadlineMigration,
} = require('../../back/migrations/0006-run-attempt-deadline');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacySequelizeCancellationDispatchRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/cancellationDispatchRepository');
const {
CancellationDispatchBindingConflictError,
CancellationDispatchFenceRejectedError,
CancellationDispatchRepositoryError,
} = require('../../back/runtime/domain/cancellationDispatchErrors');
const {
PrimaryCancellationDispatcher,
} = require('../../back/runtime/application/primaryCancellationDispatcher');
const databases = [];
let idSequence = 200;
function nextId() {
idSequence += 1;
return `019f71c0-0000-7000-8000-${String(idSequence).padStart(12, '0')}`;
}
async function createRepository() {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations: [
runSchemaMigration,
runCancellationRequestMigration,
runCancellationDispatchMigration,
runAttemptDeadlineMigration,
],
logger: { info() {} },
});
databases.push(database);
return {
database,
repository: new LegacySequelizeCancellationDispatchRepository(database),
};
}
async function insertCandidate(database, overrides = {}) {
const queryInterface = database.getQueryInterface();
const runId = overrides.runId ?? nextId();
const attemptId = overrides.attemptId ?? nextId();
const requestedAtMs = overrides.requestedAtMs ?? 1_750_000_000_100;
await queryInterface.bulkInsert(RUN_TABLE, [
{
id: runId,
project_id: 'default',
task_id: `task:${runId}`,
task_revision: 'revision-1',
trigger_type: 'manual',
execution_origin: 'manual',
execution_owner: overrides.executionOwner ?? 'runtime',
status: overrides.runStatus ?? 'running',
version: 2,
event_sequence: 0,
priority: 0,
created_at_ms: 1_750_000_000_000,
started_at_ms: 1_750_000_000_010,
cancel_requested_at_ms:
overrides.cancelRequestedAtMs === undefined
? requestedAtMs
: overrides.cancelRequestedAtMs,
cancel_reason: 'user',
},
]);
await queryInterface.bulkInsert(RUN_ATTEMPT_TABLE, [
{
id: attemptId,
run_id: runId,
attempt: 1,
status: overrides.attemptStatus ?? 'running',
executor_type: 'local_process',
callback_sequence: 0,
created_at_ms: 1_750_000_000_010,
},
]);
return { runId, attemptId, requestedAtMs };
}
function claim(candidate, overrides = {}) {
return {
...candidate,
owner: 'worker-a',
leaseToken: 'lease-a',
nowMs: candidate.requestedAtMs,
leaseDurationMs: 50,
...overrides,
};
}
afterEach(async () => {
await Promise.all(databases.splice(0).map((database) => database.close()));
});
test('fences two workers and lets a second worker recover an expired lease', async () => {
const { database, repository } = await createRepository();
const candidate = await insertCandidate(database);
const first = await repository.claim(claim(candidate));
assert.equal(first.status, 'claimed');
assert.equal(first.dispatch.version, 1);
assert.equal(first.dispatch.dispatchCount, 1);
assert.equal(first.dispatch.leaseOwner, 'worker-a');
const competing = await repository.claim(
claim(candidate, {
owner: 'worker-b',
leaseToken: 'lease-b',
nowMs: candidate.requestedAtMs + 25,
}),
);
assert.equal(competing.status, 'leased');
assert.equal(competing.dispatch.leaseOwner, 'worker-a');
const recovered = await repository.claim(
claim(candidate, {
owner: 'worker-b',
leaseToken: 'lease-b',
nowMs: candidate.requestedAtMs + 50,
}),
);
assert.equal(recovered.status, 'claimed');
assert.equal(recovered.dispatch.version, 2);
assert.equal(recovered.dispatch.dispatchCount, 2);
assert.equal(recovered.dispatch.leaseOwner, 'worker-b');
await assert.rejects(
repository.recordResult({
runId: candidate.runId,
attemptId: candidate.attemptId,
owner: 'worker-a',
leaseToken: 'lease-a',
expectedVersion: 1,
result: 'termination_requested',
atMs: candidate.requestedAtMs + 51,
eventId: nextId(),
}),
CancellationDispatchFenceRejectedError,
);
const recorded = await repository.recordResult({
runId: candidate.runId,
attemptId: candidate.attemptId,
owner: 'worker-b',
leaseToken: 'lease-b',
expectedVersion: recovered.dispatch.version,
result: 'termination_requested',
atMs: candidate.requestedAtMs + 52,
eventId: nextId(),
});
assert.equal(recorded.dispatch.status, 'dispatched');
assert.equal(recorded.dispatch.version, 3);
assert.equal(recorded.event.type, 'run.cancel_dispatched');
assert.deepEqual(recorded.event.payload, {
attempt_id: candidate.attemptId,
dispatch_count: 2,
result: 'termination_requested',
});
const terminal = await repository.claim(
claim(candidate, {
owner: 'worker-c',
leaseToken: 'lease-c',
nowMs: candidate.requestedAtMs + 100,
}),
);
assert.equal(terminal.status, 'dispatched');
});
test('persists retry backoff and only reclaims when it becomes due', async () => {
const { database, repository } = await createRepository();
const candidate = await insertCandidate(database);
const leased = await repository.claim(claim(candidate));
const retryAtMs = candidate.requestedAtMs + 1_000;
const failed = await repository.recordResult({
runId: candidate.runId,
attemptId: candidate.attemptId,
owner: 'worker-a',
leaseToken: 'lease-a',
expectedVersion: leased.dispatch.version,
result: 'dispatch_error',
atMs: candidate.requestedAtMs + 1,
nextAttemptAtMs: retryAtMs,
eventId: nextId(),
});
assert.equal(failed.dispatch.status, 'retry_wait');
assert.equal(failed.dispatch.nextAttemptAtMs, retryAtMs);
assert.equal(failed.event.type, 'run.cancel_dispatch_failed');
const early = await repository.claim(
claim(candidate, {
owner: 'worker-b',
leaseToken: 'lease-b',
nowMs: retryAtMs - 1,
}),
);
assert.equal(early.status, 'not_due');
const retry = await repository.claim(
claim(candidate, {
owner: 'worker-b',
leaseToken: 'lease-b',
nowMs: retryAtMs,
}),
);
assert.equal(retry.status, 'claimed');
assert.equal(retry.dispatch.dispatchCount, 2);
const missingController = await repository.recordResult({
runId: candidate.runId,
attemptId: candidate.attemptId,
owner: 'worker-b',
leaseToken: 'lease-b',
expectedVersion: retry.dispatch.version,
result: 'controller_missing',
atMs: retryAtMs + 1,
nextAttemptAtMs: retryAtMs + 2_000,
eventId: nextId(),
});
assert.equal(missingController.dispatch.status, 'retry_wait');
assert.equal(
missingController.dispatch.lastDispatchedAtMs,
failed.dispatch.lastDispatchedAtMs,
);
});
test('fails closed for stale candidates and conflicting Attempt bindings', async () => {
const { database, repository } = await createRepository();
const stale = await insertCandidate(database, { runStatus: 'succeeded' });
assert.deepEqual(await repository.claim(claim(stale)), {
status: 'not_eligible',
});
assert.equal(await repository.findByRunId(stale.runId), null);
const candidate = await insertCandidate(database);
await repository.claim(claim(candidate));
const secondAttemptId = nextId();
await database.getQueryInterface().bulkInsert(RUN_ATTEMPT_TABLE, [
{
id: secondAttemptId,
run_id: candidate.runId,
attempt: 2,
status: 'running',
executor_type: 'local_process',
callback_sequence: 0,
created_at_ms: candidate.requestedAtMs + 1,
},
]);
await assert.rejects(
repository.claim(
claim(
{ ...candidate, attemptId: secondAttemptId },
{
owner: 'worker-b',
leaseToken: 'lease-b',
nowMs: candidate.requestedAtMs + 50,
},
),
),
CancellationDispatchBindingConflictError,
);
});
test('rolls back dispatch state and Run version when event append fails', async () => {
const { database, repository } = await createRepository();
const candidate = await insertCandidate(database);
const leased = await repository.claim(claim(candidate));
const duplicateEventId = nextId();
await database.getQueryInterface().bulkInsert(RUN_EVENT_TABLE, [
{
id: duplicateEventId,
run_id: candidate.runId,
sequence: 99,
type: 'fixture.event',
dedupe_key: 'fixture-event',
actor_type: 'system',
payload: '{}',
created_at_ms: candidate.requestedAtMs,
},
]);
await assert.rejects(
repository.recordResult({
runId: candidate.runId,
attemptId: candidate.attemptId,
owner: 'worker-a',
leaseToken: 'lease-a',
expectedVersion: leased.dispatch.version,
result: 'already_exited',
atMs: candidate.requestedAtMs + 1,
eventId: duplicateEventId,
}),
);
const afterFailure = await repository.findByRunId(candidate.runId);
assert.equal(afterFailure.status, 'leased');
assert.equal(afterFailure.version, leased.dispatch.version);
const [run] = await database.query(
`SELECT version, event_sequence FROM ${RUN_TABLE} WHERE id = :runId`,
{
replacements: { runId: candidate.runId },
type: QueryTypes.SELECT,
},
);
assert.deepEqual(run, { version: 2, event_sequence: 0 });
const recovered = await repository.recordResult({
runId: candidate.runId,
attemptId: candidate.attemptId,
owner: 'worker-a',
leaseToken: 'lease-a',
expectedVersion: leased.dispatch.version,
result: 'already_exited',
atMs: candidate.requestedAtMs + 2,
eventId: nextId(),
});
assert.equal(recovered.dispatch.status, 'dispatched');
});
test('allows only one of two dispatchers to signal the same persisted Attempt', async () => {
const { database, repository } = await createRepository();
const candidate = await insertCandidate(database);
const source = {
async listCandidates() {
return {
candidates: [
{
runId: candidate.runId,
requestedAtMs: candidate.requestedAtMs,
reason: 'user',
attempts: [
{
attemptId: candidate.attemptId,
executorType: 'local_process',
executorHandle: 'durable-handle',
pid: 4100,
},
],
},
],
truncated: false,
unsafeAttemptOverflow: false,
};
},
};
let stopCalls = 0;
const controller = {
executorType: 'local_process',
async stop() {
stopCalls += 1;
return {
status: 'termination_requested',
termSignalSent: true,
killSignalSent: false,
};
},
};
function options(owner) {
return {
owner,
clock: () => candidate.requestedAtMs + 1,
createId: nextId,
};
}
const workerA = new PrimaryCancellationDispatcher(
source,
repository,
[controller],
options('worker-a'),
);
const workerB = new PrimaryCancellationDispatcher(
source,
repository,
[controller],
options('worker-b'),
);
const first = await workerA.dispatchBatch();
const second = await workerB.dispatchBatch();
assert.equal(first.terminationRequested, 1);
assert.equal(second.alreadyResolved, 1);
assert.equal(stopCalls, 1);
});
test('fails closed instead of reclaiming a corrupt persisted lease', async () => {
const { database, repository } = await createRepository();
const candidate = await insertCandidate(database);
await database
.getQueryInterface()
.bulkInsert(RUN_CANCELLATION_DISPATCH_TABLE, [
{
run_id: candidate.runId,
attempt_id: candidate.attemptId,
status: 'leased',
version: 1,
dispatch_count: 1,
lease_owner: 'worker-a',
lease_token: null,
lease_expires_at_ms: candidate.requestedAtMs - 1,
created_at_ms: candidate.requestedAtMs - 100,
updated_at_ms: candidate.requestedAtMs - 50,
},
]);
await assert.rejects(
repository.claim(
claim(candidate, {
owner: 'worker-b',
leaseToken: 'lease-b',
nowMs: candidate.requestedAtMs,
}),
),
CancellationDispatchRepositoryError,
);
});
@@ -0,0 +1,204 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
activateClusterControlRuntime,
} = require('../../back/runtime/application/clusterControlRuntimeActivation');
const EVIDENCE = Object.freeze({
contractName: 'control-core',
contractVersion: 1,
serverMajor: 16,
migrationIds: Object.freeze([
'pg-0001-schema-capability',
'pg-0002-run-core',
]),
});
function stack(events, recovery = { safe: true, remaining: 0, failed: 0 }) {
return {
async reconcile() {
events.push('reconcile');
return recovery;
},
async startLifecycles() {
events.push('start-lifecycles');
return true;
},
installAdmission() {
events.push('install-admission');
return () => events.push('dispose-admission');
},
async stop() {
events.push('stop-stack');
return 'stopped';
},
};
}
function options(events, overrides = {}) {
return {
enabled: true,
profile: 'cluster-control',
readiness: {
async assertReady() {
events.push('readiness');
return EVIDENCE;
},
},
create() {
events.push('create');
return stack(events);
},
audit(record) {
events.push(`audit:${record.state}`);
},
...overrides,
};
}
test('does nothing when cluster-control activation is not explicitly enabled', async () => {
const events = [];
const result = await activateClusterControlRuntime(
options(events, { enabled: false }),
);
assert.equal(result.status, 'disabled');
assert.deepEqual(events, ['audit:disabled']);
assert.equal(await result.stop(), 'stopped');
});
test('rejects the wrong deployment profile before probing the database', async () => {
const events = [];
await assert.rejects(
activateClusterControlRuntime(options(events, { profile: 'standalone' })),
/cannot activate cluster-control/,
);
assert.deepEqual(events, []);
});
test('never constructs repositories when schema readiness fails', async () => {
const events = [];
const unavailable = new Error('database unavailable');
await assert.rejects(
activateClusterControlRuntime(
options(events, {
readiness: {
async assertReady() {
events.push('readiness');
throw unavailable;
},
},
}),
),
(error) => error === unavailable,
);
assert.deepEqual(events, ['readiness', 'audit:failed']);
});
test('orders readiness, recovery, lifecycles and admission and stops idempotently', async () => {
const events = [];
const result = await activateClusterControlRuntime(options(events));
assert.equal(result.status, 'active');
assert.deepEqual(events, [
'readiness',
'audit:schema_ready',
'create',
'reconcile',
'audit:reconciled',
'start-lifecycles',
'install-admission',
'audit:active',
]);
const first = result.stop();
const second = result.stop();
assert.equal(first, second);
assert.equal(await first, 'stopped');
assert.deepEqual(events.slice(-3), [
'dispose-admission',
'stop-stack',
'audit:stopped',
]);
});
test('cleans up a constructed stack when startup recovery is unsafe', async () => {
const events = [];
await assert.rejects(
activateClusterControlRuntime(
options(events, {
create() {
events.push('create');
return stack(events, { safe: false, remaining: 1, failed: 0 });
},
}),
),
/did not converge safely/,
);
assert.equal(events.includes('start-lifecycles'), false);
assert.equal(events.includes('install-admission'), false);
assert.deepEqual(events.slice(-2), ['stop-stack', 'audit:failed']);
});
test('continues stopping the stack when admission cleanup fails', async () => {
const events = [];
const cleanupFailure = new Error('admission cleanup failed');
const result = await activateClusterControlRuntime(
options(events, {
create() {
events.push('create');
return {
...stack(events),
installAdmission() {
events.push('install-admission');
return () => {
events.push('dispose-admission');
throw cleanupFailure;
};
},
};
},
}),
);
const first = result.stop();
assert.equal(first, result.stop());
await assert.rejects(first, (error) => error === cleanupFailure);
assert.deepEqual(events.slice(-3), [
'dispose-admission',
'stop-stack',
'audit:failed',
]);
});
test('preserves an activation failure when admission rollback also fails', async () => {
const events = [];
const activationFailure = new Error('active audit failed');
await assert.rejects(
activateClusterControlRuntime(
options(events, {
create() {
events.push('create');
return {
...stack(events),
installAdmission() {
events.push('install-admission');
return () => {
events.push('dispose-admission');
throw new Error('rollback failed');
};
},
};
},
audit(record) {
events.push(`audit:${record.state}`);
if (record.state === 'active') throw activationFailure;
},
}),
),
(error) => error === activationFailure,
);
assert.deepEqual(events.slice(-3), [
'dispose-admission',
'stop-stack',
'audit:failed',
]);
});
@@ -0,0 +1,301 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
postgresqlMainMigrationStream,
} = require('../../back/migrations/postgresql');
const {
postgresqlControlSchemaContract,
} = require('../../back/migrations/postgresql/schemaContract');
const {
PostgresSchemaReadinessError,
} = require('../../back/migrations/postgresql/schemaReadiness');
const {
PostgresRunRepository,
} = require('../../back/runtime/adapters/postgresql/runRepository');
const {
bootstrapClusterControlRuntime,
} = require('../../back/runtime/adapters/postgresql/clusterControlRuntimeBootstrap');
function migrationHistory() {
return postgresqlMainMigrationStream.migrations.map((migration, index) => ({
streamId: 'postgresql-main',
dialect: 'postgresql',
migrationId: migration.id,
checksum: migration.checksum,
appliedAtMs: index + 1,
}));
}
function runtimePrivileges() {
const privileges = {
schema_migrations: [true, false, false, false],
schema_capabilities: [true, false, false, false],
runs: [true, true, true, false],
run_attempts: [true, true, true, false],
run_events: [true, true, false, false],
run_retry_policies: [true, true, true, false],
};
return Object.entries(privileges).map(
([
tableName,
[selectAllowed, insertAllowed, updateAllowed, deleteAllowed],
]) => ({
tableName,
selectAllowed,
insertAllowed,
updateAllowed,
deleteAllowed,
isOwner: false,
}),
);
}
function databaseResource(events, overrides = {}) {
const contract = postgresqlControlSchemaContract;
const pool = {
async query(text) {
events.push(
`query:${
events.filter((event) => event.startsWith('query:')).length + 1
}`,
);
if (text.includes("current_setting('server_version_num')")) {
return {
rows: [
{
serverVersionNum: overrides.serverVersionNum ?? '160014',
currentUser: 'ql3_runtime',
inRecovery: false,
transactionReadOnly: 'off',
},
],
};
}
if (text.includes('FROM "ql3"."schema_migrations"')) {
return { rows: migrationHistory() };
}
if (text.includes('FROM "ql3"."schema_capabilities"')) {
return {
rows: [
{
contractName: contract.contractName,
contractVersion: contract.contractVersion,
migrationId: contract.migrationId,
capabilities: contract.capabilities,
},
],
};
}
if (text.includes('FROM information_schema.columns')) {
return {
rows: contract.tables.flatMap((table) =>
table.columns.map((columnName) => ({
tableName: table.name,
columnName,
})),
),
};
}
if (text.includes('FROM pg_indexes')) {
return {
rows: contract.indexes.map((indexName) => ({ indexName })),
};
}
if (text.includes('FROM pg_constraint')) {
return {
rows: [
...contract.checks.map((constraintName) => ({
constraintName,
constraintType: 'check',
})),
...contract.foreignKeys.map((constraintName) => ({
constraintName,
constraintType: 'foreign_key',
})),
],
};
}
if (text.includes('FROM pg_catalog.pg_roles')) {
return {
rows: [
{
canLogin: true,
superuser: false,
createDatabase: false,
createRole: false,
replication: false,
bypassRowLevelSecurity: false,
databaseConnect: true,
},
],
};
}
if (text.includes('has_schema_privilege')) {
return { rows: [{ schemaUsage: true, schemaCreate: false }] };
}
if (text.includes('has_table_privilege')) {
return { rows: runtimePrivileges() };
}
throw new Error(`unexpected query: ${text}`);
},
async connect() {
throw new Error('Repository connections are not used during bootstrap');
},
};
return {
pool,
async close() {
events.push('close-database');
if (overrides.closeError) throw overrides.closeError;
},
};
}
function activationStack(
events,
recovery = { safe: true, remaining: 0, failed: 0 },
) {
return {
async reconcile() {
events.push('reconcile');
return recovery;
},
async startLifecycles() {
events.push('start-lifecycles');
return true;
},
installAdmission() {
events.push('install-admission');
return () => events.push('dispose-admission');
},
async stop() {
events.push('stop-stack');
return 'stopped';
},
};
}
function bootstrapOptions(events, overrides = {}) {
return {
enabled: true,
profile: 'cluster-control',
async openDatabase() {
events.push('open-database');
return databaseResource(events);
},
create({ evidence, runs }) {
events.push('create-stack');
assert.equal(evidence.contractVersion, 2);
assert.equal(runs instanceof PostgresRunRepository, true);
return activationStack(events);
},
audit(record) {
events.push(`audit:${record.state}`);
},
...overrides,
};
}
test('disabled and wrong-profile bootstrap never opens PostgreSQL', async () => {
const disabledEvents = [];
const disabled = await bootstrapClusterControlRuntime(
bootstrapOptions(disabledEvents, { enabled: false }),
);
assert.equal(disabled.status, 'disabled');
assert.deepEqual(disabledEvents, ['audit:disabled']);
const wrongProfileEvents = [];
await assert.rejects(
bootstrapClusterControlRuntime(
bootstrapOptions(wrongProfileEvents, { profile: 'edge' }),
),
/cannot activate cluster-control/,
);
assert.deepEqual(wrongProfileEvents, []);
});
test('readiness failure closes the database before returning the root error', async () => {
const events = [];
await assert.rejects(
bootstrapClusterControlRuntime(
bootstrapOptions(events, {
async openDatabase() {
events.push('open-database');
return databaseResource(events, { serverVersionNum: '150018' });
},
}),
),
(error) =>
error instanceof PostgresSchemaReadinessError &&
error.code === 'server_version_unsupported',
);
assert.equal(events.includes('create-stack'), false);
assert.deepEqual(events.slice(-2), ['audit:failed', 'close-database']);
});
test('opens once, assembles after readiness, and closes after stack shutdown', async () => {
const events = [];
const result = await bootstrapClusterControlRuntime(bootstrapOptions(events));
assert.equal(result.status, 'active');
assert.equal(events.filter((event) => event === 'open-database').length, 1);
assert.ok(events.indexOf('create-stack') > events.lastIndexOf('query:7'));
const first = result.stop();
assert.equal(first, result.stop());
assert.equal(await first, 'stopped');
assert.deepEqual(events.slice(-4), [
'dispose-admission',
'stop-stack',
'audit:stopped',
'close-database',
]);
});
test('unsafe recovery stops the stack and closes the database', async () => {
const events = [];
await assert.rejects(
bootstrapClusterControlRuntime(
bootstrapOptions(events, {
create({ runs }) {
events.push('create-stack');
assert.equal(runs instanceof PostgresRunRepository, true);
return activationStack(events, {
safe: false,
remaining: 1,
failed: 0,
});
},
}),
),
/did not converge safely/,
);
assert.equal(events.includes('install-admission'), false);
assert.deepEqual(events.slice(-3), [
'stop-stack',
'audit:failed',
'close-database',
]);
});
test('database close failure does not skip stack shutdown and remains idempotent', async () => {
const events = [];
const closeError = new Error('database close failed');
const result = await bootstrapClusterControlRuntime(
bootstrapOptions(events, {
async openDatabase() {
events.push('open-database');
return databaseResource(events, { closeError });
},
}),
);
const first = result.stop();
assert.equal(first, result.stop());
await assert.rejects(first, (error) => error === closeError);
assert.deepEqual(events.slice(-4), [
'dispose-admission',
'stop-stack',
'audit:stopped',
'close-database',
]);
});
+220
View File
@@ -0,0 +1,220 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const { Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const { runSchemaMigration } = require('../../back/migrations/0002-run-schema');
const {
runCancellationRequestMigration,
} = require('../../back/migrations/0004-run-cancellation-request');
const {
runAttemptDeadlineMigration,
} = require('../../back/migrations/0006-run-attempt-deadline');
const {
completionReceiptJournalMigration,
} = require('../../back/migrations/0007-completion-receipt-journal');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacySequelizeCompletionReceiptJournal,
} = require('../../back/runtime/adapters/legacy-sequelize/completionReceiptJournal');
const {
LegacySequelizeRunRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/runRepository');
const {
CompletionReceiptFileStore,
} = require('../../back/runtime/adapters/fs/completionReceiptFileStore');
const {
PrimaryCompletionReceiptConsumer,
} = require('../../back/runtime/application/primaryCompletionReceiptConsumer');
const {
PrimaryCompletionReceiptJournalScanner,
} = require('../../back/runtime/application/primaryCompletionReceiptJournalScanner');
const {
PrimaryRunCompletionService,
hashPrimaryCompletionToken,
} = require('../../back/runtime/application/primaryRunCompletionService');
const NOW = 1_750_900_000_000;
const TOKEN = 'j'.repeat(43);
let sequence = 2_400;
function nextId() {
sequence += 1;
return `019f7900-0000-7000-8000-${String(sequence).padStart(12, '0')}`;
}
async function setup(t) {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
t.after(() => database.close());
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations: [
runSchemaMigration,
runCancellationRequestMigration,
runAttemptDeadlineMigration,
completionReceiptJournalMigration,
],
logger: { info() {} },
});
return {
repository: new LegacySequelizeRunRepository(database),
journal: new LegacySequelizeCompletionReceiptJournal(database),
};
}
async function seed(repository, status = 'running', finishedAtMs) {
const run = {
id: nextId(),
projectId: 'default',
taskId: 'journal-test',
taskRevision: 'revision-1',
triggerType: 'manual',
executionOrigin: 'manual',
executionOwner: 'runtime',
status: status === 'running' ? 'running' : 'succeeded',
version: 0,
eventSequence: 0,
priority: 0,
createdAtMs: NOW,
...(finishedAtMs === undefined ? {} : { finishedAtMs }),
};
const attempt = {
id: nextId(),
runId: run.id,
attempt: 1,
status,
executorType: 'local_process',
callbackSequence: status === 'running' ? 0 : 1,
callbackTokenHash: hashPrimaryCompletionToken(TOKEN),
createdAtMs: NOW,
...(finishedAtMs === undefined ? {} : { finishedAtMs }),
};
await repository.transaction(async (transaction) => {
await transaction.insertRun(run);
await transaction.insertAttempt(attempt);
});
return { run, attempt };
}
test('registers idempotently and lists active and terminal pending receipts', async (t) => {
const { repository, journal } = await setup(t);
const active = await seed(repository);
const terminal = await seed(repository, 'succeeded', NOW + 20);
for (const aggregate of [active, terminal]) {
await journal.register({
runId: aggregate.run.id,
attemptId: aggregate.attempt.id,
registeredAtMs: NOW + 1,
});
}
await journal.register({
runId: active.run.id,
attemptId: active.attempt.id,
registeredAtMs: NOW + 1,
});
const page = await journal.listCandidates({
observedAtMs: NOW + 100,
limit: 8,
});
assert.equal(page.candidates.length, 2);
assert.deepEqual(
page.candidates.map((candidate) => candidate.attemptStatus).sort(),
['running', 'succeeded'],
);
});
test('defers quarantined entries until purge time and resolves idempotently', async (t) => {
const { repository, journal } = await setup(t);
const aggregate = await seed(repository);
await journal.register({
runId: aggregate.run.id,
attemptId: aggregate.attempt.id,
registeredAtMs: NOW,
});
await journal.markQuarantined({
attemptId: aggregate.attempt.id,
quarantineRef: `.quarantine/${aggregate.attempt.id.slice(0, 2)}/${
aggregate.attempt.id
}.json`,
updatedAtMs: NOW + 1,
purgeAfterMs: NOW + 100,
});
assert.equal(
(await journal.listCandidates({ observedAtMs: NOW + 99 })).candidates
.length,
0,
);
const due = await journal.listCandidates({ observedAtMs: NOW + 100 });
assert.equal(due.candidates[0].state, 'quarantined');
assert.equal(await journal.resolve(aggregate.attempt.id), true);
assert.equal(await journal.resolve(aggregate.attempt.id), false);
});
test('finds and cleans a receipt after the Run already became terminal', async (t) => {
const { repository, journal } = await setup(t);
const aggregate = await seed(repository);
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-journal-replay-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
const store = new CompletionReceiptFileStore(root);
await journal.register({
runId: aggregate.run.id,
attemptId: aggregate.attempt.id,
registeredAtMs: aggregate.attempt.createdAtMs,
});
await store.publish({
schemaVersion: 1,
runId: aggregate.run.id,
attemptId: aggregate.attempt.id,
callbackSequence: 1,
token: TOKEN,
startedAtMs: NOW + 1,
finishedAtMs: NOW + 20,
exitCode: 0,
});
const completions = new PrimaryRunCompletionService(repository, nextId);
await completions.complete({
runId: aggregate.run.id,
attemptId: aggregate.attempt.id,
callbackSequence: 1,
source: { kind: 'executor', executorType: 'local_process' },
result: {
outcome: 'succeeded',
startedAtMs: NOW + 1,
finishedAtMs: NOW + 20,
exitCode: 0,
},
});
const consumer = new PrimaryCompletionReceiptConsumer(store, completions, {
journal,
clock: { now: () => NOW + 30 },
});
const scanner = new PrimaryCompletionReceiptJournalScanner(
journal,
store,
consumer,
{ clock: { now: () => NOW + 30 } },
);
const summary = await scanner.scanBatch();
assert.equal(summary.alreadyTerminal, 1);
assert.equal(summary.failed, 0);
assert.equal(await store.read(aggregate.attempt.id), undefined);
assert.equal(
(await journal.listCandidates({ observedAtMs: NOW + 30 })).candidates
.length,
0,
);
});
@@ -0,0 +1,281 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { afterEach, test } = require('node:test');
const {
CompletionReceiptOrphanFileDirectory,
} = require('../../back/runtime/adapters/fs/completionReceiptOrphanDirectory');
const {
CompletionReceiptOrphanAuditor,
} = require('../../back/runtime/application/completionReceiptOrphanAuditor');
const { parseArguments } = require('../../scripts/ql3-receipt-audit.cjs');
const REPOSITORY_ROOT = path.resolve(__dirname, '../..');
const CLI_PATH = path.join(REPOSITORY_ROOT, 'scripts', 'ql3-receipt-audit.cjs');
const OBSERVED_AT_MS = 1_800_000_000_000;
const OLD_MTIME_MS = OBSERVED_AT_MS - 10 * 60_000;
const YOUNG_MTIME_MS = OBSERVED_AT_MS - 1_000;
const OWNED_ID = '019f7400-0000-7000-8000-000000000001';
const ACTIVE_ID = '019f7400-0000-7000-8000-000000000002';
const TERMINAL_ID = '019f7400-0000-7000-8000-000000000003';
const UNKNOWN_ID = '019f7400-0000-7000-8000-000000000004';
const YOUNG_ID = '019f7400-0000-7000-8000-000000000005';
const JOURNAL_ONLY_ID = '019f7400-0000-7000-8000-000000000006';
const roots = [];
async function temporaryRoot() {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-orphan-audit-'));
roots.push(root);
return root;
}
async function writeAt(root, shard, name, modifiedAtMs = OLD_MTIME_MS) {
const directory = path.join(root, shard);
const target = path.join(directory, name);
await fs.mkdir(directory, { recursive: true });
await fs.writeFile(target, '{}');
const timestamp = new Date(modifiedAtMs);
await fs.utimes(target, timestamp, timestamp);
return target;
}
function ownershipSource(values) {
const ownership = new Map(values.map((value) => [value.attemptId, value]));
return {
async lookup(attemptIds) {
return new Map(
attemptIds
.filter((attemptId) => ownership.has(attemptId))
.map((attemptId) => [attemptId, ownership.get(attemptId)]),
);
},
};
}
afterEach(async () => {
await Promise.all(
roots
.splice(0)
.map((root) => fs.rm(root, { recursive: true, force: true })),
);
});
test('audits Journal ownership and quarantines only old regular orphans', async () => {
const root = await temporaryRoot();
const shard = '01';
const names = {
owned: `${OWNED_ID}.json`,
active: `${ACTIVE_ID}.json`,
terminal: `${TERMINAL_ID}.json`,
unknown: `${UNKNOWN_ID}.json`,
young: `${YOUNG_ID}.json`,
temporary: `.${UNKNOWN_ID}.${'a'.repeat(32)}.tmp`,
malformed: 'unexpected.bin',
unsafe: 'unsafe-link',
};
for (const [key, name] of Object.entries(names)) {
if (key !== 'unsafe') {
await writeAt(
root,
shard,
name,
key === 'young' ? YOUNG_MTIME_MS : OLD_MTIME_MS,
);
}
}
await fs.symlink(
path.join(root, shard, names.malformed),
path.join(root, shard, names.unsafe),
);
const auditor = new CompletionReceiptOrphanAuditor(
new CompletionReceiptOrphanFileDirectory(root),
ownershipSource([
{
attemptId: OWNED_ID,
attemptStatus: 'running',
journalState: 'pending',
},
{ attemptId: ACTIVE_ID, attemptStatus: 'running' },
{ attemptId: TERMINAL_ID, attemptStatus: 'succeeded' },
]),
);
const audit = await auditor.run({
observedAtMs: OBSERVED_AT_MS,
startShard: 1,
shardCount: 1,
maxEntriesPerShard: 16,
minimumAgeMs: 5 * 60_000,
});
assert.equal(audit.scannedEntries, 8);
assert.deepEqual(audit.overflowShards, []);
assert.equal(audit.counts.journaled, 1);
assert.equal(audit.counts.active_attempt, 1);
assert.equal(audit.counts.terminal_orphan, 1);
assert.equal(audit.counts.unknown_receipt, 1);
assert.equal(audit.counts.young_unknown_receipt, 1);
assert.equal(audit.counts.stale_temporary, 1);
assert.equal(audit.counts.unknown_entry, 1);
assert.equal(audit.counts.unsafe_entry, 1);
assert.equal(
audit.entries.filter((entry) => entry.action === 'eligible').length,
4,
);
const quarantine = await auditor.run({
mode: 'quarantine',
observedAtMs: OBSERVED_AT_MS,
startShard: 1,
shardCount: 1,
maxEntriesPerShard: 16,
minimumAgeMs: 5 * 60_000,
});
assert.equal(
quarantine.entries.filter((entry) => entry.action === 'quarantined').length,
4,
);
for (const name of [
names.terminal,
names.unknown,
names.temporary,
names.malformed,
]) {
await assert.rejects(fs.lstat(path.join(root, shard, name)), /ENOENT/);
}
for (const name of [names.owned, names.active, names.young, names.unsafe]) {
assert.ok(await fs.lstat(path.join(root, shard, name)));
}
const quarantineFiles = await fs.readdir(
path.join(root, '.orphan-quarantine', shard),
);
assert.equal(quarantineFiles.length, 4);
});
test('fails closed when a shard exceeds its fixed entry capacity', async () => {
const root = await temporaryRoot();
for (const name of ['a.bin', 'b.bin', 'c.bin']) {
await writeAt(root, '02', name);
}
const auditor = new CompletionReceiptOrphanAuditor(
new CompletionReceiptOrphanFileDirectory(root),
ownershipSource([]),
);
const report = await auditor.run({
mode: 'quarantine',
observedAtMs: OBSERVED_AT_MS,
startShard: 2,
shardCount: 1,
maxEntriesPerShard: 2,
minimumAgeMs: 0,
});
assert.deepEqual(report.overflowShards, ['02']);
assert.equal(report.scannedEntries, 2);
assert.ok(
report.entries.every((entry) => entry.action === 'blocked_overflow'),
);
assert.equal((await fs.readdir(path.join(root, '02'))).length, 3);
await assert.rejects(
auditor.run({ shardCount: 33 }),
/shardCount must be between 1 and 32/,
);
await assert.rejects(
auditor.run({ maxEntriesPerShard: 65 }),
/maxEntriesPerShard must be between 1 and 64/,
);
});
test('rejects shard and quarantine directory symlink escapes', async () => {
const root = await temporaryRoot();
const outside = await temporaryRoot();
await fs.symlink(outside, path.join(root, '03'));
const directory = new CompletionReceiptOrphanFileDirectory(root);
await assert.rejects(directory.inspectShard('03', 4), /not a safe directory/);
await writeAt(root, '04', 'orphan.bin');
const snapshot = await directory.inspectShard('04', 4);
await fs.symlink(outside, path.join(root, '.orphan-quarantine'));
await assert.rejects(
directory.quarantine(snapshot.entries[0]),
/not a safe directory/,
);
assert.ok(await fs.lstat(path.join(root, '04', 'orphan.bin')));
assert.throws(
() => new CompletionReceiptOrphanFileDirectory(path.parse(root).root),
/must not be a filesystem root/,
);
});
test('parses a bounded shard cursor and keeps audit as the default mode', () => {
const options = parseArguments([
'--start-shard=fe',
'--shards=2',
'--entries-per-shard=4',
'--minimum-age-ms=1000',
]);
assert.equal(options.mode, 'audit');
assert.equal(options.startShard, 254);
assert.equal(options.shardCount, 2);
assert.equal(options.maxEntriesPerShard, 4);
assert.throws(() => parseArguments(['--shards=33']), /between 1 and 32/);
assert.throws(() => parseArguments(['--start-shard=FF']), /lowercase hex/);
});
test(
'Node 24 CLI reads ownership without opening the database for writes',
{ skip: Number(process.versions.node.split('.')[0]) < 24 },
async () => {
const root = await temporaryRoot();
const databasePath = path.join(root, 'database.sqlite');
const receiptRoot = path.join(root, 'receipts');
const { DatabaseSync } = require('node:sqlite');
const database = new DatabaseSync(databasePath);
database.exec(`
CREATE TABLE RunAttempts (
id TEXT PRIMARY KEY,
status TEXT NOT NULL
);
CREATE TABLE CompletionReceiptJournals (
attempt_id TEXT PRIMARY KEY,
state TEXT NOT NULL
);
INSERT INTO RunAttempts (id, status)
VALUES ('${TERMINAL_ID}', 'succeeded');
INSERT INTO CompletionReceiptJournals (attempt_id, state)
VALUES ('${JOURNAL_ONLY_ID}', 'pending');
`);
database.close();
await writeAt(receiptRoot, '01', `${TERMINAL_ID}.json`);
await writeAt(receiptRoot, '01', `${JOURNAL_ONLY_ID}.json`);
const result = spawnSync(
process.execPath,
[
CLI_PATH,
'--json',
`--database=${databasePath}`,
`--root=${receiptRoot}`,
'--start-shard=01',
'--shards=1',
'--entries-per-shard=4',
'--minimum-age-ms=0',
],
{ cwd: REPOSITORY_ROOT, encoding: 'utf8' },
);
assert.equal(result.status, 0, result.stderr);
const report = JSON.parse(result.stdout);
assert.equal(report.counts.terminal_orphan, 1);
assert.equal(report.counts.journaled, 1);
assert.equal(
report.entries.find((entry) => entry.attemptId === TERMINAL_ID).action,
'eligible',
);
assert.ok(
await fs.lstat(path.join(receiptRoot, '01', `${TERMINAL_ID}.json`)),
);
},
);
+158
View File
@@ -0,0 +1,158 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { afterEach, test } = require('node:test');
const {
InvalidCompletionReceiptError,
MAX_COMPLETION_RECEIPT_BYTES,
parseCompletionReceipt,
serializeCompletionReceipt,
} = require('../../back/runtime/domain/completionReceipt');
const {
CompletionReceiptAlreadyExistsError,
CompletionReceiptFileStore,
} = require('../../back/runtime/adapters/fs/completionReceiptFileStore');
const roots = [];
const RUN_ID = '019f7200-0000-7000-8000-000000000001';
const ATTEMPT_ID = '019f7200-0000-7000-8000-000000000002';
function receipt(overrides = {}) {
return {
schemaVersion: 1,
runId: RUN_ID,
attemptId: ATTEMPT_ID,
callbackSequence: 1,
token: 'a'.repeat(43),
startedAtMs: 1_750_200_000_000,
finishedAtMs: 1_750_200_000_100,
exitCode: 0,
...overrides,
};
}
async function createStore() {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-receipts-'));
roots.push(root);
return { root, store: new CompletionReceiptFileStore(root) };
}
afterEach(async () => {
await Promise.all(
roots
.splice(0)
.map((root) => fs.rm(root, { recursive: true, force: true })),
);
});
test('completion receipt codec round-trips one canonical bounded payload', () => {
const serialized = serializeCompletionReceipt(receipt());
assert.ok(Buffer.byteLength(serialized) < MAX_COMPLETION_RECEIPT_BYTES);
assert.equal(
serializeCompletionReceipt(parseCompletionReceipt(serialized)),
serialized,
);
assert.deepEqual(parseCompletionReceipt(serialized), receipt());
});
test('completion receipt codec rejects ambiguous and extensible payloads', () => {
const value = receipt();
const unknown = JSON.stringify({ ...value, command: 'secret' });
assert.throws(
() => parseCompletionReceipt(unknown),
InvalidCompletionReceiptError,
);
const duplicate = serializeCompletionReceipt(value).replace(
'"attemptId"',
`"runId":"${RUN_ID}","attemptId"`,
);
assert.throws(() => parseCompletionReceipt(duplicate), /duplicate key/);
assert.throws(
() => serializeCompletionReceipt(receipt({ finishedAtMs: 1 })),
/finishedAtMs/,
);
assert.throws(
() => serializeCompletionReceipt(receipt({ exitCode: 256 })),
/exitCode/,
);
assert.throws(
() => parseCompletionReceipt('x'.repeat(MAX_COMPLETION_RECEIPT_BYTES + 1)),
/size/,
);
});
test('file store publishes without overwrite and removes only a known receipt', async () => {
const { root, store } = await createStore();
await store.publish(receipt());
assert.deepEqual(await store.read(ATTEMPT_ID), receipt());
const target = path.join(root, ATTEMPT_ID.slice(0, 2), `${ATTEMPT_ID}.json`);
assert.equal((await fs.stat(target)).mode & 0o777, 0o600);
await assert.rejects(
store.publish(receipt({ exitCode: 1 })),
CompletionReceiptAlreadyExistsError,
);
assert.equal((await store.read(ATTEMPT_ID)).exitCode, 0);
assert.equal(await store.remove(ATTEMPT_ID), true);
assert.equal(await store.remove(ATTEMPT_ID), false);
assert.equal(await store.read(ATTEMPT_ID), undefined);
});
test('does not expose a final receipt when storage reports ENOSPC', async () => {
const { store } = await createStore();
const originalOpen = fs.open;
fs.open = async (target, ...args) => {
if (String(target).endsWith('.tmp')) {
throw Object.assign(new Error('simulated disk full'), { code: 'ENOSPC' });
}
return originalOpen(target, ...args);
};
try {
await assert.rejects(store.publish(receipt()), { code: 'ENOSPC' });
assert.equal(await store.read(ATTEMPT_ID), undefined);
} finally {
fs.open = originalOpen;
}
});
test('file store moves a known receipt to a private quarantine path', async () => {
const { root, store } = await createStore();
await store.publish(receipt());
const quarantineRef = await store.quarantine(ATTEMPT_ID);
assert.match(quarantineRef, /^\.quarantine\/[0-9a-f]{2}\/019f7200-.+\.json$/);
assert.equal(await store.read(ATTEMPT_ID), undefined);
const quarantined = path.join(root, ...quarantineRef.split('/'));
assert.equal((await fs.stat(quarantined)).mode & 0o777, 0o600);
assert.equal(await store.quarantine(ATTEMPT_ID), quarantineRef);
assert.equal(await store.purgeQuarantine(ATTEMPT_ID), true);
assert.equal(await store.purgeQuarantine(ATTEMPT_ID), false);
});
test('file store rejects traversal, oversized files, symlinks, and path mismatch', async () => {
const { root, store } = await createStore();
await assert.rejects(store.read('../../etc/passwd'), /UUIDv7/);
const directory = path.join(root, ATTEMPT_ID.slice(0, 2));
const target = path.join(directory, `${ATTEMPT_ID}.json`);
await fs.mkdir(directory, { recursive: true });
await fs.writeFile(target, 'x'.repeat(MAX_COMPLETION_RECEIPT_BYTES + 1));
await assert.rejects(store.read(ATTEMPT_ID), /byte limit/);
await fs.unlink(target);
const other = receipt({
attemptId: '019f7200-0000-7000-8000-000000000003',
});
await fs.writeFile(target, serializeCompletionReceipt(other));
await assert.rejects(store.read(ATTEMPT_ID), /do not match/);
await fs.unlink(target);
const source = path.join(directory, 'source.json');
await fs.writeFile(source, serializeCompletionReceipt(receipt()));
await fs.symlink(source, target);
await assert.rejects(store.read(ATTEMPT_ID), InvalidCompletionReceiptError);
});
@@ -0,0 +1,131 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const { DataTypes, Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const { migrations } = require('../../back/migrations');
const {
createDefaultManualPrimaryActivationStack,
} = require('../../back/runtime/adapters/legacy/defaultManualPrimaryActivation');
const {
RuntimeRolloutPolicy,
} = require('../../back/runtime/domain/runtimeRollout');
const { runMigrations } = require('../../back/migrations/runner');
async function createDatabase(t) {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
t.after(() => database.close());
const queryInterface = database.getQueryInterface();
for (const table of ['CrontabViews', 'Subscriptions', 'Crontabs', 'Envs']) {
await queryInterface.createTable(table, {
id: { type: DataTypes.INTEGER, primaryKey: true },
});
}
await queryInterface.createTable('RunningInstances', {
id: { type: DataTypes.INTEGER, primaryKey: true },
started_at: { type: DataTypes.INTEGER, allowNull: false },
});
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations,
logger: { info() {} },
});
return database;
}
test('real Primary activation stack reconciles empty state and stops cleanly', async (t) => {
const database = await createDatabase(t);
const rollout = new RuntimeRolloutPolicy({
defaultMode: 'off',
origins: { manual: 'primary' },
allowLegacyFallbackBeforeStart: false,
});
const stack = createDefaultManualPrimaryActivationStack(rollout, {
database,
owner: 'test-http-worker',
recovery: { pageSize: 8, maxPages: 2 },
cancellation: {
intervalMs: 1_000,
initialDelayMs: 60_000,
stopTimeoutMs: 1_000,
cycle: { pageSize: 8, maxPages: 2 },
},
timeout: {
intervalMs: 5_000,
initialDelayMs: 60_000,
stopTimeoutMs: 1_000,
cycle: { pageSize: 8, maxPages: 2 },
},
});
assert.deepEqual(await stack.reconcile(), {
pages: 1,
scanned: 0,
verifiedRunning: 0,
recoveredRunning: 0,
completedFromReceipt: 0,
quarantinedReceipts: 0,
publishGraceWaits: 0,
markedLost: 0,
skipped: 0,
ambiguous: 0,
failed: 0,
stopReason: 'complete',
remaining: false,
});
assert.equal(stack.startCompletion(), true);
assert.equal(stack.startCompletion(), false);
assert.equal(stack.startTimeout(), true);
assert.equal(stack.startTimeout(), false);
assert.equal(stack.startCancellation(), true);
assert.equal(stack.startCancellation(), false);
assert.equal(await stack.stopTimeout(), 'drained');
assert.equal(await stack.stopCancellation(), 'drained');
assert.equal(await stack.stopCompletion(), 'drained');
});
test('real Primary activation stack rejects invalid worker ownership', async (t) => {
const database = await createDatabase(t);
const rollout = new RuntimeRolloutPolicy({
defaultMode: 'off',
origins: { manual: 'primary' },
allowLegacyFallbackBeforeStart: false,
});
assert.throws(
() =>
createDefaultManualPrimaryActivationStack(rollout, {
database,
owner: '',
}),
RangeError,
);
});
test('local SQLite Primary stack refuses cluster-control and worker profiles', async (t) => {
const database = await createDatabase(t);
const rollout = new RuntimeRolloutPolicy({
defaultMode: 'off',
origins: { manual: 'primary' },
allowLegacyFallbackBeforeStart: false,
});
for (const deploymentProfile of ['cluster-control', 'worker']) {
assert.throws(
() =>
createDefaultManualPrimaryActivationStack(rollout, {
database,
deploymentProfile,
}),
/cannot host the local SQLite Primary stack/,
);
}
});
+100
View File
@@ -0,0 +1,100 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
localPrimaryResourcePolicy,
parseDeploymentProfile,
} = require('../../back/runtime/domain/deploymentProfile');
test('uses standalone by default and parses only explicit deployment profiles', () => {
assert.equal(parseDeploymentProfile(undefined), 'standalone');
assert.equal(parseDeploymentProfile(''), 'standalone');
for (const profile of ['edge', 'standalone', 'cluster-control', 'worker']) {
assert.equal(parseDeploymentProfile(profile), profile);
}
for (const profile of ['EDGE', ' edge', 'router', 'cluster']) {
assert.throws(() => parseDeploymentProfile(profile), TypeError);
}
});
test('keeps edge scans smaller and slower than standalone scans', () => {
const edge = localPrimaryResourcePolicy('edge');
const standalone = localPrimaryResourcePolicy('standalone');
assert.ok(edge.completion.intervalMs > standalone.completion.intervalMs);
assert.ok(edge.cancellation.intervalMs > standalone.cancellation.intervalMs);
assert.ok(edge.timeout.intervalMs > standalone.timeout.intervalMs);
assert.ok(edge.retry.intervalMs > standalone.retry.intervalMs);
assert.ok(
edge.approvedAction.intervalMs > standalone.approvedAction.intervalMs,
);
assert.ok(
edge.artifactRetention.intervalMs > standalone.artifactRetention.intervalMs,
);
assert.ok(edge.cancellation.pageSize < standalone.cancellation.pageSize);
assert.ok(edge.timeout.maxPages < standalone.timeout.maxPages);
assert.ok(edge.completion.pageSize < standalone.completion.pageSize);
assert.ok(edge.retry.pageSize < standalone.retry.pageSize);
assert.ok(
edge.approvedAction.dispatch.pageSize <
standalone.approvedAction.dispatch.pageSize,
);
assert.ok(
edge.approvedAction.recovery.maxPages <
standalone.approvedAction.recovery.maxPages,
);
assert.ok(
edge.artifactRetention.pageSize < standalone.artifactRetention.pageSize,
);
assert.ok(
edge.artifactRetention.maximumDeletions <
standalone.artifactRetention.maximumDeletions,
);
assert.equal(edge.retry.maxPages, 1);
assert.equal(standalone.retry.maxPages, 1);
assert.equal(edge.receiptPublishGraceMs, 50);
assert.equal(standalone.receiptPublishGraceMs, 100);
assert.equal(edge.receiptTerminalMissingRetentionMs, 120_000);
assert.equal(standalone.receiptTerminalMissingRetentionMs, 60_000);
assert.ok(
edge.receiptQuarantineRetentionMs < standalone.receiptQuarantineRetentionMs,
);
edge.completion.pageSize = 64;
edge.retry.pageSize = 64;
edge.approvedAction.dispatch.pageSize = 64;
edge.approvedAction.recovery.pageSize = 64;
edge.artifactRetention.pageSize = 64;
assert.notEqual(
localPrimaryResourcePolicy('edge').completion.pageSize,
edge.completion.pageSize,
);
assert.notEqual(
localPrimaryResourcePolicy('edge').retry.pageSize,
edge.retry.pageSize,
);
assert.notEqual(
localPrimaryResourcePolicy('edge').approvedAction.dispatch.pageSize,
edge.approvedAction.dispatch.pageSize,
);
assert.notEqual(
localPrimaryResourcePolicy('edge').approvedAction.recovery.pageSize,
edge.approvedAction.recovery.pageSize,
);
assert.notEqual(
localPrimaryResourcePolicy('edge').artifactRetention.pageSize,
edge.artifactRetention.pageSize,
);
});
test('refuses unsupported local control-plane topologies', () => {
assert.throws(
() => localPrimaryResourcePolicy('cluster-control'),
/cannot host the local SQLite Primary stack/,
);
assert.throws(
() => localPrimaryResourcePolicy('worker'),
/cannot host the local SQLite Primary stack/,
);
});
@@ -0,0 +1,498 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { spawn } = require('node:child_process');
const { afterEach, test } = require('node:test');
const { setTimeout: delay } = require('node:timers/promises');
const {
CompletionReceiptFileStore,
} = require('../../back/runtime/adapters/fs/completionReceiptFileStore');
const {
LocalArtifactTruncationFactStore,
localArtifactTruncationFactFileName,
} = require('../../back/runtime/adapters/fs/localArtifactTruncationFactStore');
const {
enableDurableLocalProcessOutput,
} = require('../../back/runtime/adapters/local-process/durableLocalProcessOutput');
const {
LocalProcessExecutor,
} = require('../../back/runtime/adapters/local-process/localProcessExecutor');
const {
encodeLocalArtifactTruncationFact,
} = require('../../back/runtime/domain/localArtifactTruncation');
const REPOSITORY_ROOT = path.resolve(__dirname, '../..');
const LAUNCHER_PATH = path.join(REPOSITORY_ROOT, 'shell', 'ql3-launcher.sh');
const RUN_ID = '019f7300-0000-7000-8000-000000000001';
const ATTEMPT_ID = '019f7300-0000-7000-8000-000000000002';
const CALLBACK_TOKEN = 'a'.repeat(43);
const LOG_ARTIFACT_ID = `local-${'b'.repeat(30)}`;
const roots = [];
async function temporaryRoot() {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-launcher-'));
roots.push(root);
return root;
}
function specification(overrides = {}) {
return {
runId: RUN_ID,
attemptId: ATTEMPT_ID,
projectId: 'default',
taskId: 'durable-launcher-test',
taskRevision: 'revision-1',
command: {
kind: 'argv',
file: process.execPath,
args: ['-e', "process.stdout.write('durable-output')"],
},
environmentPolicy: 'isolated',
terminationGraceMs: 100,
...overrides,
};
}
function durableContext(root, outputFilePath, write, capability = {}) {
return {
environment: {},
completionCallback: {
token: CALLBACK_TOKEN,
callbackSequence: 1,
},
output: enableDurableLocalProcessOutput(
{
async write(value) {
await write?.(value);
},
},
{
outputFilePath,
completionReceiptRoot: path.join(root, 'receipts'),
...capability,
},
),
};
}
async function waitForReceipt(store, attemptId, timeoutMs = 5_000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const value = await store.read(attemptId);
if (value) return value;
await delay(25);
}
throw new Error('Timed out waiting for completion receipt');
}
async function waitForFileContent(filePath, expected, timeoutMs = 5_000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
if ((await fs.readFile(filePath, 'utf8')).includes(expected)) return;
} catch (error) {
if (error?.code !== 'ENOENT') throw error;
}
await delay(25);
}
throw new Error(`Timed out waiting for ${expected} in ${filePath}`);
}
function waitForExit(child) {
return new Promise((resolve, reject) => {
child.once('error', reject);
child.once('exit', (code, signal) => resolve({ code, signal }));
});
}
afterEach(async () => {
await Promise.all(
roots
.splice(0)
.map((root) => fs.rm(root, { recursive: true, force: true })),
);
});
test(
'writes stdout and stderr directly and publishes one canonical receipt',
{ timeout: 10_000 },
async () => {
const root = await temporaryRoot();
const outputFilePath = path.join(root, 'logs', 'attempt.log');
let sinkWrites = 0;
const executor = new LocalProcessExecutor({
durableLauncherPath: LAUNCHER_PATH,
});
const handle = await executor.start(
specification({
command: {
kind: 'shell',
command:
"test -z \"${QL3_RECEIPT_CALLBACK_TOKEN+x}\" || exit 91; printf 'stdout-value'; printf 'stderr-value' >&2; exit 7",
shell: '/bin/sh',
},
}),
durableContext(root, outputFilePath, async () => {
sinkWrites += 1;
}),
);
const result = await handle.completion;
assert.equal(result.outcome, 'failed');
assert.equal(result.exitCode, 7);
assert.equal(sinkWrites, 0);
assert.equal(
await fs.readFile(outputFilePath, 'utf8'),
'stdout-valuestderr-value',
);
assert.equal((await fs.stat(outputFilePath)).mode & 0o777, 0o600);
const receipt = await new CompletionReceiptFileStore(
path.join(root, 'receipts'),
).read(ATTEMPT_ID);
assert.equal(receipt.runId, RUN_ID);
assert.equal(receipt.attemptId, ATTEMPT_ID);
assert.equal(receipt.callbackSequence, 1);
assert.equal(receipt.token, CALLBACK_TOKEN);
assert.equal(receipt.exitCode, 7);
assert.ok(receipt.finishedAtMs >= receipt.startedAtMs);
},
);
test(
'hard-caps durable output while draining the child to successful completion',
{ timeout: 10_000 },
async () => {
const root = await temporaryRoot();
const outputDirectory = path.join(root, 'logs');
const outputShard = path.join(outputDirectory, LOG_ARTIFACT_ID.slice(6, 8));
const outputFilePath = path.join(outputShard, `${LOG_ARTIFACT_ID}.log`);
const maximumBytes = 64 * 1024;
const executor = new LocalProcessExecutor({
durableLauncherPath: LAUNCHER_PATH,
});
const handle = await executor.start(
specification({
command: {
kind: 'argv',
file: process.execPath,
args: [
'-e',
[
'if (process.env.QL3_OUTPUT_QUOTA_FIFO) process.exit(91);',
'if (process.env.QL3_OUTPUT_TRUNCATION_TARGET) process.exit(92);',
'const chunk = Buffer.alloc(256 * 1024, 0x61);',
'process.stdout.write(chunk, () => process.exit(0));',
].join(''),
],
},
}),
durableContext(root, outputFilePath, undefined, {
maximumBytes,
logArtifactId: LOG_ARTIFACT_ID,
}),
);
const result = await handle.completion;
assert.equal(result.outcome, 'succeeded');
assert.equal(result.exitCode, 0);
assert.equal((await fs.stat(outputFilePath)).size, maximumBytes);
const truncation = await new LocalArtifactTruncationFactStore(
outputDirectory,
).read(LOG_ARTIFACT_ID);
assert.deepEqual(
{
schemaVersion: truncation.schemaVersion,
runId: truncation.runId,
attemptId: truncation.attemptId,
logArtifactId: truncation.logArtifactId,
maximumBytes: truncation.maximumBytes,
quotaReached: truncation.quotaReached,
},
{
schemaVersion: 1,
runId: RUN_ID,
attemptId: ATTEMPT_ID,
logArtifactId: LOG_ARTIFACT_ID,
maximumBytes,
quotaReached: true,
},
);
assert.ok(Number.isSafeInteger(truncation.observedAtMs));
assert.deepEqual(
(await fs.readdir(outputShard)).filter((name) => name.endsWith('.fifo')),
[],
);
assert.deepEqual(
(await fs.readdir(outputShard)).filter((name) => name.endsWith('.tmp')),
[],
);
assert.equal(
(
await waitForReceipt(
new CompletionReceiptFileStore(path.join(root, 'receipts')),
ATTEMPT_ID,
)
).exitCode,
0,
);
},
);
test('publishes a negative truncation fact when output stays below quota', async () => {
const root = await temporaryRoot();
const outputDirectory = path.join(root, 'logs');
const outputFilePath = path.join(
outputDirectory,
LOG_ARTIFACT_ID.slice(6, 8),
`${LOG_ARTIFACT_ID}.log`,
);
const executor = new LocalProcessExecutor({
durableLauncherPath: LAUNCHER_PATH,
});
const handle = await executor.start(
specification(),
durableContext(root, outputFilePath, undefined, {
maximumBytes: 64 * 1024,
logArtifactId: LOG_ARTIFACT_ID,
}),
);
assert.equal((await handle.completion).exitCode, 0);
const fact = await new LocalArtifactTruncationFactStore(outputDirectory).read(
LOG_ARTIFACT_ID,
);
assert.equal(fact.quotaReached, false);
assert.equal(fact.maximumBytes, 64 * 1024);
});
test('never overwrites an already published truncation fact', async () => {
const root = await temporaryRoot();
const outputDirectory = path.join(root, 'logs');
const outputShard = path.join(outputDirectory, LOG_ARTIFACT_ID.slice(6, 8));
const outputFilePath = path.join(outputShard, `${LOG_ARTIFACT_ID}.log`);
await fs.mkdir(outputShard, { recursive: true, mode: 0o700 });
const existing = {
schemaVersion: 1,
runId: RUN_ID,
attemptId: ATTEMPT_ID,
logArtifactId: LOG_ARTIFACT_ID,
maximumBytes: 64 * 1024,
quotaReached: false,
observedAtMs: 1,
};
await fs.writeFile(
path.join(
outputShard,
localArtifactTruncationFactFileName(LOG_ARTIFACT_ID),
),
encodeLocalArtifactTruncationFact(existing),
{ mode: 0o600 },
);
const executor = new LocalProcessExecutor({
durableLauncherPath: LAUNCHER_PATH,
});
const handle = await executor.start(
specification({
command: {
kind: 'argv',
file: process.execPath,
args: ['-e', 'process.stdout.write(Buffer.alloc(256 * 1024, 0x61))'],
},
}),
durableContext(root, outputFilePath, undefined, {
maximumBytes: 64 * 1024,
logArtifactId: LOG_ARTIFACT_ID,
}),
);
assert.equal((await handle.completion).exitCode, 0);
assert.deepEqual(
await new LocalArtifactTruncationFactStore(outputDirectory).read(
LOG_ARTIFACT_ID,
),
existing,
);
});
test(
'keeps the launcher alive through TERM until its child exits',
{ timeout: 10_000 },
async () => {
const root = await temporaryRoot();
const receiptRoot = path.join(root, 'receipts');
const outputFilePath = path.join(root, 'cancelled.log');
const executor = new LocalProcessExecutor({
durableLauncherPath: LAUNCHER_PATH,
});
const handle = await executor.start(
specification({
command: {
kind: 'argv',
file: process.execPath,
args: [
'-e',
"process.on('SIGTERM', () => setTimeout(() => process.exit(23), 100)); process.stdout.write('ready'); setInterval(() => undefined, 1000)",
],
},
terminationGraceMs: 1_000,
}),
durableContext(root, outputFilePath),
);
await waitForFileContent(outputFilePath, 'ready');
const stopped = await executor.stop(handle, {
kind: 'user',
requestedAtMs: Date.now(),
});
const result = await handle.completion;
assert.equal(stopped.killSignalSent, false);
assert.equal(result.outcome, 'cancelled');
assert.equal(result.exitCode, 23);
assert.equal(
(
await waitForReceipt(
new CompletionReceiptFileStore(receiptRoot),
ATTEMPT_ID,
)
).exitCode,
23,
);
},
);
test(
'never overwrites an already published completion receipt',
{ timeout: 10_000 },
async () => {
const root = await temporaryRoot();
const receiptRoot = path.join(root, 'receipts');
const store = new CompletionReceiptFileStore(receiptRoot);
const original = {
schemaVersion: 1,
runId: RUN_ID,
attemptId: ATTEMPT_ID,
callbackSequence: 1,
token: 'b'.repeat(43),
startedAtMs: 1_750_000_000_000,
finishedAtMs: 1_750_000_000_100,
exitCode: 19,
};
await store.publish(original);
const executor = new LocalProcessExecutor({
durableLauncherPath: LAUNCHER_PATH,
});
const handle = await executor.start(
specification(),
durableContext(root, path.join(root, 'attempt.log')),
);
assert.equal((await handle.completion).outcome, 'succeeded');
assert.deepEqual(await store.read(ATTEMPT_ID), original);
},
);
test(
'preserves the user exit code when receipt storage is unavailable',
{ timeout: 10_000 },
async () => {
const root = await temporaryRoot();
const blocked = path.join(root, 'blocked');
await fs.mkdir(blocked, { mode: 0o500 });
const target = path.join(blocked, `${ATTEMPT_ID}.json`);
const temporary = path.join(
blocked,
`.${ATTEMPT_ID}.${'a'.repeat(32)}.tmp`,
);
const child = spawn(
'/bin/sh',
[LAUNCHER_PATH, 'argv', '/bin/sh', '-c', 'exit 17'],
{
cwd: REPOSITORY_ROOT,
env: {
PATH: process.env.PATH,
QL3_RECEIPT_RUN_ID: RUN_ID,
QL3_RECEIPT_ATTEMPT_ID: ATTEMPT_ID,
QL3_RECEIPT_CALLBACK_SEQUENCE: '1',
QL3_RECEIPT_CALLBACK_TOKEN: CALLBACK_TOKEN,
QL3_RECEIPT_STARTED_AT_MS: String(Date.now()),
QL3_RECEIPT_TARGET: target,
QL3_RECEIPT_TEMPORARY: temporary,
},
stdio: 'ignore',
},
);
assert.deepEqual(await waitForExit(child), { code: 17, signal: null });
await assert.rejects(fs.lstat(target), /ENOENT/);
},
);
test(
'keeps appending output and publishes completion after the parent exits',
{ timeout: 15_000 },
async () => {
const root = await temporaryRoot();
const outputFilePath = path.join(root, 'survives-parent.log');
const receiptRoot = path.join(root, 'receipts');
const durableOutputModule = path.join(
REPOSITORY_ROOT,
'back/runtime/adapters/local-process/durableLocalProcessOutput',
);
const executorModule = path.join(
REPOSITORY_ROOT,
'back/runtime/adapters/local-process/localProcessExecutor',
);
const childProgram = [
"process.stdout.write('before-parent-exit\\n');",
"setTimeout(() => { process.stdout.write('after-parent-exit\\n'); process.exit(0); }, 300);",
].join('');
const controller = `
require('ts-node/register/transpile-only');
const { enableDurableLocalProcessOutput } = require(${JSON.stringify(
durableOutputModule,
)});
const { LocalProcessExecutor } = require(${JSON.stringify(
executorModule,
)});
const executor = new LocalProcessExecutor({ durableLauncherPath: ${JSON.stringify(
LAUNCHER_PATH,
)} });
executor.start(${JSON.stringify(
specification({
command: {
kind: 'argv',
file: process.execPath,
args: ['-e', childProgram],
},
}),
)}, {
environment: {},
completionCallback: { token: ${JSON.stringify(
CALLBACK_TOKEN,
)}, callbackSequence: 1 },
output: enableDurableLocalProcessOutput({ async write() {} }, {
outputFilePath: ${JSON.stringify(outputFilePath)},
completionReceiptRoot: ${JSON.stringify(receiptRoot)},
}),
}).then(() => process.exit(0), () => process.exit(1));
`;
const controllerProcess = spawn(process.execPath, ['-e', controller], {
cwd: REPOSITORY_ROOT,
stdio: 'ignore',
});
assert.deepEqual(await waitForExit(controllerProcess), {
code: 0,
signal: null,
});
const receipt = await waitForReceipt(
new CompletionReceiptFileStore(receiptRoot),
ATTEMPT_ID,
);
assert.equal(receipt.exitCode, 0);
assert.equal(
await fs.readFile(outputFilePath, 'utf8'),
'before-parent-exit\nafter-parent-exit\n',
);
},
);
+91
View File
@@ -0,0 +1,91 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
assertExecutionSpec,
cloneExecutionSpec,
} = require('../../back/runtime/domain/executionSpec');
const {
createExecutionSpecDigest,
} = require('../../back/runtime/domain/runDispatchOffer');
function spec(overrides = {}) {
return {
runId: 'run-1',
attemptId: 'attempt-1',
projectId: 'default',
taskId: 'task-1',
taskRevision: 'v1',
command: { kind: 'argv', file: '/usr/bin/node', args: ['task.js'] },
environmentPolicy: 'isolated',
terminationGraceMs: 5_000,
resourcePolicy: {
memoryBytes: { value: 1024, enforcement: 'best_effort' },
networkIsolation: 'best_effort',
},
...overrides,
};
}
test('validates and deep-clones a portable ExecutionSpec', () => {
const source = spec();
const cloned = cloneExecutionSpec(source);
source.command.args[0] = 'changed.js';
source.resourcePolicy.memoryBytes.value = 2048;
assert.deepEqual(cloned.command.args, ['task.js']);
assert.equal(cloned.resourcePolicy.memoryBytes.value, 1024);
});
test('drops unknown data when cloning an ExecutionSpec for an offer', () => {
const cloned = cloneExecutionSpec({
...spec(),
internalSecret: 'do-not-forward',
resourcePolicy: {
...spec().resourcePolicy,
adapterPrivateField: 'do-not-forward',
},
});
assert.equal(Object.hasOwn(cloned, 'internalSecret'), false);
assert.equal(
Object.hasOwn(cloned.resourcePolicy, 'adapterPrivateField'),
false,
);
});
test('digests only the canonical ExecutionSpec payload', () => {
const reference = spec();
assert.equal(
createExecutionSpecDigest(reference),
createExecutionSpecDigest({ ...reference, internalSecret: 'ignored' }),
);
assert.notEqual(
createExecutionSpecDigest(reference),
createExecutionSpecDigest({
...reference,
command: { kind: 'argv', file: '/usr/bin/node', args: ['changed.js'] },
}),
);
});
test('rejects control characters, relative paths, and unsafe numeric limits', () => {
assert.throws(
() => assertExecutionSpec(spec({ taskId: 'task\n2' })),
/control characters/,
);
assert.throws(
() => assertExecutionSpec(spec({ workingDirectory: 'relative/path' })),
/absolute path/,
);
assert.throws(
() =>
assertExecutionSpec(
spec({
resourcePolicy: {
memoryBytes: { value: 0, enforcement: 'required' },
},
}),
),
/positive safe integer/,
);
});
+138
View File
@@ -0,0 +1,138 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
bootstrapHeadlessWorkerRuntime,
} = require('../../back/runtime/application/headlessWorkerRuntime');
function heartbeat(calls, overrides = {}) {
return {
currentSession() {
return undefined;
},
async start() {
calls.push('heartbeat.start');
return true;
},
async drain() {
calls.push('heartbeat.drain');
return undefined;
},
async stop() {
calls.push('heartbeat.stop');
return 'drained';
},
...overrides,
};
}
test('is default-off without touching Worker dependencies', async () => {
const calls = [];
const result = await bootstrapHeadlessWorkerRuntime({
profile: 'worker',
heartbeat: heartbeat(calls),
executions: {
async drain() {
calls.push('executions.drain');
return 'drained';
},
},
});
assert.deepEqual(result, { status: 'disabled' });
assert.deepEqual(calls, []);
});
test('refuses to graft the headless topology onto a control-plane profile', async () => {
const calls = [];
await assert.rejects(
bootstrapHeadlessWorkerRuntime({
enabled: true,
profile: 'cluster-control',
heartbeat: heartbeat(calls),
executions: {
async drain() {
return 'drained';
},
},
}),
/cannot activate the headless Worker runtime/,
);
assert.deepEqual(calls, []);
});
test('advertises drain before waiting for tasks and disconnecting', async () => {
const calls = [];
const result = await bootstrapHeadlessWorkerRuntime({
enabled: true,
profile: 'worker',
heartbeat: heartbeat(calls),
executions: {
async drain() {
calls.push('executions.drain');
return 'drained';
},
},
});
assert.equal(result.status, 'active');
assert.equal(await result.runtime.drainAndStop(), 'stopped');
assert.deepEqual(calls, [
'heartbeat.start',
'heartbeat.drain',
'executions.drain',
'heartbeat.stop',
]);
});
test('keeps a draining session alive when task shutdown reaches its bound', async () => {
const calls = [];
const result = await bootstrapHeadlessWorkerRuntime({
enabled: true,
profile: 'worker',
heartbeat: heartbeat(calls),
executions: {
async drain() {
calls.push('executions.drain');
return 'timed_out';
},
},
});
assert.equal(result.status, 'active');
assert.equal(await result.runtime.drainAndStop(), 'executions_timed_out');
assert.deepEqual(calls, [
'heartbeat.start',
'heartbeat.drain',
'executions.drain',
]);
});
test('reports a control-plane disconnect failure without claiming shutdown', async () => {
const calls = [];
const result = await bootstrapHeadlessWorkerRuntime({
enabled: true,
profile: 'worker',
heartbeat: heartbeat(calls, {
async stop() {
calls.push('heartbeat.stop');
return 'disconnect_failed';
},
}),
executions: {
async drain() {
calls.push('executions.drain');
return 'drained';
},
},
});
assert.equal(result.status, 'active');
assert.equal(
await result.runtime.drainAndStop(),
'heartbeat_disconnect_failed',
);
assert.deepEqual(calls, [
'heartbeat.start',
'heartbeat.drain',
'executions.drain',
'heartbeat.stop',
]);
});
+111
View File
@@ -0,0 +1,111 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
DEFAULT_LEGACY_TERMINATION_GRACE_MS,
buildLegacyCronExecutionSpec,
} = require('../../back/runtime/adapters/legacy/legacyCronExecutionSpec');
const {
InvalidExecutionSpecError,
} = require('../../back/runtime/domain/executorErrors');
function createInput(overrides = {}) {
return {
runId: '019f70f0-0000-7000-8000-000000000001',
attemptId: '019f70f0-0000-7000-8000-000000000002',
projectId: 'default',
taskRevision: 'legacy-revision-1',
cron: {
id: 7,
command: 'demo/script.js now',
},
realTime: false,
...overrides,
};
}
test('builds the minimal legacy task.sh shell contract', () => {
assert.deepEqual(buildLegacyCronExecutionSpec(createInput()), {
runId: '019f70f0-0000-7000-8000-000000000001',
attemptId: '019f70f0-0000-7000-8000-000000000002',
projectId: 'default',
taskId: 'legacy-cron:7',
taskRevision: 'legacy-revision-1',
command: {
kind: 'shell',
command: "real_time='false' no_tee='true' ID='7' task demo/script.js now",
shell: '/bin/bash',
},
environmentPolicy: 'inherit',
terminationGraceMs: DEFAULT_LEGACY_TERMINATION_GRACE_MS,
});
});
test('preserves existing task/ql commands instead of adding a second prefix', () => {
const task = buildLegacyCronExecutionSpec(
createInput({ cron: { id: 8, command: ' task script.py now ' } }),
);
const ql = buildLegacyCronExecutionSpec(
createInput({ cron: { id: 9, command: 'ql update' } }),
);
assert.match(task.command.command, / ID='8' task script\.py now$/);
assert.match(ql.command.command, / ID='9' ql update$/);
});
test('quotes paths, hook commands, and log values without changing shell boundaries', () => {
const spec = buildLegacyCronExecutionSpec(
createInput({
realTime: true,
realLogPath: "folder with space/run's.log",
noDelay: true,
timeoutMs: 60_000,
terminationGraceMs: 2_000,
resourcePolicy: {
memoryBytes: {
value: 128 * 1024 * 1024,
enforcement: 'best_effort',
},
},
cron: {
id: 10,
command: "scripts/job.py now -- 'user value'",
taskBefore: "echo 'before'\n echo second",
taskAfter: 'echo after;\n echo done',
workDirectory: "/data/project's worktree",
logName: 'custom log',
},
}),
);
assert.equal(
spec.command.command,
"real_log_path='folder with space/run'\\''s.log' no_delay='true' real_time='true' no_tee='true' ID='10' log_name='custom log' task_before='echo '\\''before'\\''; echo second' task_after='echo after; echo done' work_dir='/data/project'\\''s worktree' task scripts/job.py now -- 'user value'",
);
assert.equal(spec.timeoutMs, 60_000);
assert.equal(spec.terminationGraceMs, 2_000);
assert.deepEqual(spec.resourcePolicy, {
memoryBytes: {
value: 128 * 1024 * 1024,
enforcement: 'best_effort',
},
});
});
test('rejects invalid legacy ids and empty commands before execution', () => {
assert.throws(
() =>
buildLegacyCronExecutionSpec(
createInput({ cron: { id: 0, command: 'script.py' } }),
),
InvalidExecutionSpecError,
);
assert.throws(
() =>
buildLegacyCronExecutionSpec(
createInput({ cron: { id: 1, command: ' ' } }),
),
InvalidExecutionSpecError,
);
});
+160
View File
@@ -0,0 +1,160 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
installLegacyExecutionObserver,
observeLegacyCancellation,
observeLegacyExecution,
observeLegacyExecutionCallback,
shadowBridgeFailureSnapshot,
} = require('../../back/runtime/compatibility/legacyExecutionBridge');
function acceptedFact(origin = 'manual') {
return {
origin,
projectId: 'default',
taskId: 'legacy-cron:1',
taskRevision: 'sha256:revision',
legacyCronId: 1,
triggerType: origin,
acceptedAtMs: 1_750_000_000_000,
};
}
test('routes only installed origins and preserves observation fact order', () => {
const facts = [];
const observer = {
begin(fact) {
facts.push(['accepted', fact]);
return {
spawned: (value) => facts.push(['spawned', value]),
running: (value) => facts.push(['running', value]),
startFailed: (value) => facts.push(['start_failed', value]),
exited: (value) => facts.push(['exited', value]),
cancelled: (value) => facts.push(['cancelled', value]),
};
},
};
const restore = installLegacyExecutionObserver(observer, ['manual']);
try {
const observation = observeLegacyExecution('manual', () => acceptedFact());
observation.spawned({ atMs: 1, pid: 10 });
observation.running({ atMs: 2 });
observation.exited({ atMs: 3, exitCode: 0 });
let ignoredFactCreated = false;
const ignored = observeLegacyExecution('boot', () => {
ignoredFactCreated = true;
return acceptedFact('boot');
});
assert.equal(ignored, undefined);
assert.equal(ignoredFactCreated, false);
} finally {
restore();
}
assert.deepEqual(
facts.map(([type]) => type),
['accepted', 'spawned', 'running', 'exited'],
);
});
test('turns synchronous observer initialization failures into a no-op', () => {
const before = shadowBridgeFailureSnapshot()['manual:begin:failed'] ?? 0;
const restore = installLegacyExecutionObserver(
{
begin() {
throw new Error('must not reach legacy execution');
},
},
['manual'],
);
try {
const observation = observeLegacyExecution('manual', () => acceptedFact());
assert.doesNotThrow(() => {
observation.spawned({ atMs: 1 });
observation.running({ atMs: 2 });
observation.exited({ atMs: 3, exitCode: 0 });
});
} finally {
restore();
}
const after = shadowBridgeFailureSnapshot()['manual:begin:failed'];
assert.equal(after, before + 1);
});
test('routes local callback and cancellation facts without persistent lookup', () => {
const facts = [];
const restore = installLegacyExecutionObserver(
{
begin() {
return {
spawned: (fact) => facts.push(['spawned', fact]),
running: (fact) => facts.push(['running', fact]),
startFailed: (fact) => facts.push(['start_failed', fact]),
exited: (fact) => facts.push(['exited', fact]),
cancelled: (fact) => facts.push(['cancelled', fact]),
};
},
},
['manual'],
);
try {
const observation = observeLegacyExecution('manual', () => ({
...acceptedFact(),
legacyCronId: 77,
}));
observation.spawned({ atMs: 1, pid: 700 });
observeLegacyExecutionCallback({
legacyCronId: 77,
pid: 700,
atMs: 2,
phase: 'running',
});
observeLegacyCancellation({
legacyCronId: 77,
pid: 700,
atMs: 3,
scope: 'one',
reason: 'user',
});
} finally {
restore();
}
assert.deepEqual(
facts.map(([operation]) => operation),
['spawned', 'spawned', 'running', 'cancelled'],
);
});
test('keeps invalid shadow origin configuration fail-open when logging fails', () => {
const Logger = require('../../back/loaders/logger').default;
const previousWarn = Logger.warn;
const previousOrigins = process.env.QL3_SHADOW_ORIGINS;
let factCreated = false;
process.env.QL3_SHADOW_ORIGINS = 'secret-invalid-origin';
Logger.warn = () => {
throw new Error('logger unavailable');
};
try {
let observation;
assert.doesNotThrow(() => {
observation = observeLegacyExecution('manual', () => {
factCreated = true;
return acceptedFact();
});
});
assert.equal(observation, undefined);
assert.equal(factCreated, false);
} finally {
Logger.warn = previousWarn;
if (previousOrigins === undefined) {
delete process.env.QL3_SHADOW_ORIGINS;
} else {
process.env.QL3_SHADOW_ORIGINS = previousOrigins;
}
}
});
+200
View File
@@ -0,0 +1,200 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
LegacyExecutionRegistry,
} = require('../../back/runtime/compatibility/legacyExecutionRegistry');
function acceptedFact(legacyCronId) {
return {
origin: 'manual',
projectId: 'default',
taskId: `legacy-cron:${legacyCronId}`,
taskRevision: 'sha256:revision',
legacyCronId,
triggerType: 'manual',
acceptedAtMs: 100,
};
}
function recorder(label, calls) {
return {
spawned: (fact) => calls.push([label, 'spawned', fact]),
running: (fact) => calls.push([label, 'running', fact]),
startFailed: (fact) => calls.push([label, 'start_failed', fact]),
exited: (fact) => calls.push([label, 'exited', fact]),
cancelled: (fact) => calls.push([label, 'cancelled', fact]),
};
}
test('correlates local callbacks by log, then pid, and removes terminal entries', () => {
const calls = [];
const registry = new LegacyExecutionRegistry();
const first = registry.register(acceptedFact(7), recorder('first', calls));
const second = registry.register(acceptedFact(7), recorder('second', calls));
first.spawned({ atMs: 101, pid: 11, logArtifactId: 'log-first' });
second.spawned({ atMs: 101, pid: 22, logArtifactId: 'log-second' });
assert.equal(
registry.callback({
legacyCronId: 7,
pid: 999,
logArtifactId: 'log-second',
atMs: 102,
phase: 'running',
}),
1,
);
assert.equal(
registry.callback({
legacyCronId: 7,
pid: 11,
atMs: 103,
phase: 'finished',
exitCode: 0,
}),
1,
);
assert.equal(registry.size(), 1);
assert.equal(
registry.cancel({
legacyCronId: 7,
atMs: 104,
scope: 'all',
reason: 'user',
}),
1,
);
assert.equal(registry.size(), 0);
assert.deepEqual(
calls.map(([label, operation]) => [label, operation]),
[
['first', 'spawned'],
['second', 'spawned'],
['second', 'spawned'],
['second', 'running'],
['first', 'exited'],
['second', 'cancelled'],
],
);
});
test('refuses ambiguous one-instance correlation and bounds local memory', () => {
const calls = [];
let overflows = 0;
const registry = new LegacyExecutionRegistry({
maxEntries: 1,
onOverflow: () => {
overflows += 1;
},
});
registry.register(acceptedFact(8), recorder('tracked', calls));
const untracked = registry.register(
acceptedFact(8),
recorder('untracked', calls),
);
assert.equal(overflows, 1);
assert.equal(registry.size(), 1);
untracked.running({ atMs: 200 });
assert.equal(
registry.callback({
legacyCronId: 8,
atMs: 201,
phase: 'finished',
}),
1,
);
assert.equal(registry.size(), 0);
assert.deepEqual(
calls.map(([label, operation]) => [label, operation]),
[
['untracked', 'running'],
['tracked', 'exited'],
],
);
const ambiguous = new LegacyExecutionRegistry();
ambiguous.register(acceptedFact(9), recorder('a', calls));
ambiguous.register(acceptedFact(9), recorder('b', calls));
assert.equal(
ambiguous.callback({
legacyCronId: 9,
atMs: 300,
phase: 'running',
}),
0,
);
const conflicting = new LegacyExecutionRegistry();
const left = conflicting.register(acceptedFact(12), recorder('left', calls));
const right = conflicting.register(
acceptedFact(12),
recorder('right', calls),
);
left.spawned({ atMs: 1, pid: 1, logArtifactId: 'left-log' });
right.spawned({ atMs: 1, pid: 2, logArtifactId: 'right-log' });
assert.equal(
conflicting.callback({
legacyCronId: 12,
pid: 1,
logArtifactId: 'right-log',
atMs: 2,
phase: 'finished',
}),
0,
);
});
test('cleans up entries after start failures and direct terminal observations', () => {
const calls = [];
const registry = new LegacyExecutionRegistry();
const failed = registry.register(acceptedFact(10), recorder('failed', calls));
failed.startFailed({ atMs: 2, errorCode: 'SPAWN_FAILED' });
const exited = registry.register(acceptedFact(11), recorder('exited', calls));
exited.exited({ atMs: 3, exitCode: 1 });
assert.equal(registry.size(), 0);
});
test('swallows local observer failures and reports them out of band', () => {
let failures = 0;
const registry = new LegacyExecutionRegistry({
onDispatchFailure: () => {
failures += 1;
},
});
const throwing = {
spawned() {
throw new Error('spawned failed');
},
running() {
throw new Error('running failed');
},
startFailed() {
throw new Error('start failed');
},
exited() {
throw new Error('exit failed');
},
cancelled() {
throw new Error('cancel failed');
},
};
const observation = registry.register(acceptedFact(13), throwing);
assert.doesNotThrow(() => {
observation.spawned({ atMs: 1, pid: 13 });
observation.running({ atMs: 2 });
registry.cancel({
legacyCronId: 13,
pid: 13,
atMs: 3,
scope: 'one',
reason: 'user',
});
});
assert.equal(failures, 3);
assert.equal(registry.size(), 0);
});
+58
View File
@@ -0,0 +1,58 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
createLegacyLogOutputRef,
LEGACY_LOG_OUTPUT_REF_PREFIX,
MAX_LEGACY_LOG_PATH_BYTES,
parseLegacyLogOutputRef,
} = require('../../back/runtime/compatibility/legacyLogOutputRef');
test('round-trips a bounded relative legacy log path', () => {
const logPath = 'task-name/2026-07-18-12-00-00.log';
const outputRef = createLegacyLogOutputRef(logPath);
assert.match(outputRef, /^legacy-log-v1\.[A-Za-z0-9_-]+$/);
assert.equal(parseLegacyLogOutputRef(outputRef), logPath);
});
test('normalizes producer paths but requires a canonical encoded reference', () => {
const canonical = createLegacyLogOutputRef('task//nested/./run.log');
assert.equal(parseLegacyLogOutputRef(canonical), 'task/nested/run.log');
const nonCanonical =
LEGACY_LOG_OUTPUT_REF_PREFIX +
Buffer.from('task//run.log').toString('base64url');
assert.equal(parseLegacyLogOutputRef(nonCanonical), null);
});
test('rejects absolute, traversing, Windows and NUL-containing paths', () => {
for (const value of [
'/var/log/secret.log',
'../secret.log',
'task/../../secret.log',
'C:\\secret.log',
'task\\secret.log',
'task/' + String.fromCharCode(0) + 'secret.log',
]) {
assert.throws(() => createLegacyLogOutputRef(value));
}
});
test('rejects oversized, malformed and non-canonical references', () => {
assert.throws(() =>
createLegacyLogOutputRef('a'.repeat(MAX_LEGACY_LOG_PATH_BYTES + 1)),
);
assert.equal(parseLegacyLogOutputRef('legacy-log-v1.'), null);
assert.equal(parseLegacyLogOutputRef('legacy-log-v1.***'), null);
assert.equal(parseLegacyLogOutputRef('legacy-log-v2.dGFzay5sb2c'), null);
assert.equal(
parseLegacyLogOutputRef(LEGACY_LOG_OUTPUT_REF_PREFIX + 'Zh'),
null,
);
assert.equal(
parseLegacyLogOutputRef(LEGACY_LOG_OUTPUT_REF_PREFIX + 'a'.repeat(513)),
null,
);
});
@@ -0,0 +1,416 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { createHash } = require('node:crypto');
const { test } = require('node:test');
const jwt = require('jsonwebtoken');
const { Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const {
IDENTITY_AUTHENTICATION_BINDING_CURRENT_INDEX,
IDENTITY_AUTHENTICATION_BINDING_SUBJECT_INDEX,
IDENTITY_AUTHENTICATION_BINDING_TABLE,
IDENTITY_SUBJECT_STATUS_INDEX,
IDENTITY_SUBJECT_TABLE,
identityDirectoryMigration,
} = require('../../back/migrations/0019-identity-directory');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacyAuthInfoSessionSource,
LegacyPanelSessionUnavailableError,
MAX_LEGACY_PANEL_TOKENS_PER_PLATFORM,
} = require('../../back/runtime/adapters/authentication/legacyAuthInfoSessionSource');
const {
LegacySequelizeIdentityDirectoryRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/identityDirectoryRepository');
const {
LegacyPanelAuthenticationRejectedError,
LegacyPanelAuthenticationService,
LegacyPanelAuthenticationUnavailableError,
} = require('../../back/runtime/application/legacyPanelAuthenticationService');
const {
IdentityDirectoryUnavailableError,
LEGACY_PANEL_IDENTITY_PROVIDER,
LEGACY_PANEL_PROVIDER_SUBJECT,
LEGACY_PRIMARY_USER_SUBJECT_ID,
} = require('../../back/runtime/domain/identityDirectory');
const SECRET = 'test-only-legacy-jwt-secret';
const ISSUED_AT_SECONDS = 100;
const EXPIRES_AT_SECONDS = 200;
const NOW_MS = 150_000;
function signToken(
payload = {
data: 'legacy-session-random-data',
iat: ISSUED_AT_SECONDS,
exp: EXPIRES_AT_SECONDS,
},
algorithm = 'HS384',
secret = SECRET,
) {
return jwt.sign(payload, secret, { algorithm });
}
async function setup(t) {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
t.after(() => database.close());
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations: [identityDirectoryMigration],
logger: { info() {} },
});
return {
database,
directory: new LegacySequelizeIdentityDirectoryRepository(database),
};
}
function sessionSource(snapshot) {
return new LegacyAuthInfoSessionSource(async () => snapshot);
}
function authentication(directory, snapshot, secret = SECRET) {
return new LegacyPanelAuthenticationService(
directory,
sessionSource(snapshot),
secret,
);
}
test('migration creates a stable singleton identity without copying legacy credentials', async (t) => {
const { database, directory } = await setup(t);
const subjects = await database
.getQueryInterface()
.select(null, IDENTITY_SUBJECT_TABLE);
const bindings = await database
.getQueryInterface()
.select(null, IDENTITY_AUTHENTICATION_BINDING_TABLE);
assert.deepEqual(subjects, [
{
id: LEGACY_PRIMARY_USER_SUBJECT_ID,
type: 'user',
status: 'active',
version: 1,
created_at_ms: 0,
updated_at_ms: 0,
},
]);
assert.deepEqual(bindings, [
{
provider: LEGACY_PANEL_IDENTITY_PROVIDER,
provider_subject: LEGACY_PANEL_PROVIDER_SUBJECT,
version: 1,
state: 'active',
subject_id: LEGACY_PRIMARY_USER_SUBJECT_ID,
created_at_ms: 0,
},
]);
const persisted = JSON.stringify({ subjects, bindings });
assert.equal(persisted.includes('username'), false);
assert.equal(persisted.includes('password'), false);
assert.equal(persisted.includes('token'), false);
assert.deepEqual(
await directory.resolveAuthenticationSubject(
LEGACY_PANEL_IDENTITY_PROVIDER,
LEGACY_PANEL_PROVIDER_SUBJECT,
),
{ type: 'user', id: LEGACY_PRIMARY_USER_SUBJECT_ID },
);
const subjectIndexes = new Set(
(await database.getQueryInterface().showIndex(IDENTITY_SUBJECT_TABLE)).map(
(index) => index.name,
),
);
const bindingIndexes = new Set(
(
await database
.getQueryInterface()
.showIndex(IDENTITY_AUTHENTICATION_BINDING_TABLE)
).map((index) => index.name),
);
assert.ok(subjectIndexes.has(IDENTITY_SUBJECT_STATUS_INDEX));
assert.ok(bindingIndexes.has(IDENTITY_AUTHENTICATION_BINDING_CURRENT_INDEX));
assert.ok(bindingIndexes.has(IDENTITY_AUTHENTICATION_BINDING_SUBJECT_INDEX));
});
test('authenticates a current HS384 legacy session as one stable single-factor user', async (t) => {
const { directory } = await setup(t);
const token = signToken();
const service = authentication(directory, {
token: '',
tokens: {
desktop: [
{
value: token,
timestamp: 123,
platform: 'desktop',
},
],
},
username: 'a-display-name-that-may-change',
twoFactorActivated: true,
});
assert.deepEqual(
await service.authenticate({ token, platform: 'desktop', nowMs: NOW_MS }),
{
subject: { type: 'user', id: LEGACY_PRIMARY_USER_SUBJECT_ID },
authenticationId: `legacy_panel:${createHash('sha256')
.update(token, 'utf8')
.digest('hex')}`,
authenticatedAtMs: ISSUED_AT_SECONDS * 1000,
expiresAtMs: EXPIRES_AT_SECONDS * 1000,
assurance: 'single_factor',
},
);
});
test('supports only the bounded legacy primary, string and TokenInfo list formats', async () => {
const token = signToken();
assert.equal(await sessionSource({ token }).isActive(token, 'mobile'), true);
assert.equal(
await sessionSource({ tokens: { desktop: token } }).isActive(
token,
'desktop',
),
true,
);
assert.equal(
await sessionSource({
tokens: { desktop: [{ value: token }] },
}).isActive(token, 'desktop'),
true,
);
assert.equal(
await sessionSource({ tokens: { mobile: [{ value: token }] } }).isActive(
token,
'desktop',
),
false,
);
await assert.rejects(
sessionSource({
tokens: {
desktop: Array.from(
{ length: MAX_LEGACY_PANEL_TOKENS_PER_PLATFORM + 1 },
() => ({ value: token }),
),
},
}).isActive(token, 'desktop'),
LegacyPanelSessionUnavailableError,
);
await assert.rejects(
sessionSource({ tokens: { desktop: [{}] } }).isActive(token, 'desktop'),
LegacyPanelSessionUnavailableError,
);
});
test('rejects logout, platform drift, expiry, wrong signature and wrong algorithm', async (t) => {
const { directory } = await setup(t);
const token = signToken();
for (const [service, request] of [
[authentication(directory, { tokens: {} }), {}],
[
authentication(directory, {
tokens: { mobile: [{ value: token }] },
}),
{},
],
[
authentication(directory, {
tokens: { desktop: [{ value: token }] },
}),
{ nowMs: EXPIRES_AT_SECONDS * 1000 },
],
[
authentication(
directory,
{ tokens: { desktop: [{ value: token }] } },
'different-secret',
),
{},
],
[
authentication(directory, {
tokens: { desktop: [{ value: signToken(undefined, 'HS256') }] },
}),
{ token: signToken(undefined, 'HS256') },
],
]) {
await assert.rejects(
service.authenticate({
token,
platform: 'desktop',
nowMs: NOW_MS,
...request,
}),
LegacyPanelAuthenticationRejectedError,
);
}
});
test('rejects extensible JWTs and request-supplied subjects before identity lookup', async (t) => {
const { directory } = await setup(t);
let sessionReads = 0;
const extraPayloadToken = signToken({
data: 'legacy-session-random-data',
iat: ISSUED_AT_SECONDS,
exp: EXPIRES_AT_SECONDS,
subject: 'attacker',
});
const service = new LegacyPanelAuthenticationService(
directory,
{
async isActive() {
sessionReads += 1;
return true;
},
},
SECRET,
);
await assert.rejects(
service.authenticate({
token: extraPayloadToken,
platform: 'desktop',
nowMs: NOW_MS,
}),
LegacyPanelAuthenticationRejectedError,
);
assert.equal(sessionReads, 0);
const futureIssuedToken = signToken({
data: 'legacy-session-random-data',
iat: ISSUED_AT_SECONDS + 60,
exp: EXPIRES_AT_SECONDS,
});
await assert.rejects(
service.authenticate({
token: futureIssuedToken,
platform: 'desktop',
nowMs: NOW_MS,
}),
LegacyPanelAuthenticationRejectedError,
);
assert.equal(sessionReads, 0);
await assert.rejects(
service.authenticate({
token: signToken(),
platform: 'desktop',
nowMs: NOW_MS,
subject: { type: 'user', id: 'attacker' },
}),
/request shape is invalid/,
);
assert.equal(sessionReads, 0);
});
test('revocation and subject disablement remove legacy authentication authority', async (t) => {
const revoked = await setup(t);
await revoked.database
.getQueryInterface()
.bulkInsert(IDENTITY_AUTHENTICATION_BINDING_TABLE, [
{
provider: LEGACY_PANEL_IDENTITY_PROVIDER,
provider_subject: LEGACY_PANEL_PROVIDER_SUBJECT,
version: 2,
state: 'revoked',
subject_id: LEGACY_PRIMARY_USER_SUBJECT_ID,
created_at_ms: NOW_MS,
},
]);
assert.equal(
await revoked.directory.resolveAuthenticationSubject(
LEGACY_PANEL_IDENTITY_PROVIDER,
LEGACY_PANEL_PROVIDER_SUBJECT,
),
null,
);
const disabled = await setup(t);
await disabled.database
.getQueryInterface()
.bulkUpdate(
IDENTITY_SUBJECT_TABLE,
{ status: 'disabled', version: 2, updated_at_ms: NOW_MS },
{ id: LEGACY_PRIMARY_USER_SUBJECT_ID },
);
const token = signToken();
await assert.rejects(
authentication(disabled.directory, {
tokens: { desktop: [{ value: token }] },
}).authenticate({ token, platform: 'desktop', nowMs: NOW_MS }),
LegacyPanelAuthenticationRejectedError,
);
});
test('fails closed on corrupt identity storage and session source failures', async (t) => {
const { database, directory } = await setup(t);
await database.query('PRAGMA ignore_check_constraints = ON');
await database
.getQueryInterface()
.bulkUpdate(
IDENTITY_AUTHENTICATION_BINDING_TABLE,
{ state: 'corrupt' },
{ provider: LEGACY_PANEL_IDENTITY_PROVIDER },
);
await assert.rejects(
directory.resolveAuthenticationSubject(
LEGACY_PANEL_IDENTITY_PROVIDER,
LEGACY_PANEL_PROVIDER_SUBJECT,
),
IdentityDirectoryUnavailableError,
);
const orphaned = await setup(t);
await orphaned.database.query('PRAGMA foreign_keys = OFF');
await orphaned.database
.getQueryInterface()
.bulkDelete(IDENTITY_SUBJECT_TABLE, {
id: LEGACY_PRIMARY_USER_SUBJECT_ID,
});
await assert.rejects(
orphaned.directory.resolveAuthenticationSubject(
LEGACY_PANEL_IDENTITY_PROVIDER,
LEGACY_PANEL_PROVIDER_SUBJECT,
),
IdentityDirectoryUnavailableError,
);
const token = signToken();
const service = new LegacyPanelAuthenticationService(
directory,
{
async isActive() {
throw new Error(`must not leak ${token}`);
},
},
SECRET,
);
await assert.rejects(
service.authenticate({ token, platform: 'desktop', nowMs: NOW_MS }),
(error) => {
assert.ok(error instanceof LegacyPanelAuthenticationUnavailableError);
assert.equal(error.message.includes(token), false);
return true;
},
);
});
test('rejects non-SQLite identity directory repositories', () => {
assert.throws(
() =>
new LegacySequelizeIdentityDirectoryRepository({
getDialect() {
return 'postgres';
},
}),
/SQLite-only/,
);
});
+396
View File
@@ -0,0 +1,396 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { afterEach, test } = require('node:test');
const { Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const { runSchemaMigration } = require('../../back/migrations/0002-run-schema');
const {
runCancellationRequestMigration,
} = require('../../back/migrations/0004-run-cancellation-request');
const {
runAttemptDeadlineMigration,
} = require('../../back/migrations/0006-run-attempt-deadline');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacySequelizeRunRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/runRepository');
const {
LegacyShadowRunCorrelator,
} = require('../../back/runtime/application/legacyShadowRunCorrelator');
const {
LegacyShadowRunWriter,
} = require('../../back/runtime/application/legacyShadowRunWriter');
const databases = [];
let idSequence = 600;
let timeSequence = 1_750_000_100_000;
function nextId() {
idSequence += 1;
return `019f70f0-0000-7000-8000-${String(idSequence).padStart(12, '0')}`;
}
function nextTime() {
timeSequence += 10;
return timeSequence;
}
async function createRepository() {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
const migrationModel = defineSchemaMigrationModel(database);
await runMigrations({
database,
migrationModel,
migrations: [
runSchemaMigration,
runCancellationRequestMigration,
runAttemptDeadlineMigration,
],
logger: { info() {} },
});
databases.push(database);
return new LegacySequelizeRunRepository(database);
}
async function createActive(writer, overrides = {}) {
const atMs = nextTime();
const reference = await writer.accept({
origin: 'manual',
projectId: 'default',
taskId: 'legacy-cron:15',
taskRevision: 'sha256:revision',
legacyCronId: 15,
triggerType: 'manual',
acceptedAtMs: atMs,
...overrides,
});
await writer.spawned(reference, {
atMs: atMs + 1,
...(overrides.pid === undefined ? {} : { pid: overrides.pid }),
...(overrides.logArtifactId === undefined
? {}
: { logArtifactId: overrides.logArtifactId }),
});
await writer.running(reference, atMs + 2);
return { reference, atMs };
}
afterEach(async () => {
await Promise.all(databases.splice(0).map((database) => database.close()));
});
test('lists bounded active candidates by legacy cron and origin', async () => {
const repository = await createRepository();
const writer = new LegacyShadowRunWriter(repository, nextId);
await createActive(writer, { pid: 11, logArtifactId: 'log-a' });
await createActive(writer, { pid: 22, logArtifactId: 'log-b' });
await createActive(writer, {
origin: 'scheduled_node',
pid: 33,
logArtifactId: 'log-c',
});
const manual = await repository.listActiveByLegacyCron({
legacyCronId: 15,
origins: ['manual'],
});
assert.equal(manual.candidates.length, 2);
assert.equal(manual.truncated, false);
assert.deepEqual(
new Set(manual.candidates.map((candidate) => candidate.pid)),
new Set([11, 22]),
);
const bounded = await repository.listActiveByLegacyCron({
legacyCronId: 15,
origins: ['manual', 'scheduled_node'],
limit: 1,
});
assert.equal(bounded.candidates.length, 1);
assert.equal(bounded.truncated, true);
await assert.rejects(
repository.listActiveByLegacyCron({
legacyCronId: 0,
origins: ['manual'],
}),
RangeError,
);
});
test('correlates callbacks by log before pid and refuses ambiguity', async () => {
const repository = await createRepository();
const writer = new LegacyShadowRunWriter(repository, nextId);
const first = await createActive(writer, {
pid: 41,
logArtifactId: 'log-first',
});
const second = await createActive(writer, {
pid: 42,
logArtifactId: 'log-second',
});
const failures = [];
const correlator = new LegacyShadowRunCorrelator(repository, writer, {
failure: (failure) => failures.push(failure),
});
const ambiguous = await correlator.callback(
{
legacyCronId: 15,
atMs: nextTime(),
phase: 'running',
},
['manual'],
);
assert.equal(ambiguous.matched, 0);
assert.equal(failures[0].reason, 'ambiguous');
const conflicting = await correlator.callback(
{
legacyCronId: 15,
pid: 41,
logArtifactId: 'log-second',
atMs: nextTime(),
phase: 'finished',
exitCode: 0,
},
['manual'],
);
assert.equal(conflicting.matched, 0);
assert.equal(failures[1].reason, 'ambiguous');
const finished = await correlator.callback(
{
legacyCronId: 15,
pid: 999,
logArtifactId: 'log-second',
atMs: nextTime(),
phase: 'finished',
exitCode: 0,
},
['manual'],
);
assert.equal(finished.matched, 1);
assert.equal(
(await repository.findRunById(second.reference.runId)).status,
'succeeded',
);
assert.equal(
(await repository.findRunById(first.reference.runId)).status,
'running',
);
});
test('cancels one exact candidate or every active candidate', async () => {
const repository = await createRepository();
const writer = new LegacyShadowRunWriter(repository, nextId);
const first = await createActive(writer, { pid: 51 });
const second = await createActive(writer, { pid: 52 });
const failures = [];
const correlator = new LegacyShadowRunCorrelator(repository, writer, {
failure: (failure) => failures.push(failure),
});
const one = await correlator.cancel(
{
legacyCronId: 15,
pid: 51,
atMs: nextTime(),
scope: 'one',
reason: 'user',
},
['manual'],
);
assert.equal(one.matched, 1);
assert.equal(
(await repository.findRunById(first.reference.runId)).status,
'cancelled',
);
assert.equal(
(await repository.findRunById(second.reference.runId)).status,
'running',
);
const all = await correlator.cancel(
{
legacyCronId: 15,
atMs: nextTime(),
scope: 'all',
reason: 'policy',
},
['manual'],
);
assert.equal(all.matched, 1);
assert.equal(
(await repository.findRunById(second.reference.runId)).status,
'cancelled',
);
assert.deepEqual(failures, []);
});
test('recovers an out-of-order finish and ignores duplicate terminal callbacks', async () => {
const repository = await createRepository();
const writer = new LegacyShadowRunWriter(repository, nextId);
const atMs = nextTime();
const reference = await writer.accept({
origin: 'manual',
projectId: 'default',
taskId: 'legacy-cron:16',
taskRevision: 'sha256:revision',
legacyCronId: 16,
triggerType: 'manual',
acceptedAtMs: atMs,
});
const failures = [];
const correlator = new LegacyShadowRunCorrelator(repository, writer, {
failure: (failure) => failures.push(failure),
});
const first = await correlator.callback(
{
legacyCronId: 16,
pid: 160,
logArtifactId: 'log-16',
atMs: atMs + 10,
phase: 'finished',
exitCode: 0,
},
['manual'],
);
assert.equal(first.matched, 1);
assert.equal(
(await repository.findRunById(reference.runId)).status,
'succeeded',
);
const events = await repository.listEvents(reference.runId);
assert.deepEqual(
events.map((event) => event.type),
[
'run.created',
'run.queued',
'run.dispatching',
'attempt.starting',
'attempt.running',
'run.running',
'attempt.succeeded',
'run.succeeded',
],
);
const duplicate = await correlator.callback(
{
legacyCronId: 16,
pid: 160,
logArtifactId: 'log-16',
atMs: atMs + 20,
phase: 'finished',
exitCode: 0,
},
['manual'],
);
assert.equal(duplicate.matched, 0);
assert.equal(failures[0].reason, 'unmatched');
assert.equal((await repository.listEvents(reference.runId)).length, 8);
});
test('keeps cancellation terminal when a late successful callback arrives', async () => {
const repository = await createRepository();
const writer = new LegacyShadowRunWriter(repository, nextId);
const active = await createActive(writer, { pid: 170 });
const failures = [];
const correlator = new LegacyShadowRunCorrelator(repository, writer, {
failure: (failure) => failures.push(failure),
});
await correlator.cancel(
{
legacyCronId: 15,
pid: 170,
atMs: nextTime(),
scope: 'one',
reason: 'user',
},
['manual'],
);
const eventCount = (await repository.listEvents(active.reference.runId))
.length;
const late = await correlator.callback(
{
legacyCronId: 15,
pid: 170,
atMs: nextTime(),
phase: 'finished',
exitCode: 0,
},
['manual'],
);
assert.equal(late.matched, 0);
assert.equal(
(await repository.findRunById(active.reference.runId)).status,
'cancelled',
);
assert.equal(
(await repository.listEvents(active.reference.runId)).length,
eventCount,
);
assert.equal(failures[0].reason, 'unmatched');
});
test('continues cancel-all correlation after an individual shadow write fails', async () => {
const writes = [];
const failures = [];
const locator = {
async listActiveByLegacyCron() {
return {
truncated: false,
candidates: [
{
runId: 'run-a',
attemptId: 'attempt-a',
origin: 'manual',
runStatus: 'running',
attemptStatus: 'running',
createdAtMs: 1,
},
{
runId: 'run-b',
attemptId: 'attempt-b',
origin: 'manual',
runStatus: 'running',
attemptStatus: 'running',
createdAtMs: 2,
},
],
};
},
};
const writer = {
async cancelled(reference) {
writes.push(reference.runId);
if (reference.runId === 'run-a') throw new Error('write failed');
},
};
const correlator = new LegacyShadowRunCorrelator(locator, writer, {
failure: (failure) => failures.push(failure),
});
const result = await correlator.cancel(
{
legacyCronId: 15,
atMs: 4,
scope: 'all',
reason: 'user',
},
['manual'],
);
assert.deepEqual(writes, ['run-a', 'run-b']);
assert.equal(result.matched, 1);
assert.equal(failures[0].reason, 'write_failed');
});
+292
View File
@@ -0,0 +1,292 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { afterEach, test } = require('node:test');
const { Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const { runSchemaMigration } = require('../../back/migrations/0002-run-schema');
const {
runCancellationRequestMigration,
} = require('../../back/migrations/0004-run-cancellation-request');
const {
runAttemptDeadlineMigration,
} = require('../../back/migrations/0006-run-attempt-deadline');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacySequelizeRunRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/runRepository');
const {
LegacyShadowRunObserver,
} = require('../../back/runtime/application/legacyShadowRunObserver');
const {
LegacyShadowRunWriter,
} = require('../../back/runtime/application/legacyShadowRunWriter');
const {
RuntimeRolloutPolicy,
} = require('../../back/runtime/domain/runtimeRollout');
const databases = [];
const ACCEPTED_AT_MS = 1_750_000_000_000;
let idSequence = 300;
function nextId() {
idSequence += 1;
return `019f70e0-0000-7000-8000-${String(idSequence).padStart(12, '0')}`;
}
async function createRepository() {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
const migrationModel = defineSchemaMigrationModel(database);
await runMigrations({
database,
migrationModel,
migrations: [
runSchemaMigration,
runCancellationRequestMigration,
runAttemptDeadlineMigration,
],
logger: { info() {} },
});
databases.push(database);
return new LegacySequelizeRunRepository(database);
}
function acceptedFact(overrides = {}) {
return {
origin: 'manual',
projectId: 'default',
taskId: 'legacy-cron:7',
taskRevision: 'sha256:revision',
taskName: 'shadow test',
legacyCronId: 7,
triggerType: 'manual',
triggeredBy: 'legacy:manual',
acceptedAtMs: ACCEPTED_AT_MS,
...overrides,
};
}
function policy(mode = 'shadow') {
return new RuntimeRolloutPolicy({
defaultMode: 'off',
origins: { manual: mode },
allowLegacyFallbackBeforeStart: false,
});
}
afterEach(async () => {
await Promise.all(databases.splice(0).map((database) => database.close()));
});
test('persists an accepted legacy execution as one queued shadow aggregate', async () => {
const repository = await createRepository();
const writer = new LegacyShadowRunWriter(repository, nextId);
const reference = await writer.accept(acceptedFact());
const run = await repository.findRunById(reference.runId);
const attempt = await repository.findAttemptById(reference.attemptId);
const events = await repository.listEvents(reference.runId);
assert.equal(run.status, 'queued');
assert.equal(run.version, 2);
assert.equal(run.eventSequence, 2);
assert.equal(run.executionOwner, 'legacy');
assert.equal(attempt.status, 'claimed');
assert.deepEqual(
events.map((event) => [event.sequence, event.type]),
[
[1, 'run.created'],
[2, 'run.queued'],
],
);
assert.equal(
events.every((event) => event.payload.shadow === true),
true,
);
});
test('serializes a successful observed process lifecycle without starting it', async () => {
const repository = await createRepository();
const references = [];
const writer = new LegacyShadowRunWriter(repository, nextId);
const originalAccept = writer.accept.bind(writer);
writer.accept = async (fact) => {
const reference = await originalAccept(fact);
references.push(reference);
return reference;
};
const failures = [];
const observer = new LegacyShadowRunObserver(policy(), writer, {
failure: (failure) => failures.push(failure),
});
const observation = observer.begin(acceptedFact());
observation.spawned({
atMs: ACCEPTED_AT_MS + 1,
pid: 4242,
executorHandle: 'legacy-local:4242',
logArtifactId: 'legacy-log:1234567890123456789012345',
});
observation.running({ atMs: ACCEPTED_AT_MS + 1 });
observation.exited({ atMs: ACCEPTED_AT_MS + 2, exitCode: 0 });
await observation.settled();
assert.deepEqual(failures, []);
const reference = references[0];
const run = await repository.findRunById(reference.runId);
const attempt = await repository.findAttemptById(reference.attemptId);
const events = await repository.listEvents(reference.runId);
assert.equal(run.status, 'succeeded');
assert.equal(run.version, 8);
assert.equal(run.eventSequence, 8);
assert.equal(attempt.status, 'succeeded');
assert.equal(attempt.pid, 4242);
assert.equal(attempt.executorHandle, 'legacy-local:4242');
assert.equal(attempt.logArtifactId, 'legacy-log:1234567890123456789012345');
assert.equal(attempt.startedAtMs, ACCEPTED_AT_MS + 1);
assert.equal(attempt.finishedAtMs, ACCEPTED_AT_MS + 2);
assert.deepEqual(
events.map((event) => event.type),
[
'run.created',
'run.queued',
'run.dispatching',
'attempt.starting',
'attempt.running',
'run.running',
'attempt.succeeded',
'run.succeeded',
],
);
});
test('maps non-zero exits and start errors to stable failed terminal states', async () => {
for (const scenario of ['exit', 'start_error']) {
const repository = await createRepository();
const references = [];
const writer = new LegacyShadowRunWriter(repository, nextId);
const originalAccept = writer.accept.bind(writer);
writer.accept = async (fact) => {
const reference = await originalAccept(fact);
references.push(reference);
return reference;
};
const observer = new LegacyShadowRunObserver(policy(), writer, {
failure() {},
});
const observation = observer.begin(
acceptedFact({ taskId: `case:${scenario}` }),
);
if (scenario === 'exit') {
observation.spawned({ atMs: ACCEPTED_AT_MS + 1, pid: 12 });
observation.running({ atMs: ACCEPTED_AT_MS + 1 });
observation.exited({ atMs: ACCEPTED_AT_MS + 2, exitCode: 9 });
} else {
observation.startFailed({
atMs: ACCEPTED_AT_MS + 1,
errorCode: 'LEGACY_PROCESS_ERROR',
});
}
await observation.settled();
const reference = references[0];
const run = await repository.findRunById(reference.runId);
const attempt = await repository.findAttemptById(reference.attemptId);
assert.equal(run.status, 'failed');
assert.equal(attempt.status, 'failed');
if (scenario === 'exit') {
assert.equal(run.errorCode, 'LEGACY_EXIT_NON_ZERO');
assert.equal(attempt.exitCode, 9);
} else {
assert.equal(run.errorCode, 'LEGACY_PROCESS_ERROR');
}
}
});
test('is default-off, fail-open, and rejects primary ownership', async () => {
let writes = 0;
const failures = [];
const writer = {
async accept() {
writes += 1;
const error = new Error('database unavailable');
error.code = 'SQLITE_BUSY';
throw error;
},
};
const off = new LegacyShadowRunObserver(policy('off'), writer, {
failure: (failure) => failures.push(failure),
});
const noOp = off.begin(acceptedFact());
noOp.spawned({ atMs: ACCEPTED_AT_MS + 1 });
await noOp.settled();
assert.equal(writes, 0);
const shadow = new LegacyShadowRunObserver(policy(), writer, {
failure: (failure) => failures.push(failure),
});
const failed = shadow.begin(acceptedFact());
failed.spawned({ atMs: ACCEPTED_AT_MS + 1 });
failed.exited({ atMs: ACCEPTED_AT_MS + 2, exitCode: 0 });
await failed.settled();
assert.equal(writes, 1);
assert.deepEqual(failures, [
{
origin: 'manual',
operation: 'accept',
errorCode: 'SQLITE_BUSY',
},
]);
const primary = new LegacyShadowRunObserver(policy('primary'), writer, {
failure() {},
});
assert.throws(() => primary.begin(acceptedFact()), /primary execution/);
});
test('reports an individual shadow write failure and continues later facts', async () => {
const calls = [];
const failures = [];
const writer = {
async accept() {
calls.push('accept');
return { runId: 'run-1', attemptId: 'attempt-1' };
},
async spawned() {
calls.push('spawned');
const error = new Error('first write lost');
error.code = 'RUN_VERSION_CONFLICT';
throw error;
},
async running() {
calls.push('running');
},
async exited() {
calls.push('exited');
},
};
const observer = new LegacyShadowRunObserver(policy(), writer, {
failure: (failure) => failures.push(failure),
});
const observation = observer.begin(acceptedFact());
observation.spawned({ atMs: ACCEPTED_AT_MS + 1 });
observation.running({ atMs: ACCEPTED_AT_MS + 2 });
observation.exited({ atMs: ACCEPTED_AT_MS + 3, exitCode: 0 });
await observation.settled();
assert.deepEqual(calls, ['accept', 'spawned', 'running', 'exited']);
assert.deepEqual(failures, [
{
origin: 'manual',
operation: 'spawned',
errorCode: 'RUN_VERSION_CONFLICT',
runId: 'run-1',
attemptId: 'attempt-1',
},
]);
});
+60
View File
@@ -0,0 +1,60 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
createLegacyLogArtifactId,
createLegacyTaskRevision,
} = require('../../back/runtime/compatibility/legacyTaskRevision');
test('creates a deterministic opaque revision from execution-affecting fields', () => {
const input = {
command: 'node script.js --token secret-value',
schedule: '0 * * * *',
extraSchedules: ['30 * * * *'],
taskBefore: 'prepare',
taskAfter: 'cleanup',
workDirectory: '/ql/scripts',
logName: 'script',
environmentRevision: 'env-v1',
sourceRevision: 'git:abc123',
};
const first = createLegacyTaskRevision(input);
const second = createLegacyTaskRevision(structuredClone(input));
assert.equal(first, second);
assert.match(first, /^sha256:[a-f0-9]{64}$/);
assert.equal(first.includes('secret-value'), false);
assert.notEqual(
first,
createLegacyTaskRevision({ ...input, schedule: '1 * * * *' }),
);
assert.notEqual(
first,
createLegacyTaskRevision({ ...input, command: 'node other.js' }),
);
});
test('preserves extra schedule order as part of the task snapshot', () => {
const left = createLegacyTaskRevision({
command: 'task',
extraSchedules: ['a', 'b'],
});
const right = createLegacyTaskRevision({
command: 'task',
extraSchedules: ['b', 'a'],
});
assert.notEqual(left, right);
});
test('creates a bounded opaque artifact id for arbitrary legacy log paths', () => {
const longPath = `custom/${'nested/'.repeat(100)}secret-task.log`;
const artifactId = createLegacyLogArtifactId(longPath);
assert.match(artifactId, /^legacy-log:[a-f0-9]{25}$/);
assert.equal(artifactId.length <= 36, true);
assert.equal(artifactId.includes('secret-task'), false);
assert.equal(createLegacyLogArtifactId(longPath), artifactId);
assert.notEqual(createLegacyLogArtifactId(`${longPath}.1`), artifactId);
});
+607
View File
@@ -0,0 +1,607 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const { Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const {
RUN_ATTEMPT_TABLE,
RUN_TABLE,
runSchemaMigration,
} = require('../../back/migrations/0002-run-schema');
const {
LOCAL_ARTIFACT_RETENTION_TABLE,
localArtifactRetentionMigration,
} = require('../../back/migrations/0015-local-artifact-retention');
const { runMigrations } = require('../../back/migrations/runner');
const {
LocalArtifactByteRangeReader,
UnsafeLocalArtifactReadTargetError,
} = require('../../back/runtime/adapters/fs/localArtifactByteRangeReader');
const {
LocalArtifactTruncationFactStore,
localArtifactTruncationFactFileName,
} = require('../../back/runtime/adapters/fs/localArtifactTruncationFactStore');
const {
LegacySequelizeLocalArtifactReadMetadataRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/localArtifactReadMetadataRepository');
const {
LocalArtifactReadEvidenceConflictError,
LocalArtifactReadService,
} = require('../../back/runtime/application/localArtifactReadService');
const {
MAX_LOCAL_ARTIFACT_READ_BYTES,
} = require('../../back/runtime/domain/artifactRead');
const {
encodeLocalArtifactTruncationFact,
} = require('../../back/runtime/domain/localArtifactTruncation');
const RUN_ID = '019f7600-0000-7000-8000-000000000001';
const ATTEMPT_ID = '019f7600-0000-7000-8000-000000000002';
const LOG_ARTIFACT_ID = `local-${'e'.repeat(30)}`;
const PROJECT_ID = 'project-a';
const SUBJECT = Object.freeze({ type: 'user', id: 'user-1' });
const RANGE = Object.freeze({ offset: 2, length: 4 });
const METADATA = Object.freeze({
projectId: PROJECT_ID,
runId: RUN_ID,
attemptId: ATTEMPT_ID,
logArtifactId: LOG_ARTIFACT_ID,
});
function request(overrides = {}) {
return {
subject: SUBJECT,
projectId: PROJECT_ID,
runId: RUN_ID,
logArtifactId: LOG_ARTIFACT_ID,
range: RANGE,
...overrides,
};
}
async function temporaryArtifactRoot(t) {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-read-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
await fs.chmod(root, 0o700);
return root;
}
async function writeArtifact(root, value = '0123456789') {
const directory = path.join(root, LOG_ARTIFACT_ID.slice(6, 8));
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
const target = path.join(directory, `${LOG_ARTIFACT_ID}.log`);
await fs.writeFile(target, value, { mode: 0o600 });
return { directory, target };
}
function fact(quotaReached) {
return {
schemaVersion: 1,
runId: RUN_ID,
attemptId: ATTEMPT_ID,
logArtifactId: LOG_ARTIFACT_ID,
maximumBytes: 64 * 1024,
quotaReached,
observedAtMs: 1_800_000_000_000,
};
}
function service(overrides = {}) {
return new LocalArtifactReadService(
overrides.metadata ?? {
async find() {
return METADATA;
},
},
overrides.authorizer ?? {
async authorize() {
return 'allow';
},
},
overrides.bytes ?? {
async read() {
return {
status: 'available',
content: Buffer.from('2345'),
start: 2,
endExclusive: 6,
totalBytes: 10,
nextOffset: 6,
};
},
},
overrides.facts ?? {
async read() {
return null;
},
},
);
}
test('validates bounded range before metadata or policy side effects', async () => {
const calls = [];
const reader = service({
metadata: {
async find() {
calls.push('metadata');
return METADATA;
},
},
authorizer: {
async authorize() {
calls.push('authorize');
return 'allow';
},
},
bytes: {
async read() {
calls.push('bytes');
return { status: 'missing' };
},
},
facts: {
async read() {
calls.push('facts');
return null;
},
},
});
await assert.rejects(
reader.read(
request({
range: { offset: 0, length: MAX_LOCAL_ARTIFACT_READ_BYTES + 1 },
}),
),
/length is invalid/,
);
assert.deepEqual(calls, []);
});
test('never touches file or truncation evidence before artifact.read allows it', async () => {
const calls = [];
const reader = service({
metadata: {
async find(input) {
calls.push(['metadata', input]);
return METADATA;
},
},
authorizer: {
async authorize(input) {
calls.push(['authorize', input]);
return 'deny';
},
},
bytes: {
async read() {
calls.push(['bytes']);
throw new Error('must not read');
},
},
facts: {
async read() {
calls.push(['facts']);
throw new Error('must not read');
},
},
});
assert.deepEqual(await reader.read(request()), {
status: 'forbidden',
effect: 'deny',
});
assert.equal(calls.length, 2);
assert.equal(calls[0][0], 'metadata');
assert.deepEqual(calls[1], [
'authorize',
{
action: 'artifact.read',
subject: SUBJECT,
projectId: PROJECT_ID,
runId: RUN_ID,
logArtifactId: LOG_ARTIFACT_ID,
},
]);
});
test('returns not_found without policy or file probes for an unbound Artifact', async () => {
const calls = [];
const reader = service({
metadata: {
async find() {
calls.push('metadata');
return null;
},
},
authorizer: {
async authorize() {
calls.push('authorize');
return 'allow';
},
},
bytes: {
async read() {
calls.push('bytes');
return { status: 'missing' };
},
},
facts: {
async read() {
calls.push('facts');
return null;
},
},
});
assert.deepEqual(await reader.read(request()), { status: 'not_found' });
assert.deepEqual(calls, ['metadata']);
});
test('reads a bounded file snapshot and preserves true, false and unknown truncation', async (t) => {
const root = await temporaryArtifactRoot(t);
const { directory } = await writeArtifact(root);
const bytes = new LocalArtifactByteRangeReader(root);
const facts = new LocalArtifactTruncationFactStore(root);
const factTarget = path.join(
directory,
localArtifactTruncationFactFileName(LOG_ARTIFACT_ID),
);
const reader = service({ bytes, facts });
const unknown = await reader.read(request());
assert.equal(unknown.status, 'available');
assert.equal(unknown.content.toString(), '2345');
assert.deepEqual(
{
start: unknown.start,
endExclusive: unknown.endExclusive,
totalBytes: unknown.totalBytes,
nextOffset: unknown.nextOffset,
truncation: unknown.truncation,
},
{
start: 2,
endExclusive: 6,
totalBytes: 10,
nextOffset: 6,
truncation: { truncated: 'unknown' },
},
);
for (const quotaReached of [false, true]) {
await fs.writeFile(
factTarget,
encodeLocalArtifactTruncationFact(fact(quotaReached)),
{ mode: 0o600 },
);
const result = await reader.read(request());
assert.deepEqual(result.truncation, {
truncated: quotaReached,
maximumBytes: 64 * 1024,
observedAtMs: 1_800_000_000_000,
});
}
});
test('returns retained without touching files and resolves an ENOENT retirement race', async () => {
const retention = Object.freeze({
disposition: 'deleted',
finishedAtMs: 100,
eligibleAtMs: 200,
bytesReclaimed: 10,
recordedAtMs: 300,
});
let fileReads = 0;
let factReads = 0;
const alreadyRetained = service({
metadata: {
async find() {
return { ...METADATA, retention };
},
},
bytes: {
async read() {
fileReads += 1;
return { status: 'missing' };
},
},
facts: {
async read() {
factReads += 1;
return null;
},
},
});
const retained = await alreadyRetained.read(request());
assert.equal(retained.status, 'retained');
assert.deepEqual(retained.retention, retention);
assert.deepEqual(retained.truncation, { truncated: 'unknown' });
assert.equal(fileReads, 0);
assert.equal(factReads, 0);
let lookups = 0;
const raced = service({
metadata: {
async find() {
lookups += 1;
return lookups === 1 ? METADATA : { ...METADATA, retention };
},
},
bytes: {
async read() {
fileReads += 1;
return { status: 'missing' };
},
},
facts: {
async read() {
factReads += 1;
return null;
},
},
});
assert.equal((await raced.read(request())).status, 'retained');
assert.equal(lookups, 2);
assert.equal(fileReads, 1);
assert.equal(factReads, 0);
});
test('distinguishes unexplained missing content and rejects drifted fact identity', async () => {
const missing = service({
bytes: {
async read() {
return { status: 'missing' };
},
},
facts: {
async read() {
return fact(true);
},
},
});
const result = await missing.read(request());
assert.equal(result.status, 'missing');
assert.deepEqual(result.truncation, {
truncated: true,
maximumBytes: 64 * 1024,
observedAtMs: 1_800_000_000_000,
});
const drifted = service({
facts: {
async read() {
return {
...fact(false),
attemptId: '019f7600-0000-7000-8000-000000000099',
};
},
},
});
await assert.rejects(
drifted.read(request()),
LocalArtifactReadEvidenceConflictError,
);
});
test('file reader refuses symlink files and shard escapes', async (t) => {
const root = await temporaryArtifactRoot(t);
const directory = path.join(root, LOG_ARTIFACT_ID.slice(6, 8));
await fs.mkdir(directory, { mode: 0o700 });
const outside = path.join(root, 'outside.log');
await fs.writeFile(outside, 'outside');
const target = path.join(directory, `${LOG_ARTIFACT_ID}.log`);
await fs.symlink(outside, target);
const reader = new LocalArtifactByteRangeReader(root);
await assert.rejects(
reader.read(LOG_ARTIFACT_ID, RANGE),
UnsafeLocalArtifactReadTargetError,
);
await fs.unlink(target);
await fs.rmdir(directory);
await fs.symlink(path.dirname(outside), directory);
await assert.rejects(
reader.read(LOG_ARTIFACT_ID, RANGE),
UnsafeLocalArtifactReadTargetError,
);
});
async function setupDatabase(t) {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
t.after(() => database.close());
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations: [runSchemaMigration, localArtifactRetentionMigration],
logger: { info() {} },
});
return database;
}
async function seedMetadata(database, overrides = {}) {
const values = {
projectId: PROJECT_ID,
runId: RUN_ID,
attemptId: ATTEMPT_ID,
logArtifactId: LOG_ARTIFACT_ID,
executionOwner: 'runtime',
executorType: 'local_process',
...overrides,
};
const query = database.getQueryInterface();
await query.bulkInsert(RUN_TABLE, [
{
id: values.runId,
project_id: values.projectId,
task_id: 'task-1',
task_revision: 'revision-1',
trigger_type: 'manual',
execution_origin: 'manual',
execution_owner: values.executionOwner,
status: 'succeeded',
version: 1,
event_sequence: 1,
priority: 0,
created_at_ms: 1,
finished_at_ms: 100,
},
]);
await query.bulkInsert(RUN_ATTEMPT_TABLE, [
{
id: values.attemptId,
run_id: values.runId,
attempt: 1,
status: 'succeeded',
executor_type: values.executorType,
log_artifact_id: values.logArtifactId,
callback_sequence: 0,
created_at_ms: 1,
finished_at_ms: 100,
},
]);
return values;
}
test('SQLite metadata binds project, runtime owner, local executor and tombstone', async (t) => {
const database = await setupDatabase(t);
const values = await seedMetadata(database);
const repository = new LegacySequelizeLocalArtifactReadMetadataRepository(
database,
);
assert.equal(
await repository.find({
projectId: 'other-project',
runId: RUN_ID,
logArtifactId: LOG_ARTIFACT_ID,
}),
null,
);
assert.deepEqual(
await repository.find({
projectId: PROJECT_ID,
runId: RUN_ID,
logArtifactId: LOG_ARTIFACT_ID,
}),
METADATA,
);
await database
.getQueryInterface()
.bulkInsert(LOCAL_ARTIFACT_RETENTION_TABLE, [
{
attempt_id: values.attemptId,
log_artifact_id: values.logArtifactId,
finished_at_ms: 100,
eligible_at_ms: 200,
disposition: 'deleted',
bytes_reclaimed: 10,
recorded_at_ms: 300,
},
]);
const retained = await repository.find({
projectId: PROJECT_ID,
runId: RUN_ID,
logArtifactId: LOG_ARTIFACT_ID,
});
assert.deepEqual(retained.retention, {
disposition: 'deleted',
finishedAtMs: 100,
eligibleAtMs: 200,
bytesReclaimed: 10,
recordedAtMs: 300,
});
});
test('SQLite metadata excludes legacy owner and non-local executor', async (t) => {
const legacyDatabase = await setupDatabase(t);
await seedMetadata(legacyDatabase, { executionOwner: 'legacy' });
const legacy = new LegacySequelizeLocalArtifactReadMetadataRepository(
legacyDatabase,
);
assert.equal(
await legacy.find({
projectId: PROJECT_ID,
runId: RUN_ID,
logArtifactId: LOG_ARTIFACT_ID,
}),
null,
);
const remoteDatabase = await setupDatabase(t);
await seedMetadata(remoteDatabase, { executorType: 'remote_worker' });
const remote = new LegacySequelizeLocalArtifactReadMetadataRepository(
remoteDatabase,
);
assert.equal(
await remote.find({
projectId: PROJECT_ID,
runId: RUN_ID,
logArtifactId: LOG_ARTIFACT_ID,
}),
null,
);
});
test('SQLite metadata rejects a tombstone bound to a different Artifact', async (t) => {
const database = await setupDatabase(t);
const values = await seedMetadata(database);
await database
.getQueryInterface()
.bulkInsert(LOCAL_ARTIFACT_RETENTION_TABLE, [
{
attempt_id: values.attemptId,
log_artifact_id: `local-${'f'.repeat(30)}`,
finished_at_ms: 100,
eligible_at_ms: 200,
disposition: 'already_absent',
bytes_reclaimed: 0,
recorded_at_ms: 300,
},
]);
const repository = new LegacySequelizeLocalArtifactReadMetadataRepository(
database,
);
await assert.rejects(
repository.find({
projectId: PROJECT_ID,
runId: RUN_ID,
logArtifactId: LOG_ARTIFACT_ID,
}),
/metadata is corrupt or ambiguous/,
);
});
test('SQLite metadata rejects a tombstone with drifted Attempt completion time', async (t) => {
const database = await setupDatabase(t);
const values = await seedMetadata(database);
await database
.getQueryInterface()
.bulkInsert(LOCAL_ARTIFACT_RETENTION_TABLE, [
{
attempt_id: values.attemptId,
log_artifact_id: values.logArtifactId,
finished_at_ms: 101,
eligible_at_ms: 200,
disposition: 'deleted',
bytes_reclaimed: 10,
recorded_at_ms: 300,
},
]);
const repository = new LegacySequelizeLocalArtifactReadMetadataRepository(
database,
);
await assert.rejects(
repository.find({
projectId: PROJECT_ID,
runId: RUN_ID,
logArtifactId: LOG_ARTIFACT_ID,
}),
/metadata is corrupt or ambiguous/,
);
});
+566
View File
@@ -0,0 +1,566 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const { QueryTypes, Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const {
RUN_ATTEMPT_TABLE,
RUN_TABLE,
runSchemaMigration,
} = require('../../back/migrations/0002-run-schema');
const {
COMPLETION_RECEIPT_JOURNAL_TABLE,
completionReceiptJournalMigration,
} = require('../../back/migrations/0007-completion-receipt-journal');
const {
LOCAL_ARTIFACT_RETENTION_TABLE,
localArtifactRetentionMigration,
} = require('../../back/migrations/0015-local-artifact-retention');
const {
LOCAL_ARTIFACT_MAINTENANCE_CURSOR_TABLE,
localArtifactMaintenanceCursorMigration,
} = require('../../back/migrations/0016-local-artifact-maintenance-cursor');
const { runMigrations } = require('../../back/migrations/runner');
const {
LocalArtifactFileRetirementStore,
UnsafeLocalArtifactRetirementError,
} = require('../../back/runtime/adapters/fs/localArtifactFileRetirementStore');
const {
LegacySequelizeLocalArtifactRetentionRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/localArtifactRetentionRepository');
const {
LegacySequelizeLocalArtifactRetentionCheckpointStore,
} = require('../../back/runtime/adapters/legacy-sequelize/localArtifactRetentionCheckpointStore');
const {
LocalArtifactRetentionService,
} = require('../../back/runtime/application/localArtifactRetentionService');
const {
localExecutionArtifactId,
} = require('../../back/runtime/domain/localExecutionArtifact');
const {
encodeLocalArtifactTruncationFact,
} = require('../../back/runtime/domain/localArtifactTruncation');
const DAY_MS = 24 * 60 * 60_000;
const OBSERVED_AT_MS = 1_800_000_000_000;
function identity(index) {
const suffix = String(index).padStart(12, '0');
return {
runId: `019f7400-0000-7000-8000-${suffix}`,
attemptId: `019f7401-0000-7000-8000-${suffix}`,
};
}
function artifactId(index) {
const ids = identity(index);
return localExecutionArtifactId({
...ids,
projectId: 'default',
taskId: `task-${index}`,
taskRevision: 'revision-1',
executorType: 'local_process',
priority: 0,
queuedAtMs: 1,
attemptCreatedAtMs: 1,
});
}
async function setup(t) {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-retention-'));
const artifacts = path.join(root, 'artifacts');
await fs.mkdir(artifacts, { recursive: true, mode: 0o700 });
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
t.after(() => database.close());
t.after(() => fs.rm(root, { recursive: true, force: true }));
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations: [
runSchemaMigration,
completionReceiptJournalMigration,
localArtifactRetentionMigration,
localArtifactMaintenanceCursorMigration,
],
logger: { info() {} },
});
return {
root,
artifacts,
database,
repository: new LegacySequelizeLocalArtifactRetentionRepository(database),
checkpoints: new LegacySequelizeLocalArtifactRetentionCheckpointStore(
database,
),
files: new LocalArtifactFileRetirementStore(artifacts),
};
}
async function seedAttempt(
context,
index,
{
finishedAtMs = OBSERVED_AT_MS - 10 * DAY_MS,
runStatus = 'succeeded',
attemptStatus = 'succeeded',
executionOwner = 'runtime',
executorType = 'local_process',
logArtifactId = artifactId(index),
} = {},
) {
const ids = identity(index);
const query = context.database.getQueryInterface();
await query.bulkInsert(RUN_TABLE, [
{
id: ids.runId,
project_id: 'default',
task_id: `task-${index}`,
task_revision: 'revision-1',
trigger_type: 'manual',
execution_origin: 'manual',
execution_owner: executionOwner,
status: runStatus,
version: 1,
event_sequence: 1,
priority: 0,
created_at_ms: finishedAtMs - 1_000,
finished_at_ms: runStatus === 'running' ? null : finishedAtMs,
},
]);
await query.bulkInsert(RUN_ATTEMPT_TABLE, [
{
id: ids.attemptId,
run_id: ids.runId,
attempt: 1,
status: attemptStatus,
executor_type: executorType,
log_artifact_id: logArtifactId,
callback_sequence: 0,
created_at_ms: finishedAtMs - 1_000,
finished_at_ms: attemptStatus === 'running' ? null : finishedAtMs,
},
]);
return { ...ids, logArtifactId, finishedAtMs };
}
async function writeArtifact(context, logArtifactId, value = 'artifact-value') {
const directory = path.join(context.artifacts, logArtifactId.slice(6, 8));
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
const target = path.join(directory, `${logArtifactId}.log`);
await fs.writeFile(target, value, { mode: 0o600 });
return { directory, target, bytes: Buffer.byteLength(value) };
}
function retentionService(context, overrides = {}) {
return new LocalArtifactRetentionService(
overrides.repository ?? context.repository,
overrides.files ?? context.files,
overrides.capacity ?? {
async inspect() {
return {
availableBytes: BigInt(1024 * 1024),
totalBytes: BigInt(2 * 1024 * 1024),
};
},
},
{
normalRetentionMs: overrides.normalRetentionMs ?? 7 * DAY_MS,
pressureRetentionMs: overrides.pressureRetentionMs ?? DAY_MS,
minimumFreeBytes: overrides.minimumFreeBytes ?? 64 * 1024,
pageSize: overrides.pageSize ?? 16,
maximumDeletions: overrides.maximumDeletions ?? 8,
clock: { now: () => OBSERVED_AT_MS },
},
);
}
test('selects only settled runtime-owned terminal local Artifacts', async (t) => {
const context = await setup(t);
const eligible = await seedAttempt(context, 1);
await seedAttempt(context, 2, { runStatus: 'running' });
await seedAttempt(context, 3, { attemptStatus: 'running' });
await seedAttempt(context, 4, { executionOwner: 'legacy' });
await seedAttempt(context, 5, { executorType: 'docker' });
await seedAttempt(context, 6, { logArtifactId: 'legacy-log-not-local' });
const receiptBlocked = await seedAttempt(context, 7);
await seedAttempt(context, 8, {
runStatus: 'lost',
attemptStatus: 'lost',
});
await context.database
.getQueryInterface()
.bulkInsert(COMPLETION_RECEIPT_JOURNAL_TABLE, [
{
attempt_id: receiptBlocked.attemptId,
run_id: receiptBlocked.runId,
state: 'pending',
registered_at_ms: 1,
updated_at_ms: 1,
},
]);
const page = await context.repository.list({
cutoffMs: OBSERVED_AT_MS - 7 * DAY_MS,
limit: 16,
});
assert.deepEqual(page.candidates, [
{
attemptId: eligible.attemptId,
logArtifactId: eligible.logArtifactId,
finishedAtMs: eligible.finishedAtMs,
},
]);
await context.database
.getQueryInterface()
.bulkDelete(COMPLETION_RECEIPT_JOURNAL_TABLE, {
attempt_id: receiptBlocked.attemptId,
});
assert.equal(
(await context.repository.list({ cutoffMs: OBSERVED_AT_MS, limit: 16 }))
.candidates.length,
2,
);
});
test('uses pressure retention, deletes durably, and records an immutable tombstone', async (t) => {
const context = await setup(t);
const candidate = await seedAttempt(context, 1, {
finishedAtMs: OBSERVED_AT_MS - 2 * DAY_MS,
});
const artifact = await writeArtifact(context, candidate.logArtifactId);
const normal = await retentionService(context).sweep();
assert.equal(normal.pressure, false);
assert.equal(normal.candidatesScanned, 0);
assert.equal((await fs.stat(artifact.target)).size, artifact.bytes);
const pressure = retentionService(context, {
capacity: {
async inspect() {
return { availableBytes: BigInt(1), totalBytes: BigInt(1024) };
},
},
});
const result = await pressure.sweep();
assert.deepEqual(
{
status: result.status,
pressure: result.pressure,
retentionMs: result.retentionMs,
recordsWritten: result.recordsWritten,
bytesReclaimed: result.bytesReclaimed,
},
{
status: 'complete',
pressure: true,
retentionMs: DAY_MS,
recordsWritten: 1,
bytesReclaimed: artifact.bytes,
},
);
await assert.rejects(fs.lstat(artifact.target), /ENOENT/);
const tombstone = await context.database
.getQueryInterface()
.select(null, LOCAL_ARTIFACT_RETENTION_TABLE, {
where: { attempt_id: candidate.attemptId },
plain: true,
});
assert.equal(tombstone.disposition, 'deleted');
assert.equal(Number(tombstone.bytes_reclaimed), artifact.bytes);
assert.equal((await pressure.sweep()).candidatesScanned, 0);
});
test('recovers a crash between file deletion and tombstone persistence', async (t) => {
const context = await setup(t);
const candidate = await seedAttempt(context, 1);
const artifact = await writeArtifact(context, candidate.logArtifactId);
let recordCalls = 0;
const crashing = retentionService(context, {
repository: {
list: (options) => context.repository.list(options),
async record() {
recordCalls += 1;
throw new Error('simulated database outage');
},
},
});
const failed = await crashing.sweep();
assert.equal(failed.entries[0].outcome, 'record_failed');
assert.equal(failed.entries[0].bytesReclaimed, artifact.bytes);
assert.equal(recordCalls, 1);
await assert.rejects(fs.lstat(artifact.target), /ENOENT/);
const recovered = await retentionService(context).sweep();
assert.equal(recovered.entries[0].outcome, 'already_absent');
assert.equal(recovered.recordsWritten, 1);
assert.equal(
await context.database
.getQueryInterface()
.rawSelect(
LOCAL_ARTIFACT_RETENTION_TABLE,
{ where: { attempt_id: candidate.attemptId } },
['disposition'],
),
'already_absent',
);
});
test('enforces a deletion budget and resumes from a stable cursor', async (t) => {
const context = await setup(t);
for (let index = 1; index <= 3; index += 1) {
const candidate = await seedAttempt(context, index);
await writeArtifact(context, candidate.logArtifactId, `artifact-${index}`);
}
const service = retentionService(context, {
pageSize: 3,
maximumDeletions: 2,
});
const first = await service.sweep();
assert.equal(first.status, 'deletion_budget_exhausted');
assert.equal(first.recordsWritten, 2);
assert.deepEqual(first.nextCursor, {
finishedAtMs: OBSERVED_AT_MS - 10 * DAY_MS,
attemptId: identity(2).attemptId,
});
const second = await service.sweep(first.nextCursor);
assert.equal(second.status, 'complete');
assert.equal(second.recordsWritten, 1);
const [count] = await context.database.query(
`SELECT COUNT(*) AS count FROM "${LOCAL_ARTIFACT_RETENTION_TABLE}"`,
{ type: QueryTypes.SELECT },
);
assert.equal(count.count, 3);
});
test('removes a stale quota FIFO but refuses symlink or non-file targets', async (t) => {
const context = await setup(t);
const logArtifactId = artifactId(1);
const artifact = await writeArtifact(context, logArtifactId);
const fifo = path.join(artifact.directory, `.${logArtifactId}.log.fifo`);
await new Promise((resolve, reject) => {
require('node:child_process').execFile(
'mkfifo',
['-m', '600', fifo],
(error) => (error ? reject(error) : resolve()),
);
});
const truncation = path.join(
artifact.directory,
`.${logArtifactId}.log.truncated.json`,
);
const truncationTemporary = path.join(
artifact.directory,
`.${logArtifactId}.log.truncated.tmp`,
);
await fs.writeFile(
truncation,
encodeLocalArtifactTruncationFact({
schemaVersion: 1,
...identity(1),
logArtifactId,
maximumBytes: 64 * 1024,
quotaReached: true,
observedAtMs: OBSERVED_AT_MS,
}),
{ mode: 0o600 },
);
await fs.writeFile(truncationTemporary, 'partial', { mode: 0o600 });
assert.equal(
(await context.files.retire(logArtifactId)).disposition,
'deleted',
);
await assert.rejects(fs.lstat(fifo), /ENOENT/);
await assert.rejects(fs.lstat(truncation), /ENOENT/);
await assert.rejects(fs.lstat(truncationTemporary), /ENOENT/);
const outside = path.join(context.root, 'outside');
await fs.writeFile(outside, 'outside');
await fs.writeFile(artifact.target, 'replacement');
await fs.unlink(artifact.target);
await fs.symlink(outside, artifact.target);
await assert.rejects(
context.files.retire(logArtifactId),
UnsafeLocalArtifactRetirementError,
);
assert.equal(await fs.readFile(outside, 'utf8'), 'outside');
});
test('treats a missing Artifact shard as already absent', async (t) => {
const context = await setup(t);
assert.deepEqual(await context.files.retire(artifactId(1)), {
disposition: 'already_absent',
bytesReclaimed: 0,
});
});
test('rejects invalid capacity and pages before touching Artifact files', async (t) => {
const context = await setup(t);
const candidate = await seedAttempt(context, 1);
const artifact = await writeArtifact(context, candidate.logArtifactId);
let retireCalls = 0;
const files = {
async retire() {
retireCalls += 1;
return { disposition: 'deleted', bytesReclaimed: artifact.bytes };
},
};
await assert.rejects(
retentionService(context, {
files,
capacity: {
async inspect() {
return { availableBytes: BigInt(2), totalBytes: BigInt(1) };
},
},
}).sweep(),
/capacity snapshot is invalid/,
);
assert.equal(retireCalls, 0);
const invalidPage = retentionService(context, {
files,
repository: {
async list() {
return {
candidates: [candidate],
truncated: true,
};
},
async record() {
throw new Error('record must remain unreachable');
},
},
});
await assert.rejects(invalidPage.sweep(), /resume cursor is inconsistent/);
assert.equal(retireCalls, 0);
assert.equal((await fs.stat(artifact.target)).size, artifact.bytes);
});
test('persists and fences the retention resume cursor without idle rewrites', async (t) => {
const context = await setup(t);
const cursor = {
finishedAtMs: OBSERVED_AT_MS - DAY_MS,
attemptId: identity(1).attemptId,
};
assert.deepEqual(await context.checkpoints.load(), { version: 0 });
assert.equal(
await context.checkpoints.compareAndSet({
expectedVersion: 0,
cursor,
updatedAtMs: OBSERVED_AT_MS,
}),
true,
);
assert.deepEqual(await context.checkpoints.load(), {
version: 1,
cursor,
});
assert.equal(
await context.checkpoints.compareAndSet({
expectedVersion: 0,
updatedAtMs: OBSERVED_AT_MS,
}),
false,
);
assert.equal(
await context.checkpoints.compareAndSet({
expectedVersion: 1,
updatedAtMs: OBSERVED_AT_MS + 1,
}),
true,
);
assert.deepEqual(await context.checkpoints.load(), { version: 2 });
assert.equal(
await context.database
.getQueryInterface()
.rawSelect(
LOCAL_ARTIFACT_MAINTENANCE_CURSOR_TABLE,
{ where: { scope: 'retention' } },
['version'],
),
2,
);
});
test('serializes concurrent initial cursor claims', async (t) => {
const context = await setup(t);
const competing = new LegacySequelizeLocalArtifactRetentionCheckpointStore(
context.database,
);
const cursor = {
finishedAtMs: OBSERVED_AT_MS - DAY_MS,
attemptId: identity(1).attemptId,
};
const results = await Promise.all([
context.checkpoints.compareAndSet({
expectedVersion: 0,
cursor,
updatedAtMs: OBSERVED_AT_MS,
}),
competing.compareAndSet({
expectedVersion: 0,
cursor,
updatedAtMs: OBSERVED_AT_MS,
}),
]);
assert.deepEqual(results.sort(), [false, true]);
assert.deepEqual(await context.checkpoints.load(), { version: 1, cursor });
});
test('refuses a symlink truncation fact before deleting its Artifact', async (t) => {
const context = await setup(t);
const logArtifactId = artifactId(1);
const artifact = await writeArtifact(context, logArtifactId);
const outside = path.join(context.root, 'outside-truncation');
await fs.writeFile(outside, 'outside');
const truncation = path.join(
artifact.directory,
`.${logArtifactId}.log.truncated.json`,
);
await fs.symlink(outside, truncation);
await assert.rejects(
context.files.retire(logArtifactId),
UnsafeLocalArtifactRetirementError,
);
assert.equal(await fs.readFile(outside, 'utf8'), 'outside');
assert.equal((await fs.stat(artifact.target)).size, artifact.bytes);
});
test('preserves the truncation fact when log deletion fails', async (t) => {
const context = await setup(t);
const logArtifactId = artifactId(1);
const artifact = await writeArtifact(context, logArtifactId);
const truncation = path.join(
artifact.directory,
`.${logArtifactId}.log.truncated.json`,
);
await fs.writeFile(
truncation,
encodeLocalArtifactTruncationFact({
schemaVersion: 1,
...identity(1),
logArtifactId,
maximumBytes: 64 * 1024,
quotaReached: true,
observedAtMs: OBSERVED_AT_MS,
}),
{ mode: 0o600 },
);
await fs.chmod(artifact.directory, 0o500);
try {
await assert.rejects(context.files.retire(logArtifactId));
} finally {
await fs.chmod(artifact.directory, 0o700);
}
assert.equal((await fs.stat(artifact.target)).size, artifact.bytes);
assert.ok((await fs.stat(truncation)).isFile());
});
@@ -0,0 +1,275 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
LocalArtifactRetentionLifecycle,
} = require('../../back/runtime/application/localArtifactRetentionLifecycle');
const CURSOR = {
finishedAtMs: 1_800_000_000_000,
attemptId: '019f7500-0000-7000-8000-000000000001',
};
function deferred() {
let resolve;
const promise = new Promise((resolvePromise) => {
resolve = resolvePromise;
});
return { promise, resolve };
}
function fakeScheduler() {
let id = 0;
const pending = new Map();
const cleared = [];
return {
pending,
cleared,
scheduler: {
setTimeout(callback, delayMs) {
const timer = {
id: ++id,
delayMs,
unrefCalls: 0,
unref() {
this.unrefCalls += 1;
},
};
pending.set(timer.id, { timer, callback });
return timer;
},
clearTimeout(timer) {
cleared.push(timer.id);
pending.delete(timer.id);
},
},
fireNext() {
const next = pending.values().next().value;
assert.ok(next, 'expected a scheduled timer');
pending.delete(next.timer.id);
next.callback();
return next.timer;
},
};
}
function memoryCheckpoints({ fenced = false } = {}) {
let checkpoint = { version: 0 };
const writes = [];
return {
writes,
store: {
async load() {
return checkpoint;
},
async compareAndSet(value) {
writes.push(value);
if (fenced || value.expectedVersion !== checkpoint.version)
return false;
checkpoint = {
version: checkpoint.version + 1,
...(value.cursor ? { cursor: { ...value.cursor } } : {}),
};
return true;
},
},
};
}
function sweepResult(overrides = {}) {
return {
status: 'complete',
pressure: false,
observedAtMs: 1_800_000_001_000,
retentionMs: 7 * 24 * 60 * 60_000,
availableBytes: BigInt(1024),
totalBytes: BigInt(2048),
candidatesScanned: 0,
deletionsAttempted: 0,
recordsWritten: 0,
failedCandidates: 0,
bytesReclaimed: 0,
entries: [],
...overrides,
};
}
async function flush() {
await new Promise((resolve) => setImmediate(resolve));
}
test('persists only changed cursors and emits bounded aggregate metrics', async () => {
const timers = fakeScheduler();
const checkpoints = memoryCheckpoints();
const cursors = [];
const summaries = [];
let calls = 0;
const lifecycle = new LocalArtifactRetentionLifecycle(
{
async sweep(cursor) {
cursors.push(cursor);
calls += 1;
if (calls === 1) {
return sweepResult({
status: 'page_complete',
candidatesScanned: 8,
deletionsAttempted: 8,
recordsWritten: 8,
bytesReclaimed: 4096,
nextCursor: CURSOR,
entries: [
{
attemptId: CURSOR.attemptId,
logArtifactId: `local-${'a'.repeat(30)}`,
outcome: 'deleted',
bytesReclaimed: 4096,
},
],
});
}
return sweepResult();
},
},
checkpoints.store,
{
intervalMs: 5_000,
initialDelayMs: 100,
scheduler: timers.scheduler,
onCycle(summary) {
summaries.push(summary);
},
},
);
assert.equal(lifecycle.start(), true);
const initial = timers.fireNext();
assert.equal(initial.delayMs, 100);
assert.equal(initial.unrefCalls, 1);
await flush();
assert.equal(summaries[0].cursorAction, 'advanced');
assert.equal(summaries[0].recordsWritten, 8);
assert.equal('entries' in summaries[0], false);
assert.equal(JSON.stringify(summaries[0]).includes(CURSOR.attemptId), false);
assert.equal(summaries[0].availableBytes, '1024');
timers.fireNext();
await flush();
assert.equal(summaries[1].cursorAction, 'cleared');
timers.fireNext();
await flush();
assert.equal(summaries[2].cursorAction, 'unchanged');
assert.deepEqual(cursors, [undefined, CURSOR, undefined]);
assert.equal(checkpoints.writes.length, 2);
assert.equal(await lifecycle.stop(), 'drained');
});
test('reports cursor fencing and observer failures without stopping cadence', async () => {
const timers = fakeScheduler();
const checkpoints = memoryCheckpoints({ fenced: true });
const errors = [];
const lifecycle = new LocalArtifactRetentionLifecycle(
{
async sweep() {
return sweepResult({ status: 'page_complete', nextCursor: CURSOR });
},
},
checkpoints.store,
{
intervalMs: 5_000,
scheduler: timers.scheduler,
onCycle(summary) {
assert.equal(summary.cursorAction, 'fenced');
throw new Error('metrics sink failed');
},
onError(error) {
errors.push(error.message);
},
},
);
lifecycle.start();
timers.fireNext();
await flush();
assert.deepEqual(errors, ['metrics sink failed']);
assert.equal(timers.pending.size, 1);
assert.equal(await lifecycle.stop(), 'drained');
});
test('never overlaps a slow sweep and bounds shutdown', async () => {
const timers = fakeScheduler();
const running = deferred();
const checkpoints = memoryCheckpoints();
let calls = 0;
const lifecycle = new LocalArtifactRetentionLifecycle(
{
async sweep() {
calls += 1;
return running.promise;
},
},
checkpoints.store,
{
intervalMs: 5_000,
stopTimeoutMs: 5,
scheduler: timers.scheduler,
},
);
lifecycle.start();
timers.fireNext();
await flush();
assert.equal(calls, 1);
assert.equal(timers.pending.size, 0);
assert.equal(await lifecycle.stop(), 'timed_out');
assert.equal(lifecycle.start(), false);
running.resolve(sweepResult());
await flush();
assert.equal(lifecycle.start(), true);
assert.equal(await lifecycle.stop(), 'drained');
});
test('rejects hot loops, invalid incomplete results, and unbounded stops', async () => {
const timers = fakeScheduler();
const checkpoints = memoryCheckpoints();
const service = {
async sweep() {
return sweepResult();
},
};
assert.throws(
() =>
new LocalArtifactRetentionLifecycle(service, checkpoints.store, {
intervalMs: 999,
}),
RangeError,
);
assert.throws(
() =>
new LocalArtifactRetentionLifecycle(service, checkpoints.store, {
intervalMs: 1_000,
stopTimeoutMs: 60_001,
}),
RangeError,
);
const errors = [];
const invalid = new LocalArtifactRetentionLifecycle(
{
async sweep() {
return sweepResult({ status: 'page_complete' });
},
},
checkpoints.store,
{
intervalMs: 1_000,
scheduler: timers.scheduler,
onError(error) {
errors.push(error.message);
},
},
);
invalid.start();
timers.fireNext();
await flush();
assert.match(errors[0], /requires a resume cursor/);
await invalid.stop();
});
+111
View File
@@ -0,0 +1,111 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
LocalArtifactTruncationFactStore,
UnsafeLocalArtifactTruncationFactError,
localArtifactTruncationFactFileName,
} = require('../../back/runtime/adapters/fs/localArtifactTruncationFactStore');
const {
decodeLocalArtifactTruncationFact,
encodeLocalArtifactTruncationFact,
} = require('../../back/runtime/domain/localArtifactTruncation');
const LOG_ARTIFACT_ID = `local-${'c'.repeat(30)}`;
const OTHER_ARTIFACT_ID = `local-${'d'.repeat(30)}`;
const FACT = {
schemaVersion: 1,
runId: '019f7500-0000-7000-8000-000000000001',
attemptId: '019f7500-0000-7000-8000-000000000002',
logArtifactId: LOG_ARTIFACT_ID,
maximumBytes: 64 * 1024,
quotaReached: true,
observedAtMs: 1_800_000_000_000,
};
async function temporaryRoot(t) {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-truncation-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
await fs.chmod(root, 0o700);
return root;
}
test('round-trips one exact canonical truncation fact', () => {
const encoded = encodeLocalArtifactTruncationFact(FACT);
assert.deepEqual(decodeLocalArtifactTruncationFact(encoded), FACT);
assert.throws(
() =>
decodeLocalArtifactTruncationFact(
JSON.stringify({ ...FACT, unknown: true }),
),
/shape is invalid/,
);
assert.throws(
() =>
decodeLocalArtifactTruncationFact(
JSON.stringify({ attemptId: FACT.attemptId, ...FACT }),
),
/not canonical/,
);
assert.throws(
() => encodeLocalArtifactTruncationFact({ ...FACT, maximumBytes: 1 }),
/maximumBytes is invalid/,
);
});
test('reads only the exact private Artifact fact and handles absence', async (t) => {
const root = await temporaryRoot(t);
const store = new LocalArtifactTruncationFactStore(root);
assert.equal(await store.read(LOG_ARTIFACT_ID), null);
const directory = path.join(root, LOG_ARTIFACT_ID.slice(6, 8));
await fs.mkdir(directory, { mode: 0o700 });
const target = path.join(
directory,
localArtifactTruncationFactFileName(LOG_ARTIFACT_ID),
);
await fs.writeFile(target, encodeLocalArtifactTruncationFact(FACT), {
mode: 0o600,
});
assert.deepEqual(await store.read(LOG_ARTIFACT_ID), FACT);
await fs.writeFile(
target,
encodeLocalArtifactTruncationFact({
...FACT,
logArtifactId: OTHER_ARTIFACT_ID,
}),
);
await assert.rejects(
store.read(LOG_ARTIFACT_ID),
UnsafeLocalArtifactTruncationFactError,
);
});
test('refuses symlink files and shard escapes', async (t) => {
const root = await temporaryRoot(t);
const store = new LocalArtifactTruncationFactStore(root);
const outside = path.join(root, 'outside.json');
await fs.writeFile(outside, encodeLocalArtifactTruncationFact(FACT));
const directory = path.join(root, LOG_ARTIFACT_ID.slice(6, 8));
await fs.mkdir(directory, { mode: 0o700 });
const target = path.join(
directory,
localArtifactTruncationFactFileName(LOG_ARTIFACT_ID),
);
await fs.symlink(outside, target);
await assert.rejects(
store.read(LOG_ARTIFACT_ID),
UnsafeLocalArtifactTruncationFactError,
);
await fs.unlink(target);
await fs.rmdir(directory);
await fs.symlink(path.dirname(outside), directory);
await assert.rejects(
store.read(LOG_ARTIFACT_ID),
UnsafeLocalArtifactTruncationFactError,
);
});
@@ -0,0 +1,254 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const { Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const {
taskExecutionRevisionMigration,
} = require('../../back/migrations/0012-task-execution-revisions');
const {
LOCAL_EXECUTION_CONTEXT_RECIPE_TABLE,
localExecutionContextRecipeMigration,
} = require('../../back/migrations/0013-local-execution-context-recipes');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacySequelizeLocalExecutionContextRecipeRepository,
LocalExecutionContextRecipeCorruptError,
} = require('../../back/runtime/adapters/legacy-sequelize/localExecutionContextRecipeRepository');
const {
LegacySequelizeTaskExecutionRevisionRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/taskExecutionRevisionRepository');
const {
LocalTaskExecutionRevisionPublisher,
} = require('../../back/runtime/application/localTaskExecutionRevisionPublisher');
const {
RecipeLocalExecutionContextMaterializer,
} = require('../../back/runtime/application/recipeLocalExecutionContextMaterializer');
const {
createLocalExecutionContextRecipe,
localExecutionContextRecipeDigest,
} = require('../../back/runtime/domain/localExecutionContextRecipe');
function contextRecipe() {
return createLocalExecutionContextRecipe([
{ name: 'MODE', kind: 'public', value: 'edge' },
{ name: 'TOKEN', kind: 'secret', secretRef: 'secret://token' },
]);
}
function revision(recipe = contextRecipe()) {
return {
projectId: 'default',
taskId: 'task-recipe',
taskRevision: 'revision-1',
executorType: 'local_process',
execution: {
command: { kind: 'argv', file: '/bin/true', args: [] },
environmentPolicy: 'isolated',
terminationGraceMs: 100,
},
contextRef: recipe.contextRef,
};
}
function candidate() {
return {
runId: 'run-recipe',
attemptId: 'attempt-recipe',
projectId: 'default',
taskId: 'task-recipe',
taskRevision: 'revision-1',
executorType: 'local_process',
priority: 0,
queuedAtMs: 1_760_000_000_000,
attemptCreatedAtMs: 1_760_000_000_000,
};
}
async function createRepositories(t) {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
t.after(() => database.close());
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations: [
taskExecutionRevisionMigration,
localExecutionContextRecipeMigration,
],
logger: { info() {} },
});
return {
database,
recipes: new LegacySequelizeLocalExecutionContextRecipeRepository(database),
revisions: new LegacySequelizeTaskExecutionRevisionRepository(database),
};
}
test('uses stable content-address and digest vectors without persisting Secret values', () => {
const recipe = contextRecipe();
assert.equal(
recipe.contextRef,
'localctx:sha256:d70176732d42e2701d95adce0371b765226a3d5085e17d14816615c933fc5c7f',
);
assert.equal(
localExecutionContextRecipeDigest(recipe),
'a1acbef2b7b882a0e6f204bd79f303a10fa6043753251ebb222adb31d42b2597',
);
assert.equal(
createLocalExecutionContextRecipe([...recipe.environment].reverse())
.contextRef,
recipe.contextRef,
);
assert.equal(JSON.stringify(recipe).includes('in-memory-secret'), false);
});
test('concurrent immutable recipe publication converges and resolves exactly', async (t) => {
const { recipes } = await createRepositories(t);
const recipe = contextRecipe();
const results = await Promise.all(
Array.from({ length: 12 }, (_, index) =>
recipes.insert(recipe, 100 + index),
),
);
assert.equal(results.filter((result) => result === 'inserted').length, 1);
assert.equal(results.filter((result) => result === 'idempotent').length, 11);
const stored = await recipes.resolve(recipe.contextRef);
assert.deepEqual(stored.environment, recipe.environment);
assert.ok(Object.isFrozen(stored));
assert.ok(Object.isFrozen(stored.environment));
assert.equal(await recipes.resolve(`${recipe.contextRef}-missing`), null);
await assert.rejects(
recipes.insert({ ...recipe, contextRef: 'localctx:sha256:wrong' }, 200),
/not content-addressed/,
);
});
test('fails closed on persisted recipe corruption and cluster dialect use', async (t) => {
const { database, recipes } = await createRepositories(t);
const recipe = contextRecipe();
await recipes.insert(recipe, 100);
await database
.getQueryInterface()
.bulkUpdate(
LOCAL_EXECUTION_CONTEXT_RECIPE_TABLE,
{ content_digest: '0'.repeat(64) },
{ context_ref: recipe.contextRef },
);
await assert.rejects(
recipes.resolve(recipe.contextRef),
LocalExecutionContextRecipeCorruptError,
);
assert.throws(
() =>
new LegacySequelizeLocalExecutionContextRecipeRepository({
getDialect: () => 'postgres',
}),
/PostgreSQL adapter/,
);
});
test('publishes recipe before revision and materializes it with ephemeral Secrets', async (t) => {
const { recipes, revisions } = await createRepositories(t);
const recipe = contextRecipe();
const publisher = new LocalTaskExecutionRevisionPublisher(recipes, revisions);
assert.deepEqual(
await publisher.publish({
revision: revision(recipe),
contextRecipe: recipe,
createdAtMs: 100,
}),
{ contextRecipe: 'inserted', revision: 'inserted' },
);
assert.deepEqual(
await publisher.publish({
revision: revision(recipe),
contextRecipe: recipe,
createdAtMs: 101,
}),
{ contextRecipe: 'idempotent', revision: 'idempotent' },
);
assert.equal(
(await revisions.resolve(revision(recipe))).contextRef,
recipe.contextRef,
);
const materializer = new RecipeLocalExecutionContextMaterializer(
recipes,
{
async prepare() {
return {
logArtifactId: `local-${'c'.repeat(30)}`,
output: { async write() {} },
dispose() {},
};
},
},
{
async resolve() {
return ['in-memory-secret'];
},
},
);
const context = await materializer.prepare({
candidate: candidate(),
contextRef: recipe.contextRef,
});
assert.deepEqual(
{ ...context.context.environment },
{
MODE: 'edge',
TOKEN: 'in-memory-secret',
},
);
});
test('a missing or failed recipe publication can never create a dangling revision', async () => {
const calls = [];
const publisher = new LocalTaskExecutionRevisionPublisher(
{
async resolve() {
return null;
},
async insert() {
calls.push('recipe');
throw new Error('recipe write failed');
},
},
{
async resolve() {
return null;
},
async insert() {
calls.push('revision');
return 'inserted';
},
},
);
const recipe = contextRecipe();
await assert.rejects(
publisher.publish({
revision: revision(recipe),
contextRecipe: recipe,
createdAtMs: 100,
}),
/recipe write failed/,
);
assert.deepEqual(calls, ['recipe']);
await assert.rejects(
publisher.publish({
revision: { ...revision(recipe), contextRef: 'localctx:sha256:other' },
contextRecipe: recipe,
createdAtMs: 100,
}),
/does not match/,
);
assert.deepEqual(calls, ['recipe']);
});
@@ -0,0 +1,225 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
LocalFileExecutionArtifactAllocator,
} = require('../../back/runtime/adapters/fs/localFileExecutionArtifactAllocator');
const {
durableLocalProcessOutput,
} = require('../../back/runtime/adapters/local-process/durableLocalProcessOutput');
const {
localExecutionArtifactId,
} = require('../../back/runtime/domain/localExecutionArtifact');
const CAPACITY_POLICY = Object.freeze({
maximumAttemptBytes: 64 * 1024,
minimumFreeBytes: 0,
});
function candidate(overrides = {}) {
return {
runId: 'run-artifact',
attemptId: 'attempt-artifact',
projectId: 'default',
taskId: 'task-artifact',
taskRevision: 'revision-1',
executorType: 'local_process',
priority: 0,
queuedAtMs: 1_760_000_000_000,
attemptCreatedAtMs: 1_760_000_000_000,
...overrides,
};
}
async function temporaryRoots(t) {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-artifact-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
return {
root,
artifacts: path.join(root, 'artifacts'),
receipts: path.join(root, 'receipts'),
};
}
function allocator(roots, policy = CAPACITY_POLICY, capacity) {
return new LocalFileExecutionArtifactAllocator(
roots.artifacts,
roots.receipts,
policy,
capacity,
);
}
test('allocates a private deterministic Artifact and serializes accepted writes', async (t) => {
const roots = await temporaryRoots(t);
const artifacts = allocator(roots);
const prepared = await artifacts.prepare(candidate());
assert.equal(prepared.logArtifactId, localExecutionArtifactId(candidate()));
assert.equal(prepared.logArtifactId.length, 36);
const capability = durableLocalProcessOutput(prepared.output);
assert.ok(capability);
assert.equal(capability.maximumBytes, CAPACITY_POLICY.maximumAttemptBytes);
assert.equal(capability.logArtifactId, prepared.logArtifactId);
assert.equal(JSON.stringify(prepared.output).includes(roots.root), false);
await Promise.all([
prepared.output.write({
stream: 'stdout',
chunk: Buffer.from('first\n'),
observedAtMs: 1,
}),
prepared.output.write({
stream: 'stderr',
chunk: Buffer.from('second\n'),
observedAtMs: 2,
}),
]);
await prepared.dispose();
assert.equal(
await fs.readFile(capability.outputFilePath, 'utf8'),
'first\nsecond\n',
);
assert.equal((await fs.stat(capability.outputFilePath)).mode & 0o777, 0o600);
assert.equal(
(await fs.stat(path.dirname(capability.outputFilePath))).mode & 0o777,
0o700,
);
await assert.rejects(
prepared.output.write({
stream: 'stdout',
chunk: Buffer.from('late'),
observedAtMs: 3,
}),
/closed/,
);
const replay = await artifacts.prepare(candidate());
assert.equal(replay.logArtifactId, prepared.logArtifactId);
await replay.output.write({
stream: 'stdout',
chunk: Buffer.from('replay\n'),
observedAtMs: 4,
});
await replay.dispose();
assert.equal(
await fs.readFile(capability.outputFilePath, 'utf8'),
'first\nsecond\nreplay\n',
);
});
test('uses a different opaque Artifact for each Attempt', async (t) => {
const roots = await temporaryRoots(t);
const artifacts = allocator(roots);
const first = await artifacts.prepare(candidate());
const second = await artifacts.prepare(candidate({ attemptId: 'attempt-2' }));
assert.notEqual(first.logArtifactId, second.logArtifactId);
await Promise.all([first.dispose(), second.dispose()]);
});
test('drains writes accepted before asynchronous disposal closes the file', async (t) => {
const roots = await temporaryRoots(t);
const artifacts = allocator(roots);
const prepared = await artifacts.prepare(candidate());
const capability = durableLocalProcessOutput(prepared.output);
const accepted = prepared.output.write({
stream: 'stdout',
chunk: Buffer.from('accepted-before-close'),
observedAtMs: 1,
});
await prepared.dispose();
await accepted;
assert.equal(
await fs.readFile(capability.outputFilePath, 'utf8'),
'accepted-before-close',
);
});
test('rejects relative roots and refuses a symlink Artifact target', async (t) => {
assert.throws(
() =>
new LocalFileExecutionArtifactAllocator(
'relative',
'/tmp/receipts',
CAPACITY_POLICY,
),
/must be absolute/,
);
const roots = await temporaryRoots(t);
const reference = candidate();
const artifactId = localExecutionArtifactId(reference);
const shard = artifactId.slice(6, 8);
const directory = path.join(roots.artifacts, shard);
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
const outside = path.join(roots.root, 'outside.log');
await fs.writeFile(outside, 'outside');
await fs.symlink(outside, path.join(directory, `${artifactId}.log`));
const artifacts = allocator(roots);
await assert.rejects(artifacts.prepare(reference));
assert.equal(await fs.readFile(outside, 'utf8'), 'outside');
});
test('hard-caps ordinary output and never writes a partial byte past quota', async (t) => {
const roots = await temporaryRoots(t);
const artifacts = allocator(roots);
const prepared = await artifacts.prepare(candidate());
const capability = durableLocalProcessOutput(prepared.output);
const oversized = Buffer.alloc(
CAPACITY_POLICY.maximumAttemptBytes + 37,
0x61,
);
await assert.rejects(
prepared.output.write({
stream: 'stdout',
chunk: oversized,
observedAtMs: 1,
}),
/byte quota/,
);
await assert.rejects(
prepared.output.write({
stream: 'stderr',
chunk: Buffer.from('must-not-append'),
observedAtMs: 2,
}),
/byte quota/,
);
await prepared.dispose();
const stored = await fs.readFile(capability.outputFilePath);
assert.equal(stored.length, CAPACITY_POLICY.maximumAttemptBytes);
assert.equal(stored.equals(oversized.subarray(0, stored.length)), true);
});
test('reserves free space before opening an Attempt Artifact', async (t) => {
const roots = await temporaryRoots(t);
let inspected = 0;
const artifacts = allocator(
roots,
{ maximumAttemptBytes: 64 * 1024, minimumFreeBytes: 128 * 1024 },
{
async inspect(root) {
inspected += 1;
assert.equal(root, roots.artifacts);
return {
availableBytes: BigInt(128 * 1024),
totalBytes: BigInt(1024 * 1024),
};
},
},
);
await assert.rejects(
artifacts.prepare(candidate()),
/capacity is unavailable/,
);
assert.equal(inspected, 1);
const artifactId = localExecutionArtifactId(candidate());
const target = path.join(
roots.artifacts,
artifactId.slice(6, 8),
`${artifactId}.log`,
);
await assert.rejects(fs.lstat(target), /ENOENT/);
});
+457
View File
@@ -0,0 +1,457 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const { setTimeout: delay } = require('node:timers/promises');
const {
LocalProcessExecutor,
} = require('../../back/runtime/adapters/local-process/localProcessExecutor');
const {
PosixProcessTerminator,
} = require('../../back/runtime/adapters/local-process/processTerminator');
const {
ExecutorCapabilityUnavailableError,
ExecutorHandleNotFoundError,
ExecutorStartError,
InvalidExecutionSpecError,
} = require('../../back/runtime/domain/executorErrors');
let idSequence = 300;
function nextId() {
idSequence += 1;
return `019f70e0-0000-7000-8000-${String(idSequence).padStart(12, '0')}`;
}
function createSpec(overrides = {}) {
return {
runId: nextId(),
attemptId: nextId(),
projectId: 'default',
taskId: 'executor-contract-test',
taskRevision: 'revision-1',
command: {
kind: 'argv',
file: process.execPath,
args: ['-e', "process.stdout.write('ok')"],
},
environmentPolicy: 'isolated',
terminationGraceMs: 100,
...overrides,
};
}
function createOutputCollector(write) {
const chunks = { stdout: [], stderr: [] };
return {
chunks,
context: {
environment: {},
output: {
async write(output) {
chunks[output.stream].push(Buffer.from(output.chunk));
await write?.(output);
},
},
},
text(stream) {
return Buffer.concat(chunks[stream]).toString('utf8');
},
};
}
test(
'executes argv commands and drains ordered stdout/stderr before completion',
{ timeout: 5_000 },
async () => {
const executor = new LocalProcessExecutor({ createHandleId: nextId });
const output = createOutputCollector(async () => delay(2));
const handle = await executor.start(
createSpec({
command: {
kind: 'argv',
file: process.execPath,
args: [
'-e',
"process.stdout.write('stdout-value'); process.stderr.write('stderr-value')",
],
},
}),
output.context,
);
assert.ok(handle.pid > 0);
assert.equal(handle.executorType, 'local_process');
const result = await handle.completion;
assert.equal(result.outcome, 'succeeded');
assert.equal(result.exitCode, 0);
assert.equal(output.text('stdout'), 'stdout-value');
assert.equal(output.text('stderr'), 'stderr-value');
assert.deepEqual(await executor.inspect(handle), {
status: 'exited',
result,
});
},
);
test(
'supports explicit shell compatibility commands and stable non-zero results',
{ timeout: 5_000 },
async () => {
const executor = new LocalProcessExecutor({ createHandleId: nextId });
const output = createOutputCollector();
const handle = await executor.start(
createSpec({
command: {
kind: 'shell',
command: "printf 'shell-stdout'; printf 'shell-stderr' >&2; exit 7",
shell: '/bin/bash',
},
}),
output.context,
);
const result = await handle.completion;
assert.equal(result.outcome, 'failed');
assert.equal(result.exitCode, 7);
assert.equal(result.errorCode, 'PROCESS_EXIT_NON_ZERO');
assert.equal(output.text('stdout'), 'shell-stdout');
assert.equal(output.text('stderr'), 'shell-stderr');
},
);
test(
'honors isolated environment and an authorized working directory',
{ timeout: 5_000 },
async () => {
const previous = process.env.QL3_PARENT_ONLY_VALUE;
process.env.QL3_PARENT_ONLY_VALUE = 'must-not-leak';
try {
const executor = new LocalProcessExecutor({ createHandleId: nextId });
const output = createOutputCollector();
output.context.environment = { QL3_SUPPLIED_VALUE: 'visible' };
const handle = await executor.start(
createSpec({
workingDirectory: process.cwd(),
command: {
kind: 'argv',
file: process.execPath,
args: [
'-e',
'process.stdout.write(JSON.stringify({ cwd: process.cwd(), supplied: process.env.QL3_SUPPLIED_VALUE, inherited: process.env.QL3_PARENT_ONLY_VALUE }))',
],
},
}),
output.context,
);
assert.equal((await handle.completion).outcome, 'succeeded');
assert.deepEqual(JSON.parse(output.text('stdout')), {
cwd: process.cwd(),
supplied: 'visible',
});
} finally {
if (previous === undefined) delete process.env.QL3_PARENT_ONLY_VALUE;
else process.env.QL3_PARENT_ONLY_VALUE = previous;
}
},
);
test(
'applies output backpressure without buffering the complete process output',
{ timeout: 10_000 },
async () => {
let activeWrites = 0;
let maximumActiveWrites = 0;
const executor = new LocalProcessExecutor({ createHandleId: nextId });
const output = createOutputCollector(async () => {
activeWrites += 1;
maximumActiveWrites = Math.max(maximumActiveWrites, activeWrites);
await delay(1);
activeWrites -= 1;
});
const handle = await executor.start(
createSpec({
command: {
kind: 'argv',
file: process.execPath,
args: [
'-e',
"for (let i = 0; i < 1000; i += 1) process.stdout.write('x'.repeat(1024))",
],
},
}),
output.context,
);
assert.equal((await handle.completion).outcome, 'succeeded');
assert.equal(output.text('stdout').length, 1_024_000);
assert.equal(maximumActiveWrites, 1);
},
);
test(
'continues draining output and reports a bounded diagnostic when the sink fails',
{ timeout: 5_000 },
async () => {
const executor = new LocalProcessExecutor({ createHandleId: nextId });
let writes = 0;
const context = {
environment: {},
output: {
async write() {
writes += 1;
throw new Error('sink contains potentially sensitive details');
},
},
};
const handle = await executor.start(
createSpec({
command: {
kind: 'argv',
file: process.execPath,
args: [
'-e',
"for (let i = 0; i < 200; i += 1) process.stdout.write('x'.repeat(1024))",
],
},
}),
context,
);
const result = await handle.completion;
assert.equal(result.outcome, 'succeeded');
assert.equal(writes, 1);
assert.deepEqual(result.diagnostics, [
{
code: 'OUTPUT_SINK_FAILED',
summary: 'Execution output sink failed; output may be incomplete',
},
]);
assert.doesNotMatch(JSON.stringify(result), /potentially sensitive/);
},
);
test(
'cancels a process group and keeps repeated stop calls idempotent',
{ timeout: 5_000 },
async () => {
const executor = new LocalProcessExecutor({ createHandleId: nextId });
const output = createOutputCollector();
const handle = await executor.start(
createSpec({
command: {
kind: 'argv',
file: process.execPath,
args: ['-e', 'setInterval(() => undefined, 1000)'],
},
}),
output.context,
);
const stopResult = await executor.stop(handle, {
kind: 'user',
requestedAtMs: Date.now(),
});
const result = await handle.completion;
assert.deepEqual(stopResult, {
status: 'termination_requested',
termSignalSent: true,
killSignalSent: false,
});
assert.equal(result.outcome, 'cancelled');
assert.equal(result.signal, 'SIGTERM');
assert.deepEqual(
await executor.stop(handle, {
kind: 'user',
requestedAtMs: Date.now(),
}),
{
status: 'already_exited',
termSignalSent: false,
killSignalSent: false,
},
);
},
);
test(
'escalates ignored timeout termination to SIGKILL and reports timed_out',
{ timeout: 5_000 },
async () => {
const executor = new LocalProcessExecutor({ createHandleId: nextId });
const output = createOutputCollector();
const handle = await executor.start(
createSpec({
// The complete test suite runs files concurrently; leave enough time
// for the child Node process to install its SIGTERM handler first.
timeoutMs: 1_000,
terminationGraceMs: 30,
command: {
kind: 'argv',
file: process.execPath,
args: [
'-e',
"process.on('SIGTERM', () => undefined); setInterval(() => undefined, 1000)",
],
},
}),
output.context,
);
const result = await handle.completion;
assert.equal(result.outcome, 'timed_out');
assert.equal(result.signal, 'SIGKILL');
assert.equal(result.errorCode, 'EXECUTION_TIMED_OUT');
},
);
test(
'observes AbortSignal cancellation without leaking listeners into completion',
{ timeout: 5_000 },
async () => {
const controller = new AbortController();
const executor = new LocalProcessExecutor({ createHandleId: nextId });
const output = createOutputCollector();
output.context.signal = controller.signal;
const handle = await executor.start(
createSpec({
command: {
kind: 'argv',
file: process.execPath,
args: ['-e', 'setInterval(() => undefined, 1000)'],
},
}),
output.context,
);
controller.abort();
assert.equal((await handle.completion).outcome, 'cancelled');
},
);
test('rejects unsupported required limits and invalid specs before spawn', async () => {
const executor = new LocalProcessExecutor({ createHandleId: nextId });
const output = createOutputCollector();
await assert.rejects(
executor.start(
createSpec({
resourcePolicy: {
memoryBytes: { value: 64 * 1024 * 1024, enforcement: 'required' },
},
}),
output.context,
),
ExecutorCapabilityUnavailableError,
);
await assert.rejects(
executor.start(
createSpec({ workingDirectory: 'relative/path' }),
output.context,
),
InvalidExecutionSpecError,
);
const bestEffort = await executor.start(
createSpec({
resourcePolicy: {
memoryBytes: {
value: 64 * 1024 * 1024,
enforcement: 'best_effort',
},
networkIsolation: 'best_effort',
},
}),
output.context,
);
assert.deepEqual((await bestEffort.completion).diagnostics, [
{
code: 'RESOURCE_POLICY_BEST_EFFORT_UNAVAILABLE',
summary:
'Best-effort capabilities were unavailable: memoryLimit, networkIsolation',
},
]);
});
test('maps spawn failures and rejects handles owned by another executor', async () => {
const executor = new LocalProcessExecutor({ createHandleId: nextId });
const output = createOutputCollector();
await assert.rejects(
executor.start(
createSpec({
command: {
kind: 'argv',
file: '/path/that/does/not/exist/ql3-command',
args: [],
},
}),
output.context,
),
ExecutorStartError,
);
await assert.rejects(
executor.inspect({
id: nextId(),
executorType: 'local_process',
runId: nextId(),
attemptId: nextId(),
startedAtMs: Date.now(),
completion: Promise.resolve({
outcome: 'lost',
startedAtMs: Date.now(),
finishedAtMs: Date.now(),
}),
}),
ExecutorHandleNotFoundError,
);
});
test('process terminator stops after TERM or escalates after the grace window', async () => {
let resolveClosed;
const closed = new Promise((resolve) => {
resolveClosed = resolve;
});
const signals = [];
const graceful = new PosixProcessTerminator((pid, signal) => {
signals.push([pid, signal]);
if (signal === 'SIGTERM') resolveClosed();
});
assert.deepEqual(
await graceful.terminate({
pid: 42,
processGroup: true,
graceMs: 50,
closed,
}),
{
alreadyExited: false,
termSignalSent: true,
killSignalSent: false,
},
);
assert.deepEqual(signals, [[-42, 'SIGTERM']]);
const escalatedSignals = [];
const escalated = new PosixProcessTerminator((pid, signal) => {
escalatedSignals.push([pid, signal]);
});
assert.deepEqual(
await escalated.terminate({
pid: 43,
processGroup: false,
graceMs: 1,
closed: new Promise(() => undefined),
}),
{
alreadyExited: false,
termSignalSent: true,
killSignalSent: true,
},
);
assert.deepEqual(escalatedSignals, [
[43, 'SIGTERM'],
[43, 'SIGKILL'],
]);
});
+217
View File
@@ -0,0 +1,217 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
createLocalProcessDurableHandle,
LinuxProcProcessIdentityProvider,
LocalProcessPersistedExecutionInspector,
MAX_LOCAL_PROCESS_DURABLE_HANDLE_BYTES,
parseLocalProcessDurableHandle,
} = require('../../back/runtime/adapters/local-process/localProcessIdentity');
const {
LocalProcessExecutor,
} = require('../../back/runtime/adapters/local-process/localProcessExecutor');
let idSequence = 900;
function nextId() {
idSequence += 1;
return `019f70f0-0000-7000-8000-${String(idSequence).padStart(12, '0')}`;
}
function identity(overrides = {}) {
return {
platform: 'linux',
bootId: '11111111-2222-3333-4444-555555555555',
pid: 4321,
processGroupId: 4321,
startTimeTicks: '987654321',
...overrides,
};
}
function procStat({
pid = 4321,
processGroupId = 4321,
startTimeTicks = '987654321',
state = 'S',
} = {}) {
const fields = Array(20).fill('0');
fields[0] = state;
fields[1] = '1';
fields[2] = String(processGroupId);
fields[19] = startTimeTicks;
return `${pid} (node worker with spaces) ${fields.join(' ')}`;
}
function missingFile() {
const error = new Error('not found');
error.code = 'ENOENT';
return error;
}
test('round-trips a bounded opaque Linux process identity', () => {
const handleId = nextId();
const durableHandle = createLocalProcessDurableHandle(handleId, identity());
assert.ok(
Buffer.byteLength(durableHandle) <= MAX_LOCAL_PROCESS_DURABLE_HANDLE_BYTES,
);
assert.deepEqual(parseLocalProcessDurableHandle(durableHandle), {
handleId,
identity: identity(),
});
assert.doesNotMatch(durableHandle, /node worker|command|environment/);
});
test('rejects malformed, oversized, and unsafe durable handles', () => {
assert.equal(parseLocalProcessDurableHandle('legacy-uuid-only'), null);
assert.equal(parseLocalProcessDurableHandle('ql3lp1.not+base64url'), null);
assert.equal(
parseLocalProcessDurableHandle(
`ql3lp1.${'a'.repeat(MAX_LOCAL_PROCESS_DURABLE_HANDLE_BYTES)}`,
),
null,
);
assert.throws(() =>
createLocalProcessDurableHandle('invalid\0handle', identity()),
);
assert.throws(() =>
createLocalProcessDurableHandle(nextId(), identity({ pid: 0 })),
);
});
test('captures and verifies boot, start-time, and process-group identity', async () => {
const files = new Map([
[
'/proc/sys/kernel/random/boot_id',
'11111111-2222-3333-4444-555555555555\n',
],
['/proc/4321/stat', procStat()],
]);
const provider = new LinuxProcProcessIdentityProvider({
platform: 'linux',
async readTextFile(path) {
const value = files.get(path);
if (value === undefined) throw missingFile();
return value;
},
});
const captured = await provider.capture(4321);
assert.deepEqual(captured, identity());
assert.deepEqual(await provider.inspect(captured), { status: 'running' });
files.set('/proc/4321/stat', procStat({ startTimeTicks: '987654322' }));
assert.deepEqual(await provider.inspect(captured), {
status: 'identity_mismatch',
});
files.set('/proc/4321/stat', procStat({ state: 'Z' }));
assert.deepEqual(await provider.inspect(captured), { status: 'exited' });
files.delete('/proc/4321/stat');
assert.deepEqual(await provider.inspect(captured), { status: 'exited' });
});
test('never accepts an identity from another boot or unsupported platform', async () => {
const provider = new LinuxProcProcessIdentityProvider({
platform: 'linux',
async readTextFile(path) {
if (path === '/proc/sys/kernel/random/boot_id') {
return 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee';
}
return procStat();
},
});
assert.deepEqual(await provider.inspect(identity()), {
status: 'identity_mismatch',
});
const unsupported = new LinuxProcProcessIdentityProvider({
platform: 'darwin',
async readTextFile() {
throw new Error('must not read /proc');
},
});
assert.equal(await unsupported.capture(4321), null);
assert.deepEqual(await unsupported.inspect(identity()), {
status: 'unsupported',
});
});
test('classifies invalid persisted values before consulting the OS', async () => {
let inspections = 0;
const inspector = new LocalProcessPersistedExecutionInspector({
async capture() {
return null;
},
async inspect() {
inspections += 1;
return { status: 'running' };
},
});
assert.deepEqual(await inspector.inspect('not-a-durable-handle'), {
status: 'invalid',
});
assert.equal(inspections, 0);
const durableHandle = createLocalProcessDurableHandle(nextId(), identity());
assert.deepEqual(await inspector.inspect(durableHandle), {
status: 'running',
identityPid: 4321,
});
assert.equal(inspections, 1);
});
test(
'LocalProcessExecutor exposes a durable identity without replacing its live handle',
{ timeout: 5_000 },
async () => {
const observed = identity();
const executor = new LocalProcessExecutor({
createHandleId: nextId,
identityProvider: {
async capture(pid) {
return { ...observed, pid, processGroupId: pid };
},
async inspect() {
return { status: 'running' };
},
},
});
const handle = await executor.start(
{
runId: nextId(),
attemptId: nextId(),
projectId: 'default',
taskId: 'durable-handle-test',
taskRevision: 'revision-1',
command: {
kind: 'argv',
file: process.execPath,
args: ['-e', 'setInterval(() => undefined, 1000)'],
},
environmentPolicy: 'isolated',
terminationGraceMs: 100,
},
{
environment: {},
output: { async write() {} },
},
);
const parsed = parseLocalProcessDurableHandle(handle.durableHandle);
assert.equal(parsed.handleId, handle.id);
assert.equal(parsed.identity.pid, handle.pid);
assert.equal(parsed.identity.processGroupId, handle.pid);
await executor.stop(handle, {
kind: 'user',
requestedAtMs: Date.now(),
});
assert.equal((await handle.completion).outcome, 'cancelled');
},
);
+310
View File
@@ -0,0 +1,310 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
LocalRunDispatcher,
} = require('../../back/runtime/application/localRunDispatcher');
const {
PrimaryClaimedRunRejectedError,
} = require('../../back/runtime/application/primaryRunOrchestrator');
const NOW = 1_760_000_000_000;
function deferred() {
let resolve;
let reject;
const promise = new Promise((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, resolve, reject };
}
function candidate(id, overrides = {}) {
return {
runId: `run-${id}`,
attemptId: `attempt-${id}`,
projectId: 'default',
taskId: `task-${id}`,
taskRevision: 'revision-1',
executorType: 'local_process',
priority: 0,
queuedAtMs: NOW,
attemptCreatedAtMs: NOW,
...overrides,
};
}
function specFor(reference, overrides = {}) {
return {
runId: reference.runId,
attemptId: reference.attemptId,
projectId: reference.projectId,
taskId: reference.taskId,
taskRevision: reference.taskRevision,
command: { kind: 'argv', file: '/bin/true', args: ['original'] },
environmentPolicy: 'isolated',
terminationGraceMs: 100,
...overrides,
};
}
function candidateSource(pages, calls = []) {
let page = 0;
return {
async listCandidates(options) {
calls.push(options);
return pages[page++] ?? [];
},
};
}
function activeFor(command, completion = Promise.resolve({})) {
return {
run: { id: command.runId },
attempt: { id: command.attemptId },
handle: {},
completion,
async cancel() {},
};
}
test('does no plan or activation work when no local candidate exists', async () => {
let planCalls = 0;
let activationCalls = 0;
const dispatcher = new LocalRunDispatcher(
candidateSource([[]]),
{
async prepare() {
planCalls += 1;
return null;
},
},
{
async activateClaimed() {
activationCalls += 1;
},
},
{ executorType: 'local_process', clock: { now: () => NOW } },
);
const result = await dispatcher.dispatchOnce();
assert.deepEqual([result.status, result.reason], ['idle', 'no_candidates']);
assert.deepEqual([planCalls, activationCalls], [0, 0]);
});
test('bounded-pages to a matching executor and activates one cloned pinned plan', async () => {
const remote = candidate('remote', { executorType: 'remote_worker' });
const local = candidate('local', {
queuedAtMs: NOW + 1,
attemptCreatedAtMs: NOW + 1,
});
const pages = [];
const sourceSpec = specFor(local, { timeoutMs: 5_000 });
const context = { environment: {}, output: { async write() {} } };
const completion = deferred();
const activations = [];
let disposed = 0;
const dispatcher = new LocalRunDispatcher(
candidateSource([[remote], [local]], pages),
{
async prepare(reference) {
assert.equal(Object.isFrozen(reference), true);
assert.equal(reference.attemptId, local.attemptId);
return {
executionSpec: sourceSpec,
context,
dispose() {
disposed += 1;
},
};
},
},
{
async activateClaimed(command) {
activations.push({ command, spec: command.createSpec() });
return activeFor(command, completion.promise);
},
},
{
executorType: 'local_process',
pageSize: 1,
maxPages: 2,
clock: { now: () => NOW },
},
);
const result = await dispatcher.dispatchOnce();
assert.equal(result.status, 'activated');
assert.deepEqual(result.stats, {
pages: 2,
candidatesScanned: 2,
executorMismatches: 1,
plansUnavailable: 0,
activationRaces: 0,
});
assert.equal(result.truncated, true);
assert.equal(pages[1].after.attemptId, remote.attemptId);
assert.equal(activations[0].command.timeoutMs, 5_000);
assert.equal(activations[0].command.context, context);
sourceSpec.command.args[0] = 'mutated';
assert.deepEqual(activations[0].spec.command.args, ['original']);
assert.equal(disposed, 0);
completion.resolve({});
await new Promise((resolve) => setImmediate(resolve));
assert.equal(disposed, 1);
});
test('continues after a stale candidate and activates the next Attempt', async () => {
const first = candidate('first');
const second = candidate('second', {
queuedAtMs: NOW + 1,
attemptCreatedAtMs: NOW + 1,
});
const attempted = [];
const dispatcher = new LocalRunDispatcher(
candidateSource([[first, second]]),
{
async prepare(reference) {
return {
executionSpec: specFor(reference),
context: { environment: {}, output: { async write() {} } },
};
},
},
{
async activateClaimed(command) {
attempted.push(command.attemptId);
if (command.attemptId === first.attemptId) {
throw new PrimaryClaimedRunRejectedError('not_queued');
}
return activeFor(command);
},
},
{
executorType: 'local_process',
pageSize: 2,
clock: { now: () => NOW },
},
);
const result = await dispatcher.dispatchOnce();
assert.equal(result.status, 'activated');
assert.deepEqual(attempted, [first.attemptId, second.attemptId]);
assert.equal(result.stats.activationRaces, 1);
});
test('fails closed on plan drift before activation and disposes its context', async () => {
const reference = candidate('drift');
let activationCalls = 0;
let disposeCalls = 0;
const dispatcher = new LocalRunDispatcher(
candidateSource([[reference]]),
{
async prepare() {
return {
executionSpec: specFor(reference, { taskRevision: 'drifted' }),
context: { environment: {}, output: { async write() {} } },
dispose() {
disposeCalls += 1;
},
};
},
},
{
async activateClaimed() {
activationCalls += 1;
},
},
{ executorType: 'local_process', clock: { now: () => NOW } },
);
await assert.rejects(dispatcher.dispatchOnce(), /identity does not match/);
assert.deepEqual([activationCalls, disposeCalls], [0, 1]);
});
test('reports missing plans and rejects unordered or unbounded pages', async () => {
const reference = candidate('missing');
const missing = new LocalRunDispatcher(
candidateSource([[reference]]),
{
async prepare() {
return null;
},
},
{ async activateClaimed() {} },
{ executorType: 'local_process', clock: { now: () => NOW } },
);
const result = await missing.dispatchOnce();
assert.deepEqual(
[result.status, result.reason],
['idle', 'plans_unavailable'],
);
const duplicate = new LocalRunDispatcher(
candidateSource([[reference, reference]]),
{
async prepare() {
return null;
},
},
{ async activateClaimed() {} },
{
executorType: 'local_process',
pageSize: 2,
clock: { now: () => NOW },
},
);
await assert.rejects(duplicate.dispatchOnce(), /not strictly ordered/);
assert.throws(
() =>
new LocalRunDispatcher(
candidateSource([[]]),
{
async prepare() {
return null;
},
},
{ async activateClaimed() {} },
{
executorType: 'local_process',
maxPages: 17,
clock: { now: () => NOW },
},
),
RangeError,
);
});
test('passes the Artifact identity to activation and awaits failed-plan cleanup', async () => {
const reference = candidate('artifact');
const order = [];
const dispatcher = new LocalRunDispatcher(
candidateSource([[reference]]),
{
async prepare() {
return {
executionSpec: specFor(reference),
context: { environment: {}, output: { async write() {} } },
logArtifactId: `local-${'a'.repeat(30)}`,
async dispose() {
await new Promise((resolve) => setImmediate(resolve));
order.push('disposed');
},
};
},
},
{
async activateClaimed(command) {
order.push(command.logArtifactId);
throw new PrimaryClaimedRunRejectedError('not_queued');
},
},
{ executorType: 'local_process', clock: { now: () => NOW } },
);
const result = await dispatcher.dispatchOnce();
assert.equal(result.status, 'idle');
assert.deepEqual(order, [`local-${'a'.repeat(30)}`, 'disposed']);
});
+440
View File
@@ -0,0 +1,440 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const { QueryTypes, Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const {
LOCAL_SECRET_ENVELOPE_TABLE,
localSecretEnvelopeMigration,
} = require('../../back/migrations/0014-local-secret-envelopes');
const { runMigrations } = require('../../back/migrations/runner');
const {
decryptLocalSecretEnvelopeToBuffer,
encryptLocalSecretEnvelope,
} = require('../../back/runtime/adapters/crypto/aes256GcmLocalSecret');
const {
LocalSecretKeyringFileProvider,
} = require('../../back/runtime/adapters/fs/localSecretKeyringFileProvider');
const {
LegacySequelizeLocalSecretEnvelopeRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/localSecretEnvelopeRepository');
const {
EncryptedLocalSecretService,
LocalSecretMutationConflictError,
LocalSecretVersionConflictError,
} = require('../../back/runtime/application/encryptedLocalSecretService');
const {
LOCAL_SECRET_ALGORITHM,
LocalSecretUnavailableError,
createLocalSecretRef,
parseLocalSecretRef,
} = require('../../back/runtime/domain/localSecret');
const KEY = Buffer.alloc(32, 0x11);
function keyProvider(key = KEY) {
return {
async active() {
return { keyId: 'edge-key-1', key: Uint8Array.from(key) };
},
async resolve(keyId) {
return keyId === 'edge-key-1'
? { keyId, key: Uint8Array.from(key) }
: null;
},
};
}
function candidate(projectId = 'default') {
return {
runId: 'run-secret',
attemptId: 'attempt-secret',
projectId,
taskId: 'task-secret',
taskRevision: 'revision-secret',
executorType: 'local_process',
priority: 0,
queuedAtMs: 1_760_000_000_000,
attemptCreatedAtMs: 1_760_000_000_000,
};
}
async function createStore(t, storage = ':memory:') {
const database = new Sequelize({
dialect: 'sqlite',
storage,
logging: false,
});
t.after(() => database.close());
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations: [localSecretEnvelopeMigration],
logger: { info() {} },
});
let nonce = 0;
const repository = new LegacySequelizeLocalSecretEnvelopeRepository(database);
const service = new EncryptedLocalSecretService(
repository,
keyProvider(),
() => Buffer.alloc(12, nonce++),
);
return { database, repository, service };
}
test('uses a stable AES-256-GCM envelope vector bound to metadata AAD', () => {
const envelope = encryptLocalSecretEnvelope(
{
projectId: 'default',
name: 'TOKEN',
version: 1,
mutationId: 'mutation-1',
keyId: 'edge-key-1',
algorithm: LOCAL_SECRET_ALGORITHM,
createdAtMs: 100,
},
'fixed-secret',
KEY,
() => Buffer.alloc(12, 0x22),
);
assert.deepEqual(
{
nonce: envelope.nonce,
ciphertext: envelope.ciphertext,
authTag: envelope.authTag,
},
{
nonce: 'IiIiIiIiIiIiIiIi',
ciphertext: 'cZ5_LKTi7DqGTbtI',
authTag: 'tsA93yEy5RJCD5Q6wngfgA',
},
);
const plaintext = decryptLocalSecretEnvelopeToBuffer(envelope, KEY);
assert.equal(plaintext.toString('utf8'), 'fixed-secret');
plaintext.fill(0);
assert.throws(
() =>
decryptLocalSecretEnvelopeToBuffer(
{ ...envelope, projectId: 'another-project' },
KEY,
),
LocalSecretUnavailableError,
);
});
test('SecretRef is canonical, project-bound and optionally pins an integer version', () => {
const current = createLocalSecretRef({ projectId: 'default', name: 'TOKEN' });
const exact = createLocalSecretRef({
projectId: 'default',
name: 'TOKEN',
version: 2,
});
assert.deepEqual(parseLocalSecretRef(current), {
projectId: 'default',
name: 'TOKEN',
});
assert.deepEqual(parseLocalSecretRef(exact), {
projectId: 'default',
name: 'TOKEN',
version: 2,
});
const unknownField = Buffer.from(
JSON.stringify({ projectId: 'default', name: 'TOKEN', extra: true }),
).toString('base64url');
assert.throws(() => parseLocalSecretRef(`qlsecret:v1:${unknownField}`));
assert.throws(() =>
createLocalSecretRef({ projectId: 'default', name: 'TOKEN', version: 0 }),
);
});
test('creates and rotates append-only versions without storing plaintext', async (t) => {
const { database, service } = await createStore(t);
const firstPlaintext = 'first-plaintext-never-persisted';
const secondPlaintext = 'second-plaintext-never-persisted';
const first = await service.put({
projectId: 'default',
name: 'TOKEN',
plaintext: firstPlaintext,
mutationId: 'create-token',
expectedCurrentVersion: 0,
createdAtMs: 100,
});
const second = await service.put({
projectId: 'default',
name: 'TOKEN',
plaintext: secondPlaintext,
mutationId: 'rotate-token',
expectedCurrentVersion: 1,
createdAtMs: 200,
});
assert.deepEqual(
{ status: first.status, version: first.version },
{ status: 'inserted', version: 1 },
);
assert.deepEqual(
{ status: second.status, version: second.version },
{ status: 'inserted', version: 2 },
);
const resolved = await service.resolve({
candidate: candidate(),
secretRefs: [
createLocalSecretRef({ projectId: 'default', name: 'TOKEN' }),
first.secretRef,
second.secretRef,
],
});
assert.deepEqual(resolved, [
secondPlaintext,
firstPlaintext,
secondPlaintext,
]);
const rows = await database.query(
`SELECT * FROM "${LOCAL_SECRET_ENVELOPE_TABLE}" ORDER BY version`,
{ type: QueryTypes.SELECT },
);
assert.equal(rows.length, 2);
for (const row of rows) {
assert.ok(Buffer.isBuffer(row.ciphertext));
assert.equal(row.ciphertext.includes(Buffer.from(firstPlaintext)), false);
assert.equal(row.ciphertext.includes(Buffer.from(secondPlaintext)), false);
assert.equal(JSON.stringify(row).includes(firstPlaintext), false);
assert.equal(JSON.stringify(row).includes(secondPlaintext), false);
}
});
test('replays mutations idempotently and fences stale rotations', async (t) => {
const { service } = await createStore(t);
const command = {
projectId: 'default',
name: 'TOKEN',
plaintext: 'same-value',
mutationId: 'create-token',
expectedCurrentVersion: 0,
createdAtMs: 100,
};
assert.equal((await service.put(command)).status, 'inserted');
assert.equal(
(await service.put({ ...command, createdAtMs: 999 })).status,
'existing',
);
await assert.rejects(
service.put({ ...command, plaintext: 'different-value' }),
LocalSecretMutationConflictError,
);
await assert.rejects(
service.put({
...command,
mutationId: 'stale-create',
plaintext: 'stale-value',
}),
LocalSecretVersionConflictError,
);
});
test('batch resolution preserves position, supports missing, and checks Project first', async (t) => {
const { repository, service } = await createStore(t);
await service.put({
projectId: 'default',
name: 'A',
plaintext: 'value-a',
mutationId: 'create-a',
expectedCurrentVersion: 0,
createdAtMs: 100,
});
await service.put({
projectId: 'default',
name: 'B',
plaintext: 'value-b',
mutationId: 'create-b',
expectedCurrentVersion: 0,
createdAtMs: 100,
});
const refs = [
{ projectId: 'default', name: 'B' },
{ projectId: 'default', name: 'missing' },
{ projectId: 'default', name: 'A', version: 1 },
];
const envelopes = await repository.resolveMany(refs);
assert.deepEqual(
envelopes.map((item) => item && item.name),
['B', null, 'A'],
);
assert.equal(
await service.resolve({
candidate: candidate(),
secretRefs: refs.map(createLocalSecretRef),
}),
null,
);
let databaseCalls = 0;
const isolated = new EncryptedLocalSecretService(
{
async append() {
throw new Error('not used');
},
async findByMutation() {
throw new Error('not used');
},
async resolveMany() {
databaseCalls += 1;
return [];
},
},
keyProvider(),
);
await assert.rejects(
isolated.resolve({
candidate: candidate('default'),
secretRefs: [
createLocalSecretRef({ projectId: 'another', name: 'TOKEN' }),
],
}),
LocalSecretUnavailableError,
);
assert.equal(databaseCalls, 0);
});
test('corrupt ciphertext and wrong keys fail with a generic non-secret error', async (t) => {
const { database, repository, service } = await createStore(t);
const plaintext = 'must-not-appear-in-errors';
await service.put({
projectId: 'default',
name: 'TOKEN',
plaintext,
mutationId: 'create-token',
expectedCurrentVersion: 0,
createdAtMs: 100,
});
await database
.getQueryInterface()
.bulkUpdate(
LOCAL_SECRET_ENVELOPE_TABLE,
{ auth_tag: Buffer.alloc(16) },
{ project_id: 'default', secret_name: 'TOKEN', version: 1 },
);
const request = {
candidate: candidate(),
secretRefs: [createLocalSecretRef({ projectId: 'default', name: 'TOKEN' })],
};
await assert.rejects(service.resolve(request), (error) => {
assert.equal(error.constructor, LocalSecretUnavailableError);
assert.equal(error.message, 'Local Secret is unavailable');
assert.equal(error.message.includes(plaintext), false);
assert.equal(error.message.includes('TOKEN'), false);
return true;
});
const wrongKeyService = new EncryptedLocalSecretService(
repository,
keyProvider(Buffer.alloc(32, 0x33)),
);
await assert.rejects(
wrongKeyService.resolve(request),
LocalSecretUnavailableError,
);
});
test('private keyring reloads rotation and rejects broad modes and symlinks', async (t) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-secret-keyring-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
const keyringPath = path.join(root, 'keyring.json');
const manifest = (activeKeyId, keys) =>
JSON.stringify({ version: 1, activeKeyId, keys });
const first = Buffer.alloc(32, 0x11).toString('base64url');
const second = Buffer.alloc(32, 0x22).toString('base64url');
await fs.writeFile(keyringPath, manifest('key-1', { 'key-1': first }), {
mode: 0o600,
});
const provider = new LocalSecretKeyringFileProvider(keyringPath);
assert.equal((await provider.active()).keyId, 'key-1');
await fs.writeFile(
keyringPath,
manifest('key-2', { 'key-1': first, 'key-2': second }),
);
await fs.chmod(keyringPath, 0o600);
assert.equal((await provider.active()).keyId, 'key-2');
assert.equal(
Buffer.from((await provider.resolve('key-1')).key).equals(KEY),
true,
);
await fs.chmod(keyringPath, 0o644);
await assert.rejects(provider.active(), LocalSecretUnavailableError);
await fs.chmod(keyringPath, 0o600);
const symlinkPath = path.join(root, 'keyring-link.json');
await fs.symlink(keyringPath, symlinkPath);
await assert.rejects(
new LocalSecretKeyringFileProvider(symlinkPath).active(),
LocalSecretUnavailableError,
);
await fs.writeFile(
keyringPath,
JSON.stringify({ version: 1, activeKeyId: 'key-1', keys: {}, extra: true }),
);
await fs.chmod(keyringPath, 0o600);
await assert.rejects(provider.active(), LocalSecretUnavailableError);
});
test('concurrent rotations serialize so exactly one expected version wins', async (t) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-secret-db-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
const storage = path.join(root, 'database.sqlite');
const firstStore = await createStore(t, storage);
await firstStore.service.put({
projectId: 'default',
name: 'TOKEN',
plaintext: 'version-one',
mutationId: 'create-token',
expectedCurrentVersion: 0,
createdAtMs: 100,
});
const secondDatabase = new Sequelize({
dialect: 'sqlite',
storage,
logging: false,
});
t.after(() => secondDatabase.close());
const secondService = new EncryptedLocalSecretService(
new LegacySequelizeLocalSecretEnvelopeRepository(secondDatabase),
keyProvider(),
);
const rotations = await Promise.allSettled([
firstStore.service.put({
projectId: 'default',
name: 'TOKEN',
plaintext: 'rotation-a',
mutationId: 'rotate-a',
expectedCurrentVersion: 1,
createdAtMs: 200,
}),
secondService.put({
projectId: 'default',
name: 'TOKEN',
plaintext: 'rotation-b',
mutationId: 'rotate-b',
expectedCurrentVersion: 1,
createdAtMs: 201,
}),
]);
assert.equal(
rotations.filter((item) => item.status === 'fulfilled').length,
1,
);
assert.equal(
rotations.filter((item) => item.status === 'rejected').length,
1,
);
assert.equal(
rotations.find((item) => item.status === 'rejected').reason.constructor,
LocalSecretVersionConflictError,
);
});
+81
View File
@@ -0,0 +1,81 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { afterEach, test } = require('node:test');
const {
LegacyManualPrimaryLogFiles,
} = require('../../back/runtime/adapters/legacy/defaultManualPrimaryRuntime');
const temporaryDirectories = [];
async function temporaryRoot() {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-manual-log-'));
temporaryDirectories.push(root);
return root;
}
function input(logName) {
return {
cron: {
id: 31,
command: 'demo.js',
extraSchedules: [],
logName,
},
acceptedAtMs: 1_750_000_000_000,
};
}
afterEach(async () => {
await Promise.all(
temporaryDirectories
.splice(0)
.map((directory) => fs.rm(directory, { recursive: true, force: true })),
);
});
test('writes manual Primary output below the configured log root', async () => {
const root = await temporaryRoot();
const logs = new LegacyManualPrimaryLogFiles(root);
const prepared = await logs.prepare(input('nested/task-31'));
assert.match(prepared.logPath, /^nested\/task-31\/.+\.log$/);
await prepared.output.write({
stream: 'stdout',
chunk: Buffer.from('hello primary\n'),
observedAtMs: 1_750_000_000_001,
});
await prepared.close();
const absolutePath = path.resolve(root, ...prepared.logPath.split('/'));
assert.equal(await fs.readFile(absolutePath, 'utf8'), 'hello primary\n');
});
test('resolves the receipt journal only after live completion cleanup', async () => {
const root = await temporaryRoot();
const receiptRoot = await temporaryRoot();
const resolved = [];
const logs = new LegacyManualPrimaryLogFiles(root, receiptRoot, {
async resolve(attemptId) {
resolved.push(attemptId);
return true;
},
});
const prepared = await logs.prepare(input('nested/task-31'));
const attemptId = '019f7900-0000-7000-8000-000000000099';
await prepared.completionCommitted(attemptId);
assert.deepEqual(resolved, [attemptId]);
await prepared.close();
});
test('rejects relative and absolute paths outside the configured log root', async () => {
const root = await temporaryRoot();
const logs = new LegacyManualPrimaryLogFiles(root);
await assert.rejects(logs.prepare(input('../outside')));
await assert.rejects(logs.prepare(input(path.resolve(root, '../outside'))));
});
+392
View File
@@ -0,0 +1,392 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { afterEach, test } = require('node:test');
const { Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const { runSchemaMigration } = require('../../back/migrations/0002-run-schema');
const {
runCancellationRequestMigration,
} = require('../../back/migrations/0004-run-cancellation-request');
const {
runAttemptDeadlineMigration,
} = require('../../back/migrations/0006-run-attempt-deadline');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacySequelizeRunRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/runRepository');
const {
ManualPrimaryOwnershipError,
ManualPrimaryRuntime,
} = require('../../back/runtime/application/manualPrimaryRuntime');
const {
installManualPrimaryExecutionRouter,
selectManualPrimaryExecutionRouter,
stopManualPrimaryAttempt,
stopManualPrimaryCron,
} = require('../../back/runtime/compatibility/manualPrimaryExecutionBridge');
const {
parseLegacyLogOutputRef,
} = require('../../back/runtime/compatibility/legacyLogOutputRef');
const {
RuntimeRolloutPolicy,
} = require('../../back/runtime/domain/runtimeRollout');
const databases = [];
const restorers = [];
const BASE_TIME = 1_750_000_000_000;
let idSequence = 1_100;
function nextId() {
idSequence += 1;
return '019f7120-0000-7000-8000-' + String(idSequence).padStart(12, '0');
}
function deferred() {
let resolve;
let reject;
const promise = new Promise((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, resolve, reject };
}
async function createRepository() {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
const migrationModel = defineSchemaMigrationModel(database);
await runMigrations({
database,
migrationModel,
migrations: [
runSchemaMigration,
runCancellationRequestMigration,
runAttemptDeadlineMigration,
],
logger: { info() {} },
});
databases.push(database);
return new LegacySequelizeRunRepository(database);
}
function rollout(mode) {
return new RuntimeRolloutPolicy({
defaultMode: 'off',
origins: { manual: mode },
allowLegacyFallbackBeforeStart: false,
});
}
class FakeExecutor {
type = 'local_process';
starts = [];
stops = [];
completion = deferred();
capabilities() {
return {
timeout: true,
processGroupTermination: true,
workingDirectory: true,
isolatedEnvironment: true,
memoryLimit: 'none',
cpuLimit: 'none',
filesystemIsolation: 'none',
networkIsolation: 'none',
};
}
async start(spec, context) {
this.starts.push({ spec, context });
if (context.signal && context.signal.aborted) {
throw new Error('aborted before fake spawn');
}
return {
id: nextId(),
executorType: this.type,
runId: spec.runId,
attemptId: spec.attemptId,
startedAtMs: BASE_TIME + 10,
pid: 7301,
completion: this.completion.promise,
};
}
async stop(handle, reason) {
this.stops.push({ handle, reason });
return {
status: 'termination_requested',
termSignalSent: true,
killSignalSent: false,
};
}
async inspect() {
return { status: 'running' };
}
}
class FakeLogs {
prepareCalls = 0;
closeCalls = 0;
committedAttempts = [];
writes = [];
async prepare(input) {
this.prepareCalls += 1;
return {
logPath: 'task-' + input.cron.id + '/manual.log',
output: {
write: async (output) => {
this.writes.push(output);
},
},
completionCommitted: async (attemptId) => {
this.committedAttempts.push(attemptId);
},
close: async () => {
this.closeCalls += 1;
},
};
}
}
function input(cronId = 21) {
return {
cron: {
id: cronId,
name: 'manual primary',
command: 'demo.js arg',
schedule: '0 * * * *',
extraSchedules: ['30 * * * *'],
taskBefore: 'echo before',
taskAfter: 'echo after',
workDirectory: '/tmp',
logName: 'task-' + cronId,
},
acceptedAtMs: BASE_TIME,
};
}
afterEach(async () => {
while (restorers.length > 0) restorers.pop()();
await Promise.all(databases.splice(0).map((database) => database.close()));
});
test('off mode selects Legacy without preparing logs or spawning', async () => {
const repository = await createRepository();
const executor = new FakeExecutor();
const logs = new FakeLogs();
const runtime = new ManualPrimaryRuntime(
repository,
executor,
rollout('off'),
logs,
{ orchestrator: { createId: nextId } },
);
assert.equal(runtime.ownsNewRuns(), false);
await assert.rejects(runtime.start(input()), ManualPrimaryOwnershipError);
assert.equal(logs.prepareCalls, 0);
assert.equal(executor.starts.length, 0);
});
test('Primary mode builds one durable Run and one legacy-compatible spec', async () => {
const repository = await createRepository();
const executor = new FakeExecutor();
const logs = new FakeLogs();
const runtime = new ManualPrimaryRuntime(
repository,
executor,
rollout('primary'),
logs,
{
clock: { now: () => BASE_TIME + 20 },
orchestrator: { createId: nextId },
},
);
const active = await runtime.start(input());
assert.equal(executor.starts.length, 1);
assert.equal(active.pid, 7301);
assert.equal(active.logPath, 'task-21/manual.log');
const started = executor.starts[0];
assert.equal(started.spec.command.kind, 'shell');
assert.match(
started.spec.command.command,
/real_log_path='task-21\/manual\.log'/,
);
assert.match(started.spec.command.command, /task demo\.js arg/);
assert.equal(started.spec.environmentPolicy, 'inherit');
assert.equal(started.context.signal.aborted, false);
const run = await repository.findRunById(active.runId);
assert.equal(run.executionOwner, 'runtime');
assert.equal(run.executionOrigin, 'manual');
assert.equal(parseLegacyLogOutputRef(run.outputRef), 'task-21/manual.log');
executor.completion.resolve({
outcome: 'succeeded',
startedAtMs: BASE_TIME + 10,
finishedAtMs: BASE_TIME + 30,
exitCode: 0,
});
const completed = await active.completion;
assert.equal(completed.outcome, 'succeeded');
assert.equal(completed.exitCode, 0);
assert.deepEqual(logs.committedAttempts, [active.attemptId]);
assert.equal(logs.closeCalls, 1);
});
test('routes stop by cron and attempt through the owning Executor', async () => {
const repository = await createRepository();
const executor = new FakeExecutor();
const runtime = new ManualPrimaryRuntime(
repository,
executor,
rollout('primary'),
new FakeLogs(),
{ orchestrator: { createId: nextId } },
);
const active = await runtime.start(input(22));
assert.deepEqual(await runtime.stopCron(999, BASE_TIME + 40), {
matched: 0,
failed: 0,
});
assert.deepEqual(
await runtime.stopAttempt(active.attemptId, BASE_TIME + 41),
{ matched: 1, failed: 0 },
);
assert.equal(executor.stops.length, 1);
assert.equal(executor.stops[0].reason.kind, 'user');
executor.completion.resolve({
outcome: 'cancelled',
startedAtMs: BASE_TIME + 10,
finishedAtMs: BASE_TIME + 50,
exitCode: 143,
});
assert.equal((await active.completion).outcome, 'cancelled');
});
test('a stop racing log preparation aborts before Executor side effects', async () => {
const repository = await createRepository();
const executor = new FakeExecutor();
const preparing = deferred();
const release = deferred();
const logs = {
async prepare() {
preparing.resolve();
await release.promise;
return {
logPath: 'task-24/racing.log',
output: { async write() {} },
async close() {},
};
},
};
const runtime = new ManualPrimaryRuntime(
repository,
executor,
rollout('primary'),
logs,
{ orchestrator: { createId: nextId } },
);
const starting = runtime.start(input(24));
await preparing.promise;
assert.deepEqual(await runtime.stopCron(24, BASE_TIME + 1), {
matched: 1,
failed: 0,
});
release.resolve();
await assert.rejects(starting);
assert.equal(executor.starts.length, 1);
assert.equal(executor.starts[0].context.signal.aborted, true);
});
test('bridge is default-off and keeps stop routing available for its owner', async () => {
assert.equal(selectManualPrimaryExecutionRouter(), undefined);
assert.deepEqual(await stopManualPrimaryCron(1, BASE_TIME), {
matched: 0,
failed: 0,
});
const calls = [];
const router = {
ownsNewRuns: () => true,
async start() {
throw new Error('not used');
},
async stopCron(cronId, requestedAtMs) {
calls.push(['cron', cronId, requestedAtMs]);
return { matched: 1, failed: 0 };
},
async stopAttempt(attemptId, requestedAtMs) {
calls.push(['attempt', attemptId, requestedAtMs]);
return { matched: 1, failed: 0 };
},
};
restorers.push(installManualPrimaryExecutionRouter(router));
assert.equal(selectManualPrimaryExecutionRouter(), router);
assert.deepEqual(await stopManualPrimaryCron(23, BASE_TIME + 1), {
matched: 1,
failed: 0,
});
assert.deepEqual(
await stopManualPrimaryAttempt('attempt-23', BASE_TIME + 2),
{
matched: 1,
failed: 0,
},
);
assert.deepEqual(calls, [
['cron', 23, BASE_TIME + 1],
['attempt', 'attempt-23', BASE_TIME + 2],
]);
});
test('rolling new triggers to off preserves the previous in-flight stop owner', async () => {
const oldOwner = {
ownsNewRuns: () => true,
async start() {
throw new Error('not used');
},
async stopCron(cronId) {
return {
matched: cronId === 25 ? 1 : 0,
failed: 0,
};
},
async stopAttempt() {
return { matched: 0, failed: 0 };
},
};
const offRouter = {
ownsNewRuns: () => false,
async start() {
throw new Error('not used');
},
async stopCron() {
return { matched: 0, failed: 0 };
},
async stopAttempt() {
return { matched: 0, failed: 0 };
},
};
restorers.push(installManualPrimaryExecutionRouter(oldOwner));
restorers.push(installManualPrimaryExecutionRouter(offRouter));
assert.equal(selectManualPrimaryExecutionRouter(), undefined);
assert.deepEqual(await stopManualPrimaryCron(25, BASE_TIME + 3), {
matched: 1,
failed: 0,
});
});
@@ -0,0 +1,326 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
activateManualPrimaryRuntime,
} = require('../../back/runtime/application/manualPrimaryRuntimeActivation');
const {
RuntimeRolloutPolicy,
} = require('../../back/runtime/domain/runtimeRollout');
const NOW = 1_750_000_000_000;
function loadResult(status, mode = 'off') {
return {
status,
policy: new RuntimeRolloutPolicy({
defaultMode: 'off',
origins: mode === 'off' ? {} : { manual: mode },
allowLegacyFallbackBeforeStart: false,
}),
audit: {
event: 'runtime.rollout_config_evaluated',
evaluatedAtMs: NOW,
sourcePath: '/data/config/qinglong3-rollout.json',
status,
revision: 'canary-1',
},
};
}
function cleanRecovery(overrides = {}) {
return {
pages: 1,
scanned: 0,
verifiedRunning: 0,
recoveredRunning: 0,
completedFromReceipt: 0,
quarantinedReceipts: 0,
publishGraceWaits: 0,
markedLost: 0,
skipped: 0,
ambiguous: 0,
failed: 0,
stopReason: 'complete',
remaining: false,
...overrides,
};
}
function router() {
return {
ownsNewRuns: () => true,
async start() {
throw new Error('not used');
},
async stopCron() {
return { matched: 0, failed: 0 };
},
async stopAttempt() {
return { matched: 0, failed: 0 };
},
};
}
function completionLifecycle(calls) {
return {
startCompletion() {
calls.push('start-completion');
return true;
},
async stopCompletion() {
calls.push('stop-completion');
return 'drained';
},
};
}
test('activation remains inert unless an accepted manifest selects Primary', async () => {
for (const [status, mode] of [
['missing', 'off'],
['disabled', 'off'],
['rejected', 'off'],
['accepted', 'shadow'],
]) {
const calls = [];
const result = await activateManualPrimaryRuntime({
load: async () => loadResult(status, mode),
create() {
calls.push('create');
throw new Error('must remain inert');
},
install() {
calls.push('install');
return () => undefined;
},
audit(record) {
calls.push(record.activation);
},
});
assert.equal(result.active, false);
assert.equal(await result.stop(), 'drained');
assert.deepEqual(calls, ['not_activated']);
}
});
test('activation reconciles before starting lifecycle and installing ownership', async () => {
const calls = [];
const result = await activateManualPrimaryRuntime({
load: async () => loadResult('accepted', 'primary'),
create() {
calls.push('create');
return {
router: router(),
...completionLifecycle(calls),
async reconcile() {
calls.push('reconcile');
return cleanRecovery({
scanned: 2,
recoveredRunning: 1,
markedLost: 1,
});
},
startTimeout() {
calls.push('start-timeout');
return true;
},
async stopTimeout() {
calls.push('stop-timeout');
return 'drained';
},
startCancellation() {
calls.push('start-cancellation');
return true;
},
async stopCancellation() {
calls.push('stop-cancellation');
return 'drained';
},
};
},
install() {
calls.push('install');
return () => calls.push('dispose');
},
audit(record) {
calls.push(`audit:${record.activation}`);
},
});
assert.equal(result.active, true);
assert.deepEqual(calls, [
'audit:selected',
'create',
'reconcile',
'audit:reconciled',
'start-completion',
'start-timeout',
'start-cancellation',
'install',
'audit:activated',
]);
assert.equal(await result.stop(), 'drained');
assert.equal(await result.stop(), 'drained');
assert.deepEqual(calls.slice(-5), [
'dispose',
'stop-timeout',
'stop-cancellation',
'stop-completion',
'audit:stopped',
]);
});
test('activation rejects unresolved recovery before starting or installing', async () => {
const calls = [];
await assert.rejects(
activateManualPrimaryRuntime({
load: async () => loadResult('accepted', 'primary'),
create() {
return {
router: router(),
...completionLifecycle(calls),
async reconcile() {
return cleanRecovery({ ambiguous: 1 });
},
startTimeout() {
calls.push('start-timeout');
return true;
},
async stopTimeout() {
calls.push('stop-timeout');
return 'drained';
},
startCancellation() {
calls.push('start');
return true;
},
async stopCancellation() {
calls.push('stop');
return 'drained';
},
};
},
install() {
calls.push('install');
return () => calls.push('dispose');
},
audit(record) {
calls.push(`audit:${record.activation}`);
},
}),
/did not converge safely/,
);
assert.deepEqual(calls, ['audit:selected', 'audit:failed']);
});
test('activation rolls back router and lifecycle when final audit fails', async () => {
const calls = [];
await assert.rejects(
activateManualPrimaryRuntime({
load: async () => loadResult('accepted', 'primary'),
create() {
return {
router: router(),
...completionLifecycle(calls),
async reconcile() {
return cleanRecovery();
},
startTimeout() {
calls.push('start-timeout');
return true;
},
async stopTimeout() {
calls.push('stop-timeout');
return 'drained';
},
startCancellation() {
calls.push('start');
return true;
},
async stopCancellation() {
calls.push('stop');
return 'drained';
},
};
},
install() {
calls.push('install');
return () => calls.push('dispose');
},
audit(record) {
calls.push(`audit:${record.activation}`);
if (record.activation === 'activated') {
throw new Error('audit unavailable');
}
},
}),
/audit unavailable/,
);
assert.deepEqual(calls, [
'audit:selected',
'audit:reconciled',
'start-completion',
'start-timeout',
'start',
'install',
'audit:activated',
'dispose',
'stop-timeout',
'stop',
'stop-completion',
'audit:failed',
]);
});
test('activation stops timeout production when cancellation lifecycle cannot start', async () => {
const calls = [];
await assert.rejects(
activateManualPrimaryRuntime({
load: async () => loadResult('accepted', 'primary'),
create() {
return {
router: router(),
...completionLifecycle(calls),
async reconcile() {
return cleanRecovery();
},
startTimeout() {
calls.push('start-timeout');
return true;
},
async stopTimeout() {
calls.push('stop-timeout');
return 'drained';
},
startCancellation() {
calls.push('start-cancellation');
return false;
},
async stopCancellation() {
calls.push('stop-cancellation');
return 'drained';
},
};
},
install() {
calls.push('install');
return () => calls.push('dispose');
},
audit(record) {
calls.push(`audit:${record.activation}`);
},
}),
/cancellation lifecycle did not start/,
);
assert.deepEqual(calls, [
'audit:selected',
'audit:reconciled',
'start-completion',
'start-timeout',
'start-cancellation',
'stop-timeout',
'stop-completion',
'audit:failed',
]);
});
+332
View File
@@ -0,0 +1,332 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
InvalidMigrationStreamError,
MigrationStreamAheadOfCodeError,
MigrationStreamChecksumMismatchError,
MigrationStreamHistoryCorruptionError,
runMigrationStream,
} = require('../../back/migrations/core/migrationStream');
const CHECKSUM_A = 'a'.repeat(64);
const CHECKSUM_B = 'b'.repeat(64);
function stream(migrations) {
return {
id: 'postgresql-main',
dialect: 'postgresql',
migrationIdScheme: 'postgres-prefixed',
checksumScheme: 'sha256',
migrations,
};
}
function memoryStore(initial = []) {
const records = new Map(
initial.map((record) => [record.migrationId, record]),
);
let ensured = 0;
return {
records,
get ensured() {
return ensured;
},
store: {
async ensureHistory() {
ensured += 1;
},
async listAll() {
return [...records.values()].map((record) => ({ ...record }));
},
async findById(id) {
const record = records.get(id);
return record ? { ...record } : null;
},
async transaction(work) {
const staged = new Map(
[...records].map(([id, record]) => [id, { ...record }]),
);
const result = await work({
context: { statements: [] },
async findById(id) {
const record = staged.get(id);
return record ? { ...record } : null;
},
async insert(record) {
if (staged.has(record.migrationId)) {
throw new Error('duplicate migration history');
}
staged.set(record.migrationId, { ...record });
},
});
records.clear();
for (const [id, record] of staged) records.set(id, record);
return result;
},
},
};
}
test('runs one prefixed migration atomically and replays without running up', async () => {
const state = memoryStore();
const statements = [];
const logs = [];
let calls = 0;
const definition = stream([
{
id: 'pg-0001-schema-history',
checksum: CHECKSUM_A,
async up(context) {
calls += 1;
context.statements.push('create schema metadata');
statements.push(...context.statements);
},
},
]);
await runMigrationStream({
stream: definition,
store: state.store,
clock: () => 100,
logger: {
info(message) {
logs.push(message);
},
},
});
await runMigrationStream({
stream: definition,
store: state.store,
clock: () => 200,
});
assert.equal(calls, 1);
assert.deepEqual(statements, ['create schema metadata']);
assert.equal(state.ensured, 2);
assert.deepEqual(
[...state.records.values()],
[
{
streamId: 'postgresql-main',
dialect: 'postgresql',
migrationId: 'pg-0001-schema-history',
checksum: CHECKSUM_A,
appliedAtMs: 100,
},
],
);
assert.deepEqual(logs, [
'[migration:postgresql-main] Applied pg-0001-schema-history',
]);
});
test('rejects checksum drift and corrupt dialect history before migration work', async () => {
const migrationId = 'pg-0001-schema-history';
const mismatch = memoryStore([
{
streamId: 'postgresql-main',
dialect: 'postgresql',
migrationId,
checksum: CHECKSUM_A,
appliedAtMs: 1,
},
]);
await assert.rejects(
runMigrationStream({
stream: stream([
{ id: migrationId, checksum: CHECKSUM_B, async up() {} },
]),
store: mismatch.store,
}),
(error) => {
assert.ok(error instanceof MigrationStreamChecksumMismatchError);
assert.equal(error.migrationId, migrationId);
assert.equal(error.databaseChecksum, CHECKSUM_A);
assert.equal(error.codeChecksum, CHECKSUM_B);
assert.equal(
error.message,
`Migration checksum mismatch: ${migrationId} ` +
`(database=${CHECKSUM_A}, code=${CHECKSUM_B})`,
);
return true;
},
);
const corrupt = memoryStore([
{
streamId: 'postgresql-main',
dialect: 'sqlite',
migrationId,
checksum: CHECKSUM_A,
appliedAtMs: 1,
},
]);
await assert.rejects(
runMigrationStream({
stream: stream([
{ id: migrationId, checksum: CHECKSUM_A, async up() {} },
]),
store: corrupt.store,
}),
MigrationStreamHistoryCorruptionError,
);
});
test('rolls migration work and history back together when up fails', async () => {
const state = memoryStore();
await assert.rejects(
runMigrationStream({
stream: stream([
{
id: 'pg-0001-schema-history',
checksum: CHECKSUM_A,
async up(context) {
context.statements.push('partial ddl');
throw new Error('ddl failed');
},
},
]),
store: state.store,
}),
/ddl failed/,
);
assert.equal(state.records.size, 0);
});
test('rechecks history inside the transaction for a concurrent leader winner', async () => {
const migrationId = 'pg-0001-schema-history';
const winner = {
streamId: 'postgresql-main',
dialect: 'postgresql',
migrationId,
checksum: CHECKSUM_A,
appliedAtMs: 10,
};
let outsideReads = 0;
let upCalls = 0;
const store = {
async ensureHistory() {},
async listAll() {
return [];
},
async findById() {
outsideReads += 1;
return null;
},
async transaction(work) {
return work({
context: {},
async findById() {
return winner;
},
async insert() {
throw new Error('unreachable');
},
});
},
};
await runMigrationStream({
stream: stream([
{
id: migrationId,
checksum: CHECKSUM_A,
async up() {
upCalls += 1;
},
},
]),
store,
});
assert.equal(outsideReads, 1);
assert.equal(upCalls, 0);
});
test('rejects ahead and non-prefix history before new migration work', async () => {
const first = {
id: 'pg-0001-schema-history',
checksum: CHECKSUM_A,
async up() {},
};
const second = {
id: 'pg-0002-run-core',
checksum: CHECKSUM_B,
async up() {},
};
const ahead = memoryStore([
{
streamId: 'postgresql-main',
dialect: 'postgresql',
migrationId: 'pg-0003-ahead',
checksum: CHECKSUM_A,
appliedAtMs: 1,
},
]);
await assert.rejects(
runMigrationStream({ stream: stream([first, second]), store: ahead.store }),
MigrationStreamAheadOfCodeError,
);
const gap = memoryStore([
{
streamId: 'postgresql-main',
dialect: 'postgresql',
migrationId: second.id,
checksum: second.checksum,
appliedAtMs: 2,
},
]);
await assert.rejects(
runMigrationStream({ stream: stream([first, second]), store: gap.store }),
MigrationStreamHistoryCorruptionError,
);
});
test('validates stream identity, immutable checksum and unique prefixed ids first', async () => {
const state = memoryStore();
for (const definition of [
{ ...stream([]), id: 'PostgreSQL' },
{ ...stream([]), migrationIdScheme: 'sqlite-numbered' },
{ ...stream([]), checksumScheme: 'unknown' },
stream([{ id: '0001', checksum: CHECKSUM_A, async up() {} }]),
stream([{ id: 'pg-0001', checksum: 'short', async up() {} }]),
stream([
{ id: 'pg-0001', checksum: CHECKSUM_A, async up() {} },
{ id: 'pg-0001', checksum: CHECKSUM_A, async up() {} },
]),
]) {
await assert.rejects(
runMigrationStream({ stream: definition, store: state.store }),
InvalidMigrationStreamError,
);
}
assert.equal(state.ensured, 0);
});
test('accepts the frozen numbered SQLite stream without rewriting its ids', async () => {
const state = memoryStore();
await runMigrationStream({
stream: {
id: 'sqlite-main',
dialect: 'sqlite',
migrationIdScheme: 'sqlite-numbered',
checksumScheme: 'legacy-opaque',
migrations: [
{
id: '0001-legacy-columns',
checksum: 'legacy checksum v1',
async up() {},
},
],
},
store: state.store,
clock: () => 1,
});
assert.equal(
state.records.get('0001-legacy-columns').migrationId,
'0001-legacy-columns',
);
assert.equal(
state.records.get('0001-legacy-columns').checksum,
'legacy checksum v1',
);
});
@@ -0,0 +1,85 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { afterEach, test } = require('node:test');
const { DataTypes, QueryTypes, Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const {
legacyColumnsMigration,
} = require('../../back/migrations/0001-legacy-columns');
const { runMigrations } = require('../../back/migrations/runner');
const databases = [];
async function createDatabase() {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
const migrationModel = defineSchemaMigrationModel(database);
databases.push(database);
return { database, migrationModel };
}
afterEach(async () => {
await Promise.all(databases.splice(0).map((database) => database.close()));
});
test('preserves unknown tables, columns, indexes, and rows during legacy migration', async () => {
const { database, migrationModel } = await createDatabase();
const queryInterface = database.getQueryInterface();
for (const table of ['CrontabViews', 'Subscriptions', 'Envs']) {
await queryInterface.createTable(table, {
id: { type: DataTypes.INTEGER, primaryKey: true },
});
}
await queryInterface.createTable('Crontabs', {
id: { type: DataTypes.INTEGER, primaryKey: true },
external_note: { type: DataTypes.STRING },
});
await queryInterface.addIndex('Crontabs', ['external_note'], {
name: 'external_crontab_note_index',
});
await queryInterface.createTable('ExternalPluginState', {
id: { type: DataTypes.INTEGER, primaryKey: true },
payload: { type: DataTypes.TEXT, allowNull: false },
});
await queryInterface.bulkInsert('Crontabs', [
{ id: 1, external_note: 'preserve-me' },
]);
await queryInterface.bulkInsert('ExternalPluginState', [
{ id: 1, payload: '{"source":"fixture"}' },
]);
const options = {
database,
migrationModel,
migrations: [legacyColumnsMigration],
logger: { info() {} },
};
await runMigrations(options);
await runMigrations(options);
const tables = await queryInterface.showAllTables();
const crontabs = await queryInterface.describeTable('Crontabs');
const indexes = await queryInterface.showIndex('Crontabs');
const cronRows = await database.query(
'SELECT id, external_note FROM Crontabs ORDER BY id',
{ type: QueryTypes.SELECT },
);
const pluginRows = await database.query(
'SELECT id, payload FROM ExternalPluginState ORDER BY id',
{ type: QueryTypes.SELECT },
);
assert.ok(tables.includes('ExternalPluginState'));
assert.ok(crontabs.external_note);
assert.ok(indexes.some((index) => index.name === 'external_crontab_note_index'));
assert.deepEqual(cronRows, [{ id: 1, external_note: 'preserve-me' }]);
assert.deepEqual(pluginRows, [{ id: 1, payload: '{"source":"fixture"}' }]);
assert.equal(await migrationModel.count(), 1);
});
+892
View File
@@ -0,0 +1,892 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { afterEach, test } = require('node:test');
const { DataTypes, QueryTypes, Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const {
legacyColumnsMigration,
} = require('../../back/migrations/0001-legacy-columns');
const {
RUN_ATTEMPT_TABLE,
RUN_EVENT_TABLE,
RUN_TABLE,
runSchemaMigration,
} = require('../../back/migrations/0002-run-schema');
const {
RUNNING_INSTANCE_ATTEMPT_INDEX,
RUNNING_INSTANCE_RUN_INDEX,
RUNNING_INSTANCE_TABLE,
runningInstanceRunReferenceMigration,
} = require('../../back/migrations/0003-running-instance-run-reference');
const {
RUN_CANCELLATION_REQUEST_INDEX,
runCancellationRequestMigration,
} = require('../../back/migrations/0004-run-cancellation-request');
const {
RUN_CANCELLATION_DISPATCH_DUE_INDEX,
RUN_CANCELLATION_DISPATCH_LEASE_INDEX,
RUN_CANCELLATION_DISPATCH_TABLE,
runCancellationDispatchMigration,
} = require('../../back/migrations/0005-run-cancellation-dispatch');
const {
RUN_ATTEMPT_DEADLINE_INDEX,
runAttemptDeadlineMigration,
} = require('../../back/migrations/0006-run-attempt-deadline');
const {
COMPLETION_RECEIPT_JOURNAL_PURGE_INDEX,
COMPLETION_RECEIPT_JOURNAL_SCAN_INDEX,
COMPLETION_RECEIPT_JOURNAL_TABLE,
completionReceiptJournalMigration,
} = require('../../back/migrations/0007-completion-receipt-journal');
const {
WORKER_REGISTRY_CAPACITY_INDEX,
WORKER_REGISTRY_LEASE_INDEX,
WORKER_REGISTRY_TABLE,
workerRegistryMigration,
} = require('../../back/migrations/0008-worker-registry');
const {
RUN_DISPATCH_LEASE_EXPIRY_INDEX,
RUN_DISPATCH_LEASE_TABLE,
RUN_DISPATCH_LEASE_TOKEN_INDEX,
RUN_DISPATCH_LEASE_WORKER_INDEX,
runDispatchLeaseMigration,
} = require('../../back/migrations/0009-run-dispatch-lease');
const { migrations } = require('../../back/migrations');
const { runMigrations } = require('../../back/migrations/runner');
const databases = [];
async function createDatabase() {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
const migrationModel = defineSchemaMigrationModel(database);
databases.push(database);
return { database, migrationModel };
}
afterEach(async () => {
await Promise.all(databases.splice(0).map((database) => database.close()));
});
test('runs a migration once and records its checksum', async () => {
const { database, migrationModel } = await createDatabase();
const queryInterface = database.getQueryInterface();
await queryInterface.createTable('Examples', {
id: { type: DataTypes.INTEGER, primaryKey: true },
});
let calls = 0;
const migrations = [
{
id: '0001-example',
checksum: 'checksum-v1',
async up({ queryInterface, transaction }) {
calls += 1;
await queryInterface.addColumn(
'Examples',
'name',
{ type: DataTypes.STRING },
{ transaction },
);
},
},
];
const options = {
database,
migrationModel,
migrations,
logger: { info() {} },
};
await runMigrations(options);
await runMigrations(options);
const description = await queryInterface.describeTable('Examples');
const applied = await migrationModel.findByPk('0001-example');
assert.ok(description.name);
assert.equal(calls, 1);
assert.equal(applied.checksum, 'checksum-v1');
});
test('rejects a changed checksum for an applied migration', async () => {
const { database, migrationModel } = await createDatabase();
const baseOptions = {
database,
migrationModel,
logger: { info() {} },
};
await runMigrations({
...baseOptions,
migrations: [
{
id: '0001-example',
checksum: 'checksum-v1',
async up() {},
},
],
});
await assert.rejects(
runMigrations({
...baseOptions,
migrations: [
{
id: '0001-example',
checksum: 'checksum-v2',
async up() {},
},
],
}),
/Migration checksum mismatch/,
);
});
test('rolls back the migration record when migration work fails', async () => {
const { database, migrationModel } = await createDatabase();
await assert.rejects(
runMigrations({
database,
migrationModel,
logger: { info() {} },
migrations: [
{
id: '0001-failing',
checksum: 'checksum-v1',
async up() {
throw new Error('migration failed');
},
},
],
}),
/migration failed/,
);
assert.equal(await migrationModel.count(), 0);
});
test('rejects duplicate migration ids before touching the database', async () => {
const { database, migrationModel } = await createDatabase();
const duplicate = {
id: '0001-duplicate',
checksum: 'checksum-v1',
async up() {},
};
await assert.rejects(
runMigrations({
database,
migrationModel,
migrations: [duplicate, duplicate],
logger: { info() {} },
}),
/Duplicate migration id/,
);
});
test('upgrades the legacy QingLong tables without swallowing schema errors', async () => {
const { database, migrationModel } = await createDatabase();
const queryInterface = database.getQueryInterface();
for (const table of ['CrontabViews', 'Subscriptions', 'Crontabs', 'Envs']) {
await queryInterface.createTable(table, {
id: { type: DataTypes.INTEGER, primaryKey: true },
});
}
const options = {
database,
migrationModel,
migrations: [legacyColumnsMigration],
logger: { info() {} },
};
await runMigrations(options);
await runMigrations(options);
const cronViews = await queryInterface.describeTable('CrontabViews');
const subscriptions = await queryInterface.describeTable('Subscriptions');
const crontabs = await queryInterface.describeTable('Crontabs');
const envs = await queryInterface.describeTable('Envs');
assert.ok(cronViews.filterRelation);
assert.ok(cronViews.type);
assert.ok(subscriptions.proxy);
assert.ok(subscriptions.autoAddCron);
assert.ok(subscriptions.autoDelCron);
assert.ok(crontabs.sub_id);
assert.ok(crontabs.extra_schedules);
assert.ok(crontabs.task_before);
assert.ok(crontabs.task_after);
assert.ok(crontabs.log_name);
assert.ok(crontabs.allow_multiple_instances);
assert.ok(crontabs.work_dir);
assert.ok(envs.isPinned);
assert.ok(envs.labels);
assert.equal(await migrationModel.count(), 1);
});
test('creates the Run aggregate schema with stable uniqueness constraints', async () => {
const { database, migrationModel } = await createDatabase();
const queryInterface = database.getQueryInterface();
const options = {
database,
migrationModel,
migrations: [runSchemaMigration],
logger: { info() {} },
};
await runMigrations(options);
await runMigrations(options);
const runs = await queryInterface.describeTable(RUN_TABLE);
const attempts = await queryInterface.describeTable(RUN_ATTEMPT_TABLE);
const events = await queryInterface.describeTable(RUN_EVENT_TABLE);
for (const column of [
'id',
'project_id',
'task_id',
'task_revision',
'legacy_cron_id',
'execution_origin',
'execution_owner',
'status',
'version',
'event_sequence',
'created_at_ms',
]) {
assert.ok(runs[column], `missing Runs.${column}`);
}
for (const column of [
'id',
'run_id',
'attempt',
'status',
'executor_type',
'callback_token_hash',
'callback_sequence',
'created_at_ms',
]) {
assert.ok(attempts[column], `missing RunAttempts.${column}`);
}
for (const column of [
'id',
'run_id',
'sequence',
'type',
'dedupe_key',
'actor_type',
'payload',
'created_at_ms',
]) {
assert.ok(events[column], `missing RunEvents.${column}`);
}
const runId = '019f70a0-0000-7000-8000-000000000001';
const attemptId = '019f70a0-0000-7000-8000-000000000002';
const baseRun = {
id: runId,
project_id: 'default',
task_id: 'legacy-cron:1',
task_revision: 'revision-1',
trigger_type: 'manual',
execution_origin: 'manual',
execution_owner: 'legacy',
status: 'created',
version: 0,
event_sequence: 0,
priority: 0,
created_at_ms: 1_750_000_000_000,
};
await queryInterface.bulkInsert(RUN_TABLE, [
{ ...baseRun, idempotency_key: 'manual-request-1' },
]);
await queryInterface.bulkInsert(RUN_ATTEMPT_TABLE, [
{
id: attemptId,
run_id: runId,
attempt: 1,
status: 'claimed',
executor_type: 'legacy_local',
callback_sequence: 0,
created_at_ms: 1_750_000_000_000,
},
]);
await queryInterface.bulkInsert(RUN_EVENT_TABLE, [
{
id: '019f70a0-0000-7000-8000-000000000003',
run_id: runId,
sequence: 1,
type: 'run.created',
dedupe_key: 'create',
actor_type: 'compatibility',
attempt_id: attemptId,
payload: JSON.stringify({ source: 'migration-test' }),
created_at_ms: 1_750_000_000_000,
},
]);
await assert.rejects(
queryInterface.bulkInsert(RUN_TABLE, [
{
...baseRun,
id: '019f70a0-0000-7000-8000-000000000004',
idempotency_key: 'manual-request-1',
},
]),
);
await assert.rejects(
queryInterface.bulkInsert(RUN_TABLE, [
{
...baseRun,
id: '019f70a0-0000-7000-8000-000000000007',
version: -1,
idempotency_key: 'manual-request-2',
},
]),
);
await assert.rejects(
queryInterface.bulkInsert(RUN_ATTEMPT_TABLE, [
{
id: '019f70a0-0000-7000-8000-000000000005',
run_id: runId,
attempt: 1,
status: 'claimed',
executor_type: 'legacy_local',
callback_sequence: 0,
created_at_ms: 1_750_000_000_000,
},
]),
);
await assert.rejects(
queryInterface.bulkInsert(RUN_EVENT_TABLE, [
{
id: '019f70a0-0000-7000-8000-000000000006',
run_id: runId,
sequence: 2,
type: 'run.created',
dedupe_key: 'create',
actor_type: 'compatibility',
payload: '{}',
created_at_ms: 1_750_000_000_001,
},
]),
);
assert.equal(await migrationModel.count(), 1);
});
test('creates a bounded completion receipt journal linked to Run Attempts', async () => {
const { database, migrationModel } = await createDatabase();
const queryInterface = database.getQueryInterface();
await runMigrations({
database,
migrationModel,
migrations: [runSchemaMigration, completionReceiptJournalMigration],
logger: { info() {} },
});
const columns = await queryInterface.describeTable(
COMPLETION_RECEIPT_JOURNAL_TABLE,
);
for (const column of [
'attempt_id',
'run_id',
'state',
'quarantine_ref',
'purge_after_ms',
'registered_at_ms',
'updated_at_ms',
]) {
assert.ok(columns[column], `missing journal.${column}`);
}
const indexes = new Set(
(await queryInterface.showIndex(COMPLETION_RECEIPT_JOURNAL_TABLE)).map(
(index) => index.name,
),
);
assert.ok(indexes.has(COMPLETION_RECEIPT_JOURNAL_SCAN_INDEX));
assert.ok(indexes.has(COMPLETION_RECEIPT_JOURNAL_PURGE_INDEX));
await assert.rejects(
queryInterface.bulkInsert(COMPLETION_RECEIPT_JOURNAL_TABLE, [
{
attempt_id: '019f70a0-0000-7000-8000-000000000099',
run_id: '019f70a0-0000-7000-8000-000000000098',
state: 'pending',
registered_at_ms: 1,
updated_at_ms: 1,
},
]),
/FOREIGN KEY|constraint/i,
);
});
test('creates the durable fenced Worker registry and bounded lookup indexes', async () => {
const { database, migrationModel } = await createDatabase();
const queryInterface = database.getQueryInterface();
await runMigrations({
database,
migrationModel,
migrations: [workerRegistryMigration],
logger: { info() {} },
});
const columns = await queryInterface.describeTable(WORKER_REGISTRY_TABLE);
for (const column of [
'id',
'session_id',
'generation',
'status',
'version',
'capabilities_json',
'capabilities_hash',
'max_concurrent_runs',
'available_slots',
'registered_at_ms',
'last_heartbeat_at_ms',
'lease_expires_at_ms',
'updated_at_ms',
]) {
assert.ok(columns[column], `missing workers.${column}`);
}
const indexes = new Set(
(await queryInterface.showIndex(WORKER_REGISTRY_TABLE)).map(
(index) => index.name,
),
);
assert.ok(indexes.has(WORKER_REGISTRY_LEASE_INDEX));
assert.ok(indexes.has(WORKER_REGISTRY_CAPACITY_INDEX));
await assert.rejects(
queryInterface.bulkInsert(WORKER_REGISTRY_TABLE, [
{
id: 'worker-invalid',
session_id: '019f7500-0000-7000-8000-000000000001',
generation: 0,
status: 'online',
version: 0,
capabilities_json: '{}',
capabilities_hash: 'a'.repeat(64),
max_concurrent_runs: 1,
available_slots: 1,
registered_at_ms: 1,
last_heartbeat_at_ms: 1,
lease_expires_at_ms: 2,
updated_at_ms: 1,
},
]),
/constraint/i,
);
});
test('creates attempt-scoped Run dispatch leases with Worker fencing indexes', async () => {
const { database, migrationModel } = await createDatabase();
const queryInterface = database.getQueryInterface();
await runMigrations({
database,
migrationModel,
migrations: [
runSchemaMigration,
workerRegistryMigration,
runDispatchLeaseMigration,
],
logger: { info() {} },
});
const columns = await queryInterface.describeTable(RUN_DISPATCH_LEASE_TABLE);
for (const column of [
'attempt_id',
'run_id',
'status',
'version',
'lease_generation',
'worker_id',
'worker_session_id',
'worker_generation',
'lease_token',
'expires_at_ms',
'completed_at_ms',
]) {
assert.ok(columns[column], `missing ${RUN_DISPATCH_LEASE_TABLE}.${column}`);
}
const indexes = await queryInterface.showIndex(RUN_DISPATCH_LEASE_TABLE);
assert.ok(
indexes.some((index) => index.name === RUN_DISPATCH_LEASE_EXPIRY_INDEX),
);
assert.ok(
indexes.some((index) => index.name === RUN_DISPATCH_LEASE_WORKER_INDEX),
);
assert.ok(
indexes.some(
(index) => index.name === RUN_DISPATCH_LEASE_TOKEN_INDEX && index.unique,
),
);
await assert.rejects(
database.query(
`INSERT INTO ${RUN_DISPATCH_LEASE_TABLE} (
attempt_id, run_id, status, version, lease_generation,
worker_id, worker_session_id, worker_generation, lease_token,
acquired_at_ms, renewed_at_ms, expires_at_ms, updated_at_ms
) VALUES (
'missing-attempt', 'missing-run', 'leased', -1, 0,
'missing-worker', '019f7800-0000-7000-8000-000000000001', 0,
'lease_token_abcdefghijklmnopqrstuvwxyz0123456789', 1, 1, 2, 1
)`,
),
);
});
test('adds stable Run references to legacy RunningInstance rows', async () => {
const { database, migrationModel } = await createDatabase();
const queryInterface = database.getQueryInterface();
await queryInterface.createTable(RUNNING_INSTANCE_TABLE, {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
cron_id: { type: DataTypes.INTEGER, allowNull: false },
pid: { type: DataTypes.INTEGER, allowNull: true },
log_path: { type: DataTypes.STRING, allowNull: true },
started_at: { type: DataTypes.INTEGER, allowNull: false },
finished_at: { type: DataTypes.INTEGER, allowNull: true },
status: { type: DataTypes.INTEGER, allowNull: false },
exit_code: { type: DataTypes.INTEGER, allowNull: true },
});
await queryInterface.bulkInsert(RUNNING_INSTANCE_TABLE, [
{ cron_id: 7, started_at: 1_750_000_000, status: 1 },
]);
const options = {
database,
migrationModel,
migrations: [runningInstanceRunReferenceMigration],
logger: { info() {} },
};
await runMigrations(options);
await runMigrations(options);
const columns = await queryInterface.describeTable(RUNNING_INSTANCE_TABLE);
assert.ok(columns.run_id);
assert.ok(columns.attempt_id);
assert.equal(columns.run_id.allowNull, true);
assert.equal(columns.attempt_id.allowNull, true);
const indexNames = new Set(
(await queryInterface.showIndex(RUNNING_INSTANCE_TABLE)).map(
(index) => index.name,
),
);
assert.ok(indexNames.has(RUNNING_INSTANCE_RUN_INDEX));
assert.ok(indexNames.has(RUNNING_INSTANCE_ATTEMPT_INDEX));
const fixture = database.define(
'RunningInstanceMigrationFixture',
{
id: { type: DataTypes.INTEGER, primaryKey: true },
cron_id: DataTypes.INTEGER,
run_id: DataTypes.STRING(36),
attempt_id: DataTypes.STRING(36),
started_at: DataTypes.INTEGER,
status: DataTypes.INTEGER,
},
{ tableName: RUNNING_INSTANCE_TABLE, timestamps: false },
);
const legacyRow = await fixture.findByPk(1, { raw: true });
assert.equal(legacyRow.run_id, null);
assert.equal(legacyRow.attempt_id, null);
const runId = '019f7110-0000-7000-8000-000000000001';
const attemptId = '019f7110-0000-7000-8000-000000000002';
await queryInterface.bulkInsert(RUNNING_INSTANCE_TABLE, [
{
cron_id: 7,
run_id: runId,
attempt_id: attemptId,
started_at: 1_750_000_001,
status: 0,
},
]);
await assert.rejects(
queryInterface.bulkInsert(RUNNING_INSTANCE_TABLE, [
{
cron_id: 7,
run_id: runId,
attempt_id: attemptId,
started_at: 1_750_000_002,
status: 0,
},
]),
);
assert.equal(await migrationModel.count(), 1);
});
test('adds durable cancellation requests without rewriting legacy Run rows', async () => {
const { database, migrationModel } = await createDatabase();
const queryInterface = database.getQueryInterface();
await runMigrations({
database,
migrationModel,
migrations: [runSchemaMigration],
logger: { info() {} },
});
const runId = '019f7110-0000-7000-8000-000000000004';
await queryInterface.bulkInsert(RUN_TABLE, [
{
id: runId,
project_id: 'default',
task_id: 'legacy-cron:7',
task_revision: 'revision-7',
trigger_type: 'manual',
execution_origin: 'manual',
execution_owner: 'runtime',
status: 'running',
version: 3,
event_sequence: 3,
priority: 0,
created_at_ms: 1_750_000_000_000,
started_at_ms: 1_750_000_000_010,
},
]);
const options = {
database,
migrationModel,
migrations: [runSchemaMigration, runCancellationRequestMigration],
logger: { info() {} },
};
await runMigrations(options);
await runMigrations(options);
const columns = await queryInterface.describeTable(RUN_TABLE);
assert.ok(columns.cancel_requested_at_ms);
assert.ok(columns.cancel_reason);
assert.equal(columns.cancel_requested_at_ms.allowNull, true);
assert.equal(columns.cancel_reason.allowNull, true);
const indexNames = new Set(
(await queryInterface.showIndex(RUN_TABLE)).map((index) => index.name),
);
assert.ok(indexNames.has(RUN_CANCELLATION_REQUEST_INDEX));
const [legacyRow] = await database.query(
`SELECT cancel_requested_at_ms, cancel_reason FROM ${RUN_TABLE} WHERE id = :runId`,
{
replacements: { runId },
type: QueryTypes.SELECT,
},
);
assert.equal(legacyRow.cancel_requested_at_ms, null);
assert.equal(legacyRow.cancel_reason, null);
assert.equal(await migrationModel.count(), 2);
});
test('adds nullable Attempt deadlines without rewriting existing attempts', async () => {
const { database, migrationModel } = await createDatabase();
const queryInterface = database.getQueryInterface();
await runMigrations({
database,
migrationModel,
migrations: [runSchemaMigration],
logger: { info() {} },
});
const runId = '019f7110-0000-7000-8000-000000000014';
const attemptId = '019f7110-0000-7000-8000-000000000015';
await queryInterface.bulkInsert(RUN_TABLE, [
{
id: runId,
project_id: 'default',
task_id: 'legacy-cron:8',
task_revision: 'revision-8',
trigger_type: 'manual',
execution_origin: 'manual',
execution_owner: 'runtime',
status: 'dispatching',
version: 2,
event_sequence: 2,
priority: 0,
created_at_ms: 1_750_000_000_000,
},
]);
await queryInterface.bulkInsert(RUN_ATTEMPT_TABLE, [
{
id: attemptId,
run_id: runId,
attempt: 1,
status: 'starting',
executor_type: 'local_process',
callback_sequence: 0,
created_at_ms: 1_750_000_000_000,
},
]);
const options = {
database,
migrationModel,
migrations: [runSchemaMigration, runAttemptDeadlineMigration],
logger: { info() {} },
};
await runMigrations(options);
await runMigrations(options);
const columns = await queryInterface.describeTable(RUN_ATTEMPT_TABLE);
assert.ok(columns.deadline_at_ms);
assert.equal(columns.deadline_at_ms.allowNull, true);
const indexNames = new Set(
(await queryInterface.showIndex(RUN_ATTEMPT_TABLE)).map(
(index) => index.name,
),
);
assert.ok(indexNames.has(RUN_ATTEMPT_DEADLINE_INDEX));
const [legacyAttempt] = await database.query(
`SELECT deadline_at_ms FROM ${RUN_ATTEMPT_TABLE} WHERE id = :attemptId`,
{
replacements: { attemptId },
type: QueryTypes.SELECT,
},
);
assert.equal(legacyAttempt.deadline_at_ms, null);
assert.equal(await migrationModel.count(), 2);
});
test('creates a fenced cancellation dispatch lease bound to a Run Attempt', async () => {
const { database, migrationModel } = await createDatabase();
const queryInterface = database.getQueryInterface();
await runMigrations({
database,
migrationModel,
migrations: [
runSchemaMigration,
runCancellationRequestMigration,
runCancellationDispatchMigration,
],
logger: { info() {} },
});
const columns = await queryInterface.describeTable(
RUN_CANCELLATION_DISPATCH_TABLE,
);
for (const column of [
'run_id',
'attempt_id',
'status',
'version',
'dispatch_count',
'next_attempt_at_ms',
'lease_owner',
'lease_token',
'lease_expires_at_ms',
'last_result',
'last_dispatched_at_ms',
'created_at_ms',
'updated_at_ms',
]) {
assert.ok(columns[column], `missing cancellation dispatch.${column}`);
}
const indexNames = new Set(
(await queryInterface.showIndex(RUN_CANCELLATION_DISPATCH_TABLE)).map(
(index) => index.name,
),
);
assert.ok(indexNames.has(RUN_CANCELLATION_DISPATCH_DUE_INDEX));
assert.ok(indexNames.has(RUN_CANCELLATION_DISPATCH_LEASE_INDEX));
const runId = '019f7110-0000-7000-8000-000000000011';
const attemptId = '019f7110-0000-7000-8000-000000000012';
await queryInterface.bulkInsert(RUN_TABLE, [
{
id: runId,
project_id: 'default',
task_id: 'legacy-cron:8',
task_revision: 'revision-8',
trigger_type: 'manual',
execution_origin: 'manual',
execution_owner: 'runtime',
status: 'running',
version: 2,
event_sequence: 2,
priority: 0,
created_at_ms: 1_750_000_000_000,
cancel_requested_at_ms: 1_750_000_000_100,
cancel_reason: 'user',
},
]);
await queryInterface.bulkInsert(RUN_ATTEMPT_TABLE, [
{
id: attemptId,
run_id: runId,
attempt: 1,
status: 'running',
executor_type: 'local_process',
callback_sequence: 0,
created_at_ms: 1_750_000_000_010,
},
]);
await queryInterface.bulkInsert(RUN_CANCELLATION_DISPATCH_TABLE, [
{
run_id: runId,
attempt_id: attemptId,
status: 'pending',
version: 0,
dispatch_count: 0,
next_attempt_at_ms: 1_750_000_000_100,
created_at_ms: 1_750_000_000_100,
updated_at_ms: 1_750_000_000_100,
},
]);
await assert.rejects(
queryInterface.bulkInsert(RUN_CANCELLATION_DISPATCH_TABLE, [
{
run_id: 'missing-run',
attempt_id: attemptId,
status: 'pending',
version: 0,
dispatch_count: 0,
created_at_ms: 1_750_000_000_100,
updated_at_ms: 1_750_000_000_100,
},
]),
);
await assert.rejects(
queryInterface.bulkUpdate(
RUN_CANCELLATION_DISPATCH_TABLE,
{ version: -1 },
{ run_id: runId },
),
);
});
test('runs the registered migration chain against a legacy database fixture', async () => {
const { database, migrationModel } = await createDatabase();
const queryInterface = database.getQueryInterface();
for (const table of ['CrontabViews', 'Subscriptions', 'Crontabs', 'Envs']) {
await queryInterface.createTable(table, {
id: { type: DataTypes.INTEGER, primaryKey: true },
});
}
const options = {
database,
migrationModel,
logger: { info() {} },
};
await runMigrations(options);
await runMigrations(options);
const tables = new Set(await queryInterface.showAllTables());
assert.ok(tables.has(RUN_TABLE));
assert.ok(tables.has(RUN_ATTEMPT_TABLE));
assert.ok(tables.has(RUN_EVENT_TABLE));
assert.ok(tables.has(RUN_CANCELLATION_DISPATCH_TABLE));
assert.equal(await migrationModel.count(), migrations.length);
});
+108
View File
@@ -0,0 +1,108 @@
const assert = require('node:assert/strict');
const { createHash } = require('node:crypto');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { afterEach, test } = require('node:test');
let nodeSqlite;
try {
nodeSqlite = require('node:sqlite');
} catch {
nodeSqlite = undefined;
}
const temporaryDirectories = [];
const nodeMajor = Number(process.versions.node.split('.')[0]);
async function temporaryRoot() {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-backup-'));
temporaryDirectories.push(root);
return root;
}
function sha256(value) {
return createHash('sha256').update(value).digest('hex');
}
afterEach(async () => {
await Promise.all(
temporaryDirectories
.splice(0)
.map((directory) => fs.rm(directory, { recursive: true, force: true })),
);
});
test(
'Node 24 Online Backup produces an integrity-checked restorable snapshot',
{ skip: nodeMajor < 24 },
async () => {
assert.equal(typeof nodeSqlite?.backup, 'function');
const root = await temporaryRoot();
const sourcePath = path.join(root, 'source.sqlite');
const backupPath = path.join(root, 'backup.sqlite');
const restoredPath = path.join(root, 'restored.sqlite');
const source = new nodeSqlite.DatabaseSync(sourcePath);
source.exec(
'PRAGMA journal_mode=WAL; CREATE TABLE facts(id INTEGER PRIMARY KEY, value TEXT NOT NULL);',
);
const insert = source.prepare('INSERT INTO facts(value) VALUES (?)');
for (let index = 0; index < 2_000; index += 1) {
insert.run('fact-' + index + '-' + 'x'.repeat(128));
}
let progressCalls = 0;
let insertedDuringBackup = false;
const pages = await nodeSqlite.backup(source, backupPath, {
rate: 1,
progress() {
progressCalls += 1;
if (!insertedDuringBackup) {
insertedDuringBackup = true;
insert.run('inserted-during-online-backup');
}
},
});
source.close();
assert.ok(pages > 0);
assert.ok(progressCalls > 0);
assert.equal(insertedDuringBackup, true);
const backup = new nodeSqlite.DatabaseSync(backupPath, {
readOnly: true,
});
assert.equal(
backup.prepare('PRAGMA integrity_check').get().integrity_check,
'ok',
);
assert.equal(
backup
.prepare(
"SELECT COUNT(*) AS count FROM facts WHERE value = 'inserted-during-online-backup'",
)
.get().count,
1,
);
const expectedCount = backup
.prepare('SELECT COUNT(*) AS count FROM facts')
.get().count;
backup.close();
await fs.copyFile(backupPath, restoredPath);
const restoredBytes = await fs.readFile(restoredPath);
const backupBytes = await fs.readFile(backupPath);
assert.equal(sha256(restoredBytes), sha256(backupBytes));
const restored = new nodeSqlite.DatabaseSync(restoredPath, {
readOnly: true,
});
assert.equal(
restored.prepare('PRAGMA quick_check').get().quick_check,
'ok',
);
assert.equal(
restored.prepare('SELECT COUNT(*) AS count FROM facts').get().count,
expectedCount,
);
restored.close();
},
);
@@ -0,0 +1,170 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { execFileSync } = require('node:child_process');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { afterEach, test } = require('node:test');
const { DataTypes, Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const {
RUN_ATTEMPT_TABLE,
RUN_EVENT_TABLE,
RUN_TABLE,
} = require('../../back/migrations/0002-run-schema');
const {
RUN_CANCELLATION_DISPATCH_TABLE,
} = require('../../back/migrations/0005-run-cancellation-dispatch');
const { migrations } = require('../../back/migrations');
const { runMigrations } = require('../../back/migrations/runner');
const {
parseArguments: parseLegacySchemaAuditArguments,
} = require('../../scripts/ql3-schema-audit.cjs');
let nodeSqlite;
try {
nodeSqlite = require('node:sqlite');
} catch {
nodeSqlite = undefined;
}
const nodeMajor = Number(process.versions.node.split('.')[0]);
const temporaryDirectories = [];
test('legacy schema audit never selects a live database implicitly', () => {
assert.throws(
() => parseLegacySchemaAuditArguments([]),
/requires an explicit --database path/,
);
assert.throws(
() =>
parseLegacySchemaAuditArguments([
'--database=/tmp/a.sqlite',
'--database=/tmp/b.sqlite',
]),
/must not be duplicated/,
);
});
async function createMigratedDatabase() {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-node-sqlite-'));
temporaryDirectories.push(root);
const storage = path.join(root, 'database.sqlite');
const database = new Sequelize({
dialect: 'sqlite',
storage,
logging: false,
});
const queryInterface = database.getQueryInterface();
for (const table of ['CrontabViews', 'Subscriptions', 'Crontabs', 'Envs']) {
await queryInterface.createTable(table, {
id: { type: DataTypes.INTEGER, primaryKey: true },
});
}
await queryInterface.createTable('RunningInstances', {
id: { type: DataTypes.INTEGER, primaryKey: true },
started_at: { type: DataTypes.INTEGER, allowNull: false },
});
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations,
logger: { info() {} },
});
await database.close();
return storage;
}
afterEach(async () => {
await Promise.all(
temporaryDirectories
.splice(0)
.map((directory) => fs.rm(directory, { recursive: true, force: true })),
);
});
test(
'Node 24 opens the Sequelize migration chain with defensive node:sqlite options',
{ skip: nodeMajor < 24 },
async () => {
assert.equal(typeof nodeSqlite?.DatabaseSync, 'function');
const storage = await createMigratedDatabase();
const database = new nodeSqlite.DatabaseSync(storage, {
allowExtension: false,
allowUnknownNamedParameters: false,
defensive: true,
enableDoubleQuotedStringLiterals: false,
enableForeignKeyConstraints: true,
readOnly: true,
timeout: 1_000,
});
try {
assert.equal(
database.prepare('PRAGMA integrity_check').get().integrity_check,
'ok',
);
const tables = database
.prepare(
"SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name",
)
.all()
.map((row) => row.name);
for (const table of [
RUN_TABLE,
RUN_ATTEMPT_TABLE,
RUN_EVENT_TABLE,
RUN_CANCELLATION_DISPATCH_TABLE,
'SchemaMigrations',
]) {
assert.ok(tables.includes(table), `missing ${table}`);
}
const runColumns = database
.prepare(`PRAGMA table_info(${RUN_TABLE})`)
.all()
.map((row) => row.name);
for (const column of [
'id',
'status',
'version',
'event_sequence',
'cancel_requested_at_ms',
]) {
assert.ok(runColumns.includes(column), `missing Runs.${column}`);
}
const attemptColumns = database
.prepare(`PRAGMA table_info(${RUN_ATTEMPT_TABLE})`)
.all()
.map((row) => row.name);
assert.ok(
attemptColumns.includes('deadline_at_ms'),
'missing RunAttempts.deadline_at_ms',
);
assert.throws(
() => database.exec('CREATE TABLE must_not_write(id INTEGER)'),
/read.?only/i,
);
} finally {
database.close();
}
const audit = JSON.parse(
execFileSync(
process.execPath,
[
path.resolve(__dirname, '../../scripts/ql3-schema-audit.cjs'),
'--json',
`--database=${storage}`,
],
{ encoding: 'utf8' },
),
);
assert.equal(audit.compatible, true);
assert.equal(audit.driftDetected, false);
},
);
@@ -0,0 +1,77 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { EventEmitter } = require('node:events');
const { test } = require('node:test');
const {
observeLegacyChildProcess,
} = require('../../back/runtime/compatibility/observeLegacyChildProcess');
function recordingObservation() {
const facts = [];
return {
facts,
observation: {
spawned(fact) {
facts.push(['spawned', fact]);
},
running(fact) {
facts.push(['running', fact]);
},
startFailed(fact) {
facts.push(['start_failed', fact]);
},
exited(fact) {
facts.push(['exited', fact]);
},
cancelled(fact) {
facts.push(['cancelled', fact]);
},
},
};
}
test('observes an existing child lifecycle without creating another process', () => {
const child = new EventEmitter();
child.pid = 4242;
const { facts, observation } = recordingObservation();
const timestamps = [100, 101];
observeLegacyChildProcess(child, observation, {
now: () => timestamps.shift(),
logArtifactId: 'legacy-log-1',
});
child.emit('spawn');
child.emit('exit', 0, null);
assert.deepEqual(facts, [
[
'spawned',
{
atMs: 100,
pid: 4242,
executorHandle: 'legacy-local:4242',
logArtifactId: 'legacy-log-1',
},
],
['running', { atMs: 100 }],
['exited', { atMs: 101, exitCode: 0 }],
]);
});
test('maps child errors and signals to bounded observation facts', () => {
const child = new EventEmitter();
const { facts, observation } = recordingObservation();
const timestamps = [200, 201];
observeLegacyChildProcess(child, observation, {
now: () => timestamps.shift(),
});
child.emit('error', new Error('must not leak'));
child.emit('exit', null, 'SIGTERM');
assert.deepEqual(facts, [
['start_failed', { atMs: 200, errorCode: 'LEGACY_PROCESS_ERROR' }],
['exited', { atMs: 201, exitCode: null, signal: 'SIGTERM' }],
]);
});
@@ -0,0 +1,145 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
createLocalProcessDurableHandle,
} = require('../../back/runtime/adapters/local-process/localProcessIdentity');
const {
LocalProcessPersistedExecutionController,
} = require('../../back/runtime/adapters/local-process/persistedLocalProcessController');
const IDENTITY = {
platform: 'linux',
bootId: '11111111-2222-3333-4444-555555555555',
pid: 4321,
processGroupId: 4321,
startTimeTicks: '123456',
};
const HANDLE = createLocalProcessDurableHandle('handle-1', IDENTITY);
function controller(inspections, overrides = {}) {
const signals = [];
let index = 0;
return {
signals,
value: new LocalProcessPersistedExecutionController({
identityProvider: {
async capture() {
throw new Error('not used');
},
async inspect(identity) {
assert.deepEqual(identity, IDENTITY);
const status = inspections[Math.min(index, inspections.length - 1)];
index += 1;
return { status };
},
},
sendSignal(pid, signal) {
signals.push({ pid, signal });
},
graceMs: 10,
pollIntervalMs: 5,
sleep: async () => undefined,
...overrides,
}),
};
}
const REASON = { kind: 'user', requestedAtMs: 1_750_000_000_000 };
test('refuses malformed, PID-mismatched, and non-leader handles without signals', async () => {
const instance = controller(['running']);
assert.equal(
(await instance.value.stop({ durableHandle: 'bad', reason: REASON }))
.status,
'invalid',
);
assert.equal(
(
await instance.value.stop({
durableHandle: HANDLE,
expectedPid: 9999,
reason: REASON,
})
).status,
'pid_mismatch',
);
const nonLeader = createLocalProcessDurableHandle('handle-2', {
...IDENTITY,
processGroupId: 4000,
});
assert.equal(
(await instance.value.stop({ durableHandle: nonLeader, reason: REASON }))
.status,
'identity_mismatch',
);
assert.deepEqual(instance.signals, []);
});
test('sends TERM only when the persisted process exits during grace', async () => {
const instance = controller(['running', 'exited']);
const result = await instance.value.stop({
durableHandle: HANDLE,
expectedPid: 4321,
reason: REASON,
});
assert.deepEqual(result, {
status: 'termination_requested',
termSignalSent: true,
killSignalSent: false,
});
assert.deepEqual(instance.signals, [{ pid: -4321, signal: 'SIGTERM' }]);
});
test('revalidates identity before escalating a persistent process to KILL', async () => {
const persistent = controller(['running', 'running', 'running', 'running']);
const killed = await persistent.value.stop({
durableHandle: HANDLE,
reason: REASON,
});
assert.equal(killed.killSignalSent, true);
assert.deepEqual(persistent.signals, [
{ pid: -4321, signal: 'SIGTERM' },
{ pid: -4321, signal: 'SIGKILL' },
]);
const changed = controller([
'running',
'running',
'running',
'identity_mismatch',
]);
const refused = await changed.value.stop({
durableHandle: HANDLE,
reason: REASON,
});
assert.equal(refused.status, 'identity_mismatch');
assert.equal(refused.killSignalSent, false);
assert.deepEqual(changed.signals, [{ pid: -4321, signal: 'SIGTERM' }]);
});
test('treats an already exited process and ESRCH as idempotent completion', async () => {
const exited = controller(['exited']);
assert.deepEqual(
await exited.value.stop({ durableHandle: HANDLE, reason: REASON }),
{
status: 'already_exited',
termSignalSent: false,
killSignalSent: false,
},
);
const noProcess = controller(['running'], {
sendSignal() {
const error = new Error('gone');
error.code = 'ESRCH';
throw error;
},
});
assert.equal(
(await noProcess.value.stop({ durableHandle: HANDLE, reason: REASON }))
.status,
'already_exited',
);
});
@@ -0,0 +1,214 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
PinnedTaskLocalRunDispatchPlanSource,
} = require('../../back/runtime/application/pinnedTaskLocalRunDispatchPlanSource');
const {
MAX_EXECUTION_ENVIRONMENT_ENTRIES,
normalizeExecutionContext,
} = require('../../back/runtime/domain/executionContext');
function candidate() {
return {
runId: 'run-pinned',
attemptId: 'attempt-2',
projectId: 'default',
taskId: 'task-pinned',
taskRevision: 'revision-7',
executorType: 'local_process',
priority: 0,
queuedAtMs: 1_760_000_000_000,
attemptCreatedAtMs: 1_760_000_000_000,
};
}
function revision(overrides = {}) {
return {
projectId: 'default',
taskId: 'task-pinned',
taskRevision: 'revision-7',
executorType: 'local_process',
execution: {
command: { kind: 'argv', file: '/bin/true', args: ['original'] },
environmentPolicy: 'isolated',
timeoutMs: 5_000,
terminationGraceMs: 100,
},
contextRef: 'context:task-pinned:revision-7',
...overrides,
};
}
test('materializes one pinned revision with fresh bounded context capabilities', async () => {
const sourceRevision = revision();
sourceRevision.execution.runId = 'must-not-override-attempt-identity';
const environment = { TOKEN: 'in-memory-secret' };
const output = { async write() {} };
const revisionRequests = [];
const contextRequests = [];
const dispose = () => undefined;
const source = new PinnedTaskLocalRunDispatchPlanSource(
{
async resolve(request) {
revisionRequests.push(request);
assert.equal(Object.isFrozen(request), true);
return sourceRevision;
},
},
{
async prepare(request) {
contextRequests.push(request);
assert.equal(Object.isFrozen(request), true);
assert.equal(Object.isFrozen(request.candidate), true);
return { context: { environment, output }, dispose };
},
},
);
const plan = await source.prepare(candidate());
assert.deepEqual(revisionRequests, [
{
projectId: 'default',
taskId: 'task-pinned',
taskRevision: 'revision-7',
},
]);
assert.equal(contextRequests[0].contextRef, sourceRevision.contextRef);
assert.equal(plan.executionSpec.runId, 'run-pinned');
assert.equal(plan.executionSpec.attemptId, 'attempt-2');
assert.equal(plan.executionSpec.timeoutMs, 5_000);
assert.equal(plan.context.output, output);
assert.equal(Object.isFrozen(plan.context.environment), true);
assert.equal(plan.dispose, dispose);
sourceRevision.execution.command.args[0] = 'mutated';
environment.TOKEN = 'mutated-secret';
assert.deepEqual(plan.executionSpec.command.args, ['original']);
assert.equal(plan.context.environment.TOKEN, 'in-memory-secret');
});
test('never falls back when the exact revision or context is unavailable', async () => {
let contextCalls = 0;
const missingRevision = new PinnedTaskLocalRunDispatchPlanSource(
{
async resolve() {
return null;
},
},
{
async prepare() {
contextCalls += 1;
return null;
},
},
);
assert.equal(await missingRevision.prepare(candidate()), null);
assert.equal(contextCalls, 0);
const missingContext = new PinnedTaskLocalRunDispatchPlanSource(
{
async resolve() {
return revision();
},
},
{
async prepare() {
return null;
},
},
);
assert.equal(await missingContext.prepare(candidate()), null);
});
test('rejects revision or executor drift before materializing any context', async () => {
let contextCalls = 0;
const create = (value) =>
new PinnedTaskLocalRunDispatchPlanSource(
{
async resolve() {
return value;
},
},
{
async prepare() {
contextCalls += 1;
return null;
},
},
);
await assert.rejects(
create(revision({ taskRevision: 'revision-latest' })).prepare(candidate()),
/does not match/,
);
await assert.rejects(
create(revision({ executorType: 'remote_worker' })).prepare(candidate()),
/does not match/,
);
assert.equal(contextCalls, 0);
});
test('bounds environment count, values, names and output capabilities', () => {
const output = { async write() {} };
assert.throws(
() =>
normalizeExecutionContext({
environment: Object.fromEntries(
Array.from(
{ length: MAX_EXECUTION_ENVIRONMENT_ENTRIES + 1 },
(_, index) => [`KEY_${index}`, 'value'],
),
),
output,
}),
/too many entries/,
);
assert.throws(
() =>
normalizeExecutionContext({
environment: { 'INVALID=NAME': 'value' },
output,
}),
/entry is invalid/,
);
assert.throws(
() =>
normalizeExecutionContext({
environment: {},
output: {},
}),
/output sink is invalid/,
);
const prototypeSafe = normalizeExecutionContext({
environment: JSON.parse('{"__proto__":"literal"}'),
output,
});
assert.equal(Object.getPrototypeOf(prototypeSafe.environment), null);
assert.equal(prototypeSafe.environment.__proto__, 'literal');
});
test('disposes materialized capabilities when context validation fails', async () => {
let disposeCalls = 0;
const source = new PinnedTaskLocalRunDispatchPlanSource(
{
async resolve() {
return revision();
},
},
{
async prepare() {
return {
context: { environment: {}, output: {} },
dispose() {
disposeCalls += 1;
},
};
},
},
);
await assert.rejects(source.prepare(candidate()), /output sink is invalid/);
assert.equal(disposeCalls, 1);
});
@@ -0,0 +1,178 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
PostgresMigrationLeaderUnavailableError,
PostgresMigrationStreamStore,
} = require('../../back/migrations/adapters/postgresMigrationStreamStore');
const {
runMigrationStream,
} = require('../../back/migrations/core/migrationStream');
const CHECKSUM = 'a'.repeat(64);
function createPool(lockResults = [true, true]) {
const records = new Map();
const calls = [];
let released = 0;
let lockIndex = 0;
function selectRecord(values, source) {
const record = source.get(values[0]);
return { rows: record ? [{ ...record }] : [] };
}
return {
calls,
records,
get released() {
return released;
},
pool: {
async query(text, values = []) {
calls.push({ scope: 'pool', text, values: [...values] });
if (text.startsWith('SELECT\n stream_id')) {
if (values.length === 0) {
return {
rows: [...records.values()].map((record) => ({ ...record })),
};
}
return selectRecord(values, records);
}
throw new Error(`unexpected pool query: ${text}`);
},
async connect() {
let staged = null;
return {
async query(text, values = []) {
calls.push({ scope: 'client', text, values: [...values] });
if (text === 'BEGIN') {
staged = new Map(
[...records].map(([id, record]) => [id, { ...record }]),
);
return { rows: [] };
}
if (text === 'COMMIT') {
records.clear();
for (const [id, record] of staged) records.set(id, record);
staged = null;
return { rows: [] };
}
if (text === 'ROLLBACK') {
staged = null;
return { rows: [] };
}
if (text.startsWith('SELECT set_config')) return { rows: [] };
if (text.startsWith('SELECT pg_try_advisory_xact_lock')) {
return { rows: [{ acquired: lockResults[lockIndex++] ?? true }] };
}
if (text.startsWith('CREATE SCHEMA')) return { rows: [] };
if (text.startsWith('CREATE TABLE')) return { rows: [] };
if (text.startsWith('SELECT\n stream_id')) {
return selectRecord(values, staged);
}
if (text.startsWith('INSERT INTO')) {
const [migrationId, streamId, dialect, checksum, appliedAtMs] =
values;
staged.set(migrationId, {
migrationId,
streamId,
dialect,
checksum,
appliedAtMs: String(appliedAtMs),
});
return { rows: [], rowCount: 1 };
}
if (text === 'CREATE TABLE ql3.run_probe(id integer)') {
return { rows: [] };
}
throw new Error(`unexpected client query: ${text}`);
},
release() {
released += 1;
},
};
},
},
};
}
function stream(up) {
return {
id: 'postgresql-main',
dialect: 'postgresql',
migrationIdScheme: 'postgres-prefixed',
checksumScheme: 'sha256',
migrations: [
{
id: 'pg-0001-schema-history',
checksum: CHECKSUM,
up,
},
],
};
}
test('serializes PostgreSQL history bootstrap and migration in advisory-lock transactions', async () => {
const state = createPool();
let upCalls = 0;
await runMigrationStream({
stream: stream(async (context) => {
upCalls += 1;
await context.query('CREATE TABLE ql3.run_probe(id integer)');
}),
store: new PostgresMigrationStreamStore(state.pool),
clock: () => 123,
});
assert.equal(upCalls, 1);
assert.equal(state.released, 2);
assert.deepEqual(state.records.get('pg-0001-schema-history'), {
migrationId: 'pg-0001-schema-history',
streamId: 'postgresql-main',
dialect: 'postgresql',
checksum: CHECKSUM,
appliedAtMs: '123',
});
assert.equal(
state.calls.filter(({ text }) =>
text.startsWith('SELECT pg_try_advisory_xact_lock'),
).length,
2,
);
assert.equal(state.calls.filter(({ text }) => text === 'COMMIT').length, 2);
});
test('fails closed before migration work when another migration leader owns the lock', async () => {
const state = createPool([true, false]);
let upCalls = 0;
await assert.rejects(
runMigrationStream({
stream: stream(async () => {
upCalls += 1;
}),
store: new PostgresMigrationStreamStore(state.pool),
}),
PostgresMigrationLeaderUnavailableError,
);
assert.equal(upCalls, 0);
assert.equal(state.records.size, 0);
assert.equal(state.calls.filter(({ text }) => text === 'ROLLBACK').length, 1);
assert.equal(state.released, 2);
});
test('rolls PostgreSQL migration work and history back together', async () => {
const state = createPool();
await assert.rejects(
runMigrationStream({
stream: stream(async () => {
throw new Error('migration work failed');
}),
store: new PostgresMigrationStreamStore(state.pool),
}),
/migration work failed/,
);
assert.equal(state.records.size, 0);
assert.equal(state.calls.filter(({ text }) => text === 'ROLLBACK').length, 1);
});
+302
View File
@@ -0,0 +1,302 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
DuplicateIdempotencyKeyError,
DuplicateRunAttemptError,
DuplicateRunEventError,
RunEventPayloadTooLargeError,
RunRepositoryBusyError,
RunRepositoryConstraintError,
} = require('../../back/runtime/domain/repositoryErrors');
const {
MAX_RUN_EVENT_PAYLOAD_BYTES,
} = require('../../back/runtime/ports/runRepository');
const {
PostgresRunRepository,
PostgresRunTransaction,
} = require('../../back/runtime/adapters/postgresql/runRepository');
const RUN = Object.freeze({
id: '019f70b0-0000-7000-8000-000000000001',
projectId: 'default',
taskId: 'task-1',
taskRevision: 'revision-1',
taskName: 'test',
triggerType: 'manual',
executionOrigin: 'manual',
executionOwner: 'runtime',
triggeredBy: 'user:1',
status: 'created',
version: 0,
eventSequence: 0,
priority: 0,
idempotencyKey: 'request-1',
createdAtMs: 1_750_000_000_000,
});
const ATTEMPT = Object.freeze({
id: '019f70b0-0000-7000-8000-000000000002',
runId: RUN.id,
attempt: 1,
status: 'claimed',
executorType: 'remote_worker',
callbackSequence: 0,
createdAtMs: 1_750_000_000_001,
});
const EVENT = Object.freeze({
id: '019f70b0-0000-7000-8000-000000000003',
runId: RUN.id,
sequence: 1,
type: 'run.created',
dedupeKey: 'run.created',
actorType: 'system',
payload: Object.freeze({ source: 'postgres-test' }),
createdAtMs: 1_750_000_000_002,
});
const RETRY_POLICY = Object.freeze({
runId: RUN.id,
maxAttempts: 3,
retryOnLost: true,
safety: 'idempotent',
backoffBaseMs: 1_000,
backoffMaxMs: 30_000,
version: 0,
createdAtMs: 1_750_000_000_002,
updatedAtMs: 1_750_000_000_002,
});
function driverError(code, constraint) {
return Object.assign(new Error('driver failure'), { code, constraint });
}
function clientHarness(handler = async () => ({ rows: [], rowCount: 0 })) {
const queries = [];
let released = 0;
const client = {
async query(text, values) {
queries.push({ text, values });
return handler(text, values, queries.length);
},
release() {
released += 1;
},
};
return {
client,
queries,
released: () => released,
pool: {
query: (...args) => client.query(...args),
async connect() {
return client;
},
},
};
}
test('orders one bounded PostgreSQL transaction and releases its client', async () => {
const harness = clientHarness();
const repository = new PostgresRunRepository(harness.pool);
const result = await repository.transaction(async () => 'committed');
assert.equal(result, 'committed');
assert.deepEqual(
harness.queries.map(({ text }) => text),
[
'BEGIN',
'SET TRANSACTION ISOLATION LEVEL READ COMMITTED',
"SELECT set_config('statement_timeout', $1, true)",
"SELECT set_config('lock_timeout', $1, true)",
"SELECT set_config('idle_in_transaction_session_timeout', $1, true)",
'COMMIT',
],
);
assert.deepEqual(
harness.queries.slice(2, 5).map(({ values }) => values[0]),
['5000ms', '1000ms', '10000ms'],
);
assert.equal(harness.released(), 1);
});
test('rolls back work errors unchanged and maps commit serialization failure', async () => {
const workHarness = clientHarness();
const workRepository = new PostgresRunRepository(workHarness.pool);
const failure = new Error('work failed');
await assert.rejects(
workRepository.transaction(async () => {
throw failure;
}),
(error) => error === failure,
);
assert.equal(workHarness.queries.at(-1).text, 'ROLLBACK');
assert.equal(workHarness.released(), 1);
const commitHarness = clientHarness(async (text) => {
if (text === 'COMMIT') throw driverError('40001');
return { rows: [], rowCount: 0 };
});
await assert.rejects(
new PostgresRunRepository(commitHarness.pool).transaction(async () => 1),
RunRepositoryBusyError,
);
assert.equal(commitHarness.queries.at(-1).text, 'ROLLBACK');
assert.equal(commitHarness.released(), 1);
});
test('normalizes bigint rows and rejects corrupt enum data', async () => {
const runRow = {
...RUN,
createdAtMs: String(RUN.createdAtMs),
scheduledForMs: '1750000000100',
taskSnapshotRef: null,
legacyCronId: null,
parentRunId: null,
retryOfRunId: null,
triggerId: null,
requestId: null,
queuedAtMs: null,
startedAtMs: null,
finishedAtMs: null,
cancelRequestedAtMs: null,
cancelReason: null,
inputRef: null,
outputRef: null,
errorCode: null,
errorSummary: null,
};
const harness = clientHarness(async (text) => {
if (text.includes('FROM "ql3"."runs"')) {
return { rows: [runRow], rowCount: 1 };
}
throw new Error(`unexpected query: ${text}`);
});
const repository = new PostgresRunRepository(harness.pool);
assert.deepEqual(await repository.findRunById(RUN.id), {
...RUN,
scheduledForMs: 1_750_000_000_100,
});
runRow.status = 'invented';
await assert.rejects(
repository.findRunById(RUN.id),
RunRepositoryConstraintError,
);
});
test('writes every aggregate shape and uses exact CAS predicates', async () => {
const queries = [];
const transaction = new PostgresRunTransaction({
async query(text, values) {
queries.push({ text, values });
return text.startsWith('UPDATE')
? { rows: [{ id: values[0] }], rowCount: 1 }
: { rows: [], rowCount: 1 };
},
});
await transaction.insertRun(RUN);
await transaction.insertAttempt(ATTEMPT);
await transaction.insertRetryPolicy(RETRY_POLICY);
await transaction.appendEvent(EVENT);
assert.equal(
await transaction.compareAndSetRun(
{ ...RUN, status: 'queued', version: 1 },
0,
),
true,
);
assert.equal(
await transaction.compareAndSetAttempt(
{ ...ATTEMPT, status: 'starting', callbackSequence: 1 },
{ status: 'claimed', callbackSequence: 0 },
),
true,
);
assert.equal(
await transaction.compareAndSetRetryPolicy(
{ ...RETRY_POLICY, version: 1, updatedAtMs: 1_750_000_000_003 },
0,
),
true,
);
const updates = queries.filter(({ text }) => text.startsWith('UPDATE'));
assert.equal(updates.length, 3);
assert.match(updates[0].text, /"version" = \$32/);
assert.equal(updates[0].values.length, 32);
assert.match(updates[1].text, /"status" = \$22/);
assert.match(updates[1].text, /"callback_sequence" = \$23/);
assert.equal(updates[1].values.length, 23);
assert.match(updates[2].text, /"version" = \$11/);
assert.equal(updates[2].values.length, 11);
for (const { text } of updates) assert.doesNotMatch(text, /\$\$/);
});
test('maps stable PostgreSQL constraints without leaking driver errors', async () => {
const transactionFor = (error) =>
new PostgresRunTransaction({
async query() {
throw error;
},
});
await assert.rejects(
transactionFor(
driverError('23505', 'ql3_runs_project_idempotency_uidx'),
).insertRun(RUN),
DuplicateIdempotencyKeyError,
);
await assert.rejects(
transactionFor(
driverError('23505', 'ql3_run_attempts_run_attempt_uidx'),
).insertAttempt(ATTEMPT),
DuplicateRunAttemptError,
);
await assert.rejects(
transactionFor(
driverError('23505', 'ql3_run_events_run_dedupe_uidx'),
).appendEvent(EVENT),
DuplicateRunEventError,
);
await assert.rejects(
transactionFor(driverError('23514')).insertRetryPolicy(RETRY_POLICY),
RunRepositoryConstraintError,
);
await assert.rejects(
transactionFor(driverError('55P03')).compareAndSetRun(
{ ...RUN, version: 1 },
0,
),
RunRepositoryBusyError,
);
});
test('rejects non-incrementing CAS and oversized event payloads before SQL', async () => {
let calls = 0;
const transaction = new PostgresRunTransaction({
async query() {
calls += 1;
return { rows: [], rowCount: 0 };
},
});
await assert.rejects(
transaction.compareAndSetRun({ ...RUN, version: 2 }, 0),
RunRepositoryConstraintError,
);
await assert.rejects(
transaction.compareAndSetRetryPolicy(
{ ...RETRY_POLICY, version: 2, updatedAtMs: 1_750_000_000_003 },
0,
),
RunRepositoryConstraintError,
);
await assert.rejects(
transaction.appendEvent({
...EVENT,
payload: { value: 'x'.repeat(MAX_RUN_EVENT_PAYLOAD_BYTES) },
}),
RunEventPayloadTooLargeError,
);
assert.equal(calls, 0);
});
@@ -0,0 +1,153 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
postgresqlMainMigrationStream,
} = require('../../back/migrations/postgresql');
const {
postgresqlControlSchemaContract,
} = require('../../back/migrations/postgresql/schemaContract');
const BANNED_CLUSTER_SCHEMA_NAMES = [
'crontabs',
'envs',
'subscriptions',
'runninginstances',
'completionreceiptjournals',
'localexecutioncontextrecipes',
'localsecretenvelopes',
'localartifactretentioncheckpoints',
'projectownerbootstrapchallenges',
'legacypanelidentitybindings',
];
function tableDefinitionSql(statements, tableName) {
const qualifiedName = `"ql3"."${tableName}"`;
return statements
.filter(
(statement) =>
statement.includes(qualifiedName) &&
(/^CREATE TABLE /.test(statement) || /^ALTER TABLE /.test(statement)),
)
.join('\n');
}
test('defines the immutable PostgreSQL capability and Run core stream', async () => {
assert.equal(postgresqlMainMigrationStream.id, 'postgresql-main');
assert.equal(postgresqlMainMigrationStream.dialect, 'postgresql');
assert.equal(
postgresqlMainMigrationStream.migrationIdScheme,
'postgres-prefixed',
);
assert.equal(postgresqlMainMigrationStream.checksumScheme, 'sha256');
assert.deepEqual(
postgresqlMainMigrationStream.migrations.map(({ id }) => id),
[
'pg-0001-schema-capability',
'pg-0002-run-core',
'pg-0003-run-retry-policy',
],
);
for (const migration of postgresqlMainMigrationStream.migrations) {
assert.match(migration.checksum, /^[0-9a-f]{64}$/);
}
});
test('keeps local-only and legacy tables out of the cluster baseline', async () => {
const statements = [];
for (const migration of postgresqlMainMigrationStream.migrations) {
await migration.up({
async query(statement) {
statements.push(statement);
return { rows: [] };
},
});
}
const canonical = statements.join('\n').toLowerCase();
for (const table of BANNED_CLUSTER_SCHEMA_NAMES) {
assert.equal(canonical.includes(table.toLowerCase()), false, table);
}
for (const table of [
'schema_capabilities',
'runs',
'run_attempts',
'run_events',
'run_retry_policies',
]) {
assert.match(canonical, new RegExp(`"ql3"\\."${table}"`));
}
assert.match(canonical, /deferrable initially deferred/);
assert.match(canonical, /'control-core'/);
assert.match(canonical, /'pg-0003-run-retry-policy'/);
assert.match(canonical, /"run_core":1,"run_retry_policy":1/);
});
test('keeps the reviewed SQL and readiness schema contract in lockstep', async () => {
const statements = [];
for (const migration of postgresqlMainMigrationStream.migrations) {
await migration.up({
async query(statement) {
statements.push(statement);
return { rows: [] };
},
});
}
const canonical = statements.join('\n');
for (const table of postgresqlControlSchemaContract.tables) {
if (table.name === 'schema_migrations') continue;
const definition = tableDefinitionSql(statements, table.name);
assert.match(definition, new RegExp(`"ql3"\\."${table.name}"`));
for (const column of table.columns) {
assert.match(definition, new RegExp(`\\b${column}\\b`));
}
}
for (const index of postgresqlControlSchemaContract.indexes) {
if (index.endsWith('_pkey')) continue;
assert.match(canonical, new RegExp(`CREATE (?:UNIQUE )?INDEX ${index}\\b`));
}
assert.doesNotMatch(canonical, /CREATE TABLE IF NOT EXISTS/);
assert.doesNotMatch(canonical, /CREATE INDEX IF NOT EXISTS/);
});
test('freezes every published PostgreSQL migration checksum', () => {
assert.deepEqual(
postgresqlMainMigrationStream.migrations.map(({ id, checksum }) => ({
id,
checksum,
})),
[
{
id: 'pg-0001-schema-capability',
checksum:
'9e3499e3bcdfe3d7b2559e64ea7bbf236a8a11ba32d6a45af131034887d5a8ab',
},
{
id: 'pg-0002-run-core',
checksum:
'5b59a7f9323746e49c6c321e89007f553a0751f25d16ffd23c3ae37dd87f76e4',
},
{
id: 'pg-0003-run-retry-policy',
checksum:
'621792cde917cc86809bbebff389443e790bdba60f73d04f7a1dc97a0ebf72db',
},
],
);
});
test('advances capability v2 only from the exact v1 predecessor', async () => {
const statements = [];
await postgresqlMainMigrationStream.migrations.at(-1).up({
async query(statement) {
statements.push(statement);
return { rows: [] };
},
});
const capability = statements.at(-1);
assert.match(capability, /contract_version = 1/);
assert.match(capability, /migration_id = 'pg-0002-run-core'/);
assert.match(capability, /capabilities = '\{"run_core":1\}'::jsonb/);
assert.match(capability, /IF NOT FOUND THEN/);
assert.match(capability, /RAISE EXCEPTION/);
});
@@ -0,0 +1,264 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
postgresqlMainMigrationStream,
} = require('../../back/migrations/postgresql');
const {
postgresqlControlSchemaContract,
} = require('../../back/migrations/postgresql/schemaContract');
const {
PostgresSchemaReadinessError,
assertPostgresSchemaReady,
} = require('../../back/migrations/postgresql/schemaReadiness');
function validHistory() {
return postgresqlMainMigrationStream.migrations.map((migration, index) => ({
streamId: 'postgresql-main',
dialect: 'postgresql',
migrationId: migration.id,
checksum: migration.checksum,
appliedAtMs: index + 1,
}));
}
function validPrivileges() {
const expected = {
schema_migrations: [true, false, false, false],
schema_capabilities: [true, false, false, false],
runs: [true, true, true, false],
run_attempts: [true, true, true, false],
run_events: [true, true, false, false],
run_retry_policies: [true, true, true, false],
};
return Object.entries(expected).map(
([
tableName,
[selectAllowed, insertAllowed, updateAllowed, deleteAllowed],
]) => ({
tableName,
selectAllowed,
insertAllowed,
updateAllowed,
deleteAllowed,
isOwner: false,
}),
);
}
function queryable(overrides = {}) {
const contract = postgresqlControlSchemaContract;
return {
async query(text) {
if (text.includes("current_setting('server_version_num')")) {
return {
rows: [
{
serverVersionNum: overrides.serverVersionNum ?? '160014',
currentUser: 'ql3_runtime',
inRecovery: overrides.inRecovery ?? false,
transactionReadOnly: overrides.transactionReadOnly ?? 'off',
},
],
};
}
if (text.includes('FROM "ql3"."schema_migrations"')) {
return { rows: overrides.history ?? validHistory() };
}
if (text.includes('FROM "ql3"."schema_capabilities"')) {
return {
rows: [
overrides.capability ?? {
contractName: 'control-core',
contractVersion: 2,
migrationId: 'pg-0003-run-retry-policy',
capabilities: { run_core: 1, run_retry_policy: 1 },
},
],
};
}
if (text.includes('FROM information_schema.columns')) {
const rows = contract.tables.flatMap((table) =>
table.columns.map((columnName) => ({
tableName: table.name,
columnName,
})),
);
if (overrides.extraTable) {
rows.push({ tableName: overrides.extraTable, columnName: 'id' });
}
return { rows };
}
if (text.includes('FROM pg_indexes')) {
return {
rows: [
...contract.indexes.map((indexName) => ({ indexName })),
...(overrides.extraIndex
? [{ indexName: overrides.extraIndex }]
: []),
],
};
}
if (text.includes('FROM pg_constraint')) {
return {
rows: [
...contract.checks.map((constraintName) => ({
constraintName,
constraintType: 'check',
})),
...contract.foreignKeys.map((constraintName) => ({
constraintName,
constraintType: 'foreign_key',
})),
...(overrides.extraConstraint
? [
{
constraintName: overrides.extraConstraint,
constraintType: 'check',
},
]
: []),
],
};
}
if (text.includes('FROM pg_catalog.pg_roles')) {
return {
rows: [
{
canLogin: true,
superuser: overrides.superuser ?? false,
createDatabase: false,
createRole: false,
replication: false,
bypassRowLevelSecurity: false,
databaseConnect: true,
},
],
};
}
if (text.includes('has_schema_privilege')) {
return {
rows: [
{
schemaUsage: true,
schemaCreate: overrides.schemaCreate ?? false,
},
],
};
}
if (text.includes('has_table_privilege')) {
return { rows: overrides.privileges ?? validPrivileges() };
}
throw new Error(`unexpected query: ${text}`);
},
};
}
test('accepts the exact PostgreSQL control schema and least-privilege runtime role', async () => {
const report = await assertPostgresSchemaReady(queryable());
assert.deepEqual(report, {
ready: true,
writablePrimary: true,
serverVersionNum: 160014,
serverMajor: 16,
currentUser: 'ql3_runtime',
contractName: 'control-core',
contractVersion: 2,
migrationIds: [
'pg-0001-schema-capability',
'pg-0002-run-core',
'pg-0003-run-retry-policy',
],
});
});
test('rejects a standby or read-only endpoint before schema inspection', async () => {
await assert.rejects(
assertPostgresSchemaReady(queryable({ inRecovery: true })),
(error) =>
error instanceof PostgresSchemaReadinessError &&
error.code === 'server_not_writable_primary' &&
error.facts.includes('in-recovery:true'),
);
await assert.rejects(
assertPostgresSchemaReady(queryable({ transactionReadOnly: 'on' })),
(error) =>
error instanceof PostgresSchemaReadinessError &&
error.code === 'server_not_writable_primary' &&
error.facts.includes('transaction-read-only:on'),
);
});
test('rejects unsupported server versions and capability drift', async () => {
await assert.rejects(
assertPostgresSchemaReady(queryable({ serverVersionNum: '150018' })),
(error) =>
error instanceof PostgresSchemaReadinessError &&
error.code === 'server_version_unsupported',
);
await assert.rejects(
assertPostgresSchemaReady(
queryable({
capability: {
contractName: 'control-core',
contractVersion: 3,
migrationId: 'pg-0003-run-retry-policy',
capabilities: { run_core: 1, run_retry_policy: 1 },
},
}),
),
(error) =>
error instanceof PostgresSchemaReadinessError &&
error.code === 'capability_invalid',
);
});
test('rejects unknown ql3 objects and an over-privileged runtime role', async () => {
await assert.rejects(
assertPostgresSchemaReady(
queryable({
extraTable: 'plugin_state',
extraIndex: 'plugin_state_pkey',
extraConstraint: 'plugin_state_payload_check',
}),
),
(error) =>
error instanceof PostgresSchemaReadinessError &&
error.code === 'schema_contract_invalid' &&
error.facts.includes('unknown-table:plugin_state') &&
error.facts.includes('unknown-check:plugin_state_payload_check'),
);
await assert.rejects(
assertPostgresSchemaReady(queryable({ schemaCreate: true })),
(error) =>
error instanceof PostgresSchemaReadinessError &&
error.code === 'runtime_role_invalid',
);
});
test('preserves database availability errors for the outer readiness layer', async () => {
const unavailable = new Error('database unavailable');
let calls = 0;
await assert.rejects(
assertPostgresSchemaReady({
async query() {
calls += 1;
if (calls === 1) {
return {
rows: [
{
serverVersionNum: '160014',
currentUser: 'ql3_runtime',
inRecovery: false,
transactionReadOnly: 'off',
},
],
};
}
throw unavailable;
},
}),
(error) => error === unavailable,
);
});
@@ -0,0 +1,409 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
PrimaryCancellationDispatcher,
} = require('../../back/runtime/application/primaryCancellationDispatcher');
const NOW_MS = 1_750_000_000_100;
function candidate(runId, overrides = {}) {
return {
runId,
requestedAtMs: NOW_MS - 10,
reason: 'user',
attempts: [
{
attemptId: `${runId}-attempt`,
executorType: 'local_process',
executorHandle: `handle:${runId}`,
pid: 4001,
},
],
...overrides,
};
}
function fakeDispatchRepository(overrides = {}) {
const claims = [];
const results = [];
return {
claims,
results,
async findByRunId() {
return null;
},
async claim(command) {
claims.push(command);
if (overrides.claim) return overrides.claim(command);
return {
status: 'claimed',
dispatch: {
runId: command.runId,
attemptId: command.attemptId,
status: 'leased',
version: 1,
dispatchCount: 1,
leaseOwner: command.owner,
leaseToken: command.leaseToken,
leaseExpiresAtMs: command.nowMs + command.leaseDurationMs,
createdAtMs: command.nowMs,
updatedAtMs: command.nowMs,
},
};
},
async recordResult(command) {
results.push(command);
if (overrides.recordResult) return overrides.recordResult(command);
return {
dispatch: {
runId: command.runId,
attemptId: command.attemptId,
status: command.nextAttemptAtMs ? 'retry_wait' : 'dispatched',
version: command.expectedVersion + 1,
dispatchCount: 1,
lastResult: command.result,
createdAtMs: NOW_MS,
updatedAtMs: command.atMs,
},
event: {
id: command.eventId,
runId: command.runId,
sequence: 1,
type: 'fixture',
actorType: 'worker',
payload: {},
createdAtMs: command.atMs,
},
};
},
};
}
function dispatcherOptions(overrides = {}) {
let id = 0;
return {
owner: 'worker-a',
leaseDurationMs: 100,
retryBaseMs: 1_000,
retryMaxMs: 8_000,
clock: () => NOW_MS,
createId: () => `019f71d0-0000-7000-8000-${String(++id).padStart(12, '0')}`,
...overrides,
};
}
test('leases before signalling and durably classifies every controller result', async () => {
const stopped = [];
const source = {
async listCandidates(options) {
assert.deepEqual(options, { limit: 8 });
return {
candidates: [
candidate('run-1'),
candidate('run-2'),
candidate('run-3', { attempts: [] }),
candidate('run-4', {
attempts: [
{
attemptId: 'attempt-4a',
executorType: 'local_process',
executorHandle: 'handle:4a',
},
{
attemptId: 'attempt-4b',
executorType: 'local_process',
executorHandle: 'handle:4b',
},
],
}),
candidate('run-5', {
attempts: [
{
attemptId: 'attempt-5',
executorType: 'remote_worker',
executorHandle: 'remote:5',
},
],
}),
candidate('run-6'),
candidate('run-7'),
candidate('run-8'),
],
truncated: true,
unsafeAttemptOverflow: false,
nextCursor: { requestedAtMs: NOW_MS - 10, runId: 'run-8' },
};
},
};
const dispatches = fakeDispatchRepository({
claim(command) {
if (command.runId === 'run-8') {
return {
status: 'leased',
dispatch: {
runId: command.runId,
attemptId: command.attemptId,
status: 'leased',
version: 3,
dispatchCount: 2,
leaseOwner: 'worker-b',
leaseToken: 'other-lease',
leaseExpiresAtMs: NOW_MS + 1_000,
createdAtMs: NOW_MS - 5_000,
updatedAtMs: NOW_MS - 100,
},
};
}
return {
status: 'claimed',
dispatch: {
runId: command.runId,
attemptId: command.attemptId,
status: 'leased',
version: 1,
dispatchCount: 1,
leaseOwner: command.owner,
leaseToken: command.leaseToken,
leaseExpiresAtMs: command.nowMs + command.leaseDurationMs,
createdAtMs: command.nowMs,
updatedAtMs: command.nowMs,
},
};
},
});
let calls = 0;
const controller = {
executorType: 'local_process',
async stop(input) {
calls += 1;
stopped.push(input);
if (calls === 1) {
return {
status: 'termination_requested',
termSignalSent: true,
killSignalSent: false,
};
}
if (calls === 2) {
return {
status: 'already_exited',
termSignalSent: false,
killSignalSent: false,
};
}
if (calls === 3) throw new Error('transient stop failure');
return {
status: 'identity_mismatch',
termSignalSent: false,
killSignalSent: false,
};
},
};
const dispatcher = new PrimaryCancellationDispatcher(
source,
dispatches,
[controller],
dispatcherOptions(),
);
const summary = await dispatcher.dispatchBatch({ limit: 8 });
assert.deepEqual(summary, {
scanned: 8,
claimed: 5,
terminationRequested: 1,
alreadyExited: 1,
pending: 5,
ambiguous: 1,
blocked: 1,
deferred: 1,
alreadyResolved: 0,
notEligible: 0,
failed: 1,
truncated: true,
unsafeAttemptOverflow: false,
nextCursor: { requestedAtMs: NOW_MS - 10, runId: 'run-8' },
});
assert.equal(stopped.length, 4);
assert.deepEqual(stopped[0], {
durableHandle: 'handle:run-1',
expectedPid: 4001,
reason: { kind: 'user', requestedAtMs: NOW_MS - 10 },
});
assert.deepEqual(
dispatches.results.map((result) => result.result),
[
'termination_requested',
'already_exited',
'controller_missing',
'dispatch_error',
'identity_mismatch',
],
);
assert.equal(
dispatches.results.find((result) => result.result === 'dispatch_error')
.nextAttemptAtMs,
NOW_MS + 1_000,
);
});
test('fails closed on unsafe overflow, duplicate controllers, and corrupt reasons', async () => {
let stopCalls = 0;
const controller = {
executorType: 'local_process',
async stop() {
stopCalls += 1;
throw new Error('must not run');
},
};
const dispatches = fakeDispatchRepository();
assert.throws(
() =>
new PrimaryCancellationDispatcher(
{ async listCandidates() {} },
dispatches,
[controller, controller],
dispatcherOptions(),
),
/Duplicate persisted Executor controller/,
);
const overflow = new PrimaryCancellationDispatcher(
{
async listCandidates() {
return {
candidates: [],
truncated: true,
unsafeAttemptOverflow: true,
};
},
},
dispatches,
[controller],
dispatcherOptions(),
);
const overflowSummary = await overflow.dispatchBatch();
assert.equal(overflowSummary.unsafeAttemptOverflow, true);
const corrupt = new PrimaryCancellationDispatcher(
{
async listCandidates() {
return {
candidates: [candidate('run-corrupt', { reason: 'raw-corruption' })],
truncated: false,
unsafeAttemptOverflow: false,
};
},
},
dispatches,
[controller],
dispatcherOptions(),
);
const corruptSummary = await corrupt.dispatchBatch();
assert.equal(corruptSummary.pending, 1);
assert.equal(stopCalls, 0);
assert.equal(dispatches.claims.length, 0);
});
test('reports persisted terminal, deferred, stale, and failed claims without signalling', async () => {
const statuses = new Map([
['run-not-due', 'not_due'],
['run-dispatched', 'dispatched'],
['run-blocked', 'blocked'],
['run-stale', 'not_eligible'],
]);
const dispatches = fakeDispatchRepository({
claim(command) {
if (command.runId === 'run-failed') throw new Error('database busy');
const status = statuses.get(command.runId);
if (status === 'not_eligible') return { status };
return {
status,
dispatch: {
runId: command.runId,
attemptId: command.attemptId,
status: status === 'not_due' ? 'retry_wait' : status,
version: 2,
dispatchCount: 1,
createdAtMs: NOW_MS - 1_000,
updatedAtMs: NOW_MS - 500,
},
};
},
});
let stopCalls = 0;
const dispatcher = new PrimaryCancellationDispatcher(
{
async listCandidates() {
return {
candidates: [
candidate('run-not-due'),
candidate('run-dispatched'),
candidate('run-blocked'),
candidate('run-stale'),
candidate('run-failed'),
],
truncated: false,
unsafeAttemptOverflow: false,
};
},
},
dispatches,
[
{
executorType: 'local_process',
async stop() {
stopCalls += 1;
throw new Error('must not run');
},
},
],
dispatcherOptions(),
);
const summary = await dispatcher.dispatchBatch();
assert.equal(summary.deferred, 1);
assert.equal(summary.alreadyResolved, 2);
assert.equal(summary.blocked, 1);
assert.equal(summary.notEligible, 1);
assert.equal(summary.failed, 1);
assert.equal(summary.pending, 2);
assert.equal(stopCalls, 0);
});
test('counts result persistence failure while retaining the signal outcome', async () => {
const dispatches = fakeDispatchRepository({
recordResult() {
throw new Error('commit failed');
},
});
const dispatcher = new PrimaryCancellationDispatcher(
{
async listCandidates() {
return {
candidates: [candidate('run-signal-before-commit')],
truncated: false,
unsafeAttemptOverflow: false,
};
},
},
dispatches,
[
{
executorType: 'local_process',
async stop() {
return {
status: 'termination_requested',
termSignalSent: true,
killSignalSent: false,
};
},
},
],
dispatcherOptions(),
);
const summary = await dispatcher.dispatchBatch();
assert.equal(summary.terminationRequested, 1);
assert.equal(summary.failed, 1);
assert.equal(summary.pending, 1);
});
@@ -0,0 +1,202 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
PrimaryCancellationLifecycle,
} = require('../../back/runtime/application/primaryCancellationLifecycle');
function deferred() {
let resolve;
let reject;
const promise = new Promise((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, resolve, reject };
}
function fakeScheduler() {
let id = 0;
const pending = new Map();
const cleared = [];
return {
pending,
cleared,
scheduler: {
setTimeout(callback, delayMs) {
const timer = {
id: ++id,
delayMs,
unrefCalls: 0,
unref() {
this.unrefCalls += 1;
},
};
pending.set(timer.id, { timer, callback });
return timer;
},
clearTimeout(timer) {
cleared.push(timer.id);
pending.delete(timer.id);
},
},
fireNext() {
const next = pending.values().next().value;
assert.ok(next, 'expected a scheduled timer');
pending.delete(next.timer.id);
next.callback();
return next.timer;
},
};
}
function cycleSummary() {
return {
pages: 1,
scanned: 1,
claimed: 1,
terminationRequested: 1,
alreadyExited: 0,
pending: 0,
ambiguous: 0,
blocked: 0,
deferred: 0,
alreadyResolved: 0,
notEligible: 0,
failed: 0,
stopReason: 'complete',
remaining: false,
};
}
async function flush() {
await new Promise((resolve) => setImmediate(resolve));
}
test('is inert until started and never overlaps slow cycles', async () => {
const timers = fakeScheduler();
const first = deferred();
let calls = 0;
const lifecycle = new PrimaryCancellationLifecycle(
{
async runCycle(options) {
calls += 1;
assert.deepEqual(options, { pageSize: 8, maxPages: 2 });
return first.promise;
},
},
{
intervalMs: 2_000,
initialDelayMs: 50,
cycle: { pageSize: 8, maxPages: 2 },
scheduler: timers.scheduler,
},
);
assert.equal(timers.pending.size, 0);
assert.equal(lifecycle.start(), true);
assert.equal(lifecycle.start(), false);
assert.equal(timers.pending.size, 1);
const initial = timers.fireNext();
assert.equal(initial.delayMs, 50);
assert.equal(initial.unrefCalls, 1);
await flush();
assert.equal(calls, 1);
assert.equal(timers.pending.size, 0);
first.resolve(cycleSummary());
await flush();
assert.equal(timers.pending.size, 1);
const next = timers.pending.values().next().value.timer;
assert.equal(next.delayMs, 2_000);
assert.equal(next.unrefCalls, 1);
assert.equal(await lifecycle.stop(), 'drained');
assert.deepEqual(timers.cleared, [next.id]);
});
test('reports cycle errors and continues without callback failure loops', async () => {
const timers = fakeScheduler();
const errors = [];
let calls = 0;
const lifecycle = new PrimaryCancellationLifecycle(
{
async runCycle() {
calls += 1;
if (calls === 1) throw new Error('database busy');
return cycleSummary();
},
},
{
intervalMs: 500,
scheduler: timers.scheduler,
onCycle() {
throw new Error('metrics sink failed');
},
onError(error) {
errors.push(error.message);
},
},
);
lifecycle.start();
timers.fireNext();
await flush();
assert.deepEqual(errors, ['database busy']);
assert.equal(timers.pending.size, 1);
timers.fireNext();
await flush();
assert.deepEqual(errors, ['database busy', 'metrics sink failed']);
assert.equal(timers.pending.size, 1);
assert.equal(await lifecycle.stop(), 'drained');
});
test('stops scheduling immediately and bounds waiting for an in-flight cycle', async () => {
const timers = fakeScheduler();
const running = deferred();
const lifecycle = new PrimaryCancellationLifecycle(
{
async runCycle() {
return running.promise;
},
},
{
intervalMs: 500,
stopTimeoutMs: 5,
scheduler: timers.scheduler,
},
);
lifecycle.start();
timers.fireNext();
await flush();
assert.equal(await lifecycle.stop(), 'timed_out');
assert.equal(timers.pending.size, 0);
assert.equal(lifecycle.start(), false);
running.resolve(cycleSummary());
await flush();
assert.equal(timers.pending.size, 0);
assert.equal(lifecycle.start(), true);
assert.equal(await lifecycle.stop(), 'drained');
});
test('rejects cadences that could create hot loops or unbounded shutdown waits', () => {
const supervisor = {
async runCycle() {
return cycleSummary();
},
};
assert.throws(
() => new PrimaryCancellationLifecycle(supervisor, { intervalMs: 249 }),
RangeError,
);
assert.throws(
() =>
new PrimaryCancellationLifecycle(supervisor, {
intervalMs: 500,
stopTimeoutMs: 60_001,
}),
RangeError,
);
});
@@ -0,0 +1,179 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { afterEach, test } = require('node:test');
const { Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const { runSchemaMigration } = require('../../back/migrations/0002-run-schema');
const {
runCancellationRequestMigration,
} = require('../../back/migrations/0004-run-cancellation-request');
const {
runAttemptDeadlineMigration,
} = require('../../back/migrations/0006-run-attempt-deadline');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacySequelizePrimaryCancellationSource,
} = require('../../back/runtime/adapters/legacy-sequelize/primaryCancellationSource');
const {
LegacySequelizeRunRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/runRepository');
const databases = [];
const BASE_TIME = 1_750_200_000_000;
let idSequence = 1_500;
function nextId() {
idSequence += 1;
return `019f7130-0000-7000-8000-${String(idSequence).padStart(12, '0')}`;
}
async function createRuntime() {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
const migrationModel = defineSchemaMigrationModel(database);
await runMigrations({
database,
migrationModel,
migrations: [
runSchemaMigration,
runCancellationRequestMigration,
runAttemptDeadlineMigration,
],
logger: { info() {} },
});
databases.push(database);
return {
repository: new LegacySequelizeRunRepository(database),
source: new LegacySequelizePrimaryCancellationSource(database),
};
}
function createRun(overrides = {}) {
return {
id: nextId(),
projectId: 'default',
taskId: 'cancel-task',
taskRevision: 'revision-1',
triggerType: 'manual',
executionOrigin: 'manual',
executionOwner: 'runtime',
status: 'running',
version: 7,
eventSequence: 7,
priority: 0,
createdAtMs: BASE_TIME,
startedAtMs: BASE_TIME + 5,
cancelRequestedAtMs: BASE_TIME + 10,
cancelReason: 'user',
...overrides,
};
}
function createAttempt(runId, overrides = {}) {
return {
id: nextId(),
runId,
attempt: 1,
status: 'running',
executorType: 'local_process',
executorHandle: `durable:${runId}`,
pid: 4321,
callbackSequence: 0,
createdAtMs: BASE_TIME,
startedAtMs: BASE_TIME + 5,
...overrides,
};
}
async function seed(repository, run, attempts = []) {
await repository.transaction(async (transaction) => {
await transaction.insertRun(run);
for (const attempt of attempts) await transaction.insertAttempt(attempt);
});
}
afterEach(async () => {
await Promise.all(databases.splice(0).map((database) => database.close()));
});
test('pages only runtime-owned non-terminal cancellation requests', async () => {
const { repository, source } = await createRuntime();
const first = createRun();
const second = createRun({
status: 'dispatching',
startedAtMs: undefined,
cancelRequestedAtMs: BASE_TIME + 11,
cancelReason: 'shutdown',
});
const terminal = createRun({
status: 'cancelled',
cancelRequestedAtMs: BASE_TIME + 9,
});
const legacy = createRun({
executionOwner: 'legacy',
cancelRequestedAtMs: BASE_TIME + 8,
});
const untouched = createRun({
cancelRequestedAtMs: undefined,
cancelReason: undefined,
});
const firstAttempt = createAttempt(first.id);
const secondAttempt = createAttempt(second.id, {
status: 'starting',
pid: undefined,
});
await seed(repository, first, [firstAttempt]);
await seed(repository, second, [secondAttempt]);
await seed(repository, terminal, [createAttempt(terminal.id)]);
await seed(repository, legacy, [createAttempt(legacy.id)]);
await seed(repository, untouched, [createAttempt(untouched.id)]);
const page1 = await source.listCandidates({ limit: 1 });
assert.equal(page1.truncated, true);
assert.deepEqual(page1.candidates, [
{
runId: first.id,
requestedAtMs: BASE_TIME + 10,
reason: 'user',
attempts: [
{
attemptId: firstAttempt.id,
executorType: 'local_process',
executorHandle: `durable:${first.id}`,
pid: 4321,
},
],
},
]);
const page2 = await source.listCandidates({
limit: 1,
cursor: page1.nextCursor,
});
assert.equal(page2.truncated, false);
assert.equal(page2.candidates[0].runId, second.id);
assert.equal(page2.candidates[0].reason, 'shutdown');
assert.equal(page2.candidates[0].attempts[0].pid, undefined);
await assert.rejects(source.listCandidates({ limit: 65 }), RangeError);
});
test('fails closed when active Attempt corruption exceeds the bounded budget', async () => {
const { repository, source } = await createRuntime();
const run = createRun();
await seed(repository, run, [
createAttempt(run.id, { attempt: 1 }),
createAttempt(run.id, { attempt: 2 }),
createAttempt(run.id, { attempt: 3 }),
]);
const page = await source.listCandidates({ limit: 1 });
assert.equal(page.unsafeAttemptOverflow, true);
assert.deepEqual(page.candidates, []);
assert.equal(page.nextCursor, undefined);
});
@@ -0,0 +1,147 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
PrimaryCancellationSupervisor,
} = require('../../back/runtime/application/primaryCancellationSupervisor');
function summary(overrides = {}) {
return {
scanned: 1,
claimed: 1,
terminationRequested: 1,
alreadyExited: 0,
pending: 0,
ambiguous: 0,
blocked: 0,
deferred: 0,
alreadyResolved: 0,
notEligible: 0,
failed: 0,
truncated: false,
unsafeAttemptOverflow: false,
...overrides,
};
}
test('paginates a bounded cancellation recovery cycle and aggregates results', async () => {
const calls = [];
const pages = [
summary({
scanned: 2,
claimed: 1,
pending: 1,
truncated: true,
nextCursor: { requestedAtMs: 100, runId: 'run-2' },
}),
summary({
scanned: 2,
claimed: 2,
terminationRequested: 0,
alreadyExited: 1,
blocked: 1,
}),
];
const supervisor = new PrimaryCancellationSupervisor({
async dispatchBatch(options) {
calls.push(options);
return pages.shift();
},
});
const result = await supervisor.runCycle({ pageSize: 2, maxPages: 4 });
assert.deepEqual(calls, [
{ limit: 2 },
{ cursor: { requestedAtMs: 100, runId: 'run-2' }, limit: 2 },
]);
assert.deepEqual(result, {
pages: 2,
scanned: 4,
claimed: 3,
terminationRequested: 1,
alreadyExited: 1,
pending: 1,
ambiguous: 0,
blocked: 1,
deferred: 0,
alreadyResolved: 0,
notEligible: 0,
failed: 0,
stopReason: 'complete',
remaining: false,
});
});
test('stops at the page limit and exposes a resume cursor', async () => {
let page = 0;
const supervisor = new PrimaryCancellationSupervisor({
async dispatchBatch() {
page += 1;
return summary({
truncated: true,
nextCursor: { requestedAtMs: 100 + page, runId: `run-${page}` },
});
},
});
const result = await supervisor.runCycle({ pageSize: 1, maxPages: 2 });
assert.equal(result.pages, 2);
assert.equal(result.stopReason, 'page_limit');
assert.equal(result.remaining, true);
assert.deepEqual(result.nextCursor, {
requestedAtMs: 102,
runId: 'run-2',
});
const resumedCalls = [];
const resumed = new PrimaryCancellationSupervisor({
async dispatchBatch(options) {
resumedCalls.push(options);
return summary();
},
});
await resumed.runCycle({ cursor: result.nextCursor, pageSize: 1 });
assert.deepEqual(resumedCalls, [
{
cursor: { requestedAtMs: 102, runId: 'run-2' },
limit: 1,
},
]);
});
test('fails closed when attempt rows overflow or pagination cannot advance', async () => {
const unsafe = new PrimaryCancellationSupervisor({
async dispatchBatch() {
return summary({
scanned: 0,
claimed: 0,
terminationRequested: 0,
truncated: true,
unsafeAttemptOverflow: true,
});
},
});
const unsafeResult = await unsafe.runCycle();
assert.equal(unsafeResult.stopReason, 'unsafe_attempt_overflow');
assert.equal(unsafeResult.remaining, true);
const stalled = new PrimaryCancellationSupervisor({
async dispatchBatch() {
return summary({ truncated: true });
},
});
const stalledResult = await stalled.runCycle();
assert.equal(stalledResult.stopReason, 'cursor_stalled');
assert.equal(stalledResult.remaining, true);
});
test('rejects unbounded cycle settings', async () => {
const supervisor = new PrimaryCancellationSupervisor({
async dispatchBatch() {
throw new Error('must not run');
},
});
await assert.rejects(supervisor.runCycle({ pageSize: 65 }), RangeError);
await assert.rejects(supervisor.runCycle({ maxPages: 65 }), RangeError);
});
@@ -0,0 +1,148 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
PrimaryCompletionReceiptJournalScanner,
} = require('../../back/runtime/application/primaryCompletionReceiptJournalScanner');
const NOW = 1_751_000_000_000;
function candidate(attemptId, overrides = {}) {
return {
attemptId,
runId: `run-${attemptId}`,
state: 'pending',
registeredAtMs: NOW - 1_000,
updatedAtMs: NOW - 1_000,
attemptStatus: 'running',
executorType: 'local_process',
...overrides,
};
}
test('scans pending journal rows and expires only old terminal missing receipts', async () => {
const resolved = [];
const consumed = [];
const scanner = new PrimaryCompletionReceiptJournalScanner(
{
async listCandidates() {
return {
candidates: [
candidate('applied'),
candidate('terminal'),
candidate('missing-active'),
candidate('missing-terminal', {
attemptStatus: 'succeeded',
finishedAtMs: NOW - 61_000,
}),
candidate('quarantined'),
candidate('remote', { executorType: 'remote_worker' }),
candidate('failed'),
],
truncated: false,
};
},
async resolve(attemptId) {
resolved.push(attemptId);
return true;
},
},
{},
{
async consume(attemptId) {
consumed.push(attemptId);
if (attemptId === 'failed') throw new Error('transient database error');
if (attemptId.startsWith('missing')) {
return { status: 'missing', cleaned: false };
}
if (attemptId === 'terminal') {
return { status: 'already_terminal', cleaned: true };
}
if (attemptId === 'quarantined') {
return { status: 'quarantined', cleaned: true };
}
return { status: 'applied', cleaned: true };
},
},
{ terminalMissingRetentionMs: 60_000, clock: { now: () => NOW } },
);
assert.deepEqual(await scanner.scanBatch({ limit: 8 }), {
scanned: 7,
applied: 1,
alreadyTerminal: 1,
quarantined: 1,
purgedQuarantines: 0,
expiredMissing: 1,
missing: 1,
cleanupPending: 0,
skipped: 1,
ambiguous: 0,
failed: 1,
truncated: false,
unsafeAttemptOverflow: false,
});
assert.deepEqual(resolved, ['missing-terminal']);
assert.deepEqual(consumed, [
'applied',
'terminal',
'missing-active',
'missing-terminal',
'quarantined',
'failed',
]);
});
test('purges due quarantine through a known Attempt path and advances cursor', async () => {
const calls = [];
const scanner = new PrimaryCompletionReceiptJournalScanner(
{
async listCandidates(options) {
calls.push(['list', options.cursor]);
return {
candidates: [
candidate('due', {
state: 'quarantined',
quarantineRef: '.quarantine/du/due.json',
purgeAfterMs: NOW,
}),
],
truncated: true,
nextCursor: { updatedAtMs: NOW - 1, attemptId: 'due' },
};
},
async resolve(attemptId) {
calls.push(['resolve', attemptId]);
return true;
},
},
{
async quarantine(attemptId) {
calls.push(['quarantine', attemptId]);
return '.quarantine/du/due.json';
},
async purgeQuarantine(attemptId) {
calls.push(['purge', attemptId]);
return true;
},
},
{ async consume() {} },
{ clock: { now: () => NOW } },
);
const result = await scanner.scanBatch({
cursor: { createdAtMs: NOW - 2, runId: 'before' },
});
assert.equal(result.purgedQuarantines, 1);
assert.deepEqual(result.nextCursor, {
createdAtMs: NOW - 1,
runId: 'due',
});
assert.deepEqual(calls, [
['list', { updatedAtMs: NOW - 2, attemptId: 'before' }],
['quarantine', 'due'],
['purge', 'due'],
['resolve', 'due'],
]);
});
@@ -0,0 +1,262 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
PrimaryCompletionReceiptLifecycle,
} = require('../../back/runtime/application/primaryCompletionReceiptLifecycle');
const {
PrimaryCompletionReceiptScanner,
} = require('../../back/runtime/application/primaryCompletionReceiptScanner');
const {
PrimaryCompletionReceiptSupervisor,
} = require('../../back/runtime/application/primaryCompletionReceiptSupervisor');
function candidate(runId, attemptId, executorType = 'local_process') {
return {
runId,
attempts: attemptId ? [{ attemptId, executorType }] : [],
};
}
function page(overrides = {}) {
return {
candidates: [],
truncated: false,
unsafeAttemptOverflow: false,
...overrides,
};
}
function scan(overrides = {}) {
return {
scanned: 1,
applied: 0,
alreadyTerminal: 0,
quarantined: 0,
purgedQuarantines: 0,
expiredMissing: 0,
missing: 1,
cleanupPending: 0,
skipped: 0,
ambiguous: 0,
failed: 0,
truncated: false,
unsafeAttemptOverflow: false,
...overrides,
};
}
test('scans only database-discovered local Attempts and isolates failures', async () => {
const consumed = [];
const scanner = new PrimaryCompletionReceiptScanner(
{
async listCandidates() {
return page({
candidates: [
candidate('run-1', 'applied'),
candidate('run-2', 'terminal'),
candidate('run-3', 'missing'),
candidate('run-4', 'cleanup-pending'),
candidate('run-5', 'failed'),
candidate('run-6', 'quarantined'),
candidate('run-7', 'remote', 'remote_worker'),
candidate('run-8'),
{
runId: 'run-9',
attempts: [
{ attemptId: 'one', executorType: 'local_process' },
{ attemptId: 'two', executorType: 'local_process' },
],
},
],
});
},
},
{
async consume(attemptId) {
consumed.push(attemptId);
if (attemptId === 'failed') throw new Error('invalid receipt');
if (attemptId === 'missing') {
return { status: 'missing', cleaned: false };
}
if (attemptId === 'terminal') {
return { status: 'already_terminal', cleaned: true };
}
if (attemptId === 'quarantined') {
return {
status: 'quarantined',
cleaned: true,
quarantineRef: '.quarantine/quarantined.json',
};
}
return {
status: 'applied',
cleaned: attemptId !== 'cleanup-pending',
};
},
},
);
assert.deepEqual(await scanner.scanBatch({ limit: 8 }), {
scanned: 9,
applied: 2,
alreadyTerminal: 1,
quarantined: 1,
purgedQuarantines: 0,
expiredMissing: 0,
missing: 1,
cleanupPending: 1,
skipped: 1,
ambiguous: 2,
failed: 1,
truncated: false,
unsafeAttemptOverflow: false,
});
assert.deepEqual(consumed, [
'applied',
'terminal',
'missing',
'cleanup-pending',
'failed',
'quarantined',
]);
});
test('supervisor aggregates bounded pages and exposes unsafe pagination', async () => {
const cursors = [];
const responses = [
scan({
applied: 1,
missing: 0,
truncated: true,
nextCursor: { createdAtMs: 10, runId: 'run-1' },
}),
scan({ alreadyTerminal: 1, cleanupPending: 1 }),
];
const supervisor = new PrimaryCompletionReceiptSupervisor({
async scanBatch(options) {
cursors.push(options.cursor);
return responses.shift();
},
});
assert.deepEqual(await supervisor.run({ pageSize: 8, maxPages: 4 }), {
pages: 2,
scanned: 2,
applied: 1,
alreadyTerminal: 1,
quarantined: 0,
purgedQuarantines: 0,
expiredMissing: 0,
missing: 1,
cleanupPending: 1,
skipped: 0,
ambiguous: 0,
failed: 0,
stopReason: 'complete',
remaining: false,
});
assert.deepEqual(cursors, [undefined, { createdAtMs: 10, runId: 'run-1' }]);
const overflow = new PrimaryCompletionReceiptSupervisor({
async scanBatch() {
return scan({ unsafeAttemptOverflow: true, truncated: true });
},
});
assert.equal((await overflow.run()).stopReason, 'unsafe_attempt_overflow');
await assert.rejects(supervisor.run({ pageSize: 0 }), RangeError);
await assert.rejects(supervisor.run({ maxPages: 65 }), RangeError);
});
test('lifecycle is explicit, unrefed, and never overlaps a slow scan', async () => {
const scheduled = [];
const scheduler = {
setTimeout(callback, delayMs) {
const timer = {
callback,
delayMs,
unrefCalled: false,
unref() {
this.unrefCalled = true;
},
};
scheduled.push(timer);
return timer;
},
clearTimeout(timer) {
timer.cleared = true;
},
};
let release;
let runs = 0;
const inFlight = new Promise((resolve) => {
release = resolve;
});
const lifecycle = new PrimaryCompletionReceiptLifecycle(
{
async run() {
runs += 1;
await inFlight;
return {};
},
},
{ intervalMs: 1_000, scheduler },
);
assert.equal(lifecycle.start(), true);
assert.equal(lifecycle.start(), false);
assert.equal(scheduled.length, 1);
assert.equal(scheduled[0].unrefCalled, true);
scheduled[0].callback();
scheduled[0].callback();
assert.equal(runs, 1);
release();
await new Promise((resolve) => setImmediate(resolve));
assert.equal(scheduled.length, 2);
assert.equal(await lifecycle.stop(), 'drained');
assert.equal(scheduled[1].cleared, true);
});
test('lifecycle resumes after a page limit and resets after reaching the tail', async () => {
const scheduled = [];
const scheduler = {
setTimeout(callback) {
const timer = { callback, unref() {} };
scheduled.push(timer);
return timer;
},
clearTimeout() {},
};
const cursors = [];
let runs = 0;
const lifecycle = new PrimaryCompletionReceiptLifecycle(
{
async run(options) {
runs += 1;
cursors.push(options.cursor);
if (runs === 1) {
return {
remaining: true,
nextCursor: { createdAtMs: 10, runId: 'run-10' },
};
}
return { remaining: false };
},
},
{ intervalMs: 1_000, scheduler },
);
lifecycle.start();
scheduled.shift().callback();
await new Promise((resolve) => setImmediate(resolve));
scheduled.shift().callback();
await new Promise((resolve) => setImmediate(resolve));
scheduled.shift().callback();
await new Promise((resolve) => setImmediate(resolve));
await lifecycle.stop();
assert.deepEqual(cursors, [
undefined,
{ createdAtMs: 10, runId: 'run-10' },
undefined,
]);
});
+485
View File
@@ -0,0 +1,485 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { afterEach, test } = require('node:test');
const { DataTypes, QueryTypes, Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const { runSchemaMigration } = require('../../back/migrations/0002-run-schema');
const {
runCancellationRequestMigration,
} = require('../../back/migrations/0004-run-cancellation-request');
const {
runAttemptDeadlineMigration,
} = require('../../back/migrations/0006-run-attempt-deadline');
const {
runningInstanceRunReferenceMigration,
} = require('../../back/migrations/0003-running-instance-run-reference');
const { runMigrations } = require('../../back/migrations/runner');
const {
PrimaryCronProjection,
} = require('../../back/runtime/adapters/legacy-sequelize/primaryCronProjection');
const {
LegacySequelizeProjectedRunRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/projectedRunRepository');
const {
LegacySequelizeRunRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/runRepository');
const {
PrimaryRunCreator,
} = require('../../back/runtime/application/primaryRunCreator');
const {
PrimaryRunOrchestrator,
} = require('../../back/runtime/application/primaryRunOrchestrator');
const {
RunCommandService,
} = require('../../back/runtime/application/runCommandService');
const {
createLegacyLogOutputRef,
} = require('../../back/runtime/compatibility/legacyLogOutputRef');
const databases = [];
const BASE_TIME = 1_750_000_000_000;
let idSequence = 900;
function nextId() {
idSequence += 1;
return '019f7110-0000-7000-8000-' + String(idSequence).padStart(12, '0');
}
function deferred() {
let resolve;
let reject;
const promise = new Promise((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, resolve, reject };
}
async function createLegacyTables(database) {
const queryInterface = database.getQueryInterface();
await queryInterface.createTable('Crontabs', {
id: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true,
},
name: { type: DataTypes.STRING, allowNull: true },
command: { type: DataTypes.STRING, allowNull: false },
status: { type: DataTypes.INTEGER, allowNull: true },
pid: { type: DataTypes.INTEGER, allowNull: true },
log_path: { type: DataTypes.STRING, allowNull: true },
last_running_time: { type: DataTypes.INTEGER, allowNull: true },
last_execution_time: { type: DataTypes.INTEGER, allowNull: true },
});
await queryInterface.createTable('RunningInstances', {
id: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true,
},
cron_id: { type: DataTypes.INTEGER, allowNull: false },
pid: { type: DataTypes.INTEGER, allowNull: true },
log_path: { type: DataTypes.STRING, allowNull: true },
started_at: { type: DataTypes.INTEGER, allowNull: false },
finished_at: { type: DataTypes.INTEGER, allowNull: true },
status: { type: DataTypes.INTEGER, allowNull: false },
exit_code: { type: DataTypes.INTEGER, allowNull: true },
});
}
async function createDatabase() {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
await createLegacyTables(database);
const migrationModel = defineSchemaMigrationModel(database);
await runMigrations({
database,
migrationModel,
migrations: [
runSchemaMigration,
runningInstanceRunReferenceMigration,
runCancellationRequestMigration,
runAttemptDeadlineMigration,
],
logger: { info() {} },
});
databases.push(database);
return database;
}
async function seedCron(database, id) {
await database.getQueryInterface().bulkInsert('Crontabs', [
{
id,
name: 'cron-' + id,
command: 'echo projected',
status: 1,
pid: null,
log_path: null,
last_running_time: null,
last_execution_time: null,
},
]);
}
async function readCron(database, id) {
const rows = await database.query(
'SELECT id, status, pid, log_path, last_running_time, last_execution_time FROM Crontabs WHERE id = :id',
{
replacements: { id },
type: QueryTypes.SELECT,
},
);
return rows[0] || null;
}
async function readInstances(database, cronId) {
return database.query(
'SELECT id, cron_id, run_id, attempt_id, pid, log_path, started_at, finished_at, status, exit_code FROM RunningInstances WHERE cron_id = :cronId ORDER BY id',
{
replacements: { cronId },
type: QueryTypes.SELECT,
},
);
}
function projectedRepository(database, extraParticipants = []) {
return new LegacySequelizeProjectedRunRepository(database, [
new PrimaryCronProjection(database),
...extraParticipants,
]);
}
class FakeExecutor {
type = 'local_process';
completion = deferred();
constructor(pid, startedAtMs) {
this.pid = pid;
this.startedAtMs = startedAtMs;
}
capabilities() {
return {
timeout: true,
processGroupTermination: true,
workingDirectory: true,
isolatedEnvironment: true,
memoryLimit: 'none',
cpuLimit: 'none',
filesystemIsolation: 'none',
networkIsolation: 'none',
};
}
async start(spec) {
return {
id: 'handle-' + this.pid,
executorType: this.type,
runId: spec.runId,
attemptId: spec.attemptId,
startedAtMs: this.startedAtMs,
pid: this.pid,
completion: this.completion.promise,
};
}
async stop() {
return {
status: 'termination_requested',
termSignalSent: true,
killSignalSent: false,
};
}
async inspect() {
return { status: 'running' };
}
}
function startCommand(cronId, acceptedAtMs, logPath) {
return {
definition: {
projectId: 'default',
taskId: 'legacy-cron:' + cronId,
taskRevision: 'revision-' + cronId,
taskName: 'projected run ' + cronId,
legacyCronId: cronId,
triggerType: 'manual',
executionOrigin: 'manual',
outputRef: createLegacyLogOutputRef(logPath),
acceptedAtMs,
actor: { type: 'user', id: 'user:1' },
},
createSpec(reference) {
return {
runId: reference.run.id,
attemptId: reference.attempt.id,
projectId: reference.run.projectId,
taskId: reference.run.taskId,
taskRevision: reference.run.taskRevision,
command: { kind: 'argv', file: '/bin/true', args: [] },
environmentPolicy: 'isolated',
terminationGraceMs: 100,
};
},
context: {
environment: {},
output: { async write() {} },
},
};
}
function result(outcome, startedAtMs, finishedAtMs, exitCode) {
return {
outcome,
startedAtMs,
finishedAtMs,
...(exitCode === undefined ? {} : { exitCode }),
};
}
afterEach(async () => {
await Promise.all(databases.splice(0).map((database) => database.close()));
});
test('projects a Primary Run and RunningInstance through success', async () => {
const database = await createDatabase();
await seedCron(database, 7);
const repository = projectedRepository(database);
const executor = new FakeExecutor(4321, BASE_TIME + 2_000);
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 3_000 },
createId: nextId,
});
const active = await orchestrator.start(
startCommand(7, BASE_TIME, 'task-7/primary.log'),
);
const runningCron = await readCron(database, 7);
assert.equal(runningCron.status, 0);
assert.equal(runningCron.pid, 4321);
assert.equal(runningCron.log_path, 'task-7/primary.log');
assert.equal(runningCron.last_execution_time, 1_750_000_003);
let instances = await readInstances(database, 7);
assert.equal(instances.length, 1);
assert.equal(instances[0].run_id, active.run.id);
assert.equal(instances[0].attempt_id, active.attempt.id);
assert.equal(instances[0].status, 0);
assert.equal(instances[0].pid, 4321);
assert.equal(instances[0].log_path, 'task-7/primary.log');
executor.completion.resolve(
result('succeeded', BASE_TIME + 2_000, BASE_TIME + 7_000, 0),
);
await active.completion;
const finishedCron = await readCron(database, 7);
assert.equal(finishedCron.status, 1);
assert.equal(finishedCron.pid, null);
assert.equal(finishedCron.log_path, 'task-7/primary.log');
assert.equal(finishedCron.last_running_time, 4);
instances = await readInstances(database, 7);
assert.equal(instances.length, 1);
assert.equal(instances[0].status, 1);
assert.equal(instances[0].finished_at, 1_750_000_007);
assert.equal(instances[0].exit_code, 0);
});
test('maps failed and cancelled attempts into legacy instance states', async () => {
const cases = [
{ cronId: 8, outcome: 'failed', expectedStatus: 3, exitCode: 17 },
{ cronId: 9, outcome: 'cancelled', expectedStatus: 2, exitCode: 143 },
];
for (const item of cases) {
const database = await createDatabase();
await seedCron(database, item.cronId);
const repository = projectedRepository(database);
const executor = new FakeExecutor(4400 + item.cronId, BASE_TIME + 1_000);
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 2_000 },
createId: nextId,
});
const active = await orchestrator.start(
startCommand(
item.cronId,
BASE_TIME,
'task-' + item.cronId + '/primary.log',
),
);
executor.completion.resolve(
result(item.outcome, BASE_TIME + 1_000, BASE_TIME + 4_000, item.exitCode),
);
await active.completion;
const cron = await readCron(database, item.cronId);
const instances = await readInstances(database, item.cronId);
assert.equal(cron.status, 1);
assert.equal(cron.pid, null);
assert.equal(instances.length, 1);
assert.equal(instances[0].status, item.expectedStatus);
assert.equal(instances[0].exit_code, item.exitCode);
}
});
test('keeps Crontab running while another Primary instance remains active', async () => {
const database = await createDatabase();
await seedCron(database, 10);
const repository = projectedRepository(database);
const firstExecutor = new FakeExecutor(5101, BASE_TIME + 1_000);
const firstOrchestrator = new PrimaryRunOrchestrator(
repository,
firstExecutor,
{
clock: { now: () => BASE_TIME + 2_000 },
createId: nextId,
},
);
const first = await firstOrchestrator.start(
startCommand(10, BASE_TIME, 'task-10/first.log'),
);
const secondExecutor = new FakeExecutor(5102, BASE_TIME + 101_000);
const secondOrchestrator = new PrimaryRunOrchestrator(
repository,
secondExecutor,
{
clock: { now: () => BASE_TIME + 102_000 },
createId: nextId,
},
);
const second = await secondOrchestrator.start(
startCommand(10, BASE_TIME + 100_000, 'task-10/second.log'),
);
assert.equal((await readCron(database, 10)).pid, 5102);
secondExecutor.completion.resolve(
result('succeeded', BASE_TIME + 101_000, BASE_TIME + 104_000, 0),
);
await second.completion;
const stillRunning = await readCron(database, 10);
assert.equal(stillRunning.status, 0);
assert.equal(stillRunning.pid, 5101);
assert.equal(stillRunning.log_path, 'task-10/first.log');
firstExecutor.completion.resolve(
result('succeeded', BASE_TIME + 1_000, BASE_TIME + 106_000, 0),
);
await first.completion;
assert.equal((await readCron(database, 10)).status, 1);
assert.equal((await readInstances(database, 10)).length, 2);
});
test('rolls back Run, event and instance writes when projection fails', async () => {
const database = await createDatabase();
await seedCron(database, 11);
const failure = {
enabled: false,
async apply() {
if (this.enabled) throw new Error('injected projection failure');
},
};
const repository = projectedRepository(database, [failure]);
const creator = new PrimaryRunCreator(repository, nextId);
const reference = await creator.create(
{
projectId: 'default',
taskId: 'legacy-cron:11',
taskRevision: 'revision-11',
legacyCronId: 11,
triggerType: 'manual',
executionOrigin: 'manual',
outputRef: createLegacyLogOutputRef('task-11/rollback.log'),
acceptedAtMs: BASE_TIME,
actor: { type: 'user', id: 'user:1' },
},
'local_process',
);
const commands = new RunCommandService(repository, nextId);
const dispatching = await commands.transitionRun({
runId: reference.run.id,
to: 'dispatching',
expectedVersion: reference.run.version,
atMs: BASE_TIME + 1_000,
actor: { type: 'system' },
});
failure.enabled = true;
await assert.rejects(
commands.transitionRunAttempt({
runId: reference.run.id,
attemptId: reference.attempt.id,
to: 'starting',
expectedRunVersion: dispatching.run.version,
atMs: BASE_TIME + 2_000,
actor: { type: 'executor', id: 'local_process' },
}),
/injected projection failure/,
);
const run = await repository.findRunById(reference.run.id);
const attempt = await repository.findAttemptById(reference.attempt.id);
assert.equal(run.status, 'dispatching');
assert.equal(run.version, dispatching.run.version);
assert.equal(attempt.status, 'claimed');
assert.equal((await repository.listEvents(run.id)).length, 3);
assert.equal((await readInstances(database, 11)).length, 0);
assert.equal((await readCron(database, 11)).status, 3);
});
test('legacy Shadow repository never writes the Primary projection', async () => {
const database = await createDatabase();
await seedCron(database, 12);
const repository = new LegacySequelizeRunRepository(database);
const run = {
id: nextId(),
projectId: 'default',
taskId: 'legacy-cron:12',
taskRevision: 'revision-12',
legacyCronId: 12,
triggerType: 'manual',
executionOrigin: 'manual',
executionOwner: 'legacy',
status: 'running',
version: 0,
eventSequence: 0,
priority: 0,
outputRef: createLegacyLogOutputRef('task-12/shadow.log'),
createdAtMs: BASE_TIME,
startedAtMs: BASE_TIME + 1_000,
};
const attempt = {
id: nextId(),
runId: run.id,
attempt: 1,
status: 'running',
executorType: 'legacy_local',
pid: 6200,
callbackSequence: 0,
createdAtMs: BASE_TIME,
startedAtMs: BASE_TIME + 1_000,
};
await repository.transaction(async (transaction) => {
await transaction.insertRun(run);
await transaction.insertAttempt(attempt);
});
const cron = await readCron(database, 12);
assert.equal(cron.status, 1);
assert.equal(cron.pid, null);
assert.equal(
await readInstances(database, 12).then((rows) => rows.length),
0,
);
});
@@ -0,0 +1,412 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const { Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const { runSchemaMigration } = require('../../back/migrations/0002-run-schema');
const {
runCancellationRequestMigration,
} = require('../../back/migrations/0004-run-cancellation-request');
const {
runAttemptDeadlineMigration,
} = require('../../back/migrations/0006-run-attempt-deadline');
const { runMigrations } = require('../../back/migrations/runner');
const {
CompletionReceiptFileStore,
} = require('../../back/runtime/adapters/fs/completionReceiptFileStore');
const {
LegacySequelizeRunRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/runRepository');
const {
PrimaryCompletionReceiptConsumer,
} = require('../../back/runtime/application/primaryCompletionReceiptConsumer');
const {
PrimaryRunCompletionService,
PrimaryCompletionUnauthorizedError,
hashPrimaryCompletionToken,
} = require('../../back/runtime/application/primaryRunCompletionService');
const {
PrimaryRunCreator,
} = require('../../back/runtime/application/primaryRunCreator');
const {
RunCommandService,
} = require('../../back/runtime/application/runCommandService');
const NOW = 1_750_000_000_000;
const TOKEN = 'completion_token_abcdefghijklmnopqrstuvwxyz0123456789';
let idSequence = 900;
function nextId() {
idSequence += 1;
return `019f70f0-0000-7000-8000-${String(idSequence).padStart(12, '0')}`;
}
async function setup(t) {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
t.after(() => database.close());
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations: [
runSchemaMigration,
runCancellationRequestMigration,
runAttemptDeadlineMigration,
],
logger: { info() {} },
});
return new LegacySequelizeRunRepository(database);
}
async function runningReference(repository) {
const creator = new PrimaryRunCreator(repository, nextId);
const commands = new RunCommandService(repository, nextId);
const created = await creator.create(
{
projectId: 'default',
taskId: 'legacy-cron:9',
taskRevision: 'revision-9',
triggerType: 'manual',
executionOrigin: 'manual',
acceptedAtMs: NOW,
actor: { type: 'user', id: 'user:1' },
},
'local_process',
);
const dispatching = await commands.transitionRun({
runId: created.run.id,
to: 'dispatching',
expectedVersion: created.run.version,
atMs: NOW + 1,
actor: { type: 'scheduler' },
});
const starting = await commands.transitionRunAttempt({
runId: created.run.id,
attemptId: created.attempt.id,
to: 'starting',
expectedRunVersion: dispatching.run.version,
atMs: NOW + 2,
callbackTokenHash: hashPrimaryCompletionToken(TOKEN),
actor: { type: 'worker', id: 'local_process' },
});
const attempt = await commands.transitionRunAttempt({
runId: created.run.id,
attemptId: created.attempt.id,
to: 'running',
expectedRunVersion: starting.run.version,
atMs: NOW + 3,
executorHandle: 'ql3lp1.test-handle',
actor: { type: 'executor', id: 'local_process' },
});
const run = await commands.transitionRun({
runId: created.run.id,
to: 'running',
expectedVersion: attempt.run.version,
atMs: NOW + 3,
actor: { type: 'executor', id: 'local_process' },
});
return { run: run.run, attempt: attempt.attempt, commands };
}
function successfulResult() {
return {
outcome: 'succeeded',
startedAtMs: NOW + 3,
finishedAtMs: NOW + 10,
exitCode: 0,
};
}
function receipt(reference) {
return {
schemaVersion: 1,
runId: reference.run.id,
attemptId: reference.attempt.id,
callbackSequence: 1,
token: TOKEN,
startedAtMs: NOW + 3,
finishedAtMs: NOW + 10,
exitCode: 0,
};
}
class FailAfterWorkRepository {
fail = true;
constructor(delegate) {
this.delegate = delegate;
}
findRunById(runId) {
return this.delegate.findRunById(runId);
}
findAttemptById(attemptId) {
return this.delegate.findAttemptById(attemptId);
}
listEvents(runId, options) {
return this.delegate.listEvents(runId, options);
}
listCancellationRequested(options) {
return this.delegate.listCancellationRequested(options);
}
transaction(work) {
return this.delegate.transaction(async (transaction) => {
const result = await work(transaction);
if (this.fail) {
this.fail = false;
throw new Error('simulated crash before commit');
}
return result;
});
}
}
class FailFirstRemoveStore {
fail = true;
constructor(delegate) {
this.delegate = delegate;
}
publish(value) {
return this.delegate.publish(value);
}
read(attemptId) {
return this.delegate.read(attemptId);
}
async remove(attemptId) {
if (this.fail) {
this.fail = false;
throw new Error('simulated crash before receipt cleanup');
}
return this.delegate.remove(attemptId);
}
}
async function receiptStore(t) {
const root = await fs.mkdtemp(
path.join(os.tmpdir(), 'ql3-completion-service-'),
);
t.after(() => fs.rm(root, { recursive: true, force: true }));
return new CompletionReceiptFileStore(root);
}
test('atomically applies one completion and treats an identical replay as terminal', async (t) => {
const repository = await setup(t);
const reference = await runningReference(repository);
const service = new PrimaryRunCompletionService(repository, nextId);
const command = {
runId: reference.run.id,
attemptId: reference.attempt.id,
callbackSequence: 1,
result: successfulResult(),
source: { kind: 'receipt', token: TOKEN },
};
const applied = await service.complete(command);
assert.equal(applied.status, 'applied');
assert.equal(applied.run.status, 'succeeded');
assert.equal(applied.attempt.status, 'succeeded');
assert.equal(applied.attempt.callbackSequence, 1);
const eventsAfterApply = await repository.listEvents(reference.run.id);
assert.doesNotMatch(JSON.stringify(eventsAfterApply), new RegExp(TOKEN));
assert.deepEqual(
eventsAfterApply.slice(-2).map((event) => event.type),
['attempt.succeeded', 'run.succeeded'],
);
const replay = await service.complete(command);
assert.equal(replay.status, 'already_terminal');
assert.equal(
(await repository.listEvents(reference.run.id)).length,
eventsAfterApply.length,
);
});
test('rejects an invalid receipt token without changing durable state', async (t) => {
const repository = await setup(t);
const reference = await runningReference(repository);
const service = new PrimaryRunCompletionService(repository, nextId);
const beforeEvents = await repository.listEvents(reference.run.id);
await assert.rejects(
service.complete({
runId: reference.run.id,
attemptId: reference.attempt.id,
callbackSequence: 1,
result: successfulResult(),
source: {
kind: 'receipt',
token: 'wrong_token_abcdefghijklmnopqrstuvwxyz0123456789',
},
}),
PrimaryCompletionUnauthorizedError,
);
assert.equal(
(await repository.findRunById(reference.run.id)).status,
'running',
);
assert.equal(
(await repository.findAttemptById(reference.attempt.id)).callbackSequence,
0,
);
assert.equal(
(await repository.listEvents(reference.run.id)).length,
beforeEvents.length,
);
});
test('quarantines an unauthorized receipt instead of retrying it forever', async (t) => {
const repository = await setup(t);
const reference = await runningReference(repository);
const store = await receiptStore(t);
await store.publish({
...receipt(reference),
token: 'wrong_token_abcdefghijklmnopqrstuvwxyz0123456789',
});
const quarantines = [];
const consumer = new PrimaryCompletionReceiptConsumer(
store,
new PrimaryRunCompletionService(repository, nextId),
{
journal: {
async markQuarantined(command) {
quarantines.push(command);
},
async resolve() {
return true;
},
},
quarantineRetentionMs: 5_000,
clock: { now: () => NOW + 20 },
},
);
const result = await consumer.consume(reference.attempt.id);
assert.equal(result.status, 'quarantined');
assert.equal(result.cleaned, true);
assert.match(result.quarantineRef, /^\.quarantine\//);
assert.deepEqual(quarantines, [
{
attemptId: reference.attempt.id,
quarantineRef: store.quarantineReference(reference.attempt.id),
updatedAtMs: NOW + 20,
purgeAfterMs: NOW + 5_020,
},
]);
assert.equal(await store.read(reference.attempt.id), undefined);
assert.equal(
(await repository.findRunById(reference.run.id)).status,
'running',
);
});
test('maps a persisted timeout request before a late successful receipt', async (t) => {
const repository = await setup(t);
const reference = await runningReference(repository);
await reference.commands.requestCancellation({
runId: reference.run.id,
attemptId: reference.attempt.id,
atMs: NOW + 5,
reason: 'timeout',
actor: { type: 'system', id: 'runtime:timeout' },
});
const completed = await new PrimaryRunCompletionService(
repository,
nextId,
).complete({
runId: reference.run.id,
attemptId: reference.attempt.id,
callbackSequence: 1,
result: successfulResult(),
source: { kind: 'receipt', token: TOKEN },
});
assert.equal(completed.run.status, 'timed_out');
assert.equal(completed.attempt.status, 'timed_out');
});
test('keeps a receipt when the terminal transaction crashes before commit', async (t) => {
const repository = await setup(t);
const reference = await runningReference(repository);
const store = await receiptStore(t);
await store.publish(receipt(reference));
const failing = new PrimaryCompletionReceiptConsumer(
store,
new PrimaryRunCompletionService(
new FailAfterWorkRepository(repository),
nextId,
),
);
await assert.rejects(
failing.consume(reference.attempt.id),
/simulated crash before commit/,
);
assert.ok(await store.read(reference.attempt.id));
assert.equal(
(await repository.findRunById(reference.run.id)).status,
'running',
);
const recovered = await new PrimaryCompletionReceiptConsumer(
store,
new PrimaryRunCompletionService(repository, nextId),
).consume(reference.attempt.id);
assert.equal(recovered.status, 'applied');
assert.equal(recovered.cleaned, true);
assert.equal(await store.read(reference.attempt.id), undefined);
assert.equal(
(await repository.listEvents(reference.run.id)).filter(
(event) => event.type === 'run.succeeded',
).length,
1,
);
});
test('replays terminal state and cleans a receipt after cleanup crashes', async (t) => {
const repository = await setup(t);
const reference = await runningReference(repository);
const fileStore = await receiptStore(t);
const store = new FailFirstRemoveStore(fileStore);
await store.publish(receipt(reference));
const consumer = new PrimaryCompletionReceiptConsumer(
store,
new PrimaryRunCompletionService(repository, nextId),
);
await assert.rejects(
consumer.consume(reference.attempt.id),
/simulated crash before receipt cleanup/,
);
assert.equal(
(await repository.findRunById(reference.run.id)).status,
'succeeded',
);
assert.ok(await store.read(reference.attempt.id));
const eventCount = (await repository.listEvents(reference.run.id)).length;
const replay = await consumer.consume(reference.attempt.id);
assert.equal(replay.status, 'already_terminal');
assert.equal(replay.cleaned, true);
assert.equal(await store.read(reference.attempt.id), undefined);
assert.equal(
(await repository.listEvents(reference.run.id)).length,
eventCount,
);
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,282 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { afterEach, test } = require('node:test');
const { Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const { runSchemaMigration } = require('../../back/migrations/0002-run-schema');
const {
runCancellationRequestMigration,
} = require('../../back/migrations/0004-run-cancellation-request');
const {
runAttemptDeadlineMigration,
} = require('../../back/migrations/0006-run-attempt-deadline');
const { runMigrations } = require('../../back/migrations/runner');
const {
CompletionReceiptFileStore,
} = require('../../back/runtime/adapters/fs/completionReceiptFileStore');
const {
LegacySequelizePrimaryRunRecoverySource,
} = require('../../back/runtime/adapters/legacy-sequelize/primaryRunRecoverySource');
const {
LegacySequelizeRunRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/runRepository');
const {
PrimaryCompletionReceiptConsumer,
} = require('../../back/runtime/application/primaryCompletionReceiptConsumer');
const {
hashPrimaryCompletionToken,
PrimaryRunCompletionService,
} = require('../../back/runtime/application/primaryRunCompletionService');
const {
PrimaryRunStartupReconciler,
} = require('../../back/runtime/application/primaryRunStartupReconciler');
const BASE_TIME = 1_750_300_000_000;
const TOKEN = 'c'.repeat(43);
const databases = [];
const roots = [];
let idSequence = 1_800;
function nextId() {
idSequence += 1;
return `019f7400-0000-7000-8000-${String(idSequence).padStart(12, '0')}`;
}
async function createRuntime() {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations: [
runSchemaMigration,
runCancellationRequestMigration,
runAttemptDeadlineMigration,
],
logger: { info() {} },
});
databases.push(database);
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-startup-receipt-'));
roots.push(root);
const repository = new LegacySequelizeRunRepository(database);
const store = new CompletionReceiptFileStore(root);
const consumer = new PrimaryCompletionReceiptConsumer(
store,
new PrimaryRunCompletionService(repository, nextId),
);
return { database, repository, store, consumer };
}
function aggregate() {
const run = {
id: nextId(),
projectId: 'default',
taskId: 'receipt-recovery',
taskRevision: 'revision-1',
triggerType: 'manual',
executionOrigin: 'manual',
executionOwner: 'runtime',
status: 'running',
version: 0,
eventSequence: 0,
priority: 0,
createdAtMs: BASE_TIME,
startedAtMs: BASE_TIME + 1,
};
const attempt = {
id: nextId(),
runId: run.id,
attempt: 1,
status: 'running',
executorType: 'local_process',
executorHandle: `durable:${run.id}`,
pid: 4321,
callbackTokenHash: hashPrimaryCompletionToken(TOKEN),
callbackSequence: 0,
createdAtMs: BASE_TIME,
startedAtMs: BASE_TIME + 1,
};
return { run, attempt };
}
function receipt(run, attempt) {
return {
schemaVersion: 1,
runId: run.id,
attemptId: attempt.id,
callbackSequence: 1,
token: TOKEN,
startedAtMs: BASE_TIME + 1,
finishedAtMs: BASE_TIME + 20,
exitCode: 0,
};
}
async function seed(repository, run, attempt) {
await repository.transaction(async (transaction) => {
await transaction.insertRun(run);
await transaction.insertAttempt(attempt);
});
}
afterEach(async () => {
await Promise.all(databases.splice(0).map((database) => database.close()));
await Promise.all(
roots
.splice(0)
.map((root) => fs.rm(root, { recursive: true, force: true })),
);
});
test('startup consumes a receipt before consulting durable identity', async () => {
const { database, repository, store, consumer } = await createRuntime();
const { run, attempt } = aggregate();
await seed(repository, run, attempt);
await store.publish(receipt(run, attempt));
let inspections = 0;
const registrations = [];
const reconciler = new PrimaryRunStartupReconciler(
repository,
new LegacySequelizePrimaryRunRecoverySource(database),
[
{
executorType: 'local_process',
async inspect() {
inspections += 1;
return { status: 'exited', identityPid: 4321 };
},
},
],
{
completionReceipts: consumer,
completionReceiptJournal: {
async register(command) {
registrations.push(command);
},
},
createEventId: nextId,
},
);
const summary = await reconciler.reconcileBatch();
assert.equal(summary.completedFromReceipt, 1);
assert.equal(summary.markedLost, 0);
assert.equal(inspections, 0);
assert.deepEqual(registrations, [
{
runId: run.id,
attemptId: attempt.id,
registeredAtMs: attempt.createdAtMs,
},
]);
assert.equal((await repository.findRunById(run.id)).status, 'succeeded');
assert.equal(
(await repository.findAttemptById(attempt.id)).status,
'succeeded',
);
assert.equal(await store.read(attempt.id), undefined);
});
test('startup rechecks a receipt after identity reports process exit', async () => {
const { database, repository, store, consumer } = await createRuntime();
const { run, attempt } = aggregate();
await seed(repository, run, attempt);
let inspections = 0;
const reconciler = new PrimaryRunStartupReconciler(
repository,
new LegacySequelizePrimaryRunRecoverySource(database),
[
{
executorType: 'local_process',
async inspect() {
inspections += 1;
await store.publish(receipt(run, attempt));
return { status: 'exited', identityPid: 4321 };
},
},
],
{ completionReceipts: consumer, createEventId: nextId },
);
const summary = await reconciler.reconcileBatch();
assert.equal(summary.completedFromReceipt, 1);
assert.equal(summary.markedLost, 0);
assert.equal(inspections, 1);
assert.equal((await repository.findRunById(run.id)).status, 'succeeded');
assert.equal(await store.read(attempt.id), undefined);
});
test('startup waits once for a late receipt after observing process exit', async () => {
const { database, repository, store, consumer } = await createRuntime();
const { run, attempt } = aggregate();
await seed(repository, run, attempt);
const waits = [];
const reconciler = new PrimaryRunStartupReconciler(
repository,
new LegacySequelizePrimaryRunRecoverySource(database),
[
{
executorType: 'local_process',
async inspect() {
return { status: 'exited', identityPid: 4321 };
},
},
],
{
completionReceipts: consumer,
createEventId: nextId,
receiptPublishGraceMs: 50,
async wait(delayMs) {
waits.push(delayMs);
await store.publish(receipt(run, attempt));
},
},
);
const summary = await reconciler.reconcileBatch();
assert.deepEqual(waits, [50]);
assert.equal(summary.publishGraceWaits, 1);
assert.equal(summary.completedFromReceipt, 1);
assert.equal(summary.markedLost, 0);
assert.equal((await repository.findRunById(run.id)).status, 'succeeded');
});
test('startup quarantines an unauthorized receipt and still verifies the process', async () => {
const { database, repository, store, consumer } = await createRuntime();
const { run, attempt } = aggregate();
await seed(repository, run, attempt);
await store.publish({
...receipt(run, attempt),
token: 'd'.repeat(43),
});
const reconciler = new PrimaryRunStartupReconciler(
repository,
new LegacySequelizePrimaryRunRecoverySource(database),
[
{
executorType: 'local_process',
async inspect() {
return { status: 'running', identityPid: 4321 };
},
},
],
{ completionReceipts: consumer, createEventId: nextId },
);
const summary = await reconciler.reconcileBatch();
assert.equal(summary.quarantinedReceipts, 1);
assert.equal(summary.completedFromReceipt, 0);
assert.equal(summary.verifiedRunning, 1);
assert.equal(summary.markedLost, 0);
assert.equal((await repository.findRunById(run.id)).status, 'running');
assert.equal(await store.read(attempt.id), undefined);
});
@@ -0,0 +1,376 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { afterEach, test } = require('node:test');
const { Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const { runSchemaMigration } = require('../../back/migrations/0002-run-schema');
const {
runCancellationRequestMigration,
} = require('../../back/migrations/0004-run-cancellation-request');
const {
runAttemptDeadlineMigration,
} = require('../../back/migrations/0006-run-attempt-deadline');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacySequelizeRunRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/runRepository');
const {
LegacySequelizePrimaryRunRecoverySource,
} = require('../../back/runtime/adapters/legacy-sequelize/primaryRunRecoverySource');
const {
PrimaryRunStartupReconciler,
} = require('../../back/runtime/application/primaryRunStartupReconciler');
const databases = [];
const BASE_TIME = 1_750_100_000_000;
let idSequence = 1200;
function nextId() {
idSequence += 1;
return `019f7100-0000-7000-8000-${String(idSequence).padStart(12, '0')}`;
}
async function createRuntime() {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
const migrationModel = defineSchemaMigrationModel(database);
await runMigrations({
database,
migrationModel,
migrations: [
runSchemaMigration,
runCancellationRequestMigration,
runAttemptDeadlineMigration,
],
logger: { info() {} },
});
databases.push(database);
return {
database,
repository: new LegacySequelizeRunRepository(database),
source: new LegacySequelizePrimaryRunRecoverySource(database),
};
}
function createRun(overrides = {}) {
return {
id: nextId(),
projectId: 'default',
taskId: 'recovery-task',
taskRevision: 'revision-1',
triggerType: 'manual',
executionOrigin: 'manual',
executionOwner: 'runtime',
status: 'running',
version: 0,
eventSequence: 0,
priority: 0,
createdAtMs: BASE_TIME,
startedAtMs: BASE_TIME + 5,
...overrides,
};
}
function createAttempt(runId, overrides = {}) {
return {
id: nextId(),
runId,
attempt: 1,
status: 'running',
executorType: 'local_process',
executorHandle: `durable:${runId}`,
pid: 4321,
callbackSequence: 0,
createdAtMs: BASE_TIME,
startedAtMs: BASE_TIME + 5,
...overrides,
};
}
async function seed(repository, run, attempts = []) {
await repository.transaction(async (transaction) => {
await transaction.insertRun(run);
for (const attempt of attempts) await transaction.insertAttempt(attempt);
});
}
function inspector(inspect) {
return {
executorType: 'local_process',
inspect,
};
}
afterEach(async () => {
await Promise.all(databases.splice(0).map((database) => database.close()));
});
test('scans only active runtime-owned Runs with a bounded stable cursor', async () => {
const { repository, source } = await createRuntime();
const first = createRun({ createdAtMs: BASE_TIME });
const second = createRun({
status: 'dispatching',
createdAtMs: BASE_TIME + 1,
startedAtMs: undefined,
});
const terminal = createRun({
status: 'succeeded',
createdAtMs: BASE_TIME + 2,
});
const legacy = createRun({
executionOwner: 'legacy',
createdAtMs: BASE_TIME + 3,
});
const firstAttempt = createAttempt(first.id);
const secondAttempt = createAttempt(second.id);
await seed(repository, first, [firstAttempt]);
await seed(repository, second, [secondAttempt]);
await seed(repository, terminal, [createAttempt(terminal.id)]);
await seed(repository, legacy, [createAttempt(legacy.id)]);
const page1 = await source.listCandidates({ limit: 1 });
assert.equal(page1.truncated, true);
assert.equal(page1.unsafeAttemptOverflow, false);
assert.deepEqual(page1.candidates, [
{
runId: first.id,
attempts: [
{
attemptId: firstAttempt.id,
executorType: 'local_process',
},
],
},
]);
const page2 = await source.listCandidates({
limit: 1,
cursor: page1.nextCursor,
});
assert.equal(page2.candidates[0].runId, second.id);
assert.equal(page2.truncated, false);
await assert.rejects(source.listCandidates({ limit: 65 }), RangeError);
});
test('fails closed when corrupt data exceeds the bounded active-attempt budget', async () => {
const { repository, source } = await createRuntime();
const run = createRun();
await seed(repository, run, [
createAttempt(run.id, { attempt: 1 }),
createAttempt(run.id, { attempt: 2 }),
createAttempt(run.id, { attempt: 3 }),
]);
const page = await source.listCandidates({ limit: 1 });
assert.equal(page.unsafeAttemptOverflow, true);
assert.deepEqual(page.candidates, []);
assert.equal(page.nextCursor, undefined);
});
test('audits a verified running process without changing its status', async () => {
const { repository, source } = await createRuntime();
const run = createRun();
const attempt = createAttempt(run.id);
await seed(repository, run, [attempt]);
const reconciler = new PrimaryRunStartupReconciler(
repository,
source,
[inspector(async () => ({ status: 'running', identityPid: 4321 }))],
{ clock: { now: () => BASE_TIME + 10 }, createEventId: nextId },
);
const summary = await reconciler.reconcileBatch();
assert.equal(summary.verifiedRunning, 1);
assert.equal(summary.markedLost, 0);
const persisted = await repository.findRunById(run.id);
assert.equal(persisted.status, 'running');
assert.equal(persisted.version, 1);
assert.deepEqual(
(await repository.listEvents(run.id)).map((event) => event.type),
['run.reconciled'],
);
assert.deepEqual((await repository.listEvents(run.id))[0].payload, {
status: 'running',
executor_type: 'local_process',
evidence: 'durable_handle',
version: 1,
});
});
test('recovers a dispatching Run when its durable Attempt is still running', async () => {
const { repository, source } = await createRuntime();
const run = createRun({ status: 'dispatching', startedAtMs: undefined });
const attempt = createAttempt(run.id);
await seed(repository, run, [attempt]);
const reconciler = new PrimaryRunStartupReconciler(
repository,
source,
[inspector(async () => ({ status: 'running', identityPid: 4321 }))],
{ clock: { now: () => BASE_TIME + 10 }, createEventId: nextId },
);
const summary = await reconciler.reconcileBatch();
assert.equal(summary.recoveredRunning, 1);
assert.equal((await repository.findRunById(run.id)).status, 'running');
assert.deepEqual(
(await repository.listEvents(run.id)).map((event) => event.type),
['run.running'],
);
});
test('marks unprovable processes lost without issuing any process action', async () => {
const { repository, source } = await createRuntime();
const cases = [
{
handle: 'invalid-handle',
inspection: { status: 'invalid' },
code: 'RECOVERY_HANDLE_INVALID',
},
{
handle: 'mismatched-handle',
inspection: { status: 'identity_mismatch', identityPid: 4321 },
code: 'RECOVERY_IDENTITY_MISMATCH',
},
{
handle: 'unsupported-handle',
inspection: { status: 'unsupported', identityPid: 4321 },
code: 'RECOVERY_IDENTITY_UNSUPPORTED',
},
{
handle: 'exited-handle',
inspection: { status: 'exited', identityPid: 4321 },
code: 'RECOVERY_PROCESS_EXITED_UNOBSERVED',
},
{
handle: 'reused-pid-handle',
inspection: { status: 'running', identityPid: 9999 },
code: 'RECOVERY_IDENTITY_PID_MISMATCH',
},
];
const inspections = new Map();
for (const [index, item] of cases.entries()) {
const run = createRun({ createdAtMs: BASE_TIME + index });
const attempt = createAttempt(run.id, { executorHandle: item.handle });
await seed(repository, run, [attempt]);
inspections.set(item.handle, item.inspection);
item.run = run;
item.attempt = attempt;
}
const reconciler = new PrimaryRunStartupReconciler(
repository,
source,
[inspector(async (handle) => inspections.get(handle))],
{ clock: { now: () => BASE_TIME + 20 }, createEventId: nextId },
);
const summary = await reconciler.reconcileBatch();
assert.equal(summary.markedLost, cases.length);
for (const item of cases) {
const run = await repository.findRunById(item.run.id);
const attempt = await repository.findAttemptById(item.attempt.id);
assert.equal(run.status, 'lost');
assert.equal(attempt.status, 'lost');
assert.equal(run.errorCode, item.code);
assert.equal(attempt.errorCode, item.code);
}
});
test('marks incomplete ownership lost before consulting the OS', async () => {
const { repository, source } = await createRuntime();
const run = createRun({ status: 'dispatching', startedAtMs: undefined });
const attempt = createAttempt(run.id, {
status: 'starting',
executorHandle: undefined,
pid: undefined,
startedAtMs: undefined,
});
await seed(repository, run, [attempt]);
const missingAttemptRun = createRun({
status: 'dispatching',
startedAtMs: undefined,
createdAtMs: BASE_TIME + 1,
});
await seed(repository, missingAttemptRun);
let inspections = 0;
const reconciler = new PrimaryRunStartupReconciler(
repository,
source,
[
inspector(async () => {
inspections += 1;
return { status: 'running' };
}),
],
{ clock: { now: () => BASE_TIME + 10 }, createEventId: nextId },
);
const summary = await reconciler.reconcileBatch();
assert.equal(summary.markedLost, 2);
assert.equal(inspections, 0);
assert.equal(
(await repository.findRunById(run.id)).errorCode,
'RECOVERY_ATTEMPT_INCOMPLETE',
);
assert.equal(
(await repository.findRunById(missingAttemptRun.id)).errorCode,
'RECOVERY_ATTEMPT_MISSING',
);
});
test('leaves ambiguous, unsupported, and transient probe failures untouched', async () => {
const { repository, source } = await createRuntime();
const ambiguous = createRun({ createdAtMs: BASE_TIME });
await seed(repository, ambiguous, [
createAttempt(ambiguous.id, { attempt: 1 }),
createAttempt(ambiguous.id, { attempt: 2 }),
]);
const unsupported = createRun({ createdAtMs: BASE_TIME + 1 });
await seed(repository, unsupported, [
createAttempt(unsupported.id, {
executorType: 'remote_worker',
executorHandle: 'remote-handle',
}),
]);
const transient = createRun({ createdAtMs: BASE_TIME + 2 });
const transientAttempt = createAttempt(transient.id);
await seed(repository, transient, [transientAttempt]);
const healthy = createRun({ createdAtMs: BASE_TIME + 3 });
const healthyAttempt = createAttempt(healthy.id);
await seed(repository, healthy, [healthyAttempt]);
const reconciler = new PrimaryRunStartupReconciler(
repository,
source,
[
inspector(async (handle) => {
if (handle === transientAttempt.executorHandle) {
throw new Error('temporary /proc failure');
}
return { status: 'running', identityPid: 4321 };
}),
],
{ clock: { now: () => BASE_TIME + 10 }, createEventId: nextId },
);
const summary = await reconciler.reconcileBatch();
assert.equal(summary.ambiguous, 1);
assert.equal(summary.skipped, 1);
assert.equal(summary.failed, 1);
assert.equal(summary.verifiedRunning, 1);
assert.equal((await repository.findRunById(ambiguous.id)).status, 'running');
assert.equal(
(await repository.findRunById(unsupported.id)).status,
'running',
);
assert.equal((await repository.findRunById(transient.id)).status, 'running');
assert.equal((await repository.findRunById(healthy.id)).status, 'running');
assert.deepEqual(
(await repository.listEvents(healthy.id)).map((event) => event.type),
['run.reconciled'],
);
});
@@ -0,0 +1,104 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
PrimaryRunStartupSupervisor,
} = require('../../back/runtime/application/primaryRunStartupSupervisor');
function page(overrides = {}) {
return {
scanned: 1,
verifiedRunning: 0,
recoveredRunning: 1,
completedFromReceipt: 0,
quarantinedReceipts: 0,
publishGraceWaits: 0,
markedLost: 0,
skipped: 0,
ambiguous: 0,
failed: 0,
truncated: false,
unsafeAttemptOverflow: false,
...overrides,
};
}
test('startup supervisor aggregates bounded pages until recovery completes', async () => {
const cursors = [];
const responses = [
page({
truncated: true,
nextCursor: { createdAtMs: 10, runId: 'run-1' },
}),
page({ verifiedRunning: 1, recoveredRunning: 0, markedLost: 1 }),
];
const supervisor = new PrimaryRunStartupSupervisor({
async reconcileBatch(options) {
cursors.push(options.cursor);
return responses.shift();
},
});
const result = await supervisor.run({ pageSize: 8, maxPages: 4 });
assert.deepEqual(cursors, [undefined, { createdAtMs: 10, runId: 'run-1' }]);
assert.deepEqual(result, {
pages: 2,
scanned: 2,
verifiedRunning: 1,
recoveredRunning: 1,
completedFromReceipt: 0,
quarantinedReceipts: 0,
publishGraceWaits: 0,
markedLost: 1,
skipped: 0,
ambiguous: 0,
failed: 0,
stopReason: 'complete',
remaining: false,
});
});
test('startup supervisor fails closed for overflow, stalled cursor, and page limit', async () => {
const overflow = new PrimaryRunStartupSupervisor({
async reconcileBatch() {
return page({ unsafeAttemptOverflow: true, truncated: true });
},
});
assert.equal((await overflow.run()).stopReason, 'unsafe_attempt_overflow');
const stalled = new PrimaryRunStartupSupervisor({
async reconcileBatch() {
return page({ truncated: true });
},
});
assert.equal((await stalled.run()).stopReason, 'cursor_stalled');
let sequence = 0;
const limited = new PrimaryRunStartupSupervisor({
async reconcileBatch() {
sequence += 1;
return page({
truncated: true,
nextCursor: { createdAtMs: sequence, runId: `run-${sequence}` },
});
},
});
const limitedResult = await limited.run({ maxPages: 2 });
assert.equal(limitedResult.stopReason, 'page_limit');
assert.equal(limitedResult.remaining, true);
assert.deepEqual(limitedResult.nextCursor, {
createdAtMs: 2,
runId: 'run-2',
});
});
test('startup supervisor rejects unbounded settings', async () => {
const supervisor = new PrimaryRunStartupSupervisor({
async reconcileBatch() {
return page();
},
});
await assert.rejects(supervisor.run({ pageSize: 0 }), RangeError);
await assert.rejects(supervisor.run({ maxPages: 65 }), RangeError);
});
+191
View File
@@ -0,0 +1,191 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
PrimaryTimeoutLifecycle,
} = require('../../back/runtime/application/primaryTimeoutLifecycle');
function deferred() {
let resolve;
let reject;
const promise = new Promise((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, resolve, reject };
}
function fakeScheduler() {
let id = 0;
const pending = new Map();
const cleared = [];
return {
pending,
cleared,
scheduler: {
setTimeout(callback, delayMs) {
const timer = {
id: ++id,
delayMs,
unrefCalls: 0,
unref() {
this.unrefCalls += 1;
},
};
pending.set(timer.id, { timer, callback });
return timer;
},
clearTimeout(timer) {
cleared.push(timer.id);
pending.delete(timer.id);
},
},
fireNext() {
const next = pending.values().next().value;
assert.ok(next, 'expected a scheduled timer');
pending.delete(next.timer.id);
next.callback();
return next.timer;
},
};
}
function cycleSummary() {
return {
pages: 1,
scanned: 1,
accepted: 1,
alreadyRequested: 0,
alreadyTerminal: 0,
failed: 0,
stopReason: 'complete',
remaining: false,
};
}
async function flush() {
await new Promise((resolve) => setImmediate(resolve));
}
test('is inert until started and never overlaps timeout scans', async () => {
const timers = fakeScheduler();
const first = deferred();
let calls = 0;
const lifecycle = new PrimaryTimeoutLifecycle(
{
async run(options) {
calls += 1;
assert.deepEqual(options, { pageSize: 8, maxPages: 2 });
return first.promise;
},
},
{
intervalMs: 30_000,
initialDelayMs: 100,
cycle: { pageSize: 8, maxPages: 2 },
scheduler: timers.scheduler,
},
);
assert.equal(timers.pending.size, 0);
assert.equal(lifecycle.start(), true);
assert.equal(lifecycle.start(), false);
const initial = timers.fireNext();
assert.equal(initial.delayMs, 100);
assert.equal(initial.unrefCalls, 1);
await flush();
assert.equal(calls, 1);
assert.equal(timers.pending.size, 0);
first.resolve(cycleSummary());
await flush();
const next = timers.pending.values().next().value.timer;
assert.equal(next.delayMs, 30_000);
assert.equal(next.unrefCalls, 1);
assert.equal(await lifecycle.stop(), 'drained');
assert.deepEqual(timers.cleared, [next.id]);
});
test('reports errors and resumes without callback failure loops', async () => {
const timers = fakeScheduler();
const errors = [];
let calls = 0;
const lifecycle = new PrimaryTimeoutLifecycle(
{
async run() {
calls += 1;
if (calls === 1) throw new Error('database busy');
return cycleSummary();
},
},
{
intervalMs: 5_000,
scheduler: timers.scheduler,
onCycle() {
throw new Error('metrics sink failed');
},
onError(error) {
errors.push(error.message);
},
},
);
lifecycle.start();
timers.fireNext();
await flush();
assert.deepEqual(errors, ['database busy']);
timers.fireNext();
await flush();
assert.deepEqual(errors, ['database busy', 'metrics sink failed']);
assert.equal(await lifecycle.stop(), 'drained');
});
test('bounds shutdown and refuses restart while an old scan is in flight', async () => {
const timers = fakeScheduler();
const running = deferred();
const lifecycle = new PrimaryTimeoutLifecycle(
{
async run() {
return running.promise;
},
},
{
intervalMs: 5_000,
stopTimeoutMs: 5,
scheduler: timers.scheduler,
},
);
lifecycle.start();
timers.fireNext();
await flush();
assert.equal(await lifecycle.stop(), 'timed_out');
assert.equal(timers.pending.size, 0);
assert.equal(lifecycle.start(), false);
running.resolve(cycleSummary());
await flush();
assert.equal(lifecycle.start(), true);
assert.equal(await lifecycle.stop(), 'drained');
});
test('rejects hot loops and unbounded shutdown waits', () => {
const supervisor = {
async run() {
return cycleSummary();
},
};
assert.throws(
() => new PrimaryTimeoutLifecycle(supervisor, { intervalMs: 249 }),
RangeError,
);
assert.throws(
() =>
new PrimaryTimeoutLifecycle(supervisor, {
intervalMs: 500,
stopTimeoutMs: 60_001,
}),
RangeError,
);
});
+213
View File
@@ -0,0 +1,213 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { afterEach, test } = require('node:test');
const { Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const { runSchemaMigration } = require('../../back/migrations/0002-run-schema');
const {
runCancellationRequestMigration,
} = require('../../back/migrations/0004-run-cancellation-request');
const {
runAttemptDeadlineMigration,
} = require('../../back/migrations/0006-run-attempt-deadline');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacySequelizeRunRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/runRepository');
const {
LegacySequelizePrimaryTimeoutSource,
} = require('../../back/runtime/adapters/legacy-sequelize/primaryTimeoutSource');
const {
PrimaryTimeoutRequester,
} = require('../../back/runtime/application/primaryTimeoutRequester');
const {
RunCommandService,
} = require('../../back/runtime/application/runCommandService');
const NOW = 1_750_300_000_000;
const databases = [];
function page(candidates, overrides = {}) {
return { candidates, truncated: false, ...overrides };
}
function candidate(index, overrides = {}) {
return {
runId: `run-${index}`,
attemptId: `attempt-${index}`,
deadlineAtMs: NOW - index,
...overrides,
};
}
afterEach(async () => {
await Promise.all(databases.splice(0).map((database) => database.close()));
});
test('commits a durable timeout request through the real Repository boundary', async () => {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
databases.push(database);
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations: [
runSchemaMigration,
runCancellationRequestMigration,
runAttemptDeadlineMigration,
],
logger: { info() {} },
});
const repository = new LegacySequelizeRunRepository(database);
const runId = '019f7300-0000-7000-8000-000000000101';
const attemptId = '019f7300-0000-7000-8000-000000000102';
await repository.transaction(async (transaction) => {
await transaction.insertRun({
id: runId,
projectId: 'default',
taskId: 'timeout-task',
taskRevision: 'revision-1',
triggerType: 'manual',
executionOrigin: 'manual',
executionOwner: 'runtime',
status: 'running',
version: 3,
eventSequence: 3,
priority: 0,
createdAtMs: NOW - 10_000,
startedAtMs: NOW - 9_000,
});
await transaction.insertAttempt({
id: attemptId,
runId,
attempt: 1,
status: 'running',
executorType: 'local_process',
callbackSequence: 0,
createdAtMs: NOW - 10_000,
startedAtMs: NOW - 9_000,
deadlineAtMs: NOW - 1,
});
});
const requester = new PrimaryTimeoutRequester(
new LegacySequelizePrimaryTimeoutSource(database),
new RunCommandService(repository),
);
const result = await requester.requestBatch({ nowMs: NOW });
assert.equal(result.accepted, 1);
assert.equal(result.failed, 0);
const persisted = await repository.findRunById(runId);
assert.equal(persisted.cancelRequestedAtMs, NOW);
assert.equal(persisted.cancelReason, 'timeout');
assert.deepEqual(
(await repository.listEvents(runId)).map((event) => ({
type: event.type,
actorType: event.actorType,
actorId: event.actorId,
})),
[
{
type: 'run.cancel_requested',
actorType: 'system',
actorId: 'runtime:timeout',
},
],
);
});
test('persists timeout intent for each due candidate without side effects', async () => {
const commands = [];
const requester = new PrimaryTimeoutRequester(
{
async listOverdue(options) {
assert.deepEqual(options, { nowMs: NOW, limit: 8 });
return page([candidate(1), candidate(2), candidate(3)]);
},
},
{
async requestCancellation(command) {
commands.push(command);
if (command.runId === 'run-1') return { status: 'accepted' };
if (command.runId === 'run-2') return { status: 'already_requested' };
return { status: 'already_terminal' };
},
},
);
const result = await requester.requestBatch({ nowMs: NOW, limit: 8 });
assert.deepEqual(result, {
scanned: 3,
accepted: 1,
alreadyRequested: 1,
alreadyTerminal: 1,
failed: 0,
truncated: false,
});
assert.deepEqual(
commands.map(({ runId, attemptId, atMs, reason, actor }) => ({
runId,
attemptId,
atMs,
reason,
actor,
})),
[1, 2, 3].map((index) => ({
runId: `run-${index}`,
attemptId: `attempt-${index}`,
atMs: NOW,
reason: 'timeout',
actor: { type: 'system', id: 'runtime:timeout' },
})),
);
});
test('isolates candidate failures and refuses future deadline output', async () => {
let calls = 0;
const requester = new PrimaryTimeoutRequester(
{
async listOverdue() {
return page([
candidate(1),
candidate(2, { deadlineAtMs: NOW + 1 }),
candidate(3),
]);
},
},
{
async requestCancellation() {
calls += 1;
if (calls === 1) throw new Error('database unavailable');
return { status: 'accepted' };
},
},
);
const result = await requester.requestBatch({ nowMs: NOW });
assert.equal(result.scanned, 3);
assert.equal(result.accepted, 1);
assert.equal(result.failed, 2);
assert.equal(calls, 2);
});
test('validates its clock before querying the source', async () => {
let queried = false;
const requester = new PrimaryTimeoutRequester(
{
async listOverdue() {
queried = true;
return page([]);
},
},
{ async requestCancellation() {} },
{ now: () => -1 },
);
await assert.rejects(requester.requestBatch(), /nowMs/);
assert.equal(queried, false);
});
+167
View File
@@ -0,0 +1,167 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { afterEach, test } = require('node:test');
const { Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const {
RUN_ATTEMPT_TABLE,
RUN_TABLE,
runSchemaMigration,
} = require('../../back/migrations/0002-run-schema');
const {
runCancellationRequestMigration,
} = require('../../back/migrations/0004-run-cancellation-request');
const {
runAttemptDeadlineMigration,
} = require('../../back/migrations/0006-run-attempt-deadline');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacySequelizePrimaryTimeoutSource,
} = require('../../back/runtime/adapters/legacy-sequelize/primaryTimeoutSource');
const databases = [];
const NOW = 1_750_300_000_000;
let sequence = 2000;
function nextId() {
sequence += 1;
return `019f7300-0000-7000-8000-${String(sequence).padStart(12, '0')}`;
}
async function createRuntime() {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations: [
runSchemaMigration,
runCancellationRequestMigration,
runAttemptDeadlineMigration,
],
logger: { info() {} },
});
databases.push(database);
return {
database,
source: new LegacySequelizePrimaryTimeoutSource(database),
};
}
async function seed(database, overrides = {}) {
const runId = overrides.runId ?? nextId();
const attemptId = overrides.attemptId ?? nextId();
await database.getQueryInterface().bulkInsert(RUN_TABLE, [
{
id: runId,
project_id: 'default',
task_id: 'timeout-task',
task_revision: 'revision-1',
trigger_type: 'manual',
execution_origin: 'manual',
execution_owner: overrides.executionOwner ?? 'runtime',
status: overrides.runStatus ?? 'running',
version: 3,
event_sequence: 3,
priority: 0,
created_at_ms: NOW - 10_000,
started_at_ms: NOW - 9_000,
cancel_requested_at_ms: overrides.cancelRequestedAtMs ?? null,
},
]);
await database.getQueryInterface().bulkInsert(RUN_ATTEMPT_TABLE, [
{
id: attemptId,
run_id: runId,
attempt: 1,
status: overrides.attemptStatus ?? 'running',
executor_type: 'local_process',
callback_sequence: 0,
created_at_ms: NOW - 10_000,
started_at_ms: NOW - 9_000,
deadline_at_ms:
overrides.deadlineAtMs === undefined ? NOW - 1 : overrides.deadlineAtMs,
},
]);
return { runId, attemptId };
}
afterEach(async () => {
await Promise.all(databases.splice(0).map((database) => database.close()));
});
test('lists only overdue active runtime-owned Attempts without cancellation', async () => {
const { database, source } = await createRuntime();
const due = await seed(database, { deadlineAtMs: NOW - 10 });
await seed(database, { deadlineAtMs: NOW + 1 });
await seed(database, { deadlineAtMs: null });
await seed(database, { executionOwner: 'legacy' });
await seed(database, { runStatus: 'succeeded' });
await seed(database, { attemptStatus: 'claimed' });
await seed(database, { cancelRequestedAtMs: NOW - 20 });
assert.deepEqual(await source.listOverdue({ nowMs: NOW }), {
candidates: [
{
runId: due.runId,
attemptId: due.attemptId,
deadlineAtMs: NOW - 10,
},
],
truncated: false,
});
});
test('paginates equal deadlines by Attempt id with a stable bounded cursor', async () => {
const { database, source } = await createRuntime();
const first = await seed(database, { deadlineAtMs: NOW - 5 });
const second = await seed(database, { deadlineAtMs: NOW - 5 });
const page1 = await source.listOverdue({ nowMs: NOW, limit: 1 });
assert.equal(page1.truncated, true);
assert.deepEqual(
page1.candidates.map((item) => item.attemptId),
[first.attemptId],
);
assert.deepEqual(page1.nextCursor, {
deadlineAtMs: NOW - 5,
attemptId: first.attemptId,
});
const page2 = await source.listOverdue({
nowMs: NOW,
limit: 1,
cursor: page1.nextCursor,
});
assert.equal(page2.truncated, false);
assert.deepEqual(
page2.candidates.map((item) => item.attemptId),
[second.attemptId],
);
});
test('rejects unbounded timeout scans and malformed cursors', async () => {
const { source } = await createRuntime();
await assert.rejects(source.listOverdue({ nowMs: -1 }), /nowMs/);
await assert.rejects(source.listOverdue({ nowMs: NOW, limit: 65 }), /limit/);
await assert.rejects(
source.listOverdue({
nowMs: NOW,
cursor: { deadlineAtMs: -1, attemptId: 'attempt' },
}),
/deadlineAtMs/,
);
await assert.rejects(
source.listOverdue({
nowMs: NOW,
cursor: { deadlineAtMs: NOW, attemptId: '' },
}),
/attemptId/,
);
});
+140
View File
@@ -0,0 +1,140 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
PrimaryTimeoutSupervisor,
} = require('../../back/runtime/application/primaryTimeoutSupervisor');
function summary(overrides = {}) {
return {
scanned: 1,
accepted: 1,
alreadyRequested: 0,
alreadyTerminal: 0,
failed: 0,
truncated: false,
...overrides,
};
}
test('aggregates bounded timeout pages with one fixed observation time', async () => {
const calls = [];
const responses = [
summary({
truncated: true,
nextCursor: { deadlineAtMs: 10, attemptId: 'attempt-1' },
}),
summary({ accepted: 0, alreadyTerminal: 1 }),
];
const supervisor = new PrimaryTimeoutSupervisor({
async requestBatch(options) {
calls.push(options);
return responses.shift();
},
});
const result = await supervisor.run({ nowMs: 100, pageSize: 8, maxPages: 4 });
assert.deepEqual(calls, [
{ nowMs: 100, limit: 8 },
{
nowMs: 100,
cursor: { deadlineAtMs: 10, attemptId: 'attempt-1' },
limit: 8,
},
]);
assert.deepEqual(result, {
pages: 2,
scanned: 2,
accepted: 1,
alreadyRequested: 0,
alreadyTerminal: 1,
failed: 0,
stopReason: 'complete',
remaining: false,
});
});
test('samples its clock once when the caller omits an observation time', async () => {
const calls = [];
let clockCalls = 0;
const responses = [
summary({
truncated: true,
nextCursor: { deadlineAtMs: 10, attemptId: 'attempt-1' },
}),
summary(),
];
const supervisor = new PrimaryTimeoutSupervisor(
{
async requestBatch(options) {
calls.push(options);
return responses.shift();
},
},
{
now() {
clockCalls += 1;
return 123;
},
},
);
await supervisor.run({ pageSize: 8 });
assert.equal(clockCalls, 1);
assert.equal(calls.length, 2);
assert.equal(calls[0].nowMs, 123);
assert.equal(calls[1].nowMs, 123);
});
test('fails closed for a stalled cursor and bounded page exhaustion', async () => {
const stalledCursor = { deadlineAtMs: 10, attemptId: 'attempt-1' };
const stalled = new PrimaryTimeoutSupervisor({
async requestBatch() {
return summary({ truncated: true, nextCursor: stalledCursor });
},
});
assert.deepEqual(await stalled.run({ cursor: stalledCursor }), {
pages: 1,
scanned: 1,
accepted: 1,
alreadyRequested: 0,
alreadyTerminal: 0,
failed: 0,
stopReason: 'cursor_stalled',
remaining: true,
nextCursor: stalledCursor,
});
let sequence = 0;
const limited = new PrimaryTimeoutSupervisor({
async requestBatch() {
sequence += 1;
return summary({
truncated: true,
nextCursor: {
deadlineAtMs: sequence,
attemptId: `attempt-${sequence}`,
},
});
},
});
const limitedResult = await limited.run({ maxPages: 2 });
assert.equal(limitedResult.stopReason, 'page_limit');
assert.equal(limitedResult.remaining, true);
assert.deepEqual(limitedResult.nextCursor, {
deadlineAtMs: 2,
attemptId: 'attempt-2',
});
});
test('rejects unbounded timeout supervisor settings', async () => {
const supervisor = new PrimaryTimeoutSupervisor({
async requestBatch() {
return summary();
},
});
await assert.rejects(supervisor.run({ pageSize: 0 }), /pageSize/);
await assert.rejects(supervisor.run({ maxPages: 65 }), /maxPages/);
await assert.rejects(supervisor.run({ nowMs: -1 }), /nowMs/);
});
+606
View File
@@ -0,0 +1,606 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const { QueryTypes, Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const {
PROJECT_ROLE_BINDING_TABLE,
PROJECT_TABLE,
projectPolicyMigration,
} = require('../../back/migrations/0017-project-policy');
const {
PROJECT_OWNER_BOOTSTRAP_CHALLENGE_ID_INDEX,
PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE,
PROJECT_OWNER_BOOTSTRAP_CURRENT_INDEX,
projectOwnerBootstrapMigration,
} = require('../../back/migrations/0018-project-owner-bootstrap');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacySequelizeProjectOwnerBootstrapRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/projectOwnerBootstrapRepository');
const {
LegacySequelizeProjectPolicyRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/projectPolicyRepository');
const {
ProjectOwnerBootstrapService,
} = require('../../back/runtime/application/projectOwnerBootstrapService');
const {
AuthenticatedPrincipalExpiredError,
normalizeAuthenticatedPrincipal,
} = require('../../back/runtime/domain/authenticatedPrincipal');
const {
ProjectOwnerBootstrapChallengeActiveError,
ProjectOwnerBootstrapClaimRejectedError,
ProjectOwnerBootstrapProjectInactiveError,
ProjectOwnerBootstrapProjectNotFoundError,
ProjectOwnerBootstrapProjectNotPristineError,
ProjectOwnerBootstrapUnauthorizedError,
ProjectOwnerBootstrapUnavailableError,
} = require('../../back/runtime/domain/projectOwnerBootstrap');
const PROJECT_ID = 'default';
const NOW = 100_000;
const TTL_MS = 60_000;
function principal(subject = { type: 'user', id: 'owner-1' }, overrides = {}) {
return {
subject,
authenticationId: 'auth-1',
authenticatedAtMs: 0,
expiresAtMs: NOW + TTL_MS * 10,
assurance: 'multi_factor',
...overrides,
};
}
function localConsolePrincipal(overrides = {}) {
return principal(
{ type: 'system', id: 'owner-bootstrap' },
{ assurance: 'local_console', ...overrides },
);
}
function deterministicRandomSource(seed = 1) {
let next = seed;
return {
bytes(size) {
const value = Buffer.alloc(size, next);
next += 1;
return value;
},
};
}
async function setup(t, storage = ':memory:') {
const database = new Sequelize({
dialect: 'sqlite',
storage,
logging: false,
});
t.after(() => database.close());
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations: [projectPolicyMigration, projectOwnerBootstrapMigration],
logger: { info() {} },
});
const repository = new LegacySequelizeProjectOwnerBootstrapRepository(
database,
);
return {
database,
repository,
service: new ProjectOwnerBootstrapService(
repository,
deterministicRandomSource(),
),
};
}
test('normalizes an exact, bounded and live authenticated principal', () => {
const value = principal();
assert.deepEqual(normalizeAuthenticatedPrincipal(value), value);
assert.throws(
() => normalizeAuthenticatedPrincipal({ ...value, scopes: ['*'] }),
/shape is invalid/,
);
assert.throws(
() =>
normalizeAuthenticatedPrincipal({
...value,
authenticationId: 'auth with spaces',
}),
/authenticationId is invalid/,
);
assert.throws(
() =>
normalizeAuthenticatedPrincipal({
...value,
expiresAtMs: value.authenticatedAtMs,
}),
/lifetime is invalid/,
);
});
test('migration records versioned challenges and both bounded lookup indexes', async (t) => {
const { database, service } = await setup(t);
const issued = await service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
});
assert.match(issued.challengeId, /^[A-Za-z0-9_-]{22}$/);
assert.match(issued.token, /^[A-Za-z0-9_-]{43}$/);
assert.equal(issued.expiresAtMs, NOW + TTL_MS);
const rows = await database
.getQueryInterface()
.select(null, PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE);
assert.equal(rows.length, 1);
assert.equal(rows[0].version, 1);
assert.equal(rows[0].challenge_id, issued.challengeId);
assert.match(rows[0].token_digest, /^[0-9a-f]{64}$/);
assert.notEqual(rows[0].token_digest, issued.token);
assert.equal(JSON.stringify(rows).includes(issued.token), false);
assert.equal(rows[0].consumed_at_ms, null);
assert.equal(
(
await database
.getQueryInterface()
.select(null, PROJECT_ROLE_BINDING_TABLE)
).length,
0,
);
const indexes = new Set(
(
await database
.getQueryInterface()
.showIndex(PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE)
).map((index) => index.name),
);
assert.ok(indexes.has(PROJECT_OWNER_BOOTSTRAP_CHALLENGE_ID_INDEX));
assert.ok(indexes.has(PROJECT_OWNER_BOOTSTRAP_CURRENT_INDEX));
});
test('only an active local-console bootstrap principal can issue', async (t) => {
const { service } = await setup(t);
for (const issuer of [
principal(),
localConsolePrincipal({ assurance: 'service' }),
principal(
{ type: 'system', id: 'different-system' },
{ assurance: 'local_console' },
),
]) {
await assert.rejects(
service.issue({
projectId: PROJECT_ID,
issuer,
nowMs: NOW,
ttlMs: TTL_MS,
}),
ProjectOwnerBootstrapUnauthorizedError,
);
}
await assert.rejects(
service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal({ expiresAtMs: NOW }),
nowMs: NOW,
ttlMs: TTL_MS,
}),
AuthenticatedPrincipalExpiredError,
);
await assert.rejects(
service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
subject: { type: 'user', id: 'injected-owner' },
}),
/request shape is invalid/,
);
});
test('does not replace a live challenge and versions a replacement only after expiry', async (t) => {
const { database, service } = await setup(t);
const request = {
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
};
const first = await service.issue(request);
await assert.rejects(
service.issue({ ...request, nowMs: NOW + 1 }),
ProjectOwnerBootstrapChallengeActiveError,
);
const second = await service.issue({
...request,
nowMs: NOW + TTL_MS,
});
assert.notEqual(second.challengeId, first.challengeId);
const rows = await database.query(
`SELECT version, challenge_id FROM "${PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE}" ORDER BY version`,
{ type: QueryTypes.SELECT },
);
assert.deepEqual(rows, [
{ version: 1, challenge_id: first.challengeId },
{ version: 2, challenge_id: second.challengeId },
]);
await assert.rejects(
service.claim({
projectId: PROJECT_ID,
challengeId: first.challengeId,
token: first.token,
principal: principal(),
nowMs: NOW + TTL_MS + 1,
}),
ProjectOwnerBootstrapClaimRejectedError,
);
});
test('claims one owner atomically and permits only exact idempotent replay', async (t) => {
const { database, service } = await setup(t);
const issued = await service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
});
const request = {
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: issued.token,
principal: principal(),
nowMs: NOW + 1,
};
const claimed = await service.claim(request);
assert.equal(claimed.status, 'claimed');
assert.deepEqual(claimed.binding, {
projectId: PROJECT_ID,
subject: { type: 'user', id: 'owner-1' },
version: 1,
state: 'active',
role: 'owner',
mutationId: `owner-bootstrap:${issued.challengeId}`,
changedBy: { type: 'system', id: 'owner-bootstrap' },
createdAtMs: NOW + 1,
});
const replay = await service.claim({ ...request, nowMs: NOW + 2 });
assert.equal(replay.status, 'existing');
assert.deepEqual(replay.binding, claimed.binding);
const policyRepository = new LegacySequelizeProjectPolicyRepository(database);
assert.deepEqual(
(await policyRepository.resolve(PROJECT_ID, principal().subject)).binding,
claimed.binding,
);
const challenge = (
await database
.getQueryInterface()
.select(null, PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE)
)[0];
assert.equal(Number(challenge.consumed_at_ms), NOW + 1);
assert.equal(challenge.claimed_subject_type, 'user');
assert.equal(challenge.claimed_subject_id, 'owner-1');
await assert.rejects(
service.claim({
...request,
principal: principal({ type: 'user', id: 'owner-2' }),
nowMs: NOW + 2,
}),
ProjectOwnerBootstrapClaimRejectedError,
);
});
test('rejects expired, malformed, mismatched and non-user claims without leaking the token', async (t) => {
const { service } = await setup(t);
const issued = await service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
});
await assert.rejects(
service.claim({
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: Buffer.alloc(32, 99).toString('base64url'),
principal: principal(),
nowMs: NOW + 1,
}),
(error) => {
assert.ok(error instanceof ProjectOwnerBootstrapClaimRejectedError);
assert.equal(error.message.includes(issued.token), false);
return true;
},
);
await assert.rejects(
service.claim({
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: issued.token,
principal: principal(),
nowMs: NOW + TTL_MS,
}),
ProjectOwnerBootstrapClaimRejectedError,
);
await assert.rejects(
service.claim({
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: issued.token,
principal: principal({ type: 'api_app', id: 'app-1' }),
nowMs: NOW + 1,
}),
ProjectOwnerBootstrapUnauthorizedError,
);
await assert.rejects(
service.claim({
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: `${issued.token}x`,
principal: principal(),
nowMs: NOW + 1,
}),
(error) => {
assert.equal(error.message.includes(issued.token), false);
return true;
},
);
await assert.rejects(
service.claim({
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: issued.token,
principal: principal(),
nowMs: NOW + 1,
subject: { type: 'user', id: 'injected-owner' },
}),
/request shape is invalid/,
);
});
test('refuses bootstrap whenever the Project already has any role binding', async (t) => {
const first = await setup(t);
await first.database.query(
`INSERT INTO "${PROJECT_ROLE_BINDING_TABLE}"
(project_id, subject_type, subject_id, version, state, role,
mutation_id, changed_by_type, changed_by_id, created_at_ms)
VALUES
('default', 'user', 'existing', 1, 'active', 'viewer',
'existing-binding', 'system', 'owner-bootstrap', 1)`,
);
await assert.rejects(
first.service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
}),
ProjectOwnerBootstrapProjectNotPristineError,
);
const second = await setup(t);
const issued = await second.service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
});
await second.database.query(
`INSERT INTO "${PROJECT_ROLE_BINDING_TABLE}"
(project_id, subject_type, subject_id, version, state, role,
mutation_id, changed_by_type, changed_by_id, created_at_ms)
VALUES
('default', 'user', 'existing', 1, 'active', 'viewer',
'existing-binding', 'system', 'owner-bootstrap', 1)`,
);
await assert.rejects(
second.service.claim({
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: issued.token,
principal: principal(),
nowMs: NOW + 1,
}),
ProjectOwnerBootstrapProjectNotPristineError,
);
});
test('fails closed for missing and archived Projects', async (t) => {
const missing = await setup(t);
await assert.rejects(
missing.service.issue({
projectId: 'missing',
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
}),
ProjectOwnerBootstrapProjectNotFoundError,
);
const archived = await setup(t);
const issued = await archived.service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
});
await archived.database.query(
`UPDATE "${PROJECT_TABLE}" SET status = 'archived' WHERE id = 'default'`,
);
await assert.rejects(
archived.service.claim({
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: issued.token,
principal: principal(),
nowMs: NOW + 1,
}),
ProjectOwnerBootstrapProjectInactiveError,
);
});
test('rolls back challenge consumption when owner binding insertion fails', async (t) => {
const { database, service } = await setup(t);
const issued = await service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
});
await database.query(
`CREATE TRIGGER reject_bootstrap_owner
BEFORE INSERT ON "${PROJECT_ROLE_BINDING_TABLE}"
WHEN NEW.mutation_id LIKE 'owner-bootstrap:%'
BEGIN
SELECT RAISE(ABORT, 'forced bootstrap binding failure');
END`,
);
const request = {
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: issued.token,
principal: principal(),
nowMs: NOW + 1,
};
await assert.rejects(
service.claim(request),
ProjectOwnerBootstrapUnavailableError,
);
let challenge = (
await database
.getQueryInterface()
.select(null, PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE)
)[0];
assert.equal(challenge.consumed_at_ms, null);
assert.equal(
(
await database
.getQueryInterface()
.select(null, PROJECT_ROLE_BINDING_TABLE)
).length,
0,
);
await database.query('DROP TRIGGER reject_bootstrap_owner');
assert.equal((await service.claim(request)).status, 'claimed');
challenge = (
await database
.getQueryInterface()
.select(null, PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE)
)[0];
assert.equal(Number(challenge.consumed_at_ms), NOW + 1);
});
test('rolls back when SQLite silently ignores challenge consumption', async (t) => {
const { database, service } = await setup(t);
const issued = await service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
});
await database.query(
`CREATE TRIGGER ignore_bootstrap_consumption
BEFORE UPDATE ON "${PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE}"
WHEN NEW.consumed_at_ms IS NOT NULL
BEGIN
SELECT RAISE(IGNORE);
END`,
);
await assert.rejects(
service.claim({
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: issued.token,
principal: principal(),
nowMs: NOW + 1,
}),
ProjectOwnerBootstrapUnavailableError,
);
const challenge = (
await database
.getQueryInterface()
.select(null, PROJECT_OWNER_BOOTSTRAP_CHALLENGE_TABLE)
)[0];
assert.equal(challenge.consumed_at_ms, null);
assert.equal(
(
await database
.getQueryInterface()
.select(null, PROJECT_ROLE_BINDING_TABLE)
).length,
0,
);
});
test('serializes claims from separate SQLite connections so exactly one owner wins', async (t) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-bootstrap-db-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
const storage = path.join(root, 'database.sqlite');
const first = await setup(t, storage);
const issued = await first.service.issue({
projectId: PROJECT_ID,
issuer: localConsolePrincipal(),
nowMs: NOW,
ttlMs: TTL_MS,
});
const secondDatabase = new Sequelize({
dialect: 'sqlite',
storage,
logging: false,
});
t.after(() => secondDatabase.close());
const secondService = new ProjectOwnerBootstrapService(
new LegacySequelizeProjectOwnerBootstrapRepository(secondDatabase),
deterministicRandomSource(50),
);
const claim = (service, ownerId) =>
service.claim({
projectId: PROJECT_ID,
challengeId: issued.challengeId,
token: issued.token,
principal: principal({ type: 'user', id: ownerId }),
nowMs: NOW + 1,
});
const results = await Promise.allSettled([
claim(first.service, 'owner-a'),
claim(secondService, 'owner-b'),
]);
assert.equal(
results.filter((result) => result.status === 'fulfilled').length,
1,
);
const rejected = results.find((result) => result.status === 'rejected');
assert.ok(rejected.reason instanceof ProjectOwnerBootstrapClaimRejectedError);
const bindings = await first.database
.getQueryInterface()
.select(null, PROJECT_ROLE_BINDING_TABLE);
assert.equal(bindings.length, 1);
assert.equal(bindings[0].role, 'owner');
assert.ok(['owner-a', 'owner-b'].includes(bindings[0].subject_id));
});
test('rejects non-SQLite bootstrap repositories', () => {
assert.throws(
() =>
new LegacySequelizeProjectOwnerBootstrapRepository({
getDialect() {
return 'postgres';
},
}),
/SQLite-only/,
);
});
+599
View File
@@ -0,0 +1,599 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const { Sequelize } = require('sequelize');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const {
PROJECT_ROLE_BINDING_CURRENT_INDEX,
PROJECT_ROLE_BINDING_MUTATION_INDEX,
PROJECT_ROLE_BINDING_SUBJECT_INDEX,
PROJECT_ROLE_BINDING_TABLE,
PROJECT_SLUG_INDEX,
PROJECT_TABLE,
projectPolicyMigration,
} = require('../../back/migrations/0017-project-policy');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacySequelizeProjectPolicyRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/projectPolicyRepository');
const {
ProjectPolicyArtifactReadAuthorizer,
} = require('../../back/runtime/adapters/policy/projectPolicyArtifactReadAuthorizer');
const {
ProjectPolicyEngine,
} = require('../../back/runtime/application/projectPolicyEngine');
const {
LocalArtifactReadService,
} = require('../../back/runtime/application/localArtifactReadService');
const {
ProjectPolicyUnavailableError,
ProjectRoleBindingMutationConflictError,
ProjectRoleBindingVersionConflictError,
normalizeProjectPermission,
normalizeProjectRoleBindingRecord,
} = require('../../back/runtime/domain/projectPolicy');
const PROJECT_ID = 'default';
const CHANGED_BY = Object.freeze({ type: 'user', id: 'local-owner' });
async function setup(t, storage = ':memory:') {
const database = new Sequelize({
dialect: 'sqlite',
storage,
logging: false,
});
t.after(() => database.close());
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations: [projectPolicyMigration],
logger: { info() {} },
});
return {
database,
repository: new LegacySequelizeProjectPolicyRepository(database),
};
}
function binding({
subject = { type: 'user', id: 'user-1' },
version = 1,
role = 'viewer',
state = 'active',
mutationId = 'mutation-1',
createdAtMs = version,
} = {}) {
return {
projectId: PROJECT_ID,
subject,
version,
state,
...(state === 'active' ? { role } : {}),
mutationId,
changedBy: CHANGED_BY,
createdAtMs,
};
}
async function append(repository, value, expectedCurrentVersion) {
return repository.append({
expectedCurrentVersion,
binding: value,
});
}
test('migration creates an ownerless default Project and bounded lookup indexes', async (t) => {
const { database, repository } = await setup(t);
const projects = await database
.getQueryInterface()
.select(null, PROJECT_TABLE);
assert.deepEqual(projects, [
{
id: 'default',
name: 'Default',
slug: 'default',
status: 'active',
version: 1,
created_at_ms: 0,
updated_at_ms: 0,
},
]);
assert.deepEqual(
await repository.resolve(PROJECT_ID, { type: 'user', id: 'local-owner' }),
{
project: {
id: 'default',
name: 'Default',
slug: 'default',
status: 'active',
version: 1,
createdAtMs: 0,
updatedAtMs: 0,
},
},
);
const projectIndexes = new Set(
(await database.getQueryInterface().showIndex(PROJECT_TABLE)).map(
(index) => index.name,
),
);
const bindingIndexes = new Set(
(
await database.getQueryInterface().showIndex(PROJECT_ROLE_BINDING_TABLE)
).map((index) => index.name),
);
assert.ok(projectIndexes.has(PROJECT_SLUG_INDEX));
assert.ok(bindingIndexes.has(PROJECT_ROLE_BINDING_CURRENT_INDEX));
assert.ok(bindingIndexes.has(PROJECT_ROLE_BINDING_MUTATION_INDEX));
assert.ok(bindingIndexes.has(PROJECT_ROLE_BINDING_SUBJECT_INDEX));
});
test('normalizes only declared permissions and exact tool identities', () => {
assert.equal(normalizeProjectPermission('artifact.read'), 'artifact.read');
assert.equal(
normalizeProjectPermission('tool.call:github.issue.read'),
'tool.call:github.issue.read',
);
for (const value of [
'logs',
'artifact.write',
'tool.call:*',
'tool.call:',
'tool.call:bad value',
]) {
assert.throws(
() => normalizeProjectPermission(value),
/permission is invalid/,
);
}
assert.throws(
() =>
normalizeProjectRoleBindingRecord({
...binding({ state: 'revoked' }),
role: 'viewer',
}),
/shape is invalid/,
);
assert.throws(
() =>
normalizeProjectRoleBindingRecord({
...binding(),
permissions: ['artifact.read'],
}),
/shape is invalid/,
);
});
test('appends immutable role versions, replays mutations and resolves latest state', async (t) => {
const { repository } = await setup(t);
const first = binding();
assert.deepEqual(await append(repository, first, 0), {
status: 'inserted',
binding: first,
});
assert.deepEqual(await append(repository, first, 0), {
status: 'existing',
binding: first,
});
await assert.rejects(
append(repository, { ...first, role: 'operator' }, 0),
ProjectRoleBindingMutationConflictError,
);
await assert.rejects(
append(repository, binding({ mutationId: 'stale-mutation' }), 0),
ProjectRoleBindingVersionConflictError,
);
const second = binding({
version: 2,
role: 'operator',
mutationId: 'mutation-2',
});
assert.equal((await append(repository, second, 1)).status, 'inserted');
assert.deepEqual(
(await repository.resolve(PROJECT_ID, first.subject)).binding,
second,
);
const revoked = binding({
version: 3,
state: 'revoked',
mutationId: 'mutation-3',
});
assert.equal((await append(repository, revoked, 2)).status, 'inserted');
assert.deepEqual(
(await repository.resolve(PROJECT_ID, first.subject)).binding,
revoked,
);
});
test('serializes concurrent first assignments so only one current version wins', async (t) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-policy-db-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
const storage = path.join(root, 'database.sqlite');
const { repository } = await setup(t, storage);
const secondDatabase = new Sequelize({
dialect: 'sqlite',
storage,
logging: false,
});
t.after(() => secondDatabase.close());
const secondRepository = new LegacySequelizeProjectPolicyRepository(
secondDatabase,
);
const subject = { type: 'api_app', id: 'app-1' };
const results = await Promise.allSettled([
append(
secondRepository,
binding({ subject, role: 'viewer', mutationId: 'concurrent-a' }),
0,
),
append(
repository,
binding({ subject, role: 'operator', mutationId: 'concurrent-b' }),
0,
),
]);
assert.equal(
results.filter((result) => result.status === 'fulfilled').length,
1,
);
const rejected = results.find((result) => result.status === 'rejected');
assert.ok(rejected.reason instanceof ProjectRoleBindingVersionConflictError);
const snapshot = await repository.resolve(PROJECT_ID, subject);
assert.equal(snapshot.binding.version, 1);
assert.ok(['viewer', 'operator'].includes(snapshot.binding.role));
});
test('defaults to deny and applies viewer, operator, archive and revocation rules', async (t) => {
const { database, repository } = await setup(t);
const engine = new ProjectPolicyEngine(repository);
const viewer = { type: 'user', id: 'viewer-1' };
assert.deepEqual(
await engine.decide({
subject: viewer,
projectId: 'missing',
permission: 'artifact.read',
}),
{ effect: 'deny', reasons: ['project_not_found'] },
);
assert.deepEqual(
await engine.decide({
subject: viewer,
projectId: PROJECT_ID,
permission: 'artifact.read',
}),
{ effect: 'deny', reasons: ['subject_unbound'] },
);
await append(
repository,
binding({ subject: viewer, role: 'viewer', mutationId: 'viewer-bind' }),
0,
);
assert.equal(
(
await engine.decide({
subject: viewer,
projectId: PROJECT_ID,
permission: 'artifact.read',
})
).effect,
'allow',
);
assert.deepEqual(
await engine.decide({
subject: viewer,
projectId: PROJECT_ID,
permission: 'run.start',
}),
{ effect: 'deny', reasons: ['permission_missing'] },
);
const operator = binding({
subject: viewer,
version: 2,
role: 'operator',
mutationId: 'viewer-promote',
});
await append(repository, operator, 1);
assert.equal(
(
await engine.decide({
subject: viewer,
projectId: PROJECT_ID,
permission: 'run.start',
})
).effect,
'allow',
);
await database
.getQueryInterface()
.bulkUpdate(
PROJECT_TABLE,
{ status: 'archived', version: 2, updated_at_ms: 2 },
{ id: PROJECT_ID },
);
assert.deepEqual(
await engine.decide({
subject: viewer,
projectId: PROJECT_ID,
permission: 'run.start',
}),
{ effect: 'deny', reasons: ['project_archived'] },
);
assert.equal(
(
await engine.decide({
subject: viewer,
projectId: PROJECT_ID,
permission: 'artifact.read',
})
).effect,
'allow',
);
await append(
repository,
binding({
subject: viewer,
version: 3,
state: 'revoked',
mutationId: 'viewer-revoke',
}),
2,
);
assert.deepEqual(
await engine.decide({
subject: viewer,
projectId: PROJECT_ID,
permission: 'artifact.read',
}),
{ effect: 'deny', reasons: ['subject_unbound'] },
);
});
test('requires approval for Agent mutations while allowing bound reads', async (t) => {
const { repository } = await setup(t);
const engine = new ProjectPolicyEngine(repository);
const agent = { type: 'agent', id: 'agent-1' };
await append(
repository,
binding({ subject: agent, role: 'operator', mutationId: 'agent-bind' }),
0,
);
assert.deepEqual(
await engine.decide({
subject: agent,
projectId: PROJECT_ID,
permission: 'run.start',
}),
{
effect: 'require_approval',
reasons: ['agent_action_requires_approval'],
},
);
assert.deepEqual(
await engine.decide({
subject: agent,
projectId: PROJECT_ID,
permission: 'tool.call:github.issue.read',
}),
{
effect: 'require_approval',
reasons: ['agent_action_requires_approval'],
},
);
assert.equal(
(
await engine.decide({
subject: agent,
projectId: PROJECT_ID,
permission: 'artifact.read',
})
).effect,
'allow',
);
});
test('enforces the owner, admin, operator and viewer permission matrix', async (t) => {
const { repository } = await setup(t);
const engine = new ProjectPolicyEngine(repository);
const cases = [
{
role: 'owner',
allow: [
'project.manage',
'policy.manage',
'task.delete',
'approval.recover',
],
deny: [],
},
{
role: 'admin',
allow: [
'policy.manage',
'task.delete',
'secret.manage',
'approval.recover',
],
deny: ['project.manage'],
},
{
role: 'operator',
allow: ['task.update', 'run.start', 'secret.use'],
deny: [
'task.delete',
'secret.manage',
'policy.manage',
'approval.recover',
],
},
{
role: 'viewer',
allow: ['project.read', 'task.read', 'run.read', 'artifact.read'],
deny: ['task.update', 'run.start', 'secret.use', 'approval.recover'],
},
];
for (const [index, item] of cases.entries()) {
const subject = { type: 'user', id: `${item.role}-user` };
await append(
repository,
binding({
subject,
role: item.role,
mutationId: `matrix-${index}`,
}),
0,
);
for (const permission of item.allow) {
assert.equal(
(await engine.decide({ subject, projectId: PROJECT_ID, permission }))
.effect,
'allow',
`${item.role} should allow ${permission}`,
);
}
for (const permission of item.deny) {
assert.equal(
(await engine.decide({ subject, projectId: PROJECT_ID, permission }))
.effect,
'deny',
`${item.role} should deny ${permission}`,
);
}
}
});
test('Artifact authorizer delegates only the bound subject, Project and permission', async () => {
const calls = [];
const authorizer = new ProjectPolicyArtifactReadAuthorizer({
async decide(value) {
calls.push(value);
return { effect: 'require_approval', reasons: ['test'] };
},
});
const effect = await authorizer.authorize({
action: 'artifact.read',
subject: { type: 'api_app', id: 'app-1' },
projectId: PROJECT_ID,
runId: '019f7600-0000-7000-8000-000000000001',
logArtifactId: `local-${'e'.repeat(30)}`,
});
assert.equal(effect, 'require_approval');
assert.deepEqual(calls, [
{
subject: { type: 'api_app', id: 'app-1' },
projectId: PROJECT_ID,
permission: 'artifact.read',
},
]);
await assert.rejects(
authorizer.authorize({
action: 'artifact.delete',
subject: { type: 'api_app', id: 'app-1' },
projectId: PROJECT_ID,
runId: '019f7600-0000-7000-8000-000000000001',
logArtifactId: `local-${'e'.repeat(30)}`,
}),
/action is invalid/,
);
});
test('real Policy Core keeps Artifact bytes unreachable until a viewer binding exists', async (t) => {
const { repository } = await setup(t);
const subject = { type: 'api_app', id: 'artifact-client' };
const runId = '019f7600-0000-7000-8000-000000000001';
const attemptId = '019f7600-0000-7000-8000-000000000002';
const logArtifactId = `local-${'e'.repeat(30)}`;
let byteReads = 0;
const service = new LocalArtifactReadService(
{
async find() {
return {
projectId: PROJECT_ID,
runId,
attemptId,
logArtifactId,
};
},
},
new ProjectPolicyArtifactReadAuthorizer(
new ProjectPolicyEngine(repository),
),
{
async read() {
byteReads += 1;
return {
status: 'available',
content: Buffer.from('log'),
start: 0,
endExclusive: 3,
totalBytes: 3,
};
},
},
{
async read() {
return null;
},
},
);
const request = {
subject,
projectId: PROJECT_ID,
runId,
logArtifactId,
range: { offset: 0, length: 1024 },
};
assert.deepEqual(await service.read(request), {
status: 'forbidden',
effect: 'deny',
});
assert.equal(byteReads, 0);
await append(
repository,
binding({ subject, role: 'viewer', mutationId: 'artifact-viewer' }),
0,
);
const available = await service.read(request);
assert.equal(available.status, 'available');
assert.equal(available.content.toString(), 'log');
assert.equal(byteReads, 1);
});
test('fails closed for corrupt current bindings and non-SQLite adapters', async (t) => {
const { database, repository } = await setup(t);
const current = binding();
await append(repository, current, 0);
await database.getQueryInterface().bulkUpdate(
PROJECT_ROLE_BINDING_TABLE,
{ role: null },
{
project_id: current.projectId,
subject_type: current.subject.type,
subject_id: current.subject.id,
version: current.version,
},
);
await assert.rejects(
repository.resolve(PROJECT_ID, current.subject),
ProjectPolicyUnavailableError,
);
assert.throws(
() =>
new LegacySequelizeProjectPolicyRepository({
getDialect() {
return 'postgres';
},
}),
/SQLite-only/,
);
});
@@ -0,0 +1,264 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
FIXTURE,
LIMITATIONS,
validateApprovalManagementKubernetesLiveReport,
} = require('../../scripts/ql3-approval-management-kubernetes-live-audit.cjs');
function digest(character) {
return 'sha256:' + character.repeat(64);
}
function validReport() {
return {
schemaVersion: 1,
fixture: FIXTURE,
observedAt: '2026-08-10T12:00:00.000Z',
platform: {
distribution: 'k3s',
kubernetesVersion: 'v1.34.3+k3s1',
architecture: 'arm64',
kubernetesImageId: digest('1'),
managementImageId: digest('2'),
cniName: 'flannel',
cniDistributionBinding: 'rancher/k3s:v1.34.3-k3s1',
controlPlaneNodes: 1,
workerNodes: 2,
cniReadyNodes: 3,
},
database: {
operator: 'cloudnative-pg',
operatorVersion: '1.30.0',
postgresVersionNumber: 180004,
postgresImageId: digest('3'),
instances: 3,
readyInstances: 3,
managerRole: 'ql3_approval_manager',
migrationCount: 54,
controlCoreCapability: 53,
tlsVerified: true,
primaryChangedDuringFailover: true,
},
deployment: {
namespace: 'qinglong3-system',
service: 'ql3-approval-management',
port: 8447,
replicas: 2,
readyReplicas: 2,
podIdentitySha256: [digest('4'), digest('5')],
nodeIdentitySha256: [digest('6'), digest('7')],
serviceAccount: 'ql3-approval-management',
automountServiceAccountToken: false,
requiredPodAntiAffinity: true,
podDisruptionBudgetMinAvailable: 1,
maxUnavailable: 0,
maxConnectionsPerPod: 2,
},
client: {
binary: 'ql3-approval-client',
operations: ['approval.inspect', 'approval.decide'],
inputKind: 'Secret',
inputImmutable: true,
callerDrivenJob: true,
backoffLimit: 0,
serviceAccountTokenMounted: false,
rbacGranted: false,
transportProtocol: 'TLSv1.3',
mutualTls: true,
servernameVerified: true,
exactPodRequests: 5,
inspectStatuses: ['found', 'found', 'found'],
decisionStatuses: ['decided', 'existing'],
responseRedacted: true,
},
identityRotation: {
overlapOldAssertionAccepted: true,
overlapNewAssertionAccepted: true,
revokedOldAssertionRejected: true,
activeNewAssertionAccepted: true,
rollbackSurgeFailedClosed: true,
twoReadyReplicasPreserved: true,
durableGenerationReachedThree: true,
},
certificateRotation: {
previousSerialSha256: digest('8'),
currentSerialSha256: digest('9'),
previousBundleSha256: digest('a'),
currentBundleSha256: digest('b'),
oldClientAcceptedBefore: true,
replacementClientAcceptedBefore: true,
oldClientRejectedAfter: true,
replacementClientAcceptedAfter: true,
fullPodReplacement: true,
allReplicasReadyThroughout: true,
},
availability: {
databaseFailureWithdrewReadiness: true,
databaseFailurePreservedLiveness: true,
stalePodsDidNotRecoverInPlace: true,
freshPodsRecoveredAfterDatabase: true,
bothReplicasServedAfterRecovery: true,
},
isolation: {
labelledClientAllowed: true,
unlabelledClientDenied: true,
wrongPortDenied: true,
kubernetesApiEgressDenied: true,
publicInternetEgressDenied: true,
cloudNativePgEgressAllowed: true,
managerSecretReadDenied: true,
managerMutationRbacDenied: true,
},
durability: {
approvalVersion: 2,
approvalState: 'approved',
decisionIdSha256: digest('c'),
allowedAuditCount: 4,
deniedAuditCount: 1,
duplicateDecisionCount: 0,
identityGeneration: 3,
survivedCloudNativePgFailover: true,
},
gates: {
realThreeNodeKubernetes: true,
realCniPolicy: true,
threeInstanceCloudNativePg: true,
twoManagerPodsOnDistinctNodes: true,
tls13ProductClientAcrossBothPods: true,
strongUserDecision: true,
identityProjectionRotation: true,
certificateRevocationRollout: true,
databaseReadinessFence: true,
durableFactsSurvivedFailover: true,
leastPrivilege: true,
passed: true,
},
limitations: [...LIMITATIONS],
};
}
function mutate(change) {
const report = structuredClone(validReport());
change(report);
return validateApprovalManagementKubernetesLiveReport(report);
}
test('accepts the exact content-free Kubernetes approval live report', () => {
assert.deepEqual(
validateApprovalManagementKubernetesLiveReport(validReport()),
{
schemaVersion: 1,
fixture: FIXTURE,
findings: [],
compatible: true,
},
);
});
test('rejects topology, migration and approval deployment weakening', () => {
for (const [code, change] of [
[
'QL3_APPROVAL_KUBERNETES_LIVE_PLATFORM',
(report) => {
report.platform.workerNodes = 1;
},
],
[
'QL3_APPROVAL_KUBERNETES_LIVE_DATABASE',
(report) => {
report.database.migrationCount = 52;
},
],
[
'QL3_APPROVAL_KUBERNETES_LIVE_DEPLOYMENT',
(report) => {
report.deployment.nodeIdentitySha256[1] =
report.deployment.nodeIdentitySha256[0];
},
],
]) {
assert.ok(mutate(change).findings.some((entry) => entry.code === code));
}
});
test('rejects widened client, incomplete rotations and false availability', () => {
for (const [code, change] of [
[
'QL3_APPROVAL_KUBERNETES_LIVE_CLIENT',
(report) => {
report.client.rbacGranted = true;
},
],
[
'QL3_APPROVAL_KUBERNETES_LIVE_IDENTITY_ROTATION',
(report) => {
report.identityRotation.revokedOldAssertionRejected = false;
},
],
[
'QL3_APPROVAL_KUBERNETES_LIVE_CERTIFICATE_ROTATION',
(report) => {
report.certificateRotation.fullPodReplacement = false;
},
],
[
'QL3_APPROVAL_KUBERNETES_LIVE_AVAILABILITY',
(report) => {
report.availability.stalePodsDidNotRecoverInPlace = false;
},
],
]) {
assert.ok(mutate(change).findings.some((entry) => entry.code === code));
}
});
test('rejects incomplete isolation and durable approval drift', () => {
assert.ok(
mutate((report) => {
report.isolation.publicInternetEgressDenied = false;
}).findings.some(
({ code }) => code === 'QL3_APPROVAL_KUBERNETES_LIVE_ISOLATION',
),
);
assert.ok(
mutate((report) => {
report.durability.approvalVersion = 1;
}).findings.some(
({ code }) => code === 'QL3_APPROVAL_KUBERNETES_LIVE_DURABILITY',
),
);
});
test('rejects secret material, widened schema, false gates and hidden limitations', () => {
for (const [code, change] of [
[
'QL3_APPROVAL_KUBERNETES_LIVE_SECRET_EXPOSURE',
(report) => {
report.client.assertion =
'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJvcGVyYXRvciJ9.signature0123456789';
},
],
[
'QL3_APPROVAL_KUBERNETES_LIVE_REPORT_SHAPE',
(report) => {
report.debug = true;
},
],
[
'QL3_APPROVAL_KUBERNETES_LIVE_GATES',
(report) => {
report.gates.passed = false;
},
],
[
'QL3_APPROVAL_KUBERNETES_LIVE_LIMITATIONS',
(report) => {
report.limitations = [];
},
],
]) {
assert.ok(mutate(change).findings.some((entry) => entry.code === code));
}
});
@@ -0,0 +1,125 @@
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');
const { test } = require('node:test');
const {
assertion,
assertionForSubject,
decisionCommand,
inspectCommand,
keyset,
reviewedKey,
weakAssertion,
} = require('../../scripts/ql3-approval-management-kubernetes-live-contract.cjs');
test('approval live report path is mandatory before mutation begins', () => {
const script = path.resolve(
__dirname,
'../../scripts/ql3-approval-management-kubernetes-live-contract.cjs',
);
const result = spawnSync(process.execPath, [script], {
encoding: 'utf8',
env: { ...process.env, QL3_APPROVAL_MANAGEMENT_KUBERNETES_LIVE: '1' },
});
assert.equal(result.status, 1);
assert.match(result.stderr, /--report=\/absolute\/private-report\.json/);
assert.doesNotMatch(result.stderr, /Docker\/Kubernetes/);
});
test('approval live can sign a strong but unauthorized User identity', () => {
const key = reviewedKey('approval-outsider-test-key');
const payload = JSON.parse(
Buffer.from(
assertionForSubject(key, 'approval-outsider', 'unit-test').split('.')[1],
'base64url',
),
);
assert.equal(payload.sub, 'approval-outsider');
assert.equal(payload.acr, 'urn:ql3:mfa');
assert.deepEqual(payload.amr, ['pwd', 'otp']);
});
test('approval live identity is audience, type, purpose and assurance bound', () => {
const key = reviewedKey('approval-live-test-key');
const document = keyset(1, [key]);
assert.equal(document.generation, 1);
assert.equal(document.audience, 'qinglong3-approval-management');
assert.deepEqual(document.revokedKids, []);
const strong = assertion(key, 'strong-unit-test');
const [encodedHeader, encodedPayload, signature] = strong.split('.');
assert.ok(signature.length > 32);
assert.deepEqual(JSON.parse(Buffer.from(encodedHeader, 'base64url')), {
alg: 'EdDSA',
kid: 'approval-live-test-key',
typ: 'ql3-approval-management+jwt',
});
const payload = JSON.parse(Buffer.from(encodedPayload, 'base64url'));
assert.equal(payload.aud, 'qinglong3-approval-management');
assert.equal(payload.ql3_purpose, 'approval-management');
assert.equal(payload.sub, 'approval-operator');
assert.equal(payload.acr, 'urn:ql3:mfa');
assert.deepEqual(payload.amr, ['pwd', 'otp']);
const weakPayload = JSON.parse(
Buffer.from(weakAssertion(key, 'weak-unit-test').split('.')[1], 'base64url'),
);
assert.equal(weakPayload.acr, 'urn:ql3:password');
assert.deepEqual(weakPayload.amr, ['pwd']);
});
test('approval live commands bind exact request, action and distinct audits', () => {
const inspect = inspectCommand(
'project-a',
'approval-a',
'inspect-a',
1,
);
assert.equal(inspect.operation, 'approval.inspect');
assert.notEqual(
inspect.request.auditEventId,
inspect.request.failureAuditEventId,
);
const decide = decisionCommand(
'project-a',
'approval-a',
'decide-a',
'decision-a',
2,
);
assert.equal(decide.operation, 'approval.decide');
assert.equal(decide.request.expectedVersion, 1);
assert.equal(decide.request.expectedAction.permission, 'run.start');
assert.equal(decide.request.decision, 'approved');
assert.equal(decide.request.reasonCode, 'reviewed');
});
test('approval live runner remains opt-in, audited and observation backed', () => {
const source = fs.readFileSync(
path.resolve(
__dirname,
'../../scripts/ql3-approval-management-kubernetes-live-contract.cjs',
),
'utf8',
);
assert.match(
source,
/QL3_APPROVAL_MANAGEMENT_KUBERNETES_LIVE !== '1'/,
);
assert.match(source, /reviewedOperatorManifest\(operatorManifestFile\)/);
assert.match(source, /validateApprovalManagementKubernetesLiveReport/);
assert.match(source, /createManagementClientExecutor/);
assert.match(source, /clientTcpProbe/);
assert.match(source, /podTcpProbe/);
assert.match(source, /weakUserRejected/);
assert.match(source, /identity ledger rollback surge failure/);
assert.match(source, /CloudNativePG primary promotion/);
assert.match(source, /migrationCount: 54/);
assert.match(source, /controlCoreCapability: 53/);
assert.match(source, /flannel\.alpha\.coreos\.com\/backend-type/);
assert.doesNotMatch(source, /kubectl.*logs/);
assert.doesNotMatch(source, /ceremony is not complete/);
});
@@ -0,0 +1,260 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
FIXTURE,
LIMITATIONS,
validateAutomationManagementKubernetesLiveReport,
} = require('../../scripts/ql3-automation-management-kubernetes-live-audit.cjs');
function digest(character) {
return `sha256:${character.repeat(64)}`;
}
function validReport() {
return {
schemaVersion: 1,
fixture: FIXTURE,
observedAt: '2026-08-01T12:00:00.000Z',
platform: {
distribution: 'k3s',
kubernetesVersion: 'v1.34.3+k3s1',
architecture: 'arm64',
kubernetesImageId: digest('1'),
managementImageId: digest('2'),
cniName: 'flannel',
cniDistributionBinding: 'rancher/k3s:v1.34.3-k3s1',
controlPlaneNodes: 1,
workerNodes: 2,
cniReadyNodes: 3,
},
database: {
operator: 'cloudnative-pg',
operatorVersion: '1.27.1',
postgresVersionNumber: 180004,
postgresImageId: digest('3'),
instances: 3,
readyInstances: 3,
managerRole: 'ql3_automation_manager',
controlCoreCapability: 53,
tlsVerified: true,
primaryChangedDuringFailover: true,
},
deployment: {
namespace: 'qinglong3-system',
service: 'ql3-automation-management',
port: 8445,
replicas: 2,
readyReplicas: 2,
podIdentitySha256: [digest('4'), digest('5')],
nodeIdentitySha256: [digest('6'), digest('7')],
serviceAccount: 'ql3-automation-management',
automountServiceAccountToken: false,
requiredPodAntiAffinity: true,
podDisruptionBudgetMinAvailable: 1,
maxUnavailable: 0,
maxConnectionsPerPod: 2,
},
client: {
binary: 'ql3-automation-client',
operation: 'task.publish',
inputKind: 'Secret',
inputImmutable: true,
callerDrivenJob: true,
backoffLimit: 0,
serviceAccountTokenMounted: false,
rbacGranted: false,
transportProtocol: 'TLSv1.3',
mutualTls: true,
servernameVerified: true,
exactPodRequests: 2,
resultStatuses: ['created', 'existing'],
responseRedacted: true,
},
identityRotation: {
overlapOldAssertionAccepted: true,
overlapNewAssertionAccepted: true,
revokedOldAssertionRejected: true,
activeNewAssertionAccepted: true,
rollbackSurgeFailedClosed: true,
twoReadyReplicasPreserved: true,
durableGenerationReachedThree: true,
},
certificateRotation: {
previousSerialSha256: digest('8'),
currentSerialSha256: digest('9'),
previousBundleSha256: digest('a'),
currentBundleSha256: digest('b'),
oldClientAcceptedBefore: true,
replacementClientAcceptedBefore: true,
oldClientRejectedAfter: true,
replacementClientAcceptedAfter: true,
fullPodReplacement: true,
allReplicasReadyThroughout: true,
},
availability: {
databaseFailureWithdrewReadiness: true,
databaseFailurePreservedLiveness: true,
stalePodsDidNotRecoverInPlace: true,
freshPodsRecoveredAfterDatabase: true,
bothReplicasServedAfterRecovery: true,
},
isolation: {
labelledClientAllowed: true,
unlabelledClientDenied: true,
wrongPortDenied: true,
kubernetesApiEgressDenied: true,
publicInternetEgressDenied: true,
cloudNativePgEgressAllowed: true,
managerSecretReadDenied: true,
managerMutationRbacDenied: true,
},
durability: {
taskRevisionCount: 4,
triggerRevisionCount: 2,
allowedAuditCount: 6,
replayDuplicateCount: 0,
taskCurrentRevision: 4,
triggerCurrentRevision: 2,
survivedCloudNativePgFailover: true,
},
gates: {
realThreeNodeKubernetes: true,
realCniPolicy: true,
threeInstanceCloudNativePg: true,
twoManagerPodsOnDistinctNodes: true,
tls13ProductClientAcrossBothPods: true,
identityProjectionRotation: true,
certificateRevocationRollout: true,
databaseReadinessFence: true,
durableFactsSurvivedFailover: true,
leastPrivilege: true,
passed: true,
},
limitations: [...LIMITATIONS],
};
}
function mutate(change) {
const report = structuredClone(validReport());
change(report);
return validateAutomationManagementKubernetesLiveReport(report);
}
test('accepts the exact low-sensitive Kubernetes automation live report', () => {
assert.deepEqual(
validateAutomationManagementKubernetesLiveReport(validReport()),
{
schemaVersion: 1,
fixture: FIXTURE,
findings: [],
compatible: true,
},
);
});
test('rejects topology, CloudNativePG and manager deployment weakening', () => {
for (const [code, change] of [
[
'QL3_AUTOMATION_KUBERNETES_LIVE_PLATFORM',
(report) => {
report.platform.workerNodes = 1;
},
],
[
'QL3_AUTOMATION_KUBERNETES_LIVE_DATABASE',
(report) => {
report.database.instances = 1;
},
],
[
'QL3_AUTOMATION_KUBERNETES_LIVE_DEPLOYMENT',
(report) => {
report.deployment.nodeIdentitySha256[1] =
report.deployment.nodeIdentitySha256[0];
},
],
]) {
assert.ok(mutate(change).findings.some((entry) => entry.code === code));
}
});
test('rejects widened client, incomplete rotations and false availability', () => {
for (const [code, change] of [
[
'QL3_AUTOMATION_KUBERNETES_LIVE_CLIENT',
(report) => {
report.client.rbacGranted = true;
},
],
[
'QL3_AUTOMATION_KUBERNETES_LIVE_IDENTITY_ROTATION',
(report) => {
report.identityRotation.revokedOldAssertionRejected = false;
},
],
[
'QL3_AUTOMATION_KUBERNETES_LIVE_CERTIFICATE_ROTATION',
(report) => {
report.certificateRotation.fullPodReplacement = false;
},
],
[
'QL3_AUTOMATION_KUBERNETES_LIVE_AVAILABILITY',
(report) => {
report.availability.stalePodsDidNotRecoverInPlace = false;
},
],
]) {
assert.ok(mutate(change).findings.some((entry) => entry.code === code));
}
});
test('rejects incomplete CNI isolation and durable fact drift', () => {
assert.ok(
mutate((report) => {
report.isolation.publicInternetEgressDenied = false;
}).findings.some(
({ code }) => code === 'QL3_AUTOMATION_KUBERNETES_LIVE_ISOLATION',
),
);
assert.ok(
mutate((report) => {
report.durability.replayDuplicateCount = 1;
}).findings.some(
({ code }) => code === 'QL3_AUTOMATION_KUBERNETES_LIVE_DURABILITY',
),
);
});
test('rejects secret material, widened schema, false gates and hidden limitations', () => {
for (const [code, change] of [
[
'QL3_AUTOMATION_KUBERNETES_LIVE_SECRET_EXPOSURE',
(report) => {
report.client.assertion =
'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJvcGVyYXRvciJ9.signature0123456789';
},
],
[
'QL3_AUTOMATION_KUBERNETES_LIVE_REPORT_SHAPE',
(report) => {
report.debug = true;
},
],
[
'QL3_AUTOMATION_KUBERNETES_LIVE_GATES',
(report) => {
report.gates.passed = false;
},
],
[
'QL3_AUTOMATION_KUBERNETES_LIVE_LIMITATIONS',
(report) => {
report.limitations = [];
},
],
]) {
assert.ok(mutate(change).findings.some((entry) => entry.code === code));
}
});
@@ -0,0 +1,80 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { test } = require('node:test');
const {
assertion,
envelope,
keyset,
reviewedKey,
taskCommand,
triggerCommand,
} = require('../../scripts/ql3-automation-management-kubernetes-live-contract.cjs');
test('automation live identity ceremony is audience, type and purpose bound', () => {
const key = reviewedKey('automation-live-test-key');
const document = keyset(1, [key]);
assert.equal(document.generation, 1);
assert.equal(document.audience, 'qinglong3-automation-management');
assert.deepEqual(document.revokedKids, []);
const token = assertion(key, 'unit-test');
const [encodedHeader, encodedPayload, signature] = token.split('.');
assert.ok(signature.length > 32);
const header = JSON.parse(Buffer.from(encodedHeader, 'base64url'));
const payload = JSON.parse(Buffer.from(encodedPayload, 'base64url'));
assert.deepEqual(header, {
alg: 'EdDSA',
kid: 'automation-live-test-key',
typ: 'ql3-automation-management+jwt',
});
assert.equal(payload.aud, 'qinglong3-automation-management');
assert.equal(payload.ql3_purpose, 'automation-management');
assert.equal(payload.sub, 'automation-operator');
});
test('automation live commands retain exact revision and task pinning', () => {
const task = taskCommand('project-a', null, '001', 'v1');
const taskEnvelope = envelope('task.publish', 'task-v1', task);
assert.equal(taskEnvelope.operation, 'task.publish');
assert.equal(taskEnvelope.request.command.expectedRevision, null);
const published = {
taskId: task.taskId,
revision: 1,
contentDigest: 'a'.repeat(64),
};
const trigger = triggerCommand('project-a', null, published, '001');
assert.equal(trigger.taskRevision, 1);
assert.equal(trigger.taskContentDigest, 'a'.repeat(64));
});
test('automation live runner remains opt-in, audited and observation backed', () => {
const source = fs.readFileSync(
path.resolve(
__dirname,
'../../scripts/ql3-automation-management-kubernetes-live-contract.cjs',
),
'utf8',
);
assert.match(source, /QL3_AUTOMATION_MANAGEMENT_KUBERNETES_LIVE !== '1'/);
assert.match(source, /reviewedOperatorManifest\(operatorManifestFile\)/);
assert.match(source, /validateAutomationManagementKubernetesLiveReport/);
assert.match(source, /clientTcpProbe\(/);
assert.match(source, /podTcpProbe\(/);
assert.match(source, /\/dev\/termination-log/);
assert.match(source, /umask 077/);
assert.match(source, /chmod 600 \/tmp\/client\.json/);
assert.match(source, /QL3_PLUGIN_PACKAGE_MANAGEMENT_CLIENT_REQUEST_FAILED/);
assert.match(source, /"\$attempt" -ge 60/);
assert.doesNotMatch(source, /logs', `job\/\$\{definition\.name\}`/);
assert.match(source, /identity ledger rollback surge failure/);
assert.match(source, /CloudNativePG primary promotion/);
assert.match(source, /flannel\.alpha\.coreos\.com\/backend-type/);
assert.match(
source,
/finalNodes = fixture\.kubectlJson\(\['get', 'nodes'\]\)/,
);
assert.match(source, /new Set\(cniReadyNodes\.map/);
assert.doesNotMatch(source, /app=flannel/);
assert.doesNotMatch(source, /ceremony is not complete/);
});
@@ -0,0 +1,127 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { test } = require('node:test');
const {
auditBarmanCloudSupplyChain,
} = require('../../scripts/ql3-barman-cloud-supply-chain-audit.cjs');
const ROOT = path.resolve(__dirname, '../..');
const LOCK =
'deploy/kubernetes/ql3-cluster/operators/barman-cloud/plugin-lock.json';
function mutateLock(transform) {
const target = path.join(ROOT, LOCK);
return (filePath, encoding) => {
const source = fs.readFileSync(filePath, encoding);
if (path.resolve(filePath) !== target) return source;
return JSON.stringify(transform(JSON.parse(source)));
};
}
test('accepts the exact Barman candidate lock while preserving release blockers', () => {
const report = auditBarmanCloudSupplyChain({ root: ROOT });
assert.equal(report.compatible, true, JSON.stringify(report.findings));
assert.equal(report.pluginVersion, '0.13.0');
assert.equal(report.releaseReady, false);
assert.deepEqual(report.releaseBlockers, [
'live-object-store-backup-wal-latest-restore-pitr-evidence',
]);
});
test('rejects a movable or drifted controller image', () => {
const report = auditBarmanCloudSupplyChain({
root: ROOT,
readFile: mutateLock((lock) => {
lock.plugin.controller.image =
'ghcr.io/cloudnative-pg/plugin-barman-cloud:v0.13.0';
return lock;
}),
});
assert.equal(report.compatible, false);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_BARMAN_CONTROLLER_IMAGE',
),
true,
);
});
test('rejects a sidecar platform digest drift', () => {
const report = auditBarmanCloudSupplyChain({
root: ROOT,
readFile: mutateLock((lock) => {
lock.plugin.sidecar.platforms['linux/arm64'] = `sha256:${'0'.repeat(64)}`;
return lock;
}),
});
assert.equal(report.compatible, false);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_BARMAN_SIDECAR_IMAGE',
),
true,
);
});
test('rejects an unverified release asset', () => {
const report = auditBarmanCloudSupplyChain({
root: ROOT,
readFile: mutateLock((lock) => {
lock.plugin.releaseManifestSha256 = 'unverified';
return lock;
}),
});
assert.equal(report.compatible, false);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_BARMAN_RELEASE_ASSET',
),
true,
);
});
test('rejects certificate authority supply-chain status drift', () => {
const report = auditBarmanCloudSupplyChain({
root: ROOT,
readFile: mutateLock((lock) => {
lock.certificateAuthority.status = 'selected-unverified';
return lock;
}),
});
assert.equal(report.compatible, false);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_BARMAN_CERTIFICATE_GATE',
),
true,
);
});
test('rejects premature release readiness or an unreviewed installer', () => {
const readiness = auditBarmanCloudSupplyChain({
root: ROOT,
readFile: mutateLock((lock) => {
lock.releaseReady = true;
lock.releaseBlockers = [];
return lock;
}),
});
assert.equal(
readiness.findings.some(
(candidate) => candidate.code === 'QL3_BARMAN_PREMATURE_RELEASE',
),
true,
);
const installer = auditBarmanCloudSupplyChain({
root: ROOT,
readDirectory: () => ['plugin-lock.json', 'manifest.yaml'],
});
assert.equal(
installer.findings.some(
(candidate) => candidate.code === 'QL3_BARMAN_INSTALLER_UNREVIEWED',
),
true,
);
});
+67
View File
@@ -0,0 +1,67 @@
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 {
createQingLong3PackageClosureBuildPlan,
resolveQingLong3Package,
} = require('../../scripts/ql3-build-package-closure.cjs');
function fixture(t, directoryName = 'ql3-example', name = '@qinglong/example') {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-build-closure-'));
const packageDirectory = path.join(root, 'packages', directoryName);
fs.mkdirSync(packageDirectory, { recursive: true });
fs.writeFileSync(
path.join(packageDirectory, 'package.json'),
JSON.stringify({ name, scripts: { build: 'tsc -p tsconfig.json' } }),
);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
return { packageDirectory, root };
}
test('builds one package and its workspace dependency closure in topology order', (t) => {
const { packageDirectory, root } = fixture(t);
assert.deepEqual(resolveQingLong3Package(root, packageDirectory), {
name: '@qinglong/example',
packageDirectory,
});
const plan = createQingLong3PackageClosureBuildPlan(root, packageDirectory);
assert.deepEqual(plan.args, [
'-r',
'--workspace-concurrency=1',
'--filter',
'@qinglong/example...',
'run',
'build',
]);
assert.equal(plan.cwd, root);
});
test('rejects package escapes, non-QL3 directories and recursive builds', (t) => {
const valid = fixture(t);
assert.throws(
() => resolveQingLong3Package(valid.root, valid.root),
/cwd must be one direct packages\/ql3-\* directory/,
);
const legacy = fixture(t, 'legacy-example');
assert.throws(
() => resolveQingLong3Package(legacy.root, legacy.packageDirectory),
/cwd must be one direct packages\/ql3-\* directory/,
);
const recursive = fixture(t, 'ql3-recursive', '@qinglong/recursive');
const manifestPath = path.join(recursive.packageDirectory, 'package.json');
fs.writeFileSync(
manifestPath,
JSON.stringify({
name: '@qinglong/recursive',
scripts: { build: 'pnpm --filter @qinglong/example build && tsc' },
}),
);
assert.throws(
() => resolveQingLong3Package(recursive.root, recursive.packageDirectory),
/build must compile only itself/,
);
});
@@ -0,0 +1,130 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { test } = require('node:test');
const {
auditCertManagerSelection,
} = require('../../scripts/ql3-cert-manager-selection-audit.cjs');
const ROOT = path.resolve(__dirname, '../..');
const SELECTION =
'deploy/kubernetes/ql3-cluster/operators/cert-manager/selection-lock.json';
const BARMAN =
'deploy/kubernetes/ql3-cluster/operators/barman-cloud/plugin-lock.json';
function mutateJson(relativePath, transform) {
const target = path.join(ROOT, relativePath);
return (filePath, encoding) => {
const source = fs.readFileSync(filePath, encoding);
if (path.resolve(filePath) !== target) return source;
return JSON.stringify(transform(JSON.parse(source)));
};
}
test('accepts the supply-chain-verified cert-manager selection with live blockers', () => {
const report = auditCertManagerSelection({ root: ROOT });
assert.equal(report.compatible, true, JSON.stringify(report.findings));
assert.equal(report.certManagerVersion, '1.20.3');
assert.equal(report.kubernetesVersion, '1.32.8');
assert.equal(report.releaseReady, false);
assert.deepEqual(report.releaseBlockers, [
'live-cert-manager-api-and-plugin-mtls-rotation-evidence',
]);
});
test('rejects selecting a cert-manager minor outside the Kubernetes baseline', () => {
const report = auditCertManagerSelection({
root: ROOT,
readFile: mutateJson(SELECTION, (selection) => {
selection.certManager.version = '1.21.0';
return selection;
}),
});
assert.equal(report.compatible, false);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_CERT_MANAGER_SELECTION',
),
true,
);
});
test('rejects widening or hiding the Kubernetes support boundary', () => {
const report = auditCertManagerSelection({
root: ROOT,
readFile: mutateJson(SELECTION, (selection) => {
selection.compatibility.supportedKubernetesMin = '1.33';
return selection;
}),
});
assert.equal(report.compatible, false);
assert.equal(
report.findings.some(
(candidate) =>
candidate.code === 'QL3_CERT_MANAGER_KUBERNETES_COMPATIBILITY',
),
true,
);
});
test('rejects pretending the live evidence gate is release-ready', () => {
const report = auditCertManagerSelection({
root: ROOT,
readFile: mutateJson(SELECTION, (selection) => {
selection.releaseReady = true;
selection.releaseBlockers = [];
return selection;
}),
});
assert.equal(report.compatible, false);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_CERT_MANAGER_PREMATURE_RELEASE',
),
true,
);
});
test('rejects certificate identity, usage or namespace drift', () => {
const report = auditCertManagerSelection({
root: ROOT,
readFile: mutateJson(SELECTION, (selection) => {
selection.pluginTls.certificates[1].dnsNames = ['unreviewed'];
return selection;
}),
});
assert.equal(report.compatible, false);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_CERT_MANAGER_PLUGIN_TLS',
),
true,
);
});
test('rejects Barman binding drift or an unverified installer', () => {
const binding = auditCertManagerSelection({
root: ROOT,
readFile: mutateJson(BARMAN, (lock) => {
lock.certificateAuthority.version = 'latest';
return lock;
}),
});
assert.equal(
binding.findings.some(
(candidate) => candidate.code === 'QL3_CERT_MANAGER_BARMAN_BINDING',
),
true,
);
const installer = auditCertManagerSelection({
root: ROOT,
readDirectory: () => ['selection-lock.json', 'manifest.yaml'],
});
assert.equal(
installer.findings.some(
(candidate) => candidate.code === 'QL3_CERT_MANAGER_INSTALLER_UNVERIFIED',
),
true,
);
});
@@ -0,0 +1,64 @@
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 {
cleanQingLong3PackageArtifacts,
} = require('../../scripts/ql3-clean-package-artifacts.cjs');
test('removes dist and paired emitted source files only from QL3 importers', (t) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-clean-artifacts-'));
t.after(() => fs.rmSync(root, { force: true, recursive: true }));
const registeredPackage = path.join(root, 'packages/ql3-runtime-core');
const unregisteredPackage = path.join(root, 'packages/ql3-stale-package');
const legacyPackage = path.join(root, 'packages/legacy-package');
for (const packageDirectory of [
registeredPackage,
unregisteredPackage,
legacyPackage,
]) {
fs.mkdirSync(path.join(packageDirectory, 'dist'), { recursive: true });
fs.writeFileSync(path.join(packageDirectory, 'dist/stale.js'), 'stale');
fs.writeFileSync(path.join(packageDirectory, 'source.ts'), 'source');
}
fs.writeFileSync(path.join(registeredPackage, 'package.json'), '{}');
fs.writeFileSync(path.join(legacyPackage, 'package.json'), '{}');
fs.mkdirSync(path.join(registeredPackage, 'src/nested'), { recursive: true });
fs.writeFileSync(path.join(registeredPackage, 'src/nested/example.ts'), '');
for (const suffix of ['js', 'js.map', 'd.ts', 'd.ts.map']) {
fs.writeFileSync(
path.join(registeredPackage, `src/nested/example.${suffix}`),
'emitted',
);
}
fs.writeFileSync(
path.join(registeredPackage, 'src/nested/intentional.js'),
'module.exports = {};',
);
assert.deepEqual(cleanQingLong3PackageArtifacts(root), [
'packages/ql3-runtime-core/dist',
]);
assert.equal(fs.existsSync(path.join(registeredPackage, 'dist')), false);
assert.equal(fs.existsSync(path.join(registeredPackage, 'source.ts')), true);
assert.equal(
fs.existsSync(path.join(registeredPackage, 'src/nested/example.ts')),
true,
);
for (const suffix of ['js', 'js.map', 'd.ts', 'd.ts.map']) {
assert.equal(
fs.existsSync(
path.join(registeredPackage, `src/nested/example.${suffix}`),
),
false,
);
}
assert.equal(
fs.existsSync(path.join(registeredPackage, 'src/nested/intentional.js')),
true,
);
assert.equal(fs.existsSync(path.join(unregisteredPackage, 'dist')), true);
assert.equal(fs.existsSync(path.join(legacyPackage, 'dist')), true);
});
@@ -0,0 +1,158 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { test } = require('node:test');
const {
auditCloudNativePgBackup,
} = require('../../scripts/ql3-cloudnativepg-backup-audit.cjs');
const ROOT = path.resolve(__dirname, '../..');
function intercept(relativePath, transform) {
const target = path.join(ROOT, relativePath);
return (filePath, encoding) => {
const source = fs.readFileSync(filePath, encoding);
return path.resolve(filePath) === target ? transform(source) : source;
};
}
test('accepts isolated CNPG-I WAL, backup and restore contracts', () => {
const report = auditCloudNativePgBackup({ root: ROOT });
assert.equal(report.compatible, true, JSON.stringify(report.findings));
assert.equal(report.plugin, 'barman-cloud.cloudnative-pg.io');
assert.equal(report.sourceCluster, 'ql3-postgres');
assert.equal(report.restoreCluster, 'ql3-postgres-restore');
assert.equal(report.retentionPolicy, '30d');
});
test('rejects deprecated in-tree backup or multiple WAL authorities', () => {
const report = auditCloudNativePgBackup({
root: ROOT,
readFile: intercept(
'deploy/kubernetes/ql3-cluster/components/barman-cloud-backup/cluster-plugin-patch.yaml',
(source) =>
`${source.replace(
'name: barman-cloud.cloudnative-pg.io',
'name: unreviewed.example',
)}\n backup:\n retentionPolicy: 30d\n`,
),
});
assert.equal(report.compatible, false);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_CNPG_WAL_ARCHIVER',
),
true,
);
});
test('rejects primary-only or implicit backup methods', () => {
const report = auditCloudNativePgBackup({
root: ROOT,
readFile: intercept(
'deploy/kubernetes/ql3-cluster/components/barman-cloud-backup/scheduled-backup.yaml',
(source) =>
source
.replace('target: prefer-standby', 'target: primary')
.replace('method: plugin', 'method: barmanObjectStore'),
),
});
assert.equal(report.compatible, false);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_CNPG_BASE_BACKUP_SCHEDULE',
),
true,
);
});
test('rejects plaintext endpoints, embedded credentials or missing retention', () => {
const report = auditCloudNativePgBackup({
root: ROOT,
readFile: intercept(
'deploy/kubernetes/ql3-cluster/components/barman-cloud-backup/object-store.s3.example.yaml',
(source) =>
source
.replace('retentionPolicy: 30d', 'retentionPolicy: 1d')
.replace('https://REPLACE_WITH_', 'http://REPLACE_WITH_')
.replace(
' configuration:',
' stringData:\n password: embedded\n configuration:',
),
),
});
assert.equal(report.compatible, false);
assert.equal(
report.findings.some(
(candidate) =>
candidate.code === 'QL3_CNPG_OBJECT_STORE_CONTRACT' ||
candidate.code === 'QL3_CNPG_OBJECT_STORE_SECRET_BOUNDARY',
),
true,
);
});
test('rejects in-place recovery or empty-WAL-archive bypass', () => {
const report = auditCloudNativePgBackup({
root: ROOT,
readFile: intercept(
'deploy/kubernetes/ql3-cluster/operations/cloudnative-pg-restore/restore-cluster.yaml',
(source) =>
source
.replace('name: ql3-postgres-restore', 'name: ql3-postgres')
.replace(
' labels:',
' annotations:\n cnpg.io/skipEmptyWalArchiveCheck: enabled\n labels:',
),
),
});
assert.equal(report.compatible, false);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_CNPG_RESTORE_ISOLATION',
),
true,
);
});
test('rejects source ObjectStore reuse as restored-cluster WAL destination', () => {
const report = auditCloudNativePgBackup({
root: ROOT,
readFile: intercept(
'deploy/kubernetes/ql3-cluster/operations/cloudnative-pg-restore/restore-cluster.yaml',
(source) =>
source.replace(
' postgresql:',
' plugins:\n - name: barman-cloud.cloudnative-pg.io\n isWALArchiver: true\n parameters:\n barmanObjectName: ql3-postgres-recovery-source\n postgresql:',
),
),
});
assert.equal(report.compatible, false);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_CNPG_RESTORE_ISOLATION',
),
true,
);
});
test('rejects legacy serverName authority inside the recovery ObjectStore', () => {
const report = auditCloudNativePgBackup({
root: ROOT,
readFile: intercept(
'deploy/kubernetes/ql3-cluster/operations/cloudnative-pg-restore/object-store.s3.example.yaml',
(source) =>
source.replace(
' s3Credentials:',
' serverName: ql3-postgres\n s3Credentials:',
),
),
});
assert.equal(report.compatible, false);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_CNPG_RECOVERY_SOURCE',
),
true,
);
});
@@ -0,0 +1,928 @@
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
assertCloudNativePgDrRunnerCapacity,
auditedCloudNativePgDrEvidence,
backupRuntimeEvidence,
certificateRotationEvidence,
digestOnlyReference,
imageIdDigest,
minioFixtureResources,
parseEvidenceReportPath,
platformDigestFromImageIndex,
preflightPrivateEvidenceReportPath,
privateDockerDataBindArgs,
postgresArchiverEvidence,
postgresClusterRuntimeEvidence,
postgresDatabaseContractEvidence,
postgresMigrationJobResource,
postgresMarkerEvidence,
postgresRestoreFixtureResources,
postgresRestoreApplicationProbeResources,
postgresRestoreApplicationRuntimeEvidence,
postgresRoleSecretResources,
postgresSourceFixtureResources,
postgresQueryJson,
postgresSql,
redactRuntimeText,
replaceExactlyOnce,
restoreMarkerEvidence,
reviewedManifest,
webhookConfigurationHasCaBundle,
writePrivateEvidenceReport,
} = require('../../scripts/ql3-cloudnativepg-barman-live-contract.cjs');
const ROOT = path.resolve(__dirname, '../..');
const POSTGRES_ROLES = [
'ql3_admin',
'ql3_ai_credential_manager',
'ql3_ai_credential_tester',
'ql3_ai_maintenance',
'ql3_approval_manager',
'ql3_automation_manager',
'ql3_migration',
'ql3_package_executor',
'ql3_package_manager',
'ql3_runtime',
'ql3_worker_credential_executor',
'ql3_worker_credential_manager',
'ql3_worker_ingress',
];
test('rejects an undersized DR runner before creating temporary cluster state', () => {
const gibibyte = 1024n * 1024n * 1024n;
assert.deepEqual(
assertCloudNativePgDrRunnerCapacity(() => ({
bavail: 35n,
bsize: gibibyte,
})),
{
minimumBytes: 35n * gibibyte,
availableBytes: 35n * gibibyte,
},
);
assert.throws(
() =>
assertCloudNativePgDrRunnerCapacity(() => ({
bavail: 34n,
bsize: gibibyte,
})),
/requires at least 35 GiB free; found 36507222016 bytes/,
);
});
test('extracts only an exact terminal Kubernetes platform image digest', () => {
const digest = `sha256:${'a'.repeat(64)}`;
assert.equal(imageIdDigest(`registry.example/image@${digest}`), digest);
assert.throws(() => imageIdDigest('registry.example/image:latest'));
assert.throws(() => imageIdDigest(`${digest}-suffix`));
});
test('removes a tag without confusing a registry port before Skopeo copy', () => {
const digest = `sha256:${'d'.repeat(64)}`;
assert.equal(
digestOnlyReference(`registry.example:5443/team/image:v1@${digest}`),
`registry.example:5443/team/image@${digest}`,
);
assert.equal(
digestOnlyReference(`registry.example:5443/team/image@${digest}`),
`registry.example:5443/team/image@${digest}`,
);
assert.throws(() => digestOnlyReference('registry.example/team/image:v1'));
});
test('redacts every runtime secret occurrence from failure diagnostics', () => {
assert.equal(
redactRuntimeText('token=secret; repeated=secret', ['secret']),
'token=[REDACTED]; repeated=[REDACTED]',
);
assert.equal(redactRuntimeText('safe', ['', undefined]), 'safe');
});
test('resolves exactly one reviewed platform child from an OCI image index', () => {
const amd64 = `sha256:${'a'.repeat(64)}`;
const arm64 = `sha256:${'b'.repeat(64)}`;
const index = {
manifests: [
{ digest: amd64, platform: { os: 'linux', architecture: 'amd64' } },
{ digest: arm64, platform: { os: 'linux', architecture: 'arm64' } },
{
digest: `sha256:${'c'.repeat(64)}`,
platform: { os: 'unknown', architecture: 'unknown' },
},
],
};
assert.equal(platformDigestFromImageIndex(index, 'arm64'), arm64);
assert.throws(() => platformDigestFromImageIndex(index, 's390x'));
assert.throws(() =>
platformDigestFromImageIndex(
{ manifests: [...index.manifests, index.manifests[1]] },
'arm64',
),
);
});
test('rewrites one reviewed release reference and rejects ambiguity', () => {
assert.equal(
replaceExactlyOnce(
'image: product:v1',
'product:v1',
'product@sha256:locked',
),
'image: product@sha256:locked',
);
assert.throws(() =>
replaceExactlyOnce(
'product:v1 product:v1',
'product:v1',
'product@sha256:locked',
),
);
assert.throws(() =>
replaceExactlyOnce(
'image: product:v2',
'product:v1',
'product@sha256:locked',
),
);
});
test('accepts only a checksum-bound regular manifest before pinning images', () => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-barman-unit-'));
const source = path.join(directory, 'source.yaml');
const target = path.join(directory, 'pinned.yaml');
try {
const manifest = `${'x'.repeat(1024)}\nimage: product:v1\n`;
fs.writeFileSync(source, manifest, { mode: 0o600, flag: 'wx' });
const digest = crypto.createHash('sha256').update(manifest).digest('hex');
const result = reviewedManifest(source, target, digest, [
['product:v1', `product:v1@sha256:${'b'.repeat(64)}`],
]);
assert.equal(result.sourceSha256, digest);
assert.match(fs.readFileSync(target, 'utf8'), /product:v1@sha256:b{64}/);
assert.throws(() =>
reviewedManifest(
source,
path.join(directory, 'rejected.yaml'),
'0'.repeat(64),
[],
),
);
} finally {
fs.rmSync(directory, { recursive: true, force: true });
}
});
test('publishes a private evidence report atomically without overwriting history', () => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-dr-report-'));
const target = path.join(directory, 'report.json');
const report = { schemaVersion: 1, fixture: 'test/evidence@v1' };
try {
assert.equal(
parseEvidenceReportPath([`--report=${target}`]),
path.normalize(target),
);
assert.equal(parseEvidenceReportPath([]), undefined);
assert.throws(() => parseEvidenceReportPath(['--report=relative.json']));
assert.throws(() =>
parseEvidenceReportPath([`--report=${target}`, '--unexpected']),
);
assert.equal(
preflightPrivateEvidenceReportPath(target),
path.join(fs.realpathSync(directory), 'report.json'),
);
const published = writePrivateEvidenceReport(target, report);
assert.equal(published.path, fs.realpathSync(target));
assert.match(published.sha256, /^sha256:[a-f0-9]{64}$/);
assert.deepEqual(JSON.parse(fs.readFileSync(target, 'utf8')), report);
assert.throws(
() => preflightPrivateEvidenceReportPath(target),
/refusing to overwrite/,
);
assert.equal(fs.statSync(target).mode & 0o077, 0);
assert.equal(
fs.readdirSync(directory).filter((name) => name.endsWith('.tmp')).length,
0,
);
assert.throws(
() => writePrivateEvidenceReport(target, { replaced: true }),
{
code: 'EEXIST',
},
);
assert.deepEqual(JSON.parse(fs.readFileSync(target, 'utf8')), report);
} finally {
fs.rmSync(directory, { recursive: true, force: true });
}
});
test('binds every image-declared data directory to private ephemeral storage', () => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-dr-data-'));
try {
const targets = [
'/var/lib/cni',
'/var/lib/kubelet',
'/var/lib/rancher/k3s',
'/var/log',
];
const args = privateDockerDataBindArgs(directory, 'ql3-test-node', targets);
assert.equal(args.length, targets.length * 2);
for (let index = 0; index < targets.length; index += 1) {
assert.equal(args[index * 2], '--mount');
const expectedDirectory = path.join(
directory,
'ql3-test-node',
`${String(index).padStart(2, '0')}-${path.basename(targets[index])}`,
);
assert.equal(
args[index * 2 + 1],
`type=bind,src=${expectedDirectory},dst=${targets[index]}`,
);
assert.equal(fs.statSync(expectedDirectory).mode & 0o777, 0o700);
}
const registryArgs = privateDockerDataBindArgs(
directory,
'ql3-test-registry',
['/var/lib/registry'],
);
assert.match(
registryArgs[1],
/type=bind,src=.*\/ql3-test-registry\/00-registry,dst=\/var\/lib\/registry$/,
);
assert.throws(() =>
privateDockerDataBindArgs('relative', 'ql3-test-node', targets),
);
assert.throws(() =>
privateDockerDataBindArgs(directory, '../escape', targets),
);
assert.throws(() =>
privateDockerDataBindArgs(directory, 'ql3-test-node', [
'/var/lib/unreviewed',
]),
);
assert.throws(() =>
privateDockerDataBindArgs(directory, 'ql3-test-node', [
'/var/log',
'/var/log',
]),
);
} finally {
fs.rmSync(directory, { recursive: true, force: true });
}
});
test('pins an explicitly reviewed repeated manifest reference count', () => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-barman-count-'));
const source = path.join(directory, 'source.yaml');
const target = path.join(directory, 'pinned.yaml');
try {
const manifest = `${'x'.repeat(
1024,
)}\nimage: product:v1\nenv: product:v1\n`;
fs.writeFileSync(source, manifest, { mode: 0o600, flag: 'wx' });
const digest = crypto.createHash('sha256').update(manifest).digest('hex');
reviewedManifest(source, target, digest, [
['product:v1', `product:v1@sha256:${'c'.repeat(64)}`, 2],
]);
assert.equal(
fs.readFileSync(target, 'utf8').match(/product:v1@sha256:c{64}/g)?.length,
2,
);
} finally {
fs.rmSync(directory, { recursive: true, force: true });
}
});
test('accepts cert-manager webhook readiness only after every CA bundle exists', () => {
const ready = {
webhooks: [
{ clientConfig: { caBundle: Buffer.from('ca-one').toString('base64') } },
{ clientConfig: { caBundle: Buffer.from('ca-two').toString('base64') } },
],
};
assert.equal(webhookConfigurationHasCaBundle(ready), true);
assert.equal(
webhookConfigurationHasCaBundle({
webhooks: [ready.webhooks[0], { clientConfig: {} }],
}),
false,
);
assert.equal(webhookConfigurationHasCaBundle({ webhooks: [] }), false);
});
test('builds a TLS object store fixture with separate writer and read-only recovery authority', () => {
const digest = `sha256:${'e'.repeat(64)}`;
const credentials = {
root: { accessKey: 'QL3ROOTTEST', secretKey: 'root-secret-value' },
writer: { accessKey: 'QL3WRITERTEST', secretKey: 'writer-secret-value' },
recovery: {
accessKey: 'QL3RECOVERYTEST',
secretKey: 'recovery-secret-value',
},
};
const fixture = minioFixtureResources({
minioImage: `registry:5000/ql3/minio@${digest}`,
clientImage: `registry:5000/ql3/minio-client@${digest}`,
credentials,
});
const serialized = JSON.stringify(fixture);
assert.doesNotMatch(serialized, /root-secret-value|writer-secret-value/);
assert.doesNotMatch(serialized, /recovery-secret-value/);
const byKindAndName = (kind, name) =>
fixture.core.items.find(
(item) => item.kind === kind && item.metadata?.name === name,
);
const writerSecret = byKindAndName('Secret', 'ql3-object-store-writer');
const recoverySecret = byKindAndName('Secret', 'ql3-object-store-recovery');
assert.notDeepEqual(writerSecret.data, recoverySecret.data);
const writerStore = byKindAndName('ObjectStore', 'ql3-postgres-backup');
const recoveryStore = byKindAndName(
'ObjectStore',
'ql3-postgres-recovery-source',
);
assert.equal(writerStore.spec.retentionPolicy, '30d');
assert.match(writerStore.spec.configuration.endpointURL, /^https:\/\//);
assert.equal(writerStore.spec.configuration.endpointCA.name, 'ql3-minio-ca');
assert.equal(
writerStore.spec.configuration.s3Credentials.accessKeyId.name,
'ql3-object-store-writer',
);
assert.equal(
recoveryStore.spec.configuration.s3Credentials.accessKeyId.name,
'ql3-object-store-recovery',
);
assert.equal(recoveryStore.spec.configuration.serverName, undefined);
const deployment = byKindAndName('Deployment', 'ql3-minio');
assert.deepEqual(
deployment.spec.template.spec.containers[0].resources.limits,
{ cpu: '500m', memory: '512Mi' },
);
assert.equal(
byKindAndName('Certificate', 'ql3-minio-server').spec.renewBefore,
'1h',
);
assert.doesNotMatch(serialized, /renewalBefore/);
const bootstrap = byKindAndName('Job', 'ql3-minio-bootstrap');
const bootstrapScript = bootstrap.spec.template.spec.containers[0].command[2];
assert.match(bootstrapScript, /mb --with-lock/);
assert.match(bootstrapScript, /retention set --default governance 30d/);
assert.match(bootstrapScript, /ilm rule add --expire-days 45/);
const verifierScript =
fixture.verifier.items[0].spec.template.spec.containers[0].command[2];
assert.match(verifierScript, /if mc cp/);
assert.match(verifierScript, /if mc rm/);
});
test('builds a constrained three-instance source cluster with one durable WAL authority', () => {
const digest = `sha256:${'f'.repeat(64)}`;
const fixture = postgresSourceFixtureResources({
postgresImage: `registry:5000/ql3/postgresql@${digest}`,
});
const { cluster, backup } = fixture;
assert.equal(cluster.spec.instances, 3);
assert.equal(cluster.spec.enableSuperuserAccess, false);
assert.equal(cluster.spec.imagePullPolicy, 'IfNotPresent');
assert.equal(cluster.spec.plugins.length, 1);
assert.deepEqual(cluster.spec.plugins[0], {
name: 'barman-cloud.cloudnative-pg.io',
isWALArchiver: true,
parameters: { barmanObjectName: 'ql3-postgres-backup' },
});
assert.equal(cluster.spec.backup, undefined);
assert.equal(
cluster.spec.postgresql.parameters.synchronous_commit,
'remote_apply',
);
assert.deepEqual(cluster.spec.postgresql.synchronous, {
method: 'any',
number: 1,
dataDurability: 'required',
failoverQuorum: true,
});
assert.deepEqual(cluster.spec.affinity, {
enablePodAntiAffinity: true,
podAntiAffinityType: 'required',
topologyKey: 'kubernetes.io/hostname',
});
assert.deepEqual(cluster.spec.resources.limits, {
cpu: '1',
memory: '512Mi',
});
assert.equal(cluster.spec.storage.size, '1Gi');
assert.equal(cluster.spec.walStorage.size, '512Mi');
assert.equal(backup.spec.method, 'plugin');
assert.equal(backup.spec.target, 'prefer-standby');
assert.equal(
backup.spec.pluginConfiguration.name,
'barman-cloud.cloudnative-pg.io',
);
const serialized = JSON.stringify(fixture);
assert.doesNotMatch(serialized, /"kind":"Secret"/);
assert.doesNotMatch(serialized, /"password":|secretKeyRef|secretAccessKey/);
assert.throws(() =>
postgresSourceFixtureResources({ postgresImage: 'postgres:latest' }),
);
});
test('builds all production DatabaseRole credentials without plaintext serialization', () => {
const credentials = Object.fromEntries(
POSTGRES_ROLES.map((role, index) => [
role,
`${String(index).padStart(2, '0')}${'A'.repeat(32)}`,
]),
);
const resources = postgresRoleSecretResources(credentials);
assert.equal(resources.kind, 'List');
assert.equal(resources.items.length, POSTGRES_ROLES.length);
assert.deepEqual(
resources.items.map((secret) =>
Buffer.from(secret.data.username, 'base64').toString('utf8'),
),
POSTGRES_ROLES,
);
assert.ok(
resources.items.every(
(secret) =>
secret.metadata.namespace === 'ql3-dr' &&
secret.type === 'kubernetes.io/basic-auth',
),
);
const serialized = JSON.stringify(resources);
for (const password of Object.values(credentials)) {
assert.doesNotMatch(serialized, new RegExp(password));
}
assert.throws(() =>
postgresRoleSecretResources({ ...credentials, unexpected: 'A'.repeat(32) }),
);
});
test('builds a digest-bound non-root migration Job using the production CLI', () => {
const digest = `sha256:${'7'.repeat(64)}`;
const image = `ql3-barman-dr-123-abcdef-registry:5000/ql3/cluster-control@${digest}`;
const job = postgresMigrationJobResource({ controlImage: image });
const pod = job.spec.template.spec;
const container = pod.containers[0];
assert.equal(job.metadata.namespace, 'ql3-dr');
assert.equal(job.spec.backoffLimit, 0);
assert.equal(pod.automountServiceAccountToken, false);
assert.equal(pod.securityContext.runAsNonRoot, true);
assert.equal(container.image, image);
assert.equal(container.imagePullPolicy, 'IfNotPresent');
assert.deepEqual(container.command, [
'node',
'/opt/qinglong/node_modules/@qinglong/cluster-postgres/dist/migration/migrationCli.js',
]);
assert.equal(container.securityContext.readOnlyRootFilesystem, true);
assert.deepEqual(container.securityContext.capabilities.drop, ['ALL']);
assert.equal(
container.env.find(({ name }) => name === 'QL3_POSTGRES_TLS_MODE').value,
'verify-full',
);
assert.equal(
container.env.find(({ name }) => name === 'QL3_POSTGRES_MIGRATION_PASSWORD')
.valueFrom.secretKeyRef.name,
'ql3-postgres-migration-auth',
);
assert.doesNotMatch(JSON.stringify(job), /ql3_migration_test|postgres:\/\//);
assert.throws(() =>
postgresMigrationJobResource({ controlImage: 'cluster-control:latest' }),
);
});
test('builds a production application readiness probe for each isolated restore', () => {
const digest = `sha256:${'6'.repeat(64)}`;
const image = `ql3-barman-dr-123-abcdef-registry:5000/ql3/cluster-control@${digest}`;
const pepper = Buffer.alloc(32, 5).toString('base64url');
const resources = postgresRestoreApplicationProbeResources({
clusterName: 'ql3-postgres-restore-latest',
controlImage: image,
apiCredentialPepper: pepper,
});
const secret = resources.items.find(({ kind }) => kind === 'Secret');
const deployment = resources.items.find(({ kind }) => kind === 'Deployment');
const pod = deployment.spec.template.spec;
const container = pod.containers[0];
assert.equal(secret.metadata.name, 'ql3-dr-application-latest-security');
assert.doesNotMatch(JSON.stringify(resources), new RegExp(pepper));
assert.equal(deployment.spec.replicas, 1);
assert.equal(pod.automountServiceAccountToken, false);
assert.equal(container.image, image);
assert.equal(container.securityContext.readOnlyRootFilesystem, true);
assert.equal(
container.env.find(({ name }) => name === 'QL3_WORKER_INGRESS_ENABLED')
.value,
'false',
);
assert.equal(
container.env.find(({ name }) => name === 'QL3_POSTGRES_RUNTIME_HOST')
.value,
'ql3-postgres-restore-latest-rw.ql3-dr.svc',
);
assert.equal(
pod.volumes.find(({ name }) => name === 'postgres-ca').secret.secretName,
'ql3-postgres-restore-latest-ca',
);
assert.equal(container.readinessProbe.httpGet.path, '/readyz');
assert.throws(() =>
postgresRestoreApplicationProbeResources({
clusterName: 'ql3-postgres',
controlImage: image,
apiCredentialPepper: pepper,
}),
);
});
test('requires the restored production application Pod to pass its real readiness probe', () => {
const clusterName = 'ql3-postgres-restore-pitr';
const name = 'ql3-dr-application-pitr';
const deployment = {
metadata: { name },
spec: { replicas: 1 },
status: { availableReplicas: 1, readyReplicas: 1 },
};
const pod = {
metadata: {
name: `${name}-abc`,
labels: {
'app.kubernetes.io/name': name,
'ql3.cloud/restore-cluster': clusterName,
},
},
status: { conditions: [{ type: 'Ready', status: 'True' }] },
};
assert.equal(
postgresRestoreApplicationRuntimeEvidence(deployment, [pod], clusterName)
.ready,
true,
);
assert.equal(
postgresRestoreApplicationRuntimeEvidence(
{ ...deployment, status: { availableReplicas: 0, readyReplicas: 0 } },
[pod],
clusterName,
).ready,
false,
);
});
test('accepts only the complete production schema owner and non-elevated role catalog', () => {
const roles = POSTGRES_ROLES.map((name) => ({
name,
login: true,
superuser: false,
createdb: false,
createrole: false,
replication: false,
bypassrls: false,
}));
const input = {
migrationCount: '52',
controlCoreCapability: '51',
databaseOwner: 'ql3_migration',
postgresVersionNumber: '180004',
roles,
};
const evidence = postgresDatabaseContractEvidence(input);
assert.equal(evidence.migrationCount, 52);
assert.equal(evidence.controlCoreCapability, 51);
assert.equal(evidence.databaseOwner, 'ql3_migration');
assert.equal(evidence.postgresVersionNumber, 180004);
assert.deepEqual(
evidence.roles.map(({ name }) => name),
POSTGRES_ROLES,
);
assert.equal(Object.hasOwn(evidence.roles[0], 'login'), false);
assert.throws(() =>
postgresDatabaseContractEvidence({ ...input, migrationCount: 51 }),
);
assert.throws(() =>
postgresDatabaseContractEvidence({
...input,
roles: roles.map((role, index) =>
index === 0 ? { ...role, superuser: true } : role,
),
}),
);
});
test('builds isolated latest and PITR restores from one read-only source authority', () => {
const digest = `sha256:${'9'.repeat(64)}`;
const postgresImage = `registry:5000/ql3/postgresql@${digest}`;
const latest = postgresRestoreFixtureResources({
postgresImage,
clusterName: 'ql3-postgres-restore-latest',
});
const targetTime = '2026-08-04T00:00:00.123Z';
const pitr = postgresRestoreFixtureResources({
postgresImage,
clusterName: 'ql3-postgres-restore-pitr',
targetTime,
});
for (const restore of [latest, pitr]) {
assert.equal(restore.spec.instances, 3);
assert.equal(restore.spec.enableSuperuserAccess, false);
assert.equal(restore.spec.plugins, undefined);
assert.equal(
restore.spec.postgresql.parameters.synchronous_commit,
'remote_apply',
);
assert.equal(restore.spec.postgresql.synchronous.number, 1);
assert.equal(restore.spec.affinity.podAntiAffinityType, 'required');
assert.equal(restore.spec.externalClusters.length, 1);
const plugin = restore.spec.externalClusters[0].plugin;
assert.equal(plugin.name, 'barman-cloud.cloudnative-pg.io');
assert.deepEqual(plugin.parameters, {
barmanObjectName: 'ql3-postgres-recovery-source',
serverName: 'ql3-postgres',
});
}
assert.deepEqual(latest.spec.bootstrap.recovery, {
source: 'ql3-postgres-origin',
});
assert.deepEqual(pitr.spec.bootstrap.recovery, {
source: 'ql3-postgres-origin',
recoveryTarget: { targetTime },
});
assert.throws(() =>
postgresRestoreFixtureResources({
postgresImage,
clusterName: 'ql3-postgres',
}),
);
assert.throws(() =>
postgresRestoreFixtureResources({
postgresImage,
clusterName: 'ql3-postgres-restore-pitr',
}),
);
});
test('accepts only a three-node ready source cluster with a live primary', () => {
const pod = (ordinal, node, ready = true) => ({
metadata: {
name: `ql3-postgres-${ordinal}`,
labels: { 'cnpg.io/cluster': 'ql3-postgres' },
},
spec: { nodeName: node },
status: {
conditions: [{ type: 'Ready', status: ready ? 'True' : 'False' }],
},
});
const cluster = {
metadata: { name: 'ql3-postgres' },
spec: { instances: 3 },
status: { currentPrimary: 'ql3-postgres-1', readyInstances: 3 },
};
const pods = [pod(1, 'node-a'), pod(2, 'node-b'), pod(3, 'node-c')];
const ready = postgresClusterRuntimeEvidence(cluster, pods);
assert.equal(ready.ready, true);
assert.deepEqual(ready.value.nodes, ['node-a', 'node-b', 'node-c']);
assert.equal(
postgresClusterRuntimeEvidence(cluster, [
pods[0],
pods[1],
pod(3, 'node-b'),
]).ready,
false,
);
assert.equal(
postgresClusterRuntimeEvidence(
{ ...cluster, status: { ...cluster.status, readyInstances: 2 } },
pods,
).ready,
false,
);
});
test('accepts only a completed plugin backup with bounded WAL evidence', () => {
const completed = {
status: {
method: 'plugin',
phase: 'completed',
instanceID: { podName: 'ql3-postgres-2' },
backupId: '20260804T010203',
beginWal: '00000001000000000000000A',
endWal: '00000001000000000000000B',
startedAt: '2026-08-04T01:02:03Z',
stoppedAt: '2026-08-04T01:02:05Z',
},
};
const result = backupRuntimeEvidence(completed);
assert.equal(result.ready, true);
assert.equal(result.value.method, 'plugin');
assert.equal(
backupRuntimeEvidence({
status: { ...completed.status, method: 'barmanObjectStore' },
}).ready,
false,
);
assert.equal(
backupRuntimeEvidence({
status: { ...completed.status, phase: 'failed' },
}).ready,
false,
);
});
test('accepts certificate rotation only after serial, Secret and revision advance', () => {
const previous = {
serialSha256: `sha256:${'1'.repeat(64)}`,
resourceVersion: '10',
};
const current = {
serialSha256: `sha256:${'2'.repeat(64)}`,
resourceVersion: '12',
};
assert.deepEqual(certificateRotationEvidence(previous, current, 1, 2), {
previousSerialSha256: previous.serialSha256,
currentSerialSha256: current.serialSha256,
previousSecretResourceVersion: '10',
currentSecretResourceVersion: '12',
});
assert.throws(() => certificateRotationEvidence(previous, previous, 1, 2));
assert.throws(() => certificateRotationEvidence(previous, current, 2, 2));
});
test('accepts WAL archiving only after a successful archived segment', () => {
assert.deepEqual(
postgresArchiverEvidence({
archivedCount: '2',
failedCount: '0',
lastArchivedWal: '00000001000000000000000C',
lastArchivedTime: '2026-08-04T01:02:06Z',
}),
{
archivedCount: 2,
failedCount: 0,
lastArchivedWal: '00000001000000000000000C',
lastArchivedTime: '2026-08-04T01:02:06Z',
},
);
assert.throws(() =>
postgresArchiverEvidence({
archivedCount: '0',
failedCount: '0',
lastArchivedWal: '',
lastArchivedTime: '',
}),
);
});
test('executes PostgreSQL evidence queries without a network credential', () => {
const calls = [];
const kubectl = (args, options) => {
calls.push({ args, options });
return { stdout: '{"ok":true}' };
};
assert.deepEqual(
postgresQueryJson(
kubectl,
'ql3-dr',
'ql3-postgres-1',
`SELECT '{"ok":true}'`,
),
{ ok: true },
);
assert.equal(
postgresSql(kubectl, 'ql3-dr', 'ql3-postgres-1', 'SELECT 1'),
'{"ok":true}',
);
assert.equal(calls.length, 2);
assert.ok(calls[0].args.includes('--username'));
assert.ok(calls[0].args.includes('postgres'));
assert.equal(calls[0].args.includes('--password'), false);
assert.deepEqual(calls[0].options, { capture: true, quiet: true });
assert.throws(() =>
postgresSql(kubectl, 'ql3-dr', 'unexpected-pod', 'SELECT 1'),
);
});
test('distinguishes latest and PITR marker boundaries exactly', () => {
assert.deepEqual(
restoreMarkerEvidence(
{ beforeMarkerPresent: true, afterMarkerPresent: true },
true,
),
{ beforeMarkerPresent: true, afterMarkerPresent: true },
);
assert.deepEqual(
restoreMarkerEvidence(
{ beforeMarkerPresent: true, afterMarkerPresent: false },
false,
),
{ beforeMarkerPresent: true, afterMarkerPresent: false },
);
assert.throws(() =>
restoreMarkerEvidence(
{ beforeMarkerPresent: false, afterMarkerPresent: false },
false,
),
);
});
test('accepts only content-free UUID, timestamp and WAL marker evidence', () => {
assert.deepEqual(
postgresMarkerEvidence({
id: '123e4567-e89b-42d3-a456-426614174001',
createdAt: '2026-08-04T01:02:03.123456Z',
wal: '00000001000000000000000D',
}),
{
id: '123e4567-e89b-42d3-a456-426614174001',
createdAt: '2026-08-04T01:02:03.123456Z',
wal: '00000001000000000000000D',
},
);
assert.throws(() =>
postgresMarkerEvidence({
id: 'before-base-backup',
createdAt: '2026-08-04T01:02:03Z',
wal: 'not-wal',
}),
);
assert.throws(() => auditedCloudNativePgDrEvidence({}));
});
test('keeps the destructive live path opt-in and isolated by prefix', () => {
const source = fs.readFileSync(
path.join(ROOT, 'scripts/ql3-cloudnativepg-barman-live-contract.cjs'),
'utf8',
);
assert.match(source, /QL3_CLOUDNATIVEPG_BARMAN_LIVE !== '1'/);
assert.ok(
source.indexOf(' assertCloudNativePgDrRunnerCapacity();') <
source.indexOf(' const temporary = fs.mkdtempSync('),
);
assert.match(source, /ql3-barman-dr-/);
assert.doesNotMatch(source, /ql3-cnpg-evidence-control-plane/);
assert.doesNotMatch(source, /apiservice\/v1\.webhook\.cert-manager\.io/);
assert.match(
source,
/imagePullPolicy: Always'[\s\S]{0,80}imagePullPolicy: IfNotPresent'/,
);
assert.match(source, /registry:2@sha256:[a-f0-9]{64}/);
assert.match(source, /skopeo\/stable:v1\.20\.0@sha256:[a-f0-9]{64}/);
assert.match(source, /Docker-Content-Digest|inspection\.Digest/);
assert.doesNotMatch(source, /imagePullPolicy: Never/);
assert.doesNotMatch(source, /'images',\s*'import'/);
assert.doesNotMatch(source, /reviewed-images\.tar/);
assert.match(source, /run\(docker, \['rm', '-f', '-v', container\], \{/);
assert.doesNotMatch(source, /run\(docker, \['rm', '-f', container\], \{/);
assert.match(source, /REGISTRY_DATA_TARGETS = Object\.freeze/);
assert.match(source, /K3S_DATA_TARGETS = Object\.freeze/);
assert.match(
source,
/io\.qinglong\.ql3\.live=cloudnativepg-barman-disaster-recovery/,
);
assert.match(source, /io\.qinglong\.ql3\.run/);
assert.match(source, /\['network', 'create', \.\.\.dockerLabels, network\]/);
assert.match(
source,
/privateDockerDataBindArgs\(\s*dockerDataRoot,\s*registry,\s*REGISTRY_DATA_TARGETS/,
);
assert.match(
source,
/privateDockerDataBindArgs\(dockerDataRoot, node, K3S_DATA_TARGETS\)/,
);
assert.match(source, /pg_stat_archiver/);
assert.match(source, /SELECT pg_switch_wal\(\)/);
assert.match(source, /backupRuntimeEvidence/);
assert.match(source, /plugin-barman-cloud/);
assert.match(source, /prefer-standby backup unexpectedly ran on the primary/);
assert.match(source, /privateKey: \{ rotationPolicy: 'Always' \}/);
assert.match(source, /Barman mutual TLS certificate rotation/);
assert.match(source, /post-rotation plugin base backup completion/);
assert.match(source, /database-roles\.yaml/);
assert.match(
source,
/@qinglong\/cluster-postgres\/dist\/migration\/migrationCli\.js/,
);
assert.match(source, /"event":"migration_completed"/);
assert.match(source, /ALTER DATABASE qinglong OWNER TO ql3_migration/);
assert.match(source, /postgresDatabaseContractEvidence/);
assert.match(source, /production application readiness/);
assert.match(source, /latestApplicationRtoSeconds/);
assert.match(source, /pitrApplicationRtoSeconds/);
assert.match(source, /ql3-postgres-restore-latest/);
assert.match(source, /ql3-postgres-restore-pitr/);
assert.match(source, /sourceClusterAfterRestores\.metadata\.uid/);
assert.match(source, /cloudnativepg-disaster-recovery@v1/);
assert.match(source, /auditedCloudNativePgDrEvidence/);
assert.match(source, /schemaAndRoles: true/);
assert.equal(
source.match(/`eviction-hard=\$\{K3S_EVICTION_HARD\}`/g)?.length,
2,
);
assert.match(
source,
/memory\.available<100Mi,nodefs\.available<64Mi,imagefs\.available<64Mi,nodefs\.inodesFree<1%/,
);
});
@@ -0,0 +1,70 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { test } = require('node:test');
const ROOT = path.resolve(__dirname, '../..');
const WORKFLOW = fs.readFileSync(
path.join(ROOT, '.github/workflows/ql3-cloudnativepg-dr-live.yml'),
'utf8',
);
const RUNNER = fs.readFileSync(
path.join(ROOT, 'scripts/ql3-cloudnativepg-barman-live-contract.cjs'),
'utf8',
);
test('keeps the expensive CloudNativePG DR evidence gate manual and exact', () => {
assert.match(WORKFLOW, /^on:\n workflow_dispatch:\s*$/m);
assert.doesNotMatch(WORKFLOW, /^ (?:push|pull_request|schedule):/m);
assert.match(WORKFLOW, /runs-on: ubuntu-24\.04/);
assert.match(WORKFLOW, /timeout-minutes: 120/);
assert.match(WORKFLOW, /at least 35 GiB free/);
assert.match(WORKFLOW, /node-version: '24\.18\.0'/);
assert.match(WORKFLOW, /version: '8\.3\.1'/);
assert.match(WORKFLOW, /kubectl v1\.32\.8/);
assert.match(
WORKFLOW,
/cert-manager\/cert-manager\/releases\/download\/v1\.20\.3/,
);
assert.match(WORKFLOW, /plugin-barman-cloud\/releases\/download\/v0\.13\.0/);
assert.match(WORKFLOW, /cloudnative-pg\/releases\/download\/v1\.30\.0/);
assert.match(WORKFLOW, /QL3_CLOUDNATIVEPG_BARMAN_LIVE: '1'/);
assert.match(WORKFLOW, /QL3_SOURCE_REVISION: \$\{\{ github\.sha \}\}/);
assert.match(WORKFLOW, /"--report=\$\{QL3_DR_REPORT\}"/);
assert.match(WORKFLOW, /stat -c '%a'.*= '600'/);
assert.match(WORKFLOW, /audit:cloudnativepg-dr-evidence:ql3/);
assert.match(
WORKFLOW,
/test\/back\/ql3CloudNativePgBarmanWorkflow\.test\.cjs/,
);
assert.match(
WORKFLOW,
/actions\/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a/,
);
assert.match(WORKFLOW, /if-no-files-found: error/);
assert.match(WORKFLOW, /retention-days: 14/);
assert.match(WORKFLOW, /overwrite: false/);
});
test('fails closed on runner resource leaks without pruning shared Docker state', () => {
assert.match(WORKFLOW, /ql3-dangling-volumes\.before/);
assert.match(WORKFLOW, /ql3-dangling-volumes\.after/);
assert.match(WORKFLOW, /diff --unified/);
assert.match(WORKFLOW, /docker ps -aq --filter name=ql3-barman-dr-/);
assert.match(WORKFLOW, /docker network ls -q --filter name=ql3-barman-dr-/);
assert.match(
WORKFLOW,
/docker ps -aq --filter label=io\.qinglong\.ql3\.live=cloudnativepg-barman-disaster-recovery/,
);
assert.match(
WORKFLOW,
/docker network ls -q --filter label=io\.qinglong\.ql3\.live=cloudnativepg-barman-disaster-recovery/,
);
assert.doesNotMatch(WORKFLOW, /(?:system|builder|volume) prune/);
assert.doesNotMatch(WORKFLOW, /continue-on-error/);
assert.doesNotMatch(WORKFLOW, /ql3-cnpg-evidence-control-plane/);
assert.match(
RUNNER,
/run\(docker, \['rm', '-f', '-v', container\], \{[\s\S]*?allowFailure: true/,
);
});
@@ -0,0 +1,197 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { test } = require('node:test');
const {
auditCloudNativePgDeployment,
} = require('../../scripts/ql3-cloudnativepg-deployment-audit.cjs');
const ROOT = path.resolve(__dirname, '../..');
function intercept(relativePath, transform) {
const target = path.join(ROOT, relativePath);
return (filePath, encoding) => {
const source = fs.readFileSync(filePath, encoding);
return path.resolve(filePath) === target ? transform(source) : source;
};
}
test('accepts the locked CloudNativePG HA and authority profile', () => {
const report = auditCloudNativePgDeployment({ root: ROOT });
assert.equal(report.compatible, true, JSON.stringify(report.findings));
assert.equal(report.operatorVersion, '1.30.0');
assert.equal(report.postgresqlVersion, '18.4');
assert.equal(report.instances, 3);
assert.deepEqual(report.roles, [
'ql3_admin',
'ql3_ai_credential_manager',
'ql3_ai_credential_tester',
'ql3_ai_maintenance',
'ql3_approval_manager',
'ql3_automation_manager',
'ql3_migration',
'ql3_package_executor',
'ql3_package_manager',
'ql3_runtime',
'ql3_worker_credential_executor',
'ql3_worker_credential_manager',
'ql3_worker_ingress',
]);
});
test('rejects a missing or rewritten operator release manifest digest', () => {
for (const transform of [
(source) => source.replace(/\s+"releaseManifestSha256": "[^"]+",/, ''),
(source) =>
source.replace(
/"releaseManifestSha256": "[^"]+"/,
`"releaseManifestSha256": "sha256:${'0'.repeat(64)}"`,
),
]) {
const report = auditCloudNativePgDeployment({
root: ROOT,
readFile: intercept(
'deploy/kubernetes/ql3-cluster/operators/cloudnative-pg/operator-lock.json',
transform,
),
});
assert.equal(report.compatible, false);
assert.ok(
report.findings.some(({ code }) => code === 'QL3_CNPG_SUPPLY_CHAIN_LOCK'),
);
}
});
test('rejects a single instance or unpinned PostgreSQL operand', () => {
const report = auditCloudNativePgDeployment({
root: ROOT,
readFile: intercept(
'deploy/kubernetes/ql3-cluster/operators/cloudnative-pg/cluster.yaml',
(source) =>
source
.replace('instances: 3', 'instances: 1')
.replace(
/imageName: .+/,
'imageName: ghcr.io/cloudnative-pg/postgresql:18.4-minimal-trixie',
),
),
});
assert.equal(report.compatible, false);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_CNPG_CLUSTER_BASELINE',
),
true,
);
});
test('rejects a privileged database role', () => {
const report = auditCloudNativePgDeployment({
root: ROOT,
readFile: intercept(
'deploy/kubernetes/ql3-cluster/operators/cloudnative-pg/database-roles.yaml',
(source) => source.replace('superuser: false', 'superuser: true'),
),
});
assert.equal(report.compatible, false);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_CNPG_DATABASE_ROLE',
),
true,
);
});
test('rejects runtime DSN authority or a non-primary endpoint', () => {
const report = auditCloudNativePgDeployment({
root: ROOT,
readFile: intercept(
'deploy/kubernetes/ql3-cluster/overlays/cloudnative-pg/postgres-runtime-patch.yaml',
(source) =>
source
.replace('$patch: delete', 'value: postgres://embedded-secret')
.replace(
'ql3-postgres-rw.qinglong3-system.svc',
'ql3-postgres-ro.qinglong3-system.svc',
),
),
});
assert.equal(report.compatible, false);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_CNPG_RUNTIME_BINDING',
),
true,
);
});
test('rejects migration credentials or CA from the runtime domain', () => {
const report = auditCloudNativePgDeployment({
root: ROOT,
readFile: intercept(
'deploy/kubernetes/ql3-cluster/operations/cloudnative-pg/migrate-job-patch.yaml',
(source) =>
source
.replaceAll(
'ql3-postgres-migration-auth',
'ql3-postgres-runtime-auth',
)
.replace('value: ql3-postgres-ca', 'value: runtime-ca')
.replace('value: ca.crt', 'value: postgres-ca.crt'),
),
});
assert.equal(report.compatible, false);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_CNPG_MIGRATION_BINDING',
),
true,
);
});
test('rejects a deployable tag or missing image transform in either application path', () => {
for (const [relativePath, transform, findingCode] of [
[
'deploy/kubernetes/ql3-cluster/overlays/cloudnative-pg/kustomization.yaml',
(source) =>
source.replace(
/\s+digest: sha256:[0-9a-f]{64}/,
'\n newTag: latest',
),
'QL3_CNPG_RUNTIME_BINDING',
],
[
'deploy/kubernetes/ql3-cluster/operations/cloudnative-pg/kustomization.yaml',
(source) => source.replace(/\nimages:[\s\S]*?(?=\npatches:)/, '\n'),
'QL3_CNPG_MIGRATION_BINDING',
],
]) {
const report = auditCloudNativePgDeployment({
root: ROOT,
readFile: intercept(relativePath, transform),
});
assert.equal(report.compatible, false);
assert.ok(report.findings.some(({ code }) => code === findingCode));
}
});
test('rejects applying credential examples through kustomize', () => {
const report = auditCloudNativePgDeployment({
root: ROOT,
readFile: intercept(
'deploy/kubernetes/ql3-cluster/operators/cloudnative-pg/kustomization.yaml',
(source) =>
source.replace(
' - database.yaml',
' - database.yaml\n - credentials.example.yaml',
),
),
});
assert.equal(report.compatible, false);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_CNPG_SECRET_APPLICATION_BOUNDARY',
),
true,
);
});
@@ -0,0 +1,412 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { execFileSync } = require('node:child_process');
const { test } = require('node:test');
const {
validateCloudNativePgDrEvidence,
validateCloudNativePgDrReleaseEvidence,
} = require('../../scripts/ql3-cloudnativepg-dr-evidence-audit.cjs');
const {
parseArguments: parseReleaseGateArguments,
} = require('../../scripts/ql3-cloudnativepg-dr-release-gate.cjs');
const SOURCE_REVISION = 'a'.repeat(40);
function hash(character) {
return `sha256:${character.repeat(64)}`;
}
function roles() {
return [
'ql3_admin',
'ql3_ai_credential_manager',
'ql3_ai_credential_tester',
'ql3_ai_maintenance',
'ql3_approval_manager',
'ql3_automation_manager',
'ql3_migration',
'ql3_package_executor',
'ql3_package_manager',
'ql3_runtime',
'ql3_worker_credential_executor',
'ql3_worker_credential_manager',
'ql3_worker_ingress',
].map((name) => ({
name,
superuser: false,
createdb: false,
createrole: false,
replication: false,
bypassrls: false,
}));
}
function restore(cluster, afterMarkerPresent) {
return {
cluster,
sourceObjectStore: 'ql3-postgres-recovery-source',
sourceServerName: 'ql3-postgres',
sourceClusterUnmodified: true,
targetWalArchiver: false,
instances: 3,
ready: true,
migrationCount: 54,
controlCoreCapability: 53,
databaseOwner: 'ql3_migration',
synchronousCommit: 'remote_apply',
synchronousStandbys: 1,
roles: roles(),
beforeMarkerPresent: true,
afterMarkerPresent,
};
}
function validReport() {
return {
schemaVersion: 1,
fixture: 'qinglong/cloudnativepg-disaster-recovery@v1',
observedAt: '2026-07-24T12:30:00.000Z',
sourceRevision: SOURCE_REVISION,
platform: {
kubernetesVersion: '1.32.8',
architecture: 'amd64',
cloudNativePgVersion: '1.30.0',
cloudNativePgImageId: hash('1'),
postgresVersionNumber: 180004,
postgresImageId: hash('2'),
barmanVersion: '0.13.0',
barmanControllerImageId:
'sha256:417449fe4f6f0a56acdeb30e4131930815f2b46b9afeb808059b57aa8b4c2ef5',
barmanSidecarImageIds: [
'sha256:15cb1a01e7c5235eedac2061cab8208e5f7c39dbda292f9c2d4ddaa0c1f211e6',
'sha256:15cb1a01e7c5235eedac2061cab8208e5f7c39dbda292f9c2d4ddaa0c1f211e6',
'sha256:15cb1a01e7c5235eedac2061cab8208e5f7c39dbda292f9c2d4ddaa0c1f211e6',
],
certManagerVersion: '1.20.3',
certManagerImageIds: [
'sha256:1e4af57beb469cc3bb0fb48b9201caea2723819b9ffd3c3ea98568f55b4dd38b',
'sha256:a2b12d27950d1603d2c8168c3ccd95d07b93ce6ec4b530316196a31db592a9c0',
'sha256:953a97df613f7da7eda8ce4b1c8d8e6b50963db0800fab595d040db6eb5cb060',
],
},
source: {
cluster: 'ql3-postgres',
backup: {
name: 'ql3-dr-backup-20260724',
phase: 'completed',
startedAt: '2026-07-24T12:00:00.000Z',
completedAt: '2026-07-24T12:05:00.000Z',
beginWal: '000000010000000000000001',
endWal: '000000010000000000000002',
},
markers: {
before: {
id: '123e4567-e89b-42d3-a456-426614174001',
createdAt: '2026-07-24T12:01:00.000Z',
wal: '000000010000000000000001',
},
after: {
id: '123e4567-e89b-42d3-a456-426614174002',
createdAt: '2026-07-24T12:10:00.000Z',
wal: '000000010000000000000003',
},
},
wal: {
archiveHealthy: true,
continuous: true,
noGaps: true,
lastArchivedWal: '000000010000000000000004',
},
},
latestRestore: restore('ql3-postgres-restore-latest', true),
pitrRestore: {
...restore('ql3-postgres-restore-pitr', false),
targetTime: '2026-07-24T12:06:00.000Z',
},
certificateRotation: {
client: {
previousSerialSha256: hash('7'),
currentSerialSha256: hash('8'),
previousSecretResourceVersion: '101',
currentSecretResourceVersion: '102',
},
server: {
previousSerialSha256: hash('9'),
currentSerialSha256: hash('a'),
previousSecretResourceVersion: '201',
currentSecretResourceVersion: '202',
},
walArchivedDuringRotation: true,
backupCompletedAfterRotation: true,
latestRestoreCompletedAfterRotation: true,
pitrCompletedAfterRotation: true,
maxObservedInterruptionSeconds: 2.4,
},
objectStoreAuthority: {
sourceObjectStore: 'ql3-postgres-backup',
recoveryObjectStore: 'ql3-postgres-recovery-source',
sourceWriterIdentitySha256: hash('b'),
recoveryReaderIdentitySha256: hash('c'),
recoveryReadOnly: true,
versioning: true,
immutability: true,
lifecycleDays: 30,
},
serviceLevels: {
targetMaxRpoSeconds: 60,
observedRpoSeconds: 5,
targetMaxDatabaseRtoSeconds: 1200,
latestDatabaseRtoSeconds: 410,
pitrDatabaseRtoSeconds: 470,
targetMaxApplicationRtoSeconds: 1800,
latestApplicationRtoSeconds: 520,
pitrApplicationRtoSeconds: 590,
},
gates: {
latestRestore: true,
pointInTimeRestore: true,
schemaAndRoles: true,
sourceIsolation: true,
certificateRotation: true,
serviceLevels: true,
passed: true,
},
};
}
test('accepts complete latest, PITR, rotation and service-level evidence', () => {
const report = validateCloudNativePgDrEvidence(validReport());
assert.equal(report.compatible, true, JSON.stringify(report.findings));
});
test('accepts fresh disaster-recovery evidence bound to the release source', () => {
const report = validateCloudNativePgDrReleaseEvidence(validReport(), {
sourceCommit: SOURCE_REVISION,
releaseVersion: '3.0.0-rc.1',
nowMs: Date.parse('2026-07-24T13:00:00.000Z'),
});
assert.equal(report.compatible, true, JSON.stringify(report.findings));
assert.equal(report.maximumAgeSeconds, 86_400);
});
test('rejects stale or source-detached disaster-recovery release evidence', () => {
const report = validateCloudNativePgDrReleaseEvidence(validReport(), {
sourceCommit: 'b'.repeat(40),
releaseVersion: '3.0.0',
nowMs: Date.parse('2026-07-26T12:30:01.000Z'),
});
assert.equal(
report.findings.some(({ code }) => code === 'QL3_DR_RELEASE_SOURCE'),
true,
);
assert.equal(
report.findings.some(({ code }) => code === 'QL3_DR_RELEASE_FRESHNESS'),
true,
);
});
test('release gate accepts only one exact private-report argument set', () => {
assert.deepEqual(
parseReleaseGateArguments([
'--report=/run/qinglong3-release-evidence/a/report.json',
`--source-commit=${SOURCE_REVISION}`,
'--release-version=3.0.0',
]),
{
reportPath: '/run/qinglong3-release-evidence/a/report.json',
sourceCommit: SOURCE_REVISION,
releaseVersion: '3.0.0',
},
);
assert.throws(
() =>
parseReleaseGateArguments([
'--report=relative.json',
`--source-commit=${SOURCE_REVISION}`,
'--release-version=3.0.0',
]),
/must be absolute/,
);
});
test('release gate CLI accepts a fresh private regular report', (t) => {
const directory = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-dr-release-gate-'),
);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
fs.chmodSync(directory, 0o700);
const report = validReport();
report.observedAt = new Date().toISOString();
const reportPath = path.join(directory, 'cloudnativepg-dr-evidence.json');
fs.writeFileSync(reportPath, `${JSON.stringify(report)}\n`, { mode: 0o600 });
const output = execFileSync(
process.execPath,
[
path.resolve(
__dirname,
'../../scripts/ql3-cloudnativepg-dr-release-gate.cjs',
),
`--report=${reportPath}`,
`--source-commit=${SOURCE_REVISION}`,
'--release-version=3.0.0-rc.1',
],
{ encoding: 'utf8' },
);
assert.equal(JSON.parse(output).compatible, true);
fs.chmodSync(reportPath, 0o700);
assert.throws(
() =>
execFileSync(
process.execPath,
[
path.resolve(
__dirname,
'../../scripts/ql3-cloudnativepg-dr-release-gate.cjs',
),
`--report=${reportPath}`,
`--source-commit=${SOURCE_REVISION}`,
'--release-version=3.0.0-rc.1',
],
{ encoding: 'utf8' },
),
/mode-0600 regular file/,
);
});
test('rejects a restore report pinned to the obsolete schema and role set', () => {
const input = validReport();
input.latestRestore.migrationCount = 17;
input.latestRestore.controlCoreCapability = 16;
input.latestRestore.roles = input.latestRestore.roles.filter(
({ name }) =>
!name.startsWith('ql3_worker_credential_') &&
name !== 'ql3_automation_manager',
);
const report = validateCloudNativePgDrEvidence(input);
assert.equal(
report.findings.some(({ code }) => code === 'QL3_DR_LATEST_RESTORE'),
true,
);
});
test('rejects credential or private material in the report', () => {
const input = validReport();
input.objectStoreAuthority.password = 'must-not-appear';
const report = validateCloudNativePgDrEvidence(input);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_DR_SECRET_EXPOSURE',
),
true,
);
});
test('rejects latest restore without both durable markers', () => {
const input = validReport();
input.latestRestore.afterMarkerPresent = false;
const report = validateCloudNativePgDrEvidence(input);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_DR_LATEST_RESTORE',
),
true,
);
});
test('rejects PITR outside the marker window or containing the later marker', () => {
const input = validReport();
input.pitrRestore.targetTime = '2026-07-24T12:11:00.000Z';
input.pitrRestore.afterMarkerPresent = true;
const report = validateCloudNativePgDrEvidence(input);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_DR_PITR_RESTORE',
),
true,
);
});
test('rejects schema, role or restored-cluster write-authority drift', () => {
const input = validReport();
input.latestRestore.controlCoreCapability = 15;
input.latestRestore.roles[0].superuser = true;
input.latestRestore.targetWalArchiver = true;
const report = validateCloudNativePgDrEvidence(input);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_DR_LATEST_RESTORE',
),
true,
);
});
test('rejects shared object-store identities or weakened retention', () => {
const input = validReport();
input.objectStoreAuthority.recoveryReaderIdentitySha256 =
input.objectStoreAuthority.sourceWriterIdentitySha256;
input.objectStoreAuthority.lifecycleDays = 7;
const report = validateCloudNativePgDrEvidence(input);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_DR_OBJECT_STORE_AUTHORITY',
),
true,
);
});
test('rejects certificate rotation without new identities and continued recovery', () => {
const input = validReport();
input.certificateRotation.client.currentSerialSha256 =
input.certificateRotation.client.previousSerialSha256;
input.certificateRotation.pitrCompletedAfterRotation = false;
const report = validateCloudNativePgDrEvidence(input);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_DR_CERTIFICATE_ROTATION',
),
true,
);
});
test('rejects observed RPO or RTO above deployment targets', () => {
const input = validReport();
input.serviceLevels.observedRpoSeconds = 61;
input.serviceLevels.pitrApplicationRtoSeconds = 1801;
const report = validateCloudNativePgDrEvidence(input);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_DR_SERVICE_LEVELS',
),
true,
);
});
test('rejects a summary that hides an independently failed gate', () => {
const input = validReport();
input.gates.pointInTimeRestore = false;
const report = validateCloudNativePgDrEvidence(input);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_DR_GATE_SUMMARY',
),
true,
);
});
test('rejects cert-manager image count, order or digest drift', () => {
const input = validReport();
input.platform.certManagerImageIds[0] = hash('d');
input.platform.certManagerImageIds.push(hash('e'));
const report = validateCloudNativePgDrEvidence(input);
assert.equal(
report.findings.some(
(candidate) => candidate.code === 'QL3_DR_PLATFORM_PROVENANCE',
),
true,
);
});
@@ -0,0 +1,208 @@
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 {
imageDigest,
imageTag,
localApplicationManifest,
manifestSha256,
reviewedOperatorManifest,
verifyImageIds,
} = require('../../scripts/ql3-cloudnativepg-live-contract.cjs');
const INDEX = `sha256:${'a'.repeat(64)}`;
const PLATFORM = `sha256:${'b'.repeat(64)}`;
function pods(...imageIds) {
return imageIds.map((imageID) => ({
status: { containerStatuses: [{ imageID }] },
}));
}
test('extracts only one exact digest-pinned image reference', () => {
assert.equal(
imageDigest(`registry.example/operand:18.4@${INDEX}`),
INDEX,
);
assert.throws(() => imageDigest('registry.example/operand:18.4'));
assert.throws(() => imageDigest(`registry.example/operand@${INDEX}:tag`));
});
test('derives a normal tagged preload reference from a reviewed image', () => {
assert.equal(
imageTag(`registry.example:5000/operand:18.4@${INDEX}`),
'registry.example:5000/operand:18.4',
);
assert.throws(() => imageTag(`registry.example/operand@${INDEX}`));
assert.throws(() => imageTag('registry.example/operand:18.4'));
});
test('replaces exactly one fail-closed application image only in live rendering', () => {
const placeholder = `registry.example.com/qinglong/qinglong3-cluster-control@sha256:${'0'.repeat(64)}`;
const rendered = `kind: Deployment\nspec:\n image: ${placeholder}\n`;
assert.equal(
localApplicationManifest(rendered),
'kind: Deployment\nspec:\n image: registry.example.com/qinglong/qinglong3-cluster-control:3.0.0-alpha.0\n',
);
assert.throws(() => localApplicationManifest('kind: Deployment\n'));
assert.throws(() =>
localApplicationManifest(`${rendered}---\n${rendered}`),
);
});
test('accepts uniform runtime reporting of the reviewed index or platform digest', () => {
assert.deepEqual(
verifyImageIds(
pods(`registry.example/operand@${INDEX}`),
[INDEX, PLATFORM],
'operand',
),
[`registry.example/operand@${INDEX}`],
);
assert.deepEqual(
verifyImageIds(
pods(`registry.example/operand@${PLATFORM}`),
[INDEX, PLATFORM],
'operand',
),
[`registry.example/operand@${PLATFORM}`],
);
});
test('rejects tags, unknown digests, missing status and widened reviewed sets', () => {
for (const invoke of [
() => verifyImageIds(pods('registry.example/operand:18.4'), [INDEX], 'operand'),
() =>
verifyImageIds(
pods(`registry.example/operand@sha256:${'c'.repeat(64)}`),
[INDEX, PLATFORM],
'operand',
),
() => verifyImageIds([{ status: {} }], [INDEX], 'operand'),
() => verifyImageIds(pods(`registry.example/operand@${INDEX}`), ['*'], 'operand'),
]) {
assert.throws(invoke);
}
});
test('creates the namespaced control identity before the migration Job', () => {
const source = fs.readFileSync(
path.resolve(__dirname, '../../scripts/ql3-cloudnativepg-live-contract.cjs'),
'utf8',
);
const namespace = source.indexOf(
"'deploy/kubernetes/ql3-cluster/base/namespace.yaml'",
);
const serviceAccount = source.indexOf(
"'deploy/kubernetes/ql3-cluster/base/service-account.yaml'",
);
const migration = source.indexOf(
"'deploy/kubernetes/ql3-cluster/operations/cloudnative-pg'",
);
assert.ok(namespace >= 0);
assert.ok(serviceAccount > namespace);
assert.ok(migration > serviceAccount);
assert.match(
source.slice(namespace, serviceAccount),
/kubectl\(\[/,
);
assert.match(
source.slice(namespace, migration),
/'-n',\s*NAMESPACE,\s*'apply',\s*'-f',\s*'deploy\/kubernetes\/ql3-cluster\/base\/service-account\.yaml'/,
);
});
test('provisions the fail-closed worker ingress identity and derives all role evidence from one set', () => {
const source = fs.readFileSync(
path.resolve(__dirname, '../../scripts/ql3-cloudnativepg-live-contract.cjs'),
'utf8',
);
assert.match(source, /const roleList = ROLE_NAMES\.map/);
assert.match(source, /WHERE rolname IN \(\$\{roleList\}\)/);
assert.match(source, /assert\.deepEqual\(schema, \['53', '52'\]\)/);
assert.match(source, /migrationCount: 54/);
assert.match(source, /contractVersion: 53/);
assert.match(source, /createWorkerIngressTls\(tempDirectory\)/);
for (const key of [
'worker-credential-pepper',
'artifact-s3-bucket',
'artifact-s3-region',
'artifact-s3-encryption',
'tls.key',
'tls.crt',
'client-ca.crt',
]) {
assert.ok(source.includes(`'${key}'`));
}
assert.match(source, /basicConstraints=critical,CA:TRUE/);
assert.match(source, /extendedKeyUsage=serverAuth/);
assert.match(source, /subjectAltName=DNS:ql3-cluster-control/);
});
test('preloads both lock-owned images before applying the operator manifest', () => {
const source = fs.readFileSync(
path.resolve(__dirname, '../../scripts/ql3-cloudnativepg-live-contract.cjs'),
'utf8',
);
const preload = source.indexOf(
'for (const reviewedImage of [OPERATOR_IMAGE, POSTGRES_IMAGE])',
);
const manifest = source.indexOf(
"'download official CloudNativePG 1.30.0 release manifest'",
);
assert.ok(preload >= 0);
assert.ok(manifest > preload);
const contract = source.slice(preload, manifest);
assert.match(contract, /docker\(\['pull', reviewedImage\]\)/);
assert.match(contract, /imageDigest\(reviewedImage\)/);
assert.match(
contract,
/const preloadTag = imageTag\(reviewedImage\)/,
);
assert.match(
contract,
/docker\(\['tag', reviewedImage, preloadTag\]\)/,
);
assert.match(
contract,
/kind\(\['load', 'docker-image', preloadTag, '--name', clusterName\]\)/,
);
assert.doesNotMatch(
contract,
/kind\(\['load', 'docker-image', reviewedImage/,
);
});
test('rejects a canonical but checksum-unreviewed operator manifest', () => {
const directory = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-cnpg-manifest-test-')),
);
const candidate = path.join(directory, 'operator.yaml');
try {
fs.writeFileSync(candidate, 'x'.repeat(2048), { mode: 0o600 });
assert.match(manifestSha256(candidate), /^sha256:[a-f0-9]{64}$/);
assert.throws(
() => reviewedOperatorManifest(candidate),
/reviewed lock digest/,
);
} finally {
fs.rmSync(directory, { recursive: true, force: true });
}
});
test('bounds remote manifest retries and removes disposable temporary state', () => {
const source = fs.readFileSync(
path.resolve(__dirname, '../../scripts/ql3-cloudnativepg-live-contract.cjs'),
'utf8',
);
assert.match(source, /'--http1\.1'/);
assert.match(source, /'--retry-max-time',\s*'300'/);
assert.match(source, /reviewedOperatorManifest\(downloadedOperatorManifest\)/);
assert.match(
source,
/fs\.rmSync\(tempDirectory, \{ recursive: true, force: true \}\)/,
);
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,575 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const {
auditClusterImageCiWorkflow,
auditClusterImageRelease,
auditReleaseWorkflow,
} = require('../../scripts/ql3-cluster-image-release-audit.cjs');
const root = path.resolve(__dirname, '../..');
const ciSource = fs.readFileSync(
path.join(root, '.github/workflows/ql3-ci.yml'),
'utf8',
);
const releaseSource = fs.readFileSync(
path.join(root, '.github/workflows/ql3-image-release.yml'),
'utf8',
);
test('accepts the reviewed native CI and digest release contracts', () => {
assert.deepEqual(auditClusterImageRelease(root), {
ci: {
images: ['control', 'control-ai', 'admin', 'local'],
nativeArchitectures: ['amd64', 'arm64'],
runtimeInventory: true,
ociAttestations: true,
osVulnerabilityScan: {
scanner: 'trivy@0.70.0',
severities: ['HIGH', 'CRITICAL'],
packageTypes: ['os'],
ignoreUnfixed: false,
},
},
release: {
trigger: 'explicit protected v3 tag dispatch',
workerManagementEvidence: {
sourceAware: true,
privateEphemeralRunner: true,
maximumAgeSeconds: 86400,
artifactUpload: false,
},
cloudNativePgDisasterRecoveryEvidence: {
sourceAware: true,
privateEphemeralRunner: true,
maximumAgeSeconds: 86400,
artifactUpload: false,
staticLocksReaudited: true,
},
osVulnerabilityScan: {
scanner: 'trivy@0.70.0',
actionCommit: 'ed142fd0673e97e23eac54620cfb913e5ce36c25',
platforms: ['linux/amd64', 'linux/arm64'],
severities: ['HIGH', 'CRITICAL'],
packageTypes: ['os'],
ignoreUnfixed: false,
maximumExceptionDays: 30,
buildOnce: true,
immutableArtifactRetentionDays: 1,
attestedToPublishedDigest: true,
},
images: ['control', 'control-ai', 'admin', 'local'],
platforms: ['linux/amd64', 'linux/arm64'],
keylessSignature: true,
buildkitAttestations: ['sbom', 'provenance'],
githubAttestations: ['provenance', 'sbom', 'os-vulnerability'],
publication: {
copier: 'regctl@0.11.5',
copierSha256:
'c93aa7638749f5aaac1a8e01787321889c78f0101809bb2880343478d0ba0467',
rebuildAfterScan: false,
tagAfterVerification: true,
},
localRolloutPreflight: true,
localRolloutApply: true,
postPublishVerification: [
'manifest',
'cosign',
'provenance',
'cyclonedx',
'os-vulnerability',
'release-tags',
],
},
});
});
test('rejects removal of the native arm64 image gate', () => {
const mutated = ciSource.replace(
'runner: ubuntu-24.04-arm\n node_arch: arm64\n image_arch: arm64\n image: control',
'runner: none\n node_arch: arm64\n image_arch: arm64\n image: control',
);
assert.throws(
() => auditClusterImageCiWorkflow(mutated),
/matrices must contain only exact/,
);
});
test('rejects removal of the native cluster-admin image gate', () => {
const mutated = ciSource.replace(
'image_arch: arm64\n image: admin',
'image_arch: arm64\n image: disabled',
);
assert.throws(
() => auditClusterImageCiWorkflow(mutated),
/matrices must contain only exact/,
);
});
test('rejects an additional unreviewed CI image authority', () => {
const mutated = ciSource.replace(
' target: runtime\n steps:',
' target: runtime\n - runner: ubuntu-24.04\n node_arch: x64\n image_arch: amd64\n image: unreviewed\n dockerfile: unreviewed/Dockerfile\n target: runtime\n steps:',
);
assert.throws(
() => auditClusterImageCiWorkflow(mutated),
/matrices must contain only exact/,
);
});
test('rejects removal of the attested OCI evidence job', () => {
const mutated = ciSource.replace(' image-oci:', ' image-oci-disabled:');
assert.throws(
() => auditClusterImageCiWorkflow(mutated),
/matrices must contain only exact/,
);
});
test('rejects a movable Trivy action in native image CI', () => {
const mutated = ciSource.replace(
'aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0',
'aquasecurity/trivy-action@v0.36.0',
);
assert.throws(
() => auditClusterImageCiWorkflow(mutated),
/exact pinned OS-only Trivy failure gate/,
);
});
test('rejects native image CI that hides unfixed OS vulnerabilities', () => {
const mutated = ciSource.replace(
" ignore-unfixed: 'false'",
" ignore-unfixed: 'true'",
);
assert.throws(
() => auditClusterImageCiWorkflow(mutated),
/exact pinned OS-only Trivy failure gate/,
);
});
test('rejects a release missing an architecture', () => {
const mutated = releaseSource.replace(
'--arm64-layout=${RUNNER_TEMP}/native/arm64/layout',
'--arm64-layout=${RUNNER_TEMP}/native/amd64/layout',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/merge and re-audit the two scanned layouts/,
);
});
test('rejects an automatic tag push that bypasses private evidence review', () => {
const mutated = releaseSource.replace(
'on:\n workflow_dispatch:',
"on:\n push:\n tags:\n - 'v3.*'\n workflow_dispatch:",
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/explicit protected-tag dispatch/,
);
});
test('rejects release publication without the private evidence dependency', () => {
const mutated = releaseSource.replace(
' - worker-management-release-evidence',
' - worker-management-release-evidence-disabled',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/depend on both protected ephemeral private evidence jobs/,
);
});
test('rejects release publication without the OS vulnerability dependency', () => {
const mutated = releaseSource.replace(
' - os-vulnerability',
' - os-vulnerability-disabled',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/depend on both protected ephemeral private evidence jobs/,
);
});
test('rejects release publication without current disaster-recovery evidence', () => {
const mutated = releaseSource.replace(
' - cluster-dr-release-evidence',
' - cluster-dr-release-evidence-disabled',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/depend on both protected ephemeral private evidence jobs/,
);
});
test('rejects disaster-recovery evidence detached from the release source', () => {
const marker = '--source-commit="${GITHUB_SHA}"';
const first = releaseSource.indexOf(marker);
const second = releaseSource.indexOf(marker, first + marker.length);
assert.notEqual(first, -1);
assert.notEqual(second, -1);
const mutated = `${releaseSource.slice(
0,
second,
)}--source-commit="detached"${releaseSource.slice(second + marker.length)}`;
assert.throws(
() => auditReleaseWorkflow(mutated),
/bind the exact report to the release identity/,
);
});
test('rejects an incomplete native OS vulnerability architecture matrix', () => {
const mutated = releaseSource.replace(
' - image: local\n runner: ubuntu-24.04-arm\n node_arch: arm64\n image_arch: arm64\n dockerfile: deploy/containers/ql3-local-application/Dockerfile',
' - image: local\n runner: ubuntu-24.04-arm\n node_arch: arm64\n image_arch: disabled\n dockerfile: deploy/containers/ql3-local-application/Dockerfile',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/OS vulnerability matrix must scan exact/,
);
});
test('rejects a movable Trivy action after the upstream supply-chain incident', () => {
const mutated = releaseSource.replace(
'aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0',
'aquasecurity/trivy-action@v0.36.0',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/build once, scan that exact OCI layout/,
);
});
test('rejects hiding unfixed high or critical OS vulnerabilities', () => {
const mutated = releaseSource.replace(
" ignore-unfixed: 'false'",
" ignore-unfixed: 'true'",
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/build once, scan that exact OCI layout/,
);
});
test('rejects widening the OS exception policy to application libraries', () => {
const mutated = releaseSource.replace(
" vuln-type: 'os'",
" vuln-type: 'os,library'",
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/build once, scan that exact OCI layout/,
);
});
test('rejects persistent scanner cache in the privileged release workflow', () => {
const mutated = releaseSource.replace(
" cache: 'false'",
" cache: 'true'",
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/build once, scan that exact OCI layout/,
);
});
test('rejects a reusable private evidence runner', () => {
const mutated = releaseSource.replace(
'ql3-release-evidence-ephemeral',
'ql3-release-evidence-persistent',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/protected ephemeral private evidence job/,
);
});
test('rejects private evidence uploaded as a workflow artifact', () => {
const mutated = releaseSource.replace(
' - name: Re-audit commit-scoped private production evidence',
' - uses: actions/upload-artifact@v4\n - name: Re-audit commit-scoped private production evidence',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/only the reviewed|never persist private evidence/,
);
});
test('rejects a private evidence path not scoped to the release commit', () => {
const mutated = releaseSource.replace(
'/run/qinglong3-release-evidence/${GITHUB_SHA}',
'/run/qinglong3-release-evidence/current',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/commit-scoped runner mount/,
);
});
test('rejects write authority in the private evidence job', () => {
const mutated = releaseSource.replace(
' timeout-minutes: 10\n permissions:\n contents: read',
' timeout-minutes: 10\n permissions:\n packages: write',
);
assert.throws(() => auditReleaseWorkflow(mutated), /keep evidence read-only/);
});
test('rejects a release missing the standalone digest signature', () => {
const mutated = releaseSource.replace(
'cosign sign --yes "${IMAGE}@${DIGEST}"',
'cosign version',
);
assert.throws(() => auditReleaseWorkflow(mutated), /keylessly sign/);
});
test('rejects a release missing application SBOM attestation', () => {
const mutated = releaseSource.replace(
'sbom-path: ${{ runner.temp }}/${{ matrix.repository }}.cdx.json',
'show-summary: true',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/reviewed application SBOM/,
);
});
test('rejects a release missing the independent admin image', () => {
const mutated = releaseSource.replace(
'- image: admin\n repository: qinglong3-cluster-admin\n runtime_root: deploy/containers/ql3-cluster-admin/runtime-dependencies',
'- image: admin-disabled\n repository: qinglong3-cluster-admin\n runtime_root: deploy/containers/ql3-cluster-admin/runtime-dependencies',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/matrix must contain only exact/,
);
});
test('rejects a release missing the AI-excluded local image', () => {
const mutated = releaseSource.replace(
'- image: local\n repository: qinglong3-local-application\n runtime_root: deploy/containers/ql3-local-application/runtime-dependencies',
'- image: local-disabled\n repository: qinglong3-local-application\n runtime_root: deploy/containers/ql3-local-application/runtime-dependencies',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/matrix must contain only exact/,
);
});
test('rejects an additional repository in the privileged release matrix', () => {
const mutated = releaseSource.replace(
' runtime_root: deploy/containers/ql3-local-application/runtime-dependencies\n steps:',
' runtime_root: deploy/containers/ql3-local-application/runtime-dependencies\n - image: unreviewed\n repository: unreviewed\n dockerfile: Dockerfile\n runtime_root: unreviewed\n steps:',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/matrix must contain only exact/,
);
});
test('rejects release publication without the vulnerability gate', () => {
const mutated = releaseSource.replace(
' --audit-level=high',
' --audit-level=none',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/high or critical production dependency advisories/,
);
});
test('rejects a release that does not select the image-specific SBOM', () => {
const mutated = releaseSource.replace(
'node scripts/ql3-cluster-image-sbom.cjs\n --image=${{ matrix.image }}',
'node scripts/ql3-cluster-image-sbom.cjs\n --image=control',
);
assert.throws(() => auditReleaseWorkflow(mutated), /selected image SBOM/);
});
test('rejects a release with reduced OIDC authority', () => {
const mutated = releaseSource.replace('id-token: write', 'id-token: read');
assert.throws(() => auditReleaseWorkflow(mutated), /grant writes only/);
});
test('rejects a movable action tag in the privileged release job', () => {
const pinned =
'actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6';
const offset = releaseSource.lastIndexOf(pinned);
const mutated = `${releaseSource.slice(
0,
offset,
)}actions/checkout@v6${releaseSource.slice(offset + pinned.length)}`;
assert.throws(
() => auditReleaseWorkflow(mutated),
/privileged publisher|immutable checkout action/,
);
});
test('rejects removal of the published manifest audit', () => {
const mutated = releaseSource.replace(
'node scripts/ql3-cluster-remote-manifest-audit.cjs',
'node --version',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/published digest manifest/,
);
});
test('rejects a local release without both live rollout Profiles', () => {
const mutated = releaseSource.replace(
' --profile=standalone',
' --profile=edge',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/apply and stop both Profiles/,
);
});
test('rejects signature verification without exact certificate identity', () => {
const mutated = releaseSource.replace(
'--certificate-identity "${certificate_identity}"',
'--certificate-identity-regexp ".*"',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/exact keyless workflow identity/,
);
});
test('rejects a GitHub attestation verification without source binding', () => {
const mutated = releaseSource.replace(
'--source-digest "${GITHUB_SHA}"',
'--source-digest "movable"',
);
assert.throws(() => auditReleaseWorkflow(mutated), /bind the source commit/);
});
test('rejects CycloneDX verification without the exact predicate type', () => {
const mutated = releaseSource.replace(
'--predicate-type "https://cyclonedx.org/bom"',
'--predicate-type "https://example.invalid/sbom"',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/CycloneDX predicate type/,
);
});
test('rejects scanning a daemon tag instead of the exact OCI tar', () => {
const mutated = releaseSource.replace(
' input: ${{ runner.temp }}/ql3-native/image.oci.tar',
' image-ref: movable-candidate:latest',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/build once, scan that exact OCI layout tar/,
);
});
test('rejects retaining scanned native release artifacts for more than one day', () => {
const mutated = releaseSource.replace(
' retention-days: 1',
' retention-days: 2',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/upload only bound immutable evidence/,
);
});
test('rejects overwrite authority on a scanned native artifact', () => {
const mutated = releaseSource.replace(
' overwrite: false',
' overwrite: true',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/upload only bound immutable evidence/,
);
});
test('rejects a movable upload-artifact action', () => {
const mutated = releaseSource.replace(
'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1',
'actions/upload-artifact@v7.0.1',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/upload only bound immutable evidence/,
);
});
test('rejects downloading an artifact not bound to the same run attempt', () => {
const mutated = releaseSource.replace(
'ql3-release-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.image }}-amd64',
'ql3-release-${{ github.run_id }}-${{ matrix.image }}-amd64',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/same-run scanned native artifacts/,
);
});
test('rejects any privileged rebuild after the native scan', () => {
const mutated = releaseSource.replace(
' node scripts/ql3-image-release-bundle.cjs \\\n --mode=merge',
' docker build .\n node scripts/ql3-image-release-bundle.cjs \\\n --mode=merge',
);
assert.throws(() => auditReleaseWorkflow(mutated), /without any rebuild/);
});
test('rejects a checksum drift in the exact OCI copier', () => {
const mutated = releaseSource.replace(
'c93aa7638749f5aaac1a8e01787321889c78f0101809bb2880343478d0ba0467',
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/checksum-pin the exact regctl OCI copier/,
);
});
test('rejects importing the scanned graph through an unverified tag', () => {
const mutated = releaseSource.replace(
'image import "${IMAGE}@${DIGEST}" "${ARCHIVE}"',
'image import "${IMAGE}:candidate" "${ARCHIVE}"',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/import the audited OCI graph by digest/,
);
});
test('rejects an image tag created before digest verification completes', () => {
const mutated = releaseSource.replace(
' "${REGCTL}" image import "${IMAGE}@${DIGEST}" "${ARCHIVE}"',
' "${REGCTL}" image copy "${IMAGE}@${DIGEST}" "${IMAGE}:early"\n "${REGCTL}" image import "${IMAGE}@${DIGEST}" "${ARCHIVE}"',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/before final tag promotion/,
);
});
test('rejects any publisher step after immutable tag promotion', () => {
const mutated = `${releaseSource}\n - name: Post-promotion mutation\n run: echo unsafe\n`;
assert.throws(
() => auditReleaseWorkflow(mutated),
/before final tag promotion/,
);
});
test('rejects removal of the digest-bound OS vulnerability attestation', () => {
const mutated = releaseSource.replace(
'predicate-type: https://qinglong.dev/attestations/image-os-vulnerability/v1',
'predicate-type: https://example.invalid/not-os-evidence',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/digest-bound OS vulnerability evidence/,
);
});
+247
View File
@@ -0,0 +1,247 @@
'use strict';
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 {
auditClusterImageSbom,
componentRef,
createClusterImageSbom,
} = require('../../scripts/ql3-cluster-image-sbom.cjs');
const root = path.resolve(__dirname, '../..');
function clone(value) {
return JSON.parse(JSON.stringify(value));
}
test('generates the exact reviewed cluster image runtime closure', () => {
const document = createClusterImageSbom({ root });
const report = auditClusterImageSbom(document, { root });
assert.deepEqual(report, {
image: 'control',
root: 'pkg:npm/%40qinglong/cluster-control-image-dependencies@3.0.0-alpha.0',
components: 46,
externalComponents: 43,
internalComponents: 3,
dependencyNodes: 47,
inventoryVerified: false,
});
assert.equal(
document.components.some((component) => component.name === 'pg-protocol'),
true,
);
assert.equal(
document.components.some((component) => component.name === 'typescript'),
false,
);
});
test('generates the optional Cluster AI image runtime closure', () => {
const document = createClusterImageSbom({ root, image: 'control-ai' });
const report = auditClusterImageSbom(document, {
root,
image: 'control-ai',
});
assert.deepEqual(report, {
image: 'control-ai',
root: 'pkg:npm/%40qinglong/cluster-control-image-dependencies@3.0.0-alpha.0',
components: 47,
externalComponents: 43,
internalComponents: 4,
dependencyNodes: 48,
inventoryVerified: false,
});
assert.equal(
document.components.some((component) => component.name === '@qinglong/ai'),
true,
);
});
test('generates the independent reviewed cluster-admin image closure', () => {
const document = createClusterImageSbom({ root, image: 'admin' });
const report = auditClusterImageSbom(document, {
root,
image: 'admin',
});
assert.deepEqual(report, {
image: 'admin',
root: 'pkg:npm/%40qinglong/cluster-admin-image-dependencies@3.0.0-alpha.0',
components: 88,
externalComponents: 84,
internalComponents: 4,
dependencyNodes: 89,
inventoryVerified: false,
});
assert.equal(
document.components.some(
(component) => component.name === '@kubernetes/client-node',
),
true,
);
assert.equal(
document.components.some(
(component) => component.name === '@aws-sdk/client-s3',
),
false,
);
});
test('generates the AI-excluded local application image closure', () => {
const document = createClusterImageSbom({ root, image: 'local' });
const report = auditClusterImageSbom(document, {
root,
image: 'local',
});
assert.deepEqual(report, {
image: 'local',
root: 'pkg:npm/%40qinglong/local-application-image@3.0.0-alpha.0',
components: 10,
externalComponents: 2,
internalComponents: 8,
dependencyNodes: 11,
inventoryVerified: false,
});
assert.deepEqual(
document.components
.filter((component) =>
['@qinglong/ai', 'drizzle-orm', 'typescript'].includes(component.name),
)
.map((component) => component.name),
[],
);
});
test('rejects a control SBOM presented as cluster-admin evidence', () => {
const document = createClusterImageSbom({ root });
assert.throws(
() =>
auditClusterImageSbom(document, {
root,
image: 'admin',
}),
/selected image profile|image manifest/,
);
});
test('rejects widened metadata and root component drift', () => {
const widened = createClusterImageSbom({ root });
widened.metadata.generatedBy = 'unreviewed';
assert.throws(
() => auditClusterImageSbom(widened, { root }),
/root component/,
);
const drifted = createClusterImageSbom({ root });
drifted.metadata.component.purl = 'pkg:npm/unrelated@1.0.0';
assert.throws(
() => auditClusterImageSbom(drifted, { root }),
/root component/,
);
});
test('rejects a missing internal dependency edge', () => {
const document = createClusterImageSbom({ root });
const controlRef = componentRef('@qinglong/cluster-control', '3.0.0-alpha.0');
const edge = document.dependencies.find((entry) => entry.ref === controlRef);
edge.dependsOn = edge.dependsOn.slice(1);
assert.throws(
() => auditClusterImageSbom(document, { root }),
/dependency edges.*reviewed runtime closure/,
);
});
test('rejects an unexpected development component', () => {
const document = createClusterImageSbom({ root });
document.components.push({
type: 'library',
'bom-ref': componentRef('typescript', '5.9.3'),
name: 'typescript',
version: '5.9.3',
purl: componentRef('typescript', '5.9.3'),
});
assert.throws(
() => auditClusterImageSbom(document, { root }),
/development component leaked/,
);
});
test('rejects tampered locked component metadata', () => {
const document = createClusterImageSbom({ root });
const pg = document.components.find((component) => component.name === 'pg');
pg.version = '8.21.0';
assert.throws(
() => auditClusterImageSbom(document, { root }),
/component metadata differs/,
);
});
test('rejects a missing or unreviewed runtime license', () => {
const missing = createClusterImageSbom({ root, image: 'local' });
delete missing.components[0].licenses;
assert.throws(
() => auditClusterImageSbom(missing, { root, image: 'local' }),
/unreviewed license/,
);
const unreviewed = createClusterImageSbom({ root, image: 'local' });
unreviewed.components[0].licenses = [{ license: { id: 'GPL-3.0-only' } }];
assert.throws(
() => auditClusterImageSbom(unreviewed, { root, image: 'local' }),
/unreviewed license/,
);
});
test('verifies a bounded package inventory against the SBOM', (t) => {
const document = createClusterImageSbom({ root });
const temporaryRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-image-inventory-'),
);
t.after(() => fs.rmSync(temporaryRoot, { recursive: true, force: true }));
for (const component of document.components) {
const packageDirectory = component.name.startsWith('@')
? path.join(temporaryRoot, ...component.name.split('/'))
: path.join(temporaryRoot, component.name);
fs.mkdirSync(packageDirectory, { recursive: true });
fs.writeFileSync(
path.join(packageDirectory, 'package.json'),
JSON.stringify({
name: component.name,
version: component.version,
}),
);
}
assert.equal(
auditClusterImageSbom(document, {
root,
inventoryRoot: temporaryRoot,
}).inventoryVerified,
true,
);
const unexpected = path.join(temporaryRoot, 'unexpected');
fs.mkdirSync(unexpected);
fs.writeFileSync(
path.join(unexpected, 'package.json'),
JSON.stringify({ name: 'unexpected', version: '1.0.0' }),
);
assert.throws(
() =>
auditClusterImageSbom(clone(document), {
root,
inventoryRoot: temporaryRoot,
}),
/runtime image package inventory differs/,
);
});
+550
View File
@@ -0,0 +1,550 @@
'use strict';
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');
const {
auditClusterOciLayout,
} = require('../../scripts/ql3-cluster-oci-layout-audit.cjs');
const {
mergeNativeLayouts,
nativeEvidenceRecord,
} = require('../../scripts/ql3-image-release-bundle.cjs');
const {
createClusterImageSbom,
} = require('../../scripts/ql3-cluster-image-sbom.cjs');
const root = path.resolve(__dirname, '../..');
const revision = 'fixture-revision';
function createFixture(t, options = {}) {
const image = options.image || 'control';
const isControl = image === 'control' || image === 'control-ai';
const isControlAi = image === 'control-ai';
const isLocal = image === 'local';
const layoutRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-oci-layout-'));
t.after(() => fs.rmSync(layoutRoot, { recursive: true, force: true }));
const blobDirectory = path.join(layoutRoot, 'blobs', 'sha256');
fs.mkdirSync(blobDirectory, { recursive: true });
fs.writeFileSync(
path.join(layoutRoot, 'oci-layout'),
JSON.stringify({ imageLayoutVersion: '1.0.0' }),
);
function blob(value, mediaType) {
const content = Buffer.isBuffer(value)
? value
: Buffer.from(JSON.stringify(value));
const digest = `sha256:${crypto
.createHash('sha256')
.update(content)
.digest('hex')}`;
fs.writeFileSync(
path.join(blobDirectory, digest.slice('sha256:'.length)),
content,
);
return { mediaType, digest, size: content.length };
}
const expectedComponents = createClusterImageSbom({
root,
image,
}).components;
const applicationPackages = expectedComponents.map((component) => ({
name: component.name,
versionInfo: component.version,
sourceInfo: `acquired package info from installed node module manifest file: /opt/qinglong/node_modules/${component.name}/package.json`,
externalRefs: [
{
referenceCategory: 'PACKAGE-MANAGER',
referenceType: 'purl',
referenceLocator: component.purl,
},
],
}));
if (options.addDevelopmentPackage) {
applicationPackages.push({
name: '@types/node',
versionInfo: '24.13.3',
sourceInfo:
'acquired package info from installed node module manifest file: /opt/qinglong/node_modules/@types/node/package.json',
externalRefs: [
{
referenceCategory: 'PACKAGE-MANAGER',
referenceType: 'purl',
referenceLocator: 'pkg:npm/%40types/node@24.13.3',
},
],
});
}
const descriptors = [];
const imageDescriptors = [];
for (const architecture of options.onlyArchitecture
? [options.onlyArchitecture]
: options.omitArm64
? ['amd64']
: ['amd64', 'arm64']) {
const config = blob(
{
architecture,
os: 'linux',
config: {
User:
options.rootArm64 && architecture === 'arm64'
? '0:0'
: isLocal
? '65532:65532'
: '10001:10001',
...(isControl ? { ExposedPorts: { '5800/tcp': {} } } : {}),
Env: [
'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
'NODE_VERSION=24.18.0',
'YARN_VERSION=1.22.22',
'NODE_ENV=production',
],
Entrypoint: [
'node',
isLocal
? '/opt/qinglong/node_modules/@qinglong/local-application/dist/cli.js'
: isControl
? isControlAi
? '/opt/qinglong/node_modules/@qinglong/cluster-control/dist/aiCli.js'
: '/opt/qinglong/node_modules/@qinglong/cluster-control/dist/cli.js'
: '/opt/qinglong/node_modules/@qinglong/cluster-admin/dist/plugin-package/recovery/pluginPackageRecoveryCli.js',
],
WorkingDir: '/opt/qinglong',
Labels: {
...(isLocal
? {
'io.qinglong.ai': 'excluded',
'io.qinglong.local.application-config': '2',
'io.qinglong.local.compose-selection': '1',
'io.qinglong.local.sqlite-contract-max': '41',
'io.qinglong.local.sqlite-contract-min': '41',
'io.qinglong.local.sqlite-write-contract': '41',
'io.qinglong.profile': 'edge,standalone',
}
: {}),
'org.opencontainers.image.description': isLocal
? 'QingLong 3.0 AI-excluded Edge and Standalone runtime'
: isControl
? isControlAi
? 'Optional QingLong 3.0 AI-enabled cluster control plane'
: 'QingLong 3.0 PostgreSQL-backed cluster control plane'
: 'QingLong 3.0 short-lived cluster administration jobs',
'org.opencontainers.image.licenses': 'Apache-2.0',
'org.opencontainers.image.revision': revision,
'org.opencontainers.image.source':
'https://github.com/whyour/qinglong',
'org.opencontainers.image.title': isLocal
? 'QingLong 3.0 Local Application'
: isControl
? isControlAi
? 'QingLong 3.0 Cluster Control AI'
: 'QingLong 3.0 Cluster Control'
: 'QingLong 3.0 Cluster Admin',
...(isLocal
? {
'org.opencontainers.image.version': '3.0.0-alpha.0',
}
: {}),
},
},
rootfs: {
type: 'layers',
diff_ids: [
`sha256:${(architecture === 'amd64' ? 'a' : 'b').repeat(64)}`,
],
},
},
'application/vnd.oci.image.config.v1+json',
);
const layer = blob(
Buffer.from(`fixture-${architecture}`),
'application/vnd.oci.image.layer.v1.tar+gzip',
);
const manifest = blob(
{
schemaVersion: 2,
mediaType: 'application/vnd.oci.image.manifest.v1+json',
config,
layers: [layer],
},
'application/vnd.oci.image.manifest.v1+json',
);
const descriptor = {
...manifest,
platform: { architecture, os: 'linux' },
};
imageDescriptors.push(descriptor);
descriptors.push(descriptor);
}
for (const imageDescriptor of imageDescriptors) {
const spdx = blob(
{
_type: 'https://in-toto.io/Statement/v1',
subject: [],
predicateType: 'https://spdx.dev/Document',
predicate: {
spdxVersion: 'SPDX-2.3',
packages: applicationPackages,
},
},
'application/vnd.in-toto+json',
);
spdx.annotations = {
'in-toto.io/predicate-type': 'https://spdx.dev/Document',
};
const provenance = blob(
{
_type: 'https://in-toto.io/Statement/v1',
subject: [],
predicateType: 'https://slsa.dev/provenance/v1',
predicate: {
buildDefinition: {
buildType:
'https://github.com/moby/buildkit/blob/master/docs/attestations/slsa-definitions.md',
externalParameters: {
request: {
frontend: 'dockerfile.v0',
args: {
'build-arg:SOURCE_REVISION': revision,
},
},
},
},
runDetails: { builder: { id: '' } },
},
},
'application/vnd.in-toto+json',
);
provenance.annotations = {
'in-toto.io/predicate-type': 'https://slsa.dev/provenance/v1',
};
const attestationLayers = options.omitProvenance
? [spdx]
: [spdx, provenance];
const attestationConfig = blob(
{
architecture: 'unknown',
os: 'unknown',
config: {},
rootfs: {
type: 'layers',
diff_ids: attestationLayers.map((layer) => layer.digest),
},
},
'application/vnd.oci.image.config.v1+json',
);
const attestationManifest = blob(
{
schemaVersion: 2,
mediaType: 'application/vnd.oci.image.manifest.v1+json',
config: attestationConfig,
layers: attestationLayers,
},
'application/vnd.oci.image.manifest.v1+json',
);
descriptors.push({
...attestationManifest,
annotations: {
'vnd.docker.reference.digest': options.unboundAttestation
? `sha256:${'f'.repeat(64)}`
: imageDescriptor.digest,
'vnd.docker.reference.type': 'attestation-manifest',
},
platform: { architecture: 'unknown', os: 'unknown' },
});
}
if (!options.onlyArchitecture) {
while (descriptors.length < 4) {
descriptors.push({
...descriptors[descriptors.length - 1],
digest: `sha256:${'0'.repeat(64)}`,
});
}
}
const imageIndex = blob(
{
schemaVersion: 2,
mediaType: 'application/vnd.oci.image.index.v1+json',
manifests: descriptors,
},
'application/vnd.oci.image.index.v1+json',
);
fs.writeFileSync(
path.join(layoutRoot, 'index.json'),
JSON.stringify({
schemaVersion: 2,
mediaType: 'application/vnd.oci.image.index.v1+json',
manifests: [imageIndex],
}),
);
return layoutRoot;
}
test('accepts two exact images with bound SBOM and provenance', (t) => {
const report = auditClusterOciLayout({
root,
layoutRoot: createFixture(t),
expectedRevision: revision,
});
assert.equal(report.platforms.length, 2);
assert.deepEqual(
report.platforms.map((entry) => entry.platform),
['linux/amd64', 'linux/arm64'],
);
assert.deepEqual(
report.platforms.map((entry) => entry.spdxApplicationPackages),
[46, 46],
);
});
test('accepts the independent cluster-admin image and attestation closure', (t) => {
const report = auditClusterOciLayout({
root,
layoutRoot: createFixture(t, { image: 'admin' }),
expectedRevision: revision,
image: 'admin',
});
assert.equal(report.image, 'admin');
assert.deepEqual(
report.platforms.map((entry) => entry.spdxApplicationPackages),
[88, 88],
);
});
test('accepts the optional Cluster AI image and attestation closure', (t) => {
const report = auditClusterOciLayout({
root,
layoutRoot: createFixture(t, { image: 'control-ai' }),
expectedRevision: revision,
image: 'control-ai',
});
assert.equal(report.image, 'control-ai');
assert.deepEqual(
report.platforms.map((entry) => entry.spdxApplicationPackages),
[47, 47],
);
});
test('accepts the AI-excluded local image and attestation closure', (t) => {
const report = auditClusterOciLayout({
root,
layoutRoot: createFixture(t, { image: 'local' }),
expectedRevision: revision,
image: 'local',
});
assert.equal(report.image, 'local');
assert.equal(report.maximumPlatformBytes, 128 * 1024 * 1024);
assert.deepEqual(
report.platforms.map((entry) => entry.spdxApplicationPackages),
[10, 10],
);
});
test('rejects cluster-control config presented as cluster-admin evidence', (t) => {
assert.throws(
() =>
auditClusterOciLayout({
root,
layoutRoot: createFixture(t),
expectedRevision: revision,
image: 'admin',
}),
/SPDX application closure differs|image config differs/,
);
});
test('rejects an absent arm64 image', (t) => {
assert.throws(
() =>
auditClusterOciLayout({
root,
layoutRoot: createFixture(t, { omitArm64: true }),
expectedRevision: revision,
}),
/platform set/,
);
});
test('rejects a root runtime config on either architecture', (t) => {
assert.throws(
() =>
auditClusterOciLayout({
root,
layoutRoot: createFixture(t, { rootArm64: true }),
expectedRevision: revision,
}),
/image config differs/,
);
});
test('rejects an attestation that is not bound to its image digest', (t) => {
assert.throws(
() =>
auditClusterOciLayout({
root,
layoutRoot: createFixture(t, { unboundAttestation: true }),
expectedRevision: revision,
}),
/one bound attestation/,
);
});
test('rejects an incomplete predicate set', (t) => {
assert.throws(
() =>
auditClusterOciLayout({
root,
layoutRoot: createFixture(t, { omitProvenance: true }),
expectedRevision: revision,
}),
/exactly SBOM and provenance/,
);
});
test('rejects a development package in the image SBOM closure', (t) => {
assert.throws(
() =>
auditClusterOciLayout({
root,
layoutRoot: createFixture(t, { addDevelopmentPackage: true }),
expectedRevision: revision,
}),
/SPDX application closure differs/,
);
});
test('accepts one native layout with one bound attestation', (t) => {
const report = auditClusterOciLayout({
root,
layoutRoot: createFixture(t, { onlyArchitecture: 'arm64' }),
expectedRevision: revision,
expectedPlatforms: ['linux/arm64'],
});
assert.deepEqual(
report.platforms.map((entry) => entry.platform),
['linux/arm64'],
);
});
test('merges two scanned native layouts into the exact audited multiarch digest', (t) => {
const amd64Layout = createFixture(t, { onlyArchitecture: 'amd64' });
const arm64Layout = createFixture(t, { onlyArchitecture: 'arm64' });
const outputParent = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-release-bundle-')),
);
t.after(() => fs.rmSync(outputParent, { recursive: true, force: true }));
const amd64Evidence = path.join(outputParent, 'amd64-evidence.json');
const arm64Evidence = path.join(outputParent, 'arm64-evidence.json');
fs.writeFileSync(
amd64Evidence,
JSON.stringify(
nativeEvidenceRecord({
root,
layoutRoot: amd64Layout,
expectedRevision: revision,
image: 'control',
platform: 'linux/amd64',
}),
),
);
fs.writeFileSync(
arm64Evidence,
JSON.stringify(
nativeEvidenceRecord({
root,
layoutRoot: arm64Layout,
expectedRevision: revision,
image: 'control',
platform: 'linux/arm64',
}),
),
);
const outputRoot = path.join(outputParent, 'merged');
const predicatePath = path.join(outputParent, 'predicate.json');
const reportPath = path.join(outputParent, 'report.json');
const report = mergeNativeLayouts({
root,
image: 'control',
expectedRevision: revision,
amd64Layout,
amd64Evidence,
arm64Layout,
arm64Evidence,
outputRoot,
predicatePath,
reportPath,
});
assert.match(report.rootIndexDigest, /^sha256:[0-9a-f]{64}$/);
assert.equal(
JSON.parse(fs.readFileSync(predicatePath, 'utf8')).subjectDigest,
report.rootIndexDigest,
);
assert.equal(
auditClusterOciLayout({
root,
layoutRoot: outputRoot,
expectedRevision: revision,
image: 'control',
}).rootIndexDigest,
report.rootIndexDigest,
);
});
test('rejects native evidence that does not exactly describe its OCI layout', (t) => {
const amd64Layout = createFixture(t, { onlyArchitecture: 'amd64' });
const arm64Layout = createFixture(t, { onlyArchitecture: 'arm64' });
const outputParent = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-release-bundle-mismatch-')),
);
t.after(() => fs.rmSync(outputParent, { recursive: true, force: true }));
const amd64Evidence = path.join(outputParent, 'amd64-evidence.json');
const arm64Evidence = path.join(outputParent, 'arm64-evidence.json');
const amd64Record = nativeEvidenceRecord({
root,
layoutRoot: amd64Layout,
expectedRevision: revision,
image: 'control',
platform: 'linux/amd64',
});
fs.writeFileSync(
amd64Evidence,
JSON.stringify({ ...amd64Record, sourceRevision: 'different' }),
);
fs.writeFileSync(
arm64Evidence,
JSON.stringify(
nativeEvidenceRecord({
root,
layoutRoot: arm64Layout,
expectedRevision: revision,
image: 'control',
platform: 'linux/arm64',
}),
),
);
assert.throws(
() =>
mergeNativeLayouts({
root,
image: 'control',
expectedRevision: revision,
amd64Layout,
amd64Evidence,
arm64Layout,
arm64Evidence,
outputRoot: path.join(outputParent, 'merged'),
predicatePath: path.join(outputParent, 'predicate.json'),
reportPath: path.join(outputParent, 'report.json'),
}),
/native vulnerability evidence differs for linux\/amd64/,
);
});
@@ -0,0 +1,140 @@
'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const {
auditPublishedManifest,
} = require('../../scripts/ql3-cluster-remote-manifest-audit.cjs');
const image = 'ghcr.io/whyour/qinglong3-cluster-control';
const digest = `sha256:${'f'.repeat(64)}`;
const descriptorDigest = (character) => `sha256:${character.repeat(64)}`;
function createManifest() {
const amd64Digest = descriptorDigest('a');
const arm64Digest = descriptorDigest('b');
return {
schemaVersion: 2,
mediaType: 'application/vnd.oci.image.index.v1+json',
manifests: [
{
mediaType: 'application/vnd.oci.image.manifest.v1+json',
digest: amd64Digest,
size: 2048,
platform: {
architecture: 'amd64',
os: 'linux',
},
},
{
mediaType: 'application/vnd.oci.image.manifest.v1+json',
digest: descriptorDigest('c'),
size: 1024,
annotations: {
'vnd.docker.reference.digest': amd64Digest,
'vnd.docker.reference.type': 'attestation-manifest',
},
platform: {
architecture: 'unknown',
os: 'unknown',
},
},
{
mediaType: 'application/vnd.oci.image.manifest.v1+json',
digest: arm64Digest,
size: 2048,
platform: {
architecture: 'arm64',
os: 'linux',
},
},
{
mediaType: 'application/vnd.oci.image.manifest.v1+json',
digest: descriptorDigest('d'),
size: 1024,
annotations: {
'vnd.docker.reference.digest': arm64Digest,
'vnd.docker.reference.type': 'attestation-manifest',
},
platform: {
architecture: 'unknown',
os: 'unknown',
},
},
],
};
}
test('accepts the exact published dual-architecture manifest contract', () => {
assert.deepEqual(
auditPublishedManifest(createManifest(), {
expectedImage: image,
expectedDigest: digest,
}),
{
reference: `${image}@${digest}`,
platforms: [
{
platform: 'linux/amd64',
digest: descriptorDigest('a'),
},
{
platform: 'linux/arm64',
digest: descriptorDigest('b'),
},
],
attestationBindings: [descriptorDigest('a'), descriptorDigest('b')],
},
);
});
test('rejects a published manifest without arm64', () => {
const manifest = createManifest();
manifest.manifests[2].platform.architecture = 'amd64';
assert.throws(
() =>
auditPublishedManifest(manifest, {
expectedImage: image,
expectedDigest: digest,
}),
/unexpected published runnable platform/,
);
});
test('rejects an unreviewed runnable platform', () => {
const manifest = createManifest();
manifest.manifests[2].platform.architecture = 's390x';
assert.throws(
() =>
auditPublishedManifest(manifest, {
expectedImage: image,
expectedDigest: digest,
}),
/unexpected published runnable platform/,
);
});
test('rejects an attestation bound to the wrong image', () => {
const manifest = createManifest();
manifest.manifests[3].annotations['vnd.docker.reference.digest'] =
descriptorDigest('a');
assert.throws(
() =>
auditPublishedManifest(manifest, {
expectedImage: image,
expectedDigest: digest,
}),
/not bound one-to-one/,
);
});
test('rejects a tag reference in place of an immutable digest', () => {
assert.throws(
() =>
auditPublishedManifest(createManifest(), {
expectedImage: image,
expectedDigest: 'latest',
}),
/immutable SHA-256/,
);
});
+168
View File
@@ -0,0 +1,168 @@
const assert = require('node:assert/strict');
const { createRequire } = require('node:module');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const ownerCliRequire = createRequire(
require.resolve('../../packages/ql3-local-owner-cli/package.json'),
);
const localSqliteRequire = createRequire(
require.resolve('../../packages/ql3-local-sqlite/package.json'),
);
const localExecutionRequire = createRequire(
require.resolve('../../packages/ql3-local-execution/package.json'),
);
const { runLocalTaskDefinitionCommandFile } = ownerCliRequire(
'@qinglong/local-owner-cli/task-definition-command',
);
const { runLocalTriggerCommandFile } = ownerCliRequire(
'@qinglong/local-owner-cli/trigger-command',
);
const { openLocalSqliteRuntimeDatabase } = localSqliteRequire(
'@qinglong/local-sqlite/runtime',
);
const { LocalSchedulerCoordinator } = localExecutionRequire(
'@qinglong/local-execution/scheduler',
);
const {
localManagementFixture,
taskPutRequest,
writeCommand,
} = require('../../packages/ql3-local-owner-cli/test/localManagementFixture.cjs');
function triggerRequest(value, task, overrides = {}) {
return {
projectId: 'default',
triggerId: 'fresh-product-trigger',
expectedRevision: null,
mutationId: 'a1000000-0000-4000-8000-000000000001',
requestId: 'fresh-trigger-put-1',
failureAuditEventId: 'a2000000-0000-4000-8000-000000000001',
taskId: task.taskId,
taskRevision: task.revision,
taskContentDigest: task.contentDigest,
spec: {
schema: 'qinglong/cron@v1',
config: {
expression: '* * * * *',
timezone: 'UTC',
misfirePolicy: 'fire_once',
},
},
enabled: true,
occurredAtMs: value.now + 1,
...overrides,
};
}
test('fresh product commands create Task then Trigger then one scheduled Run', async (t) => {
const value = await localManagementFixture(t);
const taskCreate = taskPutRequest(value, '1', {
taskId: 'fresh-product-task',
});
const task = (
await runLocalTaskDefinitionCommandFile(
writeCommand(value, 'task.put', taskCreate, 'fresh-task-create'),
)
).task;
const triggerCreate = triggerRequest(value, task);
const trigger = (
await runLocalTriggerCommandFile(
writeCommand(value, 'trigger.put', triggerCreate, 'fresh-trigger-create'),
)
).trigger;
assert.equal(trigger.taskContentDigest, task.contentDigest);
const runtime = await openLocalSqliteRuntimeDatabase({
databasePath: value.databasePath,
profile: 'edge',
});
t.after(() => runtime.close());
const clockValues = [value.now + 1, value.now + 2_000, value.now + 4_000];
const ids = [
'a3000000-0000-4000-8000-000000000001',
'a3000000-0000-4000-8000-000000000002',
'a3000000-0000-4000-8000-000000000003',
'a3000000-0000-4000-8000-000000000004',
];
const scheduler = new LocalSchedulerCoordinator(runtime.schedules, {
pageSize: 4,
misfireGraceMs: 5_000,
clock: () => clockValues.shift(),
createId: () => ids.shift(),
nextOccurrence: (_schedule, afterMs) => afterMs + 1_000,
});
assert.equal((await scheduler.scheduleOnce()).initialized, 1);
const admitted = await scheduler.scheduleOnce();
assert.equal(admitted.admitted, 1);
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
const run = database
.prepare(
`SELECT id, task_id AS "taskId", trigger_id AS "triggerId", status
FROM "Runs"`,
)
.get();
assert.deepEqual(
{ ...run },
{
id: 'a3000000-0000-4000-8000-000000000001',
taskId: task.taskId,
triggerId: trigger.triggerId,
status: 'queued',
},
);
} finally {
database.close();
}
await runLocalTaskDefinitionCommandFile(
writeCommand(
value,
'task.put',
taskPutRequest(value, '2', {
taskId: task.taskId,
expectedRevision: 1,
mutationId: 'a4000000-0000-4000-8000-000000000001',
requestId: 'fresh-task-disable-1',
failureAuditEventId: 'a5000000-0000-4000-8000-000000000001',
enabled: false,
occurredAtMs: value.now + 3_000,
}),
'fresh-task-disable',
),
);
const afterTaskDisable = await scheduler.scheduleOnce();
assert.equal(afterTaskDisable.scanned, 0);
const disabledTrigger = await runLocalTriggerCommandFile(
writeCommand(
value,
'trigger.put',
triggerRequest(value, task, {
expectedRevision: 1,
mutationId: 'a6000000-0000-4000-8000-000000000001',
requestId: 'fresh-trigger-disable-1',
failureAuditEventId: 'a7000000-0000-4000-8000-000000000001',
enabled: false,
occurredAtMs: value.now + 4_000,
}),
'fresh-trigger-disable',
),
);
assert.equal(disabledTrigger.trigger.enabled, false);
assert.equal(disabledTrigger.trigger.revision, 2);
const finalDatabase = new DatabaseSync(value.databasePath, {
readOnly: true,
});
try {
assert.equal(
finalDatabase.prepare('SELECT COUNT(*) AS count FROM "Runs"').get().count,
1,
);
} finally {
finalDatabase.close();
}
});
@@ -0,0 +1,195 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { afterEach, test } = require('node:test');
const {
ImageOsVulnerabilityPolicyError,
auditImageOsVulnerabilityPolicy,
parseArguments,
renderTrivyIgnore,
runCli,
} = require('../../scripts/ql3-image-os-vulnerability-policy.cjs');
const NOW = Date.parse('2026-08-01T12:00:00.000Z');
const temporaryDirectories = [];
function exception(overrides = {}) {
return {
id: 'CVE-2026-12345',
images: ['admin', 'control'],
purls: ['pkg:deb/debian/libssl3@3.0.0-1'],
owner: 'security/platform',
ticket: 'QLSEC-123',
expiresOn: '2026-08-15',
rationale: 'Temporary exposure accepted while the fixed base image is qualified.',
...overrides,
};
}
function policy(exceptions = []) {
return {
schemaVersion: 1,
fixture: 'qinglong/image-os-vulnerability-exceptions@v1',
exceptions,
};
}
afterEach(() => {
for (const directory of temporaryDirectories.splice(0)) {
fs.rmSync(directory, { recursive: true, force: true });
}
});
test('accepts the empty fail-closed production exception policy', () => {
const audit = auditImageOsVulnerabilityPolicy(policy(), { now: () => NOW });
assert.deepEqual(audit, {
compatible: true,
findings: [],
exceptionCount: 0,
imageExceptionCounts: {
admin: 0,
control: 0,
'control-ai': 0,
local: 0,
},
});
assert.equal(
renderTrivyIgnore(policy(), 'local', { now: () => NOW }),
'vulnerabilities:\n []\n',
);
});
test('renders only one image scoped active exception with lifecycle metadata', () => {
const document = policy([exception()]);
const admin = renderTrivyIgnore(document, 'admin', { now: () => NOW });
assert.match(admin, /CVE-2026-12345/);
assert.match(admin, /pkg:deb\/debian\/libssl3@3\.0\.0-1/);
assert.match(admin, /expired_at: 2026-08-15/);
assert.match(admin, /owner=security\/platform; ticket=QLSEC-123/);
assert.equal(
renderTrivyIgnore(document, 'local', { now: () => NOW }),
'vulnerabilities:\n []\n',
);
});
test('rejects expired, same-day and overlong exceptions', () => {
for (const expiresOn of ['2026-07-31', '2026-08-01', '2026-09-01']) {
const audit = auditImageOsVulnerabilityPolicy(
policy([exception({ expiresOn })]),
{ now: () => NOW },
);
assert.equal(audit.compatible, false);
assert.equal(
audit.findings.some(
(finding) =>
finding.code === 'QL3_IMAGE_OS_VULNERABILITY_EXCEPTION_EXPIRY',
),
true,
);
}
});
test('rejects missing ownership, ticket and meaningful rationale', () => {
const audit = auditImageOsVulnerabilityPolicy(
policy([
exception({ owner: 'UPPER', ticket: 'none', rationale: 'temporary' }),
]),
{ now: () => NOW },
);
assert.deepEqual(
audit.findings.map((finding) => finding.code),
[
'QL3_IMAGE_OS_VULNERABILITY_EXCEPTION_OWNER',
'QL3_IMAGE_OS_VULNERABILITY_EXCEPTION_TICKET',
'QL3_IMAGE_OS_VULNERABILITY_EXCEPTION_RATIONALE',
],
);
});
test('rejects unscoped images and non-OS package purls', () => {
const audit = auditImageOsVulnerabilityPolicy(
policy([
exception({
images: ['unknown'],
purls: ['pkg:npm/example@1.0.0'],
}),
]),
{ now: () => NOW },
);
assert.deepEqual(
audit.findings.map((finding) => finding.code),
[
'QL3_IMAGE_OS_VULNERABILITY_EXCEPTION_IMAGES',
'QL3_IMAGE_OS_VULNERABILITY_EXCEPTION_PURLS',
],
);
});
test('rejects duplicate, unsorted and extensible exception identities', () => {
for (const exceptions of [
[exception(), exception()],
[
exception({ id: 'CVE-2026-99999' }),
exception({ id: 'CVE-2026-12345' }),
],
[{ ...exception(), extra: true }],
]) {
const audit = auditImageOsVulnerabilityPolicy(policy(exceptions), {
now: () => NOW,
});
assert.equal(audit.compatible, false);
assert.equal(
audit.findings.some(
(finding) =>
finding.code === 'QL3_IMAGE_OS_VULNERABILITY_EXCEPTION_ID',
),
true,
);
}
});
test('creates one private no-replace Trivy ignore file through the exact CLI', () => {
const directory = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-os-policy-')),
);
temporaryDirectories.push(directory);
const policyDirectory = path.join(directory, 'deploy/containers');
fs.mkdirSync(policyDirectory, { recursive: true });
fs.writeFileSync(
path.join(policyDirectory, 'ql3-os-vulnerability-exceptions.json'),
`${JSON.stringify(policy([exception()]))}\n`,
);
const output = path.join(directory, 'admin.trivyignore.yaml');
runCli([`--image=admin`, `--output=${output}`], directory, {
now: () => NOW,
});
assert.equal(fs.statSync(output).mode & 0o777, 0o600);
assert.match(fs.readFileSync(output, 'utf8'), /QLSEC-123/);
assert.throws(
() =>
runCli([`--image=admin`, `--output=${output}`], directory, {
now: () => NOW,
}),
/output path must be unused/,
);
});
test('parses only audit mode or exact image/output render arguments', () => {
assert.deepEqual(parseArguments([]), { mode: 'audit' });
assert.equal(
parseArguments(['--image=control', '--output=/tmp/ignore.yaml']).image,
'control',
);
assert.throws(
() => parseArguments(['--image=control']),
ImageOsVulnerabilityPolicyError,
);
assert.throws(
() => parseArguments(['--image=control', '--output=/tmp/a', '--extra=x']),
ImageOsVulnerabilityPolicyError,
);
});
+360
View File
@@ -0,0 +1,360 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
RESOURCE_TIERS,
createWorkloadPlans,
parseArguments,
parseCpuMax,
parseKeyValueFile,
parseLimit,
parseMountOptions,
parseNodeTestReport,
validateEnvelope,
} = require('../../scripts/ql3-linux-resource-gate.cjs');
function envelopeFor(tierName) {
const tier = RESOURCE_TIERS[tierName];
return {
memoryMaxBytes: tier.memoryMaxBytes,
memoryPeakBytes: 64 * 1024 * 1024,
swapMaxBytes: tier.swapMaxBytes,
cpuQuotaCores: tier.cpuQuotaCores,
pidsMax: tier.pidsMax,
noNewPrivileges: 1,
seccompMode: 2,
mounts: new Map([
['/', ['ro', 'relatime']],
['/workspace', ['ro', 'nodev']],
['/tmp', ['rw', 'nosuid', 'nodev']],
]),
};
}
test('defines separate router stress, edge release and cluster control envelopes', () => {
assert.deepEqual(Object.keys(RESOURCE_TIERS), [
'router-stress-ci',
'edge-release-ci',
'cluster-control-ci',
]);
assert.equal(RESOURCE_TIERS['router-stress-ci'].supportedMinimum, false);
assert.equal(
RESOURCE_TIERS['router-stress-ci'].memoryMaxBytes,
128 * 1024 * 1024,
);
assert.equal(
RESOURCE_TIERS['edge-release-ci'].memoryMaxBytes,
256 * 1024 * 1024,
);
assert.equal(
RESOURCE_TIERS['cluster-control-ci'].memoryMaxBytes,
512 * 1024 * 1024,
);
assert.notEqual(
RESOURCE_TIERS['router-stress-ci'].workload,
RESOURCE_TIERS['cluster-control-ci'].workload,
);
});
test('parses bounded cgroup v2 and mount evidence', () => {
assert.equal(parseLimit('134217728\n', 'memory.max'), 134217728);
assert.equal(parseLimit('max\n', 'memory.max'), Number.POSITIVE_INFINITY);
assert.equal(parseCpuMax('50000 100000\n'), 0.5);
assert.deepEqual(parseKeyValueFile('oom 0\noom_kill 1\n', 'events'), {
oom: 0,
oom_kill: 1,
});
const mounts = parseMountOptions(
'overlay / overlay ro,relatime 0 0\ntmpfs /tmp tmpfs rw,nosuid,nodev 0 0\nsource /workspace fakeowner ro,nodev 0 0\n',
);
assert.deepEqual(mounts.get('/'), ['ro', 'relatime']);
assert.deepEqual(mounts.get('/tmp'), ['rw', 'nosuid', 'nodev']);
assert.deepEqual(mounts.get('/workspace'), ['ro', 'nodev']);
});
test('fails open hosts, root execution and a widened resource envelope', () => {
const valid = envelopeFor('router-stress-ci');
assert.deepEqual(
validateEnvelope('router-stress-ci', valid, {
platform: 'linux',
architecture: 'arm64',
uid: 65532,
}),
[],
);
const widened = {
...valid,
memoryMaxBytes: 256 * 1024 * 1024,
mounts: new Map([
['/', ['rw']],
['/workspace', ['rw']],
['/tmp', ['rw']],
]),
};
assert.deepEqual(
validateEnvelope('router-stress-ci', widened, {
platform: 'darwin',
architecture: 'arm64',
uid: 0,
}),
[
'memory.max 268435456 did not equal 134217728',
'platform darwin is not linux',
'resource workload must be non-root',
'/ must be mounted read-only',
'/workspace must be mounted read-only',
],
);
});
test('builds tier-specific workload plans without shell commands', () => {
const edge = createWorkloadPlans('/workspace', 'router-stress-ci');
assert.deepEqual(
edge.map(({ name }) => name),
[
'edge-executor',
'node-sqlite',
'local-workflow-product',
'local-workflow-sqlite-lock',
'local-workflow-admission-crash-recovery',
'local-workflow-control-crash-recovery',
],
);
assert.match(edge[0].script, /ql3-edge-benchmark\.cjs$/);
assert.ok(edge[0].args.includes('--max-rss-delta-mb=64'));
assert.equal(edge[2].format, 'node_test');
assert.ok(
edge[2].nodeArgs.some((argument) =>
/ql3-local-application\/test\/activation\.test\.cjs$/.test(argument),
),
);
assert.equal(edge[2].maxProcessRssBytes, 96 * 1024 * 1024);
assert.equal(edge[2].contract, undefined);
assert.equal(
edge[2].nodeArgs.includes(
'--test-name-pattern=executes one admitted Workflow',
),
true,
);
const edgeRelease = createWorkloadPlans(
'/workspace',
'edge-release-ci',
);
assert.deepEqual(
edgeRelease.map(({ name }) => name),
[
'edge-executor',
'node-sqlite',
'local-workflow-product',
'local-ai-prompt-durable-output-edge',
'local-ai-prompt-durable-output-standalone',
'local-workflow-sqlite-lock',
'local-workflow-admission-crash-recovery',
'local-workflow-control-crash-recovery',
'local-ai-prompt-model-invocation-crash-recovery',
'local-ai-prompt-outer-transaction-crash-recovery',
],
);
assert.equal(
edgeRelease[2].nodeArgs.includes(
'--test-name-pattern=executes one admitted Workflow|stops one running Workflow Task',
),
true,
);
assert.deepEqual(edgeRelease[2].contract, {
kind: 'local_workflow_product_lifecycle',
profile: 'edge',
completedWorkflowSteps: 2,
completedAttempts: 2,
cancelCommandStatus: 'accepted',
exactReplay: true,
processIdentityObserved: true,
processExited: true,
parentRunStatus: 'cancelled',
attemptStatus: 'cancelled',
cancelledStepRuns: 2,
cancelEvents: 1,
cancelAudits: 1,
physicalPowerLossProven: false,
});
assert.equal(edgeRelease[3].maxProcessRssBytes, 192 * 1024 * 1024);
assert.deepEqual(edgeRelease[3].env, {
QL3_PROMPT_RESOURCE_PROFILE: 'edge',
QL3_PROMPT_RESOURCE_OUTPUT_BYTES: String(512 * 1024),
});
assert.equal(edgeRelease[3].contract.providerCalls, 2);
assert.equal(edgeRelease[3].contract.exactReplay, true);
assert.equal(edgeRelease[3].contract.contentFree, true);
assert.equal(edgeRelease[3].contract.durableOutputBytes, 512 * 1024);
assert.equal(
edgeRelease[3].contract.maxWalWriteAmplificationPermille,
0,
);
assert.equal(edgeRelease[4].contract.profile, 'standalone');
assert.equal(edgeRelease[4].contract.journalMode, 'wal');
assert.equal(edgeRelease[4].contract.requireWalGrowth, true);
assert.match(
edge[3].script,
/ql3-local-workflow-resource-benchmark\.cjs$/,
);
assert.ok(edge[3].args.includes('--lock-samples=16'));
assert.ok(edge[3].args.includes('--max-lock-p95-ms=500'));
assert.equal(edge[4].contract.scenarios, 16);
assert.equal(edge[4].contract.physicalPowerLossProven, false);
assert.equal(edge[5].contract.scenarios, 16);
assert.equal(edge[5].contract.conclusiveStopObserved, true);
assert.equal(edge[5].contract.physicalPowerLossProven, false);
assert.equal(edgeRelease[8].contract.scenarios, 14);
assert.deepEqual(edgeRelease[8].contract.boundaries, [
'model_start',
'model_completion',
]);
assert.equal(edgeRelease[8].contract.physicalPowerLossProven, false);
assert.equal(edgeRelease[9].contract.scenarios, 20);
assert.deepEqual(edgeRelease[9].contract.operations, [
'admission',
'finalization',
]);
assert.equal(edgeRelease[9].contract.exactReplay, true);
assert.equal(edgeRelease[9].contract.contentFree, true);
assert.equal(
edgeRelease[9].contract.promptAdmissionFinalizationCrashProven,
true,
);
assert.equal(edgeRelease[9].contract.physicalPowerLossProven, false);
const cluster = createWorkloadPlans('/workspace', 'cluster-control-ci');
assert.deepEqual(
cluster.map(({ name }) => name),
['cluster-control'],
);
assert.match(cluster[0].script, /ql3-cluster-control-benchmark\.cjs$/);
});
test('rejects unknown tiers and architecture labels', () => {
assert.deepEqual(
parseArguments(['--tier=edge-release-ci', '--expected-arch=x64', '--json']),
{ tier: 'edge-release-ci', expectedArch: 'x64', json: true },
);
assert.throws(
() => parseArguments(['--tier=router']),
/--tier must be one of/,
);
assert.throws(
() => parseArguments(['--tier=router-stress-ci', '--expected-arch=arm']),
/--expected-arch must be x64 or arm64/,
);
});
test('fails closed when durable Prompt resource evidence drifts', () => {
const [edgePlan, standalonePlan] = createWorkloadPlans(
'/workspace',
'edge-release-ci',
).filter(({ contract }) => contract?.kind === 'durable_prompt_output_resource');
const evidence = {
profile: 'edge',
journalMode: 'delete',
durableOutputBytes: 512 * 1024,
providerCalls: 2,
keyLoads: 1,
keyResolutions: 1,
liveOnlyKeyLoads: 0,
exactReplay: true,
contentFree: true,
durableFacts: { attempts: 0 },
peakProcessRssBytes: 100 * 1024 * 1024,
databaseLogicalWriteAmplificationPermille: 1_383,
databaseAllocatedWriteAmplificationPermille: 1_383,
walWriteAmplificationPermille: 0,
walGrowthBytes: 0,
physicalPowerLossProven: false,
};
const output = (value) =>
`tests 1\npass 1\nfail 0\nskipped 0\nQL3_RESOURCE_EVIDENCE=${JSON.stringify(value)}\n`;
assert.equal(
parseNodeTestReport(output(evidence), edgePlan).evidence.profile,
'edge',
);
assert.throws(
() =>
parseNodeTestReport(
output({
...evidence,
databaseLogicalWriteAmplificationPermille: 3_001,
}),
edgePlan,
),
/durable Prompt output evidence violated its contract/,
);
assert.throws(
() =>
parseNodeTestReport(
output({
...evidence,
profile: 'standalone',
journalMode: 'wal',
walWriteAmplificationPermille: 0,
walGrowthBytes: 0,
}),
standalonePlan,
),
/durable Prompt output evidence violated its contract/,
);
});
test('fails closed when authenticated Workflow cancellation evidence drifts', () => {
const plan = createWorkloadPlans(
'/workspace',
'edge-release-ci',
).find(
({ contract }) =>
contract?.kind === 'local_workflow_product_lifecycle',
);
assert.ok(plan);
const completionEvidence = {
schemaVersion: 1,
profile: 'edge',
workflowSteps: 2,
attempts: 2,
peakProcessRssBytes: 79 * 1024 * 1024,
};
const cancellationEvidence = {
schemaVersion: 1,
profile: 'edge',
cancelCommandStatus: 'accepted',
exactReplay: true,
processIdentityObserved: true,
processExited: true,
parentRunStatus: 'cancelled',
attemptStatus: 'cancelled',
cancelledStepRuns: 2,
cancelEvents: 1,
cancelAudits: 1,
peakProcessRssBytes: 80 * 1024 * 1024,
physicalPowerLossProven: false,
};
const output = (value) =>
`tests 2\npass 2\nfail 0\nskipped 0\nQL3_RESOURCE_EVIDENCE=${JSON.stringify(completionEvidence)}\nQL3_RESOURCE_EVIDENCE=${JSON.stringify(value)}\n`;
assert.equal(
parseNodeTestReport(output(cancellationEvidence), plan)
.evidenceRecords[1].processExited,
true,
);
assert.throws(
() =>
parseNodeTestReport(
output({ ...cancellationEvidence, cancelAudits: 0 }),
plan,
),
/local Workflow cancellation evidence violated its contract/,
);
assert.throws(
() =>
parseNodeTestReport(
output({
...cancellationEvidence,
peakProcessRssBytes: 161 * 1024 * 1024,
}),
plan,
),
/process RSS exceeded its tier budget/,
);
});
@@ -0,0 +1,327 @@
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 {
RESOURCE_TIERS,
createWorkloadPlans,
} = require('../../scripts/ql3-linux-resource-gate.cjs');
const {
TIER_NAMES,
bundleArchitectureEvidence,
evidenceDigest,
mergeCrossArchitectureEvidence,
normalizeSource,
readJsonFile,
validateArchitectureEvidence,
} = require('../../scripts/ql3-linux-resource-release-evidence.cjs');
const scriptPath = path.resolve(
__dirname,
'../../scripts/ql3-linux-resource-release-evidence.cjs',
);
function fixtureSource(overrides = {}) {
return {
repository: 'whyour/qinglong',
revision: 'a'.repeat(40),
workflow: 'QingLong 3.0 CI',
runId: '123456',
runAttempt: 1,
...overrides,
};
}
function fixtureTierReport(tierName, architecture) {
const tier = RESOURCE_TIERS[tierName];
return {
schemaVersion: 1,
tier: tierName,
evidenceClass: tier.evidenceClass,
supportedMinimum: tier.supportedMinimum,
identity: {
platform: 'linux',
architecture,
node: 'v24.18.0',
uid: 65532,
gid: 65532,
},
envelope: {
memoryMaxBytes: tier.memoryMaxBytes,
memoryPeakBytes: Math.min(64 * 1024 * 1024, tier.memoryMaxBytes),
swapMaxBytes: tier.swapMaxBytes,
cpuQuotaCores: tier.cpuQuotaCores,
pidsMax: tier.pidsMax,
noNewPrivileges: 1,
seccompMode: 2,
rootReadOnly: true,
workspaceReadOnly: true,
tmpWritable: true,
memoryEventsBefore: {
low: 0,
high: 0,
max: 0,
oom: 0,
oom_kill: 0,
oom_group_kill: 0,
},
memoryEventsAfter: {
low: 0,
high: 0,
max: 0,
oom: 0,
oom_kill: 0,
oom_group_kill: 0,
},
},
workloads: createWorkloadPlans('/workspace', tierName).map(({ name }) => ({
name,
report: { passed: true },
})),
gates: { passed: true, violations: [] },
};
}
function fixtureReports(architecture) {
return Object.fromEntries(
TIER_NAMES.map((tierName) => [
tierName,
fixtureTierReport(tierName, architecture),
]),
);
}
function fixtureBundle(architecture, source = fixtureSource()) {
return bundleArchitectureEvidence({
source,
architecture,
reports: fixtureReports(architecture),
});
}
function writeJson(filePath, value) {
fs.writeFileSync(filePath, `${JSON.stringify(value)}\n`, 'utf8');
}
function runCli(arguments_) {
return spawnSync(process.execPath, [scriptPath, ...arguments_], {
encoding: 'utf8',
});
}
function sourceArguments(source = fixtureSource()) {
return [
`--repository=${source.repository}`,
`--revision=${source.revision}`,
`--workflow=${source.workflow}`,
`--run-id=${source.runId}`,
`--run-attempt=${source.runAttempt}`,
];
}
test('bundles native x64 and arm64 reports into source-bound release evidence', () => {
const source = fixtureSource();
const x64 = fixtureBundle('x64', source);
const arm64 = fixtureBundle('arm64', source);
const release = mergeCrossArchitectureEvidence({ source, x64, arm64 });
assert.equal(
release.fixture,
'qinglong/linux-resource-cross-architecture-evidence@v1',
);
assert.deepEqual(
release.architectures.map(({ architecture }) => architecture),
['x64', 'arm64'],
);
assert.equal(release.architectures[0].tiers.length, 3);
assert.equal(release.architectures[1].tiers.length, 3);
assert.equal(release.gates.passed, true);
assert.equal(release.releaseDigest.length, 64);
assert.notEqual(x64.bundleDigest, arm64.bundleDigest);
assert.deepEqual(release.limitations, [
'CI cgroup evidence is not a supported minimum hardware claim',
'CI evidence does not replace fixed-device power-loss, flash, thermal, or soak evidence',
'GitHub workflow identity binding is not a cryptographic hardware attestation',
]);
});
test('rejects architecture, gate, memory event and schema drift', () => {
const source = fixtureSource();
const wrongArchitecture = fixtureReports('x64');
wrongArchitecture['router-stress-ci'].identity.architecture = 'arm64';
assert.throws(
() =>
bundleArchitectureEvidence({
source,
architecture: 'x64',
reports: wrongArchitecture,
}),
/reviewed native identity/,
);
const failedGate = fixtureReports('x64');
failedGate['edge-release-ci'].gates = {
passed: false,
violations: ['benchmark failed'],
};
assert.throws(
() =>
bundleArchitectureEvidence({
source,
architecture: 'x64',
reports: failedGate,
}),
/gate did not pass/,
);
const memoryEvent = fixtureReports('x64');
memoryEvent['cluster-control-ci'].envelope.memoryEventsAfter.oom_kill = 1;
assert.throws(
() =>
bundleArchitectureEvidence({
source,
architecture: 'x64',
reports: memoryEvent,
}),
/memory event oom_kill changed/,
);
const widened = fixtureReports('x64');
widened['router-stress-ci'].unexpected = true;
assert.throws(
() =>
bundleArchitectureEvidence({
source,
architecture: 'x64',
reports: widened,
}),
/report fields are invalid/,
);
});
test('rejects source identifiers that only coerce to the reviewed text shape', () => {
for (const source of [
fixtureSource({ repository: 123 }),
fixtureSource({ revision: 123 }),
fixtureSource({ runId: 123456 }),
]) {
assert.throws(() => normalizeSource(source), /source (repository|revision|runId)/);
}
});
test('rejects tampered bundles, cross-source mixing and duplicate architecture', () => {
const source = fixtureSource();
const x64 = fixtureBundle('x64', source);
const arm64 = fixtureBundle('arm64', source);
const tamperedX64 = { ...x64, bundleDigest: '0'.repeat(64) };
assert.throws(
() => validateArchitectureEvidence(tamperedX64, source, 'x64'),
/digest or gates drifted/,
);
const otherSource = fixtureSource({ revision: 'b'.repeat(40) });
assert.throws(
() =>
mergeCrossArchitectureEvidence({
source,
x64,
arm64: fixtureBundle('arm64', otherSource),
}),
/belongs to another source/,
);
assert.throws(
() => mergeCrossArchitectureEvidence({ source, x64, arm64: x64 }),
/arm64 architecture evidence digest or gates drifted/,
);
assert.throws(
() => mergeCrossArchitectureEvidence({ source, x64, arm64: undefined }),
/plain object/,
);
});
test('enforces a shared canonical node budget across sibling branches', () => {
assert.throws(
() => evidenceDigest(Array.from({ length: 100_000 }, () => null)),
/node budget exceeded/,
);
});
test('CLI creates non-overwriting native bundles and merged evidence', (t) => {
const temporaryDirectory = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-linux-resource-release-evidence-'),
);
t.after(() => fs.rmSync(temporaryDirectory, { recursive: true, force: true }));
const source = fixtureSource();
const bundlePaths = {};
for (const architecture of ['x64', 'arm64']) {
const reports = fixtureReports(architecture);
const reportArguments = [];
for (const tierName of TIER_NAMES) {
const reportPath = path.join(
temporaryDirectory,
`${architecture}-${tierName}.json`,
);
writeJson(reportPath, reports[tierName]);
reportArguments.push(`--${tierName}=${reportPath}`);
}
bundlePaths[architecture] = path.join(
temporaryDirectory,
`${architecture}.json`,
);
const result = runCli([
'--mode=bundle',
...sourceArguments(source),
`--architecture=${architecture}`,
...reportArguments,
`--output=${bundlePaths[architecture]}`,
]);
assert.equal(result.status, 0, result.stderr);
assert.equal(JSON.parse(result.stdout).passed, true);
validateArchitectureEvidence(
JSON.parse(fs.readFileSync(bundlePaths[architecture], 'utf8')),
source,
architecture,
);
}
const releasePath = path.join(temporaryDirectory, 'release.json');
const merge = runCli([
'--mode=merge',
...sourceArguments(source),
`--x64=${bundlePaths.x64}`,
`--arm64=${bundlePaths.arm64}`,
`--output=${releasePath}`,
]);
assert.equal(merge.status, 0, merge.stderr);
assert.equal(JSON.parse(merge.stdout).passed, true);
assert.equal(
JSON.parse(fs.readFileSync(releasePath, 'utf8')).releaseDigest.length,
64,
);
const overwrite = runCli([
'--mode=merge',
...sourceArguments(source),
`--x64=${bundlePaths.x64}`,
`--arm64=${bundlePaths.arm64}`,
`--output=${releasePath}`,
]);
assert.notEqual(overwrite.status, 0);
assert.match(overwrite.stderr, /EEXIST/);
});
test('rejects symlink evidence inputs', (t) => {
const temporaryDirectory = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-linux-resource-symlink-'),
);
t.after(() => fs.rmSync(temporaryDirectory, { recursive: true, force: true }));
const target = path.join(temporaryDirectory, 'target.json');
const link = path.join(temporaryDirectory, 'link.json');
writeJson(target, { passed: true });
fs.symlinkSync(target, link);
assert.throws(() => readJsonFile(link, 'evidence'), /non-symlink file/);
});
+169
View File
@@ -0,0 +1,169 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { test } = require('node:test');
const workflowPath = path.resolve(
__dirname,
'../../.github/workflows/ql3-ci.yml',
);
test('runs each Linux resource tier on native x64 and arm64 Node 24 runners', () => {
const workflow = fs.readFileSync(workflowPath, 'utf8');
const resourceJob = workflow.match(
/ linux-resource-envelopes:\n([\s\S]*?)\n linux-resource-release-evidence:/,
)?.[1];
assert.ok(resourceJob, 'linux-resource-envelopes job is missing');
assert.match(resourceJob, /runner: ubuntu-24\.04\n\s+arch: x64/);
assert.match(resourceJob, /runner: ubuntu-24\.04-arm\n\s+arch: arm64/);
assert.match(resourceJob, /node-version: '24\.18\.0'/);
assert.match(resourceJob, /Verify native runner architecture/);
for (const tier of [
'router-stress-ci',
'edge-release-ci',
'cluster-control-ci',
]) {
assert.equal(
resourceJob.match(new RegExp(`--tier=${tier}`, 'g'))?.length,
1,
`${tier} must run exactly once per native matrix entry`,
);
}
});
test('keeps all resource containers fail-closed and exactly bounded', () => {
const workflow = fs.readFileSync(workflowPath, 'utf8');
const resourceJob = workflow.match(
/ linux-resource-envelopes:\n([\s\S]*?)\n linux-resource-release-evidence:/,
)?.[1];
assert.ok(resourceJob);
assert.equal(resourceJob.match(/docker run --rm --read-only/g)?.length, 3);
assert.equal(
resourceJob.match(/--security-opt no-new-privileges/g)?.length,
3,
);
assert.equal(resourceJob.match(/--user 65532:65532/g)?.length, 3);
assert.equal(
resourceJob.match(/--expected-arch=\$\{\{ matrix\.arch \}\}/g)?.length,
3,
);
for (const expected of [
'--memory=128m\n --memory-swap=128m\n --cpus=0.5\n --pids-limit=64',
'--memory=256m\n --memory-swap=256m\n --cpus=1\n --pids-limit=128',
'--memory=512m\n --memory-swap=512m\n --cpus=2\n --pids-limit=256',
]) {
assert.ok(
resourceJob.includes(expected),
`missing exact envelope ${expected}`,
);
}
});
test('uploads one strict source-bound evidence bundle per native architecture', () => {
const workflow = fs.readFileSync(workflowPath, 'utf8');
const resourceJob = workflow.match(
/ linux-resource-envelopes:\n([\s\S]*?)\n linux-resource-release-evidence:/,
)?.[1];
assert.ok(resourceJob);
for (const tier of [
'router-stress-ci',
'edge-release-ci',
'cluster-control-ci',
]) {
assert.equal(
resourceJob.match(
new RegExp(
`> "\\$\\{RUNNER_TEMP\\}/ql3-linux-resource-evidence/${tier}\\.json"`,
'g',
),
)?.length,
1,
`${tier} raw evidence must be captured exactly once`,
);
assert.match(
resourceJob,
new RegExp(
`--${tier}="\\$\\{RUNNER_TEMP\\}/ql3-linux-resource-evidence/${tier}\\.json"`,
),
);
}
assert.match(resourceJob, /--mode=bundle/);
assert.match(resourceJob, /--repository="\$\{SOURCE_REPOSITORY\}"/);
assert.match(resourceJob, /--revision="\$\{SOURCE_REVISION\}"/);
assert.match(resourceJob, /--workflow="\$\{SOURCE_WORKFLOW\}"/);
assert.match(resourceJob, /--run-id="\$\{SOURCE_RUN_ID\}"/);
assert.match(resourceJob, /--run-attempt="\$\{SOURCE_RUN_ATTEMPT\}"/);
assert.match(resourceJob, /--architecture="\$\{\{ matrix\.arch \}\}"/);
assert.equal(
resourceJob.match(
/actions\/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a/g,
)?.length,
1,
);
assert.match(
resourceJob,
/name: ql3-linux-resource-\$\{\{ github\.run_id \}\}-\$\{\{ github\.run_attempt \}\}-\$\{\{ matrix\.arch \}\}/,
);
assert.match(resourceJob, /if-no-files-found: error/);
assert.match(resourceJob, /overwrite: false/);
assert.doesNotMatch(resourceJob, /continue-on-error/);
});
test('merges exact x64 and arm64 artifacts into one release evidence artifact', () => {
const workflow = fs.readFileSync(workflowPath, 'utf8');
const releaseJob = workflow.match(
/ linux-resource-release-evidence:\n([\s\S]*?)\n supply-chain:/,
)?.[1];
assert.ok(releaseJob, 'linux-resource-release-evidence job is missing');
assert.match(releaseJob, /needs: linux-resource-envelopes/);
assert.equal(
releaseJob.match(
/actions\/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c/g,
)?.length,
2,
);
for (const architecture of ['x64', 'arm64']) {
assert.match(
releaseJob,
new RegExp(
`name: ql3-linux-resource-\\$\\{\\{ github\\.run_id \\}\\}-\\$\\{\\{ github\\.run_attempt \\}\\}-${architecture}`,
),
);
assert.match(
releaseJob,
new RegExp(
`path: \\$\\{\\{ runner\\.temp \\}\\}/ql3-linux-resource-evidence/${architecture}`,
),
);
}
assert.match(releaseJob, /--mode=merge/);
assert.match(
releaseJob,
/--x64="\$\{RUNNER_TEMP\}\/ql3-linux-resource-evidence\/x64\/x64\.json"/,
);
assert.match(
releaseJob,
/--arm64="\$\{RUNNER_TEMP\}\/ql3-linux-resource-evidence\/arm64\/arm64\.json"/,
);
assert.match(
releaseJob,
/name: ql3-linux-resource-release-\$\{\{ github\.run_id \}\}-\$\{\{ github\.run_attempt \}\}/,
);
assert.equal(
releaseJob.match(
/actions\/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a/g,
)?.length,
1,
);
assert.doesNotMatch(releaseJob, /pattern:|merge-multiple:|continue-on-error/);
});
test('does not duplicate the legacy edge-only budget in backend matrices', () => {
const workflow = fs.readFileSync(workflowPath, 'utf8');
const backendJob = workflow.match(
/ backend:\n([\s\S]*?)\n linux-resource-envelopes:/,
)?.[1];
assert.ok(backendJob);
assert.doesNotMatch(backendJob, /ql3-linux-resource-gate/);
assert.doesNotMatch(backendJob, /Enforce 256 MiB edge process budget/);
});
+225
View File
@@ -0,0 +1,225 @@
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 {
auditLocalImageContract,
} = require('../../scripts/ql3-local-image-audit.cjs');
const root = path.resolve(__dirname, '../..');
const source = path.join(root, 'deploy/containers/ql3-local-application');
function fixture() {
const temporaryRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-local-image-audit-'),
);
const target = path.join(
temporaryRoot,
'deploy/containers/ql3-local-application',
);
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.cpSync(source, target, { recursive: true });
const workflowTarget = path.join(
temporaryRoot,
'.github/workflows/ql3-ci.yml',
);
fs.mkdirSync(path.dirname(workflowTarget), { recursive: true });
fs.copyFileSync(
path.join(root, '.github/workflows/ql3-ci.yml'),
workflowTarget,
);
return {
root: temporaryRoot,
target,
close() {
fs.rmSync(temporaryRoot, { recursive: true, force: true });
},
};
}
test('accepts the exact AI-excluded local application image contract', () => {
const report = auditLocalImageContract(root);
assert.equal(report.compatible, true);
assert.deepEqual(report.findings, []);
assert.deepEqual(report.runtimePackages, [
'@qinglong/local-admin',
'@qinglong/local-application',
'@qinglong/local-command-file',
'@qinglong/local-execution',
'@qinglong/local-process',
'@qinglong/local-secret',
'@qinglong/local-sqlite',
'@qinglong/runtime-core',
'croner',
'semver',
]);
});
test('rejects a mutable or build-argument-controlled base image', () => {
const current = fixture();
try {
const dockerfilePath = path.join(current.target, 'Dockerfile');
const dockerfile = fs.readFileSync(dockerfilePath, 'utf8');
fs.writeFileSync(
dockerfilePath,
`ARG NODE_IMAGE=node:24-bookworm-slim\n${dockerfile.replaceAll(
/node:24\.18\.0-bookworm-slim@sha256:[0-9a-f]{64}/g,
'${NODE_IMAGE}',
)}`,
);
const report = auditLocalImageContract(current.root);
assert.equal(report.compatible, false);
assert.ok(
report.findings.some(
({ code }) => code === 'BASE_IMAGE_NOT_EXACTLY_PINNED',
),
);
assert.ok(
report.findings.some(
({ code }) => code === 'BASE_IMAGE_OVERRIDE_AUTHORITY',
),
);
} finally {
current.close();
}
});
test('rejects AI or an unreviewed dependency in the runtime closure', () => {
const current = fixture();
try {
const dockerfilePath = path.join(current.target, 'Dockerfile');
fs.appendFileSync(
dockerfilePath,
'\nCOPY --from=workspace /workspace/packages/ql3-ai/dist node_modules/@qinglong/ai/dist\n',
);
const runtimeManifestPath = path.join(
current.target,
'runtime-dependencies/package.json',
);
const runtimeManifest = JSON.parse(
fs.readFileSync(runtimeManifestPath, 'utf8'),
);
runtimeManifest.dependencies['drizzle-orm'] = '1.0.0-rc.4';
fs.writeFileSync(
runtimeManifestPath,
`${JSON.stringify(runtimeManifest, null, 2)}\n`,
);
const report = auditLocalImageContract(current.root);
assert.equal(report.compatible, false);
assert.ok(
report.findings.some(
({ code }) => code === 'AI_PRESENT_IN_RUNTIME_STAGE',
),
);
assert.ok(
report.findings.some(({ code }) => code === 'RUNTIME_DEPENDENCY_DRIFT'),
);
} finally {
current.close();
}
});
test('rejects retaining npm bin links, debug maps or declarations in the production image', () => {
const current = fixture();
try {
const dockerfilePath = path.join(current.target, 'Dockerfile');
const dockerfile = fs
.readFileSync(dockerfilePath, 'utf8')
.replace(
'RUN rm -rf node_modules/.bin \\\n' +
' && node /tmp/ql3-prune-runtime-artifact.cjs node_modules/@qinglong \\\n' +
' @qinglong/local-application \\\n' +
' @qinglong/local-application/process \\\n' +
' @qinglong/local-application/plugin-package-recovery-catalog \\\n' +
' --exclude=@qinglong/ai \\\n' +
' && rm /tmp/ql3-prune-runtime-artifact.cjs\n\n',
'',
);
fs.writeFileSync(dockerfilePath, dockerfile);
const report = auditLocalImageContract(current.root);
assert.equal(report.compatible, false);
assert.ok(
report.findings.some(
({ code }) => code === 'RUNTIME_NONESSENTIAL_FILES_NOT_REMOVED',
),
);
} finally {
current.close();
}
});
test('rejects removal of the SQLite rollout compatibility labels', () => {
const current = fixture();
try {
const dockerfilePath = path.join(current.target, 'Dockerfile');
const dockerfile = fs
.readFileSync(dockerfilePath, 'utf8')
.replace(' io.qinglong.local.sqlite-write-contract="43" \\\n', '');
fs.writeFileSync(dockerfilePath, dockerfile);
const report = auditLocalImageContract(current.root);
assert.equal(report.compatible, false);
assert.ok(
report.findings.some(
({ code }) => code === 'RUNTIME_IDENTITY_OR_LABEL_DRIFT',
),
);
} finally {
current.close();
}
});
test('rejects runtime lifecycle scripts or closure lock drift', () => {
const current = fixture();
try {
const lockPath = path.join(
current.target,
'runtime-dependencies/package-lock.json',
);
const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
lock.packages['node_modules/croner'].hasInstallScript = true;
lock.packages['node_modules/unreviewed'] = {
version: '1.0.0',
resolved: 'file:../unreviewed',
integrity: 'sha512-invalid',
};
fs.writeFileSync(lockPath, `${JSON.stringify(lock, null, 2)}\n`);
const report = auditLocalImageContract(current.root);
assert.equal(report.compatible, false);
assert.ok(
report.findings.some(({ code }) => code === 'LOCKED_PACKAGE_UNSAFE'),
);
assert.ok(
report.findings.some(({ code }) => code === 'RUNTIME_LOCK_CLOSURE_DRIFT'),
);
} finally {
current.close();
}
});
test('rejects removal of either Profile from the native image CI gate', () => {
const current = fixture();
try {
const workflowPath = path.join(
current.root,
'.github/workflows/ql3-ci.yml',
);
const workflow = fs
.readFileSync(workflowPath, 'utf8')
.replace(
' node scripts/ql3-local-image-live-contract.cjs --image="${IMAGE}" --profile=standalone\n',
'',
);
fs.writeFileSync(workflowPath, workflow);
const report = auditLocalImageContract(current.root);
assert.equal(report.compatible, false);
assert.ok(
report.findings.some(
({ code }) => code === 'LOCAL_IMAGE_CI_CONTRACT_DRIFT',
),
);
} finally {
current.close();
}
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,180 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
MAX_REVIEW_BYTES,
ROW_COUNT,
buildReport,
commandFixture,
normalizeManifest,
parseArguments,
validateReport,
} = require('../../scripts/ql3-physical-edge-adoption-scale.cjs');
function manifest(overrides = {}) {
return normalizeManifest({
schemaVersion: 1,
evidenceClass: 'physical_edge_adoption_scale_candidate',
profile: 'edge',
deviceId: 'router-a1',
expectedArchitecture: 'arm64',
expectedFilesystem: 'ext4',
rowCount: 100_000,
maxReviewFileBytes: 32 * 1024 * 1024,
...overrides,
});
}
function command(operation, options) {
return { schemaVersion: 1, operation, options };
}
function storage() {
return { logicalBytes: 4096, allocatedBytes: 4096, files: [] };
}
test('fixes the physical adoption workload at 100000 reviewed rows', () => {
assert.equal(manifest().rowCount, ROW_COUNT);
assert.equal(manifest().maxReviewFileBytes, MAX_REVIEW_BYTES);
assert.throws(() => manifest({ rowCount: 10_000 }), /fixed workload/);
assert.deepEqual(
parseArguments([
'--manifest=/data/manifest.json',
'--data-path=/data',
'--issue-command=/data/issue.json',
'--commit-command=/data/commit.json',
'--output=/data/report.json',
'--json',
]),
{
manifestPath: '/data/manifest.json',
dataPath: '/data',
issueCommandPath: '/data/issue.json',
commitCommandPath: '/data/commit.json',
outputPath: '/data/report.json',
json: true,
},
);
});
test('accepts only one contained issue and commit command pair', () => {
const shared = {
deploymentRoot: '/data/adoption',
profile: 'edge',
sourcePath: '/data/adoption/legacy.sqlite',
authorizationPath: '/data/adoption/authorization.ndjson',
credentialFilePath: '/data/adoption/credential.json',
issuerKeyringPath: '/data/adoption/issuer.json',
ownerPepperKeyringDirectory: '/data/adoption/pepper',
expectedPlanDigest: 'a'.repeat(64),
};
const issue = command('legacy-crontab.decision.issue', {
...shared,
databasePath: '/data/adoption/target.sqlite',
reviewFilePath: '/data/adoption/review.ndjson',
decisionId: '019a2b3c-4d5e-7f60-8123-456789abcdef',
});
const commit = command('legacy-crontab.adoption.commit', {
...shared,
targetPath: '/data/adoption/target.sqlite',
expectedDecisionId: issue.options.decisionId,
});
assert.equal(
commandFixture(issue, commit, '/data').targetPath,
'/data/adoption/target.sqlite',
);
assert.throws(
() =>
commandFixture(
issue,
{
...commit,
options: { ...commit.options, sourcePath: '/tmp/other.sqlite' },
},
'/data',
),
/do not describe one adoption/,
);
});
test('builds digest-bound evidence without overstating flash or power loss', () => {
const measurement = {
durationMs: 100,
peakRssBytes: 30_000_000,
sampleCount: 10,
readBytes: 4096,
writeBytes: 8192,
cancelledWriteBytes: 0,
exitCode: 0,
};
const report = buildReport({
manifest: manifest(),
observed: {
platform: 'linux',
architecture: 'arm64',
node: 'v24.18.0',
bootId: 'boot-a',
dataPath: '/data',
dataFilesystem: 'ext4',
dataMountOptions: ['rw'],
virtualizationIndicators: [],
},
preflight: {
sourceRowCount: 100_000,
reviewFileBytes: 20_000_000,
targetLedgerCount: 0,
targetStorage: storage(),
},
issue: measurement,
commit: measurement,
final: {
ledgerCount: 1,
adoptedTaskCount: 100_000,
adoptedTriggerCount: 100_000,
targetStorage: storage(),
},
generatedAt: '2026-07-22T00:00:00.000Z',
});
assert.equal(report.qualification.passed, true);
assert.equal(report.supported, false);
assert.equal(report.sha256.length, 64);
assert.ok(
report.qualification.doesNotProve.includes(
'whole_device_flash_or_nand_write_amplification',
),
);
assert.deepEqual(
validateReport(report, manifest(), {
architecture: 'arm64',
bootId: 'boot-a',
dataPath: '/data',
dataFilesystem: 'ext4',
dataMountOptions: ['rw'],
}),
[],
);
const tamperedBody = {
...report,
workload: {
...report.workload,
widened: true,
},
};
const { sha256: _sha256, ...body } = tamperedBody;
const tampered = {
...body,
sha256: require('node:crypto')
.createHash('sha256')
.update(JSON.stringify(body))
.digest('hex'),
};
assert.match(
validateReport(tampered, manifest(), {
architecture: 'arm64',
bootId: 'boot-a',
dataPath: '/data',
dataFilesystem: 'ext4',
dataMountOptions: ['rw'],
}).join('; '),
/incomplete/,
);
});
@@ -0,0 +1,399 @@
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
buildApplicationStartReport,
collectArtifactIdentity,
normalizeApplicationStartManifest,
normalizeSession,
parseArguments,
parseEventLines,
preflightArtifactMetadata,
validateApplicationStartReport,
validateArtifactAgainstManifest,
validateBootObservation,
} = require('../../scripts/ql3-physical-edge-application-start.cjs');
const {
canonicalDigest,
} = require('../../scripts/ql3-physical-edge-evidence.cjs');
const packages = [
'@qinglong/local-admin',
'@qinglong/local-application',
'@qinglong/local-command-file',
'@qinglong/local-execution',
'@qinglong/local-process',
'@qinglong/local-secret',
'@qinglong/local-sqlite',
'@qinglong/runtime-core',
'croner',
'semver',
];
function writePrivate(filePath, contents) {
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
fs.writeFileSync(filePath, contents, { mode: 0o600 });
}
function artifactFixture(t) {
const root = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-application-artifact-')),
);
fs.chmodSync(root, 0o700);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
for (const packageName of packages) {
const packageRoot = path.join(
root,
'node_modules',
...packageName.split('/'),
);
const manifest =
packageName === '@qinglong/local-application'
? {
name: packageName,
version: '3.0.0-alpha.0',
engines: { node: '>=24.18.0 <25' },
bin: { 'ql3-local-application': 'dist/cli.js' },
}
: { name: packageName, version: '1.0.0' };
writePrivate(
path.join(packageRoot, 'package.json'),
`${JSON.stringify(manifest)}\n`,
);
writePrivate(path.join(packageRoot, 'dist', 'index.js'), `'use strict';\n`);
}
writePrivate(
path.join(
root,
'node_modules',
'@qinglong',
'local-application',
'dist',
'cli.js',
),
`#!/usr/bin/env node\n'use strict';\n`,
);
return { root, identity: collectArtifactIdentity(root) };
}
function manifest(artifact, overrides = {}) {
return normalizeApplicationStartManifest({
schemaVersion: 1,
evidenceClass: 'physical_edge_application_start_candidate',
profile: 'edge',
deviceId: 'router-a1',
expectedArchitecture: 'arm64',
expectedFilesystem: 'ext4',
expectedArtifactSha256: artifact.artifactSha256,
expectedArtifactFiles: artifact.artifactFiles,
expectedArtifactBytes: artifact.artifactBytes,
expectedNodeSha256: 'b'.repeat(64),
maximumBootAgeMs: 180_000,
maximumFirstActiveMs: 30_000,
maximumSampledRssBytes: 256 * 1024 * 1024,
sampleIntervalMs: 10,
...overrides,
});
}
function boot(bootId, bootAgeMs = 1000, overrides = {}) {
return {
platform: 'linux',
architecture: 'arm64',
bootId,
dataFilesystem: 'ext4',
nodeExecutable: '/usr/bin/node',
nodeSha256: 'b'.repeat(64),
nodeVersion: 'v24.18.0',
bootAgeMs,
...overrides,
};
}
function sessionFixture(artifact) {
const sessionId = '019f0000-0000-4000-8000-000000000010';
const dataPath = '/mnt/ql3-evidence';
const deploymentRoot = path.join(
dataPath,
`.ql3-application-start-${sessionId}`,
);
const artifactRoot = '/opt/qinglong3-release';
const body = {
schemaVersion: 1,
evidenceClass: 'physical_edge_application_start_session',
sessionId,
manifestDigest: canonicalDigest(manifest(artifact)),
uid: 1000,
preparedAt: '2026-07-29T00:00:00.000Z',
artifact,
environment: boot('019f0000-0000-4000-8000-000000000001', 50_000),
paths: {
dataPath,
deploymentRoot,
artifactRoot,
applicationEntrypoint: path.join(
artifactRoot,
'node_modules',
'@qinglong',
'local-application',
'dist',
'cli.js',
),
applicationConfig: path.join(deploymentRoot, 'local-application.json'),
},
};
return { ...body, sha256: canonicalDigest(body) };
}
function reportFixture(artifact, overrides = {}) {
const session = normalizeSession(sessionFixture(artifact));
return buildApplicationStartReport({
manifest: manifest(artifact),
session,
observed: {
before: session.environment,
after: boot('019f0000-0000-4000-8000-000000000002'),
artifact,
},
measurements: {
firstActiveMs: 1200,
maximumSampledRssBytes: 80 * 1024 * 1024,
processReadBytes: 4096,
processWriteBytes: 8192,
sampleCount: 20,
eventCount: 5,
},
outcomes: {
activeEventCount: 1,
aiStatus: 'deployment_excluded',
gracefulStop: true,
exitCode: 0,
exitSignal: null,
stderrBytes: 0,
sqliteContractVersion: 41,
},
generatedAt: '2026-07-29T00:01:00.000Z',
...overrides,
});
}
test('normalizes exact Edge application start budgets', () => {
const artifact = {
artifactSha256: 'a'.repeat(64),
artifactMetadataSha256: 'd'.repeat(64),
artifactFiles: 100,
artifactBytes: 2 * 1024 * 1024,
};
assert.equal(manifest(artifact).maximumBootAgeMs, 180_000);
assert.throws(
() => manifest(artifact, { maximumBootAgeMs: 601_000 }),
/measurement budget/,
);
assert.throws(
() => manifest(artifact, { maximumSampledRssBytes: 1 }),
/measurement budget/,
);
});
test('requires phase-specific absolute paths', () => {
assert.deepEqual(
parseArguments([
'inspect',
'--artifact-root=/opt/qinglong3-release',
'--json',
]),
{
phase: 'inspect',
artifactRoot: '/opt/qinglong3-release',
json: true,
},
);
assert.equal(
parseArguments([
'prepare',
'--manifest=/mnt/data/manifest.json',
'--data-path=/mnt/data',
'--artifact-root=/opt/qinglong3-release',
'--session=/mnt/data/session.json',
]).phase,
'prepare',
);
assert.throws(
() =>
parseArguments([
'resume',
'--manifest=manifest.json',
'--session=/data/session.json',
'--output=/data/report.json',
]),
/manifestPath must be absolute/,
);
});
test('hashes one exact AI-excluded native release closure', (t) => {
const fixture = artifactFixture(t);
assert.deepEqual(fixture.identity.artifact.packages, packages);
assert.match(fixture.identity.artifact.artifactSha256, /^[a-f0-9]{64}$/);
assert.equal(
fixture.identity.applicationEntrypoint.endsWith('/dist/cli.js'),
true,
);
assert.deepEqual(
validateArtifactAgainstManifest(
manifest(fixture.identity.artifact),
fixture.identity.artifact,
{ nodeSha256: 'b'.repeat(64) },
),
[],
);
writePrivate(
path.join(fixture.root, 'node_modules', 'unexpected', 'package.json'),
'{}\n',
);
assert.throws(
() => collectArtifactIdentity(fixture.root),
/package closure is invalid/,
);
});
test('detects cross-boot artifact metadata drift without reading file content', (t) => {
const fixture = artifactFixture(t);
const entrypoint = fixture.identity.applicationEntrypoint;
const before = preflightArtifactMetadata(fixture.root, entrypoint);
assert.equal(
before.artifactMetadataSha256,
fixture.identity.artifact.artifactMetadataSha256,
);
fs.chmodSync(entrypoint, 0o400);
const after = preflightArtifactMetadata(fixture.root, entrypoint);
assert.notEqual(
after.artifactMetadataSha256,
fixture.identity.artifact.artifactMetadataSha256,
);
});
test('binds the pre-reboot session to deterministic deployment paths', () => {
const artifact = {
artifactSha256: 'a'.repeat(64),
artifactMetadataSha256: 'd'.repeat(64),
artifactFiles: 100,
artifactBytes: 2 * 1024 * 1024,
entrypointSha256: 'c'.repeat(64),
packages,
};
const session = normalizeSession(sessionFixture(artifact));
assert.equal(session.environment.bootAgeMs, 50_000);
const escaped = sessionFixture(artifact);
escaped.paths.deploymentRoot = '/opt/qinglong3';
const { sha256: ignored, ...body } = escaped;
assert.throws(
() => normalizeSession({ ...body, sha256: canonicalDigest(body) }),
/session is invalid or drifted/,
);
});
test('parses bounded production active events', () => {
const events = [];
const remaining = parseEventLines(
`${JSON.stringify({
schemaVersion: 1,
component: 'qinglong3-local-application',
level: 'info',
event: 'active',
instanceId: 'edge-a',
profile: 'edge',
aiStatus: 'deployment_excluded',
})}\npartial`,
events,
);
assert.equal(remaining, 'partial');
assert.equal(events[0].event, 'active');
assert.throws(
() => parseEventLines(`${'x'.repeat(4097)}\n`, []),
/event line exceeded/,
);
});
test('accepts only different-boot bounded native application activation', () => {
const artifact = {
artifactSha256: 'a'.repeat(64),
artifactMetadataSha256: 'd'.repeat(64),
artifactFiles: 100,
artifactBytes: 2 * 1024 * 1024,
entrypointSha256: 'c'.repeat(64),
packages,
};
const report = reportFixture(artifact);
assert.equal(report.supported, false);
assert.equal(report.qualification.passed, true);
assert.ok(
report.qualification.doesNotProve.includes(
'cold_node_runtime_or_dynamic_linker_cache',
),
);
assert.deepEqual(
validateApplicationStartReport(report, manifest(artifact), {
bootId: report.observed.after.bootId,
architecture: 'arm64',
dataFilesystem: 'ext4',
dataPath: '/mnt/ql3-evidence',
}),
[],
);
assert.deepEqual(
validateBootObservation(
manifest(artifact),
report.observed.after,
'/mnt/ql3-evidence',
),
[],
);
});
test('fails same boot, latency, sampled RSS and lifecycle drift', () => {
const artifact = {
artifactSha256: 'a'.repeat(64),
artifactMetadataSha256: 'd'.repeat(64),
artifactFiles: 100,
artifactBytes: 2 * 1024 * 1024,
entrypointSha256: 'c'.repeat(64),
packages,
};
const session = normalizeSession(sessionFixture(artifact));
const report = buildApplicationStartReport({
manifest: manifest(artifact),
session,
observed: {
before: session.environment,
after: session.environment,
artifact,
},
measurements: {
firstActiveMs: 31_000,
maximumSampledRssBytes: 300 * 1024 * 1024,
processReadBytes: 0,
processWriteBytes: 0,
sampleCount: 1,
eventCount: 1,
},
outcomes: {
activeEventCount: 0,
aiStatus: 'active',
gracefulStop: false,
exitCode: 1,
exitSignal: null,
stderrBytes: 1,
sqliteContractVersion: 34,
},
generatedAt: '2026-07-29T00:01:00.000Z',
});
assert.equal(report.qualification.passed, false);
assert.match(
report.qualification.violations.join('; '),
/reboot boundary.*measurement budget.*lifecycle outcome/,
);
});

Some files were not shown because too many files have changed in this diff Show More