ci / check (push) Waiting to run
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>
405 lines
20 KiB
TypeScript
405 lines
20 KiB
TypeScript
// /login against a fake LLeMbas, and the webui connection it writes: found by its
|
|
// discovery document, signed in by device code, one connection named after the instance with the key
|
|
// in a 0600 file — and the models, their settings, the voice and the search read from the instance
|
|
// at every start rather than copied into the config. `logout` revokes and removes exactly that.
|
|
import { afterEach, beforeEach, expect, test } from "bun:test"
|
|
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"
|
|
import { join } from "node:path"
|
|
import { parse } from "yaml"
|
|
import { loadConfig } from "../src/config/load.ts"
|
|
import { paths } from "../src/config/paths.ts"
|
|
import { normalise, PROTOCOL } from "../src/lembas/client.ts"
|
|
import { codeInstructions, instances, keyFile, login, logout, specFor, sync } from "../src/lembas/login.ts"
|
|
import { cacheFile, refreshWebui } from "../src/lembas/webui.ts"
|
|
import { resolveModel } from "../src/provider/index.ts"
|
|
import { search } from "../src/search/index.ts"
|
|
import { webuiFetch } from "../src/search/webui.ts"
|
|
import { wav } from "../src/voice/audio.ts"
|
|
import { synthesize, transcribe } from "../src/voice/speech.ts"
|
|
|
|
interface Fake {
|
|
url: string
|
|
stop(): void
|
|
polls: number
|
|
revoked: string[]
|
|
models: unknown[]
|
|
approveAfter: number
|
|
deny?: boolean
|
|
protocol: number
|
|
/** As an instance that gives no name and has no /api/v1/instance. */
|
|
old?: boolean
|
|
services: { stt?: boolean; tts?: boolean; search?: boolean; fetch?: boolean }
|
|
heard: { path: string; auth: string | null; body?: any; fields?: Record<string, string> }[]
|
|
token: string
|
|
}
|
|
|
|
let fake: Fake | undefined
|
|
let others: { stop(b?: boolean): void }[] = []
|
|
afterEach(() => {
|
|
fake?.stop()
|
|
for (const s of others) s.stop(true)
|
|
others = []
|
|
})
|
|
beforeEach(() => {
|
|
rmSync(paths.config, { recursive: true, force: true })
|
|
rmSync(paths.state, { recursive: true, force: true })
|
|
mkdirSync(paths.config, { recursive: true })
|
|
})
|
|
|
|
function lembas(o: Partial<Pick<Fake, "approveAfter" | "deny" | "protocol" | "models" | "old" | "services">> = {}): Fake {
|
|
const state: Fake = {
|
|
url: "",
|
|
stop: () => {},
|
|
polls: 0,
|
|
revoked: [],
|
|
approveAfter: o.approveAfter ?? 1,
|
|
deny: o.deny,
|
|
protocol: o.protocol ?? PROTOCOL,
|
|
old: o.old,
|
|
services: o.services ?? { stt: true, tts: true, search: true, fetch: true },
|
|
heard: [],
|
|
token: "lmb_secret",
|
|
models: o.models ?? [
|
|
{ id: "gemma", object: "model", context: 32768, vision: true, capacity: { group: "gpu", single_session: true } },
|
|
{ id: "qwen", object: "model", name: "Qwen", context: 131072, temperature: 0.6, top_p: 0.95, efforts: ["low", "high"], tools: true, capacity: { group: "gpu" }, default: true },
|
|
],
|
|
}
|
|
const server = Bun.serve({
|
|
port: 0,
|
|
async fetch(req) {
|
|
const u = new URL(req.url)
|
|
const base = `http://${u.host}`
|
|
const auth = req.headers.get("authorization")
|
|
const ours = auth === `Bearer ${state.token}`
|
|
if (u.pathname === "/.well-known/lembas.json")
|
|
return Response.json({
|
|
service: "lembas",
|
|
version: state.old ? "1.20.0" : "1.21.0",
|
|
...(state.old ? {} : { name: "Example", connection: "example" }),
|
|
protocol: state.protocol,
|
|
harness_spec: "1.3.0",
|
|
base_url: base,
|
|
api: { openai: `${base}/v1` },
|
|
login: { device: { code: `${base}/api/device/code`, token: `${base}/api/device/token`, verify: `${base}/device` } },
|
|
})
|
|
if (u.pathname === "/api/device/code") {
|
|
const body = (await req.json()) as Record<string, string>
|
|
expect(body.client_id).toStartWith("lembas-cli ")
|
|
expect(body.scope).toBe("models library link")
|
|
return Response.json({ device_code: "dev-123", user_code: "CDFG-HJKM", verification_uri: `${base}/device`, verification_uri_complete: `${base}/device?code=CDFG-HJKM`, expires_in: 600, interval: 1 })
|
|
}
|
|
if (u.pathname === "/api/device/token") {
|
|
const body = (await req.json()) as Record<string, string>
|
|
expect(body.device_code).toBe("dev-123")
|
|
state.polls++
|
|
if (state.deny) return Response.json({ error: "access_denied" }, { status: 400 })
|
|
if (state.polls <= state.approveAfter) return Response.json({ error: "authorization_pending" }, { status: 400 })
|
|
return Response.json({ access_token: state.token, token_type: "Bearer", scope: "models", account: { email: "frodo@shire.test", name: "Frodo" } })
|
|
}
|
|
if (u.pathname === "/api/v1/token" && req.method === "DELETE") {
|
|
state.revoked.push(auth ?? "")
|
|
return new Response(null, { status: 204 })
|
|
}
|
|
if (!ours) return new Response("no", { status: 401 })
|
|
if (u.pathname === "/v1/models") return Response.json({ object: "list", data: state.models })
|
|
if (u.pathname === "/api/v1/instance") {
|
|
if (state.old) return new Response("not found", { status: 404 })
|
|
const def = (state.models as { id: string; default?: boolean }[]).find((m) => m.default)?.id ?? null
|
|
return Response.json({ service: "lembas", version: "1.21.0", name: "Example", connection: "example", default_model: def, services: state.services })
|
|
}
|
|
if (u.pathname === "/v1/audio/transcriptions") {
|
|
const f = await req.formData()
|
|
const fields: Record<string, string> = {}
|
|
for (const [k, v] of f.entries()) fields[k] = typeof v === "string" ? v : `file:${(v as File).size}`
|
|
state.heard.push({ path: u.pathname, auth, fields })
|
|
return Response.json({ text: "Speak friend and enter." })
|
|
}
|
|
if (u.pathname === "/v1/audio/speech") {
|
|
state.heard.push({ path: u.pathname, auth, body: await req.json() })
|
|
return new Response(wav(new Uint8Array(320)))
|
|
}
|
|
if (u.pathname === "/api/v1/search") {
|
|
state.heard.push({ path: u.pathname, auth, body: await req.json() })
|
|
return Response.json({ provider: "searxng", results: [{ title: "Lembas", url: "https://example.org/l", snippet: "Waybread." }] })
|
|
}
|
|
if (u.pathname === "/api/v1/fetch") {
|
|
state.heard.push({ path: u.pathname, auth, body: await req.json() })
|
|
return Response.json({ url: "https://example.org/l", title: "Lembas", text: "Waybread of the elves." })
|
|
}
|
|
return new Response("not found", { status: 404 })
|
|
},
|
|
})
|
|
state.url = `http://127.0.0.1:${server.port}`
|
|
state.stop = () => server.stop(true)
|
|
return state
|
|
}
|
|
|
|
const quick = { sleep: () => Promise.resolve() }
|
|
const conns = () => parse(readFileSync(join(paths.config, "connections.yaml"), "utf8")).connections
|
|
const cfg = () => (existsSync(join(paths.config, "config.yaml")) ? (parse(readFileSync(join(paths.config, "config.yaml"), "utf8")) ?? {}) : {})
|
|
|
|
test("an address becomes a base: https unless it says http, no path", () => {
|
|
expect(normalise("ai.example.org")).toBe("https://ai.example.org")
|
|
expect(normalise("http://10.1.2.5:8080/chat/x")).toBe("http://10.1.2.5:8080")
|
|
expect(() => normalise("")).toThrow()
|
|
})
|
|
|
|
test("the code is shown as a link and as a code for User → Security → Devices", async () => {
|
|
fake = lembas()
|
|
let shown = ""
|
|
await login(fake.url, { ...quick, onCode: (s, d) => (shown = codeInstructions(s, d)) })
|
|
expect(shown).toContain(`Found Example (LLeMbas 1.21.0) at ${fake.url}`)
|
|
expect(shown).toContain(`1. Click the link: ${fake.url}/device?code=CDFG-HJKM`)
|
|
expect(shown).toContain("2. Copy the code CDFG-HJKM into User → Security → Devices")
|
|
})
|
|
|
|
test("login writes one webui connection named after the instance, and no models", async () => {
|
|
fake = lembas()
|
|
const r = await login(fake.url, { ...quick, onCode: () => {} })
|
|
expect(fake.polls).toBe(2) // pending once, then the token
|
|
expect(r.connection).toBe("example")
|
|
expect(conns().example).toEqual({ type: "webui", url: fake.url, api_key: `{file:${keyFile("example")}}` })
|
|
expect(readFileSync(keyFile("example"), "utf8").trim()).toBe("lmb_secret")
|
|
expect(statSync(keyFile("example")).mode & 0o777).toBe(0o600)
|
|
// No model was pinned: sessions start on the instance's default.
|
|
expect(cfg().model).toBeUndefined()
|
|
expect(r.instanceDefault).toBe("qwen")
|
|
expect(instances().example!.instance_name).toBe("Example")
|
|
|
|
const loaded = loadConfig()
|
|
const c = loaded.connections.example!
|
|
expect(c.dialect).toBe("openai-chat")
|
|
expect(c.base_url).toBe(`${fake.url}/v1`)
|
|
expect(c.api_key).toBe("lmb_secret")
|
|
// The instance's models with its settings, the default first; the group named after the connection.
|
|
expect(Object.keys(c.models)).toEqual(["qwen", "gemma"])
|
|
expect(c.models.qwen).toEqual({ name: "Qwen", context: 131072, temperature: 0.6, top_p: 0.95, efforts: ["low", "high"], tools: true, group: "example:gpu" })
|
|
expect(c.models.gemma).toEqual({ context: 32768, vision: true, single_session: true, group: "example:gpu" })
|
|
expect(resolveModel(loaded, undefined).ref).toBe("example/qwen")
|
|
})
|
|
|
|
test("a model changed on the instance is here at the next start, without logging in again", async () => {
|
|
fake = lembas()
|
|
await login(fake.url, { ...quick, onCode: () => {} })
|
|
fake.models = [{ id: "llama", object: "model", context: 8192, default: true }]
|
|
const r = await refreshWebui()
|
|
expect(r).toEqual([{ name: "example", ok: true, models: 1 }])
|
|
const loaded = loadConfig()
|
|
expect(Object.keys(loaded.connections.example!.models)).toEqual(["llama"])
|
|
expect(resolveModel(loaded, undefined).ref).toBe("example/llama")
|
|
// A model the instance does not list yet is still asked for, not refused here.
|
|
expect(resolveModel(loaded, "example/brand-new").id).toBe("brand-new")
|
|
})
|
|
|
|
test("an instance that does not answer leaves what it said last, and says so", async () => {
|
|
fake = lembas()
|
|
await login(fake.url, { ...quick, onCode: () => {} })
|
|
fake.stop()
|
|
const r = await refreshWebui({ timeoutMs: 1000 })
|
|
expect(r[0]!.ok).toBe(false)
|
|
const loaded = loadConfig()
|
|
expect(Object.keys(loaded.connections.example!.models)).toEqual(["qwen", "gemma"])
|
|
expect(loaded.warnings.join("\n")).toContain("example: the instance could not be asked for its models")
|
|
fake = undefined
|
|
})
|
|
|
|
test("the entry's own models: go over the instance's", async () => {
|
|
fake = lembas()
|
|
await login(fake.url, { ...quick, onCode: () => {} })
|
|
const text = readFileSync(join(paths.config, "connections.yaml"), "utf8").replace(`url: ${fake.url}`, `url: ${fake.url}\n models:\n qwen: { context: 65536 }`)
|
|
writeFileSync(join(paths.config, "connections.yaml"), text, { mode: 0o600 })
|
|
const c = loadConfig().connections.example!
|
|
expect(c.models.qwen!.context).toBe(65536)
|
|
expect(c.models.qwen!.tools).toBe(true)
|
|
})
|
|
|
|
test("an instance that gives no name is named after its host", async () => {
|
|
fake = lembas({ old: true })
|
|
const r = await login(fake.url, { ...quick, onCode: () => {} })
|
|
expect(r.connection).toBe("127-0-0-1")
|
|
expect(r.changes).toEqual([])
|
|
// Its default comes from /v1/models: still first.
|
|
expect(Object.keys(loadConfig().connections["127-0-0-1"]!.models)).toEqual(["qwen", "gemma"])
|
|
})
|
|
|
|
test("a hand-written connection of the same name is never overwritten", async () => {
|
|
writeFileSync(join(paths.config, "connections.yaml"), "connections:\n example:\n dialect: ollama\n base_url: http://x\n models: {}\n", { mode: 0o600 })
|
|
fake = lembas()
|
|
const r = await login(fake.url, { ...quick, onCode: () => {} })
|
|
expect(r.connection).toBe("example-lembas")
|
|
expect(conns().example.dialect).toBe("ollama")
|
|
})
|
|
|
|
test("an older kind of login is replaced: the old connection, its key and its pinned default go", async () => {
|
|
fake = lembas()
|
|
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n ai:\n dialect: openai-chat\n base_url: ${fake.url}/v1\n api_key: "{file:${keyFile("ai")}}"\n models: { qwen: {}, gemma: {} }\n local:\n dialect: openai-chat\n base_url: http://x/v1\n models: { m: {} }\n`, { mode: 0o600 })
|
|
mkdirSync(join(paths.config, "lembas"), { recursive: true })
|
|
writeFileSync(keyFile("ai"), "lmb_old\n", { mode: 0o600 })
|
|
writeFileSync(join(paths.config, "lembas.json"), JSON.stringify({ instances: { ai: { base_url: fake.url, connection: "ai", logged_in_at: "2026-10-07T10:00:00Z" } } }))
|
|
writeFileSync(join(paths.config, "config.yaml"), "model: ai/qwen\nsmall_model: ai/gemma\n")
|
|
|
|
const r = await login(fake.url, { ...quick, onCode: () => {} })
|
|
expect(r.connection).toBe("example")
|
|
expect(Object.keys(conns()).sort()).toEqual(["example", "local"])
|
|
expect(existsSync(keyFile("ai"))).toBe(false)
|
|
expect(fake.revoked).toEqual(["Bearer lmb_old"])
|
|
expect(Object.keys(instances())).toEqual(["example"])
|
|
const c = cfg()
|
|
// It was the instance's default, so the instance decides it now; the rest follow the new name.
|
|
expect(c.model).toBeUndefined()
|
|
expect(c.small_model).toBe("example/gemma")
|
|
expect(r.changes.join("\n")).toContain("the connection ai from an earlier login is now example")
|
|
})
|
|
|
|
test("voice and search go through the instance, and what was set up becomes the fallback", async () => {
|
|
writeFileSync(join(paths.config, "config.yaml"), "voice:\n stt:\n provider: openai\n base_url: http://stt.local/v1\nsearch:\n searxng:\n base_url: http://search.local\n")
|
|
fake = lembas()
|
|
const r = await login(fake.url, { ...quick, onCode: () => {} })
|
|
const c = cfg()
|
|
expect(c.voice.stt).toEqual({ provider: "webui", fallback: { provider: "openai", base_url: "http://stt.local/v1" } })
|
|
expect(c.voice.tts).toEqual({ provider: "webui" })
|
|
expect(c.search.order).toEqual(["webui", "searxng", "ddg"])
|
|
expect(r.changes.some((x) => x.startsWith("voice input through example"))).toBe(true)
|
|
|
|
// Loaded, webui is the instance's /v1 with this machine's key.
|
|
const v = loadConfig().config.voice!
|
|
expect(v.stt!.base_url).toBe(`${fake.url}/v1`)
|
|
expect(v.stt!.api_key).toBe("lmb_secret")
|
|
expect(await transcribe(v, wav(new Uint8Array(3200)))).toBe("Speak friend and enter.")
|
|
const heard = fake.heard.find((h) => h.path === "/v1/audio/transcriptions")!
|
|
expect(heard.auth).toBe("Bearer lmb_secret")
|
|
// The instance's own model, whatever: none is sent.
|
|
expect(heard.fields!.model).toBeUndefined()
|
|
|
|
await synthesize(v, "Mellon.")
|
|
const said = fake.heard.find((h) => h.path === "/v1/audio/speech")!.body
|
|
// The account's own voice and speed, on the instance.
|
|
expect(said).toEqual({ input: "Mellon.", response_format: "wav" })
|
|
})
|
|
|
|
test("an instance that is down falls back to the voice that was there before", async () => {
|
|
const local = Bun.serve({ port: 0, fetch: () => Response.json({ text: "from the fallback" }) })
|
|
others.push(local)
|
|
writeFileSync(join(paths.config, "config.yaml"), `voice:\n stt:\n provider: openai\n base_url: http://127.0.0.1:${local.port}/v1\n`)
|
|
fake = lembas()
|
|
await login(fake.url, { ...quick, onCode: () => {} })
|
|
const v = loadConfig().config.voice!
|
|
fake.stop()
|
|
fake = undefined
|
|
expect(await transcribe(v, wav(new Uint8Array(3200)))).toBe("from the fallback")
|
|
})
|
|
|
|
test("web search and fetch through the instance", async () => {
|
|
fake = lembas()
|
|
await login(fake.url, { ...quick, onCode: () => {} })
|
|
const s = loadConfig().config.search!
|
|
const r = await search("lembas", s, new AbortController().signal)
|
|
expect(r.provider).toBe("webui")
|
|
expect(r.results).toEqual([{ title: "Lembas", url: "https://example.org/l", snippet: "Waybread." }])
|
|
expect(fake.heard.find((h) => h.path === "/api/v1/search")!.auth).toBe("Bearer lmb_secret")
|
|
const page = await webuiFetch("https://example.org/l", s, new AbortController().signal)
|
|
expect(page.text).toBe("Waybread of the elves.")
|
|
})
|
|
|
|
test("what the instance does not offer is left alone", async () => {
|
|
fake = lembas({ services: { stt: false, tts: false, search: false, fetch: false } })
|
|
const r = await login(fake.url, { ...quick, onCode: () => {} })
|
|
expect(cfg().voice).toBeUndefined()
|
|
expect(cfg().search).toBeUndefined()
|
|
expect(r.changes).toEqual([])
|
|
})
|
|
|
|
test("webui with no webui connection is off, with a warning", () => {
|
|
writeFileSync(join(paths.config, "config.yaml"), "voice:\n tts:\n provider: webui\nsearch:\n order: [webui, ddg]\n")
|
|
const loaded = loadConfig()
|
|
expect(loaded.config.voice?.tts).toBeUndefined()
|
|
expect(loaded.config.search?.webui).toBeUndefined()
|
|
expect(loaded.warnings.join("\n")).toContain("there is no webui connection (lembas login)")
|
|
})
|
|
|
|
test("login again reads the models now", async () => {
|
|
fake = lembas()
|
|
await login(fake.url, { ...quick, onCode: () => {} })
|
|
fake.models = [{ id: "llama", object: "model", context: 8192 }]
|
|
const again = await sync("example")
|
|
expect(again.models.map((m) => m.id)).toEqual(["llama"])
|
|
expect(JSON.parse(readFileSync(cacheFile("example"), "utf8")).models[0].id).toBe("llama")
|
|
})
|
|
|
|
test("logout revokes the token and removes exactly what login wrote", async () => {
|
|
writeFileSync(join(paths.config, "connections.yaml"), "connections:\n local:\n dialect: openai-chat\n base_url: http://x/v1\n models: { m: {} }\n", { mode: 0o600 })
|
|
writeFileSync(join(paths.config, "config.yaml"), "voice:\n stt:\n provider: openai\n base_url: http://stt.local/v1\n")
|
|
fake = lembas()
|
|
const r = await login(fake.url, { ...quick, onCode: () => {} })
|
|
const out = await logout(r.connection)
|
|
expect(out.revoked).toBe(true)
|
|
expect(fake.revoked).toEqual(["Bearer lmb_secret"])
|
|
expect(existsSync(keyFile(r.connection))).toBe(false)
|
|
expect(existsSync(cacheFile(r.connection))).toBe(false)
|
|
expect(Object.keys(conns())).toEqual(["local"])
|
|
expect(instances()).toEqual({})
|
|
// Voice is what it was before the login; search no longer asks the instance.
|
|
const c = cfg()
|
|
expect(c.voice.stt).toEqual({ provider: "openai", base_url: "http://stt.local/v1" })
|
|
expect(c.voice.tts).toBeUndefined()
|
|
expect(c.search.order).toEqual(["ddg"])
|
|
})
|
|
|
|
test("a denial, another service, and another protocol are each said plainly", async () => {
|
|
fake = lembas({ deny: true })
|
|
await expect(login(fake.url, { ...quick, onCode: () => {} })).rejects.toThrow("denied")
|
|
fake.stop()
|
|
|
|
fake = lembas({ protocol: PROTOCOL + 1 })
|
|
await expect(login(fake.url, { ...quick, onCode: () => {} })).rejects.toThrow("device protocol")
|
|
fake.stop()
|
|
|
|
const other = Bun.serve({ port: 0, fetch: () => new Response("hello", { status: 404 }) })
|
|
try {
|
|
await expect(login(`http://127.0.0.1:${other.port}`, { ...quick, onCode: () => {} })).rejects.toThrow("not a LLeMbas instance")
|
|
} finally {
|
|
other.stop(true)
|
|
}
|
|
expect(existsSync(join(paths.config, "connections.yaml"))).toBe(false)
|
|
})
|
|
|
|
test("specFor leaves out what the instance did not say", () => {
|
|
expect(specFor({ id: "x" }, "ai")).toEqual({})
|
|
expect(specFor({ id: "x", name: "x", tools: false }, "ai")).toEqual({ tools: false })
|
|
expect(specFor({ id: "x", temperature: 7 }, "ai")).toEqual({})
|
|
})
|
|
|
|
test("only chat models are models; the first embedding one becomes the library's, never over one set", async () => {
|
|
fake = lembas({
|
|
models: [
|
|
{ id: "llama/qwen", object: "model", kind: "chat", provider: "llama", default: true },
|
|
{ id: "llama/gemma", object: "model", provider: "llama" },
|
|
{ id: "llama/nomic-embed", object: "model", kind: "embedding", provider: "llama" },
|
|
{ id: "llama/bge", object: "model", kind: "embedding", provider: "llama" },
|
|
{ id: "llama/whisper", object: "model", kind: "stt", provider: "llama" },
|
|
{ id: "llama/kokoro", object: "model", kind: "tts", provider: "llama" },
|
|
{ id: "llama/flux", object: "model", kind: "image", provider: "llama" },
|
|
],
|
|
})
|
|
const r = await login(fake.url, { ...quick, onCode: () => {} })
|
|
expect(r.models.map((m) => m.id)).toEqual(["llama/qwen", "llama/gemma"])
|
|
expect(cfg().embedding).toBe("example/llama/nomic-embed")
|
|
expect(r.changes).toContain("the library embeds with example/llama/nomic-embed (embedding:)")
|
|
const loaded = loadConfig()
|
|
expect(Object.keys(loaded.connections.example!.models)).toEqual(["llama/qwen", "llama/gemma"])
|
|
expect(loaded.refs.map((x) => x.id)).not.toContain("llama/whisper")
|
|
// The embedding model resolves, written either way.
|
|
const { embedderFor } = await import("../src/library/embed.ts")
|
|
expect(embedderFor(loaded, "example/llama/nomic-embed")?.model).toBe("example/llama/nomic-embed")
|
|
expect(embedderFor(loaded, "llama/bge")?.model).toBe("llama/bge")
|
|
// Set already — by hand, to something else: logging in again leaves it.
|
|
writeFileSync(join(paths.config, "config.yaml"), "embedding: local/mine\n")
|
|
const again = await sync("example")
|
|
expect(cfg().embedding).toBe("local/mine")
|
|
expect(again.changes).toEqual([])
|
|
// Logout takes out only an embedding on its own connection.
|
|
writeFileSync(join(paths.config, "config.yaml"), "embedding: example/llama/nomic-embed\n")
|
|
await logout("example")
|
|
expect(cfg().embedding).toBeUndefined()
|
|
})
|