Files
opencode/packages/opencode/test/project/vcs.test.ts
Kit Langton 8c566cb764 refactor(vcs): effectify VcsService as scoped service
Convert Vcs from Instance.state namespace to an Effect ServiceMap.Service
on the Instances LayerMap. Uses Instance.bind for the Bus.subscribe
callback (same ALS pattern as FileWatcherService). Branch state is
managed in the layer closure with scope-based cleanup.
2026-03-15 14:05:57 -04:00

118 lines
4.0 KiB
TypeScript

import { $ } from "bun"
import { afterEach, describe, expect, test } from "bun:test"
import fs from "fs/promises"
import path from "path"
import { Layer, ManagedRuntime } from "effect"
import { tmpdir } from "../fixture/fixture"
import { watcherConfigLayer, withServices } from "../fixture/instance"
import { FileWatcher, FileWatcherService } from "../../src/file/watcher"
import { Instance } from "../../src/project/instance"
import { GlobalBus } from "../../src/bus/global"
import { Vcs, VcsService } from "../../src/project/vcs"
// Skip in CI — native @parcel/watcher binding needed
const describeVcs = FileWatcher.hasNativeBinding() && !process.env.CI ? describe : describe.skip
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function withVcs(
directory: string,
body: (rt: ManagedRuntime.ManagedRuntime<FileWatcherService | VcsService, never>) => Promise<void>,
) {
return withServices(
directory,
Layer.merge(FileWatcherService.layer, VcsService.layer),
async (rt) => {
await rt.runPromise(FileWatcherService.use((s) => s.init()))
await rt.runPromise(VcsService.use((s) => s.init()))
await Bun.sleep(200)
await body(rt)
},
{ provide: [watcherConfigLayer] },
)
}
type BranchEvent = { directory?: string; payload: { type: string; properties: { branch?: string } } }
/** Wait for a Vcs.Event.BranchUpdated event on GlobalBus */
function nextBranchUpdate(directory: string, timeout = 5000) {
return new Promise<string | undefined>((resolve, reject) => {
const timer = setTimeout(() => {
GlobalBus.off("event", on)
reject(new Error("timed out waiting for BranchUpdated event"))
}, timeout)
function on(evt: BranchEvent) {
if (evt.directory !== directory) return
if (evt.payload.type !== Vcs.Event.BranchUpdated.type) return
clearTimeout(timer)
GlobalBus.off("event", on)
resolve(evt.payload.properties.branch)
}
GlobalBus.on("event", on)
})
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describeVcs("Vcs", () => {
afterEach(() => Instance.disposeAll())
test("branch() returns current branch name", async () => {
await using tmp = await tmpdir({ git: true })
await withVcs(tmp.path, async (rt) => {
const branch = await rt.runPromise(VcsService.use((s) => s.branch()))
expect(branch).toBeDefined()
expect(typeof branch).toBe("string")
})
})
test("branch() returns undefined for non-git directories", async () => {
await using tmp = await tmpdir()
await withVcs(tmp.path, async (rt) => {
const branch = await rt.runPromise(VcsService.use((s) => s.branch()))
expect(branch).toBeUndefined()
})
})
test("publishes BranchUpdated when .git/HEAD changes", async () => {
await using tmp = await tmpdir({ git: true })
const branch = `test-${Math.random().toString(36).slice(2)}`
await $`git branch ${branch}`.cwd(tmp.path).quiet()
await withVcs(tmp.path, async () => {
const pending = nextBranchUpdate(tmp.path)
const head = path.join(tmp.path, ".git", "HEAD")
await fs.writeFile(head, `ref: refs/heads/${branch}\n`)
const updated = await pending
expect(updated).toBe(branch)
})
})
test("branch() reflects the new branch after HEAD change", async () => {
await using tmp = await tmpdir({ git: true })
const branch = `test-${Math.random().toString(36).slice(2)}`
await $`git branch ${branch}`.cwd(tmp.path).quiet()
await withVcs(tmp.path, async (rt) => {
const pending = nextBranchUpdate(tmp.path)
const head = path.join(tmp.path, ".git", "HEAD")
await fs.writeFile(head, `ref: refs/heads/${branch}\n`)
await pending
const current = await rt.runPromise(VcsService.use((s) => s.branch()))
expect(current).toBe(branch)
})
})
})