import { afterEach, describe, expect } from "bun:test" import path from "path" import { Effect, Layer } from "effect" import { FSUtil } from "@opencode-ai/core/fs-util" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Global } from "@opencode-ai/core/global" import { Config } from "../../src/config/config" import { ConfigReference } from "../../src/config/reference" import { RuntimeFlags } from "../../src/effect/runtime-flags" import { Git } from "../../src/git" import { Reference } from "../../src/reference/reference" import { RepositoryCache } from "../../src/reference/repository-cache" import { disposeAllInstances, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture" import { testEffect } from "../lib/effect" afterEach(async () => { await disposeAllInstances() }) const referenceLayer = (flags: Partial = {}) => Reference.layer.pipe( Layer.provide(Config.defaultLayer), Layer.provide(RepositoryCache.defaultLayer), Layer.provide(RuntimeFlags.layer(flags)), ) const it = testEffect( Layer.mergeAll(FSUtil.defaultLayer, CrossSpawnSpawner.defaultLayer, Git.defaultLayer, referenceLayer()), ) const references = testEffect( Layer.mergeAll( FSUtil.defaultLayer, CrossSpawnSpawner.defaultLayer, Git.defaultLayer, referenceLayer({ experimentalReferences: true }), ), ) const githubBase = (url: string, self: Effect.Effect) => Effect.acquireUseRelease( Effect.sync(() => { const previous = process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL = url return previous }), () => self, (previous) => Effect.sync(() => { if (previous) process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL = previous else delete process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL }), ) const git = Effect.fn("ReferenceTest.git")(function* (cwd: string, args: string[]) { return yield* Effect.promise(async () => { const proc = Bun.spawn(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe", }) const [stdout, stderr, code] = await Promise.all([ new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited, ]) if (code !== 0) throw new Error(stderr.trim() || stdout.trim() || `git ${args.join(" ")} failed`) return stdout.trim() }) }) const waitForContent = ( fs: FSUtil.Interface, file: string, content: string, attempts = 50, ): Effect.Effect => Effect.gen(function* () { if ((yield* fs.readFileStringSafe(file)) === content) return if (attempts <= 0) throw new Error(`timed out waiting for ${file}`) yield* Effect.sleep("100 millis") yield* waitForContent(fs, file, content, attempts - 1) }) describe("reference", () => { it.live("resolves supported local and git config forms", () => Effect.gen(function* () { const root = path.resolve("opencode-reference-root") const local = Reference.resolve({ name: "docs", reference: ConfigReference.normalizeEntry({ path: "../docs" }), directory: path.join(root, "packages", "app"), worktree: root, }) const repo = Reference.resolve({ name: "effect", reference: ConfigReference.normalizeEntry({ repository: "Effect-TS/effect", branch: "main" }), directory: path.join(root, "packages", "app"), worktree: root, }) const localString = Reference.resolve({ name: "notes", reference: ConfigReference.normalizeEntry("./notes"), directory: path.join(root, "packages", "app"), worktree: root, }) const repoString = Reference.resolve({ name: "repo", reference: ConfigReference.normalizeEntry("owner/repo"), directory: path.join(root, "packages", "app"), worktree: root, }) expect(local.kind).toBe("local") if (local.kind === "local") expect(local.path).toBe(path.resolve(root, "../docs")) expect(localString.kind).toBe("local") if (localString.kind === "local") expect(localString.path).toBe(path.resolve(root, "notes")) expect(repo.kind).toBe("git") if (repo.kind === "git") { expect(repo.repository).toBe("Effect-TS/effect") expect(repo.branch).toBe("main") expect(repo.path).toBe(path.join(Global.Path.repos, "github.com", "Effect-TS", "effect")) } expect(repoString.kind).toBe("git") if (repoString.kind === "git") { expect(repoString.repository).toBe("owner/repo") expect(repoString.path).toBe(path.join(Global.Path.repos, "github.com", "owner", "repo")) } }), ) it.live("keeps invalid repository references visible without materializing", () => provideTmpdirInstance( (_dir) => Effect.gen(function* () { const reference = yield* Reference.Service const references = yield* reference.list() const invalid = yield* reference.get("bad") expect(references.map((item) => item.name)).toEqual(["bad"]) expect(invalid).toMatchObject({ name: "bad", kind: "invalid", repository: "not-a-repo", }) if (invalid?.kind === "invalid") expect(invalid.message).toContain("Repository must be a git URL") }), { config: { reference: { bad: "not-a-repo", }, }, }, ), ) it.live("marks same-cache references with different branches invalid", () => Effect.gen(function* () { const root = path.resolve("opencode-reference-root") const references = Reference.resolveAll({ directory: root, worktree: root, references: ConfigReference.normalize({ main: { repository: "owner/repo", branch: "main" }, dev: { repository: "github.com/owner/repo", branch: "dev" }, alsoMain: { repository: "https://github.com/owner/repo", branch: "main" }, }), }) expect(references.map((reference) => reference.kind)).toEqual(["git", "invalid", "git"]) expect(references[1]?.kind).toBe("invalid") if (references[1]?.kind === "invalid") { expect(references[1].message).toContain("conflicts with @main") expect(references[1].message).toContain("@dev requests dev") } }), ) it.live("represents invalid aliases as invalid references", () => Effect.gen(function* () { const root = path.resolve("opencode-reference-root") const references = Reference.resolveAll({ directory: root, worktree: root, references: ConfigReference.normalize({ "bad/name": "owner/repo", }), }) expect(references).toEqual([ { name: "bad/name", kind: "invalid", message: "Reference alias must not contain /, whitespace, comma, or backtick", }, ]) }), ) references.live("materializes configured git references during init", () => provideTmpdirInstance( (_dir) => Effect.gen(function* () { const fs = yield* FSUtil.Service const cache = path.join(Global.Path.repos, "github.com", "opencode-reference-test", "repo") yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore) yield* Effect.addFinalizer(() => fs.remove(cache, { recursive: true }).pipe(Effect.ignore)) const source = yield* tmpdirScoped({ git: true }) const remoteRoot = yield* tmpdirScoped() const remoteDir = path.join(remoteRoot, "opencode-reference-test") const remoteRepo = path.join(remoteDir, "repo.git") yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "configured\n")) yield* git(source, ["add", "."]) yield* git(source, ["commit", "-m", "add readme"]) yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie) yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo]) const reference = yield* Reference.Service yield* githubBase( `file://${remoteRoot}/`, Effect.gen(function* () { yield* reference.init() yield* waitForContent(fs, path.join(cache, "README.md"), "configured\n") }), ) expect(yield* fs.existsSafe(path.join(cache, ".git"))).toBe(true) expect(yield* fs.readFileString(path.join(cache, "README.md"))).toBe("configured\n") const resolved = yield* reference.get("docs") expect(resolved?.kind).toBe("git") if (resolved?.kind === "git") expect(resolved.path).toBe(cache) }), { config: { reference: { docs: "opencode-reference-test/repo", }, }, }, ), ) references.live("refreshes configured git references on new instance init", () => Effect.gen(function* () { const fs = yield* FSUtil.Service const cache = path.join(Global.Path.repos, "github.com", "opencode-reference-refresh", "repo") yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore) yield* Effect.addFinalizer(() => fs.remove(cache, { recursive: true }).pipe(Effect.ignore)) const source = yield* tmpdirScoped({ git: true }) const remoteRoot = yield* tmpdirScoped() const remoteDir = path.join(remoteRoot, "opencode-reference-refresh") const remoteRepo = path.join(remoteDir, "repo.git") yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v1\n")) yield* git(source, ["add", "."]) yield* git(source, ["commit", "-m", "add readme"]) yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie) yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo]) yield* githubBase( `file://${remoteRoot}/`, provideTmpdirInstance( (_dir) => Effect.gen(function* () { const reference = yield* Reference.Service yield* reference.init() yield* waitForContent(fs, path.join(cache, "README.md"), "v1\n") }), { config: { reference: { docs: "opencode-reference-refresh/repo", }, }, }, ), ) const branch = yield* git(source, ["branch", "--show-current"]) yield* git(source, ["remote", "add", "origin", remoteRepo]) yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v2\n")) yield* git(source, ["add", "."]) yield* git(source, ["commit", "-m", "update readme"]) yield* git(source, ["push", "origin", `${branch}:${branch}`]) yield* githubBase( `file://${remoteRoot}/`, provideTmpdirInstance( (_dir) => Effect.gen(function* () { const reference = yield* Reference.Service yield* reference.init() yield* waitForContent(fs, path.join(cache, "README.md"), "v2\n") }), { config: { reference: { docs: "opencode-reference-refresh/repo", }, }, }, ), ) }), ) })