Files
nanobot/tui/src/command-menu.test.ts
T

137 lines
4.0 KiB
TypeScript
Raw Normal View History

2026-08-13 12:55:26 +09:00
import { afterEach, describe, expect, test } from "bun:test"
import { createTestRenderer, type TestRendererSetup } from "@opentui/core/testing"
2026-08-13 14:35:26 +09:00
import { CommandMenu, resolveSlashCommandLifecycle } from "./command-menu"
2026-08-13 12:55:26 +09:00
import type { SlashCommand } from "./protocol"
const commands: SlashCommand[] = [
{
command: "/help",
title: "Help",
description: "Show available commands",
argHint: "",
2026-08-13 14:35:26 +09:00
lifecycle: "side_channel",
2026-08-13 12:55:26 +09:00
acceptsArgs: false,
},
{
command: "/history",
title: "History",
description: "Show recent messages",
argHint: "[n]",
2026-08-13 14:35:26 +09:00
lifecycle: "side_channel",
2026-08-13 12:55:26 +09:00
acceptsArgs: true,
},
]
describe("CommandMenu", () => {
let setup: TestRendererSetup | undefined
afterEach(() => {
if (setup && !setup.renderer.isDestroyed) setup.renderer.destroy()
setup = undefined
})
test("discovers, navigates, and completes backend commands", async () => {
setup = await createTestRenderer({ width: 80, height: 18, screenMode: "alternate-screen" })
const menu = new CommandMenu(setup.renderer, {
text: "#FFFFFF",
muted: "#999999",
border: "#555555",
})
setup.renderer.root.add(menu.root)
menu.setCommands(commands)
menu.update("/h")
await setup.renderOnce()
const frame = setup.captureCharFrame()
expect(frame).toContain(" /help")
expect(frame).toContain("/history [n]")
expect(menu.completion("/h")).toBe("/help")
expect(menu.move(1)).toBe(true)
expect(menu.complete()).toBe("/history ")
expect(menu.visible).toBe(false)
})
test("hides outside the leading command token", async () => {
setup = await createTestRenderer({ width: 60, height: 12, screenMode: "alternate-screen" })
const menu = new CommandMenu(setup.renderer, {
text: "#FFFFFF",
muted: "#999999",
border: "#555555",
})
menu.setCommands(commands)
menu.update("explain /help")
expect(menu.visible).toBe(false)
menu.update("/history 5")
expect(menu.visible).toBe(false)
})
2026-08-13 14:35:26 +09:00
test("keeps gateway commands authoritative over local actions", async () => {
setup = await createTestRenderer({ width: 60, height: 12, screenMode: "alternate-screen" })
const menu = new CommandMenu(setup.renderer, {
text: "#FFFFFF",
muted: "#999999",
border: "#555555",
})
menu.setCommands(commands, [{
command: "/help",
title: "Local help",
description: "Must not replace nanobot help",
action: "sessions",
}, {
command: "/sessions",
title: "Sessions",
description: "Switch conversations",
action: "sessions",
}])
expect(menu.resolve("/help")?.source).toBe("gateway")
expect(menu.resolve("/sessions")).toMatchObject({
source: "tui",
command: { action: "sessions" },
})
})
2026-08-19 17:49:55 +08:00
test("discovers and completes the local exit action", async () => {
setup = await createTestRenderer({ width: 60, height: 12, screenMode: "alternate-screen" })
const menu = new CommandMenu(setup.renderer, {
text: "#FFFFFF",
muted: "#999999",
border: "#555555",
})
setup.renderer.root.add(menu.root)
menu.setCommands([], [{
command: "/exit",
title: "Exit",
description: "Close this terminal UI",
action: "exit",
}])
menu.update("/ex")
await setup.renderOnce()
expect(setup.captureCharFrame()).toContain("/exit")
expect(menu.completion("/ex")).toBe("/exit")
expect(menu.resolve("/exit")).toMatchObject({
source: "tui",
command: { action: "exit" },
})
})
2026-08-13 14:35:26 +09:00
test("resolves argument-sensitive command lifecycles", () => {
const goal: SlashCommand = {
command: "/goal",
title: "Goal",
description: "Start a long-running goal",
argHint: "<goal>",
lifecycle: "agent_turn_with_args",
acceptsArgs: true,
}
expect(resolveSlashCommandLifecycle("/goal", goal)).toBe("side_channel")
expect(resolveSlashCommandLifecycle("/goal ship it", goal)).toBe("agent_turn")
expect(resolveSlashCommandLifecycle("/help extra", commands[0]!)).toBeNull()
})
2026-08-13 12:55:26 +09:00
})