import fs from "fs/promises" import path from "path" import { describe, expect, test } from "bun:test" import { Effect } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Global } from "@opencode-ai/core/global" import { SkillDiscovery } from "@opencode-ai/core/skill/discovery" import { tmpdir } from "./fixture/tmpdir" type Fixture = { tmp: Awaited> server: Bun.Server state: { skills: unknown[] files: Record requests: string[] } base: string } async function pull(skills: unknown[], files: Record = {}, fixture?: Fixture) { const state = fixture?.state ?? { skills, files, requests: [] } state.skills = skills state.files = files state.requests = [] const server = fixture?.server ?? Bun.serve({ port: 0, fetch(request) { state.requests.push(request.url) const pathname = new URL(request.url).pathname const body = pathname === "/catalog/index.json" ? JSON.stringify({ skills: state.skills }) : state.files[pathname] return new Response(body ?? "Not Found", { status: body === undefined ? 404 : 200 }) }, }) const tmp = fixture?.tmp ?? (await tmpdir()) const base = fixture?.base ?? new URL("/catalog/", server.url).href const skillDiscoveryLayer = AppNodeBuilder.build(SkillDiscovery.node, [ [Global.node, Global.layerWith({ cache: tmp.path })], ]) const directories = await Effect.runPromise( Effect.gen(function* () { return yield* (yield* SkillDiscovery.Service).pull(base) }).pipe(Effect.provide(skillDiscoveryLayer)), ) return { tmp, server, state, base, requests: state.requests, directories } } async function dispose(fixture: Fixture) { await fixture.server.stop(true) await fixture.tmp[Symbol.asyncDispose]() } describe("SkillDiscovery.pull", () => { test("rejects skill name traversal without fetching files", async () => { const result = await pull([{ name: "../outside", files: ["SKILL.md"] }]) try { expect(result.directories).toEqual([]) expect(result.requests).toEqual([`${result.base}index.json`]) expect(await fs.readdir(result.tmp.path)).toEqual([]) } finally { await dispose(result) } }) test("rejects file traversal without fetching files", async () => { const result = await pull([{ name: "deploy", files: ["SKILL.md", "../outside.md"] }]) try { expect(result.directories).toEqual([]) expect(result.requests).toEqual([`${result.base}index.json`]) expect(await fs.readdir(result.tmp.path)).toEqual([]) } finally { await dispose(result) } }) test("rejects absolute file paths without fetching files", async () => { const result = await pull([{ name: "deploy", files: ["SKILL.md", "/tmp/outside.md"] }]) try { expect(result.directories).toEqual([]) expect(result.requests).toEqual([`${result.base}index.json`]) expect(await fs.readdir(result.tmp.path)).toEqual([]) } finally { await dispose(result) } }) test("rejects cross-origin file URLs without fetching files", async () => { const result = await pull([{ name: "deploy", files: ["SKILL.md", "https://evil.example.test/outside.md"] }]) try { expect(result.directories).toEqual([]) expect(result.requests).toEqual([`${result.base}index.json`]) expect(await fs.readdir(result.tmp.path)).toEqual([]) } finally { await dispose(result) } }) test("downloads safe nested files under the skill root", async () => { const result = await pull([{ name: "deploy", files: ["SKILL.md", "references/guide.md"] }], { "/catalog/deploy/SKILL.md": "# Deploy", "/catalog/deploy/references/guide.md": "# Guide", }) try { expect(result.directories).toHaveLength(1) expect(result.requests.toSorted()).toEqual( [ `${result.base}index.json`, `${result.base}deploy/SKILL.md`, `${result.base}deploy/references/guide.md`, ].toSorted(), ) expect(await fs.readFile(path.join(result.directories[0], "SKILL.md"), "utf8")).toBe("# Deploy") expect(await fs.readFile(path.join(result.directories[0], "references", "guide.md"), "utf8")).toBe("# Guide") } finally { await dispose(result) } }) test("refreshes cached files when the version changes", async () => { const first = await pull([{ name: "deploy", version: "1", files: ["SKILL.md"] }], { "/catalog/deploy/SKILL.md": "# Old", }) try { const second = await pull( [{ name: "deploy", version: "2", files: ["SKILL.md"] }], { "/catalog/deploy/SKILL.md": "# New" }, first, ) expect(await fs.readFile(path.join(first.directories[0], "SKILL.md"), "utf8")).toBe("# New") expect(second.requests).toContain(`${first.base}deploy/SKILL.md`) const third = await pull( [{ name: "deploy", version: "2", files: ["SKILL.md"] }], { "/catalog/deploy/SKILL.md": "# Ignored" }, first, ) expect(third.requests).toEqual([`${first.base}index.json`]) } finally { await dispose(first) } }) test("publishes complete updates and removes stale files", async () => { const first = await pull([{ name: "deploy", version: "1", files: ["SKILL.md", "old.md"] }], { "/catalog/deploy/SKILL.md": "# Old", "/catalog/deploy/old.md": "old reference", }) try { const root = first.directories[0] await pull( [{ name: "deploy", version: "2", files: ["SKILL.md", "missing.md"] }], { "/catalog/deploy/SKILL.md": "# Partial" }, first, ) expect(await fs.readFile(path.join(root, "SKILL.md"), "utf8")).toBe("# Old") expect(await fs.readFile(path.join(root, "old.md"), "utf8")).toBe("old reference") await pull( [{ name: "deploy", version: "3", files: ["SKILL.md"] }], { "/catalog/deploy/SKILL.md": "# New" }, first, ) expect(await fs.readFile(path.join(root, "SKILL.md"), "utf8")).toBe("# New") expect(await Bun.file(path.join(root, "old.md")).exists()).toBe(false) } finally { await dispose(first) } }) })