164 lines
5.5 KiB
TypeScript
164 lines
5.5 KiB
TypeScript
import { describe, expect } from "bun:test"
|
|
import { Effect, Layer } from "effect"
|
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|
import { Agent } from "@opencode-ai/core/agent"
|
|
import { Image } from "@opencode-ai/core/image"
|
|
import { Permission } from "@opencode-ai/core/permission"
|
|
import { PluginRuntime } from "@opencode-ai/core/plugin/runtime"
|
|
import { Session } from "@opencode-ai/core/session"
|
|
import { Tool } from "@opencode-ai/core/tool"
|
|
import { SessionRenameTool } from "@opencode-ai/core/tool/plugin/session-rename"
|
|
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
|
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
|
import { imagePassthrough } from "./lib/image"
|
|
import { testEffect } from "./lib/effect"
|
|
import { executeTool, registerToolPlugin, toolDefinitions, toolIdentity } from "./lib/tool"
|
|
|
|
const currentSessionID = Session.ID.make("ses_session_rename_current")
|
|
const otherSessionID = Session.ID.make("ses_session_rename_other")
|
|
const renamed: Array<{ sessionID: Session.ID; title: string }> = []
|
|
const assertions: Permission.AssertInput[] = []
|
|
let deny = false
|
|
|
|
const runtime = Layer.succeed(
|
|
PluginRuntime.Service,
|
|
PluginRuntime.Service.of({
|
|
session: {
|
|
get: () => Effect.die("unused"),
|
|
create: () => Effect.die("unused"),
|
|
messages: () => Effect.die("unused"),
|
|
prompt: () => Effect.die("unused"),
|
|
generate: () => Effect.die("unused"),
|
|
command: () => Effect.die("unused"),
|
|
resume: () => Effect.die("unused"),
|
|
interrupt: () => Effect.die("unused"),
|
|
synthetic: () => Effect.die("unused"),
|
|
rename: (input) => Effect.sync(() => void renamed.push(input)),
|
|
wait: () => Effect.die("unused"),
|
|
},
|
|
job: {
|
|
start: () => Effect.die("unused"),
|
|
wait: () => Effect.die("unused"),
|
|
block: () => Effect.die("unused"),
|
|
background: () => Effect.die("unused"),
|
|
cancel: () => Effect.die("unused"),
|
|
},
|
|
location: {
|
|
agent: {
|
|
list: () => Effect.die("unused"),
|
|
},
|
|
},
|
|
}),
|
|
)
|
|
|
|
const permission = Layer.succeed(
|
|
Permission.Service,
|
|
Permission.Service.of({
|
|
assert: (input) =>
|
|
Effect.sync(() => assertions.push(input)).pipe(
|
|
Effect.andThen(
|
|
deny
|
|
? Effect.fail(
|
|
new Permission.BlockedError({
|
|
rules: [],
|
|
permission: input.action,
|
|
resources: input.resources,
|
|
}),
|
|
)
|
|
: Effect.void,
|
|
),
|
|
),
|
|
ask: () => Effect.die("unused"),
|
|
reply: () => Effect.die("unused"),
|
|
get: () => Effect.die("unused"),
|
|
forSession: () => Effect.die("unused"),
|
|
list: () => Effect.die("unused"),
|
|
}),
|
|
)
|
|
|
|
const sessionRenameToolNode = makeLocationNode({
|
|
name: "test/session-rename-tool-plugin",
|
|
layer: Layer.effectDiscard(registerToolPlugin(SessionRenameTool.Plugin)),
|
|
deps: [Tool.node, Permission.node, PluginRuntime.node],
|
|
})
|
|
|
|
const it = testEffect(
|
|
AppNodeBuilder.build(LayerNode.group([Tool.node, sessionRenameToolNode]), [
|
|
[Permission.node, permission],
|
|
[PluginRuntime.node, runtime],
|
|
[Image.node, imagePassthrough],
|
|
]),
|
|
)
|
|
|
|
describe("SessionRenameTool", () => {
|
|
it.effect("registers directly and confines rename to the invocation session", () =>
|
|
Effect.gen(function* () {
|
|
renamed.length = 0
|
|
assertions.length = 0
|
|
deny = false
|
|
const registry = yield* Tool.Service
|
|
const snapshot = yield* registry.snapshot()
|
|
|
|
expect(snapshot.definitions.map((tool) => tool.name)).toEqual(["sessionRename", "execute"])
|
|
expect(snapshot.codeModeCatalog?.some((tool) => tool.path === "sessionRename")).toBe(false)
|
|
expect(
|
|
yield* executeTool(registry, {
|
|
sessionID: currentSessionID,
|
|
...toolIdentity,
|
|
call: {
|
|
type: "tool-call",
|
|
id: "call-session-rename",
|
|
name: "sessionRename",
|
|
input: { title: "\n Focused\n title\t", sessionID: otherSessionID },
|
|
},
|
|
}),
|
|
).toEqual({
|
|
status: "completed",
|
|
output: { title: "Focused title" },
|
|
content: [{ type: "text", text: "Renamed the current session to: Focused title" }],
|
|
metadata: { title: "Focused title" },
|
|
})
|
|
expect(renamed).toEqual([{ sessionID: currentSessionID, title: "Focused title" }])
|
|
expect(assertions).toMatchObject([
|
|
{
|
|
action: "sessionRename",
|
|
resources: ["*"],
|
|
sessionID: currentSessionID,
|
|
agent: Agent.ID.make("build"),
|
|
source: { type: "tool", messageID: toolIdentity.messageID, callID: "call-session-rename" },
|
|
},
|
|
])
|
|
}),
|
|
)
|
|
|
|
it.effect("filters catalog denial and enforces leaf permission", () =>
|
|
Effect.gen(function* () {
|
|
renamed.length = 0
|
|
deny = true
|
|
const registry = yield* Tool.Service
|
|
|
|
expect(
|
|
(yield* toolDefinitions(registry, [{ action: "sessionRename", resource: "*", effect: "deny" }])).map(
|
|
(tool) => tool.name,
|
|
),
|
|
).toEqual(["execute"])
|
|
expect(
|
|
yield* executeTool(registry, {
|
|
sessionID: currentSessionID,
|
|
...toolIdentity,
|
|
call: {
|
|
type: "tool-call",
|
|
id: "call-session-rename-denied",
|
|
name: "sessionRename",
|
|
input: { title: "Denied title" },
|
|
},
|
|
}),
|
|
).toEqual({
|
|
status: "error",
|
|
error: { type: "permission.rejected", message: "Permission denied: sessionRename" },
|
|
})
|
|
expect(renamed).toEqual([])
|
|
deny = false
|
|
}),
|
|
)
|
|
})
|