2026-03-31 20:02:00 -07:00
|
|
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
|
|
|
import { mkdirSync, rmSync, writeFileSync } from "node:fs"
|
|
|
|
|
import { tmpdir } from "node:os"
|
|
|
|
|
import { join } from "node:path"
|
|
|
|
|
import { randomUUID } from "node:crypto"
|
|
|
|
|
import { createPluginInterface } from "./plugin-interface"
|
|
|
|
|
import { createAutoSlashCommandHook } from "./hooks/auto-slash-command"
|
|
|
|
|
import { createStartWorkHook } from "./hooks/start-work"
|
|
|
|
|
import { readBoulderState } from "./features/boulder-state"
|
|
|
|
|
import {
|
|
|
|
|
_resetForTesting,
|
|
|
|
|
getSessionAgent,
|
|
|
|
|
registerAgentName,
|
|
|
|
|
updateSessionAgent,
|
|
|
|
|
} from "./features/claude-code-session-state"
|
|
|
|
|
|
2026-04-10 10:47:27 +09:00
|
|
|
|
2026-03-31 20:02:00 -07:00
|
|
|
describe("createPluginInterface - command.execute.before", () => {
|
|
|
|
|
let testDir = ""
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
testDir = join(tmpdir(), `plugin-interface-start-work-${randomUUID()}`)
|
2026-05-16 17:41:35 +09:00
|
|
|
mkdirSync(join(testDir, ".omo", "plans"), { recursive: true })
|
|
|
|
|
writeFileSync(join(testDir, ".omo", "plans", "worker-plan.md"), "# Plan\n- [ ] Task 1")
|
2026-03-31 20:02:00 -07:00
|
|
|
_resetForTesting()
|
|
|
|
|
registerAgentName("prometheus")
|
|
|
|
|
registerAgentName("sisyphus")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
_resetForTesting()
|
|
|
|
|
rmSync(testDir, { recursive: true, force: true })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("executes start-work side effects for native command execution", async () => {
|
|
|
|
|
// given
|
|
|
|
|
updateSessionAgent("ses-command-before", "prometheus")
|
|
|
|
|
const pluginInterface = createPluginInterface({
|
|
|
|
|
ctx: {
|
|
|
|
|
directory: testDir,
|
|
|
|
|
client: { tui: { showToast: async () => {} } },
|
|
|
|
|
} as never,
|
|
|
|
|
pluginConfig: {} as never,
|
|
|
|
|
firstMessageVariantGate: {
|
|
|
|
|
shouldOverride: () => false,
|
|
|
|
|
markApplied: () => {},
|
|
|
|
|
markSessionCreated: () => {},
|
|
|
|
|
clear: () => {},
|
|
|
|
|
},
|
|
|
|
|
managers: {} as never,
|
|
|
|
|
hooks: {
|
|
|
|
|
autoSlashCommand: createAutoSlashCommandHook({ skills: [] }),
|
|
|
|
|
startWork: createStartWorkHook({
|
|
|
|
|
directory: testDir,
|
|
|
|
|
client: { tui: { showToast: async () => {} } },
|
|
|
|
|
} as never),
|
|
|
|
|
} as never,
|
|
|
|
|
tools: {},
|
|
|
|
|
})
|
|
|
|
|
const output = {
|
|
|
|
|
parts: [{ type: "text", text: "original" }],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// when
|
|
|
|
|
await pluginInterface["command.execute.before"]?.(
|
|
|
|
|
{
|
|
|
|
|
command: "start-work",
|
|
|
|
|
sessionID: "ses-command-before",
|
|
|
|
|
arguments: "",
|
|
|
|
|
},
|
|
|
|
|
output as never
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// then
|
|
|
|
|
expect(pluginInterface["command.execute.before"]).toBeDefined()
|
|
|
|
|
expect(output.parts[0]?.text).toContain("Auto-Selected Plan")
|
|
|
|
|
expect(output.parts[0]?.text).toContain("boulder.json has been created")
|
|
|
|
|
expect(getSessionAgent("ses-command-before")).toBe("sisyphus")
|
|
|
|
|
expect(readBoulderState(testDir)?.agent).toBe("sisyphus")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("does not run start-work side effects for other native commands with session context", async () => {
|
|
|
|
|
// given
|
|
|
|
|
updateSessionAgent("ses-handoff", "prometheus")
|
|
|
|
|
const pluginInterface = createPluginInterface({
|
|
|
|
|
ctx: {
|
|
|
|
|
directory: testDir,
|
|
|
|
|
client: { tui: { showToast: async () => {} } },
|
|
|
|
|
} as never,
|
|
|
|
|
pluginConfig: {} as never,
|
|
|
|
|
firstMessageVariantGate: {
|
|
|
|
|
shouldOverride: () => false,
|
|
|
|
|
markApplied: () => {},
|
|
|
|
|
markSessionCreated: () => {},
|
|
|
|
|
clear: () => {},
|
|
|
|
|
},
|
|
|
|
|
managers: {} as never,
|
|
|
|
|
hooks: {
|
|
|
|
|
autoSlashCommand: createAutoSlashCommandHook({ skills: [] }),
|
|
|
|
|
startWork: createStartWorkHook({
|
|
|
|
|
directory: testDir,
|
|
|
|
|
client: { tui: { showToast: async () => {} } },
|
|
|
|
|
} as never),
|
|
|
|
|
} as never,
|
|
|
|
|
tools: {},
|
|
|
|
|
})
|
|
|
|
|
const output = {
|
|
|
|
|
parts: [{ type: "text", text: "original" }],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// when
|
|
|
|
|
await pluginInterface["command.execute.before"]?.(
|
|
|
|
|
{
|
|
|
|
|
command: "handoff",
|
|
|
|
|
sessionID: "ses-handoff",
|
|
|
|
|
arguments: "",
|
|
|
|
|
},
|
|
|
|
|
output as never
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// then
|
|
|
|
|
expect(output.parts[0]?.text).toContain("HANDOFF CONTEXT")
|
|
|
|
|
expect(readBoulderState(testDir)).toBeNull()
|
|
|
|
|
expect(getSessionAgent("ses-handoff")).toBe("prometheus")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("switches native start-work to Atlas when Atlas is registered in config", async () => {
|
|
|
|
|
// given
|
|
|
|
|
registerAgentName("atlas")
|
|
|
|
|
updateSessionAgent("ses-command-atlas", "prometheus")
|
|
|
|
|
const pluginInterface = createPluginInterface({
|
|
|
|
|
ctx: {
|
|
|
|
|
directory: testDir,
|
|
|
|
|
client: { tui: { showToast: async () => {} } },
|
|
|
|
|
} as never,
|
|
|
|
|
pluginConfig: {} as never,
|
|
|
|
|
firstMessageVariantGate: {
|
|
|
|
|
shouldOverride: () => false,
|
|
|
|
|
markApplied: () => {},
|
|
|
|
|
markSessionCreated: () => {},
|
|
|
|
|
clear: () => {},
|
|
|
|
|
},
|
|
|
|
|
managers: {} as never,
|
|
|
|
|
hooks: {
|
|
|
|
|
autoSlashCommand: createAutoSlashCommandHook({ skills: [] }),
|
|
|
|
|
startWork: createStartWorkHook({
|
|
|
|
|
directory: testDir,
|
|
|
|
|
client: { tui: { showToast: async () => {} } },
|
|
|
|
|
} as never),
|
|
|
|
|
} as never,
|
|
|
|
|
tools: {},
|
|
|
|
|
})
|
|
|
|
|
const output = {
|
|
|
|
|
message: {} as Record<string, unknown>,
|
|
|
|
|
parts: [{ type: "text", text: "/start-work" }],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// when
|
|
|
|
|
await pluginInterface["chat.message"]?.(
|
|
|
|
|
{
|
|
|
|
|
sessionID: "ses-command-atlas",
|
|
|
|
|
agent: "prometheus",
|
|
|
|
|
} as never,
|
|
|
|
|
output as never
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// then
|
2026-04-08 15:55:58 +09:00
|
|
|
expect(output.message.agent).toBe("atlas")
|
2026-03-31 20:02:00 -07:00
|
|
|
expect(getSessionAgent("ses-command-atlas")).toBe("atlas")
|
|
|
|
|
expect(readBoulderState(testDir)?.agent).toBe("atlas")
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-04-01 17:45:16 -07:00
|
|
|
|
|
|
|
|
describe("createPluginInterface - ulw-loop native command smoke", () => {
|
|
|
|
|
let testDir = ""
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
testDir = join(tmpdir(), `plugin-interface-ulw-loop-${randomUUID()}`)
|
|
|
|
|
mkdirSync(testDir, { recursive: true })
|
|
|
|
|
_resetForTesting()
|
|
|
|
|
registerAgentName("sisyphus")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
_resetForTesting()
|
|
|
|
|
rmSync(testDir, { recursive: true, force: true })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("starts the ultrawork loop from the native command flow with parsed arguments intact", async () => {
|
|
|
|
|
// given
|
|
|
|
|
const startLoopCalls: Array<{
|
|
|
|
|
sessionID: string
|
|
|
|
|
prompt: string
|
|
|
|
|
options: Record<string, unknown>
|
|
|
|
|
}> = []
|
|
|
|
|
const pluginInterface = createPluginInterface({
|
|
|
|
|
ctx: {
|
|
|
|
|
directory: testDir,
|
|
|
|
|
client: { tui: { showToast: async () => {} } },
|
|
|
|
|
} as never,
|
|
|
|
|
pluginConfig: {} as never,
|
|
|
|
|
firstMessageVariantGate: {
|
|
|
|
|
shouldOverride: () => false,
|
|
|
|
|
markApplied: () => {},
|
|
|
|
|
markSessionCreated: () => {},
|
|
|
|
|
clear: () => {},
|
|
|
|
|
},
|
|
|
|
|
managers: {} as never,
|
|
|
|
|
hooks: {
|
|
|
|
|
autoSlashCommand: createAutoSlashCommandHook({ skills: [] }),
|
|
|
|
|
ralphLoop: {
|
|
|
|
|
startLoop: (sessionID: string, prompt: string, options?: Record<string, unknown>) => {
|
|
|
|
|
startLoopCalls.push({ sessionID, prompt, options: options ?? {} })
|
|
|
|
|
return true
|
|
|
|
|
},
|
|
|
|
|
cancelLoop: () => true,
|
|
|
|
|
getState: () => null,
|
|
|
|
|
},
|
|
|
|
|
} as never,
|
|
|
|
|
tools: {},
|
|
|
|
|
})
|
|
|
|
|
const output = {
|
|
|
|
|
message: {} as Record<string, unknown>,
|
|
|
|
|
parts: [{ type: "text", text: "original" }],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// when
|
|
|
|
|
await pluginInterface["command.execute.before"]?.(
|
|
|
|
|
{
|
|
|
|
|
command: "ulw-loop",
|
|
|
|
|
sessionID: "ses-ulw-native",
|
|
|
|
|
arguments: '"Ship feature" --strategy=continue',
|
|
|
|
|
},
|
|
|
|
|
output as never,
|
|
|
|
|
)
|
|
|
|
|
await pluginInterface["chat.message"]?.(
|
|
|
|
|
{
|
|
|
|
|
sessionID: "ses-ulw-native",
|
|
|
|
|
agent: "sisyphus",
|
|
|
|
|
} as never,
|
|
|
|
|
output as never,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// then
|
|
|
|
|
expect(output.parts[0]?.text).toContain("/ulw-loop Command")
|
|
|
|
|
expect(startLoopCalls).toEqual([
|
|
|
|
|
{
|
|
|
|
|
sessionID: "ses-ulw-native",
|
|
|
|
|
prompt: "Ship feature",
|
|
|
|
|
options: {
|
|
|
|
|
ultrawork: true,
|
|
|
|
|
maxIterations: undefined,
|
|
|
|
|
completionPromise: undefined,
|
|
|
|
|
strategy: "continue",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
])
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-04-27 17:36:05 +09:00
|
|
|
|
|
|
|
|
describe("createPluginInterface - backward compatibility", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
_resetForTesting()
|
|
|
|
|
registerAgentName("hephaestus")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
_resetForTesting()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("strips legacy ZWSP-prefixed agent names from persisted chat.message session state (GH-3259)", async () => {
|
|
|
|
|
// given - persisted session payload from v3.14.0-v3.16.0 with ZWSP prefix
|
|
|
|
|
const pluginInterface = createPluginInterface({
|
|
|
|
|
ctx: {
|
|
|
|
|
directory: tmpdir(),
|
|
|
|
|
client: { tui: { showToast: async () => {} } },
|
|
|
|
|
} as never,
|
|
|
|
|
pluginConfig: {} as never,
|
|
|
|
|
firstMessageVariantGate: {
|
|
|
|
|
shouldOverride: () => false,
|
|
|
|
|
markApplied: () => {},
|
|
|
|
|
markSessionCreated: () => {},
|
|
|
|
|
clear: () => {},
|
|
|
|
|
},
|
|
|
|
|
managers: {} as never,
|
|
|
|
|
hooks: {} as never,
|
|
|
|
|
tools: {},
|
|
|
|
|
})
|
|
|
|
|
const output = {
|
|
|
|
|
message: {} as Record<string, unknown>,
|
|
|
|
|
parts: [{ type: "text", text: "hello" }],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// when
|
|
|
|
|
await pluginInterface["chat.message"]?.(
|
|
|
|
|
{
|
|
|
|
|
sessionID: "ses-legacy-zwsp",
|
|
|
|
|
agent: "\u200B\u200BHephaestus - Deep Agent",
|
|
|
|
|
} as never,
|
|
|
|
|
output as never,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// then
|
|
|
|
|
expect(getSessionAgent("ses-legacy-zwsp")).toBe("Hephaestus - Deep Agent")
|
|
|
|
|
})
|
|
|
|
|
})
|