11import { test } from "node:test" ;
22import assert from "node:assert/strict" ;
3- import { MessageView , parseDiffPreview } from "../ui" ;
3+ import { parseDiffPreview } from "../ui" ;
4+ import {
5+ buildThinkingSummary ,
6+ renderMessageToStdout ,
7+ getUpdatePlanPreviewLines ,
8+ parseToolPayload ,
9+ } from "../ui/compoments/MessageView/utils" ;
10+ import { RawMode } from "../ui/contexts" ;
411import type { SessionMessage } from "../session" ;
12+ import type { ToolSummary } from "../ui/compoments/MessageView/types" ;
513
614test ( "parseDiffPreview removes headers and classifies lines" , ( ) => {
715 const lines = parseDiffPreview (
@@ -25,45 +33,237 @@ test("parseDiffPreview keeps nonstandard context lines", () => {
2533
2634test ( "MessageView summarizes thinking content across lines" , ( ) => {
2735 assert . equal (
28- getThinkingParams ( {
29- content : "Plan:\n\nInspect the code and update tests" ,
30- } ) ,
36+ buildThinkingSummary ( "Plan:\n\nInspect the code and update tests" , null , RawMode . Lite ) ,
3137 "Plan: Inspect the code and update tests"
3238 ) ;
3339} ) ;
3440
35- test ( "MessageView removes a trailing colon from thinking summaries " , ( ) => {
36- assert . equal ( getThinkingParams ( { content : "Planning:" } ) , "Planning" ) ;
41+ test ( "MessageView removes a trailing colon from thinking summary " , ( ) => {
42+ assert . equal ( buildThinkingSummary ( "Planning:" , null , RawMode . Lite ) , "Planning" ) ;
3743} ) ;
3844
39- test ( "MessageView falls back to a reasoning placeholder for hidden reasoning content" , ( ) => {
45+ test ( "MessageView falls back to a reasoning placeholder for hidden reasoning content in Lite mode " , ( ) => {
4046 assert . equal (
41- getThinkingParams ( {
42- content : "" ,
43- messageParams : { reasoning_content : "hidden chain of thought" } ,
44- } ) ,
47+ buildThinkingSummary ( "" , { reasoning_content : "hidden chain of thought" } , RawMode . Lite ) ,
4548 "(reasoning...)"
4649 ) ;
4750} ) ;
4851
49- function getThinkingParams ( overrides : Partial < SessionMessage > ) : string {
50- const view = MessageView ( { message : buildAssistantMessage ( overrides ) } ) as any ;
51- return view . props . children . props . params ;
52- }
52+ test ( "MessageView shows full reasoning content in Normal/Raw mode" , ( ) => {
53+ assert . equal (
54+ buildThinkingSummary ( "" , { reasoning_content : "hidden chain of thought" } , RawMode . None ) ,
55+ "hidden chain of thought"
56+ ) ;
57+ assert . equal (
58+ buildThinkingSummary ( "" , { reasoning_content : "hidden chain of thought" } , RawMode . Raw ) ,
59+ "hidden chain of thought"
60+ ) ;
61+ } ) ;
5362
54- function buildAssistantMessage ( overrides : Partial < SessionMessage > ) : SessionMessage {
63+ // --- renderMessageToStdout tests ---
64+
65+ function makeSessionMessage ( overrides : Partial < SessionMessage > & Pick < SessionMessage , "role" > ) : SessionMessage {
66+ const now = new Date ( ) . toISOString ( ) ;
5567 return {
56- id : "message-1" ,
57- sessionId : "session-1" ,
68+ id : overrides . id ?? `test-${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` ,
69+ sessionId : overrides . sessionId ?? "test-session" ,
70+ role : overrides . role ,
71+ content : overrides . content ?? null ,
72+ visible : overrides . visible ?? true ,
73+ compacted : overrides . compacted ?? false ,
74+ createTime : overrides . createTime ?? now ,
75+ updateTime : overrides . updateTime ?? now ,
76+ contentParams : overrides . contentParams ?? null ,
77+ messageParams : overrides . messageParams ?? null ,
78+ meta : overrides . meta ,
79+ html : overrides . html ,
80+ } ;
81+ }
82+
83+ test ( "renderMessageToStdout returns empty for invisible messages" , ( ) => {
84+ const msg = makeSessionMessage ( { role : "user" , content : "hello" , visible : false } ) ;
85+ assert . equal ( renderMessageToStdout ( msg , RawMode . Raw ) , "" ) ;
86+ } ) ;
87+
88+ test ( "renderMessageToStdout renders user messages with > prefix" , ( ) => {
89+ const msg = makeSessionMessage ( { role : "user" , content : "fix the bug" } ) ;
90+ const output = renderMessageToStdout ( msg , RawMode . Raw ) ;
91+ assert . ok ( output . includes ( "> fix the bug" ) ) ;
92+ } ) ;
93+
94+ test ( "renderMessageToStdout shows (no content) for empty user messages" , ( ) => {
95+ const msg = makeSessionMessage ( { role : "user" , content : "" } ) ;
96+ const output = renderMessageToStdout ( msg , RawMode . Raw ) ;
97+ assert . ok ( output . includes ( "(no content)" ) ) ;
98+ } ) ;
99+
100+ test ( "renderMessageToStdout renders assistant non-thinking messages with ✦" , ( ) => {
101+ const msg = makeSessionMessage ( { role : "assistant" , content : "Here is the fix" } ) ;
102+ const output = renderMessageToStdout ( msg , RawMode . Raw ) ;
103+ assert . ok ( output . includes ( "✦" ) ) ;
104+ assert . ok ( output . includes ( "Here is the fix" ) ) ;
105+ } ) ;
106+
107+ test ( "renderMessageToStdout renders assistant thinking messages with ✧ Thinking" , ( ) => {
108+ const msg = makeSessionMessage ( {
58109 role : "assistant" ,
59- content : "" ,
60- contentParams : null ,
61- messageParams : null ,
62- compacted : false ,
63- visible : true ,
64- createTime : "2026-01-01T00:00:00.000Z" ,
65- updateTime : "2026-01-01T00:00:00.000Z" ,
110+ content : "Plan:\nAnalyze the code" ,
66111 meta : { asThinking : true } ,
67- ...overrides ,
112+ } ) ;
113+ const output = renderMessageToStdout ( msg , RawMode . Lite ) ;
114+ assert . ok ( output . includes ( "✧" ) ) ;
115+ assert . ok ( output . includes ( "Thinking" ) ) ;
116+ assert . ok ( output . includes ( "Plan: Analyze the code" ) ) ;
117+ } ) ;
118+
119+ test ( "renderMessageToStdout renders tool messages with ✧ and tool name" , ( ) => {
120+ const payload = JSON . stringify ( { name : "read" , ok : true } ) ;
121+ const msg = makeSessionMessage ( { role : "tool" , content : payload } ) ;
122+ const output = renderMessageToStdout ( msg , RawMode . Raw ) ;
123+ assert . ok ( output . includes ( "✧" ) ) ;
124+ assert . ok ( output . includes ( "Read" ) ) ;
125+ } ) ;
126+
127+ test ( "renderMessageToStdout renders tool messages with resultMd output" , ( ) => {
128+ const payload = JSON . stringify ( { name : "read" , ok : true } ) ;
129+ const msg = makeSessionMessage ( {
130+ role : "tool" ,
131+ content : payload ,
132+ meta : { resultMd : "File content:\n line 1\n line 2" } ,
133+ } ) ;
134+ const output = renderMessageToStdout ( msg , RawMode . Raw ) ;
135+ assert . ok ( output . includes ( "✧" ) ) ;
136+ assert . ok ( output . includes ( "Read" ) ) ;
137+ assert . ok ( output . includes ( "└ Result" ) ) ;
138+ assert . ok ( output . includes ( "File content:" ) ) ;
139+ assert . ok ( output . includes ( "line 1" ) ) ;
140+ } ) ;
141+
142+ test ( "renderMessageToStdout renders UpdatePlan tool messages with Plan preview and resultMd" , ( ) => {
143+ const payload = JSON . stringify ( {
144+ name : "UpdatePlan" ,
145+ ok : true ,
146+ metadata : { plan : "Step 1: Analyze\nStep 2: Implement\nStep 3: Test" } ,
147+ } ) ;
148+ const msg = makeSessionMessage ( {
149+ role : "tool" ,
150+ content : payload ,
151+ meta : { resultMd : "Plan updated successfully" } ,
152+ } ) ;
153+ const output = renderMessageToStdout ( msg , RawMode . Raw ) ;
154+ assert . ok ( output . includes ( "UpdatePlan" ) ) ;
155+ assert . ok ( output . includes ( "└ Plan" ) ) ;
156+ assert . ok ( output . includes ( "Step 1: Analyze" ) ) ;
157+ assert . ok ( output . includes ( " Result" ) ) ;
158+ assert . ok ( output . includes ( "Plan updated successfully" ) ) ;
159+ } ) ;
160+
161+ test ( "renderMessageToStdout renders UpdatePlan tool messages with Plan preview" , ( ) => {
162+ const payload = JSON . stringify ( {
163+ name : "UpdatePlan" ,
164+ ok : true ,
165+ metadata : { plan : "Step 1: Analyze\nStep 2: Implement\nStep 3: Test" } ,
166+ } ) ;
167+ const msg = makeSessionMessage ( { role : "tool" , content : payload } ) ;
168+ const output = renderMessageToStdout ( msg , RawMode . Raw ) ;
169+ assert . ok ( output . includes ( "UpdatePlan" ) ) ;
170+ assert . ok ( output . includes ( "└ Plan" ) ) ;
171+ assert . ok ( output . includes ( "Step 1: Analyze" ) ) ;
172+ assert . ok ( output . includes ( "Step 2: Implement" ) ) ;
173+ // Verify resultMd is NOT included when meta.resultMd is absent
174+ assert . ok ( ! output . includes ( "└ Result" ) ) ;
175+ } ) ;
176+
177+ test ( "renderMessageToStdout renders system model change messages" , ( ) => {
178+ const msg = makeSessionMessage ( {
179+ role : "system" ,
180+ content : "Switched to deepseek-v4-pro" ,
181+ meta : { isModelChange : true } ,
182+ } ) ;
183+ const output = renderMessageToStdout ( msg , RawMode . Raw ) ;
184+ assert . ok ( output . includes ( "> Switched to deepseek-v4-pro" ) ) ;
185+ } ) ;
186+
187+ test ( "renderMessageToStdout renders system skill load messages" , ( ) => {
188+ const msg = makeSessionMessage ( {
189+ role : "system" ,
190+ content : "" ,
191+ meta : { skill : { name : "code-review" , path : "" , description : "" } } ,
192+ } ) ;
193+ const output = renderMessageToStdout ( msg , RawMode . Raw ) ;
194+ assert . ok ( output . includes ( "⚡ Loaded skill: code-review" ) ) ;
195+ } ) ;
196+
197+ test ( "renderMessageToStdout renders system summary messages" , ( ) => {
198+ const msg = makeSessionMessage ( {
199+ role : "system" ,
200+ content : "" ,
201+ meta : { isSummary : true } ,
202+ } ) ;
203+ const output = renderMessageToStdout ( msg , RawMode . Raw ) ;
204+ assert . ok ( output . includes ( "(conversation summary inserted)" ) ) ;
205+ } ) ;
206+
207+ test ( "renderMessageToStdout returns empty for unknown system messages" , ( ) => {
208+ const msg = makeSessionMessage ( { role : "system" , content : "" } ) ;
209+ assert . equal ( renderMessageToStdout ( msg , RawMode . Raw ) , "" ) ;
210+ } ) ;
211+
212+ // --- getUpdatePlanPreviewLines tests ---
213+
214+ test ( "getUpdatePlanPreviewLines returns empty for failed tool" , ( ) => {
215+ const summary : ToolSummary = { name : "UpdatePlan" , params : "" , ok : false , metadata : { plan : "Step 1" } } ;
216+ assert . deepEqual ( getUpdatePlanPreviewLines ( summary ) , [ ] ) ;
217+ } ) ;
218+
219+ test ( "getUpdatePlanPreviewLines returns empty for non-UpdatePlan tool" , ( ) => {
220+ const summary : ToolSummary = { name : "edit" , params : "" , ok : true , metadata : { plan : "Step 1" } } ;
221+ assert . deepEqual ( getUpdatePlanPreviewLines ( summary ) , [ ] ) ;
222+ } ) ;
223+
224+ test ( "getUpdatePlanPreviewLines returns empty for missing plan metadata" , ( ) => {
225+ const summary : ToolSummary = { name : "UpdatePlan" , params : "" , ok : true , metadata : null } ;
226+ assert . deepEqual ( getUpdatePlanPreviewLines ( summary ) , [ ] ) ;
227+ } ) ;
228+
229+ test ( "getUpdatePlanPreviewLines returns empty for empty plan string" , ( ) => {
230+ const summary : ToolSummary = { name : "UpdatePlan" , params : "" , ok : true , metadata : { plan : "" } } ;
231+ assert . deepEqual ( getUpdatePlanPreviewLines ( summary ) , [ ] ) ;
232+ } ) ;
233+
234+ test ( "getUpdatePlanPreviewLines extracts plan lines and filters empty rows" , ( ) => {
235+ const summary : ToolSummary = {
236+ name : "UpdatePlan" ,
237+ params : "" ,
238+ ok : true ,
239+ metadata : { plan : "Step 1: Analyze\n\nStep 2: Implement\n \nStep 3: Test" } ,
68240 } ;
69- }
241+ assert . deepEqual ( getUpdatePlanPreviewLines ( summary ) , [ "Step 1: Analyze" , "Step 2: Implement" , "Step 3: Test" ] ) ;
242+ } ) ;
243+
244+ // --- parseToolPayload tests ---
245+
246+ test ( "parseToolPayload returns defaults for null content" , ( ) => {
247+ const result = parseToolPayload ( null ) ;
248+ assert . deepEqual ( result , { name : null , ok : true , metadata : null } ) ;
249+ } ) ;
250+
251+ test ( "parseToolPayload returns defaults for invalid JSON" , ( ) => {
252+ const result = parseToolPayload ( "not valid json" ) ;
253+ assert . deepEqual ( result , { name : null , ok : true , metadata : null } ) ;
254+ } ) ;
255+
256+ test ( "parseToolPayload parses valid JSON with name/ok/metadata" , ( ) => {
257+ const result = parseToolPayload ( JSON . stringify ( { name : "read" , ok : true , metadata : { file : "src/index.ts" } } ) ) ;
258+ assert . deepEqual ( result , { name : "read" , ok : true , metadata : { file : "src/index.ts" } } ) ;
259+ } ) ;
260+
261+ test ( "parseToolPayload respects ok: false" , ( ) => {
262+ const result = parseToolPayload ( JSON . stringify ( { name : "bash" , ok : false , metadata : null } ) ) ;
263+ assert . deepEqual ( result , { name : "bash" , ok : false , metadata : null } ) ;
264+ } ) ;
265+
266+ test ( "parseToolPayload trims whitespace from name" , ( ) => {
267+ const result = parseToolPayload ( JSON . stringify ( { name : " read " , ok : true } ) ) ;
268+ assert . equal ( result . name , "read" ) ;
269+ } ) ;
0 commit comments