Initial import from garrytan/gstack@026751e (main snapshot via local relay)
Some checks failed
Workflow Lint / actionlint (push) Has been cancelled
Build CI Image / build (push) Has been cancelled
Skill Docs Freshness / check-freshness (push) Has been cancelled
Periodic Evals / build-image (push) Has been cancelled
Periodic Evals / evals (map[file:test/codex-e2e.test.ts name:e2e-codex]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/gemini-e2e.test.ts name:e2e-gemini]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-design.test.ts name:e2e-design]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-plan.test.ts name:e2e-plan]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-bugs.test.ts name:e2e-qa-bugs]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-workflow.test.ts name:e2e-qa-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-review.test.ts name:e2e-review]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-workflow.test.ts name:e2e-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-routing-e2e.test.ts name:e2e-routing]) (push) Has been cancelled
Some checks failed
Workflow Lint / actionlint (push) Has been cancelled
Build CI Image / build (push) Has been cancelled
Skill Docs Freshness / check-freshness (push) Has been cancelled
Periodic Evals / build-image (push) Has been cancelled
Periodic Evals / evals (map[file:test/codex-e2e.test.ts name:e2e-codex]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/gemini-e2e.test.ts name:e2e-gemini]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-design.test.ts name:e2e-design]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-plan.test.ts name:e2e-plan]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-bugs.test.ts name:e2e-qa-bugs]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-workflow.test.ts name:e2e-qa-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-review.test.ts name:e2e-review]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-workflow.test.ts name:e2e-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-routing-e2e.test.ts name:e2e-routing]) (push) Has been cancelled
Source: https://github.com/garrytan/gstack/commit/026751e
This commit is contained in:
204
test/gbrain-init-rollback.test.ts
Normal file
204
test/gbrain-init-rollback.test.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* 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`,
|
||||
},
|
||||
});
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user