From ec36ee421aaac6b822fce6c09b8e7c6a26cde110 Mon Sep 17 00:00:00 2001 From: mptyl Date: Sun, 28 Jun 2026 16:16:08 +0200 Subject: [PATCH] feat(backend): POST /sessions applies global settings, body is question-only workspace/provider/model/thinking now come from getSettings() injected into sessionRoutes; the request body supplies only question+name. Also teaches fake_pi_rpc to respond to set_model and set_thinking_level RPC commands so tests that pass real model settings don't hang. Co-Authored-By: Claude Opus 4.8 --- backend/src/routes/sessions.ts | 31 ++++++++++++++++++++------- backend/test/routes-sessions.test.ts | 12 ++++++++--- harness/tests/fake_pi/fake_pi_rpc.mjs | 4 ++++ 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/backend/src/routes/sessions.ts b/backend/src/routes/sessions.ts index c64c48ed..5f0bc0c3 100644 --- a/backend/src/routes/sessions.ts +++ b/backend/src/routes/sessions.ts @@ -2,17 +2,32 @@ import type { FastifyInstance } from "fastify"; import type { PiProcessManager } from "../pi/pi-process-manager.js"; import type { ThtRunner } from "../tht/tht-runner.js"; import type { SseHub } from "../sse/sse-hub.js"; +import type { Settings } from "../settings/settings-store.js"; import { getUser } from "../auth/auth.js"; -export function sessionRoutes(app: FastifyInstance, d: { mgr: PiProcessManager; tht: ThtRunner; hub: SseHub }) { +export function sessionRoutes( + app: FastifyInstance, + d: { mgr: PiProcessManager; tht: ThtRunner; hub: SseHub; getSettings: () => Settings }, +) { app.post("/sessions", async (req, reply) => { - const b = req.body as any; - // NOTE (MVP): `workspace` selects the tht `-c ` for the backend's own - // tht calls (session new/list/show). The gate/Pi side still uses the symlinked - // `config/tht.yaml` (single-workspace). Full multi-workspace on the Pi side is - // future work — the gate would need a THT_CONFIG/workspace env passed at spawn. - const { id } = await d.tht.sessionNew({ question: b.question, provider: b.provider, model: b.model, thinking: b.thinking, name: b.name, workspace: b.workspace }); - const rt = await d.mgr.spawnFor(id, { provider: b.provider, model: b.model, thinking: b.thinking, author: getUser(req).id }); + const b = req.body as { question: string; name?: string }; + const s = d.getSettings(); + // Settings (global) supply workspace/provider/model/thinking. The new-question + // form sends only the question text. `workspace` selects the tht `-c `. + const { id } = await d.tht.sessionNew({ + question: b.question, + name: b.name, + workspace: s.workspace, + provider: s.provider, + model: s.model, + thinking: s.thinking, + }); + const rt = await d.mgr.spawnFor(id, { + provider: s.provider, + model: s.model, + thinking: s.thinking, + author: getUser(req).id, + }); rt.bridge.onClientEvent((e) => d.hub.publish(id, e.type, e)); return { id }; }); diff --git a/backend/test/routes-sessions.test.ts b/backend/test/routes-sessions.test.ts index f3e36e58..7338ebf5 100644 --- a/backend/test/routes-sessions.test.ts +++ b/backend/test/routes-sessions.test.ts @@ -7,18 +7,23 @@ import { loadConfig } from "../src/config.js"; const FAKE = path.resolve("../harness/tests/fake_pi/fake_pi_rpc.mjs"); const SCRIPT = path.resolve("../harness/tests/fake_pi/scripts/f1_disambiguation.json"); -test("POST /sessions crea e avvia, GET /sessions lista; inoltra workspace a sessionNew", async () => { +test("POST /sessions usa i settings (workspace/provider/model/thinking) e crea+avvia", async () => { let sessionNewArg: any; + let spawnArg: any; const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), { thtRunner: { sessionNew: async (o: any) => { sessionNewArg = o; return { id: "s1" }; }, sessionList: async () => [{ id: "s1" }], } as any, + getSettings: () => ({ workspace: "w", provider: "zai", model: "glm-5.2", thinking: "high" }), spawnFn: () => spawn("node", [FAKE, SCRIPT]) as any, }); - const created = await app.inject({ method: "POST", url: "/sessions", payload: { workspace: "w", question: "q" } }); + const created = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } }); expect(created.json()).toEqual({ id: "s1" }); expect(sessionNewArg.workspace).toBe("w"); + expect(sessionNewArg.provider).toBe("zai"); + expect(sessionNewArg.model).toBe("glm-5.2"); + expect(sessionNewArg.thinking).toBe("high"); expect(sessionNewArg.question).toBe("q"); const list = await app.inject({ method: "GET", url: "/sessions" }); expect(list.json()).toEqual([{ id: "s1" }]); @@ -27,9 +32,10 @@ test("POST /sessions crea e avvia, GET /sessions lista; inoltra workspace a sess test("POST /sessions/:id/response inoltra al bridge (no error)", async () => { const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), { thtRunner: { sessionNew: async () => ({ id: "s1" }) } as any, + getSettings: () => ({ workspace: "w" }), spawnFn: () => spawn("node", [FAKE, SCRIPT]) as any, }); - await app.inject({ method: "POST", url: "/sessions", payload: { workspace: "w", question: "q" } }); + await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } }); const res = await app.inject({ method: "POST", url: "/sessions/s1/response", payload: { ui_response: { id: "u1", choices: ["a"] } } }); expect(res.statusCode).toBe(204); diff --git a/harness/tests/fake_pi/fake_pi_rpc.mjs b/harness/tests/fake_pi/fake_pi_rpc.mjs index 43b8d21e..4932a5e9 100644 --- a/harness/tests/fake_pi/fake_pi_rpc.mjs +++ b/harness/tests/fake_pi/fake_pi_rpc.mjs @@ -27,6 +27,10 @@ process.stdin.on("data", (chunk) => { } else if (cmd.type === "get_state") { out({ type: "response", command: "get_state", id: cmd.id, success: true, data: { sessionId: "fake", thinkingLevel: "medium", isStreaming: false } }); + } else if (cmd.type === "set_model") { + out({ type: "response", command: "set_model", id: cmd.id, success: true }); + } else if (cmd.type === "set_thinking_level") { + out({ type: "response", command: "set_thinking_level", id: cmd.id, success: true }); } } });