diff --git a/frontend/src/lib/components/home/ImportProjectModal.svelte b/frontend/src/lib/components/home/ImportProjectModal.svelte index 8e9b6acca2..b079409a9c 100644 --- a/frontend/src/lib/components/home/ImportProjectModal.svelte +++ b/frontend/src/lib/components/home/ImportProjectModal.svelte @@ -141,24 +141,21 @@ // card renders from the pick until it lands — counts are the only thing missing, and // zero counts render as no badges rather than as zeroes. // - // The answer is checked against the slug that asked for it: `resource()` aborts the - // previous controller but `fetchHubProject` takes no signal, and nothing orders the - // responses — so picking A, dismissing, then picking B can land A's name, author and - // counts over an import that writes B. - // Kept in a local rather than read back off `detail.current` inside `detail`'s own fetcher, - // which makes the resource's type circular and resolves it to `any`. - let lastDetail = $state(undefined) + // Each answer carries the slug that asked for it, and is read only while that slug is + // still the chosen one: `resource()` aborts the previous controller but `fetchHubProject` + // takes no signal, and nothing orders the responses — so picking A, dismissing, then + // picking B can land A's name, author and counts over an import that writes B. Tagging + // rather than substituting, because whatever the fetcher returns is published: handing + // back the previous project on a superseded response is how it reaches the card. const detail = resource( () => slug, - async (s) => { - if (!s) return undefined - const fetched = await fetchHubProject(s) - if (s === slug) lastDetail = fetched - return lastDetail - } + async (s) => (s ? { slug: s, project: await fetchHubProject(s) } : undefined) + ) + let fetchedDetail = $derived( + detail.current?.slug === slug ? detail.current?.project : undefined ) let project = $derived( - detail.current ?? + fetchedDetail ?? (pick ? { slug: pick.slug, diff --git a/frontend/src/lib/utils/featureUsage.test.ts b/frontend/src/lib/utils/featureUsage.test.ts index 70dd2e9de9..1857efb0c0 100644 --- a/frontend/src/lib/utils/featureUsage.test.ts +++ b/frontend/src/lib/utils/featureUsage.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('$lib/gen', () => ({ OpenAPI: { BASE: '/api' } })) @@ -133,6 +133,13 @@ describe('hubScriptUsageKey', () => { }) describe('hubProjectUsageKey', () => { + // The fixture is module-level and mutable, so each case states the world it needs rather + // than inheriting whatever the case above it left behind. + beforeEach(() => { + hubBaseUrl.url.set('https://hub.windmill.dev') + hubBaseUrl.known.set(true) + }) + it('reports the slug for every spelling of the public hub', () => { for (const hub of [ 'https://hub.windmill.dev', @@ -150,7 +157,6 @@ describe('hubProjectUsageKey', () => { it('answers private until the instance has said which hub it points at', () => { // The store is seeded with the public hub, so a settings read that failed must not // read as permission to report the name. - hubBaseUrl.url.set('https://hub.windmill.dev') hubBaseUrl.known.set(false) expect(hubProjectUsageKey('acme-payroll')).toBe('private') hubBaseUrl.known.set(true)