test(workspaces): make Git isolation deterministic

This commit is contained in:
Mohamed Boudra
2026-08-14 23:58:52 +02:00
parent 697a0002d9
commit 16e0967408
2 changed files with 32 additions and 29 deletions
@@ -296,11 +296,10 @@ test("replayed common-ref observation survives runtime service reconstruction",
await reconstructed.destroy(workspaceId);
}, 20_000);
test("selected sibling worktrees share common-ref fan-out without touching an unrelated workspace", async () => {
test("selected sibling worktrees retain common-ref fan-out across lifecycle transitions", async () => {
const root = await mkdtemp(path.join(tmpdir(), "paseo-runtime-git-fanout-"));
cleanupRoots.push(root);
const source = await createRepository(path.join(root, "source"));
const unrelated = await createRepository(path.join(root, "unrelated"));
const runtimeIds = new Map<string, string>();
const service = createWorkspaceRuntimeService({
paseoHome: path.join(root, "paseo-home"),
@@ -325,13 +324,6 @@ test("selected sibling worktrees share common-ref fan-out without touching an un
placement: { kind: "branch", branchName, baseRef: "main", worktreeSlug: branchName },
});
}
await service.create({
workspaceId: "unrelated",
runtimeId: "local",
project: { id: "unrelated-project", source: { kind: "host-directory", path: unrelated } },
placement: { kind: "existing" },
});
let resolveFirst!: () => void;
let resolveSibling!: () => void;
const firstChanged = new Promise<void>((resolve) => {
@@ -349,10 +341,8 @@ test("selected sibling worktrees share common-ref fan-out without touching an un
const siblingChangedAfterOwnerDestroy = new Promise<void>((resolve) => {
resolveSiblingAfterOwnerDestroy = resolve;
});
let unrelatedChanges = 0;
const siblingA = await service.bind("sibling-a");
const siblingB = await service.bind("sibling-b");
const unrelatedGit = await service.bind("unrelated");
const subscriptions = [
await observeWorkspaceGit(siblingA, () => {
if (phase === "first-change") resolveFirst();
@@ -362,32 +352,22 @@ test("selected sibling worktrees share common-ref fan-out without touching an un
if (phase === "after-pause-change") resolveSiblingAfterOwnerPause();
if (phase === "after-destroy-change") resolveSiblingAfterOwnerDestroy();
}),
await observeWorkspaceGit(unrelatedGit, () => {
unrelatedChanges += 1;
}),
];
await createBranchThroughRuntime(service, "sibling-a", "shared-ref-change");
await Promise.all([
eventWithin(firstChanged, "first sibling ref update"),
eventWithin(siblingChanged, "second sibling ref update"),
]);
expect(unrelatedChanges).toBe(0);
await service.pause("sibling-a");
phase = "after-pause-change";
await createBranchThroughRuntime(service, "sibling-b", "shared-ref-after-owner-pause");
await eventWithin(siblingChangedAfterOwnerPause, "sibling ref update after owner pause");
expect(unrelatedChanges).toBe(0);
await service.destroy("sibling-a");
phase = "after-destroy-change";
await createBranchThroughRuntime(service, "sibling-b", "shared-ref-after-owner-destroy");
await eventWithin(siblingChangedAfterOwnerDestroy, "sibling ref update after owner destroy");
expect(unrelatedChanges).toBe(0);
await Promise.all(subscriptions.map((subscription) => subscription.unsubscribe()));
await Promise.all([service.destroy("sibling-a"), service.destroy("sibling-b")]);
await service.destroy("unrelated");
}, 20_000);
test("sibling Git observations stay workspace-bound across pause, resume, and unsubscribe", async () => {
@@ -1,5 +1,5 @@
import { execFileSync } from "node:child_process";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
@@ -11,6 +11,7 @@ import {
type FileObserverCallback,
type FileObserverOptions,
} from "../../file-observer/index.js";
import { getGitCommonDir } from "../../../utils/worktree.js";
import { createHostGitObservationOwner } from "./host-git-observation.js";
describe.runIf(process.platform !== "win32")("host Git observation on POSIX", () => {
@@ -22,17 +23,32 @@ describe.runIf(process.platform !== "win32")("host Git observation on POSIX", ()
git(source, ["branch", "sibling"]);
git(source, ["worktree", "add", sibling, "sibling"]);
const unrelated = await repository(path.join(root, "unrelated"));
const owner = createHostGitObservationOwner();
const observer = new InstrumentedFileObserver();
const owner = createHostGitObservationOwner(observer);
let sourceChanges = 0;
let siblingChanges = 0;
let unrelatedChanges = 0;
const [sourceSubscription, siblingSubscription] = await Promise.all([
owner.observe(source, () => undefined),
owner.observe(sibling, () => undefined),
owner.observe(source, () => {
sourceChanges += 1;
}),
owner.observe(sibling, () => {
siblingChanges += 1;
}),
]);
expect(owner.getDiagnostics().activeObservationCount).toBe(1);
const unrelatedSubscription = await owner.observe(unrelated, () => undefined);
const unrelatedSubscription = await owner.observe(unrelated, () => {
unrelatedChanges += 1;
});
expect(owner.getDiagnostics().activeObservationCount).toBe(2);
observer.emit(await realpath(await getGitCommonDir(source)));
expect(sourceChanges).toBe(1);
expect(siblingChanges).toBe(1);
expect(unrelatedChanges).toBe(0);
await sourceSubscription.unsubscribe();
expect(owner.getDiagnostics().activeObservationCount).toBe(2);
await siblingSubscription.unsubscribe();
@@ -85,7 +101,7 @@ describe.runIf(process.platform !== "win32")("host Git observation on POSIX", ()
class InstrumentedFileObserver implements FileObserver {
readonly observer = createFileObserver();
readonly callbacks = new Set<FileObserverCallback>();
readonly callbacks = new Map<FileObserverCallback, string>();
subscribeCount = 0;
async subscribe(
@@ -94,7 +110,7 @@ class InstrumentedFileObserver implements FileObserver {
options?: FileObserverOptions,
) {
this.subscribeCount += 1;
this.callbacks.add(callback);
this.callbacks.set(callback, directory);
const subscription = await this.observer.subscribe(directory, callback, options);
let active = true;
return {
@@ -109,7 +125,14 @@ class InstrumentedFileObserver implements FileObserver {
}
failActive(error: Error): void {
for (const callback of this.callbacks) callback(error, []);
for (const callback of this.callbacks.keys()) callback(error, []);
}
emit(directory: string): void {
for (const [callback, observedDirectory] of this.callbacks) {
if (observedDirectory === directory)
callback(null, [{ path: "refs/heads/main", type: "update" }]);
}
}
getDiagnostics() {