mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:00:24 +08:00
220 lines
6.2 KiB
JavaScript
220 lines
6.2 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',
|
|
'runManagementCli.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',
|
|
' ql3-run stop --command-file /absolute/private-command.json',
|
|
].join('\n'),
|
|
);
|
|
const invalid = spawnSync(process.execPath, [cli, 'retry'], {
|
|
encoding: 'utf8',
|
|
});
|
|
assert.equal(invalid.status, 64);
|
|
assert.equal(
|
|
JSON.parse(invalid.stderr).code,
|
|
'LOCAL_RUN_MANAGEMENT_CLI_USAGE_INVALID',
|
|
);
|
|
});
|