* 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>
154 lines
No EOL
5.4 KiB
TypeScript
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")
|
|
})
|
|
}) |