opencode/packages/core/test/vcs.test.ts
2026-08-28 22:46:23 -04:00

862 lines
39 KiB
TypeScript

import { $ } from "bun"
import { describe, expect } from "bun:test"
import fs from "fs/promises"
import path from "path"
import { Cause, Effect, Exit, Fiber, Layer, Stream } from "effect"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { AppProcess } from "@opencode-ai/util/process"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Git } from "@opencode-ai/core/git"
import { Bus } from "@opencode-ai/core/bus"
import { Location } from "@opencode-ai/core/location"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Vcs } from "@opencode-ai/core/vcs"
import { VcsGitPlugin } from "@opencode-ai/core/plugin/vcs/git"
import type { VcsDefinition, VcsDiffInput } from "@opencode-ai/plugin/effect/vcs"
import { FileSystem } from "@opencode-ai/schema/filesystem"
import { VcsEvent } from "@opencode-ai/schema/vcs-event"
import { location } from "./fixture/location"
import { tmpdir } from "./fixture/tmpdir"
import { it } from "./lib/effect"
import { host } from "./plugin/host"
const provide = (directory: string, input: { git?: boolean; worktree?: string } = {}) =>
Effect.provide(
LayerNode.compile(LayerNode.group([Vcs.node, Bus.node, Location.node, AppProcess.node, FSUtil.node, Git.node]), [
[
Location.node,
Layer.succeed(
Location.Service,
Location.Service.of(
location(
{ directory: AbsolutePath.make(directory) },
{
projectDirectory: input.worktree ? AbsolutePath.make(input.worktree) : undefined,
...(input.git
? { vcs: { type: "git", store: AbsolutePath.make(path.join(input.worktree ?? directory, ".git")) } }
: {}),
},
),
),
),
],
]),
)
const withTmp = <A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>) =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
).pipe(Effect.flatMap((tmp) => f(tmp.path)))
const withGit = <A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>, input: { scope?: string } = {}) =>
withTmp((directory) =>
Effect.promise(async () => {
await initRepo(directory)
if (input.scope) await fs.mkdir(path.join(directory, input.scope), { recursive: true })
}).pipe(
Effect.andThen(
Effect.gen(function* () {
const vcs = yield* Vcs.Service
const context = host()
yield* VcsGitPlugin.Plugin.effect({
...context,
vcs: { ...context.vcs, transform: vcs.transform, reload: vcs.reload },
})
return yield* f(directory)
}).pipe(provide(path.join(directory, input.scope ?? "."), { git: true, worktree: directory })),
),
),
)
async function initRepo(directory: string) {
await $`git init -b main`.cwd(directory).quiet()
await $`git config core.fsmonitor false`.cwd(directory).quiet()
await $`git config commit.gpgsign false`.cwd(directory).quiet()
await $`git config user.email test@opencode.test`.cwd(directory).quiet()
await $`git config user.name Test`.cwd(directory).quiet()
}
async function commitAll(directory: string, message: string) {
await $`git add -A`.cwd(directory).quiet()
await $`git commit -m ${message}`.cwd(directory).quiet()
}
const provider = (input: Partial<VcsDefinition> = {}) =>
({
id: "custom",
name: "Custom VCS",
info: () => Effect.succeed({ branch: { current: "feature", default: "main" } }),
branches: () => Effect.succeed(["feature", "main"]),
status: () => Effect.succeed([{ file: "file.txt", additions: 1, deletions: 0, status: "added" }]),
diff: () => Effect.succeed([{ file: "file.txt", patch: "+hello", additions: 1, deletions: 0, status: "added" }]),
...input,
}) satisfies VcsDefinition
describe("Vcs", () => {
it.live("returns empty results outside version control", () =>
withTmp((directory) =>
Effect.gen(function* () {
const vcs = yield* Vcs.Service
expect(yield* vcs.info()).toEqual({ branch: {} })
expect(yield* vcs.branches()).toEqual([])
expect(yield* vcs.base()).toBeNull()
expect(yield* vcs.status()).toEqual([])
expect(yield* vcs.diff("working")).toEqual([])
expect(yield* vcs.diff("branch")).toEqual([])
}).pipe(provide(directory)),
),
)
it.live("serves scoped providers and restores the fallback after disposal", () =>
withTmp((directory) =>
Effect.gen(function* () {
const vcs = yield* Vcs.Service
const registration = yield* vcs.transform((draft) => {
draft.add(provider())
draft.default.set("custom")
})
expect(yield* vcs.info()).toEqual({ branch: { current: "feature", default: "main" } })
expect(yield* vcs.base()).toBeNull()
expect(yield* vcs.branches()).toEqual(["feature", "main"])
expect(yield* vcs.status()).toEqual([{ file: "file.txt", additions: 1, deletions: 0, status: "added" }])
expect(yield* vcs.diff("working")).toEqual([
{ file: "file.txt", patch: "+hello", additions: 1, deletions: 0, status: "added" },
])
yield* registration.dispose
expect(yield* vcs.info()).toEqual({ branch: {} })
expect(yield* vcs.status()).toEqual([])
}).pipe(provide(directory)),
),
)
it.live("automatically selects a provider matching the resolved repository", () =>
withGit(() =>
Effect.gen(function* () {
const vcs = yield* Vcs.Service
const registration = yield* vcs.transform((draft) => draft.add(provider({ id: "git" })))
expect(yield* vcs.info()).toEqual({ branch: { current: "feature", default: "main" } })
yield* registration.dispose
expect(yield* vcs.info()).toEqual({ branch: { current: "main", default: undefined } })
}),
),
)
it.live("passes location scope and bounded diff options to providers", () =>
withTmp((directory) =>
Effect.gen(function* () {
const observed: VcsDiffInput[] = []
const vcs = yield* Vcs.Service
yield* vcs.transform((draft) => {
draft.add(
provider({
diff: (input) =>
Effect.sync(() => {
observed.push(input)
return [{ file: "file.txt", patch: "+hello", additions: 1, deletions: 0, status: "added" }]
}),
}),
)
draft.default.set("custom")
})
yield* vcs.diff("committed", { context: 3, base: "release" })
expect(observed).toEqual([
{
directory,
worktree: directory,
canonical: directory,
mode: "committed",
base: "release",
context: 3,
maxOutputBytes: 10_000_000,
},
])
}).pipe(provide(directory)),
),
)
it.live("validates provider results and bounds oversized patches", () =>
withTmp((directory) =>
Effect.gen(function* () {
const vcs = yield* Vcs.Service
yield* vcs.transform((draft) => {
draft.add(
provider({
status: () => Effect.succeed([{ file: "file.txt", additions: -1, deletions: 0, status: "added" }]),
diff: () =>
Effect.succeed([
{ file: "file.txt", patch: "x".repeat(10_000_001), additions: 1, deletions: 0, status: "added" },
]),
}),
)
draft.default.set("custom")
})
expect(yield* vcs.status()).toEqual([])
const rows = yield* vcs.diff("working")
expect(rows).toHaveLength(1)
expect(Buffer.byteLength(rows[0].patch)).toBeLessThan(1000)
expect(rows[0].additions).toBe(1)
}).pipe(provide(directory)),
),
)
it.live("preserves provider interruption", () =>
withTmp((directory) =>
Effect.gen(function* () {
const vcs = yield* Vcs.Service
yield* vcs.transform((draft) => {
draft.add(provider({ status: () => Effect.never, diff: () => Effect.never, base: () => Effect.never }))
draft.default.set("custom")
})
const fiber = yield* Effect.forkChild(vcs.status())
yield* Fiber.interrupt(fiber)
const exit = yield* Fiber.await(fiber)
expect(Exit.isFailure(exit) && Cause.hasInterrupts(exit.cause)).toBeTrue()
const diff = yield* Effect.forkChild(vcs.diff("committed"))
yield* Fiber.interrupt(diff)
const interrupted = yield* Fiber.await(diff)
expect(Exit.isFailure(interrupted) && Cause.hasInterrupts(interrupted.cause)).toBeTrue()
const base = yield* Effect.forkChild(vcs.base())
yield* Fiber.interrupt(base)
const cancelled = yield* Fiber.await(base)
expect(Exit.isFailure(cancelled) && Cause.hasInterrupts(cancelled.cause)).toBeTrue()
}).pipe(provide(directory)),
),
)
it.live("lists local branches by recent activity", () =>
withGit((directory) =>
Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.writeFile(path.join(directory, "file.txt"), "one\n")
await commitAll(directory, "initial")
await $`git checkout -b z-recent`.cwd(directory).quiet()
await fs.writeFile(path.join(directory, "file.txt"), "two\n")
await $`git add -A`.cwd(directory).quiet()
await $`git commit -m recent`
.cwd(directory)
.env({
...process.env,
GIT_AUTHOR_DATE: "2030-01-01T00:00:00Z",
GIT_COMMITTER_DATE: "2030-01-01T00:00:00Z",
})
.quiet()
})
const vcs = yield* Vcs.Service
expect(yield* vcs.branches()).toEqual(["z-recent", "main"])
expect(yield* vcs.branches({ limit: 1 })).toEqual(["z-recent"])
expect(yield* vcs.branches({ search: "MAIN", limit: 1 })).toEqual(["main"])
expect(yield* vcs.branches({ search: "*" })).toEqual([])
}),
),
)
it.live("reports modified, deleted, and untracked files", () =>
withGit((directory) =>
Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.writeFile(path.join(directory, "keep.txt"), "one\ntwo\n")
await fs.writeFile(path.join(directory, "gone.txt"), "bye\n")
await commitAll(directory, "initial")
await fs.writeFile(path.join(directory, "keep.txt"), "one\nthree\n")
await fs.rm(path.join(directory, "gone.txt"))
await fs.writeFile(path.join(directory, "new.txt"), "hello\nworld\n")
})
const vcs = yield* Vcs.Service
const status = yield* vcs.status()
expect(status).toEqual([
{ file: "gone.txt", additions: 0, deletions: 1, status: "deleted" },
{ file: "keep.txt", additions: 1, deletions: 1, status: "modified" },
{ file: "new.txt", additions: 2, deletions: 0, status: "added" },
])
}),
),
)
it.live("caches branch info and publishes HEAD changes", () =>
withGit((directory) =>
Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.writeFile(path.join(directory, "file.txt"), "one\n")
await commitAll(directory, "initial")
})
const vcs = yield* Vcs.Service
const bus = yield* Bus.Service
expect(yield* vcs.info()).toEqual({ branch: { current: "main", default: undefined } })
const updated = yield* bus
.subscribe(VcsEvent.BranchUpdated)
.pipe(Stream.take(1), Stream.runHead, Effect.forkScoped({ startImmediately: true }))
yield* Effect.promise(() => $`git checkout -q -b feature`.cwd(directory).quiet())
yield* bus.publish(FileSystem.Event.Changed, { file: path.join(directory, "HEAD"), event: "change" })
expect(yield* vcs.info()).toEqual({ branch: { current: "main", default: undefined } })
yield* bus.publish(FileSystem.Event.Changed, { file: path.join(directory, ".git", "HEAD"), event: "change" })
expect(yield* Fiber.join(updated)).toMatchObject({
_tag: "Some",
value: { location: { directory }, data: { branch: "feature" } },
})
expect(yield* vcs.info()).toEqual({ branch: { current: "feature", default: "main" } })
}),
),
)
it.live("diffs the working copy against HEAD with patches", () =>
withGit((directory) =>
Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.writeFile(path.join(directory, "keep.txt"), "one\ntwo\n")
await commitAll(directory, "initial")
await fs.writeFile(path.join(directory, "keep.txt"), "one\nthree\n")
await fs.writeFile(path.join(directory, "spaced name.txt"), "hello\n")
})
const vcs = yield* Vcs.Service
const diff = yield* vcs.diff("working")
expect(diff.map((item) => ({ file: item.file, status: item.status }))).toEqual([
{ file: "keep.txt", status: "modified" },
{ file: "spaced name.txt", status: "added" },
])
expect(diff[0].patch).toContain("-two")
expect(diff[0].patch).toContain("+three")
expect(diff[0].additions).toBe(1)
expect(diff[0].deletions).toBe(1)
expect(diff[1].patch).toContain("+hello")
expect(diff[1].additions).toBe(1)
}),
),
)
it.live("respects the context option", () =>
withGit((directory) =>
Effect.gen(function* () {
const body = Array.from({ length: 20 }, (_, index) => `line-${index}`).join("\n") + "\n"
yield* Effect.promise(async () => {
await fs.writeFile(path.join(directory, "file.txt"), body)
await commitAll(directory, "initial")
await fs.writeFile(path.join(directory, "file.txt"), body.replace("line-10", "changed"))
})
const vcs = yield* Vcs.Service
const full = yield* vcs.diff("working")
expect(full[0].patch).toContain("line-0")
expect(full[0].patch).toContain("line-19")
const tight = yield* vcs.diff("working", { context: 1 })
expect(tight[0].patch).toContain("line-9")
expect(tight[0].patch).not.toContain("line-0")
}),
),
)
it.live("diffs before the first commit", () =>
withGit((directory) =>
Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.writeFile(path.join(directory, "new.txt"), "hello\n")
})
const vcs = yield* Vcs.Service
expect(yield* vcs.status()).toEqual([{ file: "new.txt", additions: 1, deletions: 0, status: "added" }])
const diff = yield* vcs.diff("working")
expect(diff).toHaveLength(1)
expect(diff[0].patch).toContain("+hello")
expect(yield* vcs.base()).toBeNull()
expect(yield* vcs.diff("branch")).toEqual(diff)
expect(yield* vcs.diff("committed")).toEqual([])
}),
),
)
it.live("diffs a feature branch against the default branch", () =>
withGit((directory) =>
Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.writeFile(path.join(directory, "file.txt"), "one\n")
await commitAll(directory, "initial")
})
const vcs = yield* Vcs.Service
expect(yield* vcs.diff("branch")).toEqual([])
yield* Effect.promise(async () => {
await $`git checkout -q -b feature main`.cwd(directory).quiet()
await fs.writeFile(path.join(directory, "file.txt"), "one\ntwo\n")
await commitAll(directory, "feature change")
})
const diff = yield* vcs.diff("branch")
expect(diff.map((item) => ({ file: item.file, status: item.status }))).toEqual([
{ file: "file.txt", status: "modified" },
])
expect(diff[0].patch).toContain("+two")
}),
),
)
it.live("separates committed, combined, and staged/unstaged/untracked working changes", () =>
withGit((directory) =>
Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.writeFile(path.join(directory, "file.txt"), "base\n")
await commitAll(directory, "initial")
await $`git checkout -b feature main`.cwd(directory).quiet()
await fs.writeFile(path.join(directory, "file.txt"), "committed\n")
await commitAll(directory, "feature")
await fs.writeFile(path.join(directory, "staged.txt"), "staged\n")
await $`git add staged.txt`.cwd(directory).quiet()
await fs.writeFile(path.join(directory, "file.txt"), "unstaged\n")
await fs.writeFile(path.join(directory, "untracked.txt"), "untracked\n")
})
const vcs = yield* Vcs.Service
const committed = yield* vcs.diff("committed")
expect(committed.map((row) => row.file)).toEqual(["file.txt"])
expect(committed[0].patch).toContain("-base\n+committed")
expect(committed[0]).toMatchObject({ additions: 1, deletions: 1 })
const combined = yield* vcs.diff("branch")
expect(combined.map((row) => row.file)).toEqual(["file.txt", "staged.txt", "untracked.txt"])
expect(combined[0].patch).toContain("-base\n+unstaged")
const working = yield* vcs.diff("working")
expect(working.map((row) => row.file)).toEqual(["file.txt", "staged.txt", "untracked.txt"])
expect(working[0].patch).toContain("-committed\n+unstaged")
}),
),
)
it.live("keeps a locally undone commit visible only in committed and working modes", () =>
withGit((directory) =>
Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.writeFile(path.join(directory, "file.txt"), "base\n")
await commitAll(directory, "initial")
await $`git checkout -b feature main`.cwd(directory).quiet()
await fs.writeFile(path.join(directory, "file.txt"), "committed\n")
await commitAll(directory, "feature")
await fs.writeFile(path.join(directory, "file.txt"), "base\n")
})
const vcs = yield* Vcs.Service
expect(yield* vcs.diff("branch")).toEqual([])
expect((yield* vcs.diff("committed"))[0].patch).toContain("-base\n+committed")
expect((yield* vcs.diff("working"))[0].patch).toContain("-committed\n+base")
}),
),
)
it.live("shows local WIP on the default branch", () =>
withGit((directory) =>
Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.writeFile(path.join(directory, "file.txt"), "base\n")
await commitAll(directory, "initial")
await fs.writeFile(path.join(directory, "file.txt"), "dirty\n")
})
const vcs = yield* Vcs.Service
expect(yield* vcs.base()).toEqual({ name: "main", ref: "refs/heads/main", source: "default" })
expect(yield* vcs.diff("branch")).toEqual(yield* vcs.diff("working"))
expect(yield* vcs.diff("committed")).toEqual([])
}),
),
)
it.live("keeps unpushed default-branch commits in the resolved review", () =>
withGit((directory) =>
Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.writeFile(path.join(directory, "file.txt"), "base\n")
await commitAll(directory, "initial")
await $`git remote add origin ${directory}`.cwd(directory).quiet()
await $`git update-ref refs/remotes/origin/main HEAD`.cwd(directory).quiet()
await $`git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main`.cwd(directory).quiet()
await fs.writeFile(path.join(directory, "file.txt"), "committed\n")
await commitAll(directory, "unpushed")
await fs.writeFile(path.join(directory, "file.txt"), "dirty\n")
})
const vcs = yield* Vcs.Service
const base = yield* vcs.base()
expect(base).toEqual({ name: "main", ref: "refs/remotes/origin/main", source: "default" })
expect((yield* vcs.diff("committed", { base: base?.ref }))[0].patch).toContain("-base\n+committed")
expect((yield* vcs.diff("branch", { base: base?.ref }))[0].patch).toContain("-base\n+dirty")
expect((yield* vcs.diff("working"))[0].patch).toContain("-committed\n+dirty")
}),
),
)
it.live("scopes committed diffs to nested paths and retains binary, rename, and deletion metadata", () =>
withGit(
(directory) =>
Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.writeFile(path.join(directory, "outside.txt"), "base\n")
await fs.writeFile(path.join(directory, "nested/old.txt"), "rename me\n")
await fs.writeFile(path.join(directory, "nested/gone.txt"), "delete me\n")
await fs.writeFile(path.join(directory, "nested/image.png"), Buffer.from([137, 80, 78, 71, 0, 1]))
await commitAll(directory, "initial")
await $`git checkout -b feature main`.cwd(directory).quiet()
await fs.writeFile(path.join(directory, "outside.txt"), "changed\n")
await fs.rename(path.join(directory, "nested/old.txt"), path.join(directory, "nested/new.txt"))
await fs.rm(path.join(directory, "nested/gone.txt"))
await fs.writeFile(path.join(directory, "nested/image.png"), Buffer.from([137, 80, 78, 71, 0, 2]))
await commitAll(directory, "feature")
})
const vcs = yield* Vcs.Service
const committed = yield* vcs.diff("committed")
expect(committed.map((row) => ({ file: row.file, status: row.status }))).toEqual([
{ file: "nested/gone.txt", status: "deleted" },
{ file: "nested/image.png", status: "modified" },
{ file: "nested/new.txt", status: "added" },
{ file: "nested/old.txt", status: "deleted" },
])
expect(committed[0].patch).toContain("-delete me")
expect(committed[1]).toMatchObject({ additions: 0, deletions: 0 })
expect(committed[1].patch).toContain("Binary files")
expect(committed[2].patch).toContain("+rename me")
yield* Effect.promise(async () => {
await fs.rm(path.join(directory, "nested/image.png"))
await fs.writeFile(path.join(directory, "nested/new.txt"), "dirty\n")
await fs.writeFile(path.join(directory, "nested/gone.txt"), "untracked resurrection\n")
await $`git add nested/new.txt`.cwd(directory).quiet()
})
expect(yield* vcs.diff("committed")).toEqual(committed)
}),
{ scope: "nested" },
),
)
it.live("uses an explicit diff base without treating upstream or Git config as a parent", () =>
withGit((directory) =>
Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.writeFile(path.join(directory, "file.txt"), "base\n")
await commitAll(directory, "initial")
await $`git checkout -b release`.cwd(directory).quiet()
await fs.writeFile(path.join(directory, "release.txt"), "release\n")
await commitAll(directory, "release")
await $`git checkout -b feature`.cwd(directory).quiet()
await fs.writeFile(path.join(directory, "feature.txt"), "feature\n")
await commitAll(directory, "feature")
await $`git branch --set-upstream-to=release feature`.cwd(directory).quiet()
await $`git config branch.feature.opencode-merge-base refs/heads/release`.cwd(directory).quiet()
})
const vcs = yield* Vcs.Service
expect(yield* vcs.base().pipe(Effect.flip)).toMatchObject({ message: "Choose a review base" })
expect((yield* vcs.diff("committed", { base: "release" })).map((row) => row.file)).toEqual(["feature.txt"])
expect((yield* vcs.diff("committed")).map((row) => row.file)).toEqual(["feature.txt", "release.txt"])
expect((yield* vcs.diff("branch", { base: "release" })).map((row) => row.file)).toEqual(["feature.txt"])
expect((yield* vcs.diff("branch")).map((row) => row.file)).toEqual(["feature.txt", "release.txt"])
expect(yield* vcs.base().pipe(Effect.flip)).toMatchObject({ message: "Choose a review base" })
}),
),
)
it.live("reports missing explicit bases and missing default bases without calling them clean", () =>
withGit((directory) =>
Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.writeFile(path.join(directory, "file.txt"), "base\n")
await commitAll(directory, "initial")
await $`git branch -m feature`.cwd(directory).quiet()
})
const vcs = yield* Vcs.Service
expect(yield* vcs.base().pipe(Effect.flip)).toMatchObject({ message: "Choose a review base" })
expect(yield* vcs.diff("committed").pipe(Effect.flip)).toMatchObject({ _tag: "Vcs.DiffError" })
expect(yield* vcs.diff("branch").pipe(Effect.flip)).toMatchObject({
_tag: "Vcs.DiffError",
message: "No review base available",
})
expect(yield* vcs.diff("branch", { base: "missing" }).pipe(Effect.flip)).toMatchObject({
_tag: "Vcs.DiffError",
})
expect(yield* vcs.diff("working", { base: "missing" })).toEqual([])
}),
),
)
it.live("diffs explicit remote-only bases without borrowing another fork's branch", () =>
withGit((directory) =>
Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.writeFile(path.join(directory, "file.txt"), "base\n")
await commitAll(directory, "initial")
await $`git remote add origin https://github.com/team/project.git`.cwd(directory).quiet()
await $`git remote add other https://github.com/other/project.git`.cwd(directory).quiet()
await $`git update-ref refs/remotes/other/release HEAD`.cwd(directory).quiet()
await fs.writeFile(path.join(directory, "release.txt"), "release parent\n")
await $`git checkout -b feature`.cwd(directory).quiet()
await commitAll(directory, "release parent")
await $`git update-ref refs/remotes/origin/release HEAD`.cwd(directory).quiet()
await fs.writeFile(path.join(directory, "feature.txt"), "feature\n")
await commitAll(directory, "feature")
})
const vcs = yield* Vcs.Service
expect((yield* vcs.diff("committed", { base: "refs/remotes/origin/release" })).map((row) => row.file)).toEqual([
"feature.txt",
])
expect((yield* vcs.diff("committed")).map((row) => row.file)).toEqual(["feature.txt", "release.txt"])
expect((yield* vcs.diff("committed", { base: "refs/remotes/other/release" })).map((row) => row.file)).toEqual([
"feature.txt",
"release.txt",
])
yield* Effect.promise(async () => {
await $`git update-ref -d refs/remotes/origin/release`.cwd(directory).quiet()
})
expect(yield* vcs.diff("committed", { base: "refs/remotes/origin/release" }).pipe(Effect.flip)).toMatchObject({
_tag: "Vcs.DiffError",
})
expect((yield* vcs.diff("committed", { base: "refs/remotes/other/release" })).map((row) => row.file)).toEqual([
"feature.txt",
"release.txt",
])
}),
),
)
for (const scenario of [
{ name: "failed", base: () => Effect.fail(new Error("provider failed")) },
{
name: "invalid",
base: () =>
Effect.succeed({
name: "release",
ref: "refs/heads/release",
source: "invalid" as const,
}),
},
]) {
it.live(`reports ${scenario.name} base metadata as unavailable rather than absent`, () =>
withTmp((directory) =>
Effect.gen(function* () {
const vcs = yield* Vcs.Service
yield* vcs.transform((draft) => {
// @ts-expect-error Invalid third-party wire metadata must be checked at runtime.
draft.add(provider({ base: scenario.base }))
draft.default.set("custom")
})
expect(yield* vcs.base().pipe(Effect.flip)).toMatchObject({ _tag: "Vcs.DiffError" })
}).pipe(provide(directory)),
),
)
}
it.live("reports provider failures and validates optional base metadata", () =>
withTmp((directory) =>
Effect.gen(function* () {
const vcs = yield* Vcs.Service
yield* vcs.transform((draft) => {
draft.add(
provider({
base: () => Effect.succeed({ name: "release", ref: "refs/heads/release", source: "reflog" }),
diff: () => Effect.fail(new Error("failed")),
}),
)
draft.default.set("custom")
})
expect((yield* vcs.base())?.source).toBe("reflog")
expect(yield* vcs.diff("committed").pipe(Effect.flip)).toMatchObject({ _tag: "Vcs.DiffError" })
}).pipe(provide(directory)),
),
)
it.live("reviews every feature commit and local changes against inferred v2, not the unrelated default", () =>
withGit((directory) =>
Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.writeFile(path.join(directory, "base.txt"), "base\n")
await commitAll(directory, "initial")
await $`git remote add origin https://example.com/team/repo.git`.cwd(directory).quiet()
await $`git update-ref refs/remotes/origin/dev HEAD`.cwd(directory).quiet()
await $`git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/dev`.cwd(directory).quiet()
await fs.writeFile(path.join(directory, "v2.txt"), "v2 parent\n")
await commitAll(directory, "v2")
await $`git update-ref refs/remotes/origin/v2 HEAD`.cwd(directory).quiet()
await $`git checkout -b feature origin/v2`.cwd(directory).quiet()
await $`git config branch.feature.opencode-merge-base refs/remotes/origin/dev`.cwd(directory).quiet()
await fs.writeFile(path.join(directory, "feature.txt"), "feature\n")
await commitAll(directory, "feature")
await fs.writeFile(path.join(directory, "second.txt"), "second\n")
await commitAll(directory, "second")
await fs.writeFile(path.join(directory, "third.txt"), "third\n")
await commitAll(directory, "third")
await fs.writeFile(path.join(directory, "untracked.txt"), "local\n")
})
const vcs = yield* Vcs.Service
const config = yield* Effect.promise(() => fs.readFile(path.join(directory, ".git/config"), "utf8"))
const base = yield* vcs.base()
expect(base).toEqual({ name: "v2", ref: "refs/remotes/origin/v2", source: "reflog" })
expect((yield* vcs.diff("committed", { base: base?.ref })).map((row) => row.file)).toEqual([
"feature.txt",
"second.txt",
"third.txt",
])
expect((yield* vcs.diff("branch", { base: base?.ref })).map((row) => row.file)).toEqual([
"feature.txt",
"second.txt",
"third.txt",
"untracked.txt",
])
expect((yield* vcs.diff("committed")).map((row) => row.file)).toEqual([
"feature.txt",
"second.txt",
"third.txt",
"v2.txt",
])
expect(yield* Effect.promise(() => fs.readFile(path.join(directory, ".git/config"), "utf8"))).toBe(config)
expect(
yield* Effect.promise(() => Bun.file(path.join(directory, ".git/opencode-review.json")).exists()),
).toBeFalse()
yield* Effect.promise(() => $`git config branch.feature.opencode-merge-base missing`.cwd(directory).quiet())
expect(yield* vcs.base()).toEqual({ name: "v2", ref: "refs/remotes/origin/v2", source: "reflog" })
yield* Effect.promise(() => $`git checkout -b uncertain HEAD`.cwd(directory).quiet())
expect(yield* vcs.base().pipe(Effect.flip)).toMatchObject({ message: "Choose a review base" })
}),
),
)
it.live("ignores stale, deleted, renamed, and self-mirroring creation hints", () =>
withGit((directory) =>
Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.writeFile(path.join(directory, "base.txt"), "base\n")
await commitAll(directory, "initial")
await $`git branch parent`.cwd(directory).quiet()
await $`git checkout -b feature parent`.cwd(directory).quiet()
})
const vcs = yield* Vcs.Service
expect((yield* vcs.base())?.source).toBe("reflog")
yield* Effect.promise(() => $`git branch -m renamed`.cwd(directory).quiet())
expect(yield* vcs.base().pipe(Effect.flip)).toMatchObject({ message: "Choose a review base" })
yield* Effect.promise(async () => {
await $`git checkout -b deleted parent`.cwd(directory).quiet()
await $`git branch -D parent`.cwd(directory).quiet()
})
expect(yield* vcs.base().pipe(Effect.flip)).toMatchObject({ message: "Choose a review base" })
yield* Effect.promise(async () => {
await $`git update-ref refs/remotes/origin/mirror HEAD`.cwd(directory).quiet()
await $`git checkout -b mirror origin/mirror`.cwd(directory).quiet()
})
expect(yield* vcs.base().pipe(Effect.flip)).toMatchObject({ message: "Choose a review base" })
yield* Effect.promise(async () => {
await $`git checkout -b stale main`.cwd(directory).quiet()
await $`git checkout --orphan unrelated`.cwd(directory).quiet()
await $`git commit -m unrelated`.cwd(directory).quiet()
await $`git branch -f main HEAD`.cwd(directory).quiet()
await $`git checkout stale`.cwd(directory).quiet()
})
expect(yield* vcs.base().pipe(Effect.flip)).toMatchObject({ message: "Choose a review base" })
yield* Effect.promise(async () => {
await $`git checkout -b rebased unrelated`.cwd(directory).quiet()
await $`git reset --soft stale`.cwd(directory).quiet()
})
expect(yield* vcs.base().pipe(Effect.flip)).toMatchObject({ message: "Choose a review base" })
}),
),
)
it.live("creates detached worktrees without review metadata and requires an explicit base without a named hint", () =>
withGit((directory) =>
Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.writeFile(path.join(directory, "base.txt"), "base\n")
await commitAll(directory, "initial")
await $`git remote add origin https://example.com/team/repo.git`.cwd(directory).quiet()
await $`git update-ref refs/remotes/origin/dev HEAD`.cwd(directory).quiet()
await $`git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/dev`.cwd(directory).quiet()
await fs.writeFile(path.join(directory, "v2.txt"), "v2\n")
await commitAll(directory, "v2")
await $`git update-ref refs/remotes/origin/v2 HEAD`.cwd(directory).quiet()
})
const git = yield* Git.Service
const repository = yield* git.repo.discover(AbsolutePath.make(directory))
if (!repository) throw new Error("Expected Git repository")
const linked = yield* git.worktree.create({
repository,
directory: AbsolutePath.make(path.join(directory, "linked")),
ref: "origin/v2",
})
expect(yield* git.history.branch(linked)).toBeUndefined()
expect(
yield* Effect.promise(() => Bun.file(path.join(linked.gitDirectory, "opencode-review.json")).exists()),
).toBeFalse()
yield* Effect.gen(function* () {
const vcs = yield* Vcs.Service
const context = host()
yield* VcsGitPlugin.Plugin.effect({
...context,
vcs: { ...context.vcs, transform: vcs.transform, reload: vcs.reload },
})
expect(yield* vcs.base().pipe(Effect.flip)).toMatchObject({ message: "Choose a review base" })
expect(yield* vcs.diff("committed", { base: "origin/v2" })).toEqual([])
expect((yield* vcs.diff("committed", { base: "origin/dev" })).map((row) => row.file)).toEqual(["v2.txt"])
yield* Effect.promise(() => $`git checkout -b child`.cwd(linked.worktree).quiet())
expect(yield* vcs.base().pipe(Effect.flip)).toMatchObject({ message: "Choose a review base" })
yield* Effect.promise(() => $`git checkout -b named origin/v2`.cwd(linked.worktree).quiet())
expect(yield* vcs.base()).toEqual({ name: "v2", ref: "refs/remotes/origin/v2", source: "reflog" })
}).pipe(provide(linked.worktree, { git: true }))
expect(
yield* Effect.promise(() => Bun.file(path.join(linked.gitDirectory, "opencode-review.json")).exists()),
).toBeFalse()
const fromHead = yield* git.worktree.create({
repository,
directory: AbsolutePath.make(path.join(directory, "from-head")),
})
expect(
yield* Effect.promise(() => Bun.file(path.join(fromHead.gitDirectory, "opencode-review.json")).exists()),
).toBeFalse()
const detached = yield* git.worktree.create({
repository: fromHead,
directory: AbsolutePath.make(path.join(directory, "detached-source")),
})
expect(
yield* Effect.promise(() => Bun.file(path.join(detached.gitDirectory, "opencode-review.json")).exists()),
).toBeFalse()
}),
),
)
it.live("rejects a branch creation anchor removed by rebase", () =>
withGit((directory) =>
Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.writeFile(path.join(directory, "base.txt"), "base\n")
await commitAll(directory, "initial")
await $`git checkout -b parent main`.cwd(directory).quiet()
await fs.writeFile(path.join(directory, "parent.txt"), "parent\n")
await commitAll(directory, "parent")
await $`git checkout -b feature parent`.cwd(directory).quiet()
await fs.writeFile(path.join(directory, "feature.txt"), "feature\n")
await commitAll(directory, "feature")
})
const vcs = yield* Vcs.Service
expect((yield* vcs.base())?.name).toBe("parent")
yield* Effect.promise(() => $`git rebase --onto main parent feature`.cwd(directory).quiet())
expect(yield* vcs.base().pipe(Effect.flip)).toMatchObject({ message: "Choose a review base" })
}),
),
)
it.live("does not infer beyond the bounded creation-reflog window", () =>
withGit((directory) =>
Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.writeFile(path.join(directory, "file.txt"), "base\n")
await commitAll(directory, "initial")
await $`git checkout -b feature main`.cwd(directory).quiet()
await fs.writeFile(path.join(directory, "file.txt"), "feature\n")
await commitAll(directory, "feature")
})
const vcs = yield* Vcs.Service
expect((yield* vcs.base())?.source).toBe("reflog")
yield* Effect.promise(async () => {
const before = (await $`git rev-parse main`.cwd(directory).text()).trim()
const after = (await $`git rev-parse HEAD`.cwd(directory).text()).trim()
const updates = Array.from(
{ length: 260 },
(_, index) => `start\nupdate refs/heads/feature ${index % 2 === 0 ? before : after}\nprepare\ncommit\n`,
).join("")
await $`git update-ref -m test --stdin < ${Buffer.from(updates)}`.cwd(directory).quiet()
})
expect(yield* vcs.base().pipe(Effect.flip)).toMatchObject({ message: "Choose a review base" })
}),
),
)
})