import assert from 'node:assert/strict'; import test from 'node:test'; import { createWorkflowEngineRegistry } from './engine-registry.mjs'; import { createWorkflowExecutionAdapter, normalizeWorkflowExecutionRequest, } from './execution-adapter.mjs'; function engine(id) { return { id, start() { throw new Error('must not dispatch during Phase 3 dry-run'); }, resume() {}, cancel() {}, getState() {}, async *streamEvents() {}, }; } function executionRequest(overrides = {}) { return { spec: { runId: 'run-1', requestId: 'request-1', workflow: { name: 'code-run-v1', version: 1 }, subject: { userId: 'user-1' }, }, idempotencyKey: 'agent-run:run-1:attempt-1', authorization: { executionAllowed: true, actorId: 'user-1' }, controls: { timeoutMs: 10_000 }, ...overrides, }; } test('workflow execution request normalizes authorization, limits and idempotency', () => { const request = normalizeWorkflowExecutionRequest(executionRequest({ idempotencyKey: ` ${'x'.repeat(250)} `, controls: { timeoutMs: 120_000, fallbackAllowed: false }, })); assert.equal(request.version, 'workflow-execution-request-v1'); assert.equal(request.idempotencyKey.length, 200); assert.equal(request.authorization.executionAllowed, true); assert.equal(request.controls.timeoutMs, 60_000); assert.equal(request.controls.fallbackAllowed, false); }); test('workflow execution adapter records a LangGraph candidate but keeps Native effective', async () => { const registry = createWorkflowEngineRegistry([engine('native'), engine('langgraph')]); const adapter = createWorkflowExecutionAdapter({ engineRegistry: registry, configService: { async selectEngine() { return { engine: 'native', candidateEngine: 'langgraph', fallbackEngine: 'native', mode: 'canary', reason: 'execution_gate_disabled', candidateReason: 'user_allowlist', configVersion: 4, }; }, }, }); const decision = await adapter.plan(executionRequest()); assert.equal(decision.version, 'workflow-execution-decision-v1'); assert.equal(decision.candidateEngine, 'langgraph'); assert.equal(decision.effectiveEngine, 'native'); assert.equal(decision.dryRun, true); assert.equal(decision.handoffAllowed, false); assert.equal(decision.candidateReason, 'user_allowlist'); assert.deepEqual(decision.gates, { implementation: true, routingSelected: false, engineRegistered: true, idempotencyKeyPresent: true, executionAuthorized: true, killSwitchOpen: true, }); await assert.rejects( () => adapter.start(executionRequest()), (error) => error.code === 'WORKFLOW_EXECUTION_HANDOFF_DISABLED' && error.decision.effectiveEngine === 'native', ); }); test('workflow execution adapter dispatches a selected and authorized LangGraph engine', async () => { let started = null; const registry = createWorkflowEngineRegistry([ engine('native'), { ...engine('langgraph'), start(spec) { started = spec; return { runId: spec.runId, status: 'waiting' }; }, }, ]); const adapter = createWorkflowExecutionAdapter({ engineRegistry: registry, configService: { async selectEngine() { return { engine: 'langgraph', candidateEngine: 'langgraph', fallbackEngine: 'native', mode: 'canary', reason: 'user_allowlist', candidateReason: 'user_allowlist', configVersion: 5, }; }, }, }); const result = await adapter.start(executionRequest()); assert.equal(result.dispatched, true); assert.equal(result.engine, 'langgraph'); assert.equal(result.decision.handoffAllowed, true); assert.equal(result.decision.dryRun, false); assert.equal(started.runId, 'run-1'); }); test('workflow execution adapter leaves native dispatch under Portal ownership', async () => { const registry = createWorkflowEngineRegistry([engine('native')]); const adapter = createWorkflowExecutionAdapter({ engineRegistry: registry, configService: { async selectEngine() { return { engine: 'native', candidateEngine: 'native', fallbackEngine: 'native', mode: 'off', reason: 'mode_off', configVersion: 1, }; }, }, }); const result = await adapter.start(executionRequest()); assert.equal(result.dispatched, false); assert.equal(result.reason, 'native_execution_owned_by_portal'); assert.equal(result.decision.effectiveEngine, 'native'); });