mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-17 07:10:12 +02:00
fa250db27b
Adds gstack-decision-search --semantic (with --query): appends a 'Related from memory' block from gbrain semantic search, scoped to the curated-memory source. Pure enhancement, reliability-first: a new lib/gstack-decision-semantic.ts is the ONLY decision module that touches gbrain and is imported lazily only on --semantic, so the reliable file path never loads gbrain code. Every path degrades to the reliable file results when gbrain is off, unconfigured, empty, or errors (never throws, 10s timeout). Built against the verified gbrain 0.42.x surface (text output [score] slug -- snippet, NOT JSON; curated-memory source resolved by worktree path, not a gstack-brain-<user> id). Deterministic-contract tests only: parser units, degrade-to-null when gbrain absent, and a fake-gbrain shim proving scope+search end-to-end. find-contradictions deferred (no verifiable CLI surface yet + curated memory not indexed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
140 lines
4.7 KiB
TypeScript
140 lines
4.7 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("searches unscoped when no worktree source is registered", () => {
|
|
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();
|
|
const hits = semanticRecall("anything", env());
|
|
expect(hits).not.toBeNull();
|
|
expect(hits![0].slug).toBe("code/x");
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|