@@ -4,13 +4,20 @@ const mocks = vi.hoisted(() => ({
44 createChat : vi . fn ( ) ,
55 findEnvironmentBySlug : vi . fn ( ) ,
66 mintUserActorToken : vi . fn ( ) ,
7+ mintPublicToken : vi . fn ( ) ,
78 headStart : vi . fn ( ) ,
9+ startSession : vi . fn ( ) ,
10+ softDeleteChat : vi . fn ( ) ,
11+ logger : { debug : vi . fn ( ) , error : vi . fn ( ) , warn : vi . fn ( ) , info : vi . fn ( ) } ,
12+ // Mutable so a test can take the head start away and drive the cold path.
13+ env : { SESSION_SECRET : "test-session-secret" , ANTHROPIC_API_KEY : "sk-test" } as Record <
14+ string ,
15+ string | undefined
16+ > ,
817} ) ) ;
918
1019vi . mock ( "~/db.server" , ( ) => ( { $replica : { } , prisma : { } } ) ) ;
11- vi . mock ( "~/env.server" , ( ) => ( {
12- env : { SESSION_SECRET : "test-session-secret" , ANTHROPIC_API_KEY : "sk-test" } ,
13- } ) ) ;
20+ vi . mock ( "~/env.server" , ( ) => ( { env : mocks . env } ) ) ;
1421vi . mock ( "~/services/session.server" , ( ) => ( {
1522 requireUser : async ( ) => ( { id : "usr_real" , admin : false , isImpersonating : false } ) ,
1623} ) ) ;
@@ -30,10 +37,10 @@ vi.mock("~/models/runtimeEnvironment.server", () => ({
3037vi . mock ( "~/services/dashboardAgent.server" , ( ) => ( {
3138 dashboardAgentApiOrigin : ( ) => "https://api.trigger.dev" ,
3239 isDashboardAgentConfigured : ( ) => true ,
33- mintDashboardAgentToken : async ( ) => "pat_public" ,
40+ mintDashboardAgentToken : mocks . mintPublicToken ,
3441 mintDashboardAgentUserActorToken : mocks . mintUserActorToken ,
3542 resolveDashboardAgentRepoSnapshot : async ( ) => null ,
36- startDashboardAgentSession : async ( ) => { } ,
43+ startDashboardAgentSession : mocks . startSession ,
3744} ) ) ;
3845vi . mock ( "~/services/dashboardAgentHeadStart.server" , ( ) => ( {
3946 startDashboardAgentHeadStart : mocks . headStart ,
@@ -50,11 +57,9 @@ vi.mock("@internal/dashboard-agent-db", () => ({
5057 listChats : vi . fn ( ) ,
5158 renameChat : vi . fn ( ) ,
5259 setChatPinned : vi . fn ( ) ,
53- softDeleteChat : vi . fn ( ) ,
54- } ) ) ;
55- vi . mock ( "~/services/logger.server" , ( ) => ( {
56- logger : { debug : vi . fn ( ) , error : vi . fn ( ) , warn : vi . fn ( ) , info : vi . fn ( ) } ,
60+ softDeleteChat : mocks . softDeleteChat ,
5761} ) ) ;
62+ vi . mock ( "~/services/logger.server" , ( ) => ( { logger : mocks . logger } ) ) ;
5863
5964import { action } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboard-agent" ;
6065
@@ -86,6 +91,10 @@ describe("dashboard agent chat creation — nothing fallible after the row exist
8691 . mockReset ( )
8792 . mockResolvedValue ( { id : "env_real" , type : "DEVELOPMENT" } ) ;
8893 mocks . mintUserActorToken . mockReset ( ) . mockResolvedValue ( "tr_uat_real" ) ;
94+ mocks . mintPublicToken . mockReset ( ) . mockResolvedValue ( "pat_public" ) ;
95+ mocks . startSession . mockReset ( ) . mockResolvedValue ( undefined ) ;
96+ mocks . softDeleteChat . mockReset ( ) . mockResolvedValue ( { deleted : true , cancelledWatches : [ ] } ) ;
97+ mocks . env . ANTHROPIC_API_KEY = "sk-test" ;
8998 } ) ;
9099
91100 it ( "creates no chat when the environment slug resolves to nothing" , async ( ) => {
@@ -97,7 +106,7 @@ describe("dashboard agent chat creation — nothing fallible after the row exist
97106 expect ( mocks . createChat ) . not . toHaveBeenCalled ( ) ;
98107 } ) ;
99108
100- it ( "creates no chat when the token mint fails" , async ( ) => {
109+ it ( "creates no chat when the delegated token mint fails" , async ( ) => {
101110 mocks . mintUserActorToken . mockRejectedValue ( new Error ( "signing key unavailable" ) ) ;
102111
103112 const response = await createChatRequest ( ) ;
@@ -118,5 +127,83 @@ describe("dashboard agent chat creation — nothing fallible after the row exist
118127 environmentId : "env_real" ,
119128 environmentName : "dev" ,
120129 } ) ;
130+ expect ( mocks . softDeleteChat ) . not . toHaveBeenCalled ( ) ;
131+ } ) ;
132+ } ) ;
133+
134+ // A failed start means no handover was dispatched and no message was sent, so any session it
135+ // did create idles out having done nothing — the chat row is safe to take back. Once the start
136+ // has resolved the session is live, and removing the chat would hide a running agent.
137+ describe ( "dashboard agent chat creation — a start that fails part way" , ( ) => {
138+ beforeEach ( ( ) => {
139+ mocks . createChat . mockReset ( ) . mockResolvedValue ( undefined ) ;
140+ mocks . headStart . mockReset ( ) . mockResolvedValue ( undefined ) ;
141+ mocks . findEnvironmentBySlug
142+ . mockReset ( )
143+ . mockResolvedValue ( { id : "env_real" , type : "DEVELOPMENT" } ) ;
144+ mocks . mintUserActorToken . mockReset ( ) . mockResolvedValue ( "tr_uat_real" ) ;
145+ mocks . mintPublicToken . mockReset ( ) . mockResolvedValue ( "pat_public" ) ;
146+ mocks . startSession . mockReset ( ) . mockResolvedValue ( undefined ) ;
147+ mocks . softDeleteChat . mockReset ( ) . mockResolvedValue ( { deleted : true , cancelledWatches : [ ] } ) ;
148+ mocks . env . ANTHROPIC_API_KEY = "sk-test" ;
149+ mocks . logger . error . mockReset ( ) ;
150+ } ) ;
151+
152+ it ( "takes the chat back when the head start fails" , async ( ) => {
153+ mocks . headStart . mockRejectedValue ( new Error ( "session create failed" ) ) ;
154+
155+ const response = await createChatRequest ( ) ;
156+
157+ expect ( response . status ) . toBe ( 500 ) ;
158+ expect ( mocks . createChat ) . toHaveBeenCalledTimes ( 1 ) ;
159+ expect ( mocks . softDeleteChat ) . toHaveBeenCalledTimes ( 1 ) ;
160+ expect ( mocks . softDeleteChat . mock . calls [ 0 ] [ 1 ] ) . toMatchObject ( {
161+ chatId : mocks . createChat . mock . calls [ 0 ] [ 1 ] . id ,
162+ userId : "usr_real" ,
163+ } ) ;
164+ } ) ;
165+
166+ it ( "takes the chat back when the cold start fails" , async ( ) => {
167+ mocks . env . ANTHROPIC_API_KEY = undefined ;
168+ mocks . startSession . mockRejectedValue ( new Error ( "session create failed" ) ) ;
169+
170+ const response = await createChatRequest ( ) ;
171+
172+ expect ( response . status ) . toBe ( 500 ) ;
173+ expect ( mocks . createChat ) . toHaveBeenCalledTimes ( 1 ) ;
174+ expect ( mocks . softDeleteChat ) . toHaveBeenCalledTimes ( 1 ) ;
175+ } ) ;
176+
177+ it ( "keeps the chat when the session is live and only its access token failed" , async ( ) => {
178+ mocks . mintPublicToken . mockRejectedValue ( new Error ( "token mint failed" ) ) ;
179+
180+ const response = await createChatRequest ( ) ;
181+
182+ expect ( response . status ) . toBe ( 500 ) ;
183+ expect ( mocks . headStart ) . toHaveBeenCalledTimes ( 1 ) ;
184+ expect ( mocks . createChat ) . toHaveBeenCalledTimes ( 1 ) ;
185+ expect ( mocks . softDeleteChat ) . not . toHaveBeenCalled ( ) ;
186+ } ) ;
187+
188+ it ( "keeps the chat when a cold-started session's access token failed" , async ( ) => {
189+ mocks . env . ANTHROPIC_API_KEY = undefined ;
190+ mocks . mintPublicToken . mockRejectedValue ( new Error ( "token mint failed" ) ) ;
191+
192+ const response = await createChatRequest ( ) ;
193+
194+ expect ( response . status ) . toBe ( 500 ) ;
195+ expect ( mocks . startSession ) . toHaveBeenCalledTimes ( 1 ) ;
196+ expect ( mocks . softDeleteChat ) . not . toHaveBeenCalled ( ) ;
197+ } ) ;
198+
199+ it ( "surfaces the start's own failure when taking the chat back also fails" , async ( ) => {
200+ mocks . headStart . mockRejectedValue ( new Error ( "session create failed" ) ) ;
201+ mocks . softDeleteChat . mockRejectedValue ( new Error ( "chat store unavailable" ) ) ;
202+
203+ const response = await createChatRequest ( ) ;
204+
205+ expect ( response . status ) . toBe ( 500 ) ;
206+ const logged = mocks . logger . error . mock . calls . map ( ( call : any [ ] ) => call [ 1 ] ?. error ?. message ) ;
207+ expect ( logged ) . toContain ( "session create failed" ) ;
121208 } ) ;
122209} ) ;
0 commit comments