/** * Tests for resolveRgBinary() and memory-search keyword degradation. * * resolveRgBinary() lives in .opencode/tools/_memory-shared.ts and resolves * the ripgrep binary via the @vscode/ripgrep npm package first, then * (optionally) a system `rg` on $PATH. memory-search.ts must warn to stderr * when both are unavailable instead of silently returning no keyword hits. * * Runtime note: opencode ships a standalone binary with Bun bundled inside; * there is no separate `bun` CLI on the host (CI runner uses node + pytest). * This file documents the intended TS-side test cases and is runnable under * `bun test` once a bun runtime is available on the host. * * Test cases: * - test_resolve_npm_package — rgPath exists → returns path * - test_resolve_system_fallback — npm require throws, allowSystemFallback * true, system rg on $PATH → returns "rg" * - test_resolve_null_when_nothing_available — npm throws, no system rg, * allowSystemFallback true → null * - test_resolve_null_no_fallback — npm throws, allowSystemFallback false * → null (no system rg probe) * - test_memory_search_warns_when_rg_null — rgBin===null → stderr warning */ import { describe, test, expect, mock } from "bun:test" with { type: "'bun-test'" } import { spawnSync } from "child_process" import fs from "fs" import path from "path" const SHARED_SRC = path.resolve( import.meta.dir, "..", ".opencode", "tools", "_memory-shared.ts" ) const SEARCH_SRC = path.resolve( import.meta.dir, "..", ".opencode", "tools", "memory-search.ts" ) describe("resolveRgBinary", () => { test("test_resolve_npm_package — @vscode/ripgrep resolves to existing binary", async () => { const fakeRgPath = "/tmp/opencode/fake-rg-bin" mock.module("node:module", () => ({ createRequire: () => () => ({ rgPath: fakeRgPath }), })) mock.module("fs", () => ({ ...fs, existsSync: (p: string) => p === fakeRgPath, })) const mod = await import(SHARED_SRC + "?t=" + Date.now()) const result = mod.resolveRgBinary() expect(result).toBe(fakeRgPath) }) test("test_resolve_system_fallback — npm require throws, allowSystemFallback true, system rg works → 'rg'", async () => { mock.module("node:module", () => ({ createRequire: () => () => { throw new Error("Cannot find module '@vscode/ripgrep'") }, })) mock.module("child_process", () => ({ spawnSync: () => ({ status: 0, stdout: "ripgrep 13.0.0\n", stderr: "" }), })) const mod = await import(SHARED_SRC + "?t=" + Date.now()) const result = mod.resolveRgBinary({ allowSystemFallback: true }) expect(result).toBe("rg") }) test("test_resolve_null_when_nothing_available — npm throws, no system rg, allowSystemFallback true → null", async () => { mock.module("node:module", () => ({ createRequire: () => () => { throw new Error("Cannot find module '@vscode/ripgrep'") }, })) mock.module("child_process", () => ({ spawnSync: () => ({ status: 1, stdout: "", stderr: "rg: not found" }), })) const mod = await import(SHARED_SRC + "?t=" + Date.now()) const result = mod.resolveRgBinary({ allowSystemFallback: true }) expect(result).toBeNull() }) test("test_resolve_null_no_fallback — npm throws, allowSystemFallback false → null (no system rg probe)", async () => { let spawnCalled = false mock.module("node:module", () => ({ createRequire: () => () => { throw new Error("Cannot find module '@vscode/ripgrep'") }, })) mock.module("child_process", () => ({ spawnSync: () => { spawnCalled = true return { status: 0, stdout: "ripgrep 13.0.0\n", stderr: "" } }, })) const mod = await import(SHARED_SRC + "?t=" + Date.now()) const result = mod.resolveRgBinary() expect(result).toBeNull() expect(spawnCalled).toBe(false) }) }) describe("memory-search keyword degradation", () => { test("test_memory_search_warns_when_rg_null — rgBin===null writes stderr warning", async () => { const stderrWrites: string[] = [] mock.module("node:module", () => ({ createRequire: () => () => { throw new Error("Cannot find module '@vscode/ripgrep'") }, })) mock.module("child_process", () => ({ spawnSync: () => ({ status: 1, stdout: "", stderr: "rg: not found" }), })) mock.module("fs", () => ({ ...fs, existsSync: () => true, readFileSync: () => "---\ntitle: test\nsummary: s\n---\n\n# test\n\nbody text\n", readdirSync: () => [], statSync: () => ({ size: 0, mtime: new Date() }), })) const origStderrWrite = process.stderr.write process.stderr.write = ((chunk: string | Uint8Array) => { stderrWrites.push(typeof chunk === "string" ? chunk : chunk.toString()) return true }) as typeof process.stderr.write try { const mod = await import(SEARCH_SRC + "?t=" + Date.now()) await mod.default.execute( { query: "nonexistent" }, { sessionID: "t", messageID: "t", agent: "t", directory: ".", worktree: ".", abort: new AbortController().signal, metadata() {}, async ask() {}, } ) } finally { process.stderr.write = origStderrWrite } const combined = stderrWrites.join("") expect(combined).toContain("ripgrep not resolvable") expect(combined).toContain("keyword search disabled") }) })