-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtui.test.ts
More file actions
159 lines (141 loc) · 5.4 KB
/
Copy pathtui.test.ts
File metadata and controls
159 lines (141 loc) · 5.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import { describe, expect, it } from "vitest";
import { SelectList } from "@earendil-works/pi-tui";
import { stripTerminalSequences } from "@earendil-works/pi-tui";
import { TranscriptView } from "../src/tui/transcript.js";
import { StatusBar } from "../src/tui/status-bar.js";
import { formatToolStart, formatToolResultLines } from "../src/tui/tool-view.js";
function plain(lines: string[]): string {
return lines.map((line) => stripTerminalSequences(line)).join("\n");
}
describe("TranscriptView", () => {
it("renders user messages", () => {
const view = new TranscriptView();
view.addUser("fix the login bug");
const rendered = plain(view.container.render(80));
expect(rendered).toContain("you");
expect(rendered).toContain("fix the login bug");
});
it("streams assistant output into an updatable entry and finalizes", () => {
const view = new TranscriptView();
const handle = view.startAssistant();
handle.update("partial ans");
let rendered = plain(view.container.render(80));
expect(rendered).toContain("partial ans");
handle.update("partial answer with more text");
rendered = plain(view.container.render(80));
expect(rendered).toContain("more text");
handle.finalize("**Done** with `code`");
rendered = plain(view.container.render(80));
expect(rendered).toContain("Done");
});
it("renders tool entries with result summaries", () => {
const view = new TranscriptView();
const entry = view.addToolEntry("bash", { command: "npm test" });
let rendered = plain(view.container.render(80));
expect(rendered).toContain("● bash npm test");
entry.complete({ command: "npm test", exitCode: 0, durationMs: 2400 }, false);
rendered = plain(view.container.render(80));
expect(rendered).toContain("✓ exit 0 · 2.4s");
});
it("renders failed tool results distinctly", () => {
const lines = plain(formatToolResultLines("bash", { exitCode: 1, durationMs: 3100 }, true));
expect(lines).toContain("✗ exit 1 · 3.1s");
});
it("shows edit diffs with add/remove markers", () => {
const rendered = plain(
formatToolResultLines(
"edit",
{ path: "a.ts", additions: 12, deletions: 3, replacements: 1, diff: "+ new line\n- old line" },
false,
),
);
expect(rendered).toContain("+12 -3");
expect(rendered).toContain("+ new line");
});
it("reports errors and info lines", () => {
const view = new TranscriptView();
view.addError("model unavailable");
view.addInfo("hint text");
const rendered = plain(view.container.render(80));
expect(rendered).toContain("error model unavailable");
expect(rendered).toContain("hint text");
});
});
describe("StatusBar", () => {
it("renders model, tokens, agents and session segments", () => {
const bar = new StatusBar();
bar.update({
model: "anthropic/claude-sonnet-4",
tokens: 12300,
agentsRunning: 2,
agentsMax: 3,
sessionId: "abcdef1234567890",
busy: true,
cwd: "/work/tinycode",
});
const rendered = plain(bar.component.render(240));
expect(rendered).toContain("anthropic/claude-sonnet-4");
expect(rendered).toContain("ctx ~12.3k");
expect(rendered).toContain("SUB-AGENTS 2/3 RUNNING");
expect(rendered).toContain("session abcdef12");
});
it("hides agent segment when none are running", () => {
const bar = new StatusBar();
bar.update({ model: "mock/m", tokens: 5, agentsRunning: 0, agentsMax: 3, busy: false, cwd: "/" });
const rendered = plain(bar.component.render(80));
expect(rendered).not.toContain("SUB-AGENTS");
});
});
describe("tool start formatting", () => {
it("summarizes tool arguments compactly", () => {
expect(plain([formatToolStart("read", { path: "src/index.ts" })])).toBe("● read src/index.ts");
expect(plain([formatToolStart("grep", { pattern: "needle", include: "*.ts" })])).toContain("needle");
});
});
describe("SelectList (permission dialog mechanics)", () => {
function makeList(): { list: SelectList; picked: string[] } {
const picked: string[] = [];
const list = new SelectList(
[
{ value: "once", label: "Allow once" },
{ value: "always", label: "Always allow this pattern" },
{ value: "deny", label: "Deny" },
],
5,
{
selectedPrefix: (t) => t,
selectedText: (t) => t,
description: (t) => t,
scrollInfo: (t) => t,
noMatch: (t) => t,
},
);
list.onSelect = (item) => picked.push(item.value);
return { list, picked };
}
it("defaults to the first option and confirms with Enter", () => {
const { list, picked } = makeList();
list.handleInput("\r");
expect(picked).toEqual(["once"]);
});
it("moves down with arrow keys before confirming", () => {
const { list, picked } = makeList();
list.handleInput("\x1b[B"); // down
list.handleInput("\r");
expect(picked).toEqual(["always"]);
});
it("supports two downs then enter for deny", () => {
const { list, picked } = makeList();
list.handleInput("\x1b[B");
list.handleInput("\x1b[B");
list.handleInput("\r");
expect(picked).toEqual(["deny"]);
});
it("renders all three options", () => {
const { list } = makeList();
const rendered = plain(list.render(60));
expect(rendered).toContain("Allow once");
expect(rendered).toContain("Always allow this pattern");
expect(rendered).toContain("Deny");
});
});