Files
HomerandClaude Opus 5.5 28ff205ed0 Custom connections are gone: the CLI talks to the LLeMbas server it is logged in to
LLeMbas CLI 2.0 is LLeMbas-server-specific (CLI rule 2). Removed: the
anthropic, gemini, ollama and responses clients, context discovery and
llama-swap unloading (discover.ts), the Connection schema with its dialect,
auth, key_cmd, discover, unload_url and one_model_at_a_time, the model
`fallback` (it only ever moved to another server), effort_map and cache, and
the multi-connection refs. Kept: openai-chat, http, sse, think, effort,
tokens and all of src/lembas.

- connections.yaml holds the login (`type: webui`, as lembas login writes
  it). A custom entry from 1.x is read past with a warning that says how to
  log in, so an old file still starts; with several logins left, the latest
  is used and the others named.
- A model's ref is the id the server serves it under; the older forms
  (`<login>/<id>`, `<login>/<model>`) still resolve and config.yaml is
  rewritten once. Without a login there is no model, and the error (and the
  first-run screen, now the sign-in alone) says `lembas login <address>`.
- A login to another server signs this machine out of the previous one.
- The window is the server's word (no discovery); a capacity group comes
  from the server's `capacity.group` alone; embeddings use the server's
  /v1/embeddings.
- `lembas models` lists the server's models; install.sh's starter files, the
  README and the JSON schemas say the same.

Tests: every fixture is a login now (the fake server lists models, answers
other GETs with 404, and can hand one model to another script, as a server
with several backends); the dialect suites and the cross-connection tests are
gone with their code.

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

