feat: add shadow LangGraph orchestrator runtime
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { MemorySaver } from '@langchain/langgraph';
|
||||
import { createLangGraphOrchestratorRuntime } from './runtime.mjs';
|
||||
|
||||
function runSpec(overrides = {}) {
|
||||
return {
|
||||
runId: 'run-shadow-1',
|
||||
requestId: 'request-shadow-1',
|
||||
workflow: { name: 'code-run-v1', version: 1 },
|
||||
subject: { userId: 'user-1' },
|
||||
input: {
|
||||
instruction: 'Add an idempotency test',
|
||||
taskType: 'code-change',
|
||||
},
|
||||
policy: {
|
||||
executionMode: 'observe-only',
|
||||
sideEffectsAllowed: false,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('LangGraph runtime checkpoints a deterministic observe-only code workflow', async () => {
|
||||
const runtime = createLangGraphOrchestratorRuntime({
|
||||
checkpointer: new MemorySaver(),
|
||||
checkpointKind: 'memory',
|
||||
durable: false,
|
||||
});
|
||||
|
||||
const state = await runtime.start(runSpec());
|
||||
assert.equal(state.runId, 'run-shadow-1');
|
||||
assert.equal(state.status, 'succeeded');
|
||||
assert.equal(state.phase, 'completed');
|
||||
assert.equal(state.result.observed, true);
|
||||
assert.equal(state.result.executed, false);
|
||||
assert.equal(state.plan.executorAdapter, 'native-agent-run');
|
||||
|
||||
const restored = await runtime.getState('run-shadow-1');
|
||||
assert.deepEqual(restored, state);
|
||||
|
||||
const eventPage = await runtime.listEvents('run-shadow-1');
|
||||
assert.deepEqual(
|
||||
eventPage.events.map((event) => event.type),
|
||||
['workflow_validated', 'workflow_planned', 'workflow_completed'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
eventPage.events.map((event) => event.sequence),
|
||||
[1, 2, 3],
|
||||
);
|
||||
assert.equal(eventPage.nextCursor, 3);
|
||||
assert.equal((await runtime.listEvents('run-shadow-1', { after: 2 })).events.length, 1);
|
||||
});
|
||||
|
||||
test('LangGraph runtime is idempotent by run id', async () => {
|
||||
const runtime = createLangGraphOrchestratorRuntime({
|
||||
checkpointer: new MemorySaver(),
|
||||
});
|
||||
|
||||
const first = await runtime.start(runSpec());
|
||||
const second = await runtime.start(runSpec({
|
||||
input: { instruction: 'This must not replace the first checkpoint' },
|
||||
}));
|
||||
|
||||
assert.deepEqual(second, first);
|
||||
assert.equal((await runtime.listEvents('run-shadow-1')).events.length, 3);
|
||||
});
|
||||
|
||||
test('LangGraph runtime rejects execution-enabled and unsupported workflows', async () => {
|
||||
const runtime = createLangGraphOrchestratorRuntime({
|
||||
checkpointer: new MemorySaver(),
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
runtime.start(runSpec({
|
||||
runId: 'run-active',
|
||||
policy: { executionMode: 'active', sideEffectsAllowed: true },
|
||||
})),
|
||||
(error) => error.code === 'WORKFLOW_OBSERVE_ONLY_REQUIRED',
|
||||
);
|
||||
await assert.rejects(
|
||||
runtime.start(runSpec({
|
||||
runId: 'run-unsupported',
|
||||
workflow: { name: 'research-v1', version: 1 },
|
||||
})),
|
||||
(error) => error.code === 'WORKFLOW_NOT_SUPPORTED' && error.status === 422,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user