130 lines
4.6 KiB
TypeScript
130 lines
4.6 KiB
TypeScript
import fs from "fs/promises"
|
|
import path from "path"
|
|
import { describe, expect } from "bun:test"
|
|
import { Effect, Layer } from "effect"
|
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
|
import { FSUtil } from "@opencode-ai/core/fs-util"
|
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
|
import { SkillV2 } from "@opencode-ai/core/skill"
|
|
import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
|
|
import { tmpdir } from "./fixture/tmpdir"
|
|
import { testEffect } from "./lib/effect"
|
|
|
|
const urls = new Map<string, AbsolutePath[]>()
|
|
let pulls = 0
|
|
const discovery = Layer.succeed(
|
|
SkillDiscovery.Service,
|
|
SkillDiscovery.Service.of({
|
|
pull: (url) => {
|
|
pulls++
|
|
return Effect.succeed(urls.get(url) ?? [])
|
|
},
|
|
}),
|
|
)
|
|
const it = testEffect(
|
|
SkillV2.layer.pipe(
|
|
Layer.provide(discovery),
|
|
Layer.provide(FSUtil.defaultLayer),
|
|
Layer.provideMerge(AgentV2.locationLayer),
|
|
),
|
|
)
|
|
|
|
function write(directory: string, name: string, description: string) {
|
|
return fs.writeFile(
|
|
path.join(directory, name, "SKILL.md"),
|
|
`---
|
|
name: ${name}
|
|
description: ${description}
|
|
---
|
|
# ${name}`,
|
|
)
|
|
}
|
|
|
|
describe("SkillV2", () => {
|
|
it.live("registers sources and resolves later source precedence", () =>
|
|
Effect.acquireRelease(
|
|
Effect.promise(() => tmpdir()),
|
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
|
).pipe(
|
|
Effect.flatMap((tmp) =>
|
|
Effect.gen(function* () {
|
|
const first = path.join(tmp.path, "first")
|
|
const second = path.join(tmp.path, "second")
|
|
yield* Effect.promise(async () => {
|
|
await fs.mkdir(path.join(first, "review"), { recursive: true })
|
|
await fs.mkdir(path.join(second, "review"), { recursive: true })
|
|
await write(first, "review", "First")
|
|
await write(second, "review", "Second")
|
|
await fs.writeFile(path.join(first, "foo.md"), "---\nslash: true\n---\n# foo")
|
|
})
|
|
|
|
const skill = yield* SkillV2.Service
|
|
const register = yield* skill.transform()
|
|
yield* register((editor) => {
|
|
editor.source({ type: "directory", path: AbsolutePath.make(first) })
|
|
editor.source({ type: "directory", path: AbsolutePath.make(first) })
|
|
editor.source({ type: "directory", path: AbsolutePath.make(second) })
|
|
expect(editor.list()).toEqual([
|
|
{ type: "directory", path: AbsolutePath.make(first) },
|
|
{ type: "directory", path: AbsolutePath.make(second) },
|
|
])
|
|
})
|
|
|
|
expect(yield* skill.sources()).toEqual([
|
|
{ type: "directory", path: AbsolutePath.make(first) },
|
|
{ type: "directory", path: AbsolutePath.make(second) },
|
|
])
|
|
expect(yield* skill.list()).toEqual([
|
|
new SkillV2.Info({
|
|
name: "foo",
|
|
slash: true,
|
|
location: AbsolutePath.make(path.join(first, "foo.md")),
|
|
content: "# foo",
|
|
}),
|
|
{
|
|
name: "review",
|
|
description: "Second",
|
|
location: AbsolutePath.make(path.join(second, "review", "SKILL.md")),
|
|
content: "# review",
|
|
},
|
|
])
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
|
|
it.live("loads URL sources and filters skills for agents", () =>
|
|
Effect.acquireRelease(
|
|
Effect.promise(() => tmpdir()),
|
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
|
).pipe(
|
|
Effect.flatMap((tmp) =>
|
|
Effect.gen(function* () {
|
|
yield* Effect.promise(async () => {
|
|
await fs.mkdir(path.join(tmp.path, "deploy"), { recursive: true })
|
|
await write(tmp.path, "deploy", "Deploy production")
|
|
})
|
|
pulls = 0
|
|
urls.set("https://example.test/skills/", [AbsolutePath.make(tmp.path)])
|
|
|
|
const agents = yield* AgentV2.Service
|
|
yield* agents.update((editor) =>
|
|
editor.update(AgentV2.ID.make("reviewer"), (agent) => {
|
|
agent.permissions.push({ action: "skill", resource: "deploy", effect: "deny" })
|
|
}),
|
|
)
|
|
|
|
const skill = yield* SkillV2.Service
|
|
const register = yield* skill.transform()
|
|
yield* register((editor) => editor.source({ type: "url", url: "https://example.test/skills/" }))
|
|
|
|
expect((yield* skill.list()).map((item) => item.name)).toEqual(["deploy"])
|
|
expect((yield* skill.list()).map((item) => item.name)).toEqual(["deploy"])
|
|
expect(pulls).toBe(1)
|
|
expect(SkillV2.available(yield* skill.list(), (yield* agents.get(AgentV2.ID.make("reviewer")))!)).toEqual([])
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
})
|