11// @vitest -environment node
2- import { describe , expect , it , vi } from "vitest" ;
2+ import { beforeEach , describe , expect , it , vi } from "vitest" ;
33const {
44 refreshChatMock,
55 refreshChatAvatarMock,
66 refreshSlashCommandsMock,
77 loadChatHistoryMock,
8+ createSessionAndRefreshMock,
89 loadSessionsMock,
910} = vi . hoisted ( ( ) => ( {
1011 refreshChatMock : vi . fn ( ) ,
1112 refreshChatAvatarMock : vi . fn ( ) ,
1213 refreshSlashCommandsMock : vi . fn ( ) ,
1314 loadChatHistoryMock : vi . fn ( ) ,
15+ createSessionAndRefreshMock : vi . fn ( ) ,
1416 loadSessionsMock : vi . fn ( ) ,
1517} ) ) ;
1618
@@ -28,10 +30,12 @@ vi.mock("./controllers/chat.ts", () => ({
2830} ) ) ;
2931
3032vi . mock ( "./controllers/sessions.ts" , ( ) => ( {
33+ createSessionAndRefresh : createSessionAndRefreshMock ,
3134 loadSessions : loadSessionsMock ,
3235} ) ) ;
3336
3437import {
38+ createChatSession ,
3539 isCronSessionKey ,
3640 parseSessionKey ,
3741 resolveAssistantAttachmentAuthToken ,
@@ -44,6 +48,15 @@ import type { SessionsListResult } from "./types.ts";
4448
4549type SessionRow = SessionsListResult [ "sessions" ] [ number ] ;
4650
51+ beforeEach ( ( ) => {
52+ refreshChatMock . mockReset ( ) ;
53+ refreshChatAvatarMock . mockReset ( ) ;
54+ refreshSlashCommandsMock . mockReset ( ) ;
55+ loadChatHistoryMock . mockReset ( ) ;
56+ createSessionAndRefreshMock . mockReset ( ) ;
57+ loadSessionsMock . mockReset ( ) ;
58+ } ) ;
59+
4760function row ( overrides : Partial < SessionRow > & { key : string } ) : SessionRow {
4861 return { kind : "direct" , updatedAt : 0 , ...overrides } ;
4962}
@@ -90,6 +103,53 @@ function createSettings(): AppViewState["settings"] {
90103 } ;
91104}
92105
106+ function createChatSessionState ( overrides : Partial < AppViewState > = { } ) {
107+ const settings = createSettings ( ) ;
108+ const state = {
109+ sessionKey : "agent:ops:main" ,
110+ chatMessage : "draft prompt" ,
111+ chatAttachments : [ { id : "att-1" , mimeType : "image/png" , dataUrl : "data:image/png;base64,AAA" } ] ,
112+ chatMessages : [ { role : "assistant" , content : "old" } ] ,
113+ chatToolMessages : [ { id : "tool-1" } ] ,
114+ chatStreamSegments : [ ] ,
115+ chatThinkingLevel : null ,
116+ chatStream : null ,
117+ chatSideResult : null ,
118+ lastError : null ,
119+ compactionStatus : null ,
120+ fallbackStatus : null ,
121+ chatAvatarUrl : null ,
122+ chatAvatarSource : null ,
123+ chatAvatarStatus : null ,
124+ chatAvatarReason : null ,
125+ chatQueue : [ ] ,
126+ chatRunId : null ,
127+ chatSending : false ,
128+ chatLoading : false ,
129+ chatSideResultTerminalRuns : new Set < string > ( ) ,
130+ chatStreamStartedAt : null ,
131+ connected : true ,
132+ client : { request : vi . fn ( ) } ,
133+ sessionsResult : {
134+ ts : 0 ,
135+ path : "" ,
136+ count : 1 ,
137+ defaults : { modelProvider : "openai" , model : "gpt-5" , contextTokens : null } ,
138+ sessions : [ row ( { key : "agent:ops:main" } ) ] ,
139+ } ,
140+ settings,
141+ applySettings ( next : typeof settings ) {
142+ state . settings = next ;
143+ } ,
144+ loadAssistantIdentity : vi . fn ( ) ,
145+ resetToolStream : vi . fn ( ) ,
146+ resetChatScroll : vi . fn ( ) ,
147+ resetChatInputHistoryNavigation : vi . fn ( ) ,
148+ ...overrides ,
149+ } as unknown as AppViewState ;
150+ return state ;
151+ }
152+
93153/* ================================================================
94154 * parseSessionKey – low-level key → type / fallback mapping
95155 * ================================================================ */
@@ -493,6 +553,73 @@ describe("resolveSessionOptionGroups", () => {
493553 } ) ;
494554} ) ;
495555
556+ describe ( "createChatSession" , ( ) => {
557+ it ( "creates a dashboard session, switches to it, and preserves the current composer" , async ( ) => {
558+ const state = createChatSessionState ( ) ;
559+ createSessionAndRefreshMock . mockResolvedValue ( "agent:ops:dashboard:new-chat" ) ;
560+ refreshChatAvatarMock . mockResolvedValue ( undefined ) ;
561+ refreshSlashCommandsMock . mockResolvedValue ( undefined ) ;
562+ loadChatHistoryMock . mockResolvedValue ( undefined ) ;
563+ loadSessionsMock . mockResolvedValue ( undefined ) ;
564+
565+ await createChatSession ( state ) ;
566+
567+ expect ( createSessionAndRefreshMock ) . toHaveBeenCalledWith (
568+ state ,
569+ {
570+ agentId : "ops" ,
571+ parentSessionKey : "agent:ops:main" ,
572+ } ,
573+ {
574+ activeMinutes : 0 ,
575+ limit : 0 ,
576+ includeGlobal : true ,
577+ includeUnknown : true ,
578+ } ,
579+ ) ;
580+ expect ( state . sessionKey ) . toBe ( "agent:ops:dashboard:new-chat" ) ;
581+ expect ( state . settings . sessionKey ) . toBe ( "agent:ops:dashboard:new-chat" ) ;
582+ expect ( state . chatMessage ) . toBe ( "draft prompt" ) ;
583+ expect ( state . chatAttachments ) . toEqual ( [
584+ { id : "att-1" , mimeType : "image/png" , dataUrl : "data:image/png;base64,AAA" } ,
585+ ] ) ;
586+ expect ( state . chatMessages ) . toEqual ( [ ] ) ;
587+ expect ( loadChatHistoryMock ) . toHaveBeenCalledWith ( state ) ;
588+ } ) ;
589+
590+ it ( "ignores a stale create response after the active session changes" , async ( ) => {
591+ const state = createChatSessionState ( ) ;
592+ createSessionAndRefreshMock . mockImplementation ( async ( ) => {
593+ state . sessionKey = "agent:ops:other" ;
594+ return "agent:ops:dashboard:new-chat" ;
595+ } ) ;
596+
597+ await createChatSession ( state ) ;
598+
599+ expect ( state . sessionKey ) . toBe ( "agent:ops:other" ) ;
600+ expect ( state . chatMessage ) . toBe ( "draft prompt" ) ;
601+ expect ( state . chatMessages ) . toEqual ( [ { role : "assistant" , content : "old" } ] ) ;
602+ expect ( loadChatHistoryMock ) . not . toHaveBeenCalled ( ) ;
603+ } ) ;
604+
605+ it ( "does not create or switch while a run is active" , async ( ) => {
606+ const state = createChatSessionState ( {
607+ chatRunId : "run-1" ,
608+ chatQueue : [ { id : "queued-1" , text : "follow up" , createdAt : 1 } ] ,
609+ } ) ;
610+
611+ await createChatSession ( state ) ;
612+
613+ expect ( createSessionAndRefreshMock ) . not . toHaveBeenCalled ( ) ;
614+ expect ( state . sessionKey ) . toBe ( "agent:ops:main" ) ;
615+ expect ( state . chatMessage ) . toBe ( "draft prompt" ) ;
616+ expect ( state . chatQueue ) . toEqual ( [ { id : "queued-1" , text : "follow up" , createdAt : 1 } ] ) ;
617+ expect ( state . lastError ) . toBe (
618+ "Start a new session after the active run or queued messages finish." ,
619+ ) ;
620+ } ) ;
621+ } ) ;
622+
496623describe ( "switchChatSession" , ( ) => {
497624 it ( "refreshes the chat avatar after clearing session-scoped state" , async ( ) => {
498625 const settings = createSettings ( ) ;
0 commit comments