153 lines
6.3 KiB
JavaScript
153 lines
6.3 KiB
JavaScript
import assert from "node:assert/strict"
|
|
import test from "node:test"
|
|
import toolkit from "../dist/plugin.js"
|
|
|
|
function createContext({ options = {}, projectID = "project-1", projectCanonical = "/workspace/demo", storage = new Map() } = {}) {
|
|
const registered = []
|
|
const vcs = {
|
|
get: async () => ({ branch: { current: "feature/toolkit", default: "main" } }),
|
|
status: async () => [{ path: "src/app.ts", status: "modified" }],
|
|
}
|
|
|
|
const context = {
|
|
location: {
|
|
directory: "/workspace/demo",
|
|
project: { id: projectID, directory: "/workspace/demo", canonical: projectCanonical },
|
|
},
|
|
options,
|
|
vcs,
|
|
session: {
|
|
get: async ({ sessionID }) => ({
|
|
id: sessionID,
|
|
projectID,
|
|
location: { directory: "/workspace/demo" },
|
|
}),
|
|
},
|
|
storage: {
|
|
get: async (key) => storage.get(key),
|
|
set: async (key, value) => { storage.set(key, value) },
|
|
remove: async (key) => { storage.delete(key) },
|
|
scan: async ({ prefix, after, limit }) => {
|
|
const keys = [...storage.keys()].filter((key) => key.startsWith(prefix)).sort().filter((key) => after === undefined || key > after)
|
|
const selected = keys.slice(0, limit)
|
|
return {
|
|
entries: selected.map((key) => ({ key, value: storage.get(key) })),
|
|
...(keys.length > selected.length && selected.length > 0 ? { next: selected.at(-1) } : {}),
|
|
}
|
|
},
|
|
},
|
|
tool: {
|
|
transform: async (register) => {
|
|
register({
|
|
namespace: () => {},
|
|
add: (definition) => registered.push(definition),
|
|
list: () => [],
|
|
get: () => undefined,
|
|
update: () => {},
|
|
remove: () => {},
|
|
})
|
|
return { dispose: async () => {} }
|
|
},
|
|
},
|
|
}
|
|
|
|
return { context, registered, vcs }
|
|
}
|
|
|
|
test("exports a stable OpenCode V2 plugin definition", () => {
|
|
assert.equal(toolkit.id, "sargatxet.opencode-toolkit")
|
|
assert.equal(typeof toolkit.setup, "function")
|
|
})
|
|
|
|
test("registers namespaced read-only project status tool", async () => {
|
|
const { context, registered } = createContext()
|
|
|
|
await toolkit.setup(context)
|
|
|
|
assert.equal(registered.length, 1)
|
|
assert.equal(registered[0].name, "project_status")
|
|
assert.deepEqual(registered[0].options, { namespace: "toolkit", codemode: true })
|
|
assert.equal(typeof registered[0].execute, "function")
|
|
assert.deepEqual(registered[0].input, {
|
|
type: "object",
|
|
properties: {},
|
|
additionalProperties: false,
|
|
})
|
|
})
|
|
|
|
test("status tool reports VCS context without mutating it", async () => {
|
|
const { context, registered, vcs } = createContext()
|
|
const originalGet = vcs.get
|
|
const originalStatus = vcs.status
|
|
|
|
await toolkit.setup(context)
|
|
const result = await registered[0].execute({}, { signal: new AbortController().signal })
|
|
|
|
assert.deepEqual(JSON.parse(result.content), {
|
|
project: {
|
|
id: "project-1",
|
|
directory: "/workspace/demo",
|
|
canonical: "/workspace/demo",
|
|
},
|
|
vcs: {
|
|
branch: { current: "feature/toolkit", default: "main" },
|
|
},
|
|
changes: [{ path: "src/app.ts", status: "modified" }],
|
|
})
|
|
assert.equal(vcs.get, originalGet)
|
|
assert.equal(vcs.status, originalStatus)
|
|
})
|
|
|
|
test("memory tools are disabled unless explicitly enabled", async () => {
|
|
const { context, registered } = createContext()
|
|
await toolkit.setup(context)
|
|
assert.deepEqual(registered.map((tool) => tool.name), ["project_status"])
|
|
})
|
|
|
|
test("session memories can be saved, listed, retrieved, and deleted without crossing sessions", async () => {
|
|
const { context, registered } = createContext({ options: { memory: { enabled: true } } })
|
|
await toolkit.setup(context)
|
|
const tools = Object.fromEntries(registered.map((tool) => [tool.name, tool]))
|
|
const execution = (sessionID) => ({ sessionID, signal: new AbortController().signal })
|
|
|
|
const saved = JSON.parse((await tools.memory_save.execute({ content: "Use strict TypeScript." }, execution("session-a"))).content)
|
|
assert.equal(saved.scope, "session")
|
|
assert.equal(typeof saved.expiresAt, "string")
|
|
assert.deepEqual(JSON.parse((await tools.memory_list.execute({}, execution("session-a"))).content).map(({ id }) => id), [saved.id])
|
|
assert.deepEqual(JSON.parse((await tools.memory_list.execute({}, execution("session-b"))).content), [])
|
|
assert.match((await tools.memory_get.execute({ id: saved.id }, execution("session-a"))).content, /Use strict TypeScript/)
|
|
assert.equal((await tools.memory_delete.execute({ id: saved.id }, execution("session-a"))).content, "Memory deleted.")
|
|
assert.deepEqual(JSON.parse((await tools.memory_list.execute({}, execution("session-a"))).content), [])
|
|
})
|
|
|
|
test("project memories require explicit configuration and are isolated by project", async () => {
|
|
const sharedStorage = new Map()
|
|
const enabled = createContext({ options: { memory: { enabled: true, project: true } }, storage: sharedStorage })
|
|
await toolkit.setup(enabled.context)
|
|
const tools = Object.fromEntries(enabled.registered.map((tool) => [tool.name, tool]))
|
|
const execution = (sessionID) => ({ sessionID, signal: new AbortController().signal })
|
|
const saved = JSON.parse((await tools.memory_save.execute({ content: "Shared project convention", scope: "project" }, execution("session-a"))).content)
|
|
assert.equal(saved.scope, "project")
|
|
assert.equal(JSON.parse((await tools.memory_list.execute({ scope: "project" }, execution("session-b"))).content).length, 1)
|
|
|
|
const other = createContext({
|
|
options: { memory: { enabled: true, project: true } },
|
|
projectID: "project-2",
|
|
projectCanonical: "/workspace/other",
|
|
storage: sharedStorage,
|
|
})
|
|
await toolkit.setup(other.context)
|
|
const otherTools = Object.fromEntries(other.registered.map((tool) => [tool.name, tool]))
|
|
assert.deepEqual(JSON.parse((await otherTools.memory_list.execute({ scope: "project" }, execution("session-c"))).content), [])
|
|
})
|
|
|
|
test("project scope fails closed when project memory has not been opted into", async () => {
|
|
const { context, registered } = createContext({ options: { memory: { enabled: true } } })
|
|
await toolkit.setup(context)
|
|
const save = registered.find((tool) => tool.name === "memory_save")
|
|
await assert.rejects(
|
|
save.execute({ content: "Not enabled", scope: "project" }, { sessionID: "session-a", signal: new AbortController().signal }),
|
|
/Project memory is disabled/,
|
|
)
|
|
})
|