import { EnvironmentId, type VcsStatusResult } from "@t3tools/contracts";import { afterEach, describe, expect, it, vi } from "vitest"; import type { WsRpcClient } from "../rpc/wsRpcClient";import { resetAppAtomRegistryForTests } from "../rpc/atomRegistry";import { getGitStatusSnapshot, resetGitStatusStateForTests, refreshGitStatus, watchGitStatus,} from "./gitStatusState"; const serviceHarness = vi.hoisted(() => ({ connections: new Map<string, any>(), listeners: new Set<() => void>(),})); vi.mock("../environments/runtime/service", () => ({ readEnvironmentConnection: (environmentId: string) => serviceHarness.connections.get(environmentId) ?? null, subscribeEnvironmentConnections: (listener: () => void) => { serviceHarness.listeners.add(listener); return () => { serviceHarness.listeners.delete(listener); }; },})); function registerListener<T>(listeners: Set<(event: T) => void>, listener: (event: T) => void) { listeners.add(listener); return () => { listeners.delete(listener); };} const gitStatusListeners = new Set<(event: VcsStatusResult) => void>();const ENVIRONMENT_ID = EnvironmentId.make("environment-local");const OTHER_ENVIRONMENT_ID = EnvironmentId.make("environment-remote");const TARGET = { environmentId: ENVIRONMENT_ID, cwd: "/repo" } as const;const FRESH_TARGET = { environmentId: ENVIRONMENT_ID, cwd: "/fresh" } as const; const BASE_STATUS: VcsStatusResult = { isRepo: true, hasPrimaryRemote: true, isDefaultRef: false, refName: "feature/push-status", hasWorkingTreeChanges: false, workingTree: { files: [], insertions: 0, deletions: 0 }, hasUpstream: true, aheadCount: 0, behindCount: 0, pr: null,}; const gitClient = { refreshStatus: vi.fn(async (input: { cwd: string }) => ({ ...BASE_STATUS, refName: `${input.cwd}-refreshed`, })), onStatus: vi.fn((input: { cwd: string }, listener: (event: VcsStatusResult) => void) => registerListener(gitStatusListeners, listener), ),}; function emitGitStatus(event: VcsStatusResult) { for (const listener of gitStatusListeners) { listener(event); }} function createRegisteredGitStatusClient(environmentId: EnvironmentId) { const listeners = new Set<(event: VcsStatusResult) => void>(); const client = { dispose: vi.fn(async () => undefined), reconnect: vi.fn(async () => undefined), terminal: { open: vi.fn(async () => undefined), write: vi.fn(async () => undefined), resize: vi.fn(async () => undefined), clear: vi.fn(async () => undefined), restart: vi.fn(async () => undefined), close: vi.fn(async () => undefined), onEvent: vi.fn(() => () => undefined), }, projects: { searchEntries: vi.fn(async () => []), writeFile: vi.fn(async () => undefined), }, shell: { openInEditor: vi.fn(async () => undefined), }, vcs: { pull: vi.fn(async () => undefined), refreshStatus: vi.fn(async (input: { cwd: string }) => ({ ...BASE_STATUS, refName: `${input.cwd}-refreshed`, })), onStatus: vi.fn((_: { cwd: string }, listener: (event: VcsStatusResult) => void) => registerListener(listeners, listener), ), listRefs: vi.fn(async () => []), createWorktree: vi.fn(async () => undefined), removeWorktree: vi.fn(async () => undefined), createRef: vi.fn(async () => undefined), switchRef: vi.fn(async () => undefined), init: vi.fn(async () => undefined), }, git: { runStackedAction: vi.fn(async () => ({}) as any), resolvePullRequest: vi.fn(async () => undefined), preparePullRequestThread: vi.fn(async () => undefined), }, server: { getConfig: vi.fn(async () => ({ environment: { environmentId, }, })), refreshProviders: vi.fn(async () => undefined), upsertKeybinding: vi.fn(async () => undefined), getSettings: vi.fn(async () => undefined), updateSettings: vi.fn(async () => undefined), subscribeConfig: vi.fn(() => () => undefined), subscribeLifecycle: vi.fn(() => () => undefined), subscribeAuthAccess: vi.fn(() => () => undefined), }, orchestration: { dispatchCommand: vi.fn(async () => undefined), getTurnDiff: vi.fn(async () => undefined), getFullThreadDiff: vi.fn(async () => undefined), subscribeShell: vi.fn(() => () => undefined), subscribeThread: vi.fn(() => () => undefined), }, } as unknown as WsRpcClient; serviceHarness.connections.set(environmentId, { kind: "saved" as const, knownEnvironment: { id: environmentId, label: `Environment ${environmentId}`, source: "manual" as const, environmentId, target: { httpBaseUrl: "http://example.test", wsBaseUrl: "ws://example.test", }, }, client, environmentId, ensureBootstrapped: async () => undefined, reconnect: async () => undefined, dispose: async () => undefined, }); for (const listener of serviceHarness.listeners) { listener(); } return { client, emit: (event: VcsStatusResult) => { for (const listener of listeners) { listener(event); } }, };} afterEach(async () => { gitStatusListeners.clear(); serviceHarness.connections.clear(); serviceHarness.listeners.clear(); gitClient.onStatus.mockClear(); gitClient.refreshStatus.mockClear(); resetGitStatusStateForTests(); resetAppAtomRegistryForTests();}); describe("gitStatusState", () => { it("starts fresh cwd state in a pending state", () => { expect(getGitStatusSnapshot(FRESH_TARGET)).toEqual({ data: null, error: null, cause: null, isPending: true, }); }); it("shares one live subscription per cwd and updates the per-cwd atom snapshot", () => { const releaseA = watchGitStatus(TARGET, gitClient); const releaseB = watchGitStatus(TARGET, gitClient); expect(gitClient.onStatus).toHaveBeenCalledOnce(); expect(getGitStatusSnapshot(TARGET)).toEqual({ data: null, error: null, cause: null, isPending: true, }); emitGitStatus(BASE_STATUS); expect(getGitStatusSnapshot(TARGET)).toEqual({ data: BASE_STATUS, error: null, cause: null, isPending: false, }); releaseA(); expect(gitStatusListeners.size).toBe(1); releaseB(); expect(gitStatusListeners.size).toBe(0); }); it("refreshes git status through the unary RPC without restarting the stream", async () => { const release = watchGitStatus(TARGET, gitClient); emitGitStatus(BASE_STATUS); const refreshed = await refreshGitStatus(TARGET, gitClient); expect(gitClient.onStatus).toHaveBeenCalledOnce(); expect(gitClient.refreshStatus).toHaveBeenCalledWith({ cwd: "/repo" }); expect(refreshed).toEqual({ ...BASE_STATUS, refName: "/repo-refreshed" }); expect(getGitStatusSnapshot(TARGET)).toEqual({ data: BASE_STATUS, error: null, cause: null, isPending: false, }); release(); }); it("keeps git status subscriptions isolated by environment when cwds match", () => { const localListeners = new Set<(event: VcsStatusResult) => void>(); const remoteListeners = new Set<(event: VcsStatusResult) => void>(); const localClient = { refreshStatus: vi.fn(), onStatus: vi.fn((_: { cwd: string }, listener: (event: VcsStatusResult) => void) => registerListener(localListeners, listener), ), }; const remoteClient = { refreshStatus: vi.fn(), onStatus: vi.fn((_: { cwd: string }, listener: (event: VcsStatusResult) => void) => registerListener(remoteListeners, listener), ), }; const remoteTarget = { environmentId: OTHER_ENVIRONMENT_ID, cwd: "/repo" } as const; const releaseLocal = watchGitStatus(TARGET, localClient); const releaseRemote = watchGitStatus(remoteTarget, remoteClient); for (const listener of localListeners) { listener(BASE_STATUS); } for (const listener of remoteListeners) { listener({ ...BASE_STATUS, refName: "remote-refName" }); } expect(getGitStatusSnapshot(TARGET).data?.refName).toBe("feature/push-status"); expect(getGitStatusSnapshot(remoteTarget).data?.refName).toBe("remote-refName"); releaseLocal(); releaseRemote(); }); it("waits for a delayed environment client registration instead of throwing", () => { const release = watchGitStatus(TARGET); expect(getGitStatusSnapshot(TARGET)).toEqual({ data: null, error: null, cause: null, isPending: true, }); const registered = createRegisteredGitStatusClient(ENVIRONMENT_ID); registered.emit(BASE_STATUS); expect(getGitStatusSnapshot(TARGET)).toEqual({ data: BASE_STATUS, error: null, cause: null, isPending: false, }); release(); }); it("resubscribes after the environment client is removed and re-registered", async () => { const firstClient = createRegisteredGitStatusClient(ENVIRONMENT_ID); const release = watchGitStatus(TARGET); firstClient.emit(BASE_STATUS); expect(getGitStatusSnapshot(TARGET).data?.refName).toBe("feature/push-status"); serviceHarness.connections.delete(ENVIRONMENT_ID); for (const listener of serviceHarness.listeners) { listener(); } expect(getGitStatusSnapshot(TARGET)).toEqual({ data: BASE_STATUS, error: null, cause: null, isPending: true, }); const secondClient = createRegisteredGitStatusClient(ENVIRONMENT_ID); secondClient.emit({ ...BASE_STATUS, refName: "reconnected-refName" }); expect(getGitStatusSnapshot(TARGET)).toEqual({ data: { ...BASE_STATUS, refName: "reconnected-refName" }, error: null, cause: null, isPending: false, }); release(); }); it("returns the cached snapshot when refresh is requested before the client is registered", async () => { await expect(refreshGitStatus(TARGET)).resolves.toBeNull(); });});
import { EnvironmentId, type VcsStatusResult } from "@t3tools/contracts";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { WsRpcClient } from "../rpc/wsRpcClient";
import { resetAppAtomRegistryForTests } from "../rpc/atomRegistry";
import {
getGitStatusSnapshot,
resetGitStatusStateForTests,
refreshGitStatus,
watchGitStatus,
} from "./gitStatusState";
const serviceHarness = vi.hoisted(() => ({
connections: new Map<string, any>(),
listeners: new Set<() => void>(),
}));
vi.mock("../environments/runtime/service", () => ({
readEnvironmentConnection: (environmentId: string) =>
serviceHarness.connections.get(environmentId) ?? null,
subscribeEnvironmentConnections: (listener: () => void) => {
serviceHarness.listeners.add(listener);
return () => {
serviceHarness.listeners.delete(listener);
};
},
}));
function registerListener<T>(listeners: Set<(event: T) => void>, listener: (event: T) => void) {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
const gitStatusListeners = new Set<(event: VcsStatusResult) => void>();
const ENVIRONMENT_ID = EnvironmentId.make("environment-local");
const OTHER_ENVIRONMENT_ID = EnvironmentId.make("environment-remote");
const TARGET = { environmentId: ENVIRONMENT_ID, cwd: "/repo" } as const;
const FRESH_TARGET = { environmentId: ENVIRONMENT_ID, cwd: "/fresh" } as const;
const BASE_STATUS: VcsStatusResult = {
isRepo: true,
hasPrimaryRemote: true,
isDefaultRef: false,
refName: "feature/push-status",
hasWorkingTreeChanges: false,
workingTree: { files: [], insertions: 0, deletions: 0 },
hasUpstream: true,
aheadCount: 0,
behindCount: 0,
pr: null,
};
const gitClient = {
refreshStatus: vi.fn(async (input: { cwd: string }) => ({
...BASE_STATUS,
refName: `${input.cwd}-refreshed`,
})),
onStatus: vi.fn((input: { cwd: string }, listener: (event: VcsStatusResult) => void) =>
registerListener(gitStatusListeners, listener),
),
};
function emitGitStatus(event: VcsStatusResult) {
for (const listener of gitStatusListeners) {
listener(event);
}
}
function createRegisteredGitStatusClient(environmentId: EnvironmentId) {
const listeners = new Set<(event: VcsStatusResult) => void>();
const client = {
dispose: vi.fn(async () => undefined),
reconnect: vi.fn(async () => undefined),
terminal: {
open: vi.fn(async () => undefined),
write: vi.fn(async () => undefined),
resize: vi.fn(async () => undefined),
clear: vi.fn(async () => undefined),
restart: vi.fn(async () => undefined),
close: vi.fn(async () => undefined),
onEvent: vi.fn(() => () => undefined),
},
projects: {
searchEntries: vi.fn(async () => []),
writeFile: vi.fn(async () => undefined),
},
shell: {
openInEditor: vi.fn(async () => undefined),
},
vcs: {
pull: vi.fn(async () => undefined),
refreshStatus: vi.fn(async (input: { cwd: string }) => ({
...BASE_STATUS,
refName: `${input.cwd}-refreshed`,
})),
onStatus: vi.fn((_: { cwd: string }, listener: (event: VcsStatusResult) => void) =>
registerListener(listeners, listener),
),
listRefs: vi.fn(async () => []),
createWorktree: vi.fn(async () => undefined),
removeWorktree: vi.fn(async () => undefined),
createRef: vi.fn(async () => undefined),
switchRef: vi.fn(async () => undefined),
init: vi.fn(async () => undefined),
},
git: {
runStackedAction: vi.fn(async () => ({}) as any),
resolvePullRequest: vi.fn(async () => undefined),
preparePullRequestThread: vi.fn(async () => undefined),
},
server: {
getConfig: vi.fn(async () => ({
environment: {
environmentId,
},
})),
refreshProviders: vi.fn(async () => undefined),
upsertKeybinding: vi.fn(async () => undefined),
getSettings: vi.fn(async () => undefined),
updateSettings: vi.fn(async () => undefined),
subscribeConfig: vi.fn(() => () => undefined),
subscribeLifecycle: vi.fn(() => () => undefined),
subscribeAuthAccess: vi.fn(() => () => undefined),
},
orchestration: {
dispatchCommand: vi.fn(async () => undefined),
getTurnDiff: vi.fn(async () => undefined),
getFullThreadDiff: vi.fn(async () => undefined),
subscribeShell: vi.fn(() => () => undefined),
subscribeThread: vi.fn(() => () => undefined),
},
} as unknown as WsRpcClient;
serviceHarness.connections.set(environmentId, {
kind: "saved" as const,
knownEnvironment: {
id: environmentId,
label: `Environment ${environmentId}`,
source: "manual" as const,
environmentId,
target: {
httpBaseUrl: "http://example.test",
wsBaseUrl: "ws://example.test",
},
},
client,
environmentId,
ensureBootstrapped: async () => undefined,
reconnect: async () => undefined,
dispose: async () => undefined,
});
for (const listener of serviceHarness.listeners) {
listener();
}
return {
client,
emit: (event: VcsStatusResult) => {
for (const listener of listeners) {
listener(event);
}
},
};
}
afterEach(async () => {
gitStatusListeners.clear();
serviceHarness.connections.clear();
serviceHarness.listeners.clear();
gitClient.onStatus.mockClear();
gitClient.refreshStatus.mockClear();
resetGitStatusStateForTests();
resetAppAtomRegistryForTests();
});
describe("gitStatusState", () => {
it("starts fresh cwd state in a pending state", () => {
expect(getGitStatusSnapshot(FRESH_TARGET)).toEqual({
data: null,
error: null,
cause: null,
isPending: true,
});
});
it("shares one live subscription per cwd and updates the per-cwd atom snapshot", () => {
const releaseA = watchGitStatus(TARGET, gitClient);
const releaseB = watchGitStatus(TARGET, gitClient);
expect(gitClient.onStatus).toHaveBeenCalledOnce();
expect(getGitStatusSnapshot(TARGET)).toEqual({
data: null,
error: null,
cause: null,
isPending: true,
});
emitGitStatus(BASE_STATUS);
expect(getGitStatusSnapshot(TARGET)).toEqual({
data: BASE_STATUS,
error: null,
cause: null,
isPending: false,
});
releaseA();
expect(gitStatusListeners.size).toBe(1);
releaseB();
expect(gitStatusListeners.size).toBe(0);
});
it("refreshes git status through the unary RPC without restarting the stream", async () => {
const release = watchGitStatus(TARGET, gitClient);
emitGitStatus(BASE_STATUS);
const refreshed = await refreshGitStatus(TARGET, gitClient);
expect(gitClient.onStatus).toHaveBeenCalledOnce();
expect(gitClient.refreshStatus).toHaveBeenCalledWith({ cwd: "/repo" });
expect(refreshed).toEqual({ ...BASE_STATUS, refName: "/repo-refreshed" });
expect(getGitStatusSnapshot(TARGET)).toEqual({
data: BASE_STATUS,
error: null,
cause: null,
isPending: false,
});
release();
});
it("keeps git status subscriptions isolated by environment when cwds match", () => {
const localListeners = new Set<(event: VcsStatusResult) => void>();
const remoteListeners = new Set<(event: VcsStatusResult) => void>();
const localClient = {
refreshStatus: vi.fn(),
onStatus: vi.fn((_: { cwd: string }, listener: (event: VcsStatusResult) => void) =>
registerListener(localListeners, listener),
),
};
const remoteClient = {
refreshStatus: vi.fn(),
onStatus: vi.fn((_: { cwd: string }, listener: (event: VcsStatusResult) => void) =>
registerListener(remoteListeners, listener),
),
};
const remoteTarget = { environmentId: OTHER_ENVIRONMENT_ID, cwd: "/repo" } as const;
const releaseLocal = watchGitStatus(TARGET, localClient);
const releaseRemote = watchGitStatus(remoteTarget, remoteClient);
for (const listener of localListeners) {
listener(BASE_STATUS);
}
for (const listener of remoteListeners) {
listener({ ...BASE_STATUS, refName: "remote-refName" });
}
expect(getGitStatusSnapshot(TARGET).data?.refName).toBe("feature/push-status");
expect(getGitStatusSnapshot(remoteTarget).data?.refName).toBe("remote-refName");
releaseLocal();
releaseRemote();
});
it("waits for a delayed environment client registration instead of throwing", () => {
const release = watchGitStatus(TARGET);
expect(getGitStatusSnapshot(TARGET)).toEqual({
data: null,
error: null,
cause: null,
isPending: true,
});
const registered = createRegisteredGitStatusClient(ENVIRONMENT_ID);
registered.emit(BASE_STATUS);
expect(getGitStatusSnapshot(TARGET)).toEqual({
data: BASE_STATUS,
error: null,
cause: null,
isPending: false,
});
release();
});
it("resubscribes after the environment client is removed and re-registered", async () => {
const firstClient = createRegisteredGitStatusClient(ENVIRONMENT_ID);
const release = watchGitStatus(TARGET);
firstClient.emit(BASE_STATUS);
expect(getGitStatusSnapshot(TARGET).data?.refName).toBe("feature/push-status");
serviceHarness.connections.delete(ENVIRONMENT_ID);
for (const listener of serviceHarness.listeners) {
listener();
}
expect(getGitStatusSnapshot(TARGET)).toEqual({
data: BASE_STATUS,
error: null,
cause: null,
isPending: true,
});
const secondClient = createRegisteredGitStatusClient(ENVIRONMENT_ID);
secondClient.emit({ ...BASE_STATUS, refName: "reconnected-refName" });
expect(getGitStatusSnapshot(TARGET)).toEqual({
data: { ...BASE_STATUS, refName: "reconnected-refName" },
error: null,
cause: null,
isPending: false,
});
release();
});
it("returns the cached snapshot when refresh is requested before the client is registered", async () => {
await expect(refreshGitStatus(TARGET)).resolves.toBeNull();
});
});