Files
paseo/packages/server/src/server/workspace-git-service.observation.integration.test.ts
T
paseo-bot[bot] 0e7f452c2f Keep duplicate-path setup out of the Git filter CI budget (#8753)
* test(server): prepare duplicate paths before measuring Git filtering

* test(server): clarify the observer path preparation boundary

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-11-04 12:25:42 +00:00

903 lines
33 KiB
TypeScript

import { flushGitCommandTrace } from "../utils/git-command-trace.js";
import { createWorkspaceRuntimeStore } from "./workspace-runtime-store/index.js";
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
import { tmpdir } from "node:os";
import path from "node:path";
import pino from "pino";
import { afterEach, expect, test, vi } from "vitest";
import {
getCheckoutStatus as readCheckoutStatus,
type CheckoutSnapshotFacts,
type CheckoutStatusGit,
} from "../utils/checkout-git.js";
import { CheckoutDiffManager } from "./checkout-diff-manager.js";
import { createFileObserver } from "./file-observer/index.js";
import { WorkspaceGitServiceImpl } from "./workspace-git-service.js";
import type { FileChange, FileObserver, SubscribeToFileChanges } from "./file-observer/index.js";
function createLogger(): pino.Logger {
return pino({ level: "warn" });
}
function createFacts(cwd: string): CheckoutSnapshotFacts {
return {
isGit: true,
worktreeRoot: cwd,
currentBranch: "main",
remoteUrl: null,
absoluteGitDir: path.join(cwd, ".git"),
gitCommonDir: path.join(cwd, ".git"),
paseoWorktree: { isPaseoOwnedWorktree: false },
storedBaseRef: null,
resolvedBaseRef: "main",
mainRepoRoot: null,
comparisonBaseRef: null,
branchRemoteName: null,
branchMergeRef: null,
pullRequestLookupTarget: { headRef: "main" },
};
}
function createStatus(cwd: string): CheckoutStatusGit {
return {
isGit: true,
repoRoot: cwd,
mainRepoRoot: null,
currentBranch: "main",
isDirty: false,
baseRef: "main",
aheadBehind: { ahead: 0, behind: 0 },
aheadOfOrigin: null,
behindOfOrigin: null,
hasRemote: false,
remoteUrl: null,
isPaseoOwnedWorktree: false,
};
}
const cleanup: Array<() => Promise<void>> = [];
afterEach(async () => {
while (cleanup.length > 0) {
await cleanup.pop()?.();
}
});
test.each([false, true])(
"disposal waits for an admitted Git refresh after observation ends (unsubscribed: %s)",
async (unsubscribeBeforeDisposal) => {
const artifacts = path.resolve(".artifacts");
mkdirSync(artifacts, { recursive: true });
const root = realpathSync(mkdtempSync(path.join(artifacts, "git-refresh-shutdown-")));
execFileSync("git", ["init", "-b", "main"], { cwd: root, stdio: "pipe" });
const entered = Promise.withResolvers<void>();
const release = Promise.withResolvers<void>();
const service = new WorkspaceGitServiceImpl({
logger: pino({ enabled: false }),
paseoHome: path.join(root, "home"),
deps: {
getCheckoutStatus: async (...args) => {
entered.resolve();
await release.promise;
return readCheckoutStatus(...args);
},
},
});
const subscription = service.registerWorkspace({ cwd: root }, () => {});
try {
await entered.promise;
if (unsubscribeBeforeDisposal) subscription.unsubscribe();
let disposed = false;
const closing = service.dispose().then(() => {
disposed = true;
return undefined;
});
await new Promise<void>((resolve) => setImmediate(resolve));
expect(disposed).toBe(false);
release.resolve();
await closing;
expect(disposed).toBe(true);
} finally {
release.resolve();
subscription.unsubscribe();
await service.dispose();
rmSync(root, { recursive: true, force: true });
}
},
);
test("a fresh checkout read makes a newly initialized repository available to cached readers", async () => {
const root = realpathSync(mkdtempSync(path.join(tmpdir(), "paseo-checkout-init-")));
const repo = path.join(root, "repo");
mkdirSync(repo);
const service = new WorkspaceGitServiceImpl({
logger: pino({ enabled: false }),
paseoHome: path.join(root, "home"),
});
cleanup.push(async () => {
await service.dispose();
rmSync(root, { recursive: true, force: true });
});
expect((await service.getSnapshot(repo, { includeForge: false })).git.isGit).toBe(false);
execFileSync("git", ["init", "-b", "main"], { cwd: repo, stdio: "pipe" });
expect((await service.getCheckout(repo)).isGit).toBe(true);
const repoRoot = await service.resolveRepoRoot(repo);
expect(realpathSync.native(repoRoot)).toBe(realpathSync.native(repo));
}, 30_000);
test("recursive observation updates tracked state and prunes ignored storms", async () => {
const tempDir = realpathSync(mkdtempSync(path.join(tmpdir(), "paseo-git-observation-")));
const repoDir = path.join(tempDir, "repo");
const trackedPath = path.join(repoDir, "src", "tracked.txt");
const ignoredDir = path.join(repoDir, "build");
const remainingIgnoredDir = path.join(repoDir, "cache");
const newlyTrackedPath = path.join(ignoredDir, "tracked.txt");
mkdirSync(path.join(repoDir, ".git"), { recursive: true });
mkdirSync(path.dirname(trackedPath), { recursive: true });
mkdirSync(ignoredDir, { recursive: true });
mkdirSync(remainingIgnoredDir, { recursive: true });
writeFileSync(trackedPath, "base\n");
writeFileSync(newlyTrackedPath, "base\n");
let activeWatcherCount = 0;
const observer = createFileObserver();
let watcherStartCount = 0;
let onWorkingTreeIgnoreUpdated: (() => void) | null = null;
const deliveredEvents: Array<{
directory: string;
events: FileChange[];
}> = [];
const subscribe: SubscribeToFileChanges = async (directory, callback, options) => {
const subscription = await observer.subscribe(
directory,
(error, events) => {
deliveredEvents.push({ directory, events });
callback(error, events);
},
options,
);
activeWatcherCount += 1;
watcherStartCount += 1;
return {
updateIgnore: async (paths) => {
await subscription.updateIgnore(paths);
if (directory === repoDir) {
const onUpdated = onWorkingTreeIgnoreUpdated;
onWorkingTreeIgnoreUpdated = null;
onUpdated?.();
}
},
unsubscribe: async () => {
await subscription.unsubscribe();
activeWatcherCount -= 1;
},
};
};
const fileObserver = {
subscribe,
getDiagnostics: () => observer.getDiagnostics(),
close: () => observer.close(),
};
let observedPath = trackedPath;
let observedRelativePath = "src/tracked.txt";
const readObservedState = () => {
const contents = readFileSync(observedPath, "utf8");
const isDirty = contents !== "base\n";
return {
isDirty,
additions: isDirty ? contents.trim().split("\n").length : 0,
};
};
const getCheckoutSnapshotFacts = vi.fn(async (cwd: string) => createFacts(cwd));
const getCheckoutStatus = vi.fn(async (cwd: string) => ({
...createStatus(cwd),
isDirty: readObservedState().isDirty,
}));
const getCheckoutShortstat = vi.fn(async () => ({
additions: readObservedState().additions,
deletions: 0,
}));
const getCheckoutWorktreeState = vi.fn(async () => {
const additions = readObservedState().additions;
return { isDirty: true, diffStat: { additions, deletions: 0 } };
});
const getCheckoutDiff = vi.fn(async () => {
const additions = readObservedState().additions;
return {
diff: "",
structured: [
{ path: observedRelativePath, additions, deletions: 0, status: "modified" as const },
],
};
});
let buildIgnored = true;
const runGitCommand = vi.fn(async (args: string[]) => {
if (args[0] === "rev-parse") {
return {
stdout: `${repoDir}\n`,
stderr: "",
truncated: false,
exitCode: 0,
signal: null,
};
}
if (args[0] === "ls-files") {
return {
stdout: `${buildIgnored ? "build/\n" : ""}cache/\n`,
stderr: "",
truncated: false,
exitCode: 0,
signal: null,
};
}
throw new Error(`Unexpected Git command: ${args.join(" ")}`);
});
const service = new WorkspaceGitServiceImpl({
logger: createLogger(),
paseoHome: path.join(tempDir, "paseo-home"),
fileObserver,
deps: {
getCheckoutSnapshotFacts,
getCheckoutStatus,
getCheckoutShortstat,
getCheckoutWorktreeState,
getCheckoutDiff,
runGitCommand,
} as never,
});
const workspaceRuntimeStore = createWorkspaceRuntimeStore();
service.onSnapshotUpdated((snapshot) => workspaceRuntimeStore.set(snapshot));
const diffManager = new CheckoutDiffManager({
workspaceRuntimeStore: workspaceRuntimeStore,
logger: createLogger(),
paseoHome: path.join(tempDir, "paseo-home"),
workspaceGitService: service,
});
const summaryListener = vi.fn();
const diffListener = vi.fn();
const summarySubscription = service.registerWorkspace({ cwd: repoDir }, summaryListener);
const diffSubscription = await diffManager.subscribe(
{ cwd: repoDir, compare: { mode: "uncommitted" } },
diffListener,
);
cleanup.push(async () => {
diffSubscription.unsubscribe();
summarySubscription.unsubscribe();
diffManager.dispose();
await service.dispose();
expect(activeWatcherCount).toBe(0);
rmSync(tempDir, { recursive: true, force: true });
});
await vi.waitFor(
() => {
expect(
activeWatcherCount,
JSON.stringify({
observer: observer.getDiagnostics(),
service: service.getMetrics(),
deliveredEvents,
}),
).toBe(2);
expect(workspaceRuntimeStore.get(repoDir)).not.toBeNull();
expect(service.getMetrics()).toMatchObject({
workspaceObservationSetupInFlightCount: 0,
workspaceRefreshInFlightCount: 0,
workspaceRefreshQueuedCount: 0,
});
},
{ timeout: 5_000 },
);
writeFileSync(trackedPath, "base\n");
await vi.waitFor(
() => {
const events = deliveredEvents.flatMap((batch) => batch.events);
expect(events.map((event) => event.path)).toContain(trackedPath);
expect(getCheckoutWorktreeState).toHaveBeenCalled();
expect(service.getMetrics()).toMatchObject({
workspaceRefreshInFlightCount: 0,
workspaceRefreshQueuedCount: 0,
});
},
{ timeout: 5_000 },
);
getCheckoutSnapshotFacts.mockClear();
getCheckoutStatus.mockClear();
getCheckoutShortstat.mockClear();
getCheckoutWorktreeState.mockClear();
getCheckoutDiff.mockClear();
runGitCommand.mockClear();
deliveredEvents.length = 0;
expect(runGitCommand).not.toHaveBeenCalled();
expect(getCheckoutWorktreeState).not.toHaveBeenCalled();
expect(getCheckoutDiff, JSON.stringify(deliveredEvents)).not.toHaveBeenCalled();
expect(service.getMetrics().workspaceRefreshQueuedCount).toBe(0);
for (let index = 0; index < 100; index += 1) {
writeFileSync(path.join(ignoredDir, `artifact-${index}.txt`), `${index}\n`);
}
await new Promise((resolve) => setTimeout(resolve, 750));
expect(runGitCommand).not.toHaveBeenCalled();
expect(getCheckoutSnapshotFacts).not.toHaveBeenCalled();
expect(getCheckoutStatus).not.toHaveBeenCalled();
expect(getCheckoutShortstat).not.toHaveBeenCalled();
expect(getCheckoutWorktreeState).not.toHaveBeenCalled();
expect(getCheckoutDiff).not.toHaveBeenCalled();
expect(service.getMetrics().workspaceRefreshQueuedCount).toBe(0);
writeFileSync(trackedPath, "first\nsecond\n");
await vi.waitFor(
() => {
expect(summaryListener).toHaveBeenLastCalledWith(
expect.objectContaining({
git: expect.objectContaining({
isDirty: true,
diffStat: { additions: 2, deletions: 0 },
}),
}),
);
expect(diffListener).toHaveBeenLastCalledWith({
cwd: repoDir,
files: [
{
path: "src/tracked.txt",
additions: 2,
deletions: 0,
status: "modified",
},
],
error: null,
});
expect(service.getMetrics().workspaceRefreshQueuedCount).toBe(0);
},
{ timeout: 5_000 },
);
writeFileSync(trackedPath, "first\nsecond\nthird\n");
await vi.waitFor(
() => {
expect(summaryListener).toHaveBeenLastCalledWith(
expect.objectContaining({
git: expect.objectContaining({
diffStat: { additions: 3, deletions: 0 },
}),
}),
);
expect(diffListener).toHaveBeenLastCalledWith(
expect.objectContaining({
files: [expect.objectContaining({ additions: 3 })],
}),
);
expect(service.getMetrics()).toMatchObject({
workspaceRefreshInFlightCount: 0,
workspaceRefreshQueuedCount: 0,
});
},
{ timeout: 5_000 },
);
expect(getCheckoutSnapshotFacts).not.toHaveBeenCalled();
expect(getCheckoutStatus).not.toHaveBeenCalled();
buildIgnored = false;
observedPath = newlyTrackedPath;
observedRelativePath = "build/tracked.txt";
let editedDuringIgnoreUpdate = false;
onWorkingTreeIgnoreUpdated = () => {
editedDuringIgnoreUpdate = true;
writeFileSync(newlyTrackedPath, "first\nsecond\n");
};
writeFileSync(path.join(repoDir, ".gitignore"), "cache/\n");
await vi.waitFor(
() => {
expect(editedDuringIgnoreUpdate).toBe(true);
expect(watcherStartCount).toBe(2);
expect(activeWatcherCount).toBe(2);
expect(summaryListener).toHaveBeenLastCalledWith(
expect.objectContaining({
git: expect.objectContaining({
isDirty: true,
diffStat: { additions: 2, deletions: 0 },
}),
}),
);
expect(diffListener).toHaveBeenLastCalledWith({
cwd: repoDir,
files: [
{
path: "build/tracked.txt",
additions: 2,
deletions: 0,
status: "modified",
},
],
error: null,
});
expect(service.getMetrics()).toMatchObject({
workspaceRefreshInFlightCount: 0,
workspaceRefreshQueuedCount: 0,
});
},
{ timeout: 8_000 },
);
getCheckoutSnapshotFacts.mockClear();
getCheckoutStatus.mockClear();
getCheckoutShortstat.mockClear();
getCheckoutWorktreeState.mockClear();
getCheckoutDiff.mockClear();
runGitCommand.mockClear();
summaryListener.mockClear();
diffListener.mockClear();
writeFileSync(newlyTrackedPath, "first\nsecond\nthird\nfourth\n");
await vi.waitFor(
() => {
expect(summaryListener).toHaveBeenLastCalledWith(
expect.objectContaining({
git: expect.objectContaining({
isDirty: true,
diffStat: { additions: 4, deletions: 0 },
}),
}),
);
expect(diffListener).toHaveBeenLastCalledWith({
cwd: repoDir,
files: [
{
path: "build/tracked.txt",
additions: 4,
deletions: 0,
status: "modified",
},
],
error: null,
});
expect(service.getMetrics()).toMatchObject({
workspaceRefreshInFlightCount: 0,
workspaceRefreshQueuedCount: 0,
});
},
{ timeout: 5_000 },
);
getCheckoutSnapshotFacts.mockClear();
getCheckoutStatus.mockClear();
getCheckoutShortstat.mockClear();
getCheckoutWorktreeState.mockClear();
getCheckoutDiff.mockClear();
runGitCommand.mockClear();
for (let index = 0; index < 100; index += 1) {
writeFileSync(path.join(remainingIgnoredDir, `artifact-${index}.txt`), `${index}\n`);
}
await new Promise((resolve) => setTimeout(resolve, 750));
expect(runGitCommand).not.toHaveBeenCalled();
expect(getCheckoutSnapshotFacts).not.toHaveBeenCalled();
expect(getCheckoutStatus).not.toHaveBeenCalled();
expect(getCheckoutShortstat).not.toHaveBeenCalled();
expect(getCheckoutWorktreeState).not.toHaveBeenCalled();
expect(getCheckoutDiff).not.toHaveBeenCalled();
expect(service.getMetrics().workspaceRefreshQueuedCount).toBe(0);
}, 30_000);
test("a late Git-ignored tree is pruned while tracked changes still notify consumers", async () => {
const tempDir = realpathSync(mkdtempSync(path.join(tmpdir(), "paseo-real-ignore-")));
const repoDir = path.join(tempDir, "repo");
mkdirSync(repoDir);
const git = (...args: string[]) =>
execFileSync("git", args, { cwd: repoDir, encoding: "utf8" }).trim();
git("init", "-q");
writeFileSync(path.join(repoDir, ".gitignore"), "node_modules/\n");
const trackedPath = path.join(repoDir, "tracked.txt");
writeFileSync(trackedPath, "before\n");
git("add", ".gitignore", "tracked.txt");
git("-c", "user.name=Test", "-c", "user.email=test@localhost", "commit", "-qm", "fixture");
const observer = createFileObserver();
const ignoredDir = path.join(repoDir, "node_modules");
const completedIgnoreUpdates: string[][] = [];
let notifications = 0;
let expectTrackedEdit = false;
let trackedBatchReachedConsumer = false;
// Observe the real subscription without replacing Git, filesystem, or watcher behavior.
const fileObserver: FileObserver = {
subscribe: async (directory, callback, options) => {
const realSubscription = await observer.subscribe(
directory,
(error, events) => {
const includesTrackedEdit =
expectTrackedEdit && events.some((e) => e.path === trackedPath);
const before = notifications;
callback(error, events);
if (includesTrackedEdit && notifications > before) trackedBatchReachedConsumer = true;
},
options,
);
return {
updateIgnore: async (paths) => {
await realSubscription.updateIgnore(paths);
completedIgnoreUpdates.push([...paths]);
},
unsubscribe: () => realSubscription.unsubscribe(),
};
},
getDiagnostics: () => observer.getDiagnostics(),
close: () => observer.close(),
};
const service = new WorkspaceGitServiceImpl({
logger: pino({ enabled: false }),
paseoHome: path.join(tempDir, "home"),
fileObserver,
});
let subscription: Awaited<ReturnType<typeof service.requestWorkingTreeWatch>> | undefined;
try {
subscription = await service.requestWorkingTreeWatch(repoDir, () => {
notifications += 1;
});
for (let index = 0; index < 20; index += 1) {
const directory = path.join(ignoredDir, `dependency-${index}`);
mkdirSync(directory, { recursive: true });
writeFileSync(path.join(directory, "index.js"), "first");
}
expect(git("ls-files", "-o", "-i", "--directory", "--exclude-standard")).toBe("node_modules/");
await expect
.poll(() => completedIgnoreUpdates.some((paths) => paths.includes(ignoredDir)), {
timeout: 5_000,
})
.toBe(true);
const atIgnoreBoundary = observer.getDiagnostics();
for (let index = 0; index < 20; index += 1) {
writeFileSync(path.join(ignoredDir, `dependency-${index}`, "second.js"), "second");
}
expectTrackedEdit = true;
writeFileSync(trackedPath, "after\n");
await expect.poll(() => trackedBatchReachedConsumer, { timeout: 5_000 }).toBe(true);
const afterTrackedEdit = observer.getDiagnostics();
expect(afterTrackedEdit.nativeHandleCount).toBe(atIgnoreBoundary.nativeHandleCount);
expect(afterTrackedEdit.nativeTrackedFileCount).toBe(atIgnoreBoundary.nativeTrackedFileCount);
expect(
afterTrackedEdit.nativeClassificationCount - atIgnoreBoundary.nativeClassificationCount,
).toBeLessThanOrEqual(1);
} finally {
subscription?.unsubscribe();
await service.dispose();
await observer.close();
rmSync(tempDir, { recursive: true, force: true });
}
}, 15_000);
test("Git refresh, command tracing and 10,000 ignored alias events do no synchronous IO", async () => {
const { execFile } = await import("node:child_process");
const { promisify } = await import("node:util");
const { fileURLToPath } = await import("node:url");
const repositoryRoot = fileURLToPath(new URL("../../../../", import.meta.url));
const source = `
import assert from 'node:assert/strict';
import { write } from 'node:fs';
import { mkdtemp, mkdir, writeFile, symlink, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { setImmediate as tick } from 'node:timers/promises';
import { createHook } from 'node:async_hooks';
import pino from 'pino';
import { WorkspaceGitServiceImpl } from './packages/server/src/server/workspace-git-service.ts';
import { runGitCommand } from './packages/server/src/utils/run-git-command.ts';
import { writePaseoWorktreeMetadata } from './packages/server/src/utils/worktree-metadata.ts';
const root = await mkdtemp(join(tmpdir(), 'git-async-contract-'));
const home = join(root, 'home');
const repo = join(root, 'repo');
const worktree = join(home, 'worktrees', 'project', 'topic');
const alias = join(root, 'alias');
const logger = pino({ level: 'trace' }, { write() {} });
// Async descriptor writes avoid Windows stderr's synchronous stream API.
const marker = (label) => new Promise((resolve, reject) => {
write(2, label + '\\n', (error) => error ? reject(error) : resolve());
});
let callback;
let notifications = 0;
const service = new WorkspaceGitServiceImpl({ logger, paseoHome: home, deps: {
subscribe: async (_root, onChange) => {
callback = onChange;
return { updateIgnore: async () => {}, unsubscribe: async () => {} };
}
} });
try {
await mkdir(repo);
await runGitCommand(['init', '-b', 'main'], { cwd: repo });
await runGitCommand(['-c', 'user.name=Test', '-c', 'user.email=test@example.com', 'commit', '--allow-empty', '-m', 'base'], { cwd: repo });
await mkdir(join(home, 'worktrees', 'project'), { recursive: true });
await runGitCommand(['worktree', 'add', '-b', 'topic', worktree], { cwd: repo });
await writePaseoWorktreeMetadata(worktree, { baseRefName: 'main' });
await writeFile(join(worktree, '.gitignore'), 'ignored/\\n');
await mkdir(join(worktree, 'ignored'));
await writeFile(join(worktree, 'ignored', 'noise'), 'noise');
await symlink(worktree, alias, process.platform === 'win32' ? 'junction' : 'dir');
const subscription = await service.requestWorkingTreeWatch(alias, () => { notifications++; });
// The observer hashes and coalesces paths in a Map before delivery. Reuse
// two prepared strings instead of constructing 10,000 equal-valued strings.
const noisePaths = [join(worktree, 'ignored', 'noise'), join(alias, 'ignored', 'noise')];
const events = Array.from({ length: process.env.GIT_IO_TRACE ? 2 : 10000 }, (_, i) => ({ type: 'update', path: noisePaths[i % 2] }));
let fsCalls = 0;
const hook = createHook({ init(_id, type) { if (type.startsWith('FSREQ')) fsCalls++; } });
// Collect repository setup and event-fixture garbage before measuring
// the cold filter. Allocations made by the filter still count.
global.gc();
await tick();
await marker('FILTER_START');
// Schedule the control only after marker IO settles, so it cannot run
// and allocate garbage before the cold filter starts. No await may
// separate this scheduling from the synchronous measurement.
const threadCpuUsage = Reflect.get(process, 'threadCpuUsage');
let unrelatedWorkStarted = false;
const unrelatedWork = tick().then(() => {
unrelatedWorkStarted = true;
const started = threadCpuUsage();
while (true) {
const cpu = threadCpuUsage(started);
if (cpu.user + cpu.system >= 80000) break;
}
});
assert.equal(unrelatedWorkStarted, false, 'control work must not contaminate the cold filter');
hook.enable();
let blockingMs;
let elapsedMs;
let cpu;
try {
// This callback is synchronous: measure its own span, without waiting
// for marker IO, loader messages or other event-loop callbacks.
const startedAt = performance.now();
const startedCpu = threadCpuUsage();
callback(null, events);
cpu = threadCpuUsage(startedCpu);
// Cap coarse Windows CPU accounting by elapsed time; exclude descheduling.
elapsedMs = performance.now() - startedAt;
blockingMs = Math.min(elapsedMs, (cpu.user + cpu.system) / 1000);
} finally {
hook.disable();
await marker('FILTER_END');
}
await unrelatedWork;
assert.equal(unrelatedWorkStarted, true, 'control work must run outside the filter span');
assert.equal(fsCalls, 0, "filesystem calls");
assert.equal(notifications, 0, "ignored event notification");
assert.ok(blockingMs < 20, 'blocking filter work: ' + JSON.stringify({ blockingMs, elapsedMs, cpu }));
console.log(JSON.stringify({ blockingMs, fsCalls }));
// Alias identity remains useful after deletion: no per-event realpath can recover this file.
callback(null, [{ type: 'delete', path: join(worktree, 'tracked.txt') }]);
assert.equal(notifications, 1);
await marker('REFRESH_START');
const snapshot = await service.getSnapshot(worktree);
assert.equal(snapshot.git.isGit, true);
assert.equal(snapshot.git.isPaseoOwnedWorktree, true);
await runGitCommand(['status', '--porcelain'], { cwd: alias, logger });
await marker('REFRESH_END');
subscription.unsubscribe();
} finally {
await service.dispose();
await rm(root, { recursive: true, force: true });
}
`;
for (const tracing of [false, true]) {
const { stdout, stderr } = await promisify(execFile)(
process.execPath,
[
"--expose-gc",
"--import",
"tsx",
...(tracing ? ["--trace-sync-io"] : []),
"--input-type=module",
"--eval",
source,
],
{
cwd: repositoryRoot,
env: { ...process.env, GIT_IO_TRACE: tracing ? "1" : "" },
timeout: 30_000,
maxBuffer: 4 * 1024 * 1024,
},
).catch((error) => {
throw new Error(String(error.stderr).replaceAll(/at .*/g, "[child stack omitted]"));
});
for (const phase of ["FILTER", "REFRESH"]) {
const output = stderr.split(`${phase}_START\n`)[1]?.split(`${phase}_END\n`)[0];
expect(output, `${phase}: synchronous filesystem IO`).toBe("");
}
const measurement = JSON.parse(stdout.trim().split("\n")[0]);
console.info("Git filter budget", { tracing, ...measurement });
expect(measurement).toMatchObject({ fsCalls: 0 });
}
}, 60_000);
test("Git event identity preserves Windows namespace, case and subtree boundaries", async () => {
const { GitObservationPaths } = await import("./git-observation-paths.js");
const paths = await GitObservationPaths.create("C:\\paseo-observation-contract");
const accept = paths.createEventFilter(["C:\\paseo-observation-contract\\Build"]);
expect(paths.relative("\\\\?\\C:\\PASEO-OBSERVATION-CONTRACT\\refs\\heads\\MixedCase")).toBe(
"refs/heads/MixedCase",
);
expect(accept("\\\\?\\C:\\paseo-observation-contract\\build\\noise")).toBe(false);
expect(accept("C:\\paseo-observation-contract\\BuildSibling\\file")).toBe(true);
});
test("concurrent alias requests establish one working-tree subscription", async () => {
const { symlink } = await import("node:fs/promises");
const root = realpathSync(mkdtempSync(path.join(tmpdir(), "git-observation-concurrent-")));
const repo = path.join(root, "repo");
const alias = path.join(root, "alias");
execFileSync("git", ["init", "-b", "main", repo], { stdio: "pipe" });
await symlink(repo, alias, process.platform === "win32" ? "junction" : "dir");
let subscriptions = 0;
const service = new WorkspaceGitServiceImpl({
logger: pino({ enabled: false }),
paseoHome: path.join(root, "home"),
deps: {
subscribe: async () => {
subscriptions++;
return { updateIgnore: async () => {}, unsubscribe: async () => {} };
},
},
});
try {
const watches = await Promise.all([
service.requestWorkingTreeWatch(repo, () => {}),
service.requestWorkingTreeWatch(alias, () => {}),
]);
expect(subscriptions).toBe(1);
for (const watch of watches) watch.unsubscribe();
} finally {
await service.dispose();
rmSync(root, { recursive: true, force: true });
}
});
test("Git event identity retains a symlink's literal target before canonicalizing its parent", async () => {
const { mkdir, symlink } = await import("node:fs/promises");
const { GitObservationPaths } = await import("./git-observation-paths.js");
const root = realpathSync(mkdtempSync(path.join(tmpdir(), "git-observation-literal-target-")));
const repository = path.join(root, "repository");
const parentAlias = path.join(root, "parent-alias");
const workspaceAlias = path.join(root, "workspace-alias");
const literalTarget = path.join(parentAlias, "repository");
const linkType = process.platform === "win32" ? "junction" : "dir";
try {
await mkdir(repository);
await symlink(root, parentAlias, linkType);
await symlink(literalTarget, workspaceAlias, linkType);
const paths = await GitObservationPaths.create(workspaceAlias);
const accept = paths.createEventFilter([path.join(workspaceAlias, "ignored")]);
for (const spelling of [workspaceAlias, literalTarget, repository]) {
expect(paths.relative(path.join(spelling, "deleted.txt"))).toBe("deleted.txt");
expect(accept(path.join(spelling, "ignored", "deleted.txt"))).toBe(false);
}
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test.skipIf(process.platform !== "linux")(
"missing folders stop degraded Git polling and explicit refreshes, then recover",
async () => {
const artifacts = path.resolve(".artifacts");
mkdirSync(artifacts, { recursive: true });
const root = mkdtempSync(path.join(artifacts, "workspace-git-missing-"));
const cwd = path.join(root, "repo");
const initializeRepo = (branch: string) => {
mkdirSync(cwd);
execFileSync("git", ["init", "-b", branch], { cwd, stdio: "pipe" });
writeFileSync(path.join(cwd, "README.md"), "fixture\n");
execFileSync("git", ["add", "."], { cwd, stdio: "pipe" });
execFileSync(
"git",
[
"-c",
"user.name=Test",
"-c",
"user.email=test@example.invalid",
"-c",
"commit.gpgsign=false",
"commit",
"-m",
"fixture",
],
{ cwd, stdio: "pipe" },
);
};
initializeRepo("main");
// Exceed the real Linux observer's directory limit to enter degraded polling.
// No watcher errors or Git responses are injected.
for (let index = 0; index < 5100; index++) {
mkdirSync(path.join(cwd, `generated-${index}`));
}
const trace = path.join(root, "git.jsonl");
writeFileSync(trace, "");
const previousTrace = process.env.PASEO_GIT_TRACE_FILE;
process.env.PASEO_GIT_TRACE_FILE = trace;
let degraded = false;
let suspended = false;
const logger = pino(
{ level: "warn" },
{
write(line) {
const message = JSON.parse(line).msg;
if (message === "Failed to start working tree watcher; using degraded polling")
degraded = true;
if (
message ===
"Workspace folder no longer exists; suspended git observation until it is back"
)
suspended = true;
},
},
);
const service = new WorkspaceGitServiceImpl({ logger, paseoHome: path.join(root, "home") });
const subscription = service.registerWorkspace({ cwd }, () => {});
const commandCount = async () => {
await flushGitCommandTrace();
return readFileSync(trace, "utf8").trim().split("\n").filter(Boolean).length;
};
try {
await expect
.poll(
() => {
const metrics = service.getMetrics();
// Metadata observation writes a canary after the working tree degrades.
return (
degraded &&
metrics.workspaceRefreshInFlightCount === 0 &&
metrics.workspaceObservationSetupInFlightCount === 0
);
},
{ timeout: 15_000 },
)
.toBe(true);
rmSync(cwd, { recursive: true, force: true });
await expect.poll(() => suspended, { timeout: 15_000 }).toBe(true);
await expect.poll(() => service.getMetrics().workspaceRefreshInFlightCount).toBe(0);
const commandsWhileMissing = await commandCount();
await service.getSnapshot(cwd, {
force: true,
includeForge: false,
reason: "refresh-button",
});
await service.refresh(cwd);
expect(await commandCount()).toBe(commandsWhileMissing);
await new Promise((resolve) => setTimeout(resolve, 16_000));
expect(await commandCount()).toBe(commandsWhileMissing);
initializeRepo("restored");
const snapshot = await service.getSnapshot(cwd, {
force: true,
includeForge: false,
reason: "refresh-button",
});
expect(snapshot.git).toMatchObject({ isGit: true, currentBranch: "restored" });
await expect
.poll(() => service.getMetrics(), { timeout: 15_000 })
.toMatchObject({
repositoryWorkspaceLinkCount: 1,
workspaceObservationSetupInFlightCount: 0,
});
} finally {
subscription.unsubscribe();
await service.dispose();
// The diagnostic sink outlives Git commands and owns writes into this fixture.
await flushGitCommandTrace();
if (previousTrace === undefined) delete process.env.PASEO_GIT_TRACE_FILE;
else process.env.PASEO_GIT_TRACE_FILE = previousTrace;
rmSync(root, { recursive: true, force: true });
}
},
60_000,
);