test: first-Linux-run environment fixes — bun-only PATH shim, claude gate, darwin-scoped pdf gates

Three environmental assumptions the Linux lane exposed:
(1) gbrain-detect's deterministic SAFE_PATH lacked the bun runtime, so
every env-shebang spawn exited 127 on CI; a scratch dir holding ONLY a
bun symlink joins the PATH (appending bun's real dir would leak its
siblings — dev boxes keep gbrain there too).
(2) host-config's 'detect finds claude' assumed a claude binary; the
secretless lane deliberately has none — gated on Bun.which.
(3) The four make-pdf render gates hard-required prerequisites on ANY
CI, but the make-pdf gate workflow is macOS-only by decision and the
Linux lane doesn't build dist/pdf — hard-require scoped to darwin.
This commit is contained in:
Garry Tan 2026-08-15 17:40:55 -07:00
parent dedcd3f4f0
commit 7508c92e3a
No known key found for this signature in database
GPG Key ID: C1F69E85C74EFE1D
6 changed files with 29 additions and 6 deletions

View File

@ -164,7 +164,10 @@ describe("diagram render gate", () => {
if (!avail.ok) {
test("diagram gate prerequisites are present (hard-required in CI)", () => {
if (process.env.CI) {
// Hard-require only where the binary is expected: the make-pdf gate
// workflow is macOS-only (path-filtered) and builds dist/pdf first.
// The Linux free lane deliberately doesn't build it — warn-skip there.
if (process.env.CI && process.platform === 'darwin') {
throw new Error(`diagram gate prerequisites missing in CI: ${avail.reason}`);
}
console.warn(`[skip] ${avail.reason}`);

View File

@ -188,7 +188,10 @@ describe("emoji render gate", () => {
// In CI, missing prerequisites are a hard failure — a silent skip would let
// the Linux tofu regression ship behind a green build. Locally, just warn.
test("emoji gate prerequisites are present (hard-required in CI)", () => {
if (process.env.CI) {
// Hard-require only where the binary is expected: the make-pdf gate
// workflow is macOS-only (path-filtered) and builds dist/pdf first.
// The Linux free lane deliberately doesn't build it — warn-skip there.
if (process.env.CI && process.platform === 'darwin') {
throw new Error(`emoji gate prerequisites missing in CI: ${avail.reason}`);
}
console.warn(`[skip] ${avail.reason}`);

View File

@ -122,7 +122,10 @@ describe("output format gate", () => {
if (!avail.ok) {
test("format gate prerequisites are present (hard-required in CI)", () => {
if (process.env.CI) {
// Hard-require only where the binary is expected: the make-pdf gate
// workflow is macOS-only (path-filtered) and builds dist/pdf first.
// The Linux free lane deliberately doesn't build it — warn-skip there.
if (process.env.CI && process.platform === 'darwin') {
throw new Error(`format gate prerequisites missing in CI: ${avail.reason}`);
}
console.warn(`[skip] ${avail.reason}`);

View File

@ -127,7 +127,10 @@ describe("landscape promotion gate", () => {
if (!avail.ok) {
test("landscape gate prerequisites are present (hard-required in CI)", () => {
if (process.env.CI) {
// Hard-require only where the binary is expected: the make-pdf gate
// workflow is macOS-only (path-filtered) and builds dist/pdf first.
// The Linux free lane deliberately doesn't build it — warn-skip there.
if (process.env.CI && process.platform === 'darwin') {
throw new Error(`landscape gate prerequisites missing in CI: ${avail.reason}`);
}
console.warn(`[skip] ${avail.reason}`);

View File

@ -25,7 +25,16 @@ const INSTALL = path.join(ROOT, 'bin', 'gstack-gbrain-install');
// dirs — this keeps `gbrain` out of PATH deterministically across dev machines
// while still finding jq, git, curl, sed, cat, etc. Each test can prepend a
// fake-gbrain dir when it wants to simulate presence.
const SAFE_PATH = '/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:/usr/local/bin';
// Deterministic PATH for spawned children — but it must still contain the
// bun runtime itself: the bin's `#!/usr/bin/env -S bun run` shebang resolves
// bun from PATH, and CI installs bun outside the standard dirs (~/.bun/bin),
// which made every spawn exit 127 on the first Linux run. Appending bun's
// REAL dir would leak its siblings (a dev box keeps gbrain in ~/.bun/bin
// too, breaking every "no gbrain on PATH" case) — so a scratch dir holds a
// symlink to bun and nothing else.
const BUN_ONLY_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'bun-only-'));
fs.symlinkSync(process.execPath, path.join(BUN_ONLY_DIR, 'bun'));
const SAFE_PATH = `/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:/usr/local/bin:${BUN_ONLY_DIR}`;
let tmpHome: string;
let tmpHomeReal: string;

View File

@ -401,7 +401,9 @@ describe('host-config-export.ts CLI', () => {
expect(exitCode).toBe(1);
});
test('detect finds claude (since we are running in claude)', () => {
// Gated: the secretless free-tests CI lane deliberately installs no claude
// CLI, so "we are running in claude" is false there by design.
test.skipIf(!Bun.which('claude'))('detect finds claude (since we are running in claude)', () => {
const { stdout, exitCode } = run('detect');
expect(exitCode).toBe(0);
// claude binary should be on PATH in this environment