Files
kilocode/packages/kilo-vscode/tests/unit/git-stats-poller.test.ts
T
2026-08-06 08:31:07 +02:00

788 lines
27 KiB
TypeScript

import { describe, it, expect } from "bun:test"
import * as fs from "fs"
import * as os from "os"
import * as path from "path"
import { GitStatsPoller, type WorktreePresenceResult } from "../../src/agent-manager/GitStatsPoller"
import { GitOps, type ExecBufferResult } from "../../src/agent-manager/GitOps"
import type { GitStatsSource } from "../../src/agent-manager/git-stats-snapshot"
import { Semaphore } from "../../src/agent-manager/semaphore"
import type { Worktree } from "../../src/agent-manager/WorktreeStateManager"
import type { WorktreeDiffEntry } from "../../src/agent-manager/types"
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
async function waitFor(check: () => boolean, timeout = 500): Promise<void> {
const start = Date.now()
while (!check()) {
if (Date.now() - start > timeout) throw new Error("timed out waiting for condition")
await sleep(5)
}
}
function worktree(id: string, remote = "origin"): Worktree {
return {
id,
branch: `branch-${id}`,
path: `/tmp/${id}`,
parentBranch: "main",
remote,
createdAt: "2026-01-01T00:00:00.000Z",
}
}
function diff(additions: number, deletions: number): WorktreeDiffEntry[] {
return [
{
file: "file.ts",
patch: "",
before: "",
after: "",
additions,
deletions,
status: "modified",
tracked: true,
generatedLike: false,
summarized: true,
stamp: `${additions}:${deletions}`,
},
]
}
function gitOps(handler: (args: string[], cwd: string) => Promise<string>): GitOps {
return new GitOps({ log: () => undefined, runGit: handler })
}
function source(
localDiff: (dir: string, base: string) => Promise<WorktreeDiffEntry[]>,
branch = "HEAD",
): GitStatsSource {
let sequence = 0
return {
status: async () => {
const fingerprint = String(++sequence)
return { branch, dirty: true, head: fingerprint, fingerprint, untracked: [] }
},
refs: async () => ({ oids: new Map(), upstreams: new Map() }),
diff: async (dir, base) => {
const entries = await localDiff(dir, base)
return {
files: entries.length,
additions: entries.reduce((sum, item) => sum + item.additions, 0),
deletions: entries.reduce((sum, item) => sum + item.deletions, 0),
}
},
}
}
class RecordingGitOps extends GitOps {
readonly commands: Array<{ args: string[]; cwd: string }> = []
aheadCalls = 0
constructor() {
super({ log: () => undefined })
}
override execGitBuffer(args: string[], cwd: string): Promise<ExecBufferResult> {
this.commands.push({ args, cwd })
return super.execGitBuffer(args, cwd)
}
override aheadBehind(cwd: string, base: string): Promise<{ ahead: number; behind: number }> {
this.aheadCalls++
return super.aheadBehind(cwd, base)
}
}
describe("GitOps", () => {
it("resolveDefaultBranch returns undefined on cache hit when there is no remote HEAD", async () => {
let calls = 0
const git = new GitOps({
log: () => undefined,
runGit: async (args) => {
calls++
if (args[0] === "symbolic-ref") throw new Error("no remote HEAD")
if (args[0] === "branch" && args[1] === "--show-current") return "main"
if (args[0] === "config" && args[1] === "branch.main.remote") return "origin"
if (args[0] === "rev-parse" && args[3] === "@{upstream}") throw new Error("no upstream")
return ""
},
})
// First call sets cache
const first = await git.resolveDefaultBranch("/test")
expect(first).toBeUndefined()
expect(calls).toBeGreaterThan(0)
// Second call reads from cache
const beforeSecondCall = calls
const second = await git.resolveDefaultBranch("/test")
expect(second).toBeUndefined()
// Should not have made any new git calls for the exact same resolution
expect(calls).toBe(beforeSecondCall)
})
})
describe("GitStatsPoller", () => {
it("uses only status and shared snapshots on an unchanged second poll", async () => {
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "gsp-optimized-"))
try {
const run = (args: string[]) => {
const result = Bun.spawnSync({
cmd: ["git", ...args],
cwd: root,
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
GIT_AUTHOR_NAME: "Test",
GIT_AUTHOR_EMAIL: "test@example.com",
GIT_COMMITTER_NAME: "Test",
GIT_COMMITTER_EMAIL: "test@example.com",
},
})
if (result.exitCode !== 0) throw new Error(Buffer.from(result.stderr).toString("utf8"))
}
run(["init", "-b", "main"])
await fs.promises.writeFile(path.join(root, "file.txt"), "one\n")
run(["add", "."])
run(["commit", "-m", "base"])
run(["remote", "add", "origin", "."])
run(["update-ref", "refs/remotes/origin/main", "HEAD"])
run(["branch", "--set-upstream-to=origin/main", "main"])
const git = new RecordingGitOps()
const poller = new GitStatsPoller({
getWorktrees: () => [],
getWorkspaceRoot: () => root,
onStats: () => undefined,
onLocalStats: () => undefined,
log: () => undefined,
intervalMs: 10,
git,
})
poller.setEnabled(true)
await waitFor(() => git.commands.filter((item) => item.args.includes("--porcelain=v2")).length >= 2, 2_000)
const statuses = git.commands
.map((item, index) => ({ ...item, index }))
.filter((item) => item.args.includes("--porcelain=v2"))
const second = git.commands.slice(statuses[1]!.index - 1)
expect(second.filter((item) => item.args.includes("diff"))).toHaveLength(0)
expect(git.aheadCalls).toBe(1)
const diffs = git.commands.filter((item) => item.args.includes("diff")).length
await fs.promises.writeFile(path.join(root, "file.txt"), "changed and larger\n")
await waitFor(() => git.commands.filter((item) => item.args.includes("diff")).length > diffs, 2_000)
const ahead = git.aheadCalls
poller.stop()
await poller.snapshot(true)
expect(git.aheadCalls).toBe(ahead + 1)
} finally {
await fs.promises.rm(root, { recursive: true, force: true })
}
})
it("keeps hot worktrees on every tick and rotates clean dormant worktrees", async () => {
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "gsp-shard-"))
const dirs = ["a", "b", "c"].map((id) => path.join(root, id))
try {
const run = (cwd: string, args: string[]) => {
const result = Bun.spawnSync({
cmd: ["git", ...args],
cwd,
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
GIT_AUTHOR_NAME: "Test",
GIT_AUTHOR_EMAIL: "test@example.com",
GIT_COMMITTER_NAME: "Test",
GIT_COMMITTER_EMAIL: "test@example.com",
},
})
if (result.exitCode !== 0) throw new Error(Buffer.from(result.stderr).toString("utf8"))
}
await fs.promises.mkdir(dirs[0]!)
run(dirs[0]!, ["init", "-b", "main"])
await fs.promises.writeFile(path.join(dirs[0]!, "file.txt"), "one\n")
run(dirs[0]!, ["add", "."])
run(dirs[0]!, ["commit", "-m", "base"])
run(dirs[0]!, ["remote", "add", "origin", "."])
run(dirs[0]!, ["update-ref", "refs/remotes/origin/main", "HEAD"])
run(dirs[0]!, ["branch", "--set-upstream-to=origin/main", "main"])
run(dirs[0]!, ["worktree", "add", "-b", "branch-b", dirs[1]!, "main"])
run(dirs[0]!, ["worktree", "add", "-b", "branch-c", dirs[2]!, "main"])
const git = new RecordingGitOps()
const hot = new Set(["a"])
const poller = new GitStatsPoller({
getWorktrees: () =>
dirs.map((dir, index) => ({
...worktree(String.fromCharCode(97 + index)),
path: dir,
branch: index === 0 ? "main" : `branch-${String.fromCharCode(97 + index)}`,
})),
getWorkspaceRoot: () => dirs[0],
getHotWorktreeIds: () => hot,
onStats: () => undefined,
onLocalStats: () => undefined,
log: () => undefined,
intervalMs: 10,
dormantIntervalMs: 30,
git,
})
poller.setEnabled(true)
await waitFor(() => git.commands.filter((item) => item.args.includes("--porcelain=v2")).length >= 15, 3_000)
poller.stop()
const counts = new Map<string, number>()
for (const item of git.commands) {
if (!item.args.includes("--porcelain=v2")) continue
counts.set(item.cwd, (counts.get(item.cwd) ?? 0) + 1)
}
expect(counts.get(dirs[0]!)).toBeGreaterThan(counts.get(dirs[1]!) ?? 0)
expect(counts.get(dirs[1]!)).toBeGreaterThan(2)
expect(counts.get(dirs[2]!)).toBeGreaterThan(2)
} finally {
await fs.promises.rm(root, { recursive: true, force: true })
}
})
it("keeps mutual exclusion when a stale fetch finishes after a restart", async () => {
let calls = 0
let running = 0
let max = 0
let gate: (() => void) | undefined
const gated = new Promise<void>((resolve) => {
gate = resolve
})
const localDiff = async () => {
calls += 1
running += 1
max = Math.max(max, running)
if (calls === 1) await gated
running -= 1
return [{ ...diff(1, 0)[0], stamp: `s${calls}` }]
}
const poller = new GitStatsPoller({
getWorktrees: () => [],
getWorkspaceRoot: () => "/tmp",
source: source(localDiff, "main"),
onStats: () => undefined,
onLocalStats: () => undefined,
log: () => undefined,
intervalMs: 5,
git: gitOps(async (args) => {
if (args[0] === "rev-parse" && args[2] === "HEAD") return "main"
if (args[0] === "symbolic-ref") return "origin/main"
if (args[0] === "rev-list") return "0 0"
return ""
}),
})
poller.setEnabled(true)
await waitFor(() => calls === 1)
// Restart while the first fetch is in flight, then let it finish. The
// stale finally must not clear the busy flag owned by the new poll.
poller.stop()
poller.setEnabled(true)
await waitFor(() => calls === 2)
gate?.()
await waitFor(() => calls >= 3)
poller.stop()
// The gated first fetch and the restarted poll overlapped by
// construction; no further overlap may occur after the restart.
expect(max).toBeLessThanOrEqual(2)
})
it("does not overlap polling runs", async () => {
let running = 0
let max = 0
let calls = 0
const localDiff = async () => {
calls += 1
running += 1
max = Math.max(max, running)
await sleep(40)
running -= 1
return diff(2, 1)
}
const poller = new GitStatsPoller({
getWorktrees: () => [worktree("a")],
getWorkspaceRoot: () => undefined,
source: source(localDiff),
onStats: () => undefined,
onLocalStats: () => undefined,
log: () => undefined,
intervalMs: 5,
git: gitOps(async (args) => {
if (args[0] === "rev-list" && args[1] === "--left-right") return "0\t1"
return ""
}),
})
poller.setEnabled(true)
await waitFor(() => calls >= 2)
poller.stop()
expect(max).toBe(1)
})
it("keeps last-known stats when a later poll fails", async () => {
let calls = 0
const emitted: Array<
Array<{ worktreeId: string; files: number; additions: number; deletions: number; ahead: number; behind: number }>
> = []
const localDiff = async () => {
calls += 1
if (calls === 1) return diff(7, 3)
throw new Error("transient backend failure")
}
const poller = new GitStatsPoller({
getWorktrees: () => [worktree("a")],
getWorkspaceRoot: () => undefined,
source: source(localDiff),
onStats: (stats) => emitted.push(stats),
onLocalStats: () => undefined,
log: () => undefined,
intervalMs: 5,
git: gitOps(async (args) => {
if (args[0] === "rev-parse" && args[3] === "@{upstream}") return "origin/main"
if (args[0] === "rev-list" && args[1] === "--left-right") return "0\t2"
return ""
}),
})
poller.setEnabled(true)
await waitFor(() => calls >= 2)
poller.stop()
expect(emitted.length).toBeGreaterThan(0)
const first = emitted[0]
if (!first) throw new Error("expected emitted stats")
expect(first[0]).toEqual({ worktreeId: "a", files: 1, additions: 7, deletions: 3, ahead: 2, behind: 0 })
const hasZeros = emitted.some((batch) =>
batch.some((item) => item.additions === 0 && item.deletions === 0 && item.ahead === 0),
)
expect(hasZeros).toBe(false)
})
it("emits present worktree probes on the poll loop", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "gsp-presence-"))
const wtPath = path.join(root, "wt-a")
fs.mkdirSync(wtPath, { recursive: true })
const presence: WorktreePresenceResult[] = []
const poller = new GitStatsPoller({
getWorktrees: () => [{ ...worktree("a"), path: wtPath }],
getWorkspaceRoot: () => root,
source: source(async () => {
throw new Error("should not be called when backend unavailable path")
}),
onStats: () => undefined,
onLocalStats: () => undefined,
onWorktreePresence: (result) => presence.push(result),
log: () => undefined,
intervalMs: 5,
git: gitOps(async (args) => {
if (args[0] === "worktree") {
return `worktree ${wtPath}\nbranch refs/heads/branch-a\n`
}
return ""
}),
})
poller.setEnabled(true)
await waitFor(() => presence.length >= 1)
poller.stop()
fs.rmSync(root, { recursive: true, force: true })
expect(presence[0]).toEqual({
worktrees: [{ worktreeId: "a", missing: false, branch: "branch-a" }],
degraded: false,
})
})
it("emits degraded probe when git worktree listing fails", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "gsp-presence-fail-"))
const wtPath = path.join(root, "wt-a")
fs.mkdirSync(wtPath, { recursive: true })
const presence: WorktreePresenceResult[] = []
const poller = new GitStatsPoller({
getWorktrees: () => [{ ...worktree("a"), path: wtPath }],
getWorkspaceRoot: () => root,
source: source(async () => diff(0, 0)),
onStats: () => undefined,
onLocalStats: () => undefined,
onWorktreePresence: (result) => presence.push(result),
log: () => undefined,
intervalMs: 5,
git: gitOps(async (args) => {
if (args[0] === "worktree") {
throw new Error("git worktree list failed")
}
return ""
}),
})
poller.setEnabled(true)
await waitFor(() => presence.length >= 1)
poller.stop()
fs.rmSync(root, { recursive: true, force: true })
expect(presence[0]).toEqual({ worktrees: [], degraded: true })
})
it("skips stats fetch for missing worktrees detected by presence probe", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "gsp-skip-missing-"))
const wtAPath = path.join(root, "wt-a")
const wtBPath = path.join(root, "wt-b")
fs.mkdirSync(wtAPath, { recursive: true })
const calls: string[] = []
const emitted: Array<Array<{ worktreeId: string; additions: number; deletions: number }>> = []
const presence: WorktreePresenceResult[] = []
const poller = new GitStatsPoller({
getWorktrees: () => [
{ ...worktree("a"), path: wtAPath },
{ ...worktree("b"), path: wtBPath },
],
getWorkspaceRoot: () => root,
source: source(async (dir) => {
calls.push(dir)
return diff(1, 1)
}),
onStats: (stats) => emitted.push(stats),
onLocalStats: () => undefined,
onWorktreePresence: (result) => presence.push(result),
log: () => undefined,
intervalMs: 5,
git: gitOps(async (args) => {
if (args[0] === "worktree") {
return `worktree ${wtAPath}\nbranch refs/heads/branch-a\n`
}
if (args[0] === "rev-parse" && args[3] === "@{upstream}") return "origin/main"
if (args[0] === "rev-list") return "1"
return ""
}),
})
poller.setEnabled(true)
await waitFor(() => calls.length >= 1)
poller.stop()
fs.rmSync(root, { recursive: true, force: true })
expect(calls.some((cwd) => cwd === wtBPath)).toBe(false)
expect(presence[0]).toEqual({
worktrees: [
{ worktreeId: "a", missing: false, branch: "branch-a" },
{ worktreeId: "b", missing: true, branch: undefined },
],
degraded: false,
})
expect(emitted[0]?.map((item) => item.worktreeId)).toEqual(["a"])
})
it("treats symlink-aliased worktree paths as present via realpath canonicalization", async () => {
// git worktree list --porcelain realpath-resolves worktree registration, so
// a worktree registered through a symlink alias (e.g. /tmp -> /private/tmp
// on macOS) is reported under its realpath. The probe must canonicalize the
// session's lexical alias the same way, or a real worktree is marked missing
// and its stats are excluded.
const root = fs.mkdtempSync(path.join(os.tmpdir(), "gsp-symlink-"))
const real = fs.mkdtempSync(path.join(os.tmpdir(), "gsp-symlink-real-"))
const alias = path.join(root, "alias")
fs.symlinkSync(real, alias)
const presence: WorktreePresenceResult[] = []
const calls: string[] = []
const emitted: Array<Array<{ worktreeId: string; additions: number; deletions: number }>> = []
const poller = new GitStatsPoller({
getWorktrees: () => [{ ...worktree("a"), path: alias }],
getWorkspaceRoot: () => root,
source: source(async (dir) => {
calls.push(dir)
return diff(3, 2)
}),
onStats: (stats) => emitted.push(stats),
onLocalStats: () => undefined,
onWorktreePresence: (result) => presence.push(result),
log: () => undefined,
intervalMs: 5,
git: gitOps(async (args) => {
if (args[0] === "worktree") {
return `worktree ${real}\nbranch refs/heads/branch-a\n`
}
if (args[0] === "rev-parse" && args[3] === "@{upstream}") return "origin/main"
if (args[0] === "rev-list") return "1"
return ""
}),
})
poller.setEnabled(true)
await waitFor(() => presence.length >= 1 && emitted.length >= 1)
poller.stop()
fs.rmSync(root, { recursive: true, force: true })
fs.rmSync(real, { recursive: true, force: true })
expect(presence[0]).toEqual({
worktrees: [{ worktreeId: "a", missing: false, branch: "branch-a" }],
degraded: false,
})
expect(calls.some((dir) => dir === alias)).toBe(true)
expect(emitted[0]?.map((item) => item.worktreeId)).toEqual(["a"])
})
it("preserves local stats when diff fails after initial success", async () => {
let diffCalls = 0
const emitted: Array<{
branch: string
files: number
additions: number
deletions: number
ahead: number
behind: number
}> = []
const localDiff = async () => {
diffCalls += 1
if (diffCalls === 1) return diff(5, 2)
throw new Error("transient backend failure")
}
const poller = new GitStatsPoller({
getWorktrees: () => [],
getWorkspaceRoot: () => "/workspace",
source: source(localDiff, "feature"),
onStats: () => undefined,
onLocalStats: (stats) => emitted.push(stats),
log: () => undefined,
intervalMs: 5,
git: gitOps(async (args) => {
if (args[0] === "rev-parse" && args[1] === "--abbrev-ref" && args[2] === "HEAD") return "feature"
if (args[0] === "rev-parse" && args[1] === "--abbrev-ref" && args[2] === "@{upstream}") return "origin/feature"
if (args[0] === "rev-list" && args[1] === "--left-right") return "0\t3"
if (args[0] === "branch") return "feature"
if (args[0] === "config") return "origin"
return ""
}),
})
poller.setEnabled(true)
await waitFor(() => diffCalls >= 2)
poller.stop()
expect(emitted.length).toBeGreaterThan(0)
expect(emitted[0]).toEqual({ branch: "feature", files: 1, additions: 5, deletions: 2, ahead: 3, behind: 0 })
expect(emitted.length).toBe(1)
})
it("falls back to <remote>/HEAD when no upstream and no <remote>/<branch>", async () => {
const emitted: Array<{
branch: string
files: number
additions: number
deletions: number
ahead: number
behind: number
}> = []
const poller = new GitStatsPoller({
getWorktrees: () => [],
getWorkspaceRoot: () => "/workspace",
source: source(async () => diff(10, 4), "my-feature"),
onStats: () => undefined,
onLocalStats: (stats) => emitted.push(stats),
log: () => undefined,
intervalMs: 500,
git: gitOps(async (args) => {
if (args[0] === "rev-parse" && args[1] === "--abbrev-ref" && args[2] === "HEAD") return "my-feature"
// no upstream configured (used by resolveTrackingBranch and resolveRemote)
if (args[0] === "rev-parse" && args[1] === "--abbrev-ref" && args[2] === "@{upstream}")
throw new Error("no upstream")
if (args[0] === "rev-parse" && args[3] === "@{upstream}") throw new Error("no upstream")
// branch.my-feature.remote = myfork
if (args[0] === "config" && args[1] === "branch.my-feature.remote") return "myfork"
// myfork/my-feature does not exist
if (args[0] === "rev-parse" && args[1] === "--verify" && args[2] === "myfork/my-feature")
throw new Error("no ref")
// myfork/HEAD resolves to the default branch
if (args[0] === "symbolic-ref" && args[2] === "refs/remotes/myfork/HEAD") return "myfork/develop"
if (args[0] === "branch") return "my-feature"
if (args[0] === "rev-list" && args[1] === "--left-right") return "0\t5"
return ""
}),
})
poller.setEnabled(true)
await waitFor(() => emitted.length >= 1)
poller.stop()
expect(emitted[0]).toEqual({ branch: "my-feature", files: 1, additions: 10, deletions: 4, ahead: 5, behind: 0 })
})
it("falls back to workingTreeStats when no tracking, no default branch, and no remote refs exist", async () => {
const emitted: Array<{
branch: string
files: number
additions: number
deletions: number
ahead: number
behind: number
}> = []
const poller = new GitStatsPoller({
getWorktrees: () => [],
getWorkspaceRoot: () => "/workspace",
source: source(async () => diff(0, 0), "orphan-branch"),
onStats: () => undefined,
onLocalStats: (stats) => emitted.push(stats),
log: () => undefined,
intervalMs: 500,
git: gitOps(async (args) => {
if (args[0] === "rev-parse" && args[1] === "--abbrev-ref" && args[2] === "HEAD") return "orphan-branch"
if (args[0] === "rev-parse" && args[1] === "--abbrev-ref" && args[2] === "@{upstream}")
throw new Error("no upstream")
if (args[0] === "rev-parse" && args[1] === "--verify" && args[2] === "origin/orphan-branch")
throw new Error("no ref")
if (args[0] === "symbolic-ref") throw new Error("no symbolic ref")
if (args[0] === "rev-parse" && args[1] === "--verify" && args[2] === "--quiet") throw new Error("no ref")
// workingTreeStats fallback: no tracked changes, no untracked files
if (args[0] === "diff") return ""
if (args[0] === "ls-files") return ""
return ""
}),
})
poller.setEnabled(true)
await waitFor(() => emitted.length >= 1)
poller.stop()
expect(emitted[0]).toEqual({
branch: "orphan-branch",
files: 0,
additions: 0,
deletions: 0,
ahead: 0,
behind: 0,
})
})
it("does not fetch from remote for ahead/behind counts", async () => {
const commands: string[][] = []
const emitted: Array<
Array<{ worktreeId: string; files: number; additions: number; deletions: number; ahead: number; behind: number }>
> = []
const poller = new GitStatsPoller({
getWorktrees: () => [worktree("a", "upstream"), worktree("b", "upstream")],
getWorkspaceRoot: () => undefined,
source: source(async () => diff(0, 0)),
onStats: (stats) => emitted.push(stats),
onLocalStats: () => undefined,
log: () => undefined,
intervalMs: 500,
git: gitOps(async (args) => {
commands.push(args)
if (args[0] === "rev-list" && args[1] === "--left-right") return "0\t0"
return ""
}),
})
poller.setEnabled(true)
await waitFor(() => emitted.length >= 1)
poller.stop()
const fetches = commands.filter((cmd) => cmd[0] === "fetch")
expect(fetches.length).toBe(0)
})
it("runs diffs in parallel without stalling (no extra semaphore layer)", async () => {
// The injected diff source is a synchronous promise. The poller does not
// wrap it in a semaphore because GitOps gates at the child-process layer,
// many worktrees can have their diffs computed concurrently without
// contending for a dedicated outer gate.
let running = 0
let peak = 0
let ticks = 0
const wts = Array.from({ length: 5 }, (_, i) => worktree(String(i)))
const poller = new GitStatsPoller({
getWorktrees: () => wts,
getWorkspaceRoot: () => undefined,
source: source(async () => {
running++
peak = Math.max(peak, running)
await sleep(20)
running--
return diff(1, 0)
}),
onStats: () => {
ticks++
},
onLocalStats: () => undefined,
log: () => undefined,
intervalMs: 5,
git: gitOps(async (args) => {
if (args[0] === "rev-list" && args[1] === "--left-right") return "0\t0"
return ""
}),
})
poller.setEnabled(true)
await waitFor(() => ticks >= 1)
poller.stop()
// All 5 diffs can run in parallel (no artificial cap at this layer).
expect(peak).toBeGreaterThan(1)
})
it("runs concurrent diffs without deadlock when GitOps semaphore is shared", async () => {
// Wire the SAME semaphore into GitOps to prove the aheadBehind path
// (which goes through GitOps.raw) does not deadlock with the diff path.
const sem = new Semaphore(2)
let ticks = 0
const wts = Array.from({ length: 5 }, (_, i) => worktree(String(i)))
const poller = new GitStatsPoller({
getWorktrees: () => wts,
getWorkspaceRoot: () => undefined,
source: source(async () => diff(1, 0)),
onStats: () => {
ticks++
},
onLocalStats: () => undefined,
log: () => undefined,
intervalMs: 5,
semaphore: sem,
git: new GitOps({
log: () => undefined,
semaphore: sem,
runGit: async (args) => {
if (args[0] === "rev-list" && args[1] === "--left-right") return "0\t0"
return ""
},
}),
})
poller.setEnabled(true)
await waitFor(() => ticks >= 1)
poller.stop()
expect(ticks).toBeGreaterThan(0)
})
})