158 lines
5.6 KiB
TypeScript
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),
|
|
)
|
|
})
|