@@ -39,6 +39,7 @@ vi.mock("../../config/sessions/paths.js", () => ({
3939const storeRuntimeLoads = vi . hoisted ( ( ) => vi . fn ( ) ) ;
4040const updateSessionStore = vi . hoisted ( ( ) => vi . fn ( ) ) ;
4141const updateAmbientTranscriptWatermarkMock = vi . hoisted ( ( ) => vi . fn ( ) . mockResolvedValue ( null ) ) ;
42+ const maybeGenerateSessionTitle = vi . hoisted ( ( ) => vi . fn ( ) ) ;
4243
4344vi . mock ( "../../config/sessions/ambient-transcript-watermark.js" , ( ) => ( {
4445 updateAmbientTranscriptWatermark : updateAmbientTranscriptWatermarkMock ,
@@ -132,6 +133,10 @@ vi.mock("./session-system-events.js", () => ({
132133 drainFormattedSystemEvents : vi . fn ( ) . mockResolvedValue ( undefined ) ,
133134} ) ) ;
134135
136+ vi . mock ( "./session-title-generator.js" , ( ) => ( {
137+ maybeGenerateSessionTitle,
138+ } ) ) ;
139+
135140vi . mock ( "./typing-mode.js" , ( ) => ( {
136141 resolveTypingMode : vi . fn ( ) . mockReturnValue ( "off" ) ,
137142} ) ) ;
@@ -344,6 +349,120 @@ describe("runPreparedReply media-only handling", () => {
344349 } ) ;
345350 } ) ;
346351
352+ it ( "forwards generated title metadata changes after a successful reply" , async ( ) => {
353+ const onSessionMetadataChanges = vi . fn ( ) ;
354+ vi . mocked ( maybeGenerateSessionTitle ) . mockImplementationOnce ( ( params : unknown ) => {
355+ ( params as { onTitleGenerated ?: ( change : unknown ) => void } ) . onTitleGenerated ?.( {
356+ sessionKey : "agent:main:title-session" ,
357+ agentId : "main" ,
358+ reason : "session-title" ,
359+ } ) ;
360+ } ) ;
361+
362+ await runPreparedReply (
363+ baseParams ( {
364+ cfg : {
365+ session : { } ,
366+ channels : { } ,
367+ agents : { defaults : { } } ,
368+ sessionTitle : { enabled : true } ,
369+ } ,
370+ agentId : "main" ,
371+ sessionKey : "agent:main:title-session" ,
372+ sessionEntry : { sessionId : "title-session" } as SessionEntry ,
373+ storePath : "/tmp/sessions.json" ,
374+ opts : { onSessionMetadataChanges } ,
375+ } ) ,
376+ ) ;
377+
378+ await vi . waitFor ( ( ) =>
379+ expect ( onSessionMetadataChanges ) . toHaveBeenCalledWith ( [
380+ {
381+ sessionKey : "agent:main:title-session" ,
382+ agentId : "main" ,
383+ reason : "session-title" ,
384+ } ,
385+ ] ) ,
386+ ) ;
387+ } ) ;
388+
389+ it ( "triggers generated title handoff after a successful block-only reply" , async ( ) => {
390+ vi . mocked ( runReplyAgent ) . mockResolvedValueOnce ( undefined ) ;
391+
392+ await runPreparedReply (
393+ baseParams ( {
394+ cfg : {
395+ session : { } ,
396+ channels : { } ,
397+ agents : { defaults : { } } ,
398+ sessionTitle : { enabled : true } ,
399+ } ,
400+ agentId : "main" ,
401+ sessionKey : "agent:main:block-title-session" ,
402+ sessionEntry : { sessionId : "block-title-session" } as SessionEntry ,
403+ storePath : "/tmp/sessions.json" ,
404+ blockStreamingEnabled : true ,
405+ } ) ,
406+ ) ;
407+
408+ await vi . waitFor ( ( ) => expect ( maybeGenerateSessionTitle ) . toHaveBeenCalledOnce ( ) ) ;
409+ expect ( maybeGenerateSessionTitle ) . toHaveBeenCalledWith (
410+ expect . objectContaining ( {
411+ sessionKey : "agent:main:block-title-session" ,
412+ } ) ,
413+ ) ;
414+ } ) ;
415+
416+ it ( "passes post-run session auth profile to generated title handoff" , async ( ) => {
417+ const { resolveSessionAuthProfileOverride } =
418+ await import ( "../../agents/auth-profiles/session-override.js" ) ;
419+ const sessionEntry : SessionEntry = {
420+ sessionId : "title-session" ,
421+ sessionFile : "/tmp/title-session.jsonl" ,
422+ modelProvider : "anthropic" ,
423+ model : "claude-opus-4-1" ,
424+ authProfileOverride : "anthropic:default" ,
425+ authProfileOverrideSource : "user" ,
426+ updatedAt : 1 ,
427+ } ;
428+ vi . mocked ( resolveSessionAuthProfileOverride ) . mockImplementation ( async ( params ) => {
429+ return params . sessionEntry ?. authProfileOverride ;
430+ } ) ;
431+ vi . mocked ( runReplyAgent ) . mockImplementationOnce ( async ( params ) => {
432+ if ( params . sessionEntry ) {
433+ params . sessionEntry . modelProvider = "zai" ;
434+ params . sessionEntry . model = "glm-4.5" ;
435+ params . sessionEntry . authProfileOverride = "zai:work" ;
436+ params . sessionEntry . authProfileOverrideSource = "auto" ;
437+ }
438+ return { text : "ok" } ;
439+ } ) ;
440+
441+ await runPreparedReply (
442+ baseParams ( {
443+ cfg : {
444+ session : { } ,
445+ channels : { } ,
446+ agents : { defaults : { } } ,
447+ sessionTitle : { enabled : true } ,
448+ } ,
449+ agentId : "main" ,
450+ sessionKey : "agent:main:title-session" ,
451+ sessionEntry,
452+ storePath : "/tmp/sessions.json" ,
453+ } ) ,
454+ ) ;
455+
456+ await vi . waitFor ( ( ) => expect ( maybeGenerateSessionTitle ) . toHaveBeenCalledOnce ( ) ) ;
457+ expect ( maybeGenerateSessionTitle ) . toHaveBeenCalledWith (
458+ expect . objectContaining ( {
459+ sessionEntry,
460+ authProfileId : "zai:work" ,
461+ authProfileIdSource : "auto" ,
462+ } ) ,
463+ ) ;
464+ } ) ;
465+
347466 it ( "propagates non-visible assistant silence for group runs" , async ( ) => {
348467 await runPreparedReply ( baseParams ( ) ) ;
349468
0 commit comments