@@ -921,6 +921,68 @@ describe("watch investigation", () => {
921921 ) . toEqual ( [ ] ) ;
922922 } ) ;
923923
924+ /** A store whose atomic close refuses rather than throws, with the reason it refuses for. */
925+ function refusingStore ( error : "not_found" | "context_mismatch" | "chat_missing" ) {
926+ const { store, calls } = fakeStore ( {
927+ openInvestigation : { id : "inv_seeded" , projectRef : "proj_abc" , environmentRef : "env_abc" } ,
928+ } ) ;
929+ const wrapped : DashboardAgentStore = {
930+ ...store ,
931+ settleInvestigationCard : async ( args ) => {
932+ calls . settleInvestigationCard . push ( args ) ;
933+ return { ok : false as const , error } ;
934+ } ,
935+ } ;
936+ return { store : wrapped , calls } ;
937+ }
938+
939+ async function investigateAgainst ( store : DashboardAgentStore , chatId : string ) {
940+ const { model } = recordingModel ( [
941+ renderStep ( inProgress , "inv_seeded" , "tc_open" ) ,
942+ textStep ( "still looking" ) ,
943+ ] ) ;
944+ harness = mockChatAgent ( dashboardAgent , {
945+ chatId,
946+ clientData : CLIENT_DATA_WITH_TOKEN ,
947+ setupLocals : ( { set } ) => {
948+ set ( dashboardAgentStoreKey , store ) ;
949+ set ( dashboardAgentModelKey , model ) ;
950+ } ,
951+ } ) ;
952+ return harness . sendAction ( INVESTIGATE ) ;
953+ }
954+
955+ function erroredWith ( turn : { chunks : unknown [ ] } , pattern : RegExp ) {
956+ return turn . chunks . some (
957+ ( chunk ) =>
958+ ( chunk as { type ?: string } ) . type === "error" &&
959+ pattern . test ( ( chunk as { errorText ?: string } ) . errorText ?? "" )
960+ ) ;
961+ }
962+
963+ /**
964+ * A refused close is the same failure as a thrown one: the card never landed, so the
965+ * panel spins until the action is retried, and only a thrown error gets it retried.
966+ */
967+ it . each ( [ "not_found" , "context_mismatch" ] as const ) (
968+ "fails the action when the close is refused with %s" ,
969+ async ( error ) => {
970+ const { store, calls } = refusingStore ( error ) ;
971+ const turn = await investigateAgainst ( store , `chat_investigate_refused_${ error } ` ) ;
972+
973+ expect ( calls . settleInvestigationCard ) . toHaveLength ( 1 ) ;
974+ expect ( erroredWith ( turn , new RegExp ( error ) ) ) . toBe ( true ) ;
975+ }
976+ ) ;
977+
978+ it ( "reports success when the close is refused because the chat is gone" , async ( ) => {
979+ const { store, calls } = refusingStore ( "chat_missing" ) ;
980+ const turn = await investigateAgainst ( store , "chat_investigate_refused_chat_missing" ) ;
981+
982+ expect ( calls . settleInvestigationCard ) . toHaveLength ( 1 ) ;
983+ expect ( erroredWith ( turn , / c h a t _ m i s s i n g | c o u l d n ' t c l o s e / ) ) . toBe ( false ) ;
984+ } ) ;
985+
924986 it ( "says nothing when the kick carries no tenancy to scope a card with" , async ( ) => {
925987 const { store, calls } = fakeStore ( ) ;
926988 harness = mockChatAgent ( dashboardAgent , {
0 commit comments