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
+3 -2
View File
@@ -7,6 +7,7 @@
// runtime detects the plan in the first assistant message, emits plan-ready,
// and holds until approve/reject/timeout.
import express from 'express';
import { GOAL_OBJECTIVE_CHAR_LIMIT } from '../session-goal/objectives.js';
const asRecord = (value) => (value && typeof value === 'object' && !Array.isArray(value) ? value : null);
@@ -300,7 +301,7 @@ export function registerPlanGateRoutes(app, runtime) {
res.status(status).json({ error: error?.message ?? fallback });
};
app.post('/api/openchamber/session/:sessionID/plan/approve', async (req, res) => {
app.post('/api/openchamber/session/:sessionID/plan/approve', express.json({ limit: '1mb' }), async (req, res) => {
try {
const sessionId = asNonEmptyString(req.params?.sessionID);
if (!sessionId) return res.status(400).json({ error: 'sessionID is required' });
@@ -313,7 +314,7 @@ export function registerPlanGateRoutes(app, runtime) {
}
});
app.post('/api/openchamber/session/:sessionID/plan/reject', async (req, res) => {
app.post('/api/openchamber/session/:sessionID/plan/reject', express.json({ limit: '1mb' }), async (req, res) => {
try {
const sessionId = asNonEmptyString(req.params?.sessionID);
const feedback = asNonEmptyString(req.body?.feedback);
@@ -1,5 +1,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createPlanGateRuntime } from './runtime.js';
import express from 'express';
import request from 'supertest';
import { createPlanGateRuntime, registerPlanGateRoutes } from './runtime.js';
const SESSION = 'ses_plan_test_1';
const DIRECTORY = '/repo';
@@ -229,3 +231,43 @@ describe('plan-gate runtime', () => {
expect(broadcasts.filter((e) => e.type === 'openchamber:plan-ready').length).toBe(0);
});
});
describe('plan-gate route middleware', () => {
it('parses JSON body without global middleware on approve route', async () => {
const runtime = createPlanGateRuntime({
globalEventHub: { subscribeEvent() { return () => {}; } },
buildOpenCodeUrl: (fetchPath) => `http://opencode.test${fetchPath}`,
getOpenCodeAuthHeaders: () => ({}),
broadcastGlobalUiEvent: () => {},
fetchImpl: vi.fn(async () => new Response(null, { status: 204 })),
planGateTimeoutMs: 60_000,
});
runtime.activate(SESSION, DIRECTORY);
const app = express();
registerPlanGateRoutes(app, runtime);
const res = await request(app)
.post(`/api/openchamber/session/${SESSION}/plan/approve`)
.send({ feedback: 'looks good' })
.expect(200);
expect(res.body.state).toBe('approved');
});
it('parses JSON body without global middleware on reject route', async () => {
const runtime = createPlanGateRuntime({
globalEventHub: { subscribeEvent() { return () => {}; } },
buildOpenCodeUrl: (fetchPath) => `http://opencode.test${fetchPath}`,
getOpenCodeAuthHeaders: () => ({}),
broadcastGlobalUiEvent: () => {},
fetchImpl: vi.fn(async () => new Response(null, { status: 204 })),
planGateTimeoutMs: 60_000,
});
runtime.activate(SESSION, DIRECTORY);
const app = express();
registerPlanGateRoutes(app, runtime);
const res = await request(app)
.post(`/api/openchamber/session/${SESSION}/plan/reject`)
.send({ feedback: 'too vague' })
.expect(200);
expect(res.body.state).toBe('rejected');
});
});