Skip to content

Commit 391aa38

Browse files
authored
test(core): serve skill fixtures locally (#35429)
1 parent 29e8502 commit 391aa38

1 file changed

Lines changed: 77 additions & 66 deletions

File tree

Lines changed: 77 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,165 +1,176 @@
11
import fs from "fs/promises"
22
import path from "path"
33
import { describe, expect, test } from "bun:test"
4-
import { Effect, Layer } from "effect"
5-
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
4+
import { Effect } from "effect"
65
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
7-
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
8-
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
96
import { Global } from "@opencode-ai/core/global"
107
import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
118
import { tmpdir } from "./fixture/tmpdir"
129

13-
const base = "https://skills.example.test/catalog/"
10+
type Fixture = {
11+
tmp: Awaited<ReturnType<typeof tmpdir>>
12+
server: Bun.Server<undefined>
13+
state: {
14+
skills: unknown[]
15+
files: Record<string, string>
16+
requests: string[]
17+
}
18+
base: string
19+
}
1420

15-
async function pull(skills: unknown[], files: Record<string, string> = {}, cache?: Awaited<ReturnType<typeof tmpdir>>) {
16-
const tmp = cache ?? (await tmpdir())
17-
const requests: string[] = []
18-
const http = Layer.succeed(
19-
HttpClient.HttpClient,
20-
HttpClient.make((request) =>
21-
Effect.sync(() => requests.push(request.url)).pipe(
22-
Effect.map(() => {
23-
const body = request.url === `${base}index.json` ? JSON.stringify({ skills }) : files[request.url]
24-
return HttpClientResponse.fromWeb(
25-
request,
26-
new Response(body ?? "Not Found", { status: body === undefined ? 404 : 200 }),
27-
)
28-
}),
29-
),
30-
),
31-
)
21+
async function pull(skills: unknown[], files: Record<string, string> = {}, fixture?: Fixture) {
22+
const state = fixture?.state ?? { skills, files, requests: [] }
23+
state.skills = skills
24+
state.files = files
25+
state.requests = []
26+
const server =
27+
fixture?.server ??
28+
Bun.serve({
29+
port: 0,
30+
fetch(request) {
31+
state.requests.push(request.url)
32+
const pathname = new URL(request.url).pathname
33+
const body = pathname === "/catalog/index.json" ? JSON.stringify({ skills: state.skills }) : state.files[pathname]
34+
return new Response(body ?? "Not Found", { status: body === undefined ? 404 : 200 })
35+
},
36+
})
37+
const tmp = fixture?.tmp ?? (await tmpdir())
38+
const base = fixture?.base ?? new URL("/catalog/", server.url).href
3239
const skillDiscoveryLayer = AppNodeBuilder.build(SkillDiscovery.node, [
33-
[LayerNodePlatform.httpClient, http],
3440
[Global.node, Global.layerWith({ cache: tmp.path })],
3541
])
3642
const directories = await Effect.runPromise(
3743
Effect.gen(function* () {
3844
return yield* (yield* SkillDiscovery.Service).pull(base)
3945
}).pipe(Effect.provide(skillDiscoveryLayer)),
4046
)
41-
return { tmp, requests, directories }
47+
return { tmp, server, state, base, requests: state.requests, directories }
48+
}
49+
50+
async function dispose(fixture: Fixture) {
51+
await fixture.server.stop(true)
52+
await fixture.tmp[Symbol.asyncDispose]()
4253
}
4354

4455
describe("SkillDiscovery.pull", () => {
4556
test("rejects skill name traversal without fetching files", async () => {
4657
const result = await pull([{ name: "../outside", files: ["SKILL.md"] }])
4758
try {
4859
expect(result.directories).toEqual([])
49-
expect(result.requests).toEqual([`${base}index.json`])
60+
expect(result.requests).toEqual([`${result.base}index.json`])
5061
expect(await fs.readdir(result.tmp.path)).toEqual([])
5162
} finally {
52-
await result.tmp[Symbol.asyncDispose]()
63+
await dispose(result)
5364
}
5465
})
5566

5667
test("rejects file traversal without fetching files", async () => {
5768
const result = await pull([{ name: "deploy", files: ["SKILL.md", "../outside.md"] }])
5869
try {
5970
expect(result.directories).toEqual([])
60-
expect(result.requests).toEqual([`${base}index.json`])
71+
expect(result.requests).toEqual([`${result.base}index.json`])
6172
expect(await fs.readdir(result.tmp.path)).toEqual([])
6273
} finally {
63-
await result.tmp[Symbol.asyncDispose]()
74+
await dispose(result)
6475
}
6576
})
6677

6778
test("rejects absolute file paths without fetching files", async () => {
6879
const result = await pull([{ name: "deploy", files: ["SKILL.md", "/tmp/outside.md"] }])
6980
try {
7081
expect(result.directories).toEqual([])
71-
expect(result.requests).toEqual([`${base}index.json`])
82+
expect(result.requests).toEqual([`${result.base}index.json`])
7283
expect(await fs.readdir(result.tmp.path)).toEqual([])
7384
} finally {
74-
await result.tmp[Symbol.asyncDispose]()
85+
await dispose(result)
7586
}
7687
})
7788

7889
test("rejects cross-origin file URLs without fetching files", async () => {
7990
const result = await pull([{ name: "deploy", files: ["SKILL.md", "https://evil.example.test/outside.md"] }])
8091
try {
8192
expect(result.directories).toEqual([])
82-
expect(result.requests).toEqual([`${base}index.json`])
93+
expect(result.requests).toEqual([`${result.base}index.json`])
8394
expect(await fs.readdir(result.tmp.path)).toEqual([])
8495
} finally {
85-
await result.tmp[Symbol.asyncDispose]()
96+
await dispose(result)
8697
}
8798
})
8899

89100
test("downloads safe nested files under the skill root", async () => {
90101
const result = await pull([{ name: "deploy", files: ["SKILL.md", "references/guide.md"] }], {
91-
[`${base}deploy/SKILL.md`]: "# Deploy",
92-
[`${base}deploy/references/guide.md`]: "# Guide",
102+
"/catalog/deploy/SKILL.md": "# Deploy",
103+
"/catalog/deploy/references/guide.md": "# Guide",
93104
})
94105
try {
95106
expect(result.directories).toHaveLength(1)
96107
expect(result.requests.toSorted()).toEqual(
97-
[`${base}index.json`, `${base}deploy/SKILL.md`, `${base}deploy/references/guide.md`].toSorted(),
108+
[
109+
`${result.base}index.json`,
110+
`${result.base}deploy/SKILL.md`,
111+
`${result.base}deploy/references/guide.md`,
112+
].toSorted(),
98113
)
99114
expect(await fs.readFile(path.join(result.directories[0], "SKILL.md"), "utf8")).toBe("# Deploy")
100115
expect(await fs.readFile(path.join(result.directories[0], "references", "guide.md"), "utf8")).toBe("# Guide")
101116
} finally {
102-
await result.tmp[Symbol.asyncDispose]()
117+
await dispose(result)
103118
}
104119
})
105120

106121
test("refreshes cached files when the version changes", async () => {
107-
const tmp = await tmpdir()
122+
const first = await pull(
123+
[{ name: "deploy", version: "1", files: ["SKILL.md"] }],
124+
{ "/catalog/deploy/SKILL.md": "# Old" },
125+
)
108126
try {
109-
const first = await pull(
110-
[{ name: "deploy", version: "1", files: ["SKILL.md"] }],
111-
{
112-
[`${base}deploy/SKILL.md`]: "# Old",
113-
},
114-
tmp,
115-
)
116127
const second = await pull(
117128
[{ name: "deploy", version: "2", files: ["SKILL.md"] }],
118-
{
119-
[`${base}deploy/SKILL.md`]: "# New",
120-
},
121-
tmp,
129+
{ "/catalog/deploy/SKILL.md": "# New" },
130+
first,
122131
)
123132

124133
expect(await fs.readFile(path.join(first.directories[0], "SKILL.md"), "utf8")).toBe("# New")
125-
expect(second.requests).toContain(`${base}deploy/SKILL.md`)
134+
expect(second.requests).toContain(`${first.base}deploy/SKILL.md`)
126135
const third = await pull(
127136
[{ name: "deploy", version: "2", files: ["SKILL.md"] }],
128-
{ [`${base}deploy/SKILL.md`]: "# Ignored" },
129-
tmp,
137+
{ "/catalog/deploy/SKILL.md": "# Ignored" },
138+
first,
130139
)
131-
expect(third.requests).toEqual([`${base}index.json`])
140+
expect(third.requests).toEqual([`${first.base}index.json`])
132141
} finally {
133-
await tmp[Symbol.asyncDispose]()
142+
await dispose(first)
134143
}
135144
})
136145

137146
test("publishes complete updates and removes stale files", async () => {
138-
const tmp = await tmpdir()
147+
const first = await pull(
148+
[{ name: "deploy", version: "1", files: ["SKILL.md", "old.md"] }],
149+
{
150+
"/catalog/deploy/SKILL.md": "# Old",
151+
"/catalog/deploy/old.md": "old reference",
152+
},
153+
)
139154
try {
140-
const first = await pull(
141-
[{ name: "deploy", version: "1", files: ["SKILL.md", "old.md"] }],
142-
{
143-
[`${base}deploy/SKILL.md`]: "# Old",
144-
[`${base}deploy/old.md`]: "old reference",
145-
},
146-
tmp,
147-
)
148155
const root = first.directories[0]
149156

150157
await pull(
151158
[{ name: "deploy", version: "2", files: ["SKILL.md", "missing.md"] }],
152-
{ [`${base}deploy/SKILL.md`]: "# Partial" },
153-
tmp,
159+
{ "/catalog/deploy/SKILL.md": "# Partial" },
160+
first,
154161
)
155162
expect(await fs.readFile(path.join(root, "SKILL.md"), "utf8")).toBe("# Old")
156163
expect(await fs.readFile(path.join(root, "old.md"), "utf8")).toBe("old reference")
157164

158-
await pull([{ name: "deploy", version: "3", files: ["SKILL.md"] }], { [`${base}deploy/SKILL.md`]: "# New" }, tmp)
165+
await pull(
166+
[{ name: "deploy", version: "3", files: ["SKILL.md"] }],
167+
{ "/catalog/deploy/SKILL.md": "# New" },
168+
first,
169+
)
159170
expect(await fs.readFile(path.join(root, "SKILL.md"), "utf8")).toBe("# New")
160171
expect(await Bun.file(path.join(root, "old.md")).exists()).toBe(false)
161172
} finally {
162-
await tmp[Symbol.asyncDispose]()
173+
await dispose(first)
163174
}
164175
})
165176
})

0 commit comments

Comments
 (0)