mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-02 17:40:02 +02:00
test: replace product tests that fake the product with real-boundary tests (F)
- 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.
This commit is contained in:
1 parent
6dc624eda6
commit
5ec930d569
33 files changed
+638
-1830
No files matched your search
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"_schema_version": 1,
|
||||
"_app_build_id": "uninitialized",
|
||||
"_accessor_hash": "uninitialized",
|
||||
"keys": {}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 96 KiB |
@@ -1,205 +0,0 @@
|
||||
/**
|
||||
* Tests the .bak-rollback contract used by /setup-gbrain Step 1.5 (broken-db
|
||||
* repair) and Step 4.5 (Path 4 opt-in to local PGLite), per plan D7.
|
||||
*
|
||||
* These code paths live in the skill TEMPLATE, not in a TypeScript helper —
|
||||
* the skill follows AI-readable instructions. The instructions specify the
|
||||
* exact sequence:
|
||||
*
|
||||
* 1. mv ~/.gbrain/config.json ~/.gbrain/config.json.gstack-bak-$(date +%s)
|
||||
* 2. gbrain init --pglite --json
|
||||
* 3. on non-zero exit: mv .bak back; surface error
|
||||
*
|
||||
* This test extracts that sequence as a shell function and verifies the
|
||||
* rollback contract using a fake `gbrain` binary that fails on init. It's
|
||||
* the test that proves "what the skill template says, when followed
|
||||
* mechanically, actually preserves the user's broken config on failure."
|
||||
*
|
||||
* Per plan codex #10 / explicit rollback scope: we only promise to restore
|
||||
* the config.json file. The PGLite directory at ~/.gbrain/pglite/ may end
|
||||
* up in a partial state — that's documented to the user, not auto-cleaned.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import {
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
writeFileSync,
|
||||
readFileSync,
|
||||
existsSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
chmodSync,
|
||||
} from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { spawnSync } from "child_process";
|
||||
|
||||
interface RollbackEnv {
|
||||
tmp: string;
|
||||
home: string;
|
||||
configPath: string;
|
||||
bindir: string;
|
||||
cleanup: () => void;
|
||||
}
|
||||
|
||||
function makeEnv(opts: { gbrainBehavior: "succeeds" | "fails" }): RollbackEnv {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "gbrain-init-rollback-"));
|
||||
const home = join(tmp, "home");
|
||||
const gbrainDir = join(home, ".gbrain");
|
||||
const configPath = join(gbrainDir, "config.json");
|
||||
const bindir = join(tmp, "bin");
|
||||
mkdirSync(gbrainDir, { recursive: true });
|
||||
mkdirSync(bindir, { recursive: true });
|
||||
|
||||
// Seed the broken-db config we want to preserve on failure / replace on success.
|
||||
writeFileSync(
|
||||
configPath,
|
||||
JSON.stringify({
|
||||
engine: "postgres",
|
||||
database_url: "postgresql://stale:test@localhost:5435/gbrain_test",
|
||||
}),
|
||||
);
|
||||
|
||||
const exitCode = opts.gbrainBehavior === "fails" ? 1 : 0;
|
||||
const onInitSuccess =
|
||||
opts.gbrainBehavior === "succeeds"
|
||||
? `cat > "${configPath}" <<JSON
|
||||
{"engine":"pglite","database_url":"pglite://${gbrainDir}/pglite"}
|
||||
JSON
|
||||
mkdir -p "${gbrainDir}/pglite"
|
||||
echo '{"status":"ok"}'`
|
||||
: `echo "Error: disk full" >&2`;
|
||||
const fake = `#!/bin/sh
|
||||
if [ "$1" = "--version" ]; then echo "gbrain 0.33.1.0"; exit 0; fi
|
||||
if [ "$1 $2" = "init --pglite" ]; then
|
||||
${onInitSuccess}
|
||||
exit ${exitCode}
|
||||
fi
|
||||
exit 0
|
||||
`;
|
||||
writeFileSync(join(bindir, "gbrain"), fake);
|
||||
chmodSync(join(bindir, "gbrain"), 0o755);
|
||||
|
||||
return {
|
||||
tmp,
|
||||
home,
|
||||
configPath,
|
||||
bindir,
|
||||
cleanup: () => rmSync(tmp, { recursive: true, force: true }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verbatim reimplementation of the skill template's Step 1.5 / 4.5 rollback
|
||||
* sequence. The skill instructs the model to execute this bash; we execute
|
||||
* the same bash here in a sandboxed environment and assert the contract.
|
||||
*
|
||||
* If gbrain templates rewrite this sequence, this test should fail until
|
||||
* the shell here is updated too. That's the point — keep the test and the
|
||||
* skill template aligned.
|
||||
*/
|
||||
function runRollbackSequence(env: RollbackEnv): { exitCode: number; stderr: string } {
|
||||
const script = `
|
||||
set -u
|
||||
BACKUP="${env.configPath}.gstack-bak-$(date +%s)-$$"
|
||||
if [ -f "${env.configPath}" ]; then
|
||||
mv "${env.configPath}" "$BACKUP"
|
||||
fi
|
||||
if ! gbrain init --pglite --json; then
|
||||
if [ -n "\${BACKUP:-}" ] && [ -f "$BACKUP" ]; then
|
||||
mv "$BACKUP" "${env.configPath}"
|
||||
fi
|
||||
echo "gbrain init failed. Existing config (if any) was restored." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "ok"
|
||||
`;
|
||||
const result = spawnSync("bash", ["-c", script], {
|
||||
encoding: "utf-8",
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: env.home,
|
||||
PATH: `${env.bindir}:/usr/bin:/bin`,
|
||||
},
|
||||
timeout: 30_000,
|
||||
});
|
||||
return {
|
||||
exitCode: result.status ?? 1,
|
||||
stderr: result.stderr || "",
|
||||
};
|
||||
}
|
||||
|
||||
describe("Step 1.5 / 4.5 .bak-rollback contract (plan D7)", () => {
|
||||
it("FAILURE PATH: when `gbrain init` fails, broken config is restored to original path", () => {
|
||||
const env = makeEnv({ gbrainBehavior: "fails" });
|
||||
try {
|
||||
const originalContent = readFileSync(env.configPath, "utf-8");
|
||||
|
||||
const r = runRollbackSequence(env);
|
||||
|
||||
expect(r.exitCode).toBe(1);
|
||||
expect(r.stderr).toContain("restored");
|
||||
|
||||
// Original config is back at the original path.
|
||||
expect(existsSync(env.configPath)).toBe(true);
|
||||
const after = readFileSync(env.configPath, "utf-8");
|
||||
expect(after).toBe(originalContent);
|
||||
|
||||
// No leftover .bak — it was renamed back to the original path.
|
||||
const baks = readdirSync(join(env.home, ".gbrain")).filter((f) =>
|
||||
f.includes(".gstack-bak-"),
|
||||
);
|
||||
expect(baks).toEqual([]);
|
||||
} finally {
|
||||
env.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("SUCCESS PATH: when `gbrain init` succeeds, the .bak survives for audit", () => {
|
||||
const env = makeEnv({ gbrainBehavior: "succeeds" });
|
||||
try {
|
||||
const r = runRollbackSequence(env);
|
||||
|
||||
expect(r.exitCode).toBe(0);
|
||||
|
||||
// New config is in place (fake gbrain wrote pglite engine).
|
||||
expect(existsSync(env.configPath)).toBe(true);
|
||||
const after = JSON.parse(readFileSync(env.configPath, "utf-8")) as {
|
||||
engine: string;
|
||||
};
|
||||
expect(after.engine).toBe("pglite");
|
||||
|
||||
// The .bak survives — user can audit before deleting.
|
||||
const baks = readdirSync(join(env.home, ".gbrain")).filter((f) =>
|
||||
f.includes(".gstack-bak-"),
|
||||
);
|
||||
expect(baks.length).toBe(1);
|
||||
} finally {
|
||||
env.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("PGLite directory partial state is NOT auto-cleaned (codex #10 scoped rollback)", () => {
|
||||
// Per the rollback scope: we only restore config.json. If gbrain init
|
||||
// started writing a PGLite dir before failing, we leave it alone and
|
||||
// surface the cleanup hint to the user.
|
||||
const env = makeEnv({ gbrainBehavior: "fails" });
|
||||
try {
|
||||
// Simulate gbrain having created a partial PGLite dir before failure
|
||||
const partial = join(env.home, ".gbrain", "pglite");
|
||||
mkdirSync(partial, { recursive: true });
|
||||
writeFileSync(join(partial, "partial-write.tmp"), "");
|
||||
|
||||
const r = runRollbackSequence(env);
|
||||
|
||||
expect(r.exitCode).toBe(1);
|
||||
// The partial dir is left in place — user gets the hint, we don't
|
||||
// assume responsibility for cleanup.
|
||||
expect(existsSync(partial)).toBe(true);
|
||||
expect(existsSync(join(partial, "partial-write.tmp"))).toBe(true);
|
||||
} finally {
|
||||
env.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,21 +1,20 @@
|
||||
/**
|
||||
* Tests the voyage-code-3 default contract in setup-gbrain's PGLite init
|
||||
* sequences. The contract lives in the skill TEMPLATE (.tmpl), not in a TS
|
||||
* helper — the skill follows AI-readable instructions.
|
||||
* setup-gbrain's local PGLite init sequences, executed from the TEMPLATE.
|
||||
*
|
||||
* Contract (asserted here):
|
||||
* 1. When VOYAGE_API_KEY is set, gstack's PGLite init passes
|
||||
* --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024
|
||||
* 2. When VOYAGE_API_KEY is unset, those flags are omitted (gbrain's
|
||||
* auto-selected provider chain takes over)
|
||||
* 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.
|
||||
*
|
||||
* Why a separate file from gbrain-init-rollback.test.ts: that file owns the
|
||||
* .bak-rollback contract (Step 1.5 / 4.5 plan D7). This file owns the
|
||||
* embedding-model selection contract. Both extract bash from the skill
|
||||
* template and execute it against a fake gbrain.
|
||||
*
|
||||
* The fake gbrain records argv to a sentinel file so the test can assert
|
||||
* exact flags. No Voyage API calls are made.
|
||||
* 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";
|
||||
@@ -24,6 +23,7 @@ import {
|
||||
mkdirSync,
|
||||
writeFileSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
existsSync,
|
||||
rmSync,
|
||||
chmodSync,
|
||||
@@ -32,230 +32,188 @@ import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { spawnSync } from "child_process";
|
||||
|
||||
interface FakeEnv {
|
||||
tmp: string;
|
||||
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 makeFakeEnv(): FakeEnv {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "gbrain-voyage-init-"));
|
||||
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(join(home, ".gbrain"), { recursive: true });
|
||||
mkdirSync(gbrainDir, { recursive: true });
|
||||
mkdirSync(bindir, { recursive: true });
|
||||
|
||||
// Fake gbrain logs every argv invocation to argvLog (one line per call),
|
||||
// succeeds on init (writes a sentinel pglite config), and returns canned
|
||||
// output for --version. Nothing else is needed for the shape test.
|
||||
const fake = `#!/bin/sh
|
||||
echo "$@" >> "${argvLog}"
|
||||
echo "$#" >> "${argvLog}.argc"
|
||||
case "$1" in
|
||||
--version)
|
||||
echo "gbrain 0.37.1.0"
|
||||
exit 0
|
||||
;;
|
||||
init)
|
||||
cat > "${home}/.gbrain/config.json" <<JSON
|
||||
{"engine":"pglite","database_path":"${home}/.gbrain/brain.pglite"}
|
||||
JSON
|
||||
echo '{"status":"success","engine":"pglite","pages":0}'
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
`;
|
||||
writeFileSync(join(bindir, "gbrain"), fake);
|
||||
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 {
|
||||
tmp,
|
||||
home,
|
||||
bindir,
|
||||
argvLog,
|
||||
cleanup: () => rmSync(tmp, { recursive: true, force: true }),
|
||||
};
|
||||
return { home, bindir, configPath, argvLog, cleanup: () => rmSync(tmp, { recursive: true, force: true }) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Verbatim reimplementation of the skill template's voyage-code-3
|
||||
* conditional. The template (setup-gbrain/sections/brain-init.md.tmpl Path 3, Step 1.5
|
||||
* inside the rollback wrapper, Step 4.5 Path 4 Yes branch) instructs the
|
||||
* model to execute this bash; we execute the same bash here and assert the
|
||||
* argv passed to gbrain matches the contract.
|
||||
*
|
||||
* If the template changes the flag set or the env-var name, this test
|
||||
* should fail until the shell here is updated too — by design.
|
||||
*/
|
||||
function runInitWithVoyageGate(
|
||||
env: FakeEnv,
|
||||
voyageKey: string | undefined,
|
||||
shell: "bash" | "zsh" = "bash",
|
||||
): string[] {
|
||||
// The template's #1798 shape: flags ride the positional params, because an
|
||||
// unquoted $VAR does NOT word-split under zsh — the whole flag string
|
||||
// arrived as ONE argv word and gbrain silently fell back to its default
|
||||
// embedding model.
|
||||
const script = `
|
||||
set -u
|
||||
set --
|
||||
if [ -n "\${VOYAGE_API_KEY:-}" ]; then
|
||||
set -- --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024
|
||||
fi
|
||||
gbrain init --pglite --json "$@"
|
||||
`;
|
||||
const baseEnv: Record<string, string> = {
|
||||
...process.env,
|
||||
HOME: env.home,
|
||||
PATH: `${env.bindir}:/usr/bin:/bin`,
|
||||
};
|
||||
if (voyageKey === undefined) {
|
||||
delete baseEnv.VOYAGE_API_KEY;
|
||||
} else {
|
||||
baseEnv.VOYAGE_API_KEY = voyageKey;
|
||||
}
|
||||
const result = spawnSync(shell, ["-c", script], {
|
||||
encoding: "utf-8",
|
||||
env: baseEnv,
|
||||
timeout: 30_000,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`init script exited ${result.status}: ${result.stderr}`);
|
||||
}
|
||||
return readFileSync(env.argvLog, "utf-8").trim().split("\n");
|
||||
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 lastArgc(env: FakeEnv): number {
|
||||
const lines = readFileSync(`${env.argvLog}.argc`, "utf-8").trim().split("\n");
|
||||
return parseInt(lines[lines.length - 1], 10);
|
||||
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("voyage-code-3 default for gstack-driven PGLite init", () => {
|
||||
it("passes voyage-code-3 flags when VOYAGE_API_KEY is set", () => {
|
||||
const env = makeFakeEnv();
|
||||
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 calls = runInitWithVoyageGate(env, "vk_test_set");
|
||||
expect(calls.length).toBe(1);
|
||||
const argv = calls[0];
|
||||
expect(argv).toContain("init --pglite --json");
|
||||
expect(argv).toContain("--embedding-model voyage:voyage-code-3");
|
||||
expect(argv).toContain("--embedding-dimensions 1024");
|
||||
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 {
|
||||
env.cleanup();
|
||||
path4.cleanup();
|
||||
remediation.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("omits voyage flags when VOYAGE_API_KEY is unset", () => {
|
||||
const env = makeFakeEnv();
|
||||
it("Path 4 with no existing config: failed init creates no backup and no config", () => {
|
||||
const sb = makeSandbox({ initFails: true });
|
||||
try {
|
||||
const calls = runInitWithVoyageGate(env, undefined);
|
||||
expect(calls.length).toBe(1);
|
||||
const argv = calls[0];
|
||||
expect(argv).toContain("init --pglite --json");
|
||||
expect(argv).not.toContain("voyage");
|
||||
expect(argv).not.toContain("--embedding-model");
|
||||
expect(argv).not.toContain("--embedding-dimensions");
|
||||
runBlock(sb, PATH4_BLOCK!);
|
||||
expect(backups(sb)).toEqual([]);
|
||||
expect(existsSync(sb.configPath)).toBe(false);
|
||||
} finally {
|
||||
env.cleanup();
|
||||
sb.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("zsh: flags arrive as SEPARATE argv words (#1798 — the shell that broke)", () => {
|
||||
if (!HAVE_ZSH) return; // zsh ships on macOS; skip quietly elsewhere
|
||||
const env = makeFakeEnv();
|
||||
try {
|
||||
const calls = runInitWithVoyageGate(env, "vk_test_set", "zsh");
|
||||
expect(calls.length).toBe(1);
|
||||
expect(calls[0]).toContain("--embedding-model voyage:voyage-code-3");
|
||||
// init --pglite --json + 4 flag words = 7 argv entries. The pre-#1798
|
||||
// unquoted-var shape produced 4 under zsh (the whole flag string as one
|
||||
// word), and gbrain silently fell back to its default embedding model.
|
||||
expect(lastArgc(env)).toBe(7);
|
||||
} finally {
|
||||
env.cleanup();
|
||||
}
|
||||
});
|
||||
describe("template alignment", () => {
|
||||
const tmpl = Object.values(TEMPLATES).map((p) => readFileSync(p, "utf-8")).join("\n");
|
||||
|
||||
it("demonstrates the #1798 collision: an unquoted flags var is ONE word under zsh", () => {
|
||||
if (!HAVE_ZSH) return;
|
||||
const env = makeFakeEnv();
|
||||
try {
|
||||
const brokenShape = `
|
||||
set -u
|
||||
GBRAIN_EMBED_FLAGS="--embedding-model voyage:voyage-code-3 --embedding-dimensions 1024"
|
||||
gbrain init --pglite --json $GBRAIN_EMBED_FLAGS
|
||||
`;
|
||||
const result = spawnSync("zsh", ["-c", brokenShape], {
|
||||
encoding: "utf-8",
|
||||
env: { ...process.env, HOME: env.home, PATH: `${env.bindir}:/usr/bin:/bin` },
|
||||
timeout: 30_000,
|
||||
});
|
||||
expect(result.status).toBe(0);
|
||||
expect(lastArgc(env)).toBe(4); // init, --pglite, --json, "<entire flag string>"
|
||||
} finally {
|
||||
env.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("template uses the positional-params shape, not an unquoted flags var", () => {
|
||||
// Carved (token-reduction Phase 4): count across the tmpl UNION — one
|
||||
// PGLite init site stays in the skeleton, the Path-3/4 sites live in the
|
||||
// brain-init section.
|
||||
const tmpl = readFileSync(
|
||||
join(import.meta.dir, "..", "setup-gbrain", "SKILL.md.tmpl"),
|
||||
"utf-8",
|
||||
) + readFileSync(
|
||||
join(import.meta.dir, "..", "setup-gbrain", "sections", "brain-init.md.tmpl"),
|
||||
"utf-8",
|
||||
) + readFileSync(
|
||||
join(import.meta.dir, "..", "setup-gbrain", "sections", "engine-remediation.md.tmpl"),
|
||||
"utf-8",
|
||||
);
|
||||
it("uses the positional-params shape at all 3 init sites, never an unquoted flags var", () => {
|
||||
expect(tmpl).not.toContain("$GBRAIN_EMBED_FLAGS");
|
||||
const sites = tmpl.match(/gbrain init --pglite --json "\$@"/g) || [];
|
||||
expect(sites.length).toBe(3);
|
||||
const setSites = tmpl.match(/set -- --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024/g) || [];
|
||||
expect(setSites.length).toBe(3);
|
||||
});
|
||||
|
||||
it("treats empty-string VOYAGE_API_KEY the same as unset (no false positive)", () => {
|
||||
const env = makeFakeEnv();
|
||||
try {
|
||||
const calls = runInitWithVoyageGate(env, "");
|
||||
expect(calls.length).toBe(1);
|
||||
expect(calls[0]).not.toContain("voyage");
|
||||
} finally {
|
||||
env.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("template alignment: the .tmpl actually contains the voyage gate", () => {
|
||||
// Belt-and-suspenders: if someone edits the template and drops the
|
||||
// VOYAGE_API_KEY conditional without updating the test above, this catches
|
||||
// it. The shell snippet under test must literally appear in the .tmpl.
|
||||
// Carved union — see comment above.
|
||||
const tmpl = readFileSync(join(import.meta.dir, "..", "setup-gbrain", "SKILL.md.tmpl"), "utf-8")
|
||||
+ readFileSync(join(import.meta.dir, "..", "setup-gbrain", "sections", "brain-init.md.tmpl"), "utf-8")
|
||||
+ readFileSync(join(import.meta.dir, "..", "setup-gbrain", "sections", "engine-remediation.md.tmpl"), "utf-8");
|
||||
|
||||
it("setup-gbrain template gates the embedding-model flag on VOYAGE_API_KEY", () => {
|
||||
// Should appear at least once (currently 3 init sites use the same gate).
|
||||
expect(tmpl).toContain('if [ -n "${VOYAGE_API_KEY:-}" ]; then');
|
||||
expect(tmpl).toContain("--embedding-model voyage:voyage-code-3");
|
||||
expect(tmpl).toContain("--embedding-dimensions 1024");
|
||||
});
|
||||
|
||||
it("setup-gbrain template uses the conditional gate at all 3 PGLite init sites", () => {
|
||||
// Count the gate occurrences. If a future edit adds/removes a PGLite
|
||||
// init site, update this expectation deliberately.
|
||||
const matches = tmpl.match(/if \[ -n "\$\{VOYAGE_API_KEY:-\}" \]; then/g);
|
||||
expect(matches?.length).toBe(3);
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -196,17 +196,6 @@ describe('gen-skill-docs', () => {
|
||||
expect(commands).toEqual(sorted);
|
||||
});
|
||||
|
||||
test('generated header is present in SKILL.md', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('AUTO-GENERATED from SKILL.md.tmpl');
|
||||
expect(content).toContain('Regenerate: bun run gen:skill-docs');
|
||||
});
|
||||
|
||||
test('generated header is present in browse/SKILL.md', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'browse', 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('AUTO-GENERATED from SKILL.md.tmpl');
|
||||
});
|
||||
|
||||
test('snapshot flags section contains all flags', () => {
|
||||
const content = readSkillUnion('browse');
|
||||
for (const flag of SNAPSHOT_FLAGS) {
|
||||
@@ -329,7 +318,7 @@ describe('gen-skill-docs', () => {
|
||||
test('no generated SKILL.md contains unresolved placeholders', () => {
|
||||
for (const skill of CLAUDE_GENERATED_SKILLS) {
|
||||
const content = fs.readFileSync(path.join(ROOT, skill.dir, 'SKILL.md'), 'utf-8');
|
||||
const unresolved = content.match(/\{\{[A-Z_]+\}\}/g);
|
||||
const unresolved = content.match(/\{\{\w+\}\}/g);
|
||||
expect(unresolved).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const ROOT = join(import.meta.dir, '..');
|
||||
const PRE_FIXTURE = join(ROOT, 'test/fixtures/ios-fix/ios-qa-swiftui-tap-pre.json');
|
||||
const PRE_SCREENSHOT = join(ROOT, 'test/fixtures/ios-fix/ios-qa-swiftui-tap-pre.png');
|
||||
|
||||
describe('ios-fix regression fixture — SwiftUI taps reported success without acting', () => {
|
||||
test('preserves the pre-fix state and physical-device screenshot', () => {
|
||||
const state = JSON.parse(readFileSync(PRE_FIXTURE, 'utf8'));
|
||||
expect(state).toEqual({
|
||||
_schema_version: 1,
|
||||
_app_build_id: 'uninitialized',
|
||||
_accessor_hash: 'uninitialized',
|
||||
keys: {},
|
||||
});
|
||||
|
||||
const png = readFileSync(PRE_SCREENSHOT);
|
||||
expect([...png.subarray(0, 8)]).toEqual([137, 80, 78, 71, 13, 10, 26, 10]);
|
||||
expect(png.readUInt32BE(16)).toBe(1206);
|
||||
expect(png.readUInt32BE(20)).toBe(2622);
|
||||
});
|
||||
|
||||
test('keeps the physical-device deploy/tap test opt-in and executable', () => {
|
||||
const deviceTest = readFileSync(join(ROOT, 'test/skill-e2e-ios-device.test.ts'), 'utf8');
|
||||
expect(deviceTest).toContain("process.env.GSTACK_IOS_DEVICE_DEPLOY === '1'");
|
||||
expect(deviceTest).toContain("'primary-button'");
|
||||
expect(deviceTest).toContain("'/tap'");
|
||||
expect(deviceTest).not.toContain("test.skip('TODO(deploy)");
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,7 @@
|
||||
* from a healthy one, and the memory corpus quietly stops growing.
|
||||
*
|
||||
* Two tests here:
|
||||
* 1. Source pin (same shape as memory-ingest-no-put_page.test.ts): the flag
|
||||
* 1. Source pin: the flag
|
||||
* is present in active code, so removing it trips the build.
|
||||
* 2. Behavioural proof of the underlying collision, using git's own ignore
|
||||
* machinery. No gbrain and no network required.
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* Regression pin for #1346: gstack-memory-ingest must never call the
|
||||
* `gbrain put_page` subcommand (renamed to `put` in gbrain v0.18+).
|
||||
*
|
||||
* The original bug shipped a literal `"put_page"` in execFileSync args,
|
||||
* crashing every transcript ingest against modern gbrain. The fix migrated
|
||||
* the per-file path to `gbrain put <slug>` and later to the batch
|
||||
* `gbrain import <dir>` runner. This test pins both surfaces: source code
|
||||
* must not contain `put_page` outside comments, and any future contributor
|
||||
* adding it back trips the build.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import { readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
const SOURCE_PATH = join(import.meta.dir, "..", "bin", "gstack-memory-ingest.ts");
|
||||
|
||||
/**
|
||||
* Strip line comments (`// ...`) and block comments (`/* ... */`) from TS
|
||||
* source so the regression check only inspects executable code. Naive but
|
||||
* sufficient — we don't need full TS parsing, just to ignore the
|
||||
* documentation/changelog mentions of the old subcommand name.
|
||||
*
|
||||
* Order matters: strip block comments first (they may span multiple lines
|
||||
* and contain `//`), then line comments. String-literal awareness is
|
||||
* intentionally skipped — if anyone writes "put_page" inside an active
|
||||
* string they want the test to fail.
|
||||
*/
|
||||
function stripComments(src: string): string {
|
||||
// Block comments — non-greedy across newlines.
|
||||
const noBlock = src.replace(/\/\*[\s\S]*?\*\//g, "");
|
||||
// Line comments — strip from `//` to end of line.
|
||||
return noBlock.replace(/\/\/[^\n]*/g, "");
|
||||
}
|
||||
|
||||
describe("gstack-memory-ingest — no put_page in active code (regression for #1346)", () => {
|
||||
it("source file does not call the renamed gbrain put_page subcommand", () => {
|
||||
const src = readFileSync(SOURCE_PATH, "utf-8");
|
||||
const stripped = stripComments(src);
|
||||
expect(stripped).not.toContain("put_page");
|
||||
});
|
||||
|
||||
it("source file does call the canonical gbrain put subcommand or gbrain import", () => {
|
||||
// Sanity check that the file actually uses one of the supported page-write
|
||||
// verbs — guards against accidentally removing all gbrain calls and having
|
||||
// the negative test above pass for the wrong reason.
|
||||
const src = readFileSync(SOURCE_PATH, "utf-8");
|
||||
const stripped = stripComments(src);
|
||||
const callsPut = /\bgbrain\s+put\b/.test(stripped) || /["']put["']/.test(stripped);
|
||||
const callsImport = /\bimport\b/.test(stripped); // `gbrain import` runner
|
||||
expect(callsPut || callsImport).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -67,8 +67,4 @@ describe('post-rename doc-regen regression (codex Finding #12)', () => {
|
||||
}
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
|
||||
test('top-level SKILL.md exists and is regenerated', () => {
|
||||
expect(fs.existsSync(path.join(ROOT, 'SKILL.md'))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -15,9 +15,7 @@
|
||||
// token cost. Same rationale as test/setup-gbrain-path4-structure.test.ts.
|
||||
// - The correct invocation form and the stale one differ only by
|
||||
// `bun run ` + `.ts`, right next to each other in the same files —
|
||||
// exactly the kind of drift a cheap structural check exists to catch,
|
||||
// matching this repo's convention (e.g. test/memory-ingest-no-put_page.test.ts
|
||||
// pinning fix #1346).
|
||||
// exactly the kind of drift a cheap structural check exists to catch.
|
||||
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
|
||||
@@ -316,25 +316,6 @@ describe('Usage string consistency', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Generated SKILL.md freshness', () => {
|
||||
test('no unresolved {{placeholders}} in generated SKILL.md', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
||||
const unresolved = content.match(/\{\{\w+\}\}/g);
|
||||
expect(unresolved).toBeNull();
|
||||
});
|
||||
|
||||
test('no unresolved {{placeholders}} in generated browse/SKILL.md', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'browse', 'SKILL.md'), 'utf-8');
|
||||
const unresolved = content.match(/\{\{\w+\}\}/g);
|
||||
expect(unresolved).toBeNull();
|
||||
});
|
||||
|
||||
test('generated SKILL.md has AUTO-GENERATED header', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('AUTO-GENERATED');
|
||||
});
|
||||
});
|
||||
|
||||
// --- Update check preamble validation ---
|
||||
|
||||
describe('Update check preamble', () => {
|
||||
|
||||
@@ -128,14 +128,6 @@ describe('#1671 invariant: no production code writes to builder-profile.jsonl',
|
||||
expect(offending).toEqual([]);
|
||||
});
|
||||
|
||||
test('office-hours/SKILL.md uses --log-session, not raw echo append', () => {
|
||||
const skill = fs.readFileSync(path.join(ROOT, 'office-hours/SKILL.md'), 'utf-8');
|
||||
// The two known writer call-sites must use the new subcommand.
|
||||
expect(skill).toContain('gstack-developer-profile --log-session');
|
||||
// And must NOT contain the old echo-append pattern.
|
||||
expect(skill).not.toMatch(/echo\s+['"][^'"]*['"]?\s*>>\s*["'][^"']*builder-profile\.jsonl/);
|
||||
});
|
||||
|
||||
test('office-hours/SKILL.md.tmpl uses --log-session, not raw echo append', () => {
|
||||
const tmpl = fs.readFileSync(path.join(ROOT, 'office-hours/SKILL.md.tmpl'), 'utf-8');
|
||||
expect(tmpl).toContain('gstack-developer-profile --log-session');
|
||||
|
||||
Reference in new issue
Block a user