128 lines
4.6 KiB
TypeScript
128 lines
4.6 KiB
TypeScript
/**
|
|
* Tests for the TUI logic layer: command parsing, action dispatch, and the
|
|
* key controller. Mirrors the Rust `#[cfg(test)]` blocks.
|
|
*/
|
|
import { describe, test, expect } from "bun:test";
|
|
import { parseCommand, applyCommand } from "./command.ts";
|
|
import { createTuiState, pushTranscript, makeChatMessage } from "./state.ts";
|
|
import { applyAction } from "./action.ts";
|
|
import { handleKey, decodeKey } from "./controller.ts";
|
|
import { Roles } from "@zesdex/domain";
|
|
|
|
function freshState() {
|
|
return createTuiState(["/tmp/zesdex-tui-test"], "/tmp/zesdex-tui-test/sess", "/tmp/zesdex-tui-test/mem");
|
|
}
|
|
|
|
describe("parseCommand", () => {
|
|
test("recognises /todo", () => {
|
|
expect(parseCommand("/todo")).toEqual({ tag: "TodoOpen" });
|
|
});
|
|
test("recognises /usage", () => {
|
|
expect(parseCommand("/usage")).toEqual({ tag: "UsageOpen" });
|
|
});
|
|
test("non-slash input is Unknown", () => {
|
|
expect(parseCommand("hello")).toEqual({ tag: "Unknown", text: "hello" });
|
|
});
|
|
test("parses /edit path", () => {
|
|
expect(parseCommand("/edit src/a.ts")).toEqual({ tag: "Edit", path: "src/a.ts" });
|
|
});
|
|
test("parses /clear with arg as Clear, without as ClearConfirm", () => {
|
|
expect(parseCommand("/clear foo")).toEqual({ tag: "Clear" });
|
|
expect(parseCommand("/clear")).toEqual({ tag: "ClearConfirm" });
|
|
});
|
|
});
|
|
|
|
describe("applyCommand", () => {
|
|
test("Help opens help overlay", () => {
|
|
expect(applyCommand({ tag: "Help" })).toEqual([{ tag: "OpenOverlay", overlay: "help" }]);
|
|
});
|
|
test("Unknown slash produces SystemNote error", () => {
|
|
expect(applyCommand({ tag: "Unknown", text: "/nope" })).toEqual([
|
|
{ tag: "SystemNote", kind: "error", message: "Unknown command: /nope" },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("applyAction", () => {
|
|
test("SubmitInput pushes user transcript and sets pendingSubmit", () => {
|
|
const s = freshState();
|
|
applyAction(s, { tag: "SubmitInput", text: "hi" });
|
|
const last = s.transcriptCache.messages[s.transcriptCache.messages.length - 1];
|
|
expect(last?.role).toBe(Roles.User);
|
|
expect(last?.content).toBe("hi");
|
|
expect(s.pendingSubmit).toBe("hi");
|
|
});
|
|
|
|
test("ForceQuit sets quit", () => {
|
|
const s = freshState();
|
|
applyAction(s, { tag: "ForceQuit" });
|
|
expect(s.quit).toBe(true);
|
|
});
|
|
|
|
test("QuitConfirm sets overlay", () => {
|
|
const s = freshState();
|
|
applyAction(s, { tag: "QuitConfirm" });
|
|
expect(s.misc.overlay).toBe("quit_confirm");
|
|
});
|
|
|
|
test("AbortTurn sets abort flag", () => {
|
|
const s = freshState();
|
|
applyAction(s, { tag: "AbortTurn" });
|
|
expect(s.abortFlag.value).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("handleKey", () => {
|
|
test("Ctrl+C produces QuitConfirm", () => {
|
|
const s = freshState();
|
|
const actions = handleKey({ code: "Char", value: "c", ctrl: true, alt: false }, s);
|
|
expect(actions).toEqual([{ tag: "QuitConfirm" }]);
|
|
});
|
|
|
|
test("typing chars inserts into buffer", () => {
|
|
const s = freshState();
|
|
handleKey({ code: "Char", value: "h", ctrl: false, alt: false }, s);
|
|
handleKey({ code: "Char", value: "i", ctrl: false, alt: false }, s);
|
|
expect(s.input.buffer).toBe("hi");
|
|
});
|
|
|
|
test("Enter submits input as SubmitInput", () => {
|
|
const s = freshState();
|
|
handleKey({ code: "Char", value: "a", ctrl: false, alt: false }, s);
|
|
const actions = handleKey({ code: "Enter", ctrl: false, alt: false }, s);
|
|
expect(actions).toEqual([{ tag: "SubmitInput", text: "a" }]);
|
|
});
|
|
|
|
test("Ctrl+Y copies last assistant message", () => {
|
|
const s = freshState();
|
|
pushTranscript(s, makeChatMessage(Roles.User, "hi"));
|
|
pushTranscript(s, makeChatMessage(Roles.Assistant, "first"));
|
|
pushTranscript(s, makeChatMessage(Roles.Tool, "tool"));
|
|
pushTranscript(s, makeChatMessage(Roles.Assistant, "second"));
|
|
handleKey({ code: "Char", value: "y", ctrl: true, alt: false }, s);
|
|
expect(s.misc.pendingClipboardCopy).toBe("second");
|
|
});
|
|
|
|
test("Ctrl+Y with no assistant pushes info toast", () => {
|
|
const s = freshState();
|
|
handleKey({ code: "Char", value: "y", ctrl: true, alt: false }, s);
|
|
expect(s.misc.pendingClipboardCopy).toBeNull();
|
|
expect(s.misc.toasts.length).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("decodeKey", () => {
|
|
test("Enter decodes", () => {
|
|
expect(decodeKey("\r")).toEqual({ code: "Enter", ctrl: false, alt: false });
|
|
});
|
|
test("arrow up decodes", () => {
|
|
expect(decodeKey("\u001b[A")).toEqual({ code: "ArrowUp", ctrl: false, alt: false });
|
|
});
|
|
test("ctrl-c decodes to ctrl Char c", () => {
|
|
expect(decodeKey("\u0003")).toEqual({ code: "Char", value: "c", ctrl: true, alt: false });
|
|
});
|
|
test("plain letter decodes", () => {
|
|
expect(decodeKey("x")).toEqual({ code: "Char", value: "x", ctrl: false, alt: false });
|
|
});
|
|
});
|