@@ -15,8 +15,11 @@ vi.mock(
1515 ( ) => ( { invalidateResourceQueries : vi . fn ( ) } )
1616)
1717
18+ import { SetEnvironmentVariables } from '@/lib/copilot/generated/tool-catalog-v1'
1819import type { PersistedStreamEventEnvelope } from '@/lib/copilot/request/session/contract'
1920import type { FilePreviewSession } from '@/lib/copilot/request/session/file-preview-session-contract'
21+ import { environmentKeys } from '@/hooks/queries/environment'
22+ import { environmentDependentSelectorKeys } from '@/hooks/selectors/cache-invalidation'
2023import { dispatchStreamEvent } from './dispatch-stream-event'
2124import { createStreamLoopContext , type StreamLoopContext } from './stream-context'
2225import { makeStreamLoopDeps , ref } from './stream-test-helpers'
@@ -38,7 +41,7 @@ function toolEnv(payload: Record<string, unknown>): PersistedStreamEventEnvelope
3841const toolCall = ( id : string , name = 'my_tool' ) =>
3942 toolEnv ( { phase : 'call' , executor : 'go' , mode : 'sync' , toolCallId : id , toolName : name } )
4043
41- const toolResult = ( id : string , success : boolean , name = 'my_tool' ) =>
44+ const toolResult = ( id : string , success : boolean , name = 'my_tool' , output ?: unknown ) =>
4245 toolEnv ( {
4346 phase : 'result' ,
4447 executor : 'go' ,
@@ -47,6 +50,7 @@ const toolResult = (id: string, success: boolean, name = 'my_tool') =>
4750 toolName : name ,
4851 success,
4952 status : success ? 'success' : 'error' ,
53+ ...( output === undefined ? { } : { output } ) ,
5054 } )
5155
5256const workspaceFileCall = ( id : string ) =>
@@ -110,6 +114,61 @@ describe('tool events (dispatch → model + side effects)', () => {
110114 expect ( toolNode ( ctx , 'tc-3' ) . status ) . toBe ( 'error' )
111115 } )
112116
117+ it . each ( [
118+ {
119+ scope : 'personal' ,
120+ output : { scope : 'personal' } ,
121+ environmentQueryKey : environmentKeys . personal ( ) ,
122+ } ,
123+ {
124+ scope : 'workspace' ,
125+ output : { scope : 'workspace' , workspaceId : 'workspace-from-result' } ,
126+ environmentQueryKey : environmentKeys . workspace ( 'workspace-from-result' ) ,
127+ } ,
128+ ] ) (
129+ 'refreshes the $scope environment before selector caches after Copilot saves secrets' ,
130+ async ( { output, environmentQueryKey } ) => {
131+ const deps = makeStreamLoopDeps ( )
132+ const invalidateQueries = vi . mocked ( deps . queryClient . invalidateQueries )
133+ invalidateQueries . mockResolvedValue ( undefined )
134+ const ctx = createStreamLoopContext ( deps )
135+
136+ dispatchStreamEvent ( ctx , toolCall ( 'environment-1' , SetEnvironmentVariables . id ) )
137+ dispatchStreamEvent (
138+ ctx ,
139+ toolResult ( 'environment-1' , true , SetEnvironmentVariables . id , output )
140+ )
141+
142+ await vi . waitFor ( ( ) => expect ( invalidateQueries ) . toHaveBeenCalledTimes ( 5 ) )
143+ expect ( invalidateQueries . mock . calls . map ( ( [ filters ] ) => filters ?. queryKey ) ) . toEqual ( [
144+ environmentQueryKey ,
145+ environmentDependentSelectorKeys . primary ,
146+ environmentDependentSelectorKeys . dynamicDetails ,
147+ environmentDependentSelectorKeys . workflowDetails ,
148+ environmentDependentSelectorKeys . workflowReplacementOptions ,
149+ ] )
150+ }
151+ )
152+
153+ it ( 'does not refresh environment or selector caches when Copilot secret storage fails' , async ( ) => {
154+ const deps = makeStreamLoopDeps ( )
155+ const invalidateQueries = vi . mocked ( deps . queryClient . invalidateQueries )
156+ invalidateQueries . mockResolvedValue ( undefined )
157+ const ctx = createStreamLoopContext ( deps )
158+
159+ dispatchStreamEvent ( ctx , toolCall ( 'environment-failed' , SetEnvironmentVariables . id ) )
160+ dispatchStreamEvent (
161+ ctx ,
162+ toolResult ( 'environment-failed' , false , SetEnvironmentVariables . id , {
163+ scope : 'workspace' ,
164+ workspaceId : 'workspace-from-result' ,
165+ } )
166+ )
167+ await Promise . resolve ( )
168+
169+ expect ( invalidateQueries ) . not . toHaveBeenCalled ( )
170+ } )
171+
113172 // The client starts terminal/browser/workflow tools straight off the call
114173 // frame rather than waiting for the server to dispatch them, so a permission
115174 // gate that only held the server would let the command run behind the prompt.
0 commit comments