'use strict'; const assert = require('node:assert/strict'); const { test } = require('node:test'); const { TaskStartFenceRejectedError, TaskStartNotFoundError, } = require('@qinglong/runtime-core/task-start'); const { createTaskDefinitionRecord, } = require('@qinglong/runtime-core/task-definition'); const { createBuiltInTaskSpecSemanticRegistry, } = require('@qinglong/runtime-core/task-spec-semantic'); const { compileClusterCommandTaskDefinition, } = require('@qinglong/runtime-core/cluster-execution-revision'); const { PostgresTaskStartRepository, } = require('@qinglong/cluster-postgres/task-start'); const IDS = [ '019f7300-0000-7000-8000-000000000801', '019f7300-0000-7000-8000-000000000802', '019f7300-0000-7000-8000-000000000803', '019f7300-0000-7000-8000-000000000804', ]; const MUTATION_ID = '019f7300-0000-7000-8000-000000000800'; const TASK_SEMANTICS = createBuiltInTaskSpecSemanticRegistry(); const TASK_COMMAND = { projectId: 'project-1', taskId: 'task-1', expectedRevision: null, mutationId: '019f7300-0000-7000-8000-000000000899', name: 'Task 1', kind: 'command', spec: { schema: 'qinglong/command@v1', config: { command: { kind: 'argv', file: '/bin/echo', args: ['cluster'] }, }, }, labels: {}, enabled: true, occurredAtMs: 20, }; const DEFINITION = createTaskDefinitionRecord({ ...TASK_COMMAND, spec: TASK_SEMANTICS.normalize({ projectId: TASK_COMMAND.projectId, taskId: TASK_COMMAND.taskId, kind: TASK_COMMAND.kind, spec: TASK_COMMAND.spec, }), }, 10); const DISABLED_DEFINITION = createTaskDefinitionRecord({ ...TASK_COMMAND, enabled: false, spec: TASK_SEMANTICS.normalize({ projectId: TASK_COMMAND.projectId, taskId: TASK_COMMAND.taskId, kind: TASK_COMMAND.kind, spec: TASK_COMMAND.spec, }), }, 10); const EXECUTION = compileClusterCommandTaskDefinition( DEFINITION, TASK_SEMANTICS, ); function command(overrides = {}) { return { projectId: 'project-1', taskId: 'task-1', mutationId: MUTATION_ID, expectedRevision: DEFINITION.revision, expectedContentDigest: DEFINITION.contentDigest, runId: IDS[0], attemptId: IDS[1], createdEventId: IDS[2], queuedEventId: IDS[3], subject: { type: 'user', id: 'user-1' }, policyFence: { projectVersion: 2, bindingVersion: 3 }, ...overrides, }; } function taskRow(overrides = {}) { return { projectId: DEFINITION.projectId, taskId: DEFINITION.taskId, taskRevision: DEFINITION.revision, definitionMutationId: DEFINITION.mutationId, taskName: DEFINITION.name, description: null, taskKind: DEFINITION.kind, specJson: DEFINITION.spec, labelsJson: DEFINITION.labels, enabled: DEFINITION.enabled, taskContentDigest: DEFINITION.contentDigest, taskCreatedAtMs: DEFINITION.createdAtMs, taskUpdatedAtMs: DEFINITION.updatedAtMs, ...overrides, }; } function executionRow() { return { projectId: EXECUTION.projectId, taskId: EXECUTION.taskId, sourceRevision: EXECUTION.sourceRevision, taskRevision: EXECUTION.taskRevision, sourceContentDigest: EXECUTION.sourceContentDigest, executorType: EXECUTION.executorType, planSchema: EXECUTION.planSchema, planJson: { command: EXECUTION.command, environment: EXECUTION.environment, placement: EXECUTION.placement, }, contentDigest: EXECUTION.contentDigest, createdAtMs: EXECUTION.createdAtMs, }; } function fixture(options = {}) { const calls = []; const client = { async query(sql, params = []) { const normalized = sql.replace(/\s+/g, ' ').trim(); calls.push({ sql: normalized, params }); if ( normalized.startsWith('BEGIN') || normalized === 'COMMIT' || normalized === 'ROLLBACK' || normalized.startsWith('SELECT set_config') || normalized.startsWith('INSERT INTO') ) return { rows: [], rowCount: normalized.startsWith('INSERT') ? 1 : 0 }; if (normalized.includes('FROM "ql3"."projects"')) { const rows = options.projectRows ?? [{ projectStatus: 'active', projectVersion: 2, }]; return { rows, rowCount: rows.length }; } if (normalized.includes('FROM "ql3"."project_role_bindings"')) { const rows = options.bindingRows ?? [{ bindingVersion: 3, bindingState: 'active', bindingRole: 'operator', }]; return { rows, rowCount: rows.length }; } if (normalized.includes('FROM "ql3"."runs"')) { const rows = options.runRows ?? []; return { rows, rowCount: rows.length }; } if (normalized.includes('FROM "ql3"."task_definitions"')) { const rows = options.taskRows ?? [taskRow()]; return { rows, rowCount: rows.length }; } if (normalized.includes('FROM "ql3"."task_execution_revisions"')) { const rows = options.executionRows ?? [executionRow()]; return { rows, rowCount: rows.length }; } if (normalized.includes('statement_timestamp()')) { return { rows: [{ nowMs: 1_000 }], rowCount: 1 }; } if (normalized.includes('FROM "ql3"."run_attempts"')) { const rows = options.attemptRows ?? []; return { rows, rowCount: rows.length }; } if (normalized.includes('FROM "ql3"."run_events"')) { const rows = options.eventRows ?? []; return { rows, rowCount: rows.length }; } throw new Error(`Unexpected SQL: ${normalized}`); }, release() { calls.push({ sql: 'RELEASE', params: [] }); }, }; return { repository: new PostgresTaskStartRepository({ async connect() { return client; }, }), calls, }; } test('revalidates Policy and Task/execution digests before one atomic Run aggregate', async () => { const { repository, calls } = fixture(); assert.deepEqual(await repository.startTask(command()), { status: 'accepted', projectId: 'project-1', taskId: 'task-1', taskRevision: 1, taskContentDigest: DEFINITION.contentDigest, runId: IDS[0], attemptId: IDS[1], runStatus: 'queued', runVersion: 2, eventSequence: 2, executorType: 'remote_worker', executionRevisionDigest: EXECUTION.contentDigest, createdAtMs: 1_000, }); const project = calls.findIndex(({ sql }) => sql.includes('FROM "ql3"."projects"')); const binding = calls.findIndex(({ sql }) => sql.includes('project_role_bindings')); const task = calls.findIndex(({ sql }) => sql.includes('task_definitions')); const execution = calls.findIndex(({ sql }) => sql.includes('task_execution_revisions')); assert.ok(project < binding && binding < task && task < execution); assert.equal(calls.filter(({ sql }) => sql.startsWith('INSERT INTO')).length, 4); assert.equal(calls.some(({ sql }) => sql === 'COMMIT'), true); }); test('returns the original durable identities for an exact replay', async () => { const createdPayload = { status: 'created', version: 1, execution_owner: 'runtime', executor_type: 'remote_worker', execution_revision_digest: EXECUTION.contentDigest, task_revision: 1, task_content_digest: DEFINITION.contentDigest, mutation_id: MUTATION_ID, policy_fence: { project_version: 2, binding_version: 3 }, }; const { repository, calls } = fixture({ runRows: [{ runId: IDS[0], projectId: 'project-1', taskId: 'task-1', taskRevisionRef: EXECUTION.taskRevision, triggerType: 'task_start', executionOrigin: 'manual', executionOwner: 'runtime', triggeredBy: 'user-1', requestId: MUTATION_ID, priority: 0, createdAtMs: 1_000, }], attemptRows: [{ attemptId: IDS[1], executorType: 'remote_worker' }], eventRows: [ { sequence: 1, type: 'run.created', actorType: 'user', actorId: 'user-1', payload: createdPayload, createdAtMs: 1_000, }, { sequence: 2, type: 'run.queued', actorType: 'user', actorId: 'user-1', payload: { from_status: 'created', to_status: 'queued', version: 2 }, createdAtMs: 1_000, }, ], }); const replay = await repository.startTask(command({ runId: '019f7300-0000-7000-8000-000000000901', attemptId: '019f7300-0000-7000-8000-000000000902', createdEventId: '019f7300-0000-7000-8000-000000000903', queuedEventId: '019f7300-0000-7000-8000-000000000904', })); assert.equal(replay.status, 'existing'); assert.equal(replay.runId, IDS[0]); assert.equal(replay.attemptId, IDS[1]); assert.equal(calls.some(({ sql }) => sql.includes('task_definitions')), false); assert.equal(calls.some(({ sql }) => sql.startsWith('INSERT INTO')), false); }); test('rejects missing, authorization, definition and disabled fences', async () => { await assert.rejects( fixture({ projectRows: [] }).repository.startTask(command()), TaskStartNotFoundError, ); await assert.rejects( fixture({ bindingRows: [{ bindingVersion: 4, bindingState: 'revoked', bindingRole: null, }] }).repository.startTask(command()), (error) => error instanceof TaskStartFenceRejectedError && error.reason === 'authorization_changed', ); await assert.rejects( fixture().repository.startTask(command({ expectedRevision: 2 })), (error) => error instanceof TaskStartFenceRejectedError && error.reason === 'definition_changed', ); await assert.rejects( fixture({ taskRows: [taskRow({ enabled: false, taskContentDigest: DISABLED_DEFINITION.contentDigest, })], }).repository.startTask(command({ expectedContentDigest: DISABLED_DEFINITION.contentDigest, })), (error) => error instanceof TaskStartFenceRejectedError && error.reason === 'task_disabled', ); });