-
Notifications
You must be signed in to change notification settings - Fork 113
Expand file tree
/
Copy pathmessage-view.test.ts
More file actions
326 lines (284 loc) · 12.2 KB
/
Copy pathmessage-view.test.ts
File metadata and controls
326 lines (284 loc) · 12.2 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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
import { test } from "node:test";
import assert from "node:assert/strict";
import React from "react";
import { renderToString } from "ink";
import { parseDiffPreview } from "../ui";
import { MessageView, getPromptEchoContentWidth } from "../ui/components/MessageView";
import {
buildThinkingSummary,
formatBashStatusParams,
formatToolStatusParams,
renderMessageToStdout,
getUpdatePlanPreviewLines,
parseToolPayload,
} from "../ui/components/MessageView/utils";
import { RawMode } from "../ui/contexts";
import type { SessionMessage } from "../session";
import type { ToolSummary } from "../ui/components/MessageView/types";
test("parseDiffPreview removes headers and classifies lines", () => {
const lines = parseDiffPreview(
["--- a/file.txt", "+++ b/file.txt", "@@ -1,1 +1,1 @@", " context", "-old", "+new"].join("\n")
);
assert.deepEqual(lines, [
{ marker: " ", content: "context", kind: "context" },
{ marker: "-", content: "old", kind: "removed" },
{ marker: "+", content: "new", kind: "added" },
]);
});
test("parseDiffPreview keeps nonstandard context lines", () => {
const lines = parseDiffPreview("...\n+added");
assert.deepEqual(lines, [
{ marker: " ", content: "...", kind: "context" },
{ marker: "+", content: "added", kind: "added" },
]);
});
test("MessageView summarizes thinking content across lines", () => {
assert.equal(
buildThinkingSummary("Plan:\n\nInspect the code and update tests", null, RawMode.Lite),
"Plan: Inspect the code and update tests"
);
});
test("MessageView removes a trailing colon from thinking summary", () => {
assert.equal(buildThinkingSummary("Planning:", null, RawMode.Lite), "Planning");
});
test("MessageView falls back to a reasoning placeholder for hidden reasoning content in Lite mode", () => {
assert.equal(
buildThinkingSummary("", { reasoning_content: "hidden chain of thought" }, RawMode.Lite),
"(reasoning...)"
);
});
test("MessageView shows full reasoning content in Normal/Raw mode", () => {
assert.equal(
buildThinkingSummary("", { reasoning_content: "hidden chain of thought" }, RawMode.None),
"hidden chain of thought"
);
assert.equal(
buildThinkingSummary("", { reasoning_content: "hidden chain of thought" }, RawMode.Raw),
"hidden chain of thought"
);
});
test("formatBashStatusParams compacts multi-line commands and keeps the final description", () => {
assert.equal(
formatBashStatusParams('python3 -c "\nprint(1)\nprint(2)\n" # Run inline script'),
'python3 -c " ... " # Run inline script'
);
});
test("formatToolStatusParams preserves compacted Bash params but truncates other tools", () => {
assert.equal(
formatToolStatusParams({
name: "bash",
params: "cat <<'EOF'\nhello\nEOF # Print heredoc",
ok: true,
metadata: null,
}),
"cat <<'EOF' ... EOF # Print heredoc"
);
assert.equal(formatToolStatusParams({ name: "read", params: "first\nsecond", ok: true, metadata: null }), "first");
});
// --- renderMessageToStdout tests ---
function makeSessionMessage(overrides: Partial<SessionMessage> & Pick<SessionMessage, "role">): SessionMessage {
const now = new Date().toISOString();
return {
id: overrides.id ?? `test-${Math.random().toString(36).slice(2)}`,
sessionId: overrides.sessionId ?? "test-session",
role: overrides.role,
content: overrides.content ?? null,
visible: overrides.visible ?? true,
compacted: overrides.compacted ?? false,
createTime: overrides.createTime ?? now,
updateTime: overrides.updateTime ?? now,
contentParams: overrides.contentParams ?? null,
messageParams: overrides.messageParams ?? null,
meta: overrides.meta,
html: overrides.html,
};
}
test("renderMessageToStdout returns empty for invisible messages", () => {
const msg = makeSessionMessage({ role: "user", content: "hello", visible: false });
assert.equal(renderMessageToStdout(msg, RawMode.Raw), "");
});
test("renderMessageToStdout renders user messages with > prefix", () => {
const msg = makeSessionMessage({ role: "user", content: "fix the bug" });
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes("> fix the bug"));
});
test("renderMessageToStdout shows (no content) for empty user messages", () => {
const msg = makeSessionMessage({ role: "user", content: "" });
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes("(no content)"));
});
test("MessageView echoes submitted user prompts with live prompt wrapping width", () => {
assert.equal(getPromptEchoContentWidth(8), 6);
const msg = makeSessionMessage({ role: "user", content: "abcdefg" });
const output = renderToString(React.createElement(MessageView, { message: msg, width: 8 }), { columns: 8 });
assert.equal(output, "> abcdef\n g\n");
});
test("MessageView echoes model changes with submitted prompt wrapping", () => {
const msg = makeSessionMessage({
role: "system",
content: "abcdefgh",
meta: { isModelChange: true },
});
const output = renderToString(React.createElement(MessageView, { message: msg, width: 8 }), { columns: 8 });
assert.equal(output, "> abcdef\n gh\n");
});
test("renderMessageToStdout renders assistant non-thinking messages with ✦", () => {
const msg = makeSessionMessage({ role: "assistant", content: "Here is the fix" });
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes("✦"));
assert.ok(output.includes("Here is the fix"));
});
test("renderMessageToStdout renders assistant thinking messages with ✧ Thinking", () => {
const msg = makeSessionMessage({
role: "assistant",
content: "Plan:\nAnalyze the code",
meta: { asThinking: true },
});
const output = renderMessageToStdout(msg, RawMode.Lite);
assert.ok(output.includes("✧"));
assert.ok(output.includes("Thinking"));
assert.ok(output.includes("Plan: Analyze the code"));
});
test("renderMessageToStdout renders tool messages with ✧ and tool name", () => {
const payload = JSON.stringify({ name: "read", ok: true });
const msg = makeSessionMessage({ role: "tool", content: payload });
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes("✧"));
assert.ok(output.includes("Read"));
});
test("renderMessageToStdout renders tool messages with resultMd output", () => {
const payload = JSON.stringify({ name: "read", ok: true });
const msg = makeSessionMessage({
role: "tool",
content: payload,
meta: { resultMd: "File content:\n line 1\n line 2" },
});
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes("✧"));
assert.ok(output.includes("Read"));
assert.ok(output.includes("└ Result"));
assert.ok(output.includes("File content:"));
assert.ok(output.includes("line 1"));
});
test("renderMessageToStdout compacts multi-line Bash params", () => {
const payload = JSON.stringify({ name: "bash", ok: true });
const msg = makeSessionMessage({
role: "tool",
content: payload,
meta: { paramsMd: 'python3 -c "\nprint(1)\nprint(2)\n" # Run inline script' },
});
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes('python3 -c " ... " # Run inline script'));
assert.ok(!output.includes("print(1)"));
});
test("renderMessageToStdout renders UpdatePlan tool messages with Plan preview and resultMd", () => {
const payload = JSON.stringify({
name: "UpdatePlan",
ok: true,
metadata: { plan: "Step 1: Analyze\nStep 2: Implement\nStep 3: Test" },
});
const msg = makeSessionMessage({
role: "tool",
content: payload,
meta: { resultMd: "Plan updated successfully" },
});
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes("UpdatePlan"));
assert.ok(output.includes("└ Plan"));
assert.ok(output.includes("Step 1: Analyze"));
assert.ok(output.includes(" Result"));
assert.ok(output.includes("Plan updated successfully"));
});
test("renderMessageToStdout renders UpdatePlan tool messages with Plan preview", () => {
const payload = JSON.stringify({
name: "UpdatePlan",
ok: true,
metadata: { plan: "Step 1: Analyze\nStep 2: Implement\nStep 3: Test" },
});
const msg = makeSessionMessage({ role: "tool", content: payload });
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes("UpdatePlan"));
assert.ok(output.includes("└ Plan"));
assert.ok(output.includes("Step 1: Analyze"));
assert.ok(output.includes("Step 2: Implement"));
// Verify resultMd is NOT included when meta.resultMd is absent
assert.ok(!output.includes("└ Result"));
});
test("renderMessageToStdout renders system model change messages", () => {
const msg = makeSessionMessage({
role: "system",
content: "Switched to deepseek-v4-pro",
meta: { isModelChange: true },
});
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes("> Switched to deepseek-v4-pro"));
});
test("renderMessageToStdout renders system skill load messages", () => {
const msg = makeSessionMessage({
role: "system",
content: "",
meta: { skill: { name: "code-review", path: "", description: "" } },
});
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes("⚡ Loaded skill: code-review"));
});
test("renderMessageToStdout renders system summary messages", () => {
const msg = makeSessionMessage({
role: "system",
content: "",
meta: { isSummary: true },
});
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes("(conversation summary inserted)"));
});
test("renderMessageToStdout returns empty for unknown system messages", () => {
const msg = makeSessionMessage({ role: "system", content: "" });
assert.equal(renderMessageToStdout(msg, RawMode.Raw), "");
});
// --- getUpdatePlanPreviewLines tests ---
test("getUpdatePlanPreviewLines returns empty for failed tool", () => {
const summary: ToolSummary = { name: "UpdatePlan", params: "", ok: false, metadata: { plan: "Step 1" } };
assert.deepEqual(getUpdatePlanPreviewLines(summary), []);
});
test("getUpdatePlanPreviewLines returns empty for non-UpdatePlan tool", () => {
const summary: ToolSummary = { name: "edit", params: "", ok: true, metadata: { plan: "Step 1" } };
assert.deepEqual(getUpdatePlanPreviewLines(summary), []);
});
test("getUpdatePlanPreviewLines returns empty for missing plan metadata", () => {
const summary: ToolSummary = { name: "UpdatePlan", params: "", ok: true, metadata: null };
assert.deepEqual(getUpdatePlanPreviewLines(summary), []);
});
test("getUpdatePlanPreviewLines returns empty for empty plan string", () => {
const summary: ToolSummary = { name: "UpdatePlan", params: "", ok: true, metadata: { plan: "" } };
assert.deepEqual(getUpdatePlanPreviewLines(summary), []);
});
test("getUpdatePlanPreviewLines extracts plan lines and filters empty rows", () => {
const summary: ToolSummary = {
name: "UpdatePlan",
params: "",
ok: true,
metadata: { plan: "Step 1: Analyze\n\nStep 2: Implement\n \nStep 3: Test" },
};
assert.deepEqual(getUpdatePlanPreviewLines(summary), ["Step 1: Analyze", "Step 2: Implement", "Step 3: Test"]);
});
// --- parseToolPayload tests ---
test("parseToolPayload returns defaults for null content", () => {
const result = parseToolPayload(null);
assert.deepEqual(result, { name: null, ok: true, metadata: null });
});
test("parseToolPayload returns defaults for invalid JSON", () => {
const result = parseToolPayload("not valid json");
assert.deepEqual(result, { name: null, ok: true, metadata: null });
});
test("parseToolPayload parses valid JSON with name/ok/metadata", () => {
const result = parseToolPayload(JSON.stringify({ name: "read", ok: true, metadata: { file: "src/index.ts" } }));
assert.deepEqual(result, { name: "read", ok: true, metadata: { file: "src/index.ts" } });
});
test("parseToolPayload respects ok: false", () => {
const result = parseToolPayload(JSON.stringify({ name: "bash", ok: false, metadata: null }));
assert.deepEqual(result, { name: "bash", ok: false, metadata: null });
});
test("parseToolPayload trims whitespace from name", () => {
const result = parseToolPayload(JSON.stringify({ name: " read ", ok: true }));
assert.equal(result.name, "read");
});