From 7fcbcaabcf05947e71e5c0e28c82c69ad9ffddd1 Mon Sep 17 00:00:00 2001 From: xarmian Date: Thu, 24 Sep 2026 10:16:11 -0400 Subject: [PATCH] test(web): a superseded collections load does not commit, tested through collectionStore (BUG-2144) (#1509) loadCollections already drops a response a newer load superseded (collectionsFlight's isLatest), which is BUG-2144's fix, but nothing tested that binding: with the drop removed, every store test still passed. The existing superseded-load leg resolves every request with the same list, so it could not see which one committed. This one answers each workspace with its own list and resolves them out of order. Claude-Session: https://claude.ai/code/session_01Vf8uZmYdsrQqJtbAmjUFSX --- .../collectionsSupersededLoad.svelte.test.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 web/src/lib/stores/collectionsSupersededLoad.svelte.test.ts diff --git a/web/src/lib/stores/collectionsSupersededLoad.svelte.test.ts b/web/src/lib/stores/collectionsSupersededLoad.svelte.test.ts new file mode 100644 index 000000000..10104d21c --- /dev/null +++ b/web/src/lib/stores/collectionsSupersededLoad.svelte.test.ts @@ -0,0 +1,70 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { Collection } from '$lib/types'; + +/** + * BUG-2144: a slower `loadCollections(A)` resolving AFTER `loadCollections(B)` + * must not leave A's collections in the global store while B is active. + * + * The guard is `loadCollections`' `!isLatest()` drop (collectionsFlight). It is + * tested here through `collectionStore` itself, because the only other test of + * `isLatest` is singleFlight.test.ts, which tests the primitive and not its + * binding (CONVE-19): with the drop removed from `loadCollections`, every store + * test still passed (measured on BUG-2144's trail). The existing superseded- + * load leg in collectionsEnsure resolves every request with the SAME list, so + * it could not see which one committed; each workspace here answers with its + * own list, which is what makes the stale commit observable. + */ + +async function flush(): Promise { + await new Promise((r) => setTimeout(r, 0)); +} + +function coll(slug: string): Collection { + return { id: `id-${slug}`, slug, name: slug, is_default: true, sort_order: 0 } as Collection; +} + +async function load() { + vi.resetModules(); + const { api } = await import('$lib/api/client'); + const { collectionStore } = await import('./collections.svelte'); + return { api, collectionStore }; +} + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe('BUG-2144 — a superseded collections load does not commit', () => { + it('CONTROL: a single load commits its list and stamps its workspace', async () => { + const { api, collectionStore } = await load(); + vi.spyOn(api.collections, 'list').mockResolvedValue([coll('alpha-only')]); + + await collectionStore.loadCollections('alpha'); + + expect(collectionStore.collections.map((c) => c.slug)).toEqual(['alpha-only']); + expect(collectionStore.collectionsWorkspace).toBe('alpha'); + }); + + it('A then B, B answers first, A answers last: the store holds B and says so', async () => { + const { api, collectionStore } = await load(); + const pending = new Map void>(); + vi.spyOn(api.collections, 'list').mockImplementation( + (ws: string) => new Promise((resolve) => pending.set(ws, resolve)) + ); + + const alpha = collectionStore.loadCollections('alpha'); + const beta = collectionStore.loadCollections('beta'); + await flush(); + expect([...pending.keys()].sort()).toEqual(['alpha', 'beta']); + + pending.get('beta')!([coll('beta-only')]); + await beta; + pending.get('alpha')!([coll('alpha-only')]); + await alpha; + + expect(collectionStore.collections.map((c) => c.slug)).toEqual(['beta-only']); + expect(collectionStore.collectionsWorkspace).toBe('beta'); + expect(collectionStore.collectionsAreFreshFor('beta')).toBe(true); + expect(collectionStore.collectionsAreFreshFor('alpha')).toBe(false); + }); +});