150 lines
4.6 KiB
JavaScript
150 lines
4.6 KiB
JavaScript
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');
|
|
});
|