mirror of https://github.com/garrytan/gstack.git
139 lines
4.8 KiB
TypeScript
139 lines
4.8 KiB
TypeScript
/**
|
|
* Tests for lib/gstack-decision-semantic.ts — the OPTIONAL gbrain enhancement.
|
|
*
|
|
* The load-bearing contract is DEGRADE-TO-NULL: when gbrain is absent/errors, every
|
|
* entry point returns null (caller shows reliable file results), never throws, never
|
|
* hangs. We also pin the text-surface parser deterministically and prove the
|
|
* end-to-end scope+search path with a fake `gbrain` shim on PATH (no live gbrain).
|
|
*/
|
|
|
|
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
import * as fs from "fs";
|
|
import * as os from "os";
|
|
import * as path from "path";
|
|
import {
|
|
parseSearchHits,
|
|
resolveMemorySourceId,
|
|
semanticRecall,
|
|
} from "../lib/gstack-decision-semantic";
|
|
|
|
describe("parseSearchHits (text surface)", () => {
|
|
const sample = [
|
|
"[0.91] decisions/foo -- We chose PGLite for the local engine",
|
|
"a banner line that is not a hit",
|
|
"",
|
|
"[0.42] docs/bar -- Some other relevant snippet",
|
|
"[0.05] noise/baz -- below the threshold",
|
|
].join("\n");
|
|
|
|
test("parses scored lines, skips non-hit lines", () => {
|
|
const hits = parseSearchHits(sample, 0.1, 10);
|
|
expect(hits).toHaveLength(2);
|
|
expect(hits[0]).toEqual({ score: 0.91, slug: "decisions/foo", snippet: "We chose PGLite for the local engine" });
|
|
expect(hits[1].slug).toBe("docs/bar");
|
|
});
|
|
|
|
test("applies minScore floor", () => {
|
|
expect(parseSearchHits(sample, 0.5, 10)).toHaveLength(1);
|
|
});
|
|
|
|
test("applies limit", () => {
|
|
expect(parseSearchHits(sample, 0.0, 1)).toHaveLength(1);
|
|
});
|
|
|
|
test("empty / garbage input yields no hits (no throw)", () => {
|
|
expect(parseSearchHits("", 0.1, 10)).toEqual([]);
|
|
expect(parseSearchHits("not a hit at all\n???", 0.1, 10)).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("degrade-to-null contract (gbrain absent)", () => {
|
|
// HOME without ~/.gbrain so buildGbrainEnv doesn't seed a DB; PATH without gbrain.
|
|
const absentEnv = { PATH: "/nonexistent-bin-dir", HOME: os.tmpdir() };
|
|
|
|
test("semanticRecall returns null on empty query (no spawn)", () => {
|
|
expect(semanticRecall(" ", absentEnv)).toBeNull();
|
|
});
|
|
|
|
test("semanticRecall returns null when gbrain is not on PATH", () => {
|
|
expect(semanticRecall("pglite", absentEnv)).toBeNull();
|
|
});
|
|
|
|
test("resolveMemorySourceId returns null when gbrain is not on PATH", () => {
|
|
expect(resolveMemorySourceId(absentEnv)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("end-to-end with a fake gbrain shim", () => {
|
|
let binDir: string;
|
|
let homeDir: string;
|
|
|
|
function writeShim(body: string): void {
|
|
const p = path.join(binDir, "gbrain");
|
|
fs.writeFileSync(p, body, { mode: 0o755 });
|
|
fs.chmodSync(p, 0o755);
|
|
}
|
|
function env(): NodeJS.ProcessEnv {
|
|
// Keep the real PATH so /usr/bin/env + bash resolve; prepend the shim dir.
|
|
return { PATH: `${binDir}:${process.env.PATH}`, HOME: homeDir };
|
|
}
|
|
|
|
beforeEach(() => {
|
|
binDir = fs.mkdtempSync(path.join(os.tmpdir(), "gbrain-shim-"));
|
|
homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "gbrain-home-"));
|
|
});
|
|
afterEach(() => {
|
|
fs.rmSync(binDir, { recursive: true, force: true });
|
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test("resolves the worktree-backed source and scopes search to it", () => {
|
|
writeShim(
|
|
`#!/usr/bin/env bash
|
|
if [ "$1" = "sources" ]; then
|
|
echo '{"sources":[{"id":"code","local_path":"/repo","page_count":100},{"id":"default","local_path":"/u/.gstack-brain-worktree","page_count":3}]}'
|
|
exit 0
|
|
fi
|
|
if [ "$1" = "search" ]; then
|
|
if printf '%s ' "$@" | grep -q -- "--source default"; then
|
|
echo "[0.91] decisions/foo -- We chose PGLite for the local engine"
|
|
else
|
|
echo "[0.91] WRONG-SOURCE -- unscoped fallback"
|
|
fi
|
|
echo "[0.05] noise/baz -- below threshold"
|
|
exit 0
|
|
fi
|
|
exit 1
|
|
`,
|
|
);
|
|
expect(resolveMemorySourceId(env())).toBe("default");
|
|
const hits = semanticRecall("pglite", env());
|
|
expect(hits).not.toBeNull();
|
|
expect(hits).toHaveLength(1);
|
|
expect(hits![0].slug).toBe("decisions/foo"); // proves --source default was forwarded
|
|
});
|
|
|
|
test("degrades to null when no curated-memory source (no unscoped fallback)", () => {
|
|
writeShim(
|
|
`#!/usr/bin/env bash
|
|
if [ "$1" = "sources" ]; then echo '{"sources":[{"id":"code","local_path":"/repo"}]}'; exit 0; fi
|
|
if [ "$1" = "search" ]; then echo "[0.50] code/x -- unscoped hit"; exit 0; fi
|
|
exit 1
|
|
`,
|
|
);
|
|
expect(resolveMemorySourceId(env())).toBeNull();
|
|
// no worktree-backed source → null, NOT an unscoped search that would pull code/doc hits
|
|
expect(semanticRecall("anything", env())).toBeNull();
|
|
});
|
|
|
|
test("degrades to null when gbrain search exits non-zero", () => {
|
|
writeShim(
|
|
`#!/usr/bin/env bash
|
|
if [ "$1" = "sources" ]; then echo '{"sources":[{"id":"default","local_path":"/u/.gstack-brain-worktree"}]}'; exit 0; fi
|
|
exit 1
|
|
`,
|
|
);
|
|
expect(semanticRecall("pglite", env())).toBeNull();
|
|
});
|
|
});
|