|
| 1 | +/** |
| 2 | + * @vitest-environment node |
| 3 | + * |
| 4 | + * Multi-replica (store-enabled) coverage for the copilot live-merge stale-check. The main |
| 5 | + * `file-doc.test.ts` runs with the store DISABLED (single-replica fallback); this file mocks an ENABLED |
| 6 | + * store so the cross-process branch of `mergeMarkdownIntoRoom` — staleness against the SHARED synced |
| 7 | + * version under the merge lock, and `recordVersion` writing `setSyncedVersion` — is exercised directly. |
| 8 | + * The enabled merge path reads its base from the shared store (not an in-memory room), so no JOIN/seed |
| 9 | + * is needed: calling `applyMarkdownToLiveFileDoc` against the fake store drives the branch on its own. |
| 10 | + */ |
| 11 | +import { beforeEach, describe, expect, it, vi } from 'vitest' |
| 12 | +import * as Y from 'yjs' |
| 13 | + |
| 14 | +const { mockFetchFileDocMerge } = vi.hoisted(() => ({ |
| 15 | + mockFetchFileDocMerge: vi.fn(), |
| 16 | +})) |
| 17 | + |
| 18 | +/** |
| 19 | + * A minimal ENABLED store: in-memory monotonic synced version (mirrors SET_VERSION_IF_NEWER_SCRIPT), a |
| 20 | + * non-null stream state so the merge has a base, and no-op locks/publish. Only the surface the |
| 21 | + * store-enabled merge path touches is implemented. |
| 22 | + */ |
| 23 | +const fakeStore = { |
| 24 | + enabled: true, |
| 25 | + versions: new Map<string, number>(), |
| 26 | + acquireMergeSlot: vi.fn(async () => 'token'), |
| 27 | + releaseMergeSlot: vi.fn(async () => {}), |
| 28 | + getStreamState: vi.fn(async () => new Uint8Array([1])), |
| 29 | + publishAndWait: vi.fn(async () => {}), |
| 30 | + getSyncedVersion: vi.fn(async (name: string) => fakeStore.versions.get(name) ?? null), |
| 31 | + setSyncedVersion: vi.fn(async (name: string, version: number) => { |
| 32 | + fakeStore.versions.set(name, Math.max(fakeStore.versions.get(name) ?? 0, version)) |
| 33 | + }), |
| 34 | +} |
| 35 | + |
| 36 | +vi.mock('@sim/platform-authz/rooms', () => ({ authorizeRoom: vi.fn() })) |
| 37 | + |
| 38 | +vi.mock('@/handlers/file-doc-app', () => ({ |
| 39 | + fetchFileDocSeed: vi.fn(), |
| 40 | + fetchFileDocMerge: mockFetchFileDocMerge, |
| 41 | + fetchFileDocPersist: vi.fn(), |
| 42 | +})) |
| 43 | + |
| 44 | +vi.mock('@/handlers/file-doc-store', () => ({ |
| 45 | + getFileDocStore: () => fakeStore, |
| 46 | + REDIS_ORIGIN: Symbol('redis'), |
| 47 | + REDIS_SNAPSHOT_ORIGIN: Symbol('redis-snapshot'), |
| 48 | +})) |
| 49 | + |
| 50 | +import { applyMarkdownToLiveFileDoc } from '@/handlers/file-doc' |
| 51 | + |
| 52 | +const ROOM_NAME = 'workspace-file-doc:file-1' |
| 53 | + |
| 54 | +describe('applyMarkdownToLiveFileDoc — multi-replica (store-enabled) ordering', () => { |
| 55 | + beforeEach(() => { |
| 56 | + vi.clearAllMocks() |
| 57 | + fakeStore.versions.clear() |
| 58 | + fakeStore.acquireMergeSlot.mockResolvedValue('token') |
| 59 | + fakeStore.getStreamState.mockResolvedValue(new Uint8Array([1])) |
| 60 | + mockFetchFileDocMerge.mockResolvedValue(Y.encodeStateAsUpdate(new Y.Doc())) |
| 61 | + }) |
| 62 | + |
| 63 | + it('stale-checks against the SHARED synced version and never records a streaming merge', async () => { |
| 64 | + // A durable write records the shared synced version cluster-wide. |
| 65 | + expect(await applyMarkdownToLiveFileDoc('file-1', '# durable', { version: 100 })).toBe( |
| 66 | + 'applied' |
| 67 | + ) |
| 68 | + expect(fakeStore.setSyncedVersion).toHaveBeenCalledWith(ROOM_NAME, 100) |
| 69 | + mockFetchFileDocMerge.mockClear() |
| 70 | + |
| 71 | + // A delayed streaming snapshot older than the SHARED version (e.g. from another process) is stale — |
| 72 | + // rejected under the lock before any diff is built, so the live doc never regresses. |
| 73 | + expect(await applyMarkdownToLiveFileDoc('file-1', '# older stream', { streamedAt: 50 })).toBe( |
| 74 | + 'stale' |
| 75 | + ) |
| 76 | + expect(mockFetchFileDocMerge).not.toHaveBeenCalled() |
| 77 | + |
| 78 | + // A streaming snapshot newer than the shared version applies (advances the live view)... |
| 79 | + expect(await applyMarkdownToLiveFileDoc('file-1', '# newer stream', { streamedAt: 200 })).toBe( |
| 80 | + 'applied' |
| 81 | + ) |
| 82 | + // ...but it is never recorded: a durable write between the two (150) still applies. Had 200 been |
| 83 | + // recorded to the shared store, 150 would be rejected as stale. |
| 84 | + expect(await applyMarkdownToLiveFileDoc('file-1', '# durable again', { version: 150 })).toBe( |
| 85 | + 'applied' |
| 86 | + ) |
| 87 | + expect(fakeStore.setSyncedVersion).toHaveBeenCalledWith(ROOM_NAME, 150) |
| 88 | + // setSyncedVersion fired only for the two durable writes, never for a streaming snapshot. |
| 89 | + expect(fakeStore.setSyncedVersion).toHaveBeenCalledTimes(2) |
| 90 | + }) |
| 91 | +}) |
0 commit comments