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

246 lines
6.8 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 {
RunCancellationNotFoundError,
} = require('@qinglong/runtime-core/run-cancellation');
const {
openLocalSqliteRuntimeDatabase,
} = require('@qinglong/local-sqlite/runtime');
const {
runLocalRunStopCommandFile,
} = require('../dist/run-management/runStopCommand.js');
const {
auditRows,
localManagementFixture,
writeCommand,
} = require('./localManagementFixture.cjs');
function uuid(value) {
return `019f9300-0000-4000-8000-${value.toString(16).padStart(12, '0')}`;
}
async function createRunningSource(value, suffix = 1) {
const runtime = await openLocalSqliteRuntimeDatabase({
databasePath: value.databasePath,
profile: 'edge',
});
try {
const task = (
await runtime.taskDefinitions.appendTaskDefinitionRevision({
projectId: 'default',
taskId: `stop-product-task-${suffix}`,
expectedRevision: null,
mutationId: uuid(10 + suffix),
name: 'Stop product task',
kind: 'command',
spec: {
schema: 'qinglong/command@v1',
config: {
command: {
kind: 'argv',
file: '/bin/echo',
args: ['stop-product'],
},
},
},
labels: {},
enabled: true,
occurredAtMs: value.now,
})
).definition;
return await (
await runtime.taskStartRepository()
).startTask({
projectId: 'default',
taskId: task.taskId,
mutationId: uuid(20 + suffix),
expectedRevision: task.revision,
expectedContentDigest: task.contentDigest,
runId: uuid(30 + suffix),
attemptId: uuid(40 + suffix),
createdEventId: uuid(50 + suffix),
queuedEventId: uuid(60 + suffix),
subject: { type: 'user', id: 'automation-user' },
policyFence: { projectVersion: 1, bindingVersion: 1 },
});
} finally {
await runtime.close();
}
}
function request(value, runId, overrides = {}) {
return {
projectId: 'default',
runId,
mutationId: uuid(100),
requestId: 'local-run-stop-product-1',
auditEventId: uuid(101),
failureAuditEventId: uuid(102),
occurredAtMs: value.now,
...overrides,
};
}
test('strongly stops one Local Run and replays through the unified CLI', async (t) => {
const value = await localManagementFixture(t);
const source = await createRunningSource(value);
const commandFile = writeCommand(
value,
'run.stop',
request(value, source.runId),
'run-stop',
);
const accepted = await runLocalRunStopCommandFile(commandFile);
assert.equal(accepted.schemaVersion, 1);
assert.equal(accepted.operation, 'run.stop');
assert.equal(accepted.stop.status, 'accepted');
assert.equal(accepted.stop.runId, source.runId);
assert.equal(accepted.stop.cancelReason, 'user');
const cli = path.join(
__dirname,
'..',
'dist',
'run-management',
'runManagementCli.js',
);
const replay = spawnSync(
process.execPath,
[cli, 'stop', '--command-file', commandFile],
{ encoding: 'utf8' },
);
assert.equal(replay.status, 0, replay.stderr);
assert.equal(JSON.parse(replay.stdout).stop.status, 'already_requested');
assert.deepEqual(
auditRows(value.databasePath)
.filter(({ operationId }) => operationId === 'run.stop')
.map((row) => ({ ...row })),
[
{
eventId: uuid(101),
operationId: 'run.stop',
outcome: 'allowed',
reasonsJson: '["role_grant","strong_authentication"]',
},
],
);
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.deepEqual(
{
...database
.prepare(
`SELECT "cancel_reason" AS "cancelReason",
(SELECT count(*) FROM "RunEvents"
WHERE "run_id" = ? AND "type" = 'run.cancel_requested')
AS "eventCount"
FROM "Runs" WHERE "id" = ?`,
)
.get(source.runId, source.runId),
},
{ cancelReason: 'user', eventCount: 1 },
);
} finally {
database.close();
}
});
test('audits a missing stop target without creating Run state', async (t) => {
const value = await localManagementFixture(t);
const commandFile = writeCommand(
value,
'run.stop',
request(value, 'missing-run', {
mutationId: uuid(110),
requestId: 'local-run-stop-missing',
auditEventId: uuid(111),
failureAuditEventId: uuid(112),
}),
'run-stop-missing',
);
await assert.rejects(
runLocalRunStopCommandFile(commandFile),
RunCancellationNotFoundError,
);
assert.deepEqual(
auditRows(value.databasePath)
.filter(({ operationId }) => operationId === 'run.stop')
.map((row) => ({ ...row })),
[
{
eventId: uuid(112),
operationId: 'run.stop',
outcome: 'denied',
reasonsJson: '["run_not_found"]',
},
],
);
});
test('denies a Viewer and preserves the running Run', async (t) => {
const value = await localManagementFixture(t);
const source = await createRunningSource(value, 2);
const policyDatabase = new DatabaseSync(value.databasePath);
try {
policyDatabase
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings" (
"project_id", "subject_type", "subject_id", "version", "state",
"role", "mutation_id", "changed_by_type", "changed_by_id",
"created_at_ms"
) VALUES ('default', 'user', 'automation-user', 2, 'active', 'viewer',
'run-stop-viewer-binding', 'user', 'automation-user', ?)`,
)
.run(value.now + 1);
} finally {
policyDatabase.close();
}
const commandFile = writeCommand(
value,
'run.stop',
request(value, source.runId, {
mutationId: uuid(120),
requestId: 'local-run-stop-viewer',
auditEventId: uuid(121),
failureAuditEventId: uuid(122),
}),
'run-stop-viewer',
);
await assert.rejects(
runLocalRunStopCommandFile(commandFile),
(error) => error?.code === 'LOCAL_RUN_STOP_COMMAND_AUTHORIZATION_REJECTED',
);
assert.deepEqual(
auditRows(value.databasePath)
.filter(({ operationId }) => operationId === 'run.stop')
.map((row) => ({ ...row })),
[
{
eventId: uuid(122),
operationId: 'run.stop',
outcome: 'denied',
reasonsJson: '["run_stop_fence_rejected"]',
},
],
);
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.equal(
database
.prepare(
`SELECT "cancel_requested_at_ms" AS value FROM "Runs" WHERE "id" = ?`,
)
.get(source.runId).value,
null,
);
} finally {
database.close();
}
});