|
| 1 | +import { QueryClient, QueryObserver } from '@tanstack/react-query' |
1 | 2 | import { beforeEach, describe, expect, it, vi } from 'vitest' |
2 | 3 | import { resourceScopeKey } from '@/lib/core/resource-scope' |
3 | 4 | import { knowledgeKeys } from '@/hooks/queries/utils/knowledge-keys' |
@@ -26,8 +27,12 @@ vi.mock('@/hooks/queries/utils/workflow-cache', () => ({ |
26 | 27 | import type { PersistedStreamEventEnvelope } from '@/lib/mothership/request/session/contract' |
27 | 28 | import { handleResourceEvent } from '@/app/workspace/[workspaceId]/home/hooks/stream/handle-resource-event' |
28 | 29 | import type { StreamLoopContext } from '@/app/workspace/[workspaceId]/home/hooks/stream/stream-context' |
29 | | -import { makeStreamLoopDeps } from '@/app/workspace/[workspaceId]/home/hooks/stream/stream-test-helpers' |
| 30 | +import { |
| 31 | + makeStreamLoopDeps, |
| 32 | + ref, |
| 33 | +} from '@/app/workspace/[workspaceId]/home/hooks/stream/stream-test-helpers' |
30 | 34 | import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types' |
| 35 | +import { type MothershipChatHistory, mothershipChatKeys } from '@/hooks/queries/mothership-chats' |
31 | 36 | import { useTableViewPinStore } from '@/stores/table/view-pin/store' |
32 | 37 |
|
33 | 38 | type ResourceEvent = Extract<PersistedStreamEventEnvelope, { type: 'resource' }> |
@@ -109,6 +114,89 @@ function tableUpsertEvent(id: string, viewId?: string): PersistedStreamEventEnve |
109 | 114 | } as PersistedStreamEventEnvelope |
110 | 115 | } |
111 | 116 |
|
| 117 | +describe('live resource effects', () => { |
| 118 | + const detailKey = mothershipChatKeys.detail('chat-1') |
| 119 | + const kept: MothershipResource = { type: 'file', id: 'kept', title: 'kept.md' } |
| 120 | + const dropped: MothershipResource = { type: 'file', id: 'dropped', title: 'dropped.md' } |
| 121 | + const created: MothershipResource = { type: 'table', id: 'tbl-new', title: 'Leads' } |
| 122 | + |
| 123 | + /** A server whose stored resources the effects below have already changed. */ |
| 124 | + function serverChat() { |
| 125 | + let stored = [kept, dropped] |
| 126 | + const reads = vi.fn( |
| 127 | + async (): Promise<MothershipChatHistory> => ({ |
| 128 | + id: 'chat-1', |
| 129 | + title: null, |
| 130 | + messages: [], |
| 131 | + activeStreamId: 'stream-1', |
| 132 | + resources: stored, |
| 133 | + }) |
| 134 | + ) |
| 135 | + return { reads, persistEffects: () => (stored = [kept, created]) } |
| 136 | + } |
| 137 | + |
| 138 | + function effect(op: 'upsert' | 'remove', resource: MothershipResource): ResourceEvent { |
| 139 | + return { |
| 140 | + ...removeEvent('file', resource.id), |
| 141 | + payload: { op, effectId: `effect:${resource.id}`, resource }, |
| 142 | + } as ResourceEvent |
| 143 | + } |
| 144 | + |
| 145 | + function applyEffects(queryClient: QueryClient) { |
| 146 | + const deps = makeStreamLoopDeps({ queryClient, chatIdRef: ref<string | undefined>('chat-1') }) |
| 147 | + handleResourceEvent({ deps } as StreamLoopContext, effect('upsert', created)) |
| 148 | + handleResourceEvent({ deps } as StreamLoopContext, effect('remove', dropped)) |
| 149 | + } |
| 150 | + |
| 151 | + it('records effects in the cached chat without reloading its transcript', async () => { |
| 152 | + const queryClient = new QueryClient() |
| 153 | + const server = serverChat() |
| 154 | + queryClient.setQueryData(detailKey, await server.reads()) |
| 155 | + server.reads.mockClear() |
| 156 | + const unsubscribe = new QueryObserver(queryClient, { |
| 157 | + queryKey: detailKey, |
| 158 | + queryFn: server.reads, |
| 159 | + staleTime: Number.POSITIVE_INFINITY, |
| 160 | + }).subscribe(() => {}) |
| 161 | + |
| 162 | + server.persistEffects() |
| 163 | + applyEffects(queryClient) |
| 164 | + await queryClient.refetchQueries({ queryKey: detailKey, stale: true }) |
| 165 | + |
| 166 | + expect(queryClient.getQueryData<MothershipChatHistory>(detailKey)?.resources).toEqual([ |
| 167 | + kept, |
| 168 | + created, |
| 169 | + ]) |
| 170 | + expect(server.reads).not.toHaveBeenCalled() |
| 171 | + unsubscribe() |
| 172 | + }) |
| 173 | + |
| 174 | + it.each([ |
| 175 | + { cached: true, label: 'a cached chat' }, |
| 176 | + { cached: false, label: 'a chat loading for the first time' }, |
| 177 | + ])( |
| 178 | + 'restarts a transcript read of $label that began before the effects were stored', |
| 179 | + async ({ cached }) => { |
| 180 | + const queryClient = new QueryClient() |
| 181 | + const server = serverChat() |
| 182 | + if (cached) queryClient.setQueryData(detailKey, await server.reads()) |
| 183 | + const staleRead = queryClient.fetchQuery({ queryKey: detailKey, queryFn: server.reads }) |
| 184 | + |
| 185 | + server.persistEffects() |
| 186 | + applyEffects(queryClient) |
| 187 | + await staleRead.catch(() => undefined) |
| 188 | + |
| 189 | + await vi.waitFor(() => { |
| 190 | + expect(queryClient.isFetching({ queryKey: detailKey })).toBe(0) |
| 191 | + expect(queryClient.getQueryData<MothershipChatHistory>(detailKey)?.resources).toEqual([ |
| 192 | + kept, |
| 193 | + created, |
| 194 | + ]) |
| 195 | + }) |
| 196 | + } |
| 197 | + ) |
| 198 | +}) |
| 199 | + |
112 | 200 | describe('handleResourceEvent saved-view pins', () => { |
113 | 201 | beforeEach(() => { |
114 | 202 | useTableViewPinStore.getState().reset() |
|
0 commit comments