opencode-config/tests/test_memory_keyword_search.ts
Sergey 7249f1fcb5
fix(memory): keyword search out-of-box + doctor gaps + tests (#124)
* fix(docker): add system ripgrep via apt for keyword search fallback

* fix(ci): install ripgrep and npm deps for keyword search in CI

* fix(memory-search): warn when ripgrep not resolvable instead of silent skip

* fix(memory-doctor): check rg status, arch mismatch, and require errors

* test(memory): add keyword search and doctor unit tests

* test(memory): unskip e2e keyword tests without RUN_LIVE

* docs(handoff): add handoff and ADR for keyword search fix

* test(memory): remove unused var in keyword search test

* docs(handoff): set PR number to 124

* docs(project-map): update after structural changes (PR#124)

---------

Co-authored-by: opencode-agent <agent@opencode.local>
2026-07-29 17:20:59 +03:00

154 lines
No EOL
5.4 KiB
TypeScript

/**
* 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")
})
})