import { describe, expect, test } from "bun:test" import { advertisedEfforts, effortsFromTemplate, stripEffort } from "../src/provider/effort.ts" import { ThinkSplitter } from "../src/provider/think.ts" import { sseJson } from "../src/provider/sse.ts" import { replace } from "../src/tool/replace.ts" describe("think splitter", () => { test("plain text is never held back", () => { const s = new ThinkSplitter() expect(s.feed("hello world")).toEqual([{ kind: "text", text: "hello world" }]) }) test("a trailing partial tag is held until resolved", () => { const s = new ThinkSplitter() expect(s.feed("a bc")).toEqual([ { kind: "reasoning", text: "b" }, { kind: "text", text: "c" }, ]) }) test("flush releases a held partial", () => { const s = new ThinkSplitter() s.feed("x <") expect(s.flush()).toEqual([{ kind: "text", text: "<" }]) }) }) describe("effort helpers", () => { test("advertised efforts are whole words after 'supported'", () => { expect(advertisedEfforts("Unexpected reasoning effort high. Supported types are xhigh (default), medium, and low.")).toEqual(["low", "medium", "xhigh"]) expect(advertisedEfforts("nothing here")).toEqual([]) }) test("efforts from a chat template", () => { const tpl = "{%- if reasoning_effort not in ('xhigh', 'medium', 'low') %}{{ raise_exception('bad') }}{% endif %}" expect(effortsFromTemplate(tpl)).toEqual(["low", "medium", "xhigh"]) expect(effortsFromTemplate("{%- set reasoning_effort = 'medium' %}")).toEqual([]) }) test("strip removes both forms but keeps other kwargs", () => { expect(stripEffort({ reasoning_effort: "low", chat_template_kwargs: { reasoning_effort: "low", x: 1 } })).toEqual({ chat_template_kwargs: { x: 1 } }) }) }) describe("sse", () => { test("frames without blank lines between them, comments, CRLF", async () => { const body = new Response(': keep-alive\r\ndata: {"a":1}\r\ndata: {"a":2}\n\ndata: [DONE]\n\n').body! const got = [] for await (const f of sseJson(body)) got.push(f.data) expect(got).toEqual([{ a: 1 }, { a: 2 }]) }) }) describe("replace (OpenCode cascade)", () => { test("exact, indentation-flexible, and ambiguity", () => { expect(replace("a\nb\nc", "b", "B")).toBe("a\nB\nc") expect(replace(" if x:\n y()\n", "if x:\n y()", "if z:\n y()")).toContain("if z:") expect(() => replace("x x", "x", "y")).toThrow("multiple") expect(replace("x x", "x", "y", true)).toBe("y y") }) }) describe("sse error lines", () => { test("a bare JSON error line and an `error:` field both surface", async () => { const body = new Response('data: {"a":1}\n\n{"error":{"code":500,"message":"boom"}}\n\nerror: {"message":"two"}\n\n').body! const got = [] for await (const f of sseJson(body)) got.push(f) expect(got).toEqual([ { event: undefined, data: { a: 1 } }, { event: "error", data: { error: { code: 500, message: "boom" } } }, { event: "error", data: { error: { message: "two" } } }, ]) }) }) describe("sse final line", () => { test("an error on the last line with no newline after it is not lost", async () => { const body = new Response('data: {"a":1}\n\n{"error":{"message":"last"}}').body! const got = [] for await (const f of sseJson(body)) got.push(f) expect(got.at(-1)).toEqual({ event: "error", data: { error: { message: "last" } } }) }) }) describe("grep", () => { test("literal search, and a hint when a regex finds nothing", async () => { const { grepTool } = await import("../src/tool/search.ts") const { mkdtempSync, writeFileSync } = await import("node:fs") const { tmpdir } = await import("node:os") const { join } = await import("node:path") const root = mkdtempSync(join(tmpdir(), "lembas-grep-")) writeFileSync(join(root, "a.py"), "return total / (len(values) - 1)\n") const ctx = { root, cwd: root, signal: new AbortController().signal, readFiles: new Set(), fileStamps: new Map(), bashTimeoutMs: 1000 } const miss = await grepTool.run({ pattern: "(len(values) - 1)" }, ctx) expect(miss.output).toContain("literal: true") const hit = await grepTool.run({ pattern: "(len(values) - 1)", literal: true }, ctx) expect(hit.output).toContain("a.py:1:") }) }) describe("prompt families", () => { test("every family gets the shared base plus its own overlay; the overlay is chosen by id or by config", async () => { const { assembleSystem } = await import("../src/prompt/assemble.ts") const base = { modelRef: "x/y", cwd: "/p", root: "/p", isGit: false, mode: "manual" as const, planDir: "/p/.agent/plans", toolNames: [] } const local = assembleSystem({ ...base, family: "local" }) expect(local).toContain("# Using tools") expect(local).toContain("# Act, don't describe") const gpt = assembleSystem({ ...base, family: "gpt" }) expect(gpt).toContain("# Using tools") expect(gpt).toContain("Use apply_patch for hand edits") expect(gpt).not.toContain("# Act, don't describe") expect(assembleSystem({ ...base, family: "anthropic" })).toContain("# Professional objectivity") expect(assembleSystem({ ...base, family: "gemini" })).toContain("1. **Understand**") expect(assembleSystem({ ...base, family: "nonsense" })).not.toContain("