@@ -5,6 +5,7 @@ import type {
55 ResolvedDeepcodingSettings ,
66 SessionEntry ,
77 SessionManagerOptions ,
8+ SessionMessage ,
89 SessionStatus ,
910 UserPromptContent ,
1011} from "@vegamo/deepcode-core" ;
@@ -84,6 +85,7 @@ function pipedInput(content: string): ExecInputStream {
8485type ManagerScenario = {
8586 finalStatus ?: SessionStatus ;
8687 finalReply ?: string | null ;
88+ finalMessageParams ?: unknown ;
8789 failReason ?: string | null ;
8890 resumeExists ?: boolean ;
8991 throwFromPrompt ?: Error ;
@@ -127,10 +129,10 @@ function createHarness(scenario: ManagerScenario = {}) {
127129 entry = createEntry ( activeId , "completed" ) ;
128130 return activeId ;
129131 } ,
130- handleUserPrompt : async ( prompt ) => {
132+ handleUserPrompt : async ( prompt , promptOptions ) => {
131133 submitted . push ( prompt ) ;
132134 if ( scenario . throwFromPrompt ) throw scenario . throwFromPrompt ;
133- activeId ??= "new-session" ;
135+ activeId ??= promptOptions ?. sessionId ?? "new-session" ;
134136 options . onSessionEntryUpdated ?.( createEntry ( activeId , "processing" ) ) ;
135137 options . onAssistantMessage (
136138 {
@@ -154,6 +156,21 @@ function createHarness(scenario: ManagerScenario = {}) {
154156 failReason : scenario . failReason ?? null ,
155157 askPermissions : scenario . askPermissions ,
156158 } ) ;
159+ const finalMessage : SessionMessage = {
160+ id : "final-message" ,
161+ sessionId : activeId ,
162+ role : "assistant" ,
163+ content : entry . assistantReply ,
164+ contentParams : null ,
165+ messageParams : scenario . finalMessageParams ?? null ,
166+ compacted : false ,
167+ visible : Boolean ( entry . assistantReply ) ,
168+ createTime : entry . createTime ,
169+ updateTime : entry . updateTime ,
170+ } ;
171+ options . onAssistantMessage ( finalMessage , true ) ;
172+ options . onAssistantMessage ( { ...finalMessage , id : "notice" , content : "diagnostic" } , false ) ;
173+ options . onAssistantMessage ( { ...finalMessage , id : "tool" , role : "tool" , content : "tool output" } , true ) ;
157174 options . onSessionEntryUpdated ?.( entry ) ;
158175 } ,
159176 initMcpServers : async ( servers ) => {
@@ -224,6 +241,76 @@ test("runExecMode submits piped stdin in the persisted user prompt", async () =>
224241 assert . deepEqual ( harness . stderr , [ ] ) ;
225242} ) ;
226243
244+ for ( const sessionOptions of [
245+ { sessionId : RESUME_ID } ,
246+ { resumeSessionId : RESUME_ID } ,
247+ { forkSessionId : RESUME_ID } ,
248+ { } ,
249+ ] ) {
250+ test ( `runExecMode outputs only the final message as JSON with ${ JSON . stringify ( sessionOptions ) } ` , async ( ) => {
251+ const harness = createHarness ( { resumeExists : true , finalMessageParams : { reasoning_content : "thoughts" } } ) ;
252+ const code = await runExecMode (
253+ { prompt : "task" , projectRoot : "/tmp/project" , input : ttyInput ( ) , json : true , ...sessionOptions } ,
254+ harness . dependencies
255+ ) ;
256+ assert . equal ( code , 0 ) ;
257+ assert . equal ( harness . stdout . length , 1 ) ;
258+ assert . deepEqual ( JSON . parse ( harness . stdout [ 0 ] ) , {
259+ id : "final-message" ,
260+ sessionId : sessionOptions . forkSessionId
261+ ? "forked-session"
262+ : ( sessionOptions . sessionId ?? sessionOptions . resumeSessionId ?? "new-session" ) ,
263+ role : "assistant" ,
264+ content : "final answer" ,
265+ messageParams : { reasoning_content : "thoughts" } ,
266+ compacted : false ,
267+ visible : true ,
268+ createTime : "2026-01-01T00:00:00.000Z" ,
269+ updateTime : "2026-01-01T00:00:00.000Z" ,
270+ } ) ;
271+ assert . deepEqual ( harness . stderr , [ ] ) ;
272+ assert . equal ( harness . disposed , 1 ) ;
273+ } ) ;
274+ }
275+
276+ test ( "runExecMode outputs JSON even when the final message has no text" , async ( ) => {
277+ const harness = createHarness ( { finalReply : null } ) ;
278+ const code = await runExecMode (
279+ { prompt : "task" , projectRoot : "/tmp/project" , input : ttyInput ( ) , json : true } ,
280+ harness . dependencies
281+ ) ;
282+ assert . equal ( code , 0 ) ;
283+ const message = JSON . parse ( harness . stdout [ 0 ] ) ;
284+ assert . equal ( message . id , "final-message" ) ;
285+ assert . equal ( message . visible , false ) ;
286+ assert . equal ( "content" in message , false ) ;
287+ } ) ;
288+
289+ for ( const finalStatus of [ "failed" , "ask_permission" , "waiting_for_user" , "interrupted" ] as const ) {
290+ test ( `runExecMode keeps stderr diagnostics and exit status in JSON mode for ${ finalStatus } ` , async ( ) => {
291+ const harness = createHarness ( { finalStatus } ) ;
292+ const code = await runExecMode (
293+ { prompt : "task" , projectRoot : "/tmp/project" , input : ttyInput ( ) , json : true } ,
294+ harness . dependencies
295+ ) ;
296+ assert . equal ( code , 1 ) ;
297+ assert . deepEqual ( harness . stdout , [ ] ) ;
298+ assert . equal ( harness . stderr . length , 1 ) ;
299+ assert . equal ( harness . disposed , 1 ) ;
300+ } ) ;
301+ }
302+
303+ test ( "runExecMode keeps stdout empty on SIGINT in JSON mode" , async ( ) => {
304+ const harness = createHarness ( { duringPrompt : ( ) => harness . emitSigint ( ) } ) ;
305+ const code = await runExecMode (
306+ { prompt : "task" , projectRoot : "/tmp/project" , input : ttyInput ( ) , json : true } ,
307+ harness . dependencies
308+ ) ;
309+ assert . equal ( code , 130 ) ;
310+ assert . deepEqual ( harness . stdout , [ ] ) ;
311+ assert . equal ( harness . disposed , 1 ) ;
312+ } ) ;
313+
227314test ( "runExecMode resumes a validated session before submitting" , async ( ) => {
228315 const harness = createHarness ( { resumeExists : true , finalReply : "continued" } ) ;
229316 const code = await runExecMode (
0 commit comments