3d3495528c
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
99 lines
3.8 KiB
TypeScript
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);
|
|
});
|
|
});
|