fix: wire plan-gate activation and add express.json to steer/plan-gate routes

- Wire planGateRuntime.activate() into session creation path when planGate is true
  (Bug 1: sessions map stayed empty, plan/status always returned none)
- Add express.json({ limit: '1mb' }) to session-steer and plan-gate POST routes
  (Bug 2: req.body was undefined, POST with JSON body returned 400)
- Pass planGateRuntime dependency to createOpenChamberSessionService
- Add route-level and integration tests for both fixes
This commit is contained in:
2026-09-07 23:41:36 +00:00
parent 092db7cae4
commit 9a0c67081e
7 changed files with 164 additions and 5 deletions
@@ -416,6 +416,7 @@ export const createOpenChamberSessionService = (dependencies) => {
emitSessionCreatedEvent,
createSessionGoal: createSessionGoalOverride,
sessionKnowledgeRuntime = null,
planGateRuntime = null,
} = dependencies;
// Last user message of an existing session, as a selection to reuse. Returns
@@ -758,6 +759,10 @@ export const createOpenChamberSessionService = (dependencies) => {
...(cardID ? { cardID } : {}),
});
if (planGate && planGateRuntime && typeof planGateRuntime.activate === 'function') {
planGateRuntime.activate(sessionID, sessionDirectory, cardID || '');
}
let dispatch = { model, agent, variant, promptDispatched: false, dispatchedAsCommand: false };
if (prompt) {
dispatch = await dispatchPrompt({
@@ -916,4 +916,72 @@ describe('openchamber session routes', () => {
globalThis.fetch = originalFetch;
}
});
it('activates planGateRuntime when planGate is true', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn(async () => ({ ok: true, json: async () => ({ id: 'ses_pg_1' }) }));
const activateMock = vi.fn();
try {
const { app } = createApp({ planGateRuntime: { activate: activateMock } });
const response = await request(app)
.post('/api/openchamber/sessions')
.send({ directory: '/repo/app', planGate: true })
.expect(200);
expect(activateMock).toHaveBeenCalledTimes(1);
expect(activateMock).toHaveBeenCalledWith('ses_pg_1', '/repo/app', '');
expect(response.body.planGate).toBe(true);
expect(response.body.sessionId).toBe('ses_pg_1');
} finally {
globalThis.fetch = originalFetch;
}
});
it('does not activate planGateRuntime when planGate is absent', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn(async () => ({ ok: true, json: async () => ({ id: 'ses_npg_1' }) }));
const activateMock = vi.fn();
try {
const { app } = createApp({ planGateRuntime: { activate: activateMock } });
await request(app)
.post('/api/openchamber/sessions')
.send({ directory: '/repo/app' })
.expect(200);
expect(activateMock).not.toHaveBeenCalled();
} finally {
globalThis.fetch = originalFetch;
}
});
it('activates planGateRuntime with cardID when both planGate and cardID are provided', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn(async () => ({ ok: true, json: async () => ({ id: 'ses_pg_2' }) }));
const activateMock = vi.fn();
try {
const { app } = createApp({ planGateRuntime: { activate: activateMock } });
await request(app)
.post('/api/openchamber/sessions')
.send({ directory: '/repo/app', planGate: true, cardID: 'card_xyz' })
.expect(200);
expect(activateMock).toHaveBeenCalledWith('ses_pg_2', '/repo/app', 'card_xyz');
} finally {
globalThis.fetch = originalFetch;
}
});
it('gracefully skips planGateRuntime when not provided', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn(async () => ({ ok: true, json: async () => ({ id: 'ses_no_rt' }) }));
try {
const { app } = createApp();
await request(app)
.post('/api/openchamber/sessions')
.send({ directory: '/repo/app', planGate: true })
.expect(200);
} finally {
globalThis.fetch = originalFetch;
}
});
});