Files
LLeMbas-CLI/tests/tui/app.test.tsx
T
HomerandClaude Opus 5.5 f9bad01ed7
ci / check (push) Waiting to run
LLeMbas CLI 1.0.0
The first public release of LLeMbas CLI: a terminal coding agent and project manager for any LLM
API, with permission modes, git snapshots, memory and skills, knowledge bases, MCP, voice, and a
link to a LLeMbas instance whose web UI can work its sessions too. Signed Linux binaries for x64
and arm64.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:59:03 +00:00

989 lines
46 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterEach, expect, test } from "bun:test"
import { delta, toolCall, usage } from "../fake-provider.ts"
import { tui } from "./harness.tsx"
let t: Awaited<ReturnType<typeof tui>> | undefined
afterEach(() => t?.stop())
test("/model opens the picker; ↓ + enter switches", async () => {
t = await tui([])
await t.input.typeText("/model")
t.input.pressEnter()
expect(await t.until("↑↓ move")).toBe(true)
t.input.pressArrow("down")
await t.settle()
t.input.pressEnter()
await t.settle()
expect(t.app.engine.model.ref).toBe("fake/other")
expect(t.frame()).toContain("model: fake/other")
})
test("a prompt runs; a bash call asks; y allows it; the answer renders as markdown", async () => {
t = await tui([
{ chunks: [delta({ content: "Checking." }), toolCall(0, "c1", "bash", '{"command":"echo hi > out.txt","description":"Write out.txt"}'), usage(9000, 40)] },
{ chunks: [delta({ content: "## Done\n\nWrote **out.txt**." }), usage(12000, 60)] },
])
await t.input.typeText("make a file")
t.input.pressEnter()
expect(await t.until("Allow once")).toBe(true)
expect(t.frame()).toContain("echo hi > out.txt")
await t.input.typeText("y")
expect(await t.until("Wrote out.txt.")).toBe(true)
const f = t.frame()
expect(f).toContain("❯ Ask") // the y that answered did not also land in the prompt
expect(f).toContain("┃ make a file")
expect(f).toContain("┊ 💻 bash Write out.txt · exit 0")
expect(f).not.toContain("## Done") // heading marker concealed
expect(f).toContain("ctx 12k/33k 37%")
expect(await Bun.file(`${t.cwd}/out.txt`).text()).toBe("hi\n")
})
test("shift+tab cycles manual → edit → plan → manual; the status bar follows", async () => {
t = await tui([])
expect(t.frame()).toContain(" manual · fake/coder · low")
for (const want of ["edit", "plan", "manual"] as const) {
t.input.pressTab({ shift: true })
await t.settle()
expect(t.app.engine.mode).toBe(want)
expect(t.frame()).toContain(` ${want} · fake/coder`)
}
})
test("deny with a reason sends the reason to the model", async () => {
t = await tui([
{ chunks: [toolCall(0, "c1", "bash", '{"command":"rm hello.txt"}')] },
{ chunks: [delta({ content: "Leaving it." })] },
])
await t.input.typeText("delete it")
t.input.pressEnter()
expect(await t.until("Allow once")).toBe(true)
await t.input.typeText("r")
await t.settle()
await t.input.typeText("keep the file")
t.input.pressEnter()
expect(await t.until("Leaving it.")).toBe(true)
expect(t.fake.requests[1].messages.at(-1).content).toContain("keep the file")
expect(t.frame()).toContain("⛔")
})
test("e edits the command before allowing it: the edited line runs, and the model is told", async () => {
t = await tui([
{ chunks: [toolCall(0, "c1", "bash", '{"command":"echo hi > out.txt"}')] },
{ chunks: [delta({ content: "Noted." })] },
])
await t.input.typeText("make a file")
t.input.pressEnter()
expect(await t.until("Edit, then allow")).toBe(true)
await t.input.typeText("e")
expect(await t.until("run › echo hi > out.txt")).toBe(true)
await t.input.typeText(" && echo edited >> out.txt")
t.input.pressEnter()
expect(await t.until("Noted.")).toBe(true)
expect(await Bun.file(`${t.cwd}/out.txt`).text()).toBe("hi\nedited\n")
expect(t.fake.requests[1].messages.at(-1).content).toContain("The user changed the command before allowing it. What ran: echo hi > out.txt && echo edited >> out.txt")
})
test("↑ recalls the previous prompt", async () => {
t = await tui([{ chunks: [delta({ content: "one" })] }])
await t.input.typeText("first prompt")
t.input.pressEnter()
expect(await t.until("one")).toBe(true)
t.input.pressArrow("up")
await t.settle()
expect(t.frame()).toContain("❯ first prompt")
})
test("/compact replaces the conversation with a summary", async () => {
t = await tui([
{ chunks: [delta({ content: "The answer is 42." })] },
{ chunks: [delta({ content: "## What we are doing\nFinding the answer. It is 42." })] },
])
await t.input.typeText("what is the answer")
t.input.pressEnter()
expect(await t.until("The answer is 42.")).toBe(true)
await t.input.typeText("/compact")
t.input.pressEnter()
expect(await t.until("compacted")).toBe(true)
expect(t.fake.requests[1].messages[0].content).toContain("## Transcript")
expect(t.app.engine.messages[0]).toMatchObject({ role: "user" })
expect(JSON.stringify(t.app.engine.messages[0])).toContain("It is 42.")
})
test("/undo restores the files and puts the prompt back; /redo re-applies; /diff shows the change", async () => {
t = await tui([
{ chunks: [toolCall(0, "c1", "read", '{"path":"hello.txt"}')] },
{ chunks: [toolCall(0, "c2", "edit", JSON.stringify({ path: "hello.txt", old: "world", new: "there" }))] },
{ chunks: [delta({ content: "Changed it." })] },
])
t.app.engine.mode = "edit"
await t.input.typeText("change the greeting")
t.input.pressEnter()
expect(await t.until("Changed it.")).toBe(true)
expect(await Bun.file(`${t.cwd}/hello.txt`).text()).toBe("hello there\n")
await t.input.typeText("/diff")
t.input.pressEnter()
expect(await t.until("changes in this session")).toBe(true)
await t.input.typeText("/undo")
t.input.pressEnter()
expect(await t.until("undone — restored 1 file: hello.txt")).toBe(true)
expect(await Bun.file(`${t.cwd}/hello.txt`).text()).toBe("hello world\n")
expect(t.frame()).toContain("❯ change the greeting")
expect(t.frame()).not.toContain("Changed it.")
expect(t.app.engine.messages).toHaveLength(0)
// clear the restored prompt, then redo
for (let i = 0; i < 20; i++) t.input.pressBackspace()
await t.input.typeText("/redo")
t.input.pressEnter()
expect(await t.until("redone")).toBe(true)
expect(await Bun.file(`${t.cwd}/hello.txt`).text()).toBe("hello there\n")
expect(t.frame()).toContain("Changed it.")
})
test("/commit drafts a message from the session's files, and commits only those on confirm", async () => {
t = await tui([
{ chunks: [toolCall(0, "c1", "write", JSON.stringify({ path: "notes.md", content: "# notes\n" }))] },
{ chunks: [delta({ content: "Wrote notes." })] },
{ chunks: [delta({ content: "```\nAdd notes file\n```" })] },
])
const git = (...a: string[]) => Bun.spawnSync(["git", "-C", t!.cwd, "-c", "user.name=t", "-c", "user.email=t@t", "-c", "commit.gpgsign=false", ...a])
git("add", "hello.txt")
git("commit", "-q", "-m", "Initial")
Bun.write(`${t.cwd}/unrelated.txt`, "not mine\n")
t.app.engine.mode = "edit"
await t.input.typeText("write notes")
t.input.pressEnter()
expect(await t.until("Wrote notes.")).toBe(true)
await t.input.typeText("/commit")
t.input.pressEnter()
expect(await t.until("Commit 1 file?")).toBe(true)
expect(t.frame()).toContain("Add notes file")
process.env.GIT_AUTHOR_NAME = process.env.GIT_COMMITTER_NAME = "t"
process.env.GIT_AUTHOR_EMAIL = process.env.GIT_COMMITTER_EMAIL = "t@t"
t.input.pressEnter()
expect(await t.until("committed")).toBe(true)
const log = git("log", "--format=%s", "--name-only", "-1").stdout.toString()
expect(log).toContain("Add notes file")
expect(log).toContain("notes.md")
expect(log).not.toContain("unrelated.txt")
})
test("the question card: pick by number, multiple with space, ask back — and the model gets it all", async () => {
t = await tui([
{
chunks: [
toolCall(
0,
"q1",
"ask_user",
JSON.stringify({
why: "Before I start.",
questions: [
{ header: "DB", question: "Which database?", options: [{ label: "SQLite" }, { label: "PostgreSQL", recommended: true, description: "the server one" }] },
{ header: "Layers", question: "Which layers?", multiple: true, options: ["API", "CLI", "Web"] },
{ header: "Deploy", question: "Where?", options: ["Docker", "Bare"] },
],
}),
),
],
},
{ chunks: [delta({ content: "Docker adds a minute. Asking again." })] },
])
await t.input.typeText("plan it")
t.input.pressEnter()
expect(await t.until("Which database?")).toBe(true)
const f = t.frame()
expect(f).toContain("Before I start.")
expect(f.indexOf("PostgreSQL (Recommended)")).toBeLessThan(f.indexOf("SQLite")) // recommended first
expect(f).toContain("Something else…")
expect(f).toContain("Ask back…")
await t.input.typeText("1")
expect(await t.until("Which layers?")).toBe(true)
t.input.pressKey(" ")
t.input.pressArrow("down")
await t.settle()
t.input.pressKey(" ")
t.input.pressEnter()
expect(await t.until("Where?")).toBe(true)
for (let i = 0; i < 3; i++) t.input.pressArrow("down")
await t.settle()
t.input.pressEnter()
await t.settle()
await t.input.typeText("what does docker cost?")
t.input.pressEnter()
expect(await t.until("Asking again.")).toBe(true)
const result = t.fake.requests[1].messages.at(-1).content as string
expect(result).toContain("→ PostgreSQL (the one you recommended)")
expect(result).toContain("→ API, CLI")
expect(result).toContain('↩ counter-question: "what does docker cost?"')
})
test("esc dismisses the questions; the todo panel shows open work", async () => {
t = await tui([
{ chunks: [toolCall(0, "t1", "todo", JSON.stringify({ todos: [{ content: "read the code", status: "completed" }, { content: "fix the bug", status: "in_progress" }, { content: "run tests", status: "pending" }] })), toolCall(1, "q1", "ask_user", '{"question":"Go on?","options":["yes","no"]}')] },
{ chunks: [delta({ content: "Carrying on." })] },
])
await t.input.typeText("go")
t.input.pressEnter()
expect(await t.until("Go on?")).toBe(true)
expect(t.frame()).toContain("todo 1/3")
expect(t.frame()).toContain("▸ fix the bug")
t.input.pressEscape()
expect(await t.until("Carrying on.")).toBe(true)
expect(t.fake.requests[1].messages.at(-1).content).toContain("dismissed")
})
test("@ lists files; tab takes one; the model receives the file", async () => {
t = await tui([{ chunks: [delta({ content: "It says hello world." })] }])
await t.input.typeText("what does @hel")
expect(await t.until("hello.txt")).toBe(true)
t.input.pressTab()
await t.settle()
expect(t.frame()).toContain("❯ what does @hello.txt")
await t.input.typeText("say?")
t.input.pressEnter()
expect(await t.until("It says hello world.")).toBe(true)
const user = t.fake.requests[0].messages.at(-1).content as string
expect(user).toContain("what does @hello.txt say?")
expect(user).toContain('<file path="hello.txt">\n1: hello world\n</file>')
expect(t.frame()).toContain("attached hello.txt")
expect(t.frame()).not.toContain("<file path")
})
test("/ fuzzy-lists commands; a custom command sends its expanded body", async () => {
const { mkdirSync, writeFileSync } = await import("node:fs")
const { join } = await import("node:path")
const { paths } = await import("../../src/config/paths.ts")
mkdirSync(join(paths.config, "commands"), { recursive: true })
writeFileSync(join(paths.config, "commands", "explain.md"), "---\ndescription: explain a file\n---\nExplain $1 in one line.\n")
t = await tui([{ chunks: [delta({ content: "A greeting." })] }])
await t.input.typeText("/und")
expect(await t.until("/undo")).toBe(true)
for (let i = 0; i < 4; i++) t.input.pressBackspace()
await t.input.typeText("/expl")
expect(await t.until("explain a file")).toBe(true)
t.input.pressTab()
await t.input.typeText("hello.txt")
t.input.pressEnter()
expect(await t.until("A greeting.")).toBe(true)
expect(t.fake.requests[0].messages.at(-1).content).toContain("Explain hello.txt in one line.")
expect(t.frame()).toContain("┃ /explain hello.txt")
})
test("!command runs at once and its output goes with the next prompt", async () => {
t = await tui([{ chunks: [delta({ content: "Seen it." })] }])
await t.input.typeText("!echo from-the-shell")
t.input.pressEnter()
expect(await t.until("goes to the model with your next message")).toBe(true)
expect(t.frame()).toContain("! echo from-the-shell")
await t.input.typeText("did you see that?")
t.input.pressEnter()
expect(await t.until("Seen it.")).toBe(true)
const user = t.fake.requests[0].messages.at(-1).content as string
expect(user).toContain('<shell command="echo from-the-shell">')
expect(user).toContain("from-the-shell")
})
test("thinking stops the clock when the model starts a call, even if the call then waits for approval", async () => {
t = await tui([
{ chunks: [delta({ reasoning_content: "I should write a file." }), toolCall(0, "c1", "bash", '{"command":"echo x > y.txt"}')] },
{ chunks: [delta({ content: "done" })] },
])
await t.input.typeText("go")
t.input.pressEnter()
expect(await t.until("Allow once")).toBe(true)
await Bun.sleep(1200)
await t.settle()
expect(t.frame()).toMatch(/Thought for 0\.\ds · ~\d+ tokens?/)
await t.input.typeText("n")
expect(await t.until("done")).toBe(true)
})
test("pasting an image's path (quoted, as a file manager does) inserts an @reference", async () => {
t = await tui([])
await Bun.write(`${t.cwd}/my shot.png`, "x")
await t.input.pasteBracketedText(`'${t.cwd}/my shot.png'`)
await t.settle()
expect(t.frame()).toContain(`@${t.cwd}/my\\ shot.png`)
})
test("the plan card: the plan shown as markdown; a approves and the status bar says edit", async () => {
t = await tui([
{ chunks: [toolCall(0, "w1", "write", JSON.stringify({ path: ".agent/plans/2026-09-29-x.md", content: "# The plan\n\n## Changes\n- **one** thing\n" }))] },
{ chunks: [toolCall(0, "x1", "plan_submit", '{"path":".agent/plans/2026-09-29-x.md"}')] },
{ chunks: [delta({ content: "Implementing now." })] },
])
await t.input.typeText("/plan tidy up")
t.input.pressEnter()
expect(await t.until("Approve — edit mode")).toBe(true)
expect(await t.until("one thing")).toBe(true) // markdown is parsed off-thread; it draws a moment later
const f = t.frame()
expect(f).toContain("plan · .agent/plans/2026-09-29-x.md")
const card = f.slice(f.indexOf("╭─ plan ·"))
expect(card).toContain("The plan")
expect(card).toContain("one thing")
expect(card).not.toContain("**one**") // rendered, not raw
expect(f).toContain(" plan · fake/coder")
await t.input.typeText("a")
expect(await t.until("Implementing now.")).toBe(true)
expect(t.frame()).toContain(" edit · fake/coder")
})
test("thinking is one line — its text is not shown; the provider's count of it is; ctrl+r opens it", async () => {
t = await tui([
{
chunks: [
delta({ reasoning_content: "First I weigh the options.\nThen the SECRET-INNER-STEP." }),
delta({ content: "The answer." }),
{ choices: [], usage: { prompt_tokens: 10, completion_tokens: 60, completion_tokens_details: { reasoning_tokens: 1234 } } },
],
},
])
await t.input.typeText("go")
t.input.pressEnter()
expect(await t.until("The answer.")).toBe(true)
expect(await t.until("· 1,234 tokens")).toBe(true)
expect(t.frame()).toMatch(/Thought for \d+\.\ds · 1,234 tokens/)
expect(t.frame()).not.toContain("SECRET-INNER-STEP")
t.input.pressKey("r", { ctrl: true })
expect(await t.until("SECRET-INNER-STEP")).toBe(true)
})
test("while it thinks: the thought says only Thinking…; the line at the bottom has the verb, the task's time and tokens", async () => {
t = await tui([{ gapMs: 1500, chunks: [delta({ reasoning_content: "Weighing it up: HIDDEN-WHILE-THINKING and more words here." }), delta({ content: "Done thinking." })] }])
await t.input.typeText("go")
t.input.pressEnter()
expect(await t.until("~15 tokens")).toBe(true)
const frame = t.frame()
expect(frame).toMatch(/^\s*Thinking\.{1,3}\s*$/m)
expect(frame).toMatch(/(pondering|contemplating|musing|cogitating|ruminating|deliberating|mulling|reflecting|reasoning|analyzing|synthesizing|formulating)… \d+s · ~15 tokens · esc to stop/)
expect(frame).not.toContain("HIDDEN-WHILE-THINKING")
expect(await t.until("Done thinking.")).toBe(true)
expect(t.frame()).toMatch(/Thought for \d\.\ds · ~15 tokens/)
})
test("the line at the bottom counts the whole task: the clock and the tokens go on across steps", async () => {
t = await tui([
{ chunks: [toolCall(0, "c1", "list", "{}"), { choices: [], usage: { prompt_tokens: 10, completion_tokens: 40 } }] },
{ gapMs: 1200, chunks: [delta({ content: "Half" }), delta({ content: " way." }), { choices: [], usage: { prompt_tokens: 20, completion_tokens: 25 } }] },
])
await t.input.typeText("go")
t.input.pressEnter()
// The first step's 40 tokens, then the second's reply growing on top of them.
expect(await t.until("Half")).toBe(true)
// The spinner line redraws on its own tick, slower than the reply's text: on a slow machine the
// frame that first shows "Half" can still carry the line from before. The 1.2 s gap holds this
// state, so wait for the line within it rather than reading the first frame.
const counted = /writing… \d+s · ~4\d tokens · esc to stop/
for (let i = 0; i < 20 && !counted.test(t.frame()); i++) await Bun.sleep(50), await t.settle()
expect(t.frame()).toMatch(counted)
expect(await t.until("way.")).toBe(true)
for (let i = 0; i < 60 && t.frame().includes("esc to stop"); i++) await Bun.sleep(100), await t.settle()
expect(t.frame()).not.toContain("esc to stop")
expect(t.view.taskTokens()).toEqual({ n: 65, estimated: false })
})
test("many reasoning blocks do not pile up resize listeners (Node's leak warning printed over the screen)", async () => {
const turn = (n: number) => ({ chunks: [delta({ reasoning_content: `thinking ${n}` }), delta({ content: `answer ${n}` })] })
t = await tui(Array.from({ length: 15 }, (_, i) => turn(i)))
const before = t.setup.renderer.listenerCount("resize")
for (let i = 0; i < 15; i++) {
await t.input.typeText(`q${i}`)
t.input.pressEnter()
expect(await t.until(`answer ${i}`)).toBe(true)
}
expect(t.setup.renderer.listenerCount("resize")).toBeLessThanOrEqual(before + 1)
}, 60_000)
test("/review sends the session's diff with the review brief in plan mode, then the mode is back", async () => {
t = await tui([
{ chunks: [toolCall(0, "c1", "read", '{"path":"hello.txt"}')] },
{ chunks: [toolCall(0, "c2", "edit", JSON.stringify({ path: "hello.txt", old: "world", new: "there" }))] },
{ chunks: [delta({ content: "Changed." })] },
{ chunks: [delta({ content: "No findings." })] },
])
t.app.engine.mode = "edit"
await t.input.typeText("change it")
t.input.pressEnter()
expect(await t.until("Changed.")).toBe(true)
await t.input.typeText("/review")
t.input.pressEnter()
expect(await t.until("No findings.")).toBe(true)
const req = t.fake.requests.at(-1)
expect(req.messages[0].content).toContain("Permission mode: plan")
const user = req.messages.at(-1).content as string
expect(user).toContain("Findings first, ordered by severity")
expect(user).toContain("+hello there")
expect(t.frame()).toContain("┃ /review")
expect(t.app.engine.mode).toBe("edit")
})
test("/decide records a decision; /tasks shows the board", async () => {
t = await tui([])
const { mkdirSync } = await import("node:fs")
mkdirSync(`${t.cwd}/.agent`)
await t.input.typeText("/decide Storage: SQLite, one file")
t.input.pressEnter()
expect(await t.until("recorded:")).toBe(true)
expect(await Bun.file(`${t.cwd}/.agent/decisions.md`).text()).toContain("**Decision:** SQLite, one file")
await t.input.typeText("/tasks")
t.input.pressEnter()
expect(await t.until("the board is empty")).toBe(true)
})
test("a skill is a /command: listed in the palette, and it sends the skill with the instruction", async () => {
const { mkdirSync, writeFileSync, rmSync } = await import("node:fs")
const { join } = await import("node:path")
const { paths } = await import("../../src/config/paths.ts")
const dir = join(paths.config, "skills", "tidy-imports")
mkdirSync(dir, { recursive: true })
writeFileSync(join(dir, "SKILL.md"), "---\nname: tidy-imports\ndescription: Use when imports need sorting.\n---\n\nSort imports alphabetically.\n")
t = await tui([{ chunks: [delta({ content: "Sorted." })] }])
await t.input.typeText("/tidy")
expect(await t.until("Use when imports need sorting.")).toBe(true)
t.input.pressTab()
await t.input.typeText("in app.ts")
t.input.pressEnter()
expect(await t.until("Sorted.")).toBe(true)
const sent = t.fake.requests[0].messages.at(-1).content
expect(sent).toContain('invoked the "tidy-imports" skill')
expect(sent).toContain("Sort imports alphabetically.")
expect(sent).toContain("The user's instruction with it: in app.ts")
// the skills list was in the system prompt the session started with
expect(t.fake.requests[0].messages[0].content).toContain("- tidy-imports: Use when imports need sorting.")
rmSync(join(paths.config, "skills"), { recursive: true, force: true })
})
test("/personality picks one and remembers it; /memory lists entries, and forgets or changes one", async () => {
const { readFileSync, rmSync, existsSync } = await import("node:fs")
const { join } = await import("node:path")
const { paths } = await import("../../src/config/paths.ts")
t = await tui([])
await t.input.typeText("/personality pirate")
t.input.pressEnter()
expect(await t.until("personality: one of none, concise")).toBe(true)
await t.input.typeText("/personality formal")
t.input.pressEnter()
expect(await t.until("personality: formal")).toBe(true)
expect(readFileSync(join(paths.config, "config.yaml"), "utf8")).toContain("personality: formal")
await t.input.typeText("/memory")
t.input.pressEnter()
expect(await t.until("nothing remembered yet")).toBe(true)
t.app.memory.apply("user", [{ action: "add", content: "The user is called Ana." }, { action: "add", content: "Ana likes tabs." }])
await t.input.typeText("/memory")
t.input.pressEnter()
expect(await t.until("[user] The user is called Ana.")).toBe(true)
await t.input.typeText("tabs")
await t.settle()
t.input.pressEnter()
expect(await t.until("Forget it")).toBe(true)
t.input.pressEnter()
expect(await t.until("user: forgotten")).toBe(true)
expect(t.app.memory.entries("user")).toEqual(["The user is called Ana."])
if (existsSync(join(paths.config, "config.yaml"))) rmSync(join(paths.config, "config.yaml"))
rmSync(t.app.memory.file("user"), { force: true })
}, 30_000)
test("MCP: a server's prompt is a /server:prompt command; /mcp lists the server; its tool line fits at 80 columns", async () => {
const { join } = await import("node:path")
const server = join(import.meta.dir, "..", "fixtures", "mcp", "server.ts")
t = await tui(
[
{ chunks: [toolCall(0, "c1", "mcp__fix__echo", '{"text":"hi"}')] },
{ chunks: [delta({ content: "Greeted." })] },
],
{ width: 80, height: 24 },
`mode: unrestricted\nmcp:\n fix:\n command: ["${process.execPath}", "${server}"]\n`,
)
await t.app.mcpReady
await t.input.typeText("/fix:gr")
expect(await t.until("Greet someone")).toBe(true)
t.input.pressTab()
await t.input.typeText("Jaro")
t.input.pressEnter()
expect(await t.until("Greeted.")).toBe(true)
expect(t.fake.requests[0].messages.at(-1).content).toContain("Say hello to Jaro.")
expect(t.frame()).toContain("🔌")
for (const line of t.frame().split("\n")) expect(Bun.stringWidth(line)).toBeLessThanOrEqual(80)
await t.input.typeText("/mcp")
t.input.pressEnter()
expect(await t.until("● fix local")).toBe(true)
}, 20_000)
test("MCP: a server's prompt cannot attach a file by naming it with @", async () => {
const { join } = await import("node:path")
const { mkdtempSync, writeFileSync } = await import("node:fs")
const { tmpdir } = await import("node:os")
const server = join(import.meta.dir, "..", "fixtures", "mcp", "server.ts")
const secret = join(mkdtempSync(join(tmpdir(), "ph-secret-")), "id_key")
writeFileSync(secret, "PRIVATE-KEY-MATERIAL")
t = await tui([{ chunks: [delta({ content: "Looked." })] }], { width: 100, height: 30 }, `mcp:\n fix:\n command: ["${process.execPath}", "${server}"]\n`)
await t.app.mcpReady
await t.input.typeText(`/fix:peek ${secret}`)
t.input.pressEnter()
expect(await t.until("Looked.")).toBe(true)
const sent = JSON.stringify(t.fake.requests[0].messages.at(-1))
expect(sent).toContain("Look at @")
expect(sent).not.toContain("PRIVATE-KEY-MATERIAL")
}, 20_000)
test("voice: ctrl+t records, silence ends it, what was said lands in the prompt; /speak and /voice", async () => {
const { join } = await import("node:path")
const stt = Bun.serve({ port: 0, fetch: () => Response.json({ text: "Fix the failing test." }) })
const mic = join(import.meta.dir, "..", "fixtures", "voice", "mic.ts")
try {
t = await tui([], { width: 100, height: 30 }, `voice:\n stt: { base_url: "http://127.0.0.1:${stt.port}/v1" }\n tts: { base_url: "http://127.0.0.1:${stt.port}/v1" }\n recorder_command: ["${process.execPath}", "${mic}", "1"]\n silence_seconds: 0.4\n`)
expect(t.frame()).toContain("🎤 Ctrl+T")
t.input.pressKey("t", { ctrl: true })
expect(await t.until("● REC")).toBe(true)
expect(await t.until("❯ Fix the failing test.", 8000)).toBe(true)
expect(t.fake.requests).toHaveLength(0) // drafted, not sent
for (let i = 0; i < 25; i++) t.input.pressBackspace()
await t.input.typeText("/speak")
t.input.pressEnter()
expect(await t.until("replies are spoken")).toBe(true)
expect(await t.until("🔊 on")).toBe(true)
await t.input.typeText("/voice")
t.input.pressEnter()
expect(await t.until("input: openai · http://127.0.0.1")).toBe(true)
} finally {
stt.stop(true)
}
}, 20_000)
test("/release: the plan, then the dialog; Release commits and tags, pushing nothing; /changelog sends the commits", async () => {
const { writeFileSync } = await import("node:fs")
const { join } = await import("node:path")
const { git } = await import("../../src/git/run.ts")
t = await tui([{ chunks: [delta({ content: "Updated the changelog." })] }])
const d = t.cwd
writeFileSync(join(d, "package.json"), '{ "name": "demo", "version": "0.1.0" }\n')
writeFileSync(join(d, "CHANGELOG.md"), "# Changelog\n\n## [Unreleased]\n\n### Added\n- Things.\n")
for (const a of [["config", "user.email", "t@e"], ["config", "user.name", "T"], ["config", "commit.gpgsign", "false"], ["config", "tag.gpgsign", "false"], ["add", "package.json", "CHANGELOG.md", "hello.txt"], ["commit", "-q", "-m", "Add the demo"]]) git(d, a)
;(t.app.project as any).gitRoot = d
await t.input.typeText("/changelog")
t.input.pressEnter()
expect(await t.until("Updated the changelog.")).toBe(true)
expect(t.fake.requests[0].messages.at(-1).content).toContain("- Add the demo")
await t.input.typeText("/release minor")
t.input.pressEnter()
expect(await t.until("Release v0.2.0?")).toBe(true)
expect(t.frame()).toContain("package.json: 0.1.0 → 0.2.0")
t.input.pressEnter()
expect(await t.until("nothing pushed")).toBe(true)
expect(git(d, ["tag", "-l", "--format=%(contents)", "v0.2.0"]).out).toBe("demo 0.2.0\n\n### Added\n- Things.")
})
test("asking about an edit shows the change itself, not only the file", async () => {
t = await tui([
{ chunks: [toolCall(0, "r1", "read", '{"path":"hello.txt"}')] },
{ chunks: [toolCall(0, "e1", "edit", JSON.stringify({ path: "hello.txt", old: "hello world", new: "hello lembas" }))] },
{ chunks: [delta({ content: "Changed." })] },
])
await t.input.typeText("change it")
t.input.pressEnter()
expect(await t.until("Allow once")).toBe(true)
const f = t.frame()
expect(f).toContain("hello.txt +1 −1")
expect(f).toMatch(/-\s*hello world/)
expect(f).toMatch(/\+\s*hello lembas/)
expect(await Bun.file(`${t.cwd}/hello.txt`).text()).toBe("hello world\n") // nothing written while asking
await t.input.typeText("y")
expect(await t.until("Changed.")).toBe(true)
expect(await Bun.file(`${t.cwd}/hello.txt`).text()).toBe("hello lembas\n")
})
test("a change that cannot apply says so while asking", async () => {
t = await tui([
{ chunks: [toolCall(0, "r1", "read", '{"path":"hello.txt"}')] },
{ chunks: [toolCall(0, "e1", "edit", JSON.stringify({ path: "hello.txt", old: "not there", new: "x" }))] },
{ chunks: [delta({ content: "ok" })] },
])
await t.input.typeText("change it")
t.input.pressEnter()
expect(await t.until("Allow once")).toBe(true)
expect(t.frame()).toContain("this will fail:")
})
test("/copy copies the last reply and says how", async () => {
t = await tui([{ chunks: [delta({ content: "## Plan\n\nUse **ws**." })] }])
await t.input.typeText("go")
t.input.pressEnter()
expect(await t.until("Use ws.")).toBe(true)
await t.input.typeText("/copy")
t.input.pressEnter()
expect(await t.until("copied 20 characters")).toBe(true)
})
test("dragging over a reply copies what it selects", async () => {
t = await tui([{ chunks: [delta({ content: "SELECTME please copy this line" })] }])
await t.input.typeText("go")
t.input.pressEnter()
expect(await t.until("SELECTME")).toBe(true)
const rows = t.frame().split("\n")
const y = rows.findIndex((l) => l.includes("SELECTME"))
const x = rows[y]!.indexOf("SELECTME")
await t.setup.mockMouse.drag(x, y, x + 8, y) // both ends are in: 9 cells, "SELECTME "
expect(await t.until("copied 9 characters")).toBe(true)
// In the status bar, not the conversation; and gone again by itself.
expect(t.frame().split("\n").filter((l) => l.trim()).at(-1)).toContain("copied 9 characters")
expect(t.view.state.items.some((i) => JSON.stringify(i).includes("copied 9"))).toBe(false)
})
test("copying while the model thinks does not split the thinking block", async () => {
t = await tui([
{ chunks: [delta({ content: "FIRST answer" })] },
{ chunks: [delta({ reasoning_content: "weighing it" }), ...Array.from({ length: 40 }, () => delta({ reasoning_content: " more" })), delta({ content: "SECOND answer" })], gapMs: 30 },
])
await t.input.typeText("one")
t.input.pressEnter()
expect(await t.until("FIRST answer")).toBe(true)
await t.input.typeText("two")
t.input.pressEnter()
// While the thought streams: copy the first answer.
const end = Date.now() + 3000
while (!t.view.state.items.some((i) => i.kind === "reasoning" && i.streaming) && Date.now() < end) await t.settle(20)
await t.input.typeText("/copy 1")
t.input.pressEnter()
expect(await t.until("copied 12 characters")).toBe(true)
expect(await t.until("SECOND answer")).toBe(true)
expect(t.view.state.items.filter((i) => i.kind === "reasoning")).toHaveLength(1)
expect(t.view.state.items.some((i) => JSON.stringify(i).includes("copied"))).toBe(false)
})
test("/icons plain swaps every picture for a one-column symbol, and is saved", async () => {
t = await tui([{ chunks: [toolCall(0, "r1", "read", '{"path":"hello.txt"}')] }, { chunks: [delta({ content: "ok" })] }])
await t.input.typeText("/icons plain")
t.input.pressEnter()
expect(await t.until("icons: plain")).toBe(true)
await t.input.typeText("go")
t.input.pressEnter()
expect(await t.until("┊ » read")).toBe(true)
expect(t.frame()).not.toContain("📖")
const { readFileSync } = await import("node:fs")
const { join } = await import("node:path")
const { paths } = await import("../../src/config/paths.ts")
expect(readFileSync(join(paths.config, "config.yaml"), "utf8")).toContain("icons: plain")
})
test("the frame rate follows every capability answer, not just the first", async () => {
const { frameRateFor } = await import("../../src/tui/index.tsx")
const r = { targetFps: 30, maxFps: 60 }
frameRateFor(r, { sync: false }) // an early answer, before the one about mode 2026
expect(r).toEqual({ targetFps: 12, maxFps: 12 })
frameRateFor(r, { sync: true }) // kitty, WezTerm, foot… once their answer arrives
expect(r).toEqual({ targetFps: 30, maxFps: 60 })
})
test("/terminal says what was found out about the terminal", async () => {
t = await tui([])
await t.input.typeText("/terminal")
t.input.pressEnter()
// The test renderer asks nothing of a terminal; the frame rate and mouse are said regardless.
expect(await t.until("frame rate:")).toBe(true)
})
test("/checkpoint saves the files; /checkpoints goes back to one and tells the model", async () => {
t = await tui([])
await t.input.typeText("/checkpoint before the rewrite")
t.input.pressEnter()
expect(await t.until("checkpoint saved: before the rewrite")).toBe(true)
await Bun.write(`${t.cwd}/hello.txt`, "rewritten\n")
await t.input.typeText("/checkpoints")
t.input.pressEnter()
expect(await t.until("Checkpoints")).toBe(true)
t.input.pressEnter()
expect(await t.until("1 file differ")).toBe(true)
expect(t.frame()).toContain("Go back to it")
t.input.pressEnter()
expect(await t.until('back at "before the rewrite" — restored 1 file')).toBe(true)
expect(await Bun.file(`${t.cwd}/hello.txt`).text()).toBe("hello world\n")
expect(t.app.engine.pendingContext.at(-1)).toContain('restored the files to the checkpoint "before the rewrite"')
expect(t.app.snapshots!.checkpoints().map((c) => c.label)).toEqual(['before going back to "before the rewrite"', "before the rewrite"])
})
test("/branch makes and switches; the status bar shows the branch; the list marks the current one", async () => {
t = await tui([])
const sh = (...a: string[]) => Bun.spawnSync(["git", "-C", t!.cwd, "-c", "user.name=t", "-c", "user.email=t@t", "-c", "commit.gpgsign=false", ...a])
sh("add", "hello.txt")
sh("commit", "-q", "-m", "first")
await t.input.typeText("/branch feature/greeting")
t.input.pressEnter()
expect(await t.until("made feature/greeting from")).toBe(true)
expect(await t.until("@feature/greeting")).toBe(true)
expect(t.app.engine.pendingContext.at(-1)).toContain("switched the repository to the branch feature/greeting")
await t.input.typeText("/branch")
t.input.pressEnter()
expect(await t.until("Branches")).toBe(true)
expect(t.frame()).toContain("New branch…")
expect(t.frame()).toMatch(/feature\/greeting/)
})
test("a long edit's diff shows short in the transcript — and parses — then whole with ctrl+o", async () => {
t = await tui([
{ chunks: [toolCall(0, "w1", "write", JSON.stringify({ path: "long.txt", content: Array.from({ length: 50 }, (_, i) => `row ${i + 1}`).join("\n") + "\n" }))] },
{ chunks: [delta({ content: "Wrote it." })] },
], { width: 100, height: 60 })
t.app.engine.mode = "edit"
await t.input.typeText("write long.txt")
t.input.pressEnter()
expect(await t.until("Wrote it.")).toBe(true)
expect(t.frame()).not.toContain("Error parsing diff")
expect(t.frame()).toContain("row 1")
expect(t.frame()).toMatch(/… \d+ more lines \(ctrl\+o\)/)
expect(t.frame()).not.toContain("row 50")
t.input.pressKey("o", { ctrl: true })
expect(await t.until("row 50")).toBe(true)
expect(t.frame()).not.toContain("more lines (ctrl+o)")
expect(t.frame()).not.toContain("Error parsing diff")
})
for (const size of [{ width: 100, height: 30 }, { width: 80, height: 24 }])
test(`a long command to approve stays on screen at ${size.width}x${size.height}: its purpose, a few rows, the choices; ctrl+o shows the rest`, async () => {
const command = Array.from({ length: 60 }, (_, i) => `echo step-${i + 1}`).join("\n")
t = await tui([{ chunks: [toolCall(0, "c1", "bash", JSON.stringify({ command, description: "Print every step so we can see where the build stops" }))] }, { chunks: [delta({ content: "done" })] }], size)
await t.input.typeText("go")
t.input.pressEnter()
expect(await t.until("Allow once")).toBe(true)
const rows = t.frame().split("\n")
expect(rows.filter((l) => l.trim()).at(-1)).toContain("fake/coder") // the status bar is still the last row
expect(t.frame()).toContain("Print every step so we can see where the build stops")
expect(t.frame()).toContain("echo step-1")
expect(t.frame()).not.toContain("echo step-60")
expect(t.frame()).toMatch(/… \d+ more lines — ctrl\+o shows all/)
t.input.pressKey("o", { ctrl: true })
await t.settle()
for (let i = 0; i < 12; i++) t.input.pressKey("\u001b[6~") // PageDown
const end = await t.until("echo step-60", 3000)
if (!end) console.error(t.frame())
expect(end).toBe(true)
expect(t.frame()).toContain("Allow once")
expect(t.frame().split("\n").filter((l) => l.trim()).at(-1)).toContain("fake/coder")
await t.input.typeText("y")
expect(await t.until("done")).toBe(true)
}, 20_000)
test("/settings: pick a setting, a value and where it holds; the status bar follows; global is written", async () => {
t = await tui([])
await t.input.typeText("/settings")
t.input.pressEnter()
expect(await t.until("Settings")).toBe(true)
await t.input.typeText("titles")
await t.settle()
t.input.pressEnter()
expect(await t.until("titles — now model")).toBe(true)
t.input.pressArrow("down")
await t.settle()
t.input.pressEnter()
expect(await t.until("— where?")).toBe(true)
t.input.pressArrow("down")
await t.settle()
t.input.pressEnter()
expect(await t.until("titles = prompt in the global config")).toBe(true)
expect(await Bun.file(`${process.env.LEMBAS_HOME}/.config/lembas/config.yaml`).text()).toContain("titles: prompt")
// A number is typed: the first row takes what was typed.
await t.input.typeText("/settings limits.steps")
t.input.pressEnter()
expect(await t.until("limits.steps — now")).toBe(true)
await t.input.typeText("40")
await t.settle()
expect(t.frame()).toContain('set to "40"')
t.input.pressEnter()
expect(await t.until("— where?")).toBe(true)
t.input.pressEnter()
expect(await t.until("limits.steps = 40 for this session")).toBe(true)
expect(t.app.engine.o.maxSteps).toBe(40)
}, 30_000)
test("the agent changes the effort when asked: the approval says so, and the status bar shows it", async () => {
t = await tui([
{ chunks: [toolCall(0, "c1", "settings", '{"action":"set","key":"effort","value":"high","purpose":"You asked me to think harder"}')] },
{ chunks: [delta({ content: "Thinking harder now." })] },
])
await t.input.typeText("think harder from now on")
t.input.pressEnter()
expect(await t.until("Allow once")).toBe(true)
expect(t.frame()).toContain("You asked me to think harder")
await t.input.typeText("y")
expect(await t.until("Thinking harder now.")).toBe(true)
expect(t.app.engine.effort).toBe("high")
expect(t.frame()).toContain("fake/coder · high")
})
test("enter while it works: the message waits above the box, then goes in at the next step, shown where it went", async () => {
t = await tui([
{ gapMs: 400, chunks: [delta({ content: "Looking." }), toolCall(0, "c1", "list", "{}")] },
{ chunks: [delta({ content: "Noted the change." }, "stop")] },
])
await t.input.typeText("go")
t.input.pressEnter()
expect(await t.until("Looking.")).toBe(true)
await t.input.typeText("use b instead")
t.input.pressEnter()
expect(await t.until("↳ queued use b instead · at the next step")).toBe(true)
expect(await t.until("Noted the change.")).toBe(true)
await t.settle()
const f = t.frame()
expect(f).toContain("↳ sent while it worked")
expect(f).not.toContain("↳ queued")
expect(f.indexOf("use b instead")).toBeLessThan(f.indexOf("Noted the change."))
expect(JSON.stringify(t.fake.requests[1].messages.at(-1))).toContain("use b instead")
})
test("a command refused while it works keeps its text in the box", async () => {
t = await tui([{ gapMs: 400, chunks: [delta({ content: "Slow" }), delta({ content: " reply." }, "stop")] }])
await t.input.typeText("go")
t.input.pressEnter()
expect(await t.until("Slow")).toBe(true)
await t.input.typeText("/undo")
t.input.pressEnter()
expect(await t.until("wait for the reply to finish")).toBe(true)
expect(t.frame()).toMatch(/❯\s*\/undo/)
})
test("esc mid-reply, then ctrl+g: it continues, and the model sees what it had already written", async () => {
t = await tui([
{ gapMs: 300, chunks: [delta({ content: "Step one is done. " }), delta({ content: "Step two" }), delta({ content: " is next." }, "stop")] },
{ chunks: [delta({ content: "Step two is done too." }, "stop")] },
])
await t.input.typeText("do the steps")
t.input.pressEnter()
expect(await t.until("Step one is done.")).toBe(true)
t.input.pressEscape()
expect(await t.until("cancelled")).toBe(true)
await t.settle()
t.input.pressKey("g", { ctrl: true })
expect(await t.until("↻ continue")).toBe(true)
expect(await t.until("Step two is done too.")).toBe(true)
const sent = t.fake.requests[1].messages as { role: string; content: unknown }[]
// What it had said before the stop, then the request to carry on.
expect(sent.at(-2)).toMatchObject({ role: "assistant" })
expect(JSON.stringify(sent.at(-2)!.content)).toContain("Step one is done.")
expect(JSON.stringify(sent.at(-1)!.content)).toContain("Continue from where you stopped")
})
test("/continue with nothing to continue, and while it works, says so", async () => {
t = await tui([{ gapMs: 300, chunks: [delta({ content: "Working" }), delta({ content: " on it." }, "stop")] }])
await t.input.typeText("/continue")
t.input.pressEnter()
expect(await t.until("nothing to continue yet")).toBe(true)
await t.input.typeText("go")
t.input.pressEnter()
expect(await t.until("Working")).toBe(true)
t.input.pressKey("g", { ctrl: true })
expect(await t.until("it is still working")).toBe(true)
})
test("/agents lists the subagents; /agents new writes a starter; /agents <name> <task> hands it to the model", async () => {
t = await tui([{ chunks: [delta({ content: "Handing it over." }, "stop")] }])
await t.input.typeText("/agents")
t.input.pressEnter()
expect(await t.until("explore (builtin)")).toBe(true)
expect(t.frame()).toContain("general (builtin)")
const name = `scout${Date.now() % 100000}`
await t.input.typeText(`/agents new ${name}`)
t.input.pressEnter()
expect(await t.until(`${name}.md`)).toBe(true)
const file = Bun.file(`${process.env.LEMBAS_HOME}/.config/lembas/agents/${name}.md`)
expect(await file.text()).toContain(`You are the ${name} subagent`)
await t.input.typeText("/agents explore where is the config loaded")
t.input.pressEnter()
expect(await t.until("Handing it over.")).toBe(true)
expect(JSON.stringify(t.fake.requests[0].messages.at(-1))).toContain('agent: \\"explore\\"')
})
test("while the server reads a long prompt: the line at the bottom says how far it is, then the reply takes over", async () => {
const pp = (processed: number, ms: number) => ({ choices: [{ index: 0, delta: { role: "assistant", content: null }, finish_reason: null }], prompt_progress: { total: 140000, cache: 10000, processed, time_ms: ms } })
t = await tui([{ gapMs: 500, chunks: [pp(0, 0), pp(80000, 160000), delta({ content: "Read it all." }, "stop")] }])
await t.input.typeText("go")
t.input.pressEnter()
expect(await t.until("reading the prompt 64% · 90k of 140k · ~1m 40s left")).toBe(true)
expect(await t.until("Read it all.")).toBe(true)
await t.settle()
expect(t.frame()).not.toContain("reading the prompt")
})
test("a mouse wheel that arrives as a burst of ↑ scrolls the transcript; one ↑ still walks history", async () => {
t = await tui([{ chunks: [delta({ content: Array.from({ length: 60 }, (_, i) => `line ${i}`).join("\n\n") }, "stop")] }], { width: 80, height: 24 })
await t.input.typeText("first prompt")
t.input.pressEnter()
expect(await t.until("line 59")).toBe(true)
await t.settle()
const before = t.frame()
// What a terminal not passing the mouse on sends for one notch: three ↑ at once.
for (let i = 0; i < 3; i++) t.input.pressKey("ARROW_UP")
await t.settle()
const after = t.frame()
expect(after).not.toBe(before)
expect(after).not.toContain("❯ first prompt")
// One ↑ on its own: the last prompt comes back, as before.
await Bun.sleep(40)
t.input.pressKey("ARROW_UP")
await t.settle()
expect(t.frame()).toContain("❯ first prompt")
})
test("↑ in an empty box walks the messages waiting to go in, newest first; ↓ walks back", async () => {
t = await tui([{ gapMs: 600, chunks: [delta({ content: "Working" }), delta({ content: " on" }), delta({ content: " it." }, "stop")] }, { chunks: [delta({ content: "ok" }, "stop")] }])
await t.input.typeText("go")
t.input.pressEnter()
expect(await t.until("Working")).toBe(true)
for (const m of ["fix alpha", "fix beta"]) {
await t.input.typeText(m)
t.input.pressEnter()
}
expect(await t.until("↳ queued fix beta")).toBe(true)
const press = async (k: "ARROW_UP" | "ARROW_DOWN") => {
await Bun.sleep(30)
t!.input.pressKey(k)
await t!.settle()
}
await press("ARROW_UP")
expect(t.frame()).toContain("❯ fix beta")
expect(t.frame()).not.toContain("↳ queued fix beta")
expect(t.app.engine.queued).toBe(1)
await press("ARROW_UP")
expect(t.frame()).toContain("❯ fix alpha")
expect(t.frame()).toContain("↳ queued fix beta")
await press("ARROW_DOWN")
expect(t.frame()).toContain("❯ fix beta")
await press("ARROW_DOWN")
expect(t.app.engine.queued).toBe(2)
expect(t.frame()).toContain("↳ queued fix alpha")
expect(t.frame()).toContain("↳ queued fix beta")
})
test("ctrl+↑ scrolls the transcript three lines; pgup half a screen", async () => {
t = await tui([{ chunks: [delta({ content: Array.from({ length: 60 }, (_, i) => `line ${i}`).join("\n\n") }, "stop")] }], { width: 80, height: 24 })
await t.input.typeText("go")
t.input.pressEnter()
expect(await t.until("line 59")).toBe(true)
await t.settle()
const top = () => t!.frame().split("\n").find((l) => /line \d+/.test(l))
const first = top()
t.input.pressKey("ARROW_UP", { ctrl: true })
await t.settle()
expect(top()).not.toBe(first)
expect(t.frame()).not.toContain("❯ go")
})
test("/sessions says how to delete, and ctrl+d deletes the session under the cursor", async () => {
t = await tui([], { width: 100, height: 30 }, "", { store: true })
const store = t.app.store!
const root = t.app.project.root
const said = store.createSession(root, "fake/coder")
store.append(said.id, { role: "user", parts: [{ type: "text", text: "an old question" }] })
store.setTitle(said.id, "An old question")
// An empty one is not offered at all.
store.createSession(root, "fake/coder")
await t.input.typeText("/sessions")
t.input.pressEnter()
expect(await t.until("enter resume · ctrl+d delete")).toBe(true)
expect(t.frame()).toContain("An old question")
expect(t.frame().match(/ses_/g)).toBeNull()
t.input.pressKey("d", { ctrl: true })
expect(await t.until("Delete “An old question”?")).toBe(true)
t.input.pressEnter()
expect(await t.until("no other sessions in this project")).toBe(true)
expect(store.session(said.id)).toBeUndefined()
})