| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 |
- 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/util/global"
- import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
- import { tmpdir } from "./fixture/tmpdir"
- type Fixture = {
- tmp: Awaited<ReturnType<typeof tmpdir>>
- server: Bun.Server<undefined>
- state: {
- skills: unknown[]
- files: Record<string, string>
- requests: string[]
- }
- base: string
- }
- async function pull(skills: unknown[], files: Record<string, string> = {}, 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)
- }
- })
- })
|