/** * Unit tests for bin/gstack-brain-context-load.ts (Lane C). * * Tests CLI surface, template var substitution, manifest vs default-fallback * routing, datamark envelope wrapping, and graceful degradation when gbrain * CLI is missing. Full E2E (real gbrain MCP calls) lives in Lane F. */ import { describe, it, expect } from "bun:test"; import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; import { spawnSync } from "child_process"; const SCRIPT = join(import.meta.dir, "..", "bin", "gstack-brain-context-load.ts"); function runScript(args: string[], env: Record = {}): { stdout: string; stderr: string; exitCode: number } { const result = spawnSync("bun", [SCRIPT, ...args], { encoding: "utf-8", timeout: 30000, env: { ...process.env, ...env }, }); return { stdout: result.stdout || "", stderr: result.stderr || "", exitCode: result.status ?? 1, }; } describe("gstack-brain-context-load CLI", () => { it("--help exits 0 with usage", () => { const r = runScript(["--help"]); expect(r.exitCode).toBe(0); expect(r.stderr).toContain("Usage: gstack-brain-context-load"); expect(r.stderr).toContain("--skill"); expect(r.stderr).toContain("--repo"); }); it("rejects unknown flag", () => { const r = runScript(["--bogus"]); expect(r.exitCode).toBe(1); expect(r.stderr).toContain("Unknown argument: --bogus"); }); it("--limit must be positive integer", () => { const r = runScript(["--limit", "0"]); expect(r.exitCode).toBe(1); expect(r.stderr).toContain("--limit requires a positive integer"); }); }); describe("gstack-brain-context-load — manifest dispatch", () => { it("falls back to default manifest when --skill resolves to no file", () => { const r = runScript(["--skill", "nonexistent-skill-xyz", "--repo", "test-repo", "--explain", "--quiet"]); expect(r.exitCode).toBe(0); expect(r.stderr).toContain("mode=default"); // 3 queries in default expect(r.stderr).toContain("queries=3"); }); it("uses skill manifest when --skill-file points at a valid SKILL.md", () => { const dir = mkdtempSync(join(tmpdir(), "gstack-bcl-")); const skillFile = join(dir, "SKILL.md"); writeFileSync( skillFile, `--- name: test-skill gbrain: schema: 1 context_queries: - id: my-prior kind: filesystem glob: "${dir}/notes/*.md" sort: mtime_desc limit: 5 render_as: "## My prior notes" --- body `, "utf-8" ); // Create some matching files mkdirSync(join(dir, "notes")); writeFileSync(join(dir, "notes", "one.md"), "first\n"); writeFileSync(join(dir, "notes", "two.md"), "second\n"); const r = runScript(["--skill-file", skillFile, "--repo", "test-repo", "--explain"]); expect(r.exitCode).toBe(0); expect(r.stderr).toContain("mode=manifest"); expect(r.stderr).toContain("queries=1"); expect(r.stdout).toContain("## My prior notes"); expect(r.stdout).toContain("one.md"); expect(r.stdout).toContain("two.md"); rmSync(dir, { recursive: true, force: true }); }); it("wraps rendered body in USER_TRANSCRIPT_DATA envelope (datamark per D12)", () => { const dir = mkdtempSync(join(tmpdir(), "gstack-bcl-")); const skillFile = join(dir, "SKILL.md"); writeFileSync( skillFile, `--- name: x gbrain: schema: 1 context_queries: - id: fs kind: filesystem glob: "${dir}/*.md" render_as: "## FS results" --- `, "utf-8" ); writeFileSync(join(dir, "a.md"), "x\n"); const r = runScript(["--skill-file", skillFile]); expect(r.exitCode).toBe(0); expect(r.stdout).toContain(""); expect(r.stdout).toContain(""); rmSync(dir, { recursive: true, force: true }); }); it("substitutes {repo_slug} in render_as", () => { const dir = mkdtempSync(join(tmpdir(), "gstack-bcl-")); const skillFile = join(dir, "SKILL.md"); writeFileSync( skillFile, `--- name: x gbrain: schema: 1 context_queries: - id: fs kind: filesystem glob: "${dir}/*.md" render_as: "## My events for {repo_slug}" --- `, "utf-8" ); writeFileSync(join(dir, "a.md"), "x\n"); const r = runScript(["--skill-file", skillFile, "--repo", "my-test-repo"]); expect(r.exitCode).toBe(0); expect(r.stdout).toContain("## My events for my-test-repo"); rmSync(dir, { recursive: true, force: true }); }); it("skips queries with unresolved template vars (logged via --explain)", () => { const dir = mkdtempSync(join(tmpdir(), "gstack-bcl-")); const skillFile = join(dir, "SKILL.md"); writeFileSync( skillFile, `--- name: x gbrain: schema: 1 context_queries: - id: needs-user kind: filesystem glob: "${dir}/{user_slug}/file.md" render_as: "## Needs user_slug" --- `, "utf-8" ); // No --user passed; {user_slug} unresolved const r = runScript(["--skill-file", skillFile, "--repo", "x", "--explain"]); expect(r.exitCode).toBe(0); expect(r.stderr).toContain("template vars unresolved"); expect(r.stderr).toContain("user_slug"); rmSync(dir, { recursive: true, force: true }); }); it("--quiet suppresses rendered output", () => { const dir = mkdtempSync(join(tmpdir(), "gstack-bcl-")); const skillFile = join(dir, "SKILL.md"); writeFileSync( skillFile, `--- name: x gbrain: schema: 1 context_queries: - id: fs kind: filesystem glob: "${dir}/*.md" render_as: "## Stuff" --- `, "utf-8" ); writeFileSync(join(dir, "a.md"), "x\n"); const r = runScript(["--skill-file", skillFile, "--quiet"]); expect(r.exitCode).toBe(0); expect(r.stdout).toBe(""); rmSync(dir, { recursive: true, force: true }); }); }); describe("gstack-brain-context-load — graceful gbrain absence", () => { it("vector + list queries still complete (with SKIP) when gbrain CLI is missing", () => { // We can't easily un-install gbrain; rely on the helper's own missing-binary // detection. The default manifest uses kind: list which calls gbrain. If // gbrain is missing, the helper should still exit 0 and explain shows SKIP. // We use --explain to verify the SKIP code path doesn't hard-fail. const r = runScript(["--repo", "test-repo", "--explain", "--quiet"]); expect(r.exitCode).toBe(0); // Either OK (gbrain available) or SKIP (gbrain missing or query timeout) — both fine expect(r.stderr).toMatch(/(OK|SKIP)/); }); });