feat: add pluggable workflow orchestrator controls
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
buildRunEvent,
|
||||
normalizeRunSpec,
|
||||
stableRolloutBucket,
|
||||
} from './contracts.mjs';
|
||||
import {
|
||||
createRemoteWorkflowEngine,
|
||||
createWorkflowEngineRegistry,
|
||||
} from './engine-registry.mjs';
|
||||
|
||||
test('orchestrator contracts normalize run specs without exposing framework types', () => {
|
||||
const spec = normalizeRunSpec({
|
||||
runId: 'run-1',
|
||||
requestId: 'request-1',
|
||||
workflow: { name: 'code-run-v1', version: 2 },
|
||||
subject: { userId: 'user-1' },
|
||||
input: { instruction: 'fix tests' },
|
||||
});
|
||||
assert.equal(spec.version, 'orchestrator-run-v1');
|
||||
assert.equal(spec.workflow.name, 'code-run-v1');
|
||||
assert.equal(spec.workflow.version, 2);
|
||||
assert.equal(spec.subject.userId, 'user-1');
|
||||
|
||||
const event = buildRunEvent({
|
||||
runId: spec.runId,
|
||||
sequence: 1,
|
||||
type: 'workflow.started',
|
||||
});
|
||||
assert.equal(event.version, 'orchestrator-event-v1');
|
||||
assert.equal(event.type, 'workflow.started');
|
||||
assert.equal(stableRolloutBucket('run-1'), stableRolloutBucket('run-1'));
|
||||
});
|
||||
|
||||
test('workflow engine registry enforces the framework-neutral contract', () => {
|
||||
const engine = {
|
||||
id: 'native',
|
||||
start() {},
|
||||
resume() {},
|
||||
cancel() {},
|
||||
getState() {},
|
||||
async *streamEvents() {},
|
||||
};
|
||||
const registry = createWorkflowEngineRegistry([engine]);
|
||||
assert.equal(registry.has('native'), true);
|
||||
assert.equal(registry.get('native'), engine);
|
||||
assert.throws(
|
||||
() => registry.register({ id: 'broken' }),
|
||||
/missing methods/,
|
||||
);
|
||||
});
|
||||
|
||||
test('remote workflow engine speaks only the versioned orchestrator HTTP contract', async () => {
|
||||
const calls = [];
|
||||
const engine = createRemoteWorkflowEngine({
|
||||
baseUrl: 'http://orchestrator.internal',
|
||||
fetchImpl: async (url, init) => {
|
||||
calls.push({ url, init });
|
||||
return {
|
||||
ok: true,
|
||||
async json() {
|
||||
return url.endsWith('/events') ? { events: [{ type: 'run.succeeded' }] } : { ok: true };
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
await engine.start({ runId: 'run-1' });
|
||||
await engine.resume('run-1', { approvalId: 'approval-1', decision: 'approve' });
|
||||
await engine.cancel('run-1');
|
||||
await engine.getState('run-1');
|
||||
const events = [];
|
||||
for await (const event of engine.streamEvents('run-1')) events.push(event);
|
||||
|
||||
assert.deepEqual(calls.map((call) => [call.init?.method ?? 'GET', call.url]), [
|
||||
['POST', 'http://orchestrator.internal/v1/runs'],
|
||||
['POST', 'http://orchestrator.internal/v1/runs/run-1/resume'],
|
||||
['POST', 'http://orchestrator.internal/v1/runs/run-1/cancel'],
|
||||
['GET', 'http://orchestrator.internal/v1/runs/run-1'],
|
||||
['GET', 'http://orchestrator.internal/v1/runs/run-1/events'],
|
||||
]);
|
||||
assert.deepEqual(events, [{ type: 'run.succeeded' }]);
|
||||
});
|
||||
Reference in New Issue
Block a user