mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-11 15:07:26 +08:00
feat: bin/gstack-gbrain-sync — V1 unified sync verb (Lane B)
Orchestrates three storage tiers per plan §"Storage tiering": 1. Code (current repo) → gbrain import (Supabase or local PGLite) 2. Transcripts + curated memory → gstack-memory-ingest (typed put_page) 3. Curated artifacts to git → gstack-brain-sync (existing pipeline) Modes: --incremental (default, mtime fast-path) / --full (~25-35 min per ED2 honest budget) / --dry-run (preview, no writes). Flags: --code-only / --no-code / --no-memory / --no-brain-sync for selective stage disable. Each stage failure is non-fatal; subsequent stages still run. State at ~/.gstack/.gbrain-sync-state.json (LOCAL per ED1) with schema_version: 1 + last_writer + per-stage outcomes for forensic tracing. --watch daemon explicitly deferred to V1.5 P0 TODO per Codex F3 (reverses the "no daemon" invariant). Continuous sync rides the existing preamble-boundary hook only. 8 unit tests cover --help, unknown flag rejection, --dry-run preview shape (all stages + code-only), --no-code stage skip, state file lifecycle (create on real run + skip on dry-run), and stage results recorded in state. All passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
140
test/gstack-gbrain-sync.test.ts
Normal file
140
test/gstack-gbrain-sync.test.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 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 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user