281 lines
14 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.
#!/usr/bin/env bun
// Smoke test of the COMPILED binary — what `bun test` cannot see: that the bundle, OpenTUI's
// native library and bun:sqlite FTS5 all survive `bun build --compile`.
// bun run scripts/smoke.ts [path/to/binary]
import { existsSync, mkdirSync, mkdtempSync, readdirSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join, resolve } from "node:path"
import { delta, fakeProvider, toolCall, usage } from "../tests/fake-provider.ts"
import { drive, fakeHome } from "./drive.ts"
const bin = resolve(process.argv[2] ?? "dist/lembas")
const home = mkdtempSync(join(tmpdir(), "lembas-smoke-"))
const env = { ...process.env, LEMBAS_HOME: home }
let failed = 0
const check = (name: string, ok: boolean, detail = "") => {
console.log(`${ok ? "✓" : "✗"} ${name}${ok ? "" : " " + detail}`)
if (!ok) failed++
}
const run = (args: string[], cwd = home) => {
const r = Bun.spawnSync([bin, ...args], { cwd, env, stdout: "pipe", stderr: "pipe" })
return { code: r.exitCode, out: r.stdout.toString(), err: r.stderr.toString() }
}
// 1. --version
const v = run(["--version"])
check("--version", v.code === 0 && /^\d+\.\d+\.\d+/.test(v.out), v.err)
// 2. a headless run against a fake endpoint, with the session store on (FTS5 inside the binary)
const fake = fakeProvider([{ chunks: [delta({ content: "smoke ok" }), usage(5, 2)] }], { served: [{ id: "fake/m" }] })
mkdirSync(join(home, ".config/lembas"), { recursive: true })
writeFileSync(
join(home, ".config/lembas/connections.yaml"),
`connections:\n fake:\n type: webui\n url: ${fake.url}\n api_key: smoke\n models:\n fake/m: {}\n`,
{ mode: 0o600 },
)
writeFileSync(join(home, ".config/lembas/config.yaml"), "model: fake/m\nupdate: { auto: off }\n")
const r = await new Promise<{ code: number; out: string; err: string }>((res) => {
const p = Bun.spawn([bin, "run", "say hi"], { cwd: home, env, stdout: "pipe", stderr: "pipe" })
Promise.all([new Response(p.stdout).text(), new Response(p.stderr).text(), p.exited]).then(([out, err, code]) => res({ code, out, err }))
})
fake.stop()
check("run → streamed answer", r.code === 0 && r.out === "smoke ok\n", JSON.stringify(r))
const s = run(["sessions"])
check("sessions → recorded (sqlite + FTS5 in the binary)", s.code === 0 && s.out.includes("fake/m"), s.err || s.out)
// 2b. serve --stdio: the ACP agent inside the binary answers initialize, opens a session and
// streams a reply — the code the LLeMbas link runs too.
{
const f = fakeProvider([{ chunks: [delta({ content: "acp ok" })] }], { served: [{ id: "fake/m" }] })
writeFileSync(
join(home, ".config/lembas/connections.yaml"),
`connections:\n fake:\n type: webui\n url: ${f.url}\n api_key: smoke\n models:\n fake/m: {}\n`,
{ mode: 0o600 },
)
const p = Bun.spawn([bin, "serve", "--stdio"], { cwd: home, env, stdin: "pipe", stdout: "pipe", stderr: "pipe" })
const lines: any[] = []
const reader = (async () => {
let buf = ""
for await (const chunk of p.stdout as unknown as AsyncIterable<Uint8Array>) {
buf += new TextDecoder().decode(chunk)
let nl: number
while ((nl = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, nl).trim()
buf = buf.slice(nl + 1)
if (line) lines.push(JSON.parse(line))
}
}
})()
const send = (m: unknown) => p.stdin.write(JSON.stringify(m) + "\n")
const answer = async (id: number) => {
for (let i = 0; i < 300; i++) {
const hit = lines.find((l) => l.id === id)
if (hit) return hit
await Bun.sleep(20)
}
return undefined
}
send({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: 1, clientCapabilities: {} } })
const init = await answer(1)
send({ jsonrpc: "2.0", id: 2, method: "session/new", params: { cwd: home, mcpServers: [] } })
const session = await answer(2)
send({ jsonrpc: "2.0", id: 3, method: "session/prompt", params: { sessionId: session?.result?.sessionId, prompt: [{ type: "text", text: "hi" }] } })
const done = await answer(3)
const text = lines.filter((l) => l.method === "session/update" && l.params.update.sessionUpdate === "agent_message_chunk").map((l) => l.params.update.content.text).join("")
p.stdin.end()
await Promise.race([p.exited, Bun.sleep(3000)])
p.kill()
await reader.catch(() => {})
f.stop()
check("serve --stdio → ACP session streams a reply", init?.result?.protocolVersion === 1 && done?.result?.stopReason === "end_turn" && text === "acp ok", JSON.stringify({ init, session, done, text }))
}
// 3. the real TUI in a real PTY, read back through a headless xterm, at two sizes
for (const [cols, rows] of [
[120, 40],
[80, 24],
] as const) {
const { fake: f, home: h, cwd } = fakeHome([
{ chunks: [delta({ content: "Let me check." }), toolCall(0, "c1", "bash", '{"command":"echo smoke > out.txt","description":"Write out.txt"}'), usage(9000, 40)] },
{ chunks: [delta({ content: "## Done\n\nWrote **out.txt**." }), usage(12000, 60)] },
])
const d = await drive({ cols, rows, bin, home: h, cwd })
// A fresh directory opens with the first-run question; trust it (the default), then work.
const firstRun = await d.waitFor("Trust this directory?")
await d.key("\r")
const drawn = await d.waitFor("shift+tab mode · /help")
await d.type("make a file")
await d.key("\r")
const asked = await d.waitFor("Allow once")
await d.key("y")
const answered = await d.waitFor("Wrote out.txt.")
await Bun.sleep(200)
const screen = d.screen()
const wrapped = d.wrapped()
await d.key("\x03")
await d.key("\x03")
const code = await d.close()
f.stop()
const file = await Bun.file(join(cwd, "out.txt")).text().catch(() => "")
check(
`TUI ${cols}x${rows}: first run → prompt → approval → answer → quit`,
firstRun && drawn && asked && answered && file === "smoke\n" && code === 0,
`firstRun=${firstRun} drawn=${drawn} asked=${asked} answered=${answered} file=${JSON.stringify(file)} exit=${code}\n${screen.join("\n")}`,
)
check(`TUI ${cols}x${rows}: nothing wider than the terminal`, wrapped.length === 0, `wrapped rows: ${wrapped.join(", ")}`)
check(`TUI ${cols}x${rows}: status bar on the last row`, (screen[rows - 1] ?? "").includes("manual · fake/coder"), screen[rows - 1] ?? "")
}
// 3b. jobs, at 80×24: a command the model starts in the background is counted in the status bar;
// /jobs opens the list (the row fits the terminal) and esc closes it; quitting with it running asks
// first, and y stops it and leaves.
{
const { fake: f, home: h, cwd } = fakeHome([
{ chunks: [delta({ content: "Starting it." }), toolCall(0, "c1", "bash", '{"command":"sleep 60","background":true,"description":"A long server that keeps running"}'), usage(900, 40)] },
{ chunks: [delta({ content: "Started it in the background." }), usage(1200, 20)] },
])
const d = await drive({ cols: 80, rows: 24, bin, home: h, cwd })
await d.waitFor("Trust this directory?")
await d.key("\r")
await d.waitFor("shift+tab mode · /help")
await d.type("start a server")
await d.key("\r")
const asked = await d.waitFor("Allow once")
await d.key("y")
const started = await d.waitFor("Started it in the background.")
const counted = await d.waitFor("1 job ·")
await d.type("/jobs")
await d.key("\r")
const opened = await d.waitFor("enter open · k stop")
// The row: id, the description, the state, the time, and the box's border after them.
const row = d.screen().find((l) => l.includes("job1")) ?? ""
const listed = /job1 .*A long server.*running +\d+s +│/.test(row)
const wrapped = d.wrapped()
const screen = d.screen()
await d.key("\x1b")
await Bun.sleep(400)
const closed = !d.screen().some((l) => l.includes("enter open · k stop"))
await d.key("\x03")
await d.key("\x03")
const confirm = await d.waitFor("1 job running — stop it and quit?")
await d.key("y")
const code = await d.close()
f.stop()
check(
"TUI 80x24: a background job is counted; /jobs opens and closes; quitting with it running asks, y stops it and quits",
asked && started && counted && opened && listed && closed && confirm && code === 0,
`asked=${asked} started=${started} counted=${counted} opened=${opened} listed=${listed} closed=${closed} confirm=${confirm} exit=${code}\n${screen.join("\n")}`,
)
check("TUI 80x24: the jobs list is no wider than the terminal", wrapped.length === 0, `wrapped rows: ${wrapped.join(", ")}`)
}
// 4. a fresh install: no model yet. A screen that says what to add (not an exception with the
// terminal left in mouse-reporting mode); r once it is added opens the session; the terminal is
// given back as it was found.
{
const { fake: f, home: h, cwd } = fakeHome([{ chunks: [delta({ content: "hello after setup" }), usage(5, 2)] }])
const cfgDir = join(h, ".config/lembas")
const connections = await Bun.file(join(cfgDir, "connections.yaml")).text()
await Bun.write(join(cfgDir, "connections.yaml"), "connections: {}\n")
await Bun.write(join(cfgDir, "config.yaml"), "mode: manual\nupdate: { auto: off }\n")
const d = await drive({ cols: 100, rows: 30, bin, home: h, cwd })
await d.waitFor("Trust this directory?")
await d.key("\r")
const setup = await d.waitFor("not logged in to a LLeMbas server")
await Bun.write(join(cfgDir, "connections.yaml"), connections)
await Bun.write(join(cfgDir, "config.yaml"), "model: fake/coder\nupdate: { auto: off }\n")
// The screen opens on the sign-in, its address focused: ctrl+r tries again from there.
const focused = await d.waitFor("Log in to a LLeMbas instance")
await d.key("\x12")
const opened = await d.waitFor("fake/coder")
await d.type("hi")
await d.key("\r")
const answered = await d.waitFor("hello after setup")
await d.key("\x03")
await d.key("\x03")
const code = await d.close()
const modes = d.modes()
f.stop()
check("fresh install: no model → setup screen on the sign-in; ctrl+r after adding one → a session", setup && focused && opened && answered && code === 0, `setup=${setup} focused=${focused} opened=${opened} answered=${answered} exit=${code}\n${d.screen().join("\n")}`)
check("fresh install: the terminal is given back (mouse reporting off, normal screen)", modes.mouse === "none" && !modes.altScreen, JSON.stringify(modes))
}
// 5. /reload: the same process starts again (execv) on the same session — the conversation is
// there, the terminal is the TUI's again, and quitting leaves it as it was found.
{
const { fake: f, home: h, cwd } = fakeHome([
{ chunks: [delta({ content: "first answer" }), usage(5, 2)] },
{ chunks: [delta({ content: "second answer" }), usage(9, 2)] },
])
const d = await drive({ cols: 100, rows: 30, bin, home: h, cwd })
await d.waitFor("Trust this directory?")
await d.key("\r")
await d.waitFor("fake/coder")
await d.type("hello")
await d.key("\r")
const answered = await d.waitFor("first answer")
await d.type("/reload")
await d.key("\r")
// The new program draws its welcome box again, with the conversation loaded under it.
await Bun.sleep(300)
const back = await d.waitFor("shift+tab mode · /help")
// The new program's next request carries the conversation from before the reload.
await d.type("again")
await d.key("\r")
await d.waitFor("second answer")
const history = JSON.stringify(f.requests[1]?.messages ?? [])
const kept = history.includes("hello") && history.includes("first answer")
const screen = d.screen()
await d.key("\x03")
await d.key("\x03")
const code = await d.close()
const modes = d.modes()
f.stop()
check("/reload: started again, back on the same session", answered && back && kept && code === 0, `answered=${answered} back=${back} kept=${kept} exit=${code}\n${screen.join("\n")}`)
check("/reload: the terminal is given back on quit", modes.mouse === "none" && !modes.altScreen, JSON.stringify(modes))
}
// 6. local mode (-l): everything in the project, nothing written anywhere else — the user's
// directories, HOME and TMPDIR are checked file by file before and after a whole session.
{
const { fake: f, home: h, cwd } = fakeHome([
{ chunks: [delta({ content: "Writing it." }), toolCall(0, "c1", "bash", '{"command":"echo local > out.txt","description":"Write out.txt"}'), usage(900, 40)] },
{ chunks: [delta({ content: "local done" }), usage(1200, 20)] },
])
const tmp = mkdtempSync(join(tmpdir(), "lembas-smoke-tmp-"))
const listing = (dir: string): string[] => {
const out: string[] = []
for (const e of readdirSync(dir, { withFileTypes: true, recursive: true })) out.push(join(e.parentPath, e.name))
return out.sort()
}
const before = { home: listing(h), tmp: listing(tmp) }
const d = await drive({ cols: 100, rows: 30, bin, home: h, cwd, args: ["-l"], env: { HOME: h, TMPDIR: tmp, XDG_CONFIG_HOME: join(h, ".config"), XDG_DATA_HOME: join(h, ".local/share"), XDG_STATE_HOME: join(h, ".local/state") } })
const firstRun = await d.waitFor("Trust this directory?")
await d.key("\r")
const drawn = await d.waitFor("shift+tab mode · /help", 15000)
await d.type("make a file")
await d.key("\r")
const asked = await d.waitFor("Allow once")
await d.key("y")
const answered = await d.waitFor("local done")
await d.type("/remote")
await d.key("\r")
const refused = await d.waitFor("off in local mode")
const screen = d.screen()
await d.key("\x03")
await d.key("\x03")
const code = await d.close()
f.stop()
const after = { home: listing(h), tmp: listing(tmp) }
const grew = [...after.home.filter((p) => !before.home.includes(p)), ...after.tmp.filter((p) => !before.tmp.includes(p))]
const kept = ["data/sessions.db", "state/trust.json"].every((p) => existsSync(join(cwd, ".agent/local/lembas", p)))
check(
"local mode: a whole session works from the project",
firstRun && drawn && asked && answered && refused && kept && code === 0 && (screen[29] ?? "").includes("local"),
`firstRun=${firstRun} drawn=${drawn} asked=${asked} answered=${answered} refused=${refused} kept=${kept} exit=${code}\n${screen.join("\n")}`,
)
check("local mode: nothing written outside the project (home, XDG dirs, TMPDIR)", grew.length === 0, grew.join(", "))
}
process.exit(failed ? 1 : 0)