import assert from 'node:assert/strict'; import test from 'node:test'; import { buildExecutorJobEvent, createExecutorAdapterRegistry, createExecutorGateway, createInMemoryExecutorJobStore, createPhase3ExecutorAdapterRegistry, createPhase5ExecutorAdapterRegistry, normalizeExecutorJobRequest, } from './executor-gateway.mjs'; function request(overrides = {}) { return { jobId: 'job-1', idempotencyKey: 'run-1:node-execute:attempt-1', executor: 'aider', task: { type: 'code_change', instruction: 'Fix the login form validation', workspaceRef: { kind: 'mindspace-workspace', id: 'workspace-1' }, }, subject: { userId: 'user-1' }, authorization: { executionAllowed: true, actorId: 'user-1' }, policy: { sideEffectsAllowed: true, networkAllowed: false }, controls: { timeoutMs: 20_000, cancellationAllowed: true, fallbackExecutor: 'goosed', }, ...overrides, }; } test('executor job contract uses resource references and bounded controls', () => { const normalized = normalizeExecutorJobRequest(request({ task: { instruction: 'x'.repeat(40_000), workspaceRef: { kind: 'workspace', id: 'workspace-1' }, inputRefs: [ { kind: 'artifact', id: 'artifact-1' }, { kind: '', id: 'invalid' }, ], }, controls: { timeoutMs: 100, fallbackExecutor: 'openhands' }, })); assert.equal(normalized.version, 'executor-job-request-v1'); assert.equal(normalized.task.instruction.length, 32_000); assert.deepEqual(normalized.task.workspaceRef, { kind: 'workspace', id: 'workspace-1' }); assert.deepEqual(normalized.task.inputRefs, [{ kind: 'artifact', id: 'artifact-1' }]); assert.equal(normalized.controls.timeoutMs, 500); assert.equal(normalized.controls.fallbackExecutor, 'openhands'); assert.equal('cwd' in normalized.task, false); assert.throws( () => normalizeExecutorJobRequest(request({ jobId: '../unsafe' })), (error) => error.code === 'EXECUTOR_JOB_ID_INVALID', ); const event = buildExecutorJobEvent({ eventId: 'event-1', jobId: 'job-1', sequence: 1, type: 'executor_job_blocked', timestamp: 1234, data: { status: 'blocked' }, }); assert.equal(event.version, 'executor-job-event-v1'); assert.equal(event.sequence, 1); assert.deepEqual(event.data, { status: 'blocked' }); }); test('Phase 3 executor catalog exposes disabled Goosed, Aider and OpenHands adapters', () => { const registry = createPhase3ExecutorAdapterRegistry(); assert.deepEqual( registry.list().map((adapter) => adapter.id), ['goosed', 'aider', 'openhands'], ); for (const adapter of registry.list()) { assert.equal(adapter.enabled, false); assert.equal(adapter.dispatchImplemented, false); assert.equal(adapter.status, 'contract-only'); } }); test('executor adapter registry rejects incomplete and duplicate adapters', () => { const registry = createExecutorAdapterRegistry(); assert.throws(() => registry.register({ id: 'broken' }), /missing method/); const adapter = { id: 'custom', submit() {}, cancel() {}, getState() {}, async *streamEvents() {}, }; registry.register(adapter); assert.throws(() => registry.register(adapter), /already registered/); }); test('Executor Gateway records a blocked idempotent job without calling an adapter', async () => { let now = 1000; const gateway = createExecutorGateway({ store: createInMemoryExecutorJobStore(), nowMs: () => now, }); const preview = await gateway.preview(request()); assert.equal(preview.executor, 'aider'); assert.equal(preview.fallbackExecutor, 'goosed'); assert.equal(preview.fallbackRegistered, true); assert.equal(preview.fallbackAvailable, false); assert.equal(preview.dispatchAllowed, false); assert.equal(preview.gates.implementation, true); assert.equal(preview.gates.serviceEnabled, false); assert.equal(preview.gates.adapterEnabled, false); const first = await gateway.createJob(request()); assert.equal(first.created, true); assert.equal(first.job.status, 'blocked'); assert.equal(first.job.attempts, 0); assert.equal(first.job.reason, 'executor_execution_disabled'); now = 2000; const repeated = await gateway.createJob(request()); assert.equal(repeated.created, false); assert.equal(repeated.job.jobId, first.job.jobId); assert.equal(repeated.job.createdAt, 1000); const eventPage = await gateway.listEvents('job-1'); assert.deepEqual(eventPage.events.map((event) => event.type), ['executor_job_blocked']); assert.equal(eventPage.nextCursor, 1); }); test('Executor Gateway rejects idempotency key reuse with a different payload', async () => { const gateway = createExecutorGateway(); await gateway.createJob(request()); await assert.rejects( () => gateway.createJob(request({ jobId: 'job-2', task: { type: 'code_change', instruction: 'A different instruction', workspaceRef: { kind: 'mindspace-workspace', id: 'workspace-1' }, }, })), (error) => error.code === 'EXECUTOR_IDEMPOTENCY_CONFLICT', ); }); test('Executor Gateway cancellation is idempotent and never invokes a disabled adapter', async () => { let now = 1000; const gateway = createExecutorGateway({ nowMs: () => now }); await gateway.createJob(request()); now = 1500; const cancelled = await gateway.cancel('job-1', { reason: 'user_cancelled' }); assert.equal(cancelled.status, 'cancelled'); assert.equal(cancelled.reason, 'user_cancelled'); assert.equal(cancelled.completedAt, 1500); assert.deepEqual(await gateway.cancel('job-1'), cancelled); assert.equal(await gateway.cancel('missing-job'), null); const eventPage = await gateway.listEvents('job-1', { after: 1 }); assert.deepEqual(eventPage.events.map((event) => event.type), ['executor_job_cancelled']); assert.equal(eventPage.events[0].sequence, 2); assert.equal(eventPage.nextCursor, 2); assert.equal(await gateway.listEvents('missing-job'), null); }); test('Executor Gateway deletes only terminal jobs and their private request state', async () => { const gateway = createExecutorGateway(); await gateway.createJob(request()); assert.equal(await gateway.deleteJob('job-1'), true); assert.equal(await gateway.getJob('job-1'), null); assert.equal(await gateway.listEvents('job-1'), null); assert.equal(await gateway.deleteJob('job-1'), false); const activeGateway = createExecutorGateway({ registry: createPhase5ExecutorAdapterRegistry({ enabledExecutors: ['aider'] }), executionEnabled: true, }); await activeGateway.createJob(request()); await assert.rejects( () => activeGateway.deleteJob('job-1'), (error) => error.code === 'EXECUTOR_JOB_NOT_TERMINAL' && error.status === 409, ); }); test('Executor Gateway exposes only terminal workflow-linked retention candidates', async () => { let now = 1000; const gateway = createExecutorGateway({ nowMs: () => now }); await gateway.createJob(request({ metadata: { workflowRunId: 'run-retention-1' }, })); assert.deepEqual( await gateway.listRetentionCandidates({ before: 999 }), [], ); assert.deepEqual( await gateway.listRetentionCandidates({ before: 1000 }), [{ jobId: 'job-1', workflowRunId: 'run-retention-1', completedAt: 1000, }], ); now = 2000; await gateway.createJob(request({ jobId: 'job-without-link', idempotencyKey: 'without-link', metadata: {}, })); assert.equal((await gateway.listRetentionCandidates({ before: 3000 })).length, 1); }); test('Executor Gateway reports implemented capability while execution remains disabled', () => { const status = createExecutorGateway().status(); assert.equal(status.version, 'executor-gateway-status-v1'); assert.equal(status.dispatchImplemented, true); assert.equal(status.executionEnabled, false); assert.deepEqual(status.store, { kind: 'memory', durable: false }); }); test('Executor Gateway queues only explicitly enabled and authorized worker jobs', async () => { const gateway = createExecutorGateway({ registry: createPhase5ExecutorAdapterRegistry({ enabledExecutors: ['aider'] }), executionEnabled: true, }); const created = await gateway.createJob(request()); assert.equal(created.job.status, 'queued'); assert.equal(created.job.reason, 'queued_for_worker'); assert.equal(created.job.dispatchAllowed, true); assert.equal(created.job.maxAttempts, 3); assert.equal(created.job.request.task.instruction, 'Fix the login form validation'); const projected = await gateway.getJob('job-1'); assert.equal('request' in projected, false); assert.equal('requestFingerprint' in projected, false); assert.deepEqual( (await gateway.listEvents('job-1')).events.map((event) => event.type), ['executor_job_queued'], ); });