@@ -13,6 +13,9 @@ const mockSetQueriesData = vi.hoisted(() => vi.fn());
1313const mockSetQueryData = vi . hoisted ( ( ) => vi . fn ( ) ) ;
1414const mockUpdateSessionTaskTitle = vi . hoisted ( ( ) => vi . fn ( ) ) ;
1515const mockPrompts = vi . hoisted ( ( ) => ( { value : [ ] as string [ ] } ) ) ;
16+ const mockSessionSummary = vi . hoisted ( ( ) => ( {
17+ value : undefined as string | undefined ,
18+ } ) ) ;
1619const mockSessionStoreSetters = vi . hoisted ( ( ) => ( { updateSession : vi . fn ( ) } ) ) ;
1720const mockTitleAttachmentPaths = vi . hoisted ( ( ) => ( { value : [ ] as string [ ] } ) ) ;
1821const mockTitleAttachmentClear = vi . hoisted ( ( ) => vi . fn ( ) ) ;
@@ -77,7 +80,14 @@ vi.mock("@posthog/ui/shell/titleAttachmentStore", () => ({
7780vi . mock ( "@posthog/ui/features/sessions/sessionStore" , ( ) => {
7881 const state = {
7982 taskIdIndex : { "task-1" : "run-1" } ,
80- sessions : { "run-1" : { events : mockPrompts . value } } ,
83+ get sessions ( ) {
84+ return {
85+ "run-1" : {
86+ events : mockPrompts . value ,
87+ conversationSummary : mockSessionSummary . value ,
88+ } ,
89+ } ;
90+ } ,
8191 } ;
8292 const fn = Object . assign (
8393 ( selector : ( s : typeof state ) => unknown ) => selector ( state ) ,
@@ -89,6 +99,7 @@ vi.mock("@posthog/ui/features/sessions/sessionStore", () => {
8999 } ;
90100} ) ;
91101
102+ import { useTitleGenerationStore } from "@posthog/ui/features/sessions/titleGenerationStore" ;
92103import { useChatTitleGenerator } from "./useChatTitleGenerator" ;
93104
94105const TASK_ID = "task-1" ;
@@ -113,11 +124,18 @@ function cacheTask(task: Task): void {
113124 mockGetQueriesData . mockReturnValue ( [ [ [ "tasks" , "list" ] , [ task ] ] ] ) ;
114125}
115126
127+ interface TitleAndSummaryResult {
128+ title : string ;
129+ summary : string ;
130+ }
131+
116132describe ( "useChatTitleGenerator" , ( ) => {
117133 beforeEach ( ( ) => {
118134 vi . clearAllMocks ( ) ;
135+ useTitleGenerationStore . setState ( { byTaskId : { } } ) ;
119136 mockIsAuthenticated . value = true ;
120137 mockPrompts . value = [ ] ;
138+ mockSessionSummary . value = undefined ;
121139 mockTitleAttachmentPaths . value = [ ] ;
122140 mockEnrichDescription . mockImplementation ( ( desc : string ) =>
123141 Promise . resolve ( desc ) ,
@@ -382,4 +400,89 @@ describe("useChatTitleGenerator", () => {
382400
383401 expect ( mockGenerateTitle ) . not . toHaveBeenCalled ( ) ;
384402 } ) ;
403+
404+ it ( "does not regenerate when the chat view remounts after a generation" , async ( ) => {
405+ mockGenerateTitle . mockResolvedValue ( {
406+ title : "Fix login bug" ,
407+ summary : "User is fixing a login issue" ,
408+ } ) ;
409+ mockPrompts . value = [ "Fix the login bug" ] ;
410+
411+ const first = renderHook ( ( ) =>
412+ useChatTitleGenerator ( createTask ( { title : "Raw prompt title" } ) ) ,
413+ ) ;
414+ await waitFor ( ( ) => {
415+ expect ( mockGenerateTitle ) . toHaveBeenCalledTimes ( 1 ) ;
416+ } ) ;
417+ first . unmount ( ) ;
418+
419+ renderHook ( ( ) =>
420+ useChatTitleGenerator ( createTask ( { title : "Fix login bug" } ) ) ,
421+ ) ;
422+
423+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
424+ expect ( mockGenerateTitle ) . toHaveBeenCalledTimes ( 1 ) ;
425+ } ) ;
426+
427+ it ( "shares one in-flight guard across simultaneously mounted views" , async ( ) => {
428+ let resolveGeneration ! : ( value : TitleAndSummaryResult ) => void ;
429+ mockGenerateTitle . mockReturnValue (
430+ new Promise ( ( resolve ) => {
431+ resolveGeneration = resolve ;
432+ } ) ,
433+ ) ;
434+ mockPrompts . value = [ "Fix the login bug" ] ;
435+
436+ renderHook ( ( ) =>
437+ useChatTitleGenerator ( createTask ( { title : "Raw prompt title" } ) ) ,
438+ ) ;
439+ renderHook ( ( ) =>
440+ useChatTitleGenerator ( createTask ( { title : "Raw prompt title" } ) ) ,
441+ ) ;
442+
443+ await waitFor ( ( ) => {
444+ expect ( mockGenerateTitle ) . toHaveBeenCalledTimes ( 1 ) ;
445+ } ) ;
446+
447+ resolveGeneration ( { title : "Fix login bug" , summary : "" } ) ;
448+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
449+ expect ( mockGenerateTitle ) . toHaveBeenCalledTimes ( 1 ) ;
450+ } ) ;
451+
452+ it ( "skips catch-up generation when the title is locked and a summary exists" , async ( ) => {
453+ const lockedTask = createTask ( {
454+ title : "Custom auth title" ,
455+ description : "fix auth" ,
456+ title_manually_set : true ,
457+ } ) ;
458+ cacheTask ( lockedTask ) ;
459+ mockSessionSummary . value = "User wants to fix auth" ;
460+ mockPrompts . value = Array . from ( { length : 8 } , ( _ , i ) => `prompt ${ i } ` ) ;
461+
462+ renderHook ( ( ) => useChatTitleGenerator ( lockedTask ) ) ;
463+
464+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
465+ expect ( mockGenerateTitle ) . not . toHaveBeenCalled ( ) ;
466+ } ) ;
467+
468+ it ( "runs catch-up generation for a locked title when no summary exists yet" , async ( ) => {
469+ const lockedTask = createTask ( {
470+ title : "Custom auth title" ,
471+ description : "fix auth" ,
472+ title_manually_set : true ,
473+ } ) ;
474+ cacheTask ( lockedTask ) ;
475+ mockGenerateTitle . mockResolvedValue ( {
476+ title : "Auto title" ,
477+ summary : "User wants to fix auth" ,
478+ } ) ;
479+ mockPrompts . value = Array . from ( { length : 8 } , ( _ , i ) => `prompt ${ i } ` ) ;
480+
481+ renderHook ( ( ) => useChatTitleGenerator ( lockedTask ) ) ;
482+
483+ await waitFor ( ( ) => {
484+ expect ( mockGenerateTitle ) . toHaveBeenCalledTimes ( 1 ) ;
485+ } ) ;
486+ expect ( mockUpdateTask ) . not . toHaveBeenCalled ( ) ;
487+ } ) ;
385488} ) ;
0 commit comments