import fs from "fs/promises" import path from "path" import { describe, expect, test } from "bun:test" import { Effect, Layer } from "effect" import { HttpClient, HttpClientResponse } from "effect/unstable/http" import { FSUtil } from "@opencode-ai/core/fs-util" import { Global } from "@opencode-ai/core/global" import { SkillDiscovery } from "@opencode-ai/core/skill/discovery" import { tmpdir } from "./fixture/tmpdir" const base = "https://skills.example.test/catalog/" async function pull(skills: unknown[], files: Record = {}) { const tmp = await tmpdir() const requests: string[] = [] const http = Layer.succeed( HttpClient.HttpClient, HttpClient.make((request) => Effect.sync(() => requests.push(request.url)).pipe( Effect.map(() => { const body = request.url === `${base}index.json` ? JSON.stringify({ skills }) : files[request.url] return HttpClientResponse.fromWeb( request, new Response(body ?? "Not Found", { status: body === undefined ? 404 : 200 }), ) }), ), ), ) const layer = SkillDiscovery.layer.pipe( Layer.provide(http), Layer.provide(FSUtil.defaultLayer), Layer.provide(Global.layerWith({ cache: tmp.path })), ) const directories = await Effect.runPromise( Effect.gen(function* () { return yield* (yield* SkillDiscovery.Service).pull(base) }).pipe(Effect.provide(layer)), ) return { tmp, requests, directories } } 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([`${base}index.json`]) expect(await fs.readdir(result.tmp.path)).toEqual([]) } finally { await result.tmp[Symbol.asyncDispose]() } }) 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([`${base}index.json`]) expect(await fs.readdir(result.tmp.path)).toEqual([]) } finally { await result.tmp[Symbol.asyncDispose]() } }) 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([`${base}index.json`]) expect(await fs.readdir(result.tmp.path)).toEqual([]) } finally { await result.tmp[Symbol.asyncDispose]() } }) 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([`${base}index.json`]) expect(await fs.readdir(result.tmp.path)).toEqual([]) } finally { await result.tmp[Symbol.asyncDispose]() } }) test("downloads safe nested files under the skill root", async () => { const result = await pull([{ name: "deploy", files: ["SKILL.md", "references/guide.md"] }], { [`${base}deploy/SKILL.md`]: "# Deploy", [`${base}deploy/references/guide.md`]: "# Guide", }) try { expect(result.directories).toHaveLength(1) expect(result.requests.toSorted()).toEqual( [`${base}index.json`, `${base}deploy/SKILL.md`, `${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 result.tmp[Symbol.asyncDispose]() } }) })