feat: add shadow LangGraph orchestrator runtime
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createWorkflowShadowObserver } from './shadow-observer.mjs';
|
||||
|
||||
function jsonResponse(body, status = 200) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
test('shadow observer skips without creating a remote client when mode does not select shadow', async () => {
|
||||
let fetchCalls = 0;
|
||||
const observer = createWorkflowShadowObserver({
|
||||
configService: {
|
||||
async selectEngine() {
|
||||
return { shadowEngine: null, reason: 'mode_off', mode: 'off' };
|
||||
},
|
||||
async getRuntimeState() {
|
||||
throw new Error('must not load runtime config');
|
||||
},
|
||||
},
|
||||
fetchImpl: async () => {
|
||||
fetchCalls += 1;
|
||||
return jsonResponse({});
|
||||
},
|
||||
});
|
||||
|
||||
const result = await observer({
|
||||
runId: 'run-1',
|
||||
requestId: 'request-1',
|
||||
userId: 'user-1',
|
||||
});
|
||||
assert.deepEqual(result, {
|
||||
observed: false,
|
||||
reason: 'mode_off',
|
||||
mode: 'off',
|
||||
});
|
||||
assert.equal(fetchCalls, 0);
|
||||
});
|
||||
|
||||
test('shadow observer sends a bounded observe-only RunSpec to LangGraph service', async () => {
|
||||
let capturedUrl = null;
|
||||
let capturedInit = null;
|
||||
const observer = createWorkflowShadowObserver({
|
||||
configService: {
|
||||
async selectEngine() {
|
||||
return {
|
||||
shadowEngine: 'langgraph',
|
||||
reason: 'shadow',
|
||||
mode: 'shadow',
|
||||
configVersion: 7,
|
||||
};
|
||||
},
|
||||
async getRuntimeState() {
|
||||
return {
|
||||
config: {
|
||||
serviceUrl: 'http://orchestrator.internal:8093',
|
||||
requestTimeoutMs: 1200,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
serviceToken: 'internal-token',
|
||||
fetchImpl: async (url, init) => {
|
||||
capturedUrl = url;
|
||||
capturedInit = init;
|
||||
return jsonResponse({ runId: 'run-2', status: 'succeeded' }, 202);
|
||||
},
|
||||
});
|
||||
|
||||
const result = await observer({
|
||||
runId: 'run-2',
|
||||
requestId: 'request-2',
|
||||
userId: 'user-2',
|
||||
sessionId: 'session-2',
|
||||
taskType: 'code-change',
|
||||
userMessage: {
|
||||
content: [{ type: 'text', text: 'Implement the service boundary' }],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.observed, true);
|
||||
assert.equal(capturedUrl, 'http://orchestrator.internal:8093/v1/runs');
|
||||
assert.equal(capturedInit.headers.authorization, 'Bearer internal-token');
|
||||
const body = JSON.parse(capturedInit.body);
|
||||
assert.equal(body.version, 'orchestrator-run-v1');
|
||||
assert.equal(body.policy.executionMode, 'observe-only');
|
||||
assert.equal(body.policy.sideEffectsAllowed, false);
|
||||
assert.deepEqual(body.input.sessionRef, { kind: 'goose-session', id: 'session-2' });
|
||||
assert.equal(body.metadata.configVersion, 7);
|
||||
});
|
||||
Reference in New Issue
Block a user