/** * Integration tests for POST /api/sessions/:id/steer (mid-run steering): * - 202 while a Task is running, forwarding the trimmed text to the core session; * - 400 for empty / non-string text; * - 409 not_running when the Session is idle (the frontend then falls back to a * normal task POST); * - 404 for foreign/unknown sessions (via the shared resolveSession lookup). */ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { approvalDecision, assistantText, toolCall, userText } from "@prismshadow/penguin-core"; import type { ApproveFn, OmniMessage } from "@prismshadow/penguin-core"; import type { SessionRow } from "../src/db/repos/sessions.js"; import type { RuntimeSession } from "../src/runtime/session-manager.js"; import { apiClient, createTestApp, provisionUser, waitFor } from "./helpers.js"; import type { TestApp } from "./helpers.js"; const SID = "session-2026-07-06-10-00-00-ccdd0001"; /** Fake Session that parks on one approval (keeps the Task running) and records steer calls. */ function steeringFakeSession(sessionId: string, steered: string[]): RuntimeSession { return { sessionId, toolPermission: () => "rw", generateTitle: async () => ({ title: null, usage: null }), compactability: () => "ok" as const, steer: (text: string) => { steered.push(text); return true; }, async *run(_input: OmniMessage[], opts: { approve: ApproveFn; signal: AbortSignal }) { const tc = toolCall({ name: "exec_command", arguments: "{}", toolCallId: "tc-steer" }); yield tc; const decision = await opts.approve(tc); yield approvalDecision(decision, "tc-steer"); yield assistantText("done"); }, async *compact() {}, }; } describe("steer route", () => { let t: TestApp; let api: ReturnType; let steered: string[]; beforeEach(async () => { t = await createTestApp(); const { cookie } = await provisionUser(t.app, "steerer"); api = apiClient(t.app, cookie); const row: SessionRow = { sessionId: SID, projectId: "steerer-default_project", agentId: "default_agent", modelId: "m1", provider: "custom", workspace: "/tmp/w", approvalMode: "always-ask", title: null, createdAt: new Date().toISOString(), }; t.deps.sessionsRepo.insert(row); steered = []; t.deps.manager.adopt(row, steeringFakeSession(SID, steered)); }); afterEach(async () => { await t.cleanup(); }); it("idle → 409 not_running (the frontend falls back to a normal task POST)", async () => { const res = await api.post(`/api/sessions/${SID}/steer`, { text: "hello" }); expect(res.status).toBe(409); const body = (await res.json()) as { error: { code: string } }; expect(body.error.code).toBe("not_running"); expect(steered).toEqual([]); }); it("running → 202, the trimmed text reaches the core session; empty text → 400", async () => { await t.deps.manager.startTask(SID, [userText("go")]); await waitFor(() => t.deps.manager.pendingApprovalCount(SID) === 1); expect((await api.post(`/api/sessions/${SID}/steer`, { text: " " })).status).toBe(400); expect((await api.post(`/api/sessions/${SID}/steer`, { text: 42 })).status).toBe(400); expect(steered).toEqual([]); const ok = await api.post(`/api/sessions/${SID}/steer`, { text: " focus on tests " }); expect(ok.status).toBe(202); expect(steered).toEqual(["focus on tests"]); t.deps.manager.decideApproval(SID, "tc-steer", "allow"); await waitFor(() => t.deps.manager.statusOf(SID) === "idle"); }); it("unknown session → 404", async () => { const res = await api.post(`/api/sessions/session-ghost/steer`, { text: "x" }); expect(res.status).toBe(404); }); });