| 1 | import assert from "node:assert/strict"; |
| 2 | import { readFileSync } from "node:fs"; |
| 3 | |
| 4 | let generation = 0; |
| 5 | const statusWriteIsAllowed = (candidateGeneration: number, rebuildFailed: boolean) => !rebuildFailed && candidateGeneration === generation; |
| 6 | const renderedStates: string[] = []; |
| 7 | let resolveRemoteStatus: ((state: string) => void) | undefined; |
| 8 | const remoteStatus = new Promise<string>((resolve) => { |
| 9 | resolveRemoteStatus = resolve; |
| 10 | }); |
| 11 | const remoteGeneration = generation; |
| 12 | const lateRemoteWrite = remoteStatus.then((state) => { |
| 13 | if (statusWriteIsAllowed(remoteGeneration, false)) renderedStates.push(state); |
| 14 | }); |
| 15 | |
| 16 | // The desktop rebuild rejects after the finished event has already started its |
| 17 | // status read. Restoring the retryable failure must fence out that late read. |
| 18 | generation += 1; |
| 19 | renderedStates.push("degraded"); |
| 20 | resolveRemoteStatus?.("opening"); |
| 21 | await lateRemoteWrite; |
| 22 | assert.deepEqual( |
| 23 | renderedStates, |
| 24 | ["degraded"], |
| 25 | "a late pre-failure status response cannot replace the restored retry state", |
| 26 | ); |
| 27 | |
| 28 | const postFailureGeneration = generation; |
| 29 | if (statusWriteIsAllowed(postFailureGeneration, true)) renderedStates.push("opening"); |
| 30 | assert.deepEqual( |
| 31 | renderedStates, |
| 32 | ["degraded"], |
| 33 | "status reads started after a failed rebuild cannot replace its retry state during watcher startup", |
| 34 | ); |
| 35 | |
| 36 | const retryGeneration = generation; |
| 37 | if (statusWriteIsAllowed(retryGeneration, false)) renderedStates.push("ready"); |
| 38 | assert.deepEqual(renderedStates, ["degraded", "ready"], "a new rebuild re-enables authoritative status writes"); |
| 39 | |
| 40 | const projectTreeSource = readFileSync(new URL("../components/ProjectTree.tsx", import.meta.url), "utf8"); |
| 41 | assert.match( |
| 42 | projectTreeSource, |
| 43 | /catch \{\s*catalogRebuildFailedRef\.current = true; catalogStatusGenerationRef\.current \+= 1; setCatalogStatus\(catalogStatus\)/, |
| 44 | "ProjectTree invalidates older status reads before restoring a failed rebuild", |
| 45 | ); |
| 46 | assert.match( |
| 47 | projectTreeSource, |
| 48 | /GetSessionCatalogStatus\(\)[\s\S]*!catalogRebuildFailedRef\.current && catalogStatusGeneration === catalogStatusGenerationRef\.current/, |
| 49 | "ProjectTree fences every event-driven status write behind rebuild ownership", |
| 50 | ); |
| 51 | assert.match( |
| 52 | projectTreeSource, |
| 53 | /GetProjectTreeSnapshot\(\)[\s\S]*!catalogRebuildFailedRef\.current && catalogStatusGeneration === catalogStatusGenerationRef\.current/, |
| 54 | "ProjectTree fences snapshot status writes behind the same rebuild ownership", |
| 55 | ); |
| 56 | |
| 57 | console.log("session catalog status ownership tests passed"); |
| 58 |