Files
qinglong/packages/ql3-local-owner-cli/test/runRetryCommand.test.cjs
T

217 lines
6.1 KiB
JavaScript

const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const { DatabaseSync } = require('node:sqlite');
const path = require('node:path');
const { test } = require('node:test');
const {
RunManualRetryNotFoundError,
} = require('@qinglong/runtime-core/run-manual-retry');
const {
openLocalSqliteRuntimeDatabase,
} = require('@qinglong/local-sqlite/runtime');
const {
runLocalRunRetryCommandFile,
} = require('../dist/run-management/runRetryCommand.js');
const {
auditRows,
localManagementFixture,
writeCommand,
} = require('./localManagementFixture.cjs');
function uuid(value) {
return `019f9200-0000-4000-8000-${value.toString(16).padStart(12, '0')}`;
}
async function createFailedSource(value) {
const runtime = await openLocalSqliteRuntimeDatabase({
databasePath: value.databasePath,
profile: 'edge',
});
let started;
try {
const task = (
await runtime.taskDefinitions.appendTaskDefinitionRevision({
projectId: 'default',
taskId: 'retry-product-task',
expectedRevision: null,
mutationId: uuid(1),
name: 'Retry product task',
kind: 'command',
spec: {
schema: 'qinglong/command@v1',
config: {
command: {
kind: 'argv',
file: '/bin/echo',
args: ['retry-product'],
},
},
},
labels: {},
enabled: true,
occurredAtMs: value.now,
})
).definition;
started = await (
await runtime.taskStartRepository()
).startTask({
projectId: 'default',
taskId: task.taskId,
mutationId: uuid(2),
expectedRevision: task.revision,
expectedContentDigest: task.contentDigest,
runId: uuid(3),
attemptId: uuid(4),
createdEventId: uuid(5),
queuedEventId: uuid(6),
subject: { type: 'user', id: 'automation-user' },
policyFence: { projectVersion: 1, bindingVersion: 1 },
});
} finally {
await runtime.close();
}
const database = new DatabaseSync(value.databasePath);
try {
database
.prepare(
`UPDATE "Runs"
SET "status" = 'failed', "version" = 3, "event_sequence" = 3,
"finished_at_ms" = ?, "error_code" = 'PRODUCT_TEST_FAILURE',
"error_summary" = 'failed'
WHERE "id" = ?`,
)
.run(value.now + 10, started.runId);
database
.prepare(
`UPDATE "RunAttempts"
SET "status" = 'failed', "finished_at_ms" = ?,
"error_code" = 'PRODUCT_TEST_FAILURE',
"error_summary" = 'failed'
WHERE "id" = ?`,
)
.run(value.now + 10, started.attemptId);
database
.prepare(
`INSERT INTO "RunEvents" (
"id", "run_id", "sequence", "type", "dedupe_key",
"actor_type", "actor_id", "attempt_id", "payload",
"created_at_ms"
) VALUES (?, ?, 3, 'run.failed', 'product-test-failed', 'executor',
'product-test', ?, '{}', ?)`,
)
.run(uuid(7), started.runId, started.attemptId, value.now + 10);
} finally {
database.close();
}
return started;
}
function request(value, sourceRunId, overrides = {}) {
return {
projectId: 'default',
sourceRunId,
mutationId: uuid(10),
expectedRunVersion: 3,
expectedRunStatus: 'failed',
requestId: 'local-run-retry-product-1',
auditEventId: uuid(11),
failureAuditEventId: uuid(12),
occurredAtMs: value.now,
...overrides,
};
}
test('runs a strongly authenticated manual retry and exactly replays it', async (t) => {
const value = await localManagementFixture(t);
const source = await createFailedSource(value);
const commandFile = writeCommand(
value,
'run.retry',
request(value, source.runId),
'run-retry',
);
const accepted = await runLocalRunRetryCommandFile(commandFile);
assert.equal(accepted.schemaVersion, 1);
assert.equal(accepted.operation, 'run.retry');
assert.equal(accepted.retry.status, 'accepted');
assert.equal(accepted.retry.retryOfRunId, source.runId);
assert.equal(accepted.retry.runStatus, 'queued');
const replay = await runLocalRunRetryCommandFile(commandFile);
assert.equal(replay.retry.status, 'existing');
assert.equal(replay.retry.runId, accepted.retry.runId);
assert.deepEqual(
auditRows(value.databasePath)
.filter(({ operationId }) => operationId === 'run.retry')
.map((row) => ({ ...row })),
[
{
eventId: uuid(11),
operationId: 'run.retry',
outcome: 'allowed',
reasonsJson: '["role_grant","strong_authentication"]',
},
],
);
});
test('audits a missing source without creating Run state', async (t) => {
const value = await localManagementFixture(t);
const commandFile = writeCommand(
value,
'run.retry',
request(value, 'missing-run', {
mutationId: uuid(20),
requestId: 'local-run-retry-missing',
auditEventId: uuid(21),
failureAuditEventId: uuid(22),
}),
'run-retry-missing',
);
await assert.rejects(
runLocalRunRetryCommandFile(commandFile),
RunManualRetryNotFoundError,
);
assert.deepEqual(
auditRows(value.databasePath)
.filter(({ operationId }) => operationId === 'run.retry')
.map((row) => ({ ...row })),
[
{
eventId: uuid(22),
operationId: 'run.retry',
outcome: 'denied',
reasonsJson: '["run_not_found"]',
},
],
);
});
test('binary exposes only the private command-file retry interface', () => {
const cli = path.join(
__dirname,
'..',
'dist',
'run-management',
'runRetryCli.js',
);
const help = spawnSync(process.execPath, [cli, '--help'], {
encoding: 'utf8',
});
assert.equal(help.status, 0);
assert.equal(
help.stdout.trim(),
'Usage: ql3-run retry --command-file /absolute/private-command.json',
);
const invalid = spawnSync(process.execPath, [cli, 'retry'], {
encoding: 'utf8',
});
assert.equal(invalid.status, 64);
assert.equal(
JSON.parse(invalid.stderr).code,
'LOCAL_RUN_RETRY_CLI_USAGE_INVALID',
);
});