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 { RunCommandService, } = require('../../back/runtime/application/runCommandService'); const { RunAttemptNotFoundError, RunNotFoundError, } = require('../../back/runtime/application/commandErrors'); const { DuplicateRunEventError, RunRepositoryConstraintError, } = require('../../back/runtime/domain/repositoryErrors'); const { RunVersionConflictError, } = require('../../back/runtime/domain/stateMachineErrors'); const databases = []; const CREATED_AT_MS = 1_750_000_000_000; let idSequence = 200; function nextId() { idSequence += 1; return `019f70d0-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 createRun(overrides = {}) { return { id: nextId(), projectId: 'default', taskId: 'legacy-cron:1', taskRevision: 'revision-1', triggerType: 'manual', executionOrigin: 'manual', executionOwner: 'runtime', status: 'created', version: 0, eventSequence: 0, priority: 0, createdAtMs: CREATED_AT_MS, ...overrides, }; } function createAttempt(runId, overrides = {}) { return { id: nextId(), runId, attempt: 1, status: 'claimed', executorType: 'legacy_local', callbackSequence: 0, createdAtMs: CREATED_AT_MS + 1, ...overrides, }; } async function seed(repository, run, attempt) { await repository.transaction(async (transaction) => { await transaction.insertRun(run); if (attempt) await transaction.insertAttempt(attempt); }); } afterEach(async () => { await Promise.all(databases.splice(0).map((database) => database.close())); }); test('atomically persists a Run transition and its ordered event', async () => { const repository = await createRepository(); const run = createRun(); const eventId = nextId(); const service = new RunCommandService(repository, () => eventId); await seed(repository, run); const result = await service.transitionRun({ runId: run.id, to: 'queued', expectedVersion: 0, atMs: CREATED_AT_MS + 1, actor: { type: 'user', id: 'user:1' }, }); assert.equal(result.run.status, 'queued'); assert.equal(result.run.version, 1); assert.equal(result.run.eventSequence, 1); assert.deepEqual(await repository.findRunById(run.id), result.run); assert.deepEqual(await repository.listEvents(run.id), [ { id: eventId, runId: run.id, sequence: 1, type: 'run.queued', dedupeKey: 'run-transition:0:queued', actorType: 'user', actorId: 'user:1', payload: { from_status: 'created', to_status: 'queued', version: 1, }, createdAtMs: CREATED_AT_MS + 1, }, ]); }); test('rolls back the state update when event persistence fails', async () => { const repository = await createRepository(); const run = createRun(); const duplicateEventId = nextId(); const service = new RunCommandService(repository, () => duplicateEventId); await seed(repository, run); await service.transitionRun({ runId: run.id, to: 'queued', expectedVersion: 0, atMs: CREATED_AT_MS + 1, actor: { type: 'system' }, }); await assert.rejects( service.transitionRun({ runId: run.id, to: 'dispatching', expectedVersion: 1, atMs: CREATED_AT_MS + 2, actor: { type: 'scheduler' }, }), DuplicateRunEventError, ); const persisted = await repository.findRunById(run.id); assert.equal(persisted.status, 'queued'); assert.equal(persisted.version, 1); assert.equal(persisted.eventSequence, 1); assert.equal((await repository.listEvents(run.id)).length, 1); }); test('atomically persists an Attempt transition, Run CAS, and event', async () => { const repository = await createRepository(); const run = createRun({ status: 'dispatching', version: 2, eventSequence: 2, }); const attempt = createAttempt(run.id); const eventId = nextId(); const service = new RunCommandService(repository, () => eventId); await seed(repository, run, attempt); const result = await service.transitionRunAttempt({ runId: run.id, attemptId: attempt.id, to: 'starting', expectedRunVersion: 2, atMs: CREATED_AT_MS + 2, deadlineAtMs: CREATED_AT_MS + 30_000, actor: { type: 'worker', id: 'worker:edge-1' }, }); assert.equal(result.run.status, 'dispatching'); assert.equal(result.run.version, 3); assert.equal(result.run.eventSequence, 3); assert.equal(result.attempt.status, 'starting'); assert.equal(result.attempt.deadlineAtMs, CREATED_AT_MS + 30_000); assert.deepEqual(await repository.findRunById(run.id), result.run); assert.deepEqual( await repository.findAttemptById(attempt.id), result.attempt, ); assert.deepEqual(await repository.listEvents(run.id), [ { id: eventId, runId: run.id, sequence: 3, type: 'attempt.starting', dedupeKey: `attempt-transition:${attempt.id}:2:starting`, actorType: 'worker', actorId: 'worker:edge-1', attemptId: attempt.id, payload: { attempt_id: attempt.id, attempt: 1, from_status: 'claimed', to_status: 'starting', version: 3, deadline_at_ms: CREATED_AT_MS + 30_000, }, createdAtMs: CREATED_AT_MS + 2, }, ]); }); test('persists one idempotent cancellation request and its audit event', async () => { const repository = await createRepository(); const run = createRun({ status: 'running', version: 6, eventSequence: 6, startedAtMs: CREATED_AT_MS + 5, }); const attempt = createAttempt(run.id, { status: 'running', startedAtMs: CREATED_AT_MS + 4, }); const eventId = nextId(); const service = new RunCommandService(repository, () => eventId); await seed(repository, run, attempt); const accepted = await service.requestCancellation({ runId: run.id, attemptId: attempt.id, atMs: CREATED_AT_MS + 10, reason: 'user', actor: { type: 'user', id: 'user:1' }, }); assert.equal(accepted.status, 'accepted'); assert.equal(accepted.run.status, 'running'); assert.equal(accepted.run.version, 7); assert.equal(accepted.run.cancelRequestedAtMs, CREATED_AT_MS + 10); assert.equal(accepted.run.cancelReason, 'user'); const duplicate = await service.requestCancellation({ runId: run.id, attemptId: attempt.id, atMs: CREATED_AT_MS + 11, reason: 'policy', actor: { type: 'system' }, }); assert.equal(duplicate.status, 'already_requested'); assert.equal(duplicate.run.version, 7); assert.equal(duplicate.run.cancelReason, 'user'); assert.deepEqual(await repository.listEvents(run.id), [ { id: eventId, runId: run.id, sequence: 7, type: 'run.cancel_requested', dedupeKey: `run-cancel-request:${attempt.id}`, actorType: 'user', actorId: 'user:1', attemptId: attempt.id, payload: { status: 'running', reason: 'user', requested_at_ms: CREATED_AT_MS + 10, version: 7, }, createdAtMs: CREATED_AT_MS + 10, }, ]); }); test('refuses cancellation after the Attempt reached a terminal state', async () => { const repository = await createRepository(); const run = createRun({ status: 'running', version: 7, eventSequence: 7, startedAtMs: CREATED_AT_MS + 5, }); const attempt = createAttempt(run.id, { status: 'succeeded', startedAtMs: CREATED_AT_MS + 4, finishedAtMs: CREATED_AT_MS + 9, exitCode: 0, }); const service = new RunCommandService(repository, nextId); await seed(repository, run, attempt); const result = await service.requestCancellation({ runId: run.id, attemptId: attempt.id, atMs: CREATED_AT_MS + 10, reason: 'user', actor: { type: 'user' }, }); assert.equal(result.status, 'already_terminal'); assert.equal((await repository.findRunById(run.id)).version, 7); assert.deepEqual(await repository.listEvents(run.id), []); }); test('rejects missing aggregates and stale command versions without side effects', async () => { const repository = await createRepository(); const service = new RunCommandService(repository, nextId); const run = createRun(); await seed(repository, run); await assert.rejects( service.transitionRun({ runId: '019f70d0-0000-7000-8000-000000999999', to: 'queued', expectedVersion: 0, atMs: CREATED_AT_MS + 1, actor: { type: 'user' }, }), RunNotFoundError, ); await assert.rejects( service.transitionRun({ runId: run.id, to: 'queued', expectedVersion: 1, atMs: CREATED_AT_MS + 1, actor: { type: 'user' }, }), RunVersionConflictError, ); await assert.rejects( service.transitionRunAttempt({ runId: run.id, attemptId: '019f70d0-0000-7000-8000-000000999998', to: 'starting', expectedRunVersion: 0, atMs: CREATED_AT_MS + 1, actor: { type: 'worker' }, }), RunAttemptNotFoundError, ); assert.deepEqual(await repository.findRunById(run.id), run); assert.deepEqual(await repository.listEvents(run.id), []); }); test('enforces repository compare-and-set preconditions and stale predicates', async () => { const repository = await createRepository(); const run = createRun(); const attempt = createAttempt(run.id); await seed(repository, run, attempt); await repository.transaction(async (transaction) => { await assert.rejects( transaction.compareAndSetRun({ ...run, version: 3 }, 0), RunRepositoryConstraintError, ); assert.equal( await transaction.compareAndSetRun({ ...run, version: 2 }, 1), false, ); assert.equal( await transaction.compareAndSetAttempt( { ...attempt, status: 'starting' }, { status: 'running', callbackSequence: 0 }, ), false, ); }); assert.deepEqual(await repository.findRunById(run.id), run); assert.deepEqual(await repository.findAttemptById(attempt.id), attempt); });