| 1 | import assert from "node:assert/strict"; |
| 2 | import { |
| 3 | acceptWorkspaceRefreshForTests, |
| 4 | activateWorkspaceRefreshScopeForTests, |
| 5 | reconcileWorkspaceRefreshForTests, |
| 6 | resetWorkspaceRefreshStoreForTests, |
| 7 | workspaceRefreshSnapshotForTests, |
| 8 | } from "../lib/workspaceRefreshStore"; |
| 9 | import { createWorkspaceRefreshScheduler, type WorkspaceRefreshTimer } from "../lib/workspaceRefreshScheduler"; |
| 10 | import { workspaceRefreshActions } from "../lib/workspaceRefreshInvalidation"; |
| 11 | import type { WireWorkspaceChanged, WorkspaceRevisions } from "../lib/types"; |
| 12 | |
| 13 | const revisions = (content: number): WorkspaceRevisions => ({ content, tree: content, workingTree: content, gitMeta: 0, session: content }); |
| 14 | const event = (content: number): WireWorkspaceChanged => ({ |
| 15 | revisions: revisions(content), changes: [], allPaths: true, source: "filesystem", watchState: "active", |
| 16 | }); |
| 17 | |
| 18 | function workspaceScopesKeepIndependentRevisionBaselines() { |
| 19 | resetWorkspaceRefreshStoreForTests(); |
| 20 | activateWorkspaceRefreshScopeForTests("tab", "root-a"); |
| 21 | acceptWorkspaceRefreshForTests("tab", event(100)); |
| 22 | assert.equal(workspaceRefreshSnapshotForTests("tab", "root-a").revisions.content, 100); |
| 23 | |
| 24 | activateWorkspaceRefreshScopeForTests("tab", "root-b"); |
| 25 | acceptWorkspaceRefreshForTests("tab", event(1)); |
| 26 | assert.equal(workspaceRefreshSnapshotForTests("tab", "root-b").revisions.content, 1, |
| 27 | "a new root must not inherit the old root's monotonic baseline"); |
| 28 | assert.equal(workspaceRefreshSnapshotForTests("tab", "root-a").revisions.content, 100); |
| 29 | } |
| 30 | |
| 31 | async function reconciliationForcesVisibleResourcesWithoutMovingBackwards() { |
| 32 | resetWorkspaceRefreshStoreForTests(); |
| 33 | activateWorkspaceRefreshScopeForTests("tab", "root"); |
| 34 | acceptWorkspaceRefreshForTests("tab", event(5)); |
| 35 | const initialSequence = workspaceRefreshSnapshotForTests("tab", "root").sequence; |
| 36 | reconcileWorkspaceRefreshForTests("tab", "root", { revisions: revisions(5), watchState: "active" }); |
| 37 | assert.equal(workspaceRefreshSnapshotForTests("tab", "root").sequence, initialSequence, |
| 38 | "ordinary revision reconciliation must not reload an unchanged active workspace"); |
| 39 | reconcileWorkspaceRefreshForTests("tab", "root", { revisions: revisions(5), watchState: "active" }, true); |
| 40 | const forced = workspaceRefreshSnapshotForTests("tab", "root"); |
| 41 | assert.equal(forced.sequence, initialSequence + 1, "focus reconciliation must refresh unwatched external resources even at the same revision"); |
| 42 | assert.equal(forced.allPaths, true); |
| 43 | assert.equal(forced.source, "reconcile"); |
| 44 | |
| 45 | acceptWorkspaceRefreshForTests("tab", event(6)); |
| 46 | reconcileWorkspaceRefreshForTests("tab", "root", { revisions: revisions(5), watchState: "active" }, true); |
| 47 | const current = workspaceRefreshSnapshotForTests("tab", "root"); |
| 48 | assert.equal(current.revisions.content, 6, "an older focus response must not overwrite a newer workspace event"); |
| 49 | assert.equal(current.source, "reconcile", "the focus fallback must still invalidate visible resources after a newer event"); |
| 50 | assert.equal(current.allPaths, true); |
| 51 | } |
| 52 | |
| 53 | function focusReconciliationInvalidatesVisibleResourcesAtEqualRevisions() { |
| 54 | const current = revisions(5); |
| 55 | const focused: WireWorkspaceChanged = { |
| 56 | revisions: current, changes: [], allPaths: true, source: "reconcile", watchState: "active", |
| 57 | }; |
| 58 | assert.deepEqual(workspaceRefreshActions(current, { ...focused, sequence: 2 }, false), { |
| 59 | forceVisible: true, content: true, tree: true, workingTree: true, gitMeta: false, |
| 60 | }); |
| 61 | assert.equal(workspaceRefreshActions(current, { ...focused, sequence: 3 }, true).gitMeta, true, |
| 62 | "focus fallback refreshes history only while that resource is visible"); |
| 63 | |
| 64 | const ordinary: WireWorkspaceChanged = { |
| 65 | revisions: current, changes: [], allPaths: true, source: "filesystem", watchState: "active", |
| 66 | }; |
| 67 | assert.deepEqual(workspaceRefreshActions(current, { ...ordinary, sequence: 4 }, true), { |
| 68 | forceVisible: false, content: false, tree: false, workingTree: false, gitMeta: false, |
| 69 | }); |
| 70 | } |
| 71 | |
| 72 | class FakeTimer implements WorkspaceRefreshTimer { |
| 73 | callbacks: Array<() => void> = []; |
| 74 | schedule(callback: () => void): unknown { this.callbacks.push(callback); return callback; } |
| 75 | cancel(handle: unknown): void { this.callbacks = this.callbacks.filter((callback) => callback !== handle); } |
| 76 | fire(): void { const callback = this.callbacks.shift(); assert.ok(callback); callback(); } |
| 77 | } |
| 78 | |
| 79 | async function refreshSchedulerUsesTrailingQuietWindowAndBoundsConcurrency() { |
| 80 | const timer = new FakeTimer(); |
| 81 | const scheduler = createWorkspaceRefreshScheduler(300, timer); |
| 82 | const runs: string[] = []; |
| 83 | let releaseFirst!: () => void; |
| 84 | const first = new Promise<void>((resolve) => { releaseFirst = resolve; }); |
| 85 | let confirmTrailing!: () => void; |
| 86 | const trailingStarted = new Promise<void>((resolve) => { confirmTrailing = resolve; }); |
| 87 | |
| 88 | scheduler.trigger(() => { runs.push("superseded"); }); |
| 89 | scheduler.trigger(async () => { runs.push("first"); await first; }); |
| 90 | assert.equal(timer.callbacks.length, 1, "retriggering must reset the quiet window"); |
| 91 | timer.fire(); |
| 92 | await Promise.resolve(); |
| 93 | assert.deepEqual(runs, ["first"]); |
| 94 | |
| 95 | scheduler.trigger(() => { runs.push("trailing"); confirmTrailing(); }); |
| 96 | timer.fire(); |
| 97 | await Promise.resolve(); |
| 98 | assert.deepEqual(runs, ["first"], "a refresh must not overlap the in-flight load"); |
| 99 | releaseFirst(); |
| 100 | await trailingStarted; |
| 101 | assert.deepEqual(runs, ["first", "trailing"]); |
| 102 | } |
| 103 | |
| 104 | await workspaceScopesKeepIndependentRevisionBaselines(); |
| 105 | await reconciliationForcesVisibleResourcesWithoutMovingBackwards(); |
| 106 | focusReconciliationInvalidatesVisibleResourcesAtEqualRevisions(); |
| 107 | await refreshSchedulerUsesTrailingQuietWindowAndBoundsConcurrency(); |
| 108 | console.log("ok workspace refresh scope isolation and scheduling"); |
| 109 |