opencode/packages/core/test/vcs-backends.test.ts
2026-07-06 18:21:43 +05:30

158 lines
5.6 KiB
TypeScript

import { describe, expect } from "bun:test"
import { Cause, Effect, Exit, Fiber, Layer, Scope } from "effect"
import { Vcs as PluginVcs } from "@opencode-ai/plugin/v2/effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Location } from "@opencode-ai/core/location"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Vcs } from "@opencode-ai/core/vcs"
import { VcsBackends } from "@opencode-ai/core/vcs/backends"
import { location } from "./fixture/location"
import { it } from "./lib/effect"
const directory = AbsolutePath.make("/repo")
const provide = Effect.provide(
LayerNode.compile(LayerNode.group([Vcs.node, VcsBackends.node]), [
[
Location.node,
Layer.succeed(
Location.Service,
Location.Service.of(
location({ directory }, { vcs: { type: "fake", store: AbsolutePath.make("/repo/.fake") } }),
),
),
],
]),
)
const status = [{ file: "a.txt", additions: 1, deletions: 0, status: "added" as const }]
const diff = [{ file: "a.txt", patch: "+hello", additions: 1, deletions: 0, status: "added" as const }]
const backend = (overrides: Partial<PluginVcs.Adapter> = {}): PluginVcs.Backend => ({
type: "fake",
make: () => ({
status: () => Effect.succeed(status),
diff: () => Effect.succeed(diff),
...overrides,
}),
})
const register = (input: PluginVcs.Backend) =>
Effect.gen(function* () {
const backends = yield* VcsBackends.Service
return yield* backends.register(input)
})
describe("VcsBackends", () => {
it.live("serves status and diff through a registered backend", () =>
Effect.gen(function* () {
yield* register(backend())
const vcs = yield* Vcs.Service
expect(yield* vcs.status()).toEqual(status)
expect(yield* vcs.diff("working")).toEqual(diff)
}).pipe(Effect.scoped, provide),
)
it.live("passes normalized options and bounds plugin diffs", () =>
Effect.gen(function* () {
let received: Parameters<PluginVcs.Adapter["diff"]>[1] | undefined
yield* register(
backend({
diff: (_mode, options) => {
received = options
return Effect.succeed([{ ...diff[0], patch: "x".repeat(options.maxOutputBytes + 1) }])
},
}),
)
const vcs = yield* Vcs.Service
expect(yield* vcs.diff("working", { context: 3 })).toEqual([{ ...diff[0], patch: undefined }])
expect(received).toEqual({ context: 3, maxOutputBytes: 10_000_000 })
}).pipe(Effect.scoped, provide),
)
it.live("passes the location scope to the adapter factory", () =>
Effect.gen(function* () {
let scope: PluginVcs.AdapterScope | undefined
yield* register({
type: "fake",
make: (input) => {
scope = input
return backend().make(input)
},
})
yield* (yield* Vcs.Service).status()
expect(scope).toEqual({ directory: "/repo", worktree: "/repo", store: "/repo/.fake" })
}).pipe(Effect.scoped, provide),
)
it.live("returns empty results when no backend is registered", () =>
Effect.gen(function* () {
const vcs = yield* Vcs.Service
expect(yield* vcs.status()).toEqual([])
expect(yield* vcs.diff("working")).toEqual([])
}).pipe(Effect.scoped, provide),
)
it.live("frees the type when the registration scope closes", () =>
Effect.gen(function* () {
const backends = yield* VcsBackends.Service
const scope = yield* Scope.make()
yield* backends.register(backend()).pipe(Scope.provide(scope))
expect((yield* (yield* Vcs.Service).status()).length).toBe(1)
yield* Scope.close(scope, Exit.void)
expect(yield* (yield* Vcs.Service).status()).toEqual([])
yield* register(backend())
}).pipe(Effect.scoped, provide),
)
it.live("rejects duplicate and reserved types", () =>
Effect.gen(function* () {
yield* register(backend())
const duplicate = yield* register(backend()).pipe(Effect.exit)
expect(Exit.isFailure(duplicate)).toBe(true)
const reserved = yield* register({ ...backend(), type: "git" }).pipe(Effect.exit)
expect(Exit.isFailure(reserved)).toBe(true)
const invalid = yield* register({ ...backend(), type: "Not A Slug" }).pipe(Effect.exit)
expect(Exit.isFailure(invalid)).toBe(true)
}).pipe(Effect.scoped, provide),
)
it.live("degrades failing adapters to empty results", () =>
Effect.gen(function* () {
yield* register(
backend({
status: () => Effect.die(new Error("backend exploded")),
diff: () => Effect.sync(() => {
throw new Error("sync explosion")
}),
}),
)
const vcs = yield* Vcs.Service
expect(yield* vcs.status()).toEqual([])
expect(yield* vcs.diff("working")).toEqual([])
}).pipe(Effect.scoped, provide),
)
it.live("preserves adapter interruption", () =>
Effect.gen(function* () {
yield* register(backend({ status: () => Effect.never }))
const vcs = yield* Vcs.Service
const fiber = yield* Effect.forkChild(vcs.status())
yield* Fiber.interrupt(fiber)
const exit = yield* Fiber.await(fiber)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) expect(Cause.hasInterrupts(exit.cause)).toBe(true)
}).pipe(Effect.scoped, provide),
)
it.live("drops rows that fail schema validation", () =>
Effect.gen(function* () {
yield* register(
backend({
status: () => Effect.succeed([{ file: "a.txt", additions: -1, deletions: 0, status: "added" }]),
}),
)
expect(yield* (yield* Vcs.Service).status()).toEqual([])
}).pipe(Effect.scoped, provide),
)
})