feat: add dry-run workflow execution boundary
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
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: false,
|
||||
routingSelected: true,
|
||||
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 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');
|
||||
});
|
||||
Reference in New Issue
Block a user