Files
penguin-harness/packages/server/test/steer.test.ts
T

99 lines
3.8 KiB
TypeScript

/**
* 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;
},
skipReconnectWait: () => false,
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<typeof apiClient>;
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);
});
});