Skip to content

Commit dd62d81

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(selectors): refresh caches after copilot secret updates
1 parent fe59f0d commit dd62d81

2 files changed

Lines changed: 84 additions & 2 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.test.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ vi.mock(
1515
() => ({ invalidateResourceQueries: vi.fn() })
1616
)
1717

18+
import { SetEnvironmentVariables } from '@/lib/copilot/generated/tool-catalog-v1'
1819
import type { PersistedStreamEventEnvelope } from '@/lib/copilot/request/session/contract'
1920
import 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'
2023
import { dispatchStreamEvent } from './dispatch-stream-event'
2124
import { createStreamLoopContext, type StreamLoopContext } from './stream-context'
2225
import { makeStreamLoopDeps, ref } from './stream-test-helpers'
@@ -38,7 +41,7 @@ function toolEnv(payload: Record<string, unknown>): PersistedStreamEventEnvelope
3841
const 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

5256
const 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.

apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import {
44
MothershipStreamV1ToolPhase,
55
MothershipStreamV1ToolStatus,
66
} from '@/lib/copilot/generated/mothership-stream-v1'
7-
import { ApplyFileEdit, PrepareFileEdit } from '@/lib/copilot/generated/tool-catalog-v1'
7+
import {
8+
ApplyFileEdit,
9+
PrepareFileEdit,
10+
SetEnvironmentVariables,
11+
} from '@/lib/copilot/generated/tool-catalog-v1'
812
import type { PersistedStreamEventEnvelope } from '@/lib/copilot/request/session/contract'
913
import {
1014
extractResourcesFromToolResult,
@@ -26,8 +30,10 @@ import {
2630
type ToolNode,
2731
} from '@/app/workspace/[workspaceId]/home/hooks/stream/turn-model'
2832
import { deploymentKeys } from '@/hooks/queries/deployments'
33+
import { environmentKeys } from '@/hooks/queries/environment'
2934
import { folderKeys } from '@/hooks/queries/utils/folder-keys'
3035
import { invalidateWorkflowLists } from '@/hooks/queries/utils/invalidate-workflow-lists'
36+
import { invalidateEnvironmentDependentSelectorQueries } from '@/hooks/selectors/cache-invalidation'
3137

3238
type ToolEvent = Extract<PersistedStreamEventEnvelope, { type: 'tool' }>
3339

@@ -71,6 +77,23 @@ function runToolResultSideEffects(ctx: StreamLoopContext, node: ToolNode): void
7177
void invalidateWorkflowLists(deps.queryClient, deps.workspaceId, ['active', 'archived'])
7278
}
7379

80+
if (name === SetEnvironmentVariables.id && isSuccess) {
81+
const out = output as Record<string, unknown> | undefined
82+
const isPersonal = out?.scope === 'personal'
83+
const workspaceId = typeof out?.workspaceId === 'string' ? out.workspaceId : deps.workspaceId
84+
85+
void (async () => {
86+
if (isPersonal) {
87+
await deps.queryClient.invalidateQueries({ queryKey: environmentKeys.personal() })
88+
} else {
89+
await deps.queryClient.invalidateQueries({
90+
queryKey: environmentKeys.workspace(workspaceId),
91+
})
92+
}
93+
await invalidateEnvironmentDependentSelectorQueries(deps.queryClient)
94+
})()
95+
}
96+
7497
const extractedResources =
7598
isSuccess && isResourceToolName(name)
7699
? extractResourcesFromToolResult(name, params, output)

0 commit comments

Comments
 (0)