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(), 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(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(); }); });