test(core): serve skill fixtures locally (#35429)
This commit is contained in:
parent
29e8502bb0
commit
391aa38281
1 changed files with 77 additions and 66 deletions
|
|
@ -1,36 +1,42 @@
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect } from "effect"
|
||||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
|
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
|
import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
|
|
||||||
const base = "https://skills.example.test/catalog/"
|
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> = {}, cache?: Awaited<ReturnType<typeof tmpdir>>) {
|
async function pull(skills: unknown[], files: Record<string, string> = {}, fixture?: Fixture) {
|
||||||
const tmp = cache ?? (await tmpdir())
|
const state = fixture?.state ?? { skills, files, requests: [] }
|
||||||
const requests: string[] = []
|
state.skills = skills
|
||||||
const http = Layer.succeed(
|
state.files = files
|
||||||
HttpClient.HttpClient,
|
state.requests = []
|
||||||
HttpClient.make((request) =>
|
const server =
|
||||||
Effect.sync(() => requests.push(request.url)).pipe(
|
fixture?.server ??
|
||||||
Effect.map(() => {
|
Bun.serve({
|
||||||
const body = request.url === `${base}index.json` ? JSON.stringify({ skills }) : files[request.url]
|
port: 0,
|
||||||
return HttpClientResponse.fromWeb(
|
fetch(request) {
|
||||||
request,
|
state.requests.push(request.url)
|
||||||
new Response(body ?? "Not Found", { status: body === undefined ? 404 : 200 }),
|
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, [
|
const skillDiscoveryLayer = AppNodeBuilder.build(SkillDiscovery.node, [
|
||||||
[LayerNodePlatform.httpClient, http],
|
|
||||||
[Global.node, Global.layerWith({ cache: tmp.path })],
|
[Global.node, Global.layerWith({ cache: tmp.path })],
|
||||||
])
|
])
|
||||||
const directories = await Effect.runPromise(
|
const directories = await Effect.runPromise(
|
||||||
|
|
@ -38,7 +44,12 @@ async function pull(skills: unknown[], files: Record<string, string> = {}, cache
|
||||||
return yield* (yield* SkillDiscovery.Service).pull(base)
|
return yield* (yield* SkillDiscovery.Service).pull(base)
|
||||||
}).pipe(Effect.provide(skillDiscoveryLayer)),
|
}).pipe(Effect.provide(skillDiscoveryLayer)),
|
||||||
)
|
)
|
||||||
return { tmp, requests, directories }
|
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", () => {
|
describe("SkillDiscovery.pull", () => {
|
||||||
|
|
@ -46,10 +57,10 @@ describe("SkillDiscovery.pull", () => {
|
||||||
const result = await pull([{ name: "../outside", files: ["SKILL.md"] }])
|
const result = await pull([{ name: "../outside", files: ["SKILL.md"] }])
|
||||||
try {
|
try {
|
||||||
expect(result.directories).toEqual([])
|
expect(result.directories).toEqual([])
|
||||||
expect(result.requests).toEqual([`${base}index.json`])
|
expect(result.requests).toEqual([`${result.base}index.json`])
|
||||||
expect(await fs.readdir(result.tmp.path)).toEqual([])
|
expect(await fs.readdir(result.tmp.path)).toEqual([])
|
||||||
} finally {
|
} finally {
|
||||||
await result.tmp[Symbol.asyncDispose]()
|
await dispose(result)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -57,10 +68,10 @@ describe("SkillDiscovery.pull", () => {
|
||||||
const result = await pull([{ name: "deploy", files: ["SKILL.md", "../outside.md"] }])
|
const result = await pull([{ name: "deploy", files: ["SKILL.md", "../outside.md"] }])
|
||||||
try {
|
try {
|
||||||
expect(result.directories).toEqual([])
|
expect(result.directories).toEqual([])
|
||||||
expect(result.requests).toEqual([`${base}index.json`])
|
expect(result.requests).toEqual([`${result.base}index.json`])
|
||||||
expect(await fs.readdir(result.tmp.path)).toEqual([])
|
expect(await fs.readdir(result.tmp.path)).toEqual([])
|
||||||
} finally {
|
} finally {
|
||||||
await result.tmp[Symbol.asyncDispose]()
|
await dispose(result)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -68,10 +79,10 @@ describe("SkillDiscovery.pull", () => {
|
||||||
const result = await pull([{ name: "deploy", files: ["SKILL.md", "/tmp/outside.md"] }])
|
const result = await pull([{ name: "deploy", files: ["SKILL.md", "/tmp/outside.md"] }])
|
||||||
try {
|
try {
|
||||||
expect(result.directories).toEqual([])
|
expect(result.directories).toEqual([])
|
||||||
expect(result.requests).toEqual([`${base}index.json`])
|
expect(result.requests).toEqual([`${result.base}index.json`])
|
||||||
expect(await fs.readdir(result.tmp.path)).toEqual([])
|
expect(await fs.readdir(result.tmp.path)).toEqual([])
|
||||||
} finally {
|
} finally {
|
||||||
await result.tmp[Symbol.asyncDispose]()
|
await dispose(result)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -79,87 +90,87 @@ describe("SkillDiscovery.pull", () => {
|
||||||
const result = await pull([{ name: "deploy", files: ["SKILL.md", "https://evil.example.test/outside.md"] }])
|
const result = await pull([{ name: "deploy", files: ["SKILL.md", "https://evil.example.test/outside.md"] }])
|
||||||
try {
|
try {
|
||||||
expect(result.directories).toEqual([])
|
expect(result.directories).toEqual([])
|
||||||
expect(result.requests).toEqual([`${base}index.json`])
|
expect(result.requests).toEqual([`${result.base}index.json`])
|
||||||
expect(await fs.readdir(result.tmp.path)).toEqual([])
|
expect(await fs.readdir(result.tmp.path)).toEqual([])
|
||||||
} finally {
|
} finally {
|
||||||
await result.tmp[Symbol.asyncDispose]()
|
await dispose(result)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("downloads safe nested files under the skill root", async () => {
|
test("downloads safe nested files under the skill root", async () => {
|
||||||
const result = await pull([{ name: "deploy", files: ["SKILL.md", "references/guide.md"] }], {
|
const result = await pull([{ name: "deploy", files: ["SKILL.md", "references/guide.md"] }], {
|
||||||
[`${base}deploy/SKILL.md`]: "# Deploy",
|
"/catalog/deploy/SKILL.md": "# Deploy",
|
||||||
[`${base}deploy/references/guide.md`]: "# Guide",
|
"/catalog/deploy/references/guide.md": "# Guide",
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
expect(result.directories).toHaveLength(1)
|
expect(result.directories).toHaveLength(1)
|
||||||
expect(result.requests.toSorted()).toEqual(
|
expect(result.requests.toSorted()).toEqual(
|
||||||
[`${base}index.json`, `${base}deploy/SKILL.md`, `${base}deploy/references/guide.md`].toSorted(),
|
[
|
||||||
|
`${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], "SKILL.md"), "utf8")).toBe("# Deploy")
|
||||||
expect(await fs.readFile(path.join(result.directories[0], "references", "guide.md"), "utf8")).toBe("# Guide")
|
expect(await fs.readFile(path.join(result.directories[0], "references", "guide.md"), "utf8")).toBe("# Guide")
|
||||||
} finally {
|
} finally {
|
||||||
await result.tmp[Symbol.asyncDispose]()
|
await dispose(result)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("refreshes cached files when the version changes", async () => {
|
test("refreshes cached files when the version changes", async () => {
|
||||||
const tmp = await tmpdir()
|
const first = await pull(
|
||||||
|
[{ name: "deploy", version: "1", files: ["SKILL.md"] }],
|
||||||
|
{ "/catalog/deploy/SKILL.md": "# Old" },
|
||||||
|
)
|
||||||
try {
|
try {
|
||||||
const first = await pull(
|
|
||||||
[{ name: "deploy", version: "1", files: ["SKILL.md"] }],
|
|
||||||
{
|
|
||||||
[`${base}deploy/SKILL.md`]: "# Old",
|
|
||||||
},
|
|
||||||
tmp,
|
|
||||||
)
|
|
||||||
const second = await pull(
|
const second = await pull(
|
||||||
[{ name: "deploy", version: "2", files: ["SKILL.md"] }],
|
[{ name: "deploy", version: "2", files: ["SKILL.md"] }],
|
||||||
{
|
{ "/catalog/deploy/SKILL.md": "# New" },
|
||||||
[`${base}deploy/SKILL.md`]: "# New",
|
first,
|
||||||
},
|
|
||||||
tmp,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(await fs.readFile(path.join(first.directories[0], "SKILL.md"), "utf8")).toBe("# New")
|
expect(await fs.readFile(path.join(first.directories[0], "SKILL.md"), "utf8")).toBe("# New")
|
||||||
expect(second.requests).toContain(`${base}deploy/SKILL.md`)
|
expect(second.requests).toContain(`${first.base}deploy/SKILL.md`)
|
||||||
const third = await pull(
|
const third = await pull(
|
||||||
[{ name: "deploy", version: "2", files: ["SKILL.md"] }],
|
[{ name: "deploy", version: "2", files: ["SKILL.md"] }],
|
||||||
{ [`${base}deploy/SKILL.md`]: "# Ignored" },
|
{ "/catalog/deploy/SKILL.md": "# Ignored" },
|
||||||
tmp,
|
first,
|
||||||
)
|
)
|
||||||
expect(third.requests).toEqual([`${base}index.json`])
|
expect(third.requests).toEqual([`${first.base}index.json`])
|
||||||
} finally {
|
} finally {
|
||||||
await tmp[Symbol.asyncDispose]()
|
await dispose(first)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("publishes complete updates and removes stale files", async () => {
|
test("publishes complete updates and removes stale files", async () => {
|
||||||
const tmp = await tmpdir()
|
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 {
|
try {
|
||||||
const first = await pull(
|
|
||||||
[{ name: "deploy", version: "1", files: ["SKILL.md", "old.md"] }],
|
|
||||||
{
|
|
||||||
[`${base}deploy/SKILL.md`]: "# Old",
|
|
||||||
[`${base}deploy/old.md`]: "old reference",
|
|
||||||
},
|
|
||||||
tmp,
|
|
||||||
)
|
|
||||||
const root = first.directories[0]
|
const root = first.directories[0]
|
||||||
|
|
||||||
await pull(
|
await pull(
|
||||||
[{ name: "deploy", version: "2", files: ["SKILL.md", "missing.md"] }],
|
[{ name: "deploy", version: "2", files: ["SKILL.md", "missing.md"] }],
|
||||||
{ [`${base}deploy/SKILL.md`]: "# Partial" },
|
{ "/catalog/deploy/SKILL.md": "# Partial" },
|
||||||
tmp,
|
first,
|
||||||
)
|
)
|
||||||
expect(await fs.readFile(path.join(root, "SKILL.md"), "utf8")).toBe("# Old")
|
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")
|
expect(await fs.readFile(path.join(root, "old.md"), "utf8")).toBe("old reference")
|
||||||
|
|
||||||
await pull([{ name: "deploy", version: "3", files: ["SKILL.md"] }], { [`${base}deploy/SKILL.md`]: "# New" }, tmp)
|
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 fs.readFile(path.join(root, "SKILL.md"), "utf8")).toBe("# New")
|
||||||
expect(await Bun.file(path.join(root, "old.md")).exists()).toBe(false)
|
expect(await Bun.file(path.join(root, "old.md")).exists()).toBe(false)
|
||||||
} finally {
|
} finally {
|
||||||
await tmp[Symbol.asyncDispose]()
|
await dispose(first)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue