feat(KB-641): merge kb/kb-641 (auto-resolved)

- feat(KB-641): complete Step 5 — add changeset
- feat(KB-641): complete Step 2 — update terminal route errors
This commit is contained in:
gsxdsm
2026-04-01 15:25:08 -07:00
parent 193f83074d
commit c4b611605e
3 changed files with 83 additions and 10 deletions

View File

@@ -4736,9 +4736,13 @@ describe("Terminal session routes", () => {
});
describe("POST /api/terminal/sessions", () => {
it("returns 503 when max sessions reached (session is null)", async () => {
it("returns 503 with specific max sessions error", async () => {
const mockService = {
createSession: vi.fn().mockResolvedValue(null),
createSession: vi.fn().mockResolvedValue({
success: false,
code: "max_sessions",
error: "Maximum terminal sessions reached. Please close an existing terminal and try again.",
}),
};
vi.spyOn(terminalServiceModule, "getTerminalService").mockReturnValue(mockService as any);
@@ -4751,7 +4755,69 @@ describe("Terminal session routes", () => {
);
expect(res.status).toBe(503);
expect(res.body.error).toContain("Max sessions");
expect(res.body).toEqual({
error: "Maximum terminal sessions reached. Please close an existing terminal and try again.",
code: "max_sessions",
});
vi.restoreAllMocks();
});
it.each([
["invalid_shell", 400, "Shell not allowed. Please use a supported shell (bash, zsh, sh, cmd, powershell)."],
["pty_load_failed", 503, "Terminal service unavailable. The PTY module could not be loaded."],
["pty_spawn_failed", 500, "Failed to start terminal shell process."],
] as const)("returns %s errors with the correct status and body", async (code, status, error) => {
const mockService = {
createSession: vi.fn().mockResolvedValue({
success: false,
code,
error,
}),
};
vi.spyOn(terminalServiceModule, "getTerminalService").mockReturnValue(mockService as any);
const res = await REQUEST(
buildApp(),
"POST",
"/api/terminal/sessions",
JSON.stringify({ shell: "/bad/shell" }),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(status);
expect(res.body).toEqual({ error, code });
vi.restoreAllMocks();
});
it("returns 201 for a successful session creation", async () => {
const mockService = {
createSession: vi.fn().mockResolvedValue({
success: true,
session: {
id: "term-123",
shell: "/bin/zsh",
cwd: "/fake/root",
},
}),
};
vi.spyOn(terminalServiceModule, "getTerminalService").mockReturnValue(mockService as any);
const res = await REQUEST(
buildApp(),
"POST",
"/api/terminal/sessions",
JSON.stringify({}),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(201);
expect(res.body).toEqual({
sessionId: "term-123",
shell: "/bin/zsh",
cwd: "/fake/root",
});
vi.restoreAllMocks();
});

View File

@@ -4145,21 +4145,28 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
const { cwd, cols, rows } = req.body;
const terminalService = getTerminalService(store.getRootDir());
const session = await terminalService.createSession({
const result = await terminalService.createSession({
cwd,
cols: typeof cols === "number" ? cols : undefined,
rows: typeof rows === "number" ? rows : undefined,
});
if (!session) {
res.status(503).json({ error: "Failed to create session. Max sessions may be reached." });
if (!result.success) {
const statusByCode = {
max_sessions: 503,
invalid_shell: 400,
pty_load_failed: 503,
pty_spawn_failed: 500,
} as const;
res.status(statusByCode[result.code]).json({ error: result.error, code: result.code });
return;
}
res.status(201).json({
sessionId: session.id,
shell: session.shell,
cwd: session.cwd,
sessionId: result.session.id,
shell: result.session.shell,
cwd: result.session.cwd,
});
} catch (err: any) {
res.status(500).json({ error: err.message || "Failed to create terminal session" });