mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-21 04:10:47 +02:00
test: cover the code-intelligence contract, adapters, selection, and consent
Capability matrix, result parsers, selection store + per-repo consent + provider-OFF, egress gating, and each adapter end-to-end against a fake CLI shim (gbrain, graphify) or injected fetch + temp config.json (sourcebot), plus PROVIDER_UNAVAILABLE degrade for every provider. 19 tests, no live tools required. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f0091e0992
commit
9650e8b056
+186
-149
@@ -1,17 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* Tests for lib/code-intelligence — the OPTIONAL, repo-oriented provider contract.
|
* Tests for lib/code-intelligence — the OPTIONAL, repo-oriented provider contract
|
||||||
|
* with three REAL adapters (GBrain CLI, Graphify CLI, Sourcebot HTTP) plus the
|
||||||
|
* selection store the `gstack-code-intelligence` CLI drives.
|
||||||
*
|
*
|
||||||
* Load-bearing properties:
|
* Load-bearing properties:
|
||||||
* - Every provider advertises the four required capabilities; optional ops are
|
* - Capability matrix: every provider advertises the four required ops; only
|
||||||
* declined with a typed CAPABILITY_UNSUPPORTED, never a silent no-op.
|
* GBrain advertises the document ops (add/delete/export).
|
||||||
* - Sourcebot/Graphify are phase-1 capability declarations: they prove contract
|
* - Consent: non-local providers refuse to index without per-repo consent; a
|
||||||
* fit and throw PROVIDER_UNAVAILABLE until a host MCP transport is wired.
|
* localhost Sourcebot and Graphify are local and need none.
|
||||||
* - The picker recommends GBrain first and resolves to null (provider-OFF) when
|
* - GBrain search + status work end-to-end against a fake `gbrain` shim.
|
||||||
* GBrain is unavailable.
|
* - Graphify index/search/status work end-to-end against a fake `graphify` shim.
|
||||||
* - Non-local providers refuse to move repo content off the machine without
|
* - Sourcebot register (config.json edit) + search work against an injected fetch.
|
||||||
* explicit per-repo consent (PROVIDER_NOT_CONSENTED).
|
* - Selection persists to $GSTACK_HOME; no selection = provider-OFF (null).
|
||||||
* - The GBrain adapter works end-to-end against a fake `gbrain` shim on PATH.
|
* - Every adapter degrades to PROVIDER_UNAVAILABLE when its tool/server is absent.
|
||||||
* - Missing CLI degrades (PROVIDER_UNAVAILABLE), never crashes.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
||||||
@@ -19,149 +20,210 @@ import * as fs from "fs";
|
|||||||
import * as os from "os";
|
import * as os from "os";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import {
|
import {
|
||||||
assertRequiredCapabilities,
|
|
||||||
CodeProviderError,
|
|
||||||
REQUIRED_CAPABILITIES,
|
REQUIRED_CAPABILITIES,
|
||||||
GbrainProvider,
|
GbrainProvider,
|
||||||
SourcebotProvider,
|
|
||||||
GraphifyProvider,
|
GraphifyProvider,
|
||||||
|
SourcebotProvider,
|
||||||
parseGbrainSearch,
|
parseGbrainSearch,
|
||||||
recommendCodeProvider,
|
parseGraphifyQuery,
|
||||||
resolveCodeProvider,
|
parseSourcebotSearch,
|
||||||
|
readSelection,
|
||||||
|
setProvider,
|
||||||
|
setConsent,
|
||||||
|
hasConsent,
|
||||||
|
resolveSelectedProvider,
|
||||||
RECOMMENDED_ORDER,
|
RECOMMENDED_ORDER,
|
||||||
} from "../lib/code-intelligence";
|
} from "../lib/code-intelligence";
|
||||||
|
|
||||||
describe("capability matrix invariants", () => {
|
describe("capability matrix", () => {
|
||||||
test("every provider advertises the four required capabilities", () => {
|
test("every provider advertises the four required capabilities", () => {
|
||||||
for (const p of [new GbrainProvider(), new SourcebotProvider(), new GraphifyProvider()]) {
|
for (const p of [new GbrainProvider(), new SourcebotProvider(), new GraphifyProvider()]) {
|
||||||
for (const cap of REQUIRED_CAPABILITIES) expect(p.has(cap)).toBe(true);
|
for (const cap of REQUIRED_CAPABILITIES) expect(p.has(cap)).toBe(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("GBrain advertises all seven (document axis)", () => {
|
test("only GBrain advertises the document ops; local flags are right", () => {
|
||||||
const g = new GbrainProvider();
|
const g = new GbrainProvider();
|
||||||
for (const cap of ["add", "delete", "export"] as const) expect(g.has(cap)).toBe(true);
|
|
||||||
expect(g.local).toBe(false);
|
expect(g.local).toBe(false);
|
||||||
});
|
for (const cap of ["add", "delete", "export"] as const) expect(g.has(cap)).toBe(true);
|
||||||
|
|
||||||
test("Sourcebot is search-only: declines add/delete/export", () => {
|
const s = new SourcebotProvider({ baseUrl: "http://localhost:3000" });
|
||||||
const s = new SourcebotProvider();
|
expect(s.local).toBe(true); // loopback → content stays on machine
|
||||||
expect(s.has("add")).toBe(false);
|
expect(s.has("add")).toBe(false);
|
||||||
expect(s.has("delete")).toBe(false);
|
expect(new SourcebotProvider({ baseUrl: "https://sb.example.com" }).local).toBe(false);
|
||||||
expect(s.has("export")).toBe(false);
|
|
||||||
expect(s.local).toBe(false);
|
const gf = new GraphifyProvider();
|
||||||
|
expect(gf.local).toBe(true);
|
||||||
|
expect(gf.has("export")).toBe(true);
|
||||||
|
expect(gf.has("add")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Graphify is local, exports, declines add/delete", () => {
|
|
||||||
const g = new GraphifyProvider();
|
|
||||||
expect(g.local).toBe(true);
|
|
||||||
expect(g.has("export")).toBe(true);
|
|
||||||
expect(g.has("add")).toBe(false);
|
|
||||||
expect(g.has("delete")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("assertRequiredCapabilities rejects an incomplete provider", () => {
|
|
||||||
expect(() => assertRequiredCapabilities("sourcebot", new Set(["search"]))).toThrow(/missing required/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("CodeProviderError rejects an unknown failure code", () => {
|
|
||||||
// @ts-expect-error deliberately invalid code
|
|
||||||
expect(() => new CodeProviderError("NOPE", "x")).toThrow(/Unknown code-provider failure/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("optional ops decline with a typed failure", () => {
|
|
||||||
test("Sourcebot declines add/delete/export with CAPABILITY_UNSUPPORTED", async () => {
|
|
||||||
const s = new SourcebotProvider();
|
|
||||||
await expect(s.add({ slug: "x", body: "y" })).rejects.toMatchObject({ code: "CAPABILITY_UNSUPPORTED" });
|
|
||||||
await expect(s.delete("x")).rejects.toMatchObject({ code: "CAPABILITY_UNSUPPORTED" });
|
|
||||||
await expect(s.export({ id: "x" })).rejects.toMatchObject({ code: "CAPABILITY_UNSUPPORTED" });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Graphify advertises export → not CAPABILITY_UNSUPPORTED, but unwired in phase 1", async () => {
|
|
||||||
await expect(new GraphifyProvider().export({ id: "x" })).rejects.toMatchObject({ code: "PROVIDER_UNAVAILABLE" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("phase-1 declaration adapters degrade to PROVIDER_UNAVAILABLE", () => {
|
|
||||||
test("Sourcebot.search is unwired until a transport lands", async () => {
|
|
||||||
await expect(new SourcebotProvider().search("q")).rejects.toMatchObject({ code: "PROVIDER_UNAVAILABLE" });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("empty query short-circuits to no hits (no throw)", async () => {
|
|
||||||
expect(await new SourcebotProvider().search(" ")).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("status is partial + unknown (no live probe yet)", async () => {
|
|
||||||
const s = await new GraphifyProvider().status({ id: "repo" });
|
|
||||||
expect(s).toMatchObject({ id: "repo", state: "unknown", partial: true });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("egress consent gate", () => {
|
|
||||||
test("non-local registerSource without consent → PROVIDER_NOT_CONSENTED", async () => {
|
|
||||||
await expect(new SourcebotProvider().registerSource({ id: "repo", path: "/repo" })).rejects.toMatchObject({
|
|
||||||
code: "PROVIDER_NOT_CONSENTED",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("local provider (Graphify) skips the egress gate (falls through to unwired)", async () => {
|
|
||||||
// Local means nothing leaves the machine, so consent is not required — it
|
|
||||||
// reaches the phase-1 PROVIDER_UNAVAILABLE, NOT a consent rejection.
|
|
||||||
await expect(new GraphifyProvider().registerSource({ id: "repo", path: "/repo" })).rejects.toMatchObject({
|
|
||||||
code: "PROVIDER_UNAVAILABLE",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("parseGbrainSearch (text surface)", () => {
|
|
||||||
const sample = ["[0.91] slug/a -- snippet one", "banner", "[0.05] slug/b -- below floor"].join("\n");
|
|
||||||
test("parses scored lines, applies floor + limit", () => {
|
|
||||||
expect(parseGbrainSearch(sample, 0.1, 10)).toEqual([
|
|
||||||
{ ref: "slug/a", score: 0.91, snippet: "snippet one", kind: "document" },
|
|
||||||
]);
|
|
||||||
expect(parseGbrainSearch(sample, 0.0, 1)).toHaveLength(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("picker — recommend GBrain first", () => {
|
|
||||||
test("RECOMMENDED_ORDER puts GBrain first", () => {
|
test("RECOMMENDED_ORDER puts GBrain first", () => {
|
||||||
expect(RECOMMENDED_ORDER[0]).toBe("gbrain");
|
expect(RECOMMENDED_ORDER[0]).toBe("gbrain");
|
||||||
expect([...RECOMMENDED_ORDER]).toEqual(["gbrain", "sourcebot", "graphify"]);
|
expect([...RECOMMENDED_ORDER]).toEqual(["gbrain", "sourcebot", "graphify"]);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("GBrain resolves when usable", () => {
|
describe("parsers", () => {
|
||||||
expect(recommendCodeProvider({ gbrainStatus: "ok" }).map((p) => p.id)).toEqual(["gbrain"]);
|
test("parseGbrainSearch (text surface)", () => {
|
||||||
expect(resolveCodeProvider({ gbrainStatus: "ok" })?.id).toBe("gbrain");
|
const hits = parseGbrainSearch("[0.91] slug/a -- one\nbanner\n[0.05] slug/b -- low", 0.1, 10);
|
||||||
|
expect(hits).toEqual([{ ref: "slug/a", score: 0.91, snippet: "one", kind: "document" }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("timeout status counts as usable (engine slow, not absent)", () => {
|
test("parseGraphifyQuery maps path-like lines to refs", () => {
|
||||||
expect(resolveCodeProvider({ gbrainStatus: "timeout" })?.id).toBe("gbrain");
|
const hits = parseGraphifyQuery("src/a.ts calls foo()\njust prose here", 10);
|
||||||
|
expect(hits[0]).toMatchObject({ ref: "src/a.ts", kind: "graph-node" });
|
||||||
|
expect(hits[1]).toMatchObject({ ref: "graphify" });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("provider-OFF: GBrain down → resolveCodeProvider null", () => {
|
test("parseSourcebotSearch maps files to file:line hits, tolerates garbage", () => {
|
||||||
expect(recommendCodeProvider({ gbrainStatus: "no-cli" })).toEqual([]);
|
const payload = {
|
||||||
expect(resolveCodeProvider({ gbrainStatus: "no-cli" })).toBeNull();
|
files: [{ fileName: { text: "src/x.ts" }, chunks: [{ content: "hit", matchRanges: [{ start: { lineNumber: 12 } }] }] }],
|
||||||
expect(resolveCodeProvider({ gbrainStatus: "missing-config" })).toBeNull();
|
};
|
||||||
|
expect(parseSourcebotSearch(payload, 10)).toEqual([{ ref: "src/x.ts:12", snippet: "hit", kind: "file" }]);
|
||||||
|
expect(parseSourcebotSearch("nope", 10)).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("GBrain adapter end-to-end (fake shim on PATH)", () => {
|
describe("selection store + provider-OFF", () => {
|
||||||
|
let home: string;
|
||||||
|
let env: NodeJS.ProcessEnv;
|
||||||
|
beforeEach(() => {
|
||||||
|
home = fs.mkdtempSync(path.join(os.tmpdir(), "ci-home-"));
|
||||||
|
env = { ...process.env, GSTACK_HOME: home };
|
||||||
|
});
|
||||||
|
afterEach(() => fs.rmSync(home, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
test("no selection = provider-OFF (null)", () => {
|
||||||
|
expect(readSelection(env).provider).toBeNull();
|
||||||
|
expect(resolveSelectedProvider({ env })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("select persists and resolves the provider", () => {
|
||||||
|
setProvider("graphify", env);
|
||||||
|
expect(readSelection(env).provider).toBe("graphify");
|
||||||
|
expect(resolveSelectedProvider({ env })?.id).toBe("graphify");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("consent is per-repo", () => {
|
||||||
|
const repo = path.join(home, "repoA");
|
||||||
|
expect(hasConsent(repo, env)).toBe(false);
|
||||||
|
setConsent(repo, true, env);
|
||||||
|
expect(hasConsent(repo, env)).toBe(true);
|
||||||
|
expect(hasConsent(path.join(home, "repoB"), env)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("egress consent gate", () => {
|
||||||
|
test("GBrain (non-local) registerSource without consent → PROVIDER_NOT_CONSENTED", async () => {
|
||||||
|
await expect(new GbrainProvider().registerSource({ id: "code", path: "/repo" })).rejects.toMatchObject({
|
||||||
|
code: "PROVIDER_NOT_CONSENTED",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Graphify (local) is exempt from the egress gate", async () => {
|
||||||
|
// Local → no consent needed; it reaches the CLI (absent here → UNAVAILABLE),
|
||||||
|
// NOT a consent rejection.
|
||||||
|
await expect(
|
||||||
|
new GraphifyProvider({ env: { PATH: "/nonexistent" } }).registerSource({ id: "r", path: os.tmpdir() }),
|
||||||
|
).rejects.toMatchObject({ code: "PROVIDER_UNAVAILABLE" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Graphify adapter (fake graphify shim on PATH)", () => {
|
||||||
|
let binDir: string;
|
||||||
|
let repo: string;
|
||||||
|
function env(): NodeJS.ProcessEnv {
|
||||||
|
return { PATH: `${binDir}:${process.env.PATH}` };
|
||||||
|
}
|
||||||
|
beforeEach(() => {
|
||||||
|
binDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-gf-bin-"));
|
||||||
|
repo = fs.mkdtempSync(path.join(os.tmpdir(), "ci-gf-repo-"));
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(binDir, { recursive: true, force: true });
|
||||||
|
fs.rmSync(repo, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("index builds a graph and status reports ready + node count", async () => {
|
||||||
|
// Shim: `graphify .` writes graphify-out/graph.json in cwd; `graphify query` prints a hit line.
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(binDir, "graphify"),
|
||||||
|
`#!/usr/bin/env bash
|
||||||
|
if [ "$1" = "query" ]; then echo "src/a.ts -> src/b.ts (calls)"; exit 0; fi
|
||||||
|
mkdir -p "$PWD/graphify-out"
|
||||||
|
echo '{"nodes":[1,2,3]}' > "$PWD/graphify-out/graph.json"
|
||||||
|
exit 0
|
||||||
|
`,
|
||||||
|
{ mode: 0o755 },
|
||||||
|
);
|
||||||
|
const gf = new GraphifyProvider({ root: repo, env: env() });
|
||||||
|
const reg = await gf.registerSource({ id: "r", path: repo });
|
||||||
|
expect(reg.state).toBe("ready");
|
||||||
|
expect(reg.itemCount).toBe(3);
|
||||||
|
|
||||||
|
const hits = await gf.search("what calls b", { source: repo });
|
||||||
|
expect(hits[0].ref).toBe("src/a.ts");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("missing graphify CLI degrades to PROVIDER_UNAVAILABLE", async () => {
|
||||||
|
await expect(
|
||||||
|
new GraphifyProvider({ root: repo, env: { PATH: binDir } }).search("q"),
|
||||||
|
).rejects.toMatchObject({ code: "PROVIDER_UNAVAILABLE" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Sourcebot adapter (injected fetch + temp config)", () => {
|
||||||
|
test("registerSource writes a local git connection to config.json", async () => {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-sb-"));
|
||||||
|
const configPath = path.join(dir, "config.json");
|
||||||
|
const sb = new SourcebotProvider({ baseUrl: "http://localhost:3000", configPath });
|
||||||
|
await sb.registerSource({ id: "myrepo", path: "/abs/repo" });
|
||||||
|
const written = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
||||||
|
expect(written.connections.myrepo).toEqual({ type: "git", url: "file:///abs/repo" });
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("search POSTs /api/search and maps files to hits", async () => {
|
||||||
|
const calls: Array<{ url: string; body: unknown }> = [];
|
||||||
|
const fetchStub = (async (url: string, init: RequestInit) => {
|
||||||
|
calls.push({ url: String(url), body: JSON.parse(String(init.body)) });
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ files: [{ fileName: { text: "a.ts" }, chunks: [{ content: "x", matchRanges: [{ start: { lineNumber: 3 } }] }] }] }),
|
||||||
|
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||||
|
);
|
||||||
|
}) as unknown as typeof fetch;
|
||||||
|
const sb = new SourcebotProvider({ baseUrl: "http://localhost:3000", fetch: fetchStub });
|
||||||
|
const hits = await sb.search("foo", { limit: 5 });
|
||||||
|
expect(calls[0].url).toBe("http://localhost:3000/api/search");
|
||||||
|
expect((calls[0].body as { isRegexEnabled: boolean }).isRegexEnabled).toBe(true);
|
||||||
|
expect(hits).toEqual([{ ref: "a.ts:3", snippet: "x", kind: "file" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unreachable server degrades to PROVIDER_UNAVAILABLE", async () => {
|
||||||
|
const fetchStub = (async () => {
|
||||||
|
throw new Error("ECONNREFUSED");
|
||||||
|
}) as unknown as typeof fetch;
|
||||||
|
await expect(
|
||||||
|
new SourcebotProvider({ baseUrl: "http://localhost:3999", fetch: fetchStub }).search("q"),
|
||||||
|
).rejects.toMatchObject({ code: "PROVIDER_UNAVAILABLE" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("registerSource without SOURCEBOT_CONFIG → PROVIDER_UNAVAILABLE", async () => {
|
||||||
|
const sb = new SourcebotProvider({ baseUrl: "http://localhost:3000", env: {} });
|
||||||
|
await expect(sb.registerSource({ id: "r", path: "/x" })).rejects.toMatchObject({ code: "PROVIDER_UNAVAILABLE" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("GBrain adapter end-to-end (fake gbrain shim on PATH)", () => {
|
||||||
let binDir: string;
|
let binDir: string;
|
||||||
let homeDir: 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 {
|
function env(): NodeJS.ProcessEnv {
|
||||||
return { PATH: `${binDir}:${process.env.PATH}`, HOME: homeDir };
|
return { PATH: `${binDir}:${process.env.PATH}`, HOME: homeDir };
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
binDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-gbrain-shim-"));
|
binDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-gb-bin-"));
|
||||||
homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-gbrain-home-"));
|
homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-gb-home-"));
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
fs.rmSync(binDir, { recursive: true, force: true });
|
fs.rmSync(binDir, { recursive: true, force: true });
|
||||||
@@ -169,50 +231,25 @@ describe("GBrain adapter end-to-end (fake shim on PATH)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("search scopes to source and parses hits", async () => {
|
test("search scopes to source and parses hits", async () => {
|
||||||
writeShim(`#!/usr/bin/env bash
|
fs.writeFileSync(
|
||||||
|
path.join(binDir, "gbrain"),
|
||||||
|
`#!/usr/bin/env bash
|
||||||
if [ "$1" = "search" ]; then
|
if [ "$1" = "search" ]; then
|
||||||
if printf '%s ' "$@" | grep -q -- "--source code"; then
|
if printf '%s ' "$@" | grep -q -- "--source code"; then echo "[0.88] src/x.ts -- match"; else echo "[0.10] wrong -- unscoped"; fi
|
||||||
echo "[0.88] src/x.ts -- match in code source"
|
|
||||||
else
|
|
||||||
echo "[0.10] wrong -- unscoped"
|
|
||||||
fi
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
exit 1
|
exit 1
|
||||||
`);
|
`,
|
||||||
|
{ mode: 0o755 },
|
||||||
|
);
|
||||||
const hits = await new GbrainProvider().search("where", { env: env(), source: "code" });
|
const hits = await new GbrainProvider().search("where", { env: env(), source: "code" });
|
||||||
expect(hits).toHaveLength(1);
|
expect(hits).toHaveLength(1);
|
||||||
expect(hits[0].ref).toBe("src/x.ts");
|
expect(hits[0].ref).toBe("src/x.ts");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("status(source) reports ready + page_count", async () => {
|
|
||||||
writeShim(`#!/usr/bin/env bash
|
|
||||||
if [ "$1" = "sources" ]; then echo '{"sources":[{"id":"code","local_path":"/repo","page_count":42}]}'; exit 0; fi
|
|
||||||
exit 1
|
|
||||||
`);
|
|
||||||
const s = await new GbrainProvider().status({ id: "code" }, { env: env() });
|
|
||||||
expect(s.state).toBe("ready");
|
|
||||||
expect(s.itemCount).toBe(42);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("status(source) reports absent for an unregistered id", async () => {
|
|
||||||
writeShim(`#!/usr/bin/env bash
|
|
||||||
if [ "$1" = "sources" ]; then echo '{"sources":[]}'; exit 0; fi
|
|
||||||
exit 1
|
|
||||||
`);
|
|
||||||
expect((await new GbrainProvider().status({ id: "nope" }, { env: env() })).state).toBe("absent");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("missing CLI degrades to PROVIDER_UNAVAILABLE", async () => {
|
test("missing CLI degrades to PROVIDER_UNAVAILABLE", async () => {
|
||||||
// No shim written; PATH points only at an empty dir.
|
|
||||||
await expect(
|
await expect(
|
||||||
new GbrainProvider().search("q", { env: { PATH: binDir, HOME: homeDir } }),
|
new GbrainProvider().search("q", { env: { PATH: binDir, HOME: homeDir } }),
|
||||||
).rejects.toMatchObject({ code: "PROVIDER_UNAVAILABLE" });
|
).rejects.toMatchObject({ code: "PROVIDER_UNAVAILABLE" });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("registerSource requires egress consent (GBrain is non-local)", async () => {
|
|
||||||
await expect(
|
|
||||||
new GbrainProvider().registerSource({ id: "code", path: "/repo" }, { env: env() }),
|
|
||||||
).rejects.toMatchObject({ code: "PROVIDER_NOT_CONSENTED" });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user