Files
gstack/test/gstack-gbrain-sync.test.ts
T
Garry Tan a0bfa001d3 v1.26.1.0 fix: gbrain-sync orchestrator resolves sibling via import.meta.dir (#1312)
* fix: gbrain-sync orchestrator resolves brain-sync sibling via import.meta.dir

Codex M9: runBrainSyncPush hardcoded ~/.claude/skills/gstack/bin/gstack-brain-sync,
so any host that wasn't Claude Code (Codex CLI, dev workspace) hit the existsSync
guard and silently skipped curated-artifact push. Replace with the sibling-resolution
pattern already in runMemoryIngest at line 193.

Regression test asserts the orchestrator no longer takes the lying-skip path when
HOME has no ~/.claude/skills/gstack tree.

* chore: bump plan-review preamble ratchet + regenerate ship goldens

The 33 KB preamble byte budget hadn't been bumped through v1.25.1.0
(AskUserQuestion recommendation pattern) and v1.26.0.0 (gbrain sync block).
plan-ceo-review SKILL.md sat at 33,018 bytes — 18 over the ratchet. Comment
in the test already authorizes this kind of intentional-growth bump.
Lifted to 34 KB which gives ~700 B of headroom for the next preamble change.

claude-ship-SKILL.md and factory-ship-SKILL.md golden fixtures regenerated
against the live /ship template — v1.25.1.0 added the canonical
"Recommendation: <action> because ..." line to the adversarial subagent
prompts but the goldens were never re-baked.

* chore: bump version and changelog (v1.26.1.0)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 20:12:28 -07:00

163 lines
6.3 KiB
TypeScript

/**
* Unit tests for bin/gstack-gbrain-sync.ts (Lane B).
*
* Tests CLI surface (modes + flags + help). Stage internals (gbrain import,
* memory ingest, brain-sync push) shell out to external binaries and are
* exercised by Lane F E2E tests; here we verify orchestration + dry-run
* preview + state file lifecycle + flag composition.
*/
import { describe, it, expect } from "bun:test";
import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync, mkdirSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { spawnSync } from "child_process";
const SCRIPT = join(import.meta.dir, "..", "bin", "gstack-gbrain-sync.ts");
function makeTestHome(): string {
return mkdtempSync(join(tmpdir(), "gstack-gbrain-sync-"));
}
function runScript(args: string[], env: Record<string, string> = {}): { stdout: string; stderr: string; exitCode: number } {
const result = spawnSync("bun", [SCRIPT, ...args], {
encoding: "utf-8",
timeout: 60000,
env: { ...process.env, ...env },
});
return {
stdout: result.stdout || "",
stderr: result.stderr || "",
exitCode: result.status ?? 1,
};
}
describe("gstack-gbrain-sync CLI", () => {
it("--help exits 0 with usage text", () => {
const r = runScript(["--help"]);
expect(r.exitCode).toBe(0);
expect(r.stderr).toContain("Usage: gstack-gbrain-sync");
expect(r.stderr).toContain("--incremental");
expect(r.stderr).toContain("--full");
expect(r.stderr).toContain("--dry-run");
});
it("rejects unknown flag", () => {
const r = runScript(["--bogus"]);
expect(r.exitCode).toBe(1);
expect(r.stderr).toContain("Unknown argument: --bogus");
});
it("--dry-run with --code-only reports the code import preview only", () => {
const home = makeTestHome();
const gstackHome = join(home, ".gstack");
mkdirSync(gstackHome, { recursive: true });
const r = runScript(["--dry-run", "--code-only", "--quiet"], { HOME: home, GSTACK_HOME: gstackHome });
expect(r.exitCode).toBe(0);
expect(r.stdout).toContain("would: gbrain import");
// memory + brain-sync stages should not appear
expect(r.stdout).not.toContain("gstack-memory-ingest --probe");
expect(r.stdout).not.toContain("gstack-brain-sync --discover-new");
rmSync(home, { recursive: true, force: true });
});
it("--dry-run with all stages shows previews for all three", () => {
const home = makeTestHome();
const gstackHome = join(home, ".gstack");
mkdirSync(gstackHome, { recursive: true });
const r = runScript(["--dry-run"], { HOME: home, GSTACK_HOME: gstackHome });
expect(r.exitCode).toBe(0);
expect(r.stdout).toContain("would: gbrain import");
expect(r.stdout).toContain("would: gstack-memory-ingest");
expect(r.stdout).toContain("would: gstack-brain-sync");
rmSync(home, { recursive: true, force: true });
});
it("--no-code skips the code import stage", () => {
const home = makeTestHome();
const gstackHome = join(home, ".gstack");
mkdirSync(gstackHome, { recursive: true });
const r = runScript(["--dry-run", "--no-code"], { HOME: home, GSTACK_HOME: gstackHome });
expect(r.exitCode).toBe(0);
expect(r.stdout).not.toContain("would: gbrain import");
expect(r.stdout).toContain("would: gstack-memory-ingest");
rmSync(home, { recursive: true, force: true });
});
it("writes a state file with schema_version: 1 after a non-dry run", () => {
const home = makeTestHome();
const gstackHome = join(home, ".gstack");
mkdirSync(gstackHome, { recursive: true });
// Run with all stages disabled to avoid actually invoking gbrain/memory-ingest
const r = runScript(["--incremental", "--no-code", "--no-memory", "--no-brain-sync", "--quiet"], {
HOME: home,
GSTACK_HOME: gstackHome,
});
expect(r.exitCode).toBe(0);
const statePath = join(gstackHome, ".gbrain-sync-state.json");
expect(existsSync(statePath)).toBe(true);
const state = JSON.parse(readFileSync(statePath, "utf-8"));
expect(state.schema_version).toBe(1);
expect(state.last_writer).toBe("gstack-gbrain-sync");
expect(typeof state.last_sync).toBe("string");
rmSync(home, { recursive: true, force: true });
});
it("does NOT write state file on --dry-run", () => {
const home = makeTestHome();
const gstackHome = join(home, ".gstack");
mkdirSync(gstackHome, { recursive: true });
const r = runScript(["--dry-run"], { HOME: home, GSTACK_HOME: gstackHome });
expect(r.exitCode).toBe(0);
const statePath = join(gstackHome, ".gbrain-sync-state.json");
expect(existsSync(statePath)).toBe(false);
rmSync(home, { recursive: true, force: true });
});
it("records stage results in state file", () => {
const home = makeTestHome();
const gstackHome = join(home, ".gstack");
mkdirSync(gstackHome, { recursive: true });
runScript(["--incremental", "--no-code", "--no-memory", "--no-brain-sync", "--quiet"], {
HOME: home,
GSTACK_HOME: gstackHome,
});
const state = JSON.parse(readFileSync(join(gstackHome, ".gbrain-sync-state.json"), "utf-8"));
expect(Array.isArray(state.last_stages)).toBe(true);
// With all stages disabled, last_stages is empty
expect(state.last_stages.length).toBe(0);
rmSync(home, { recursive: true, force: true });
});
it("brain-sync stage resolves the sibling binary, not a HOME-rooted path", () => {
// Regression for Codex M9: pre-fix the orchestrator looked up
// ~/.claude/skills/gstack/bin/gstack-brain-sync, which silently no-op'd
// on Codex installs and dev workspaces with the misleading summary
// "skipped (gstack-brain-sync not installed)". Post-fix it resolves
// a sibling via import.meta.dir and actually invokes the script.
const home = makeTestHome();
const gstackHome = join(home, ".gstack");
mkdirSync(gstackHome, { recursive: true });
const r = runScript(
["--incremental", "--no-code", "--no-memory", "--quiet"],
{ HOME: home, GSTACK_HOME: gstackHome },
);
// Don't assert exit code (sibling spawn may legitimately error in a
// sandboxed test). Assert only that we did NOT take the lying-skip path.
const combined = r.stdout + r.stderr;
expect(combined).not.toContain("skipped (gstack-brain-sync not installed)");
rmSync(home, { recursive: true, force: true });
});
});