Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions tests/tui/fixtures.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ export const sessionDetailFixture = {
ok: true,
session: {
...sessionsFixture[0],
branch: "task/cfd631a87666-feat-tui-session-detail-modal-full-session-drill",
provider: "openai",
model: "gpt-5.4",
tokensIn: 2300,
tokensOut: 1100,
runtimeMs: 30000,
turns: Array.from({ length: 18 }, (_, index) => ({
id: `turn-${index + 1}`,
number: index + 1,
timestamp: `2026-03-23T00:00:${String(index).padStart(2, "0")}.000Z`,
tokenDelta: 50 + index,
durationMs: 1000 + (index * 250),
lastToolCall: index % 2 === 0 ? "shell.exec" : "assistant.message",
})),
messages: [
{ timestamp: "2026-03-23T00:00:00.000Z", role: "user", content: "Start debugging" },
{ timestamp: "2026-03-23T00:00:05.000Z", role: "assistant", content: "Loaded the relevant logs" },
Expand All @@ -77,9 +91,15 @@ export const sessionDiffFixture = {
ok: true,
summary: "2 files changed",
diff: {
formatted: [
"diff --git a/src/app.mjs b/src/app.mjs",
"--- a/src/app.mjs",
"+++ b/src/app.mjs",
...Array.from({ length: 45 }, (_, index) => index % 3 === 0 ? `+added line ${index}` : index % 3 === 1 ? `-removed line ${index}` : ` context line ${index}`),
].join("\\n"),
files: [
{ filename: "src/app.mjs", additions: 8, deletions: 2 },
{ filename: "tests/app.test.mjs", additions: 4, deletions: 0 },
{ filename: "src/app.mjs", additions: 8, deletions: 2, patch: "+added line\n-removed line\n context" },
{ filename: "tests/app.test.mjs", additions: 4, deletions: 0, patch: "+test line" },
],
},
};
Expand Down Expand Up @@ -130,3 +150,4 @@ export function createMockWsClient() {
},
};
}

113 changes: 113 additions & 0 deletions tests/tui/screens.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,117 @@ describe("tui screen rendering", () => {
await view.unmount();
});

it("opens a session detail modal on enter and renders timeline, diff, and logs", async () => {
const bridge = createMockBridge();
const view = await renderInk(
React.createElement(AgentsScreen, {
wsBridge: bridge,
host: "127.0.0.1",
port: 3080,
sessions: sessionsFixture,
stats: monitorStatsFixture,
}),
{ columns: 220, rows: 60 },
);

await waitFor(() => view.text().includes("Backoff queue (1)"));
await view.press("`r", 80);
await waitFor(() => view.text().includes("Session Detail"));

expect(view.text()).toContain("Task ID");
expect(view.text()).toContain("Turn Timeline");
expect(view.text()).toContain("Latest Diff");
expect(view.text()).toContain("Stdout");
expect(view.text()).toContain("[S]teer");

await view.unmount();
});

it("sends a steer message from session detail and shows confirmation", async () => {
const bridge = createMockBridge();
const view = await renderInk(
React.createElement(AgentsScreen, {
wsBridge: bridge,
host: "127.0.0.1",
port: 3080,
sessions: sessionsFixture,
stats: monitorStatsFixture,
}),
{ columns: 220, rows: 60 },
);

await waitFor(() => view.text().includes("Backoff queue (1)"));
await view.press("`r", 80);
await waitFor(() => view.text().includes("Session Detail"));

await view.press("s", 40);
await waitFor(() => view.text().includes("Steer message:"));
await view.press("Please continue with focused logging", 50);
await view.press("`r", 100);

await waitFor(() => view.text().includes("Steer sent ✓"));
expect(globalThis.fetch).toHaveBeenCalledWith(
expect.stringContaining("/api/sessions/session-active-1/message?workspace=all"),
expect.objectContaining({ method: "POST" }),
);

await view.unmount();
});

it("scrolls the turn timeline independently inside session detail", async () => {
const bridge = createMockBridge();
const view = await renderInk(
React.createElement(AgentsScreen, {
wsBridge: bridge,
host: "127.0.0.1",
port: 3080,
sessions: sessionsFixture,
stats: monitorStatsFixture,
}),
{ columns: 220, rows: 28 },
);

await waitFor(() => view.text().includes("Backoff queue (1)"));
await view.press("`r", 80);
await waitFor(() => view.text().includes("Session Detail"));

expect(view.text()).toContain("| 2026-03-23 00:00:00.000 |");
expect(view.text()).not.toContain("| 2026-03-23 00:00:17.000 |");

await view.press("\u001b[6~", 80);
await waitFor(() => view.text().includes("| 2026-03-23 00:00:17.000 |"));

await view.unmount();
});

it("streams live stdout into the right panel while detail is open", async () => {
const bridge = createMockBridge();
const view = await renderInk(
React.createElement(AgentsScreen, {
wsBridge: bridge,
host: "127.0.0.1",
port: 3080,
sessions: sessionsFixture,
stats: monitorStatsFixture,
}),
{ columns: 220, rows: 60 },
);

await waitFor(() => view.text().includes("Backoff queue (1)"));
await view.press("`r", 80);
await waitFor(() => view.text().includes("Session Detail"));

bridge.emit("logs:stream", {
sessionId: "session-active-1",
timestamp: "2026-03-23T00:00:31.000Z",
stream: "stdout",
line: "Steer message accepted by running session",
Comment on lines +223 to +226
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test emits a logs:stream payload with {sessionId, stream, line}, but the repo’s canonical logs:stream event contract requires {logType, raw, line, level, timestamp, filePath} (and forbids additional properties). The test should use the canonical shape (or, if the intent is to add sessionId to the contract, update the schema + emitter and then align this test with that new contract).

Suggested change
sessionId: "session-active-1",
timestamp: "2026-03-23T00:00:31.000Z",
stream: "stdout",
line: "Steer message accepted by running session",
logType: "stdout",
raw: "Steer message accepted by running session",
line: "Steer message accepted by running session",
level: "info",
timestamp: "2026-03-23T00:00:31.000Z",
filePath: "",

Copilot uses AI. Check for mistakes.
});

await waitFor(() => view.text().includes("Steer message accepted by running session"));
await view.unmount();
});

it("navigates app tabs with numeric key input", async () => {
const bridge = createMockBridge();
const view = await renderInk(
Expand Down Expand Up @@ -151,3 +262,5 @@ describe("tui screen rendering", () => {
expect(bridge.disconnect).toHaveBeenCalled();
});
});


Comment on lines +265 to +266
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing blank lines at end of file were introduced here; please remove to keep diffs clean.

Suggested change

Copilot uses AI. Check for mistakes.
Loading
Loading