feat: add shadow LangGraph orchestrator runtime
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { MemorySaver } from '@langchain/langgraph';
|
||||
import { createOrchestratorApp } from './app.mjs';
|
||||
import { createLangGraphOrchestratorRuntime } from './runtime.mjs';
|
||||
|
||||
async function listen(app) {
|
||||
const server = await new Promise((resolve) => {
|
||||
const candidate = app.listen(0, '127.0.0.1', () => resolve(candidate));
|
||||
});
|
||||
const address = server.address();
|
||||
return {
|
||||
baseUrl: `http://127.0.0.1:${address.port}`,
|
||||
close: () => new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function spec() {
|
||||
return {
|
||||
runId: 'http-shadow-1',
|
||||
requestId: 'http-request-1',
|
||||
workflow: { name: 'code-run-v1', version: 1 },
|
||||
input: { instruction: 'Inspect without executing' },
|
||||
policy: { executionMode: 'observe-only', sideEffectsAllowed: false },
|
||||
};
|
||||
}
|
||||
|
||||
test('orchestrator HTTP API exposes health and authenticated run endpoints', async (t) => {
|
||||
const runtime = createLangGraphOrchestratorRuntime({
|
||||
checkpointer: new MemorySaver(),
|
||||
checkpointKind: 'memory',
|
||||
});
|
||||
const server = await listen(createOrchestratorApp({
|
||||
runtime,
|
||||
serviceToken: 'test-service-token',
|
||||
}));
|
||||
t.after(server.close);
|
||||
|
||||
const health = await fetch(`${server.baseUrl}/health`);
|
||||
assert.equal(health.status, 200);
|
||||
assert.equal((await health.json()).execution, 'observe-only');
|
||||
const ready = await fetch(`${server.baseUrl}/ready`);
|
||||
assert.equal(ready.status, 200);
|
||||
assert.equal((await ready.json()).ready, true);
|
||||
|
||||
const unauthorized = await fetch(`${server.baseUrl}/v1/runs`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(spec()),
|
||||
});
|
||||
assert.equal(unauthorized.status, 401);
|
||||
|
||||
const created = await fetch(`${server.baseUrl}/v1/runs`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
authorization: 'Bearer test-service-token',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(spec()),
|
||||
});
|
||||
assert.equal(created.status, 202);
|
||||
assert.equal((await created.json()).status, 'succeeded');
|
||||
|
||||
const events = await fetch(`${server.baseUrl}/v1/runs/http-shadow-1/events?after=1`, {
|
||||
headers: { authorization: 'Bearer test-service-token' },
|
||||
});
|
||||
const eventBody = await events.json();
|
||||
assert.equal(events.status, 200);
|
||||
assert.deepEqual(eventBody.events.map((event) => event.sequence), [2, 3]);
|
||||
});
|
||||
|
||||
test('orchestrator HTTP API reports missing runs and observe-only violations', async (t) => {
|
||||
const runtime = createLangGraphOrchestratorRuntime({
|
||||
checkpointer: new MemorySaver(),
|
||||
});
|
||||
const server = await listen(createOrchestratorApp({ runtime }));
|
||||
t.after(server.close);
|
||||
|
||||
const missing = await fetch(`${server.baseUrl}/v1/runs/missing`);
|
||||
assert.equal(missing.status, 404);
|
||||
|
||||
const activeSpec = spec();
|
||||
activeSpec.runId = 'http-active-1';
|
||||
activeSpec.policy = { executionMode: 'active', sideEffectsAllowed: true };
|
||||
const rejected = await fetch(`${server.baseUrl}/v1/runs`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(activeSpec),
|
||||
});
|
||||
assert.equal(rejected.status, 422);
|
||||
assert.equal((await rejected.json()).error.code, 'WORKFLOW_OBSERVE_ONLY_REQUIRED');
|
||||
});
|
||||
Reference in New Issue
Block a user