mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-02 17:40:02 +02:00
- design: serve.test.ts drove an inline mirror server; now two tests run the real serve() on an ephemeral port (reload confinement, submit exit 0). - setup-gbrain: rollback + voyage tests execute the template-extracted init blocks (3 sites) instead of drifted local bash copies. - terminal-agent: internalHandler source greps replaced by a behavioral /internal/grant + /internal/revoke auth matrix (no/wrong/valid token). - /health: server-security-surface and the server-auth / security-audit-r2 / sidebar-tabs source greps fold into one liveness-only check on the real body; the L4 sidecar wiring gets a behavioral /pty-inject-scan test. - delete tautologies (browser-manager onDisconnect, memory-command #12), ios swiftui tap fixture self-check, memory-ingest put_page grep, detach source greps, sidebar-agent absence pins, dead-CSS pins + the dead CSS, security-audit-r2 Task 1 + the test-only meta-commands re-export, duplicate generated-SKILL.md checks. - make-pdf coverage-gaps cases move into their owner test files.
220 lines
8.7 KiB
TypeScript
220 lines
8.7 KiB
TypeScript
/**
|
|
* setup-gbrain's local PGLite init sequences, executed from the TEMPLATE.
|
|
*
|
|
* The contract lives in skill template prose the model executes, not in a TS
|
|
* helper, so this file extracts each fenced bash block that runs
|
|
* `gbrain init --pglite --json "$@"` from the .tmpl files and runs it against
|
|
* a fake `gbrain` in a sandboxed HOME. A template edit changes what runs here;
|
|
* there is no hand-copied shell to drift.
|
|
*
|
|
* Contracts:
|
|
* 1. voyage-code-3 default: with VOYAGE_API_KEY set, every init site passes
|
|
* --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024 as
|
|
* separate argv words (also under zsh, #1798); unset or empty omits them.
|
|
* 2. .bak rollback (plan D7): the two rollback-wrapped sites move an existing
|
|
* ~/.gbrain/config.json aside, restore it byte-for-byte when init fails
|
|
* (leaving a partial PGLite dir alone), and keep the backup for audit when
|
|
* init succeeds.
|
|
*/
|
|
|
|
import { describe, it, expect } from "bun:test";
|
|
import {
|
|
mkdtempSync,
|
|
mkdirSync,
|
|
writeFileSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
existsSync,
|
|
rmSync,
|
|
chmodSync,
|
|
} from "fs";
|
|
import { tmpdir } from "os";
|
|
import { join } from "path";
|
|
import { spawnSync } from "child_process";
|
|
|
|
const SETUP_GBRAIN = join(import.meta.dir, "..", "setup-gbrain");
|
|
const TEMPLATES = {
|
|
skeleton: join(SETUP_GBRAIN, "SKILL.md.tmpl"),
|
|
brainInit: join(SETUP_GBRAIN, "sections", "brain-init.md.tmpl"),
|
|
remediation: join(SETUP_GBRAIN, "sections", "engine-remediation.md.tmpl"),
|
|
};
|
|
const INIT_CALL = 'gbrain init --pglite --json "$@"';
|
|
|
|
function initBlocks(tmplPath: string): string[] {
|
|
const src = readFileSync(tmplPath, "utf-8");
|
|
return [...src.matchAll(/```bash\n([\s\S]*?)```/g)]
|
|
.map((m) => m[1])
|
|
.filter((block) => block.includes(INIT_CALL));
|
|
}
|
|
|
|
const PATH3_BLOCK = initBlocks(TEMPLATES.brainInit).find((b) => !b.includes("gstack-bak"));
|
|
const PATH4_BLOCK = initBlocks(TEMPLATES.brainInit).find((b) => b.includes("gstack-bak"));
|
|
const REMEDIATION_BLOCK = initBlocks(TEMPLATES.remediation).find((b) => b.includes("gstack-bak"));
|
|
const ROLLBACK_SITES = { "Path 4 local code search": PATH4_BLOCK, "engine remediation": REMEDIATION_BLOCK };
|
|
const ALL_SITES = { "Path 3 PGLite": PATH3_BLOCK, ...ROLLBACK_SITES };
|
|
|
|
interface Sandbox {
|
|
home: string;
|
|
bindir: string;
|
|
configPath: string;
|
|
argvLog: string;
|
|
cleanup: () => void;
|
|
}
|
|
|
|
function makeSandbox(opts: { initFails?: boolean; seedConfig?: boolean } = {}): Sandbox {
|
|
const tmp = mkdtempSync(join(tmpdir(), "gbrain-pglite-init-"));
|
|
const home = join(tmp, "home");
|
|
const gbrainDir = join(home, ".gbrain");
|
|
const bindir = join(tmp, "bin");
|
|
const configPath = join(gbrainDir, "config.json");
|
|
const argvLog = join(tmp, "gbrain-argv.log");
|
|
mkdirSync(gbrainDir, { recursive: true });
|
|
mkdirSync(bindir, { recursive: true });
|
|
const installer = join(home, ".claude", "skills", "gstack", "bin", "gstack-gbrain-install");
|
|
mkdirSync(join(installer, ".."), { recursive: true });
|
|
writeFileSync(installer, "#!/bin/sh\nexit 0\n");
|
|
chmodSync(installer, 0o755);
|
|
if (opts.seedConfig) {
|
|
writeFileSync(configPath, JSON.stringify({ engine: "postgres", database_url: "postgresql://stale@localhost/gbrain" }));
|
|
}
|
|
const onInit = opts.initFails
|
|
? `mkdir -p "${gbrainDir}/pglite" && : > "${gbrainDir}/pglite/partial-write.tmp"; echo "Error: disk full" >&2; exit 1`
|
|
: `printf '{"engine":"pglite"}' > "${configPath}"; echo '{"status":"success"}'; exit 0`;
|
|
writeFileSync(
|
|
join(bindir, "gbrain"),
|
|
`#!/bin/sh\necho "$@" >> "${argvLog}"\necho "$#" >> "${argvLog}.argc"\nif [ "$1" = "init" ]; then ${onInit}; fi\nexit 0\n`,
|
|
);
|
|
chmodSync(join(bindir, "gbrain"), 0o755);
|
|
return { home, bindir, configPath, argvLog, cleanup: () => rmSync(tmp, { recursive: true, force: true }) };
|
|
}
|
|
|
|
function runBlock(sb: Sandbox, block: string, opts: { voyageKey?: string; shell?: "bash" | "zsh" } = {}) {
|
|
const env: Record<string, string> = { ...process.env, HOME: sb.home, PATH: `${sb.bindir}:/usr/bin:/bin` };
|
|
delete env.VOYAGE_API_KEY;
|
|
if (opts.voyageKey !== undefined) env.VOYAGE_API_KEY = opts.voyageKey;
|
|
const r = spawnSync(opts.shell ?? "bash", ["-c", block], { encoding: "utf-8", env, timeout: 30_000 });
|
|
const argv = existsSync(sb.argvLog) ? readFileSync(sb.argvLog, "utf-8").trim().split("\n") : [];
|
|
const argc = existsSync(`${sb.argvLog}.argc`)
|
|
? readFileSync(`${sb.argvLog}.argc`, "utf-8").trim().split("\n").map(Number)
|
|
: [];
|
|
return { status: r.status, stderr: r.stderr ?? "", argv, argc };
|
|
}
|
|
|
|
function backups(sb: Sandbox): string[] {
|
|
return readdirSync(join(sb.home, ".gbrain")).filter((f) => f.includes(".gstack-bak-"));
|
|
}
|
|
|
|
const HAVE_ZSH = spawnSync("zsh", ["-c", "true"], { timeout: 30_000 }).status === 0;
|
|
|
|
describe("template extraction", () => {
|
|
it("finds all three PGLite init blocks (a template restructure must update this file)", () => {
|
|
expect(PATH3_BLOCK).toBeDefined();
|
|
expect(PATH4_BLOCK).toBeDefined();
|
|
expect(REMEDIATION_BLOCK).toBeDefined();
|
|
expect(initBlocks(TEMPLATES.skeleton)).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("voyage-code-3 default at every PGLite init site", () => {
|
|
for (const [site, block] of Object.entries(ALL_SITES)) {
|
|
it(`${site}: passes voyage-code-3 flags when VOYAGE_API_KEY is set`, () => {
|
|
const sb = makeSandbox();
|
|
try {
|
|
const r = runBlock(sb, block!, { voyageKey: "vk_test_set" });
|
|
expect(r.argv).toEqual(["init --pglite --json --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024"]);
|
|
expect(r.argc).toEqual([7]);
|
|
} finally {
|
|
sb.cleanup();
|
|
}
|
|
});
|
|
|
|
it(`${site}: omits voyage flags when VOYAGE_API_KEY is unset or empty`, () => {
|
|
for (const voyageKey of [undefined, ""]) {
|
|
const sb = makeSandbox();
|
|
try {
|
|
const r = runBlock(sb, block!, { voyageKey });
|
|
expect(r.argv).toEqual(["init --pglite --json"]);
|
|
} finally {
|
|
sb.cleanup();
|
|
}
|
|
}
|
|
});
|
|
|
|
it(`${site}: zsh passes the flags as SEPARATE argv words (#1798)`, () => {
|
|
if (!HAVE_ZSH) return;
|
|
const sb = makeSandbox();
|
|
try {
|
|
expect(runBlock(sb, block!, { voyageKey: "vk_test_set", shell: "zsh" }).argc).toEqual([7]);
|
|
} finally {
|
|
sb.cleanup();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
describe(".bak rollback contract (plan D7)", () => {
|
|
for (const [site, block] of Object.entries(ROLLBACK_SITES)) {
|
|
it(`${site}: failed init restores the original config and leaves partial PGLite state alone`, () => {
|
|
const sb = makeSandbox({ initFails: true, seedConfig: true });
|
|
try {
|
|
const original = readFileSync(sb.configPath, "utf-8");
|
|
const r = runBlock(sb, block!);
|
|
expect(r.stderr).toContain("restored");
|
|
expect(readFileSync(sb.configPath, "utf-8")).toBe(original);
|
|
expect(backups(sb)).toEqual([]);
|
|
expect(existsSync(join(sb.home, ".gbrain", "pglite", "partial-write.tmp"))).toBe(true);
|
|
} finally {
|
|
sb.cleanup();
|
|
}
|
|
});
|
|
|
|
it(`${site}: successful init installs the new config and keeps the backup for audit`, () => {
|
|
const sb = makeSandbox({ seedConfig: true });
|
|
try {
|
|
const r = runBlock(sb, block!);
|
|
expect(r.status).toBe(0);
|
|
expect(JSON.parse(readFileSync(sb.configPath, "utf-8")).engine).toBe("pglite");
|
|
expect(backups(sb).length).toBe(1);
|
|
} finally {
|
|
sb.cleanup();
|
|
}
|
|
});
|
|
}
|
|
|
|
it("Path 4 continues setup after a failed init; engine remediation stops with exit 1", () => {
|
|
const path4 = makeSandbox({ initFails: true, seedConfig: true });
|
|
const remediation = makeSandbox({ initFails: true, seedConfig: true });
|
|
try {
|
|
const p4 = runBlock(path4, PATH4_BLOCK!);
|
|
expect(p4.status).toBe(0);
|
|
expect(p4.stderr).toContain("Continuing setup without local code search");
|
|
expect(runBlock(remediation, REMEDIATION_BLOCK!).status).toBe(1);
|
|
} finally {
|
|
path4.cleanup();
|
|
remediation.cleanup();
|
|
}
|
|
});
|
|
|
|
it("Path 4 with no existing config: failed init creates no backup and no config", () => {
|
|
const sb = makeSandbox({ initFails: true });
|
|
try {
|
|
runBlock(sb, PATH4_BLOCK!);
|
|
expect(backups(sb)).toEqual([]);
|
|
expect(existsSync(sb.configPath)).toBe(false);
|
|
} finally {
|
|
sb.cleanup();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("template alignment", () => {
|
|
const tmpl = Object.values(TEMPLATES).map((p) => readFileSync(p, "utf-8")).join("\n");
|
|
|
|
it("uses the positional-params shape at all 3 init sites, never an unquoted flags var", () => {
|
|
expect(tmpl).not.toContain("$GBRAIN_EMBED_FLAGS");
|
|
expect(tmpl.match(/gbrain init --pglite --json "\$@"/g)?.length).toBe(3);
|
|
expect(tmpl.match(/set -- --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024/g)?.length).toBe(3);
|
|
expect(tmpl.match(/if \[ -n "\$\{VOYAGE_API_KEY:-\}" \]; then/g)?.length).toBe(3);
|
|
});
|
|
});
|