Files
paseo/packages/server/src/server/workspace-git-service.test.ts
T
Mohamed BoudraandGitHub 0a106dd35f Keep Git status and diffs current across platforms (#2775)
* fix(server): make Git observation cross-platform

Platform-specific watchers and forced full refreshes amplified Git processes and could miss nested changes. Share checkout and repository observation internally, scope final-state refreshes, and bound degraded polling so streamed summaries and diffs stay correct under bursts.

* fix(server): refresh Git worktree state on metadata changes

* fix(server): keep Git watcher runtime inputs current

* fix(server): normalize watcher runtime boundaries

* fix(server): reconcile Git observer topology

* test(server): report Git watcher event batches

* fix(server): close Git observation gaps

* chore(ci): remove temporary Git observation jobs
2026-08-02 12:20:20 +02:00

968 lines
31 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import os from "node:os";
import path, { join } from "node:path";
import type pino from "pino";
import type { ForgeService } from "../services/forge-service.js";
import type {
CheckoutSnapshotFacts,
CheckoutStatusGit,
PullRequestStatusResult,
} from "../utils/checkout-git.js";
import {
WorkspaceGitServiceImpl,
type WorkspaceGitRuntimeSnapshot,
} from "./workspace-git-service.js";
const REPO_CWD = path.resolve("/tmp/repo");
function createLogger() {
const logger = {
child: () => logger,
debug: vi.fn(),
warn: vi.fn(),
};
return logger;
}
function createSnapshot(
cwd: string,
overrides?: {
git?: Partial<WorkspaceGitRuntimeSnapshot["git"]>;
forge?: Partial<WorkspaceGitRuntimeSnapshot["forge"]>;
},
): WorkspaceGitRuntimeSnapshot {
const base: WorkspaceGitRuntimeSnapshot = {
cwd,
git: {
isGit: true,
repoRoot: cwd,
mainRepoRoot: null,
currentBranch: "main",
remoteUrl: "https://github.com/acme/repo.git",
isPaseoOwnedWorktree: false,
isDirty: false,
baseRef: "main",
aheadBehind: { ahead: 0, behind: 0 },
aheadOfOrigin: 0,
behindOfOrigin: 0,
hasRemote: true,
diffStat: { additions: 1, deletions: 0 },
},
forge: {
featuresEnabled: true,
pullRequest: {
url: "https://github.com/acme/repo/pull/123",
title: "Update feature",
state: "open",
baseRefName: "main",
headRefName: "feature",
isMerged: false,
},
error: null,
},
};
const featuresEnabled = overrides?.forge?.featuresEnabled ?? base.forge.featuresEnabled;
const authState =
overrides?.forge?.authState ?? (featuresEnabled ? "authenticated" : "no_remote");
const forgeName = resolveSnapshotForgeName(featuresEnabled, overrides);
return {
cwd,
git: {
...base.git,
...overrides?.git,
},
forge: {
...base.forge,
...overrides?.forge,
featuresEnabled,
authState,
...(forgeName ? { forge: forgeName } : {}),
pullRequest: resolveSnapshotPullRequest(base, overrides),
error: resolveSnapshotError(base, overrides),
},
};
}
function hasForgeOverride(
overrides: { forge?: Partial<WorkspaceGitRuntimeSnapshot["forge"]> } | undefined,
key: keyof WorkspaceGitRuntimeSnapshot["forge"],
): boolean {
return Boolean(overrides?.forge && key in overrides.forge);
}
function resolveSnapshotForgeName(
featuresEnabled: boolean,
overrides: { forge?: Partial<WorkspaceGitRuntimeSnapshot["forge"]> } | undefined,
): string | undefined {
if (hasForgeOverride(overrides, "forge")) {
return overrides?.forge?.forge;
}
return featuresEnabled ? "github" : undefined;
}
function resolveSnapshotPullRequest(
base: WorkspaceGitRuntimeSnapshot,
overrides: { forge?: Partial<WorkspaceGitRuntimeSnapshot["forge"]> } | undefined,
): WorkspaceGitRuntimeSnapshot["forge"]["pullRequest"] {
if (hasForgeOverride(overrides, "pullRequest")) {
return overrides?.forge?.pullRequest ?? null;
}
return base.forge.pullRequest;
}
function resolveSnapshotError(
base: WorkspaceGitRuntimeSnapshot,
overrides: { forge?: Partial<WorkspaceGitRuntimeSnapshot["forge"]> } | undefined,
): WorkspaceGitRuntimeSnapshot["forge"]["error"] {
if (hasForgeOverride(overrides, "error")) {
return overrides?.forge?.error ?? null;
}
return base.forge.error;
}
function createCheckoutStatus(
cwd: string,
overrides?: Partial<CheckoutStatusGit>,
): CheckoutStatusGit {
return {
isGit: true,
repoRoot: cwd,
mainRepoRoot: null,
currentBranch: "main",
isDirty: false,
baseRef: "main",
aheadBehind: { ahead: 0, behind: 0 },
aheadOfOrigin: 0,
behindOfOrigin: 0,
hasRemote: true,
remoteUrl: "https://github.com/acme/repo.git",
isPaseoOwnedWorktree: false,
...overrides,
};
}
function createCheckoutSnapshotFacts(cwd: string): CheckoutSnapshotFacts {
return {
isGit: true,
worktreeRoot: cwd,
currentBranch: "main",
remoteUrl: "https://github.com/acme/repo.git",
absoluteGitDir: join(cwd, ".git"),
gitCommonDir: join(cwd, ".git"),
paseoWorktree: { isPaseoOwnedWorktree: false },
storedBaseRef: null,
resolvedBaseRef: "main",
mainRepoRoot: null,
comparisonBaseRef: null,
branchRemoteName: "origin",
branchMergeRef: "refs/heads/main",
pullRequestLookupTarget: { headRef: "main" },
};
}
function createPullRequestStatusResult(
overrides?: Partial<PullRequestStatusResult>,
): PullRequestStatusResult {
return {
status: {
url: "https://github.com/acme/repo/pull/123",
title: "Update feature",
state: "open",
baseRefName: "main",
headRefName: "feature",
isMerged: false,
},
authState: "authenticated",
featuresEnabled: true,
githubFeaturesEnabled: true,
...overrides,
};
}
async function flushPromises(): Promise<void> {
for (let i = 0; i < 6; i++) {
await Promise.resolve();
}
}
function createDeferred<T>() {
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
function createAsyncSubscription() {
return { unsubscribe: vi.fn(() => Promise.resolve()) };
}
function createGitHubServiceStub(): ForgeService {
return {
listPullRequests: vi.fn(async () => []),
listIssues: vi.fn(async () => []),
searchIssuesAndPrs: vi.fn(async () => ({
items: [],
featuresEnabled: true,
githubFeaturesEnabled: true,
})),
getPullRequest: vi.fn(async () => ({
number: 1,
title: "PR",
url: "https://github.com/acme/repo/pull/1",
state: "OPEN",
body: null,
baseRefName: "main",
headRefName: "feature",
labels: [],
})),
getPullRequestHeadRef: vi.fn(async () => "feature"),
getPullRequestCheckoutTarget: vi.fn(async ({ number }) => ({
number,
baseRefName: "main",
headRefName: "feature",
headOwnerLogin: null,
headRepositorySshUrl: null,
headRepositoryUrl: null,
isCrossRepository: false,
})),
getCurrentPullRequestStatus: vi.fn(async () => null),
createPullRequest: vi.fn(async () => ({
url: "https://github.com/acme/repo/pull/1",
number: 1,
})),
mergePullRequest: vi.fn(async () => ({ success: true })),
isAuthenticated: vi.fn(async () => true),
authProbeCanThrow: true,
invalidate: vi.fn(),
};
}
interface CreateServiceTestOptions {
subscribe?: ReturnType<typeof vi.fn>;
getCheckoutStatus?: ReturnType<typeof vi.fn>;
getCheckoutSnapshotFacts?: ReturnType<typeof vi.fn>;
getCheckoutShortstat?: ReturnType<typeof vi.fn>;
getCheckoutWorktreeState?: ReturnType<typeof vi.fn>;
getPullRequestStatus?: ReturnType<typeof vi.fn>;
github?: ForgeService;
resolveAbsoluteGitDir?: ReturnType<typeof vi.fn>;
hasOriginRemote?: ReturnType<typeof vi.fn>;
runGitFetch?: ReturnType<typeof vi.fn>;
runGitCommand?: ReturnType<typeof vi.fn>;
getWorkspaceGitSelfHealPhaseMs?: (cwd: string) => number;
now?: () => Date;
}
function buildDefaultTestServiceDeps() {
return {
subscribe: vi.fn(async () => ({ unsubscribe: vi.fn(async () => {}) })),
getCheckoutSnapshotFacts: vi.fn(async (cwd: string) => createCheckoutSnapshotFacts(cwd)),
getCheckoutStatus: vi.fn(async (cwd: string) => createCheckoutStatus(cwd)),
getCheckoutShortstat: vi.fn(async () => ({
additions: 1,
deletions: 0,
})),
getCheckoutWorktreeState: vi.fn(),
getPullRequestStatus: vi.fn(async () => createPullRequestStatusResult()),
forgeOverrides: { github: createGitHubServiceStub() },
resolveAbsoluteGitDir: vi.fn(async () => join(REPO_CWD, ".git")),
hasOriginRemote: vi.fn(async () => false),
runGitFetch: vi.fn(async () => {}),
runGitCommand: vi.fn(async () => ({
stdout: `${REPO_CWD}\n`,
stderr: "",
truncated: false,
exitCode: 0,
signal: null,
})),
getWorkspaceGitSelfHealPhaseMs: vi.fn(() => 30_000),
now: () => new Date("2026-04-12T00:00:00.000Z"),
};
}
function createService(options?: CreateServiceTestOptions) {
const deps = { ...buildDefaultTestServiceDeps(), ...options };
deps.getCheckoutWorktreeState =
options?.getCheckoutWorktreeState ??
vi.fn(async (cwd: string) => {
const status = await deps.getCheckoutStatus(cwd);
if (!status.isGit) {
throw new Error("Expected a git checkout");
}
return {
isDirty: status.isDirty,
diffStat: await deps.getCheckoutShortstat(),
};
});
return new WorkspaceGitServiceImpl({
logger: createLogger() as unknown as pino.Logger,
paseoHome: "/tmp/paseo-test",
deps,
});
}
describe("WorkspaceGitServiceImpl", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
test("registerWorkspace returns a subscription without an initial snapshot contract", async () => {
const service = createService();
const listener = vi.fn();
const subscription = service.registerWorkspace({ cwd: REPO_CWD }, listener);
expect(subscription).toEqual({ unsubscribe: expect.any(Function) });
expect("initial" in subscription).toBe(false);
expect(listener).not.toHaveBeenCalled();
expect(service.peekSnapshot(REPO_CWD)).toBeNull();
subscription.unsubscribe();
service.dispose();
});
test("onSnapshotUpdated emits only for observed workspace snapshots and can unsubscribe", async () => {
const service = createService();
const snapshotListener = vi.fn();
const snapshotSubscription = service.onSnapshotUpdated(snapshotListener);
await service.getSnapshot(REPO_CWD, { force: true, reason: "unobserved" });
expect(snapshotListener).not.toHaveBeenCalled();
const workspaceSubscription = service.registerWorkspace({ cwd: REPO_CWD }, vi.fn());
await service.getSnapshot(REPO_CWD, { force: true, reason: "observed" });
expect(snapshotListener).toHaveBeenCalledTimes(1);
expect(snapshotListener).toHaveBeenCalledWith(createSnapshot(REPO_CWD));
snapshotSubscription.unsubscribe();
await service.getSnapshot(REPO_CWD, { force: true, reason: "after-unsubscribe" });
expect(snapshotListener).toHaveBeenCalledTimes(1);
workspaceSubscription.unsubscribe();
service.dispose();
});
test("getSnapshot populates github pull request state in the runtime snapshot", async () => {
const getPullRequestStatus = vi.fn(async () =>
createPullRequestStatusResult({
status: {
url: "https://github.com/acme/repo/pull/999",
title: "Ship runtime centralization",
state: "open",
baseRefName: "main",
headRefName: "workspace-git-service",
isMerged: false,
},
}),
);
const service = createService({
getPullRequestStatus,
now: () => new Date("2026-04-12T02:03:04.000Z"),
});
await expect(service.getSnapshot(REPO_CWD)).resolves.toEqual(
createSnapshot(REPO_CWD, {
forge: {
pullRequest: {
url: "https://github.com/acme/repo/pull/999",
title: "Ship runtime centralization",
state: "open",
baseRefName: "main",
headRefName: "workspace-git-service",
isMerged: false,
},
},
}),
);
expect(getPullRequestStatus).toHaveBeenCalledTimes(1);
service.dispose();
});
test("getSnapshot does not probe isAuthenticated for a forge adapter that never throws from it", async () => {
const gitlabIsAuthenticated = vi.fn(async () => false);
const gitlabStub: ForgeService = {
...createGitHubServiceStub(),
isAuthenticated: gitlabIsAuthenticated,
authProbeCanThrow: undefined,
};
const getPullRequestStatus = vi.fn(async () => createPullRequestStatusResult());
const service = createService({
getCheckoutStatus: vi.fn(async (cwd: string) =>
createCheckoutStatus(cwd, { remoteUrl: "https://gitlab.com/acme/repo.git" }),
),
getPullRequestStatus,
forgeOverrides: { gitlab: gitlabStub },
});
await service.getSnapshot(REPO_CWD);
expect(gitlabIsAuthenticated).not.toHaveBeenCalled();
expect(getPullRequestStatus).toHaveBeenCalledTimes(1);
service.dispose();
});
test("getSnapshot keeps plain git classification when shortstat lookup fails", async () => {
const getCheckoutShortstat = vi.fn(async () => {
throw new Error(
"Missing Paseo worktree base metadata: /tmp/repo/.git/worktrees/feature/paseo/worktree.json",
);
});
const service = createService({
getCheckoutStatus: vi.fn(async (cwd: string) =>
createCheckoutStatus(cwd, {
repoRoot: cwd,
currentBranch: "feature/worktree",
isPaseoOwnedWorktree: false,
mainRepoRoot: "/tmp/main-repo",
}),
),
getCheckoutShortstat,
});
await expect(service.getSnapshot(REPO_CWD)).resolves.toEqual(
createSnapshot(REPO_CWD, {
git: {
repoRoot: REPO_CWD,
currentBranch: "feature/worktree",
isPaseoOwnedWorktree: false,
mainRepoRoot: "/tmp/main-repo",
diffStat: null,
},
}),
);
});
test("non-forced workspace refresh does not reload GitHub or emit when state is unchanged", async () => {
let nowMs = Date.parse("2026-04-12T00:00:00.000Z");
const getPullRequestStatus = vi.fn(async () => createPullRequestStatusResult());
const service = createService({
getPullRequestStatus,
now: () => new Date(nowMs),
});
const listener = vi.fn();
await service.getSnapshot(REPO_CWD);
const subscription = service.registerWorkspace({ cwd: REPO_CWD }, listener);
nowMs += 3_000;
await service.refresh(REPO_CWD);
expect(getPullRequestStatus).toHaveBeenCalledTimes(1);
expect(listener).not.toHaveBeenCalled();
subscription.unsubscribe();
service.dispose();
});
test("cold getSnapshot calls share one workspace target setup and cache the snapshot", async () => {
const checkoutStatusDeferred = createDeferred<CheckoutStatusGit>();
const getCheckoutStatus = vi.fn(async () => checkoutStatusDeferred.promise);
const getPullRequestStatus = vi.fn(async () => createPullRequestStatusResult());
const resolveAbsoluteGitDir = vi.fn(async () => join(REPO_CWD, ".git"));
const service = createService({
getCheckoutStatus,
getPullRequestStatus,
resolveAbsoluteGitDir,
});
const firstSnapshotPromise = service.getSnapshot(REPO_CWD);
const secondSnapshotPromise = service.getSnapshot(join(REPO_CWD, "."));
await vi.waitFor(() => {
expect(getCheckoutStatus).toHaveBeenCalledTimes(1);
});
expect(getPullRequestStatus).toHaveBeenCalledTimes(0);
expect(resolveAbsoluteGitDir).toHaveBeenCalledTimes(0);
checkoutStatusDeferred.resolve(createCheckoutStatus(REPO_CWD));
await expect(Promise.all([firstSnapshotPromise, secondSnapshotPromise])).resolves.toEqual([
createSnapshot(REPO_CWD),
createSnapshot(REPO_CWD),
]);
expect(getCheckoutStatus).toHaveBeenCalledTimes(1);
expect(getPullRequestStatus).toHaveBeenCalledTimes(1);
expect(resolveAbsoluteGitDir).toHaveBeenCalledTimes(0);
expect(service.peekSnapshot(REPO_CWD)).toEqual(createSnapshot(REPO_CWD));
await expect(service.getSnapshot(REPO_CWD)).resolves.toEqual(createSnapshot(REPO_CWD));
expect(getCheckoutStatus).toHaveBeenCalledTimes(1);
expect(getPullRequestStatus).toHaveBeenCalledTimes(1);
service.dispose();
});
test("multiple listeners on the same workspace share one observation setup", async () => {
const getPullRequestStatus = vi.fn(async () => createPullRequestStatusResult());
const getCheckoutSnapshotFacts = vi.fn(async (cwd: string) => createCheckoutSnapshotFacts(cwd));
const resolveAbsoluteGitDir = vi.fn(async () => join(REPO_CWD, ".git"));
let nowMs = Date.parse("2026-04-12T00:00:00.000Z");
const service = createService({
getCheckoutSnapshotFacts,
getPullRequestStatus,
resolveAbsoluteGitDir,
now: () => new Date(nowMs),
});
const first = service.registerWorkspace({ cwd: REPO_CWD }, vi.fn());
const second = service.registerWorkspace({ cwd: REPO_CWD }, vi.fn());
await flushPromises();
expect(getPullRequestStatus).toHaveBeenCalledTimes(0);
expect(getCheckoutSnapshotFacts).toHaveBeenCalledTimes(1);
expect(resolveAbsoluteGitDir).toHaveBeenCalledTimes(0);
first.unsubscribe();
second.unsubscribe();
service.dispose();
});
test("equivalent cwd strings share one workspace target across service entry points", async () => {
const getPullRequestStatus = vi.fn(async () => createPullRequestStatusResult());
const resolveAbsoluteGitDir = vi.fn(async () => join(REPO_CWD, ".git"));
let nowMs = Date.parse("2026-04-12T00:00:00.000Z");
const service = createService({
getPullRequestStatus,
resolveAbsoluteGitDir,
now: () => new Date(nowMs),
});
const subscription = service.registerWorkspace({ cwd: join(REPO_CWD, ".") }, vi.fn());
await expect(service.getSnapshot(join(REPO_CWD, "."))).resolves.toEqual(
createSnapshot(REPO_CWD),
);
expect(service.peekSnapshot(REPO_CWD)).toEqual(createSnapshot(REPO_CWD));
nowMs += 3_000;
await service.refresh(REPO_CWD);
await expect(service.getSnapshot(join(REPO_CWD, "."))).resolves.toEqual(
createSnapshot(REPO_CWD),
);
expect(getPullRequestStatus).toHaveBeenCalledTimes(1);
expect(resolveAbsoluteGitDir).toHaveBeenCalledTimes(0);
subscription.unsubscribe();
service.dispose();
});
test("repo-level fetch intervals are shared for workspaces in the same repo", async () => {
const firstFetch = createDeferred<void>();
const runGitFetch = vi.fn(() => firstFetch.promise);
const hasOriginRemote = vi.fn(async () => true);
const getCheckoutSnapshotFacts = vi.fn(async (cwd: string) => ({
...createCheckoutSnapshotFacts(cwd),
gitCommonDir: join(REPO_CWD, ".git"),
absoluteGitDir: join(REPO_CWD, ".git"),
}));
const resolveAbsoluteGitDir = vi.fn(async () => join(REPO_CWD, ".git"));
const service = createService({
getCheckoutSnapshotFacts,
resolveAbsoluteGitDir,
hasOriginRemote,
runGitFetch,
});
const first = service.registerWorkspace({ cwd: REPO_CWD }, vi.fn());
const second = service.registerWorkspace(
{ cwd: join(REPO_CWD, "packages", "server") },
vi.fn(),
);
await vi.waitFor(() => {
expect(getCheckoutSnapshotFacts).toHaveBeenCalledTimes(2);
expect(runGitFetch).toHaveBeenCalledTimes(1);
expect(service.getMetrics().workspaceRefreshInFlightCount).toBe(0);
});
expect(resolveAbsoluteGitDir).toHaveBeenCalledTimes(0);
expect(hasOriginRemote).toHaveBeenCalledTimes(0);
expect(runGitFetch).toHaveBeenCalledTimes(1);
firstFetch.resolve();
await flushPromises();
await vi.advanceTimersByTimeAsync(1_000);
await vi.waitFor(() => {
expect(getCheckoutSnapshotFacts).toHaveBeenCalledTimes(4);
});
await vi.advanceTimersByTimeAsync(180_000);
await flushPromises();
expect(runGitFetch).toHaveBeenCalledTimes(2);
first.unsubscribe();
second.unsubscribe();
service.dispose();
});
test("explicit forced snapshot refresh recomputes github state and notifies listeners", async () => {
const getPullRequestStatus = vi
.fn<() => Promise<PullRequestStatusResult>>()
.mockResolvedValueOnce(
createPullRequestStatusResult({
status: {
url: "https://github.com/acme/repo/pull/123",
title: "Before refresh",
state: "open",
baseRefName: "main",
headRefName: "feature",
isMerged: false,
},
}),
)
.mockResolvedValueOnce(
createPullRequestStatusResult({
status: {
url: "https://github.com/acme/repo/pull/123",
title: "After refresh",
state: "merged",
baseRefName: "main",
headRefName: "feature",
isMerged: true,
},
}),
);
const nowValues = [new Date("2026-04-12T00:00:00.000Z"), new Date("2026-04-12T00:05:00.000Z")];
const service = createService({
getPullRequestStatus,
now: () => nowValues.shift() ?? new Date("2026-04-12T00:05:00.000Z"),
});
const listener = vi.fn();
const initialSnapshot = await service.getSnapshot(REPO_CWD);
const subscription = service.registerWorkspace({ cwd: REPO_CWD }, listener);
expect(initialSnapshot.forge.pullRequest?.title).toBe("Before refresh");
await service.getSnapshot(REPO_CWD, {
force: true,
reason: "test-force-github-refresh",
});
await flushPromises();
expect(getPullRequestStatus).toHaveBeenCalledTimes(2);
expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenCalledWith(
createSnapshot(REPO_CWD, {
forge: {
pullRequest: {
url: "https://github.com/acme/repo/pull/123",
title: "After refresh",
state: "merged",
baseRefName: "main",
headRefName: "feature",
isMerged: true,
},
},
}),
);
subscription.unsubscribe();
service.dispose();
});
test("unchanged runtime snapshots do not emit duplicate updates", async () => {
const getCheckoutStatus = vi
.fn<() => Promise<CheckoutStatusGit>>()
.mockResolvedValueOnce(createCheckoutStatus(REPO_CWD, { remoteUrl: null }))
.mockResolvedValueOnce(
createCheckoutStatus(REPO_CWD, {
currentBranch: "feature/runtime-payloads",
remoteUrl: null,
aheadBehind: { ahead: 2, behind: 0 },
aheadOfOrigin: 2,
}),
)
.mockResolvedValueOnce(
createCheckoutStatus(REPO_CWD, {
currentBranch: "feature/runtime-payloads",
remoteUrl: null,
aheadBehind: { ahead: 2, behind: 0 },
aheadOfOrigin: 2,
}),
);
const getPullRequestStatus = vi.fn<() => Promise<PullRequestStatusResult>>().mockResolvedValue(
createPullRequestStatusResult({
status: {
url: "https://github.com/acme/repo/pull/123",
title: "Runtime payloads",
state: "open",
baseRefName: "main",
headRefName: "feature/runtime-payloads",
isMerged: false,
},
}),
);
let nowMs = Date.parse("2026-04-12T00:00:00.000Z");
const service = createService({
getCheckoutStatus,
getPullRequestStatus,
now: () => new Date(nowMs),
});
const listener = vi.fn();
const initialSnapshot = await service.getSnapshot(REPO_CWD);
const subscription = service.registerWorkspace({ cwd: REPO_CWD }, listener);
expect(initialSnapshot.git.currentBranch).toBe("main");
nowMs += 3_000;
await service.refresh(REPO_CWD);
await flushPromises();
nowMs += 3_000;
await service.refresh(REPO_CWD);
await flushPromises();
expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenCalledWith(
createSnapshot(REPO_CWD, {
git: {
currentBranch: "feature/runtime-payloads",
remoteUrl: null,
aheadBehind: { ahead: 2, behind: 0 },
aheadOfOrigin: 2,
},
forge: {
featuresEnabled: false,
pullRequest: null,
},
}),
);
subscription.unsubscribe();
service.dispose();
});
test("forced snapshot refresh emits even when the fingerprint matches", async () => {
const getCheckoutStatus = vi.fn(async () => createCheckoutStatus(REPO_CWD));
const getPullRequestStatus = vi.fn(async () => createPullRequestStatusResult());
let nowMs = Date.parse("2026-04-12T00:00:00.000Z");
const service = createService({
getCheckoutStatus,
getPullRequestStatus,
now: () => new Date(nowMs),
});
const listener = vi.fn();
await service.getSnapshot(REPO_CWD);
const subscription = service.registerWorkspace({ cwd: REPO_CWD }, listener);
await service.getSnapshot(REPO_CWD, {
force: true,
reason: "test-force-emit",
});
await flushPromises();
expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenCalledWith(createSnapshot(REPO_CWD));
subscription.unsubscribe();
service.dispose();
});
test("watches nested repository changes through one recursive checkout subscription", async () => {
const unsubscribe = vi.fn(async () => {});
const subscribe = vi.fn(async () => ({ unsubscribe }));
const service = createService({ subscribe });
const subscription = await service.requestWorkingTreeWatch(
path.join(REPO_CWD, "packages", "server"),
vi.fn(),
);
expect(subscription.repoRoot).toBe(REPO_CWD);
expect(subscribe).toHaveBeenCalledTimes(1);
expect(subscribe).toHaveBeenCalledWith(REPO_CWD, expect.any(Function), {
ignore: [join(REPO_CWD, ".git")],
});
subscription.unsubscribe();
expect(unsubscribe).toHaveBeenCalledTimes(1);
service.dispose();
});
test("requestWorkingTreeWatch reference-counts watchers by cwd", async () => {
const unsubscribe = vi.fn(async () => {});
const subscribe = vi.fn(async () => ({ unsubscribe }));
const service = createService({ subscribe });
const firstListener = vi.fn();
const secondListener = vi.fn();
const first = await service.requestWorkingTreeWatch(REPO_CWD, firstListener);
const second = await service.requestWorkingTreeWatch(join(REPO_CWD, "."), secondListener);
expect(first.repoRoot).toBe(REPO_CWD);
expect(second.repoRoot).toBe(REPO_CWD);
expect(subscribe).toHaveBeenCalledTimes(1);
first.unsubscribe();
expect(unsubscribe).not.toHaveBeenCalled();
second.unsubscribe();
expect(unsubscribe).toHaveBeenCalledTimes(1);
service.dispose();
});
test("sets a bounded polling fallback when recursive observation is unavailable", async () => {
const subscribe = vi.fn(async () => {
throw new Error("watcher unavailable");
});
const service = createService({ subscribe });
const subscription = await service.requestWorkingTreeWatch(REPO_CWD, vi.fn());
expect(vi.getTimerCount()).toBe(1);
subscription.unsubscribe();
service.dispose();
});
test("non-git directories fall back to watching cwd with polling", async () => {
const subscribe = vi.fn(async () => createAsyncSubscription());
const runGitCommand = vi.fn(async () => {
throw new Error("not a git repository");
});
const resolveAbsoluteGitDir = vi.fn(async () => null);
const service = createService({
subscribe,
runGitCommand,
resolveAbsoluteGitDir,
});
const plainCwd = path.join(os.tmpdir(), "plain");
const subscription = await service.requestWorkingTreeWatch(plainCwd, vi.fn());
expect(subscription.repoRoot).toBeNull();
expect(subscribe).toHaveBeenCalledWith(plainCwd, expect.any(Function), {
ignore: [join(plainCwd, ".git")],
});
expect(vi.getTimerCount()).toBe(1);
subscription.unsubscribe();
service.dispose();
});
test("working tree changes notify watch listeners immediately", async () => {
const watchCallbacks: Array<
(error: Error | null, events: Array<{ path: string; type: "update" }>) => void
> = [];
const subscribe = vi.fn(async (_path: string, callback: (typeof watchCallbacks)[number]) => {
watchCallbacks.push(callback);
return createAsyncSubscription();
});
const service = createService({ subscribe });
const listener = vi.fn();
const subscription = await service.requestWorkingTreeWatch(REPO_CWD, listener);
expect(watchCallbacks).toHaveLength(1);
watchCallbacks[0]?.(null, [{ path: join(REPO_CWD, "file.ts"), type: "update" }]);
expect(listener).toHaveBeenCalledTimes(1);
subscription.unsubscribe();
service.dispose();
});
test("working tree changes force a fresh diff stat for workspace subscribers", async () => {
const watchCallbacks: Array<{
path: string;
callback: (error: Error | null, events: Array<{ path: string; type: "update" }>) => void;
}> = [];
const subscribe = vi.fn(
async (watchPath: string, callback: (typeof watchCallbacks)[number]["callback"]) => {
watchCallbacks.push({ path: watchPath, callback });
return createAsyncSubscription();
},
);
const getCheckoutWorktreeState = vi.fn(async () => ({
isDirty: true,
diffStat: { additions: 8, deletions: 3 },
}));
const backgroundFetch = createDeferred<void>();
const service = createService({
getCheckoutWorktreeState,
runGitFetch: vi.fn(() => backgroundFetch.promise),
subscribe,
});
const workspaceListener = vi.fn();
const initialSnapshot = await service.getSnapshot(REPO_CWD);
const workspaceSubscription = service.registerWorkspace({ cwd: REPO_CWD }, workspaceListener);
const diffSubscription = await service.requestWorkingTreeWatch(REPO_CWD, vi.fn());
await vi.waitFor(() => {
expect(service.getMetrics().repositoryWorkspaceLinkCount).toBe(1);
});
expect(initialSnapshot.git.diffStat).toEqual({ additions: 1, deletions: 0 });
const repoRootWatch = watchCallbacks.find((entry) => entry.path === REPO_CWD);
expect(repoRootWatch).toBeDefined();
repoRootWatch?.callback(null, [{ path: join(REPO_CWD, "file.ts"), type: "update" }]);
await vi.advanceTimersByTimeAsync(1_000);
await flushPromises();
expect(getCheckoutWorktreeState).toHaveBeenCalledWith(
REPO_CWD,
expect.objectContaining({ paseoHome: "/tmp/paseo-test" }),
);
expect(workspaceListener).toHaveBeenCalledWith(
createSnapshot(REPO_CWD, {
git: { isDirty: true, diffStat: { additions: 8, deletions: 3 } },
}),
);
diffSubscription.unsubscribe();
workspaceSubscription.unsubscribe();
service.dispose();
});
test("checkoutDiffCache evicts least-recently-used entries past its size cap", async () => {
vi.useRealTimers();
const getCheckoutDiff = vi.fn(async (cwd: string) => ({
diff: `diff for ${cwd}`,
}));
const service = createService({
getCheckoutDiff: getCheckoutDiff as unknown as ReturnType<typeof vi.fn>,
});
const CACHE_MAX = 64;
const OVERFLOW = 5;
for (let i = 0; i < CACHE_MAX + OVERFLOW; i++) {
await service.getCheckoutDiff(`/tmp/repo-${i}`, { mode: "uncommitted" });
}
expect(getCheckoutDiff).toHaveBeenCalledTimes(CACHE_MAX + OVERFLOW);
await service.getCheckoutDiff(`/tmp/repo-${CACHE_MAX - 1}`, { mode: "uncommitted" });
expect(getCheckoutDiff).toHaveBeenCalledTimes(CACHE_MAX + OVERFLOW);
await service.getCheckoutDiff("/tmp/repo-0", { mode: "uncommitted" });
expect(getCheckoutDiff).toHaveBeenCalledTimes(CACHE_MAX + OVERFLOW + 1);
service.dispose();
});
});