diff --git a/browse/test/gstack-config.test.ts b/browse/test/gstack-config.test.ts index e342a50b7..670c1b092 100644 --- a/browse/test/gstack-config.test.ts +++ b/browse/test/gstack-config.test.ts @@ -15,12 +15,20 @@ const SCRIPT = join(import.meta.dir, '..', '..', 'bin', 'gstack-config'); let stateDir: string; function run(args: string[] = [], extraEnv: Record = {}) { + // The script resolves GSTACK_STATE_ROOT > GSTACK_HOME > GSTACK_STATE_DIR. + // Scrub the two higher-priority overrides from the inherited env so a + // leaked GSTACK_HOME/GSTACK_STATE_ROOT from another test file (or the + // operator shell) can never redirect reads/writes away from this test's + // temp stateDir. + const env: Record = { + ...process.env, + GSTACK_STATE_DIR: stateDir, + ...extraEnv, + }; + if (!('GSTACK_STATE_ROOT' in extraEnv)) delete env.GSTACK_STATE_ROOT; + if (!('GSTACK_HOME' in extraEnv)) delete env.GSTACK_HOME; const result = Bun.spawnSync(['bash', SCRIPT, ...args], { - env: { - ...process.env, - GSTACK_STATE_DIR: stateDir, - ...extraEnv, - }, + env, stdout: 'pipe', stderr: 'pipe', }); diff --git a/test/codex-resume-flag-semantics.test.ts b/test/codex-resume-flag-semantics.test.ts index 9075e0489..da866418b 100644 --- a/test/codex-resume-flag-semantics.test.ts +++ b/test/codex-resume-flag-semantics.test.ts @@ -15,8 +15,21 @@ import { describe, test, expect } from 'bun:test'; import { spawnSync } from 'child_process'; +// "On PATH" is not enough: a broken install (e.g. the npm wrapper missing its +// platform binary @openai/codex-darwin-arm64) resolves via `which` but can't +// answer any subcommand, so every assertion would fail on wrapper stack traces +// instead of real flag-semantics regressions. Probe `codex --version` and only +// run when the CLI actually executes; a working codex still gets the full +// assertions. const codexPath = spawnSync('which', ['codex'], { encoding: 'utf-8' }).stdout.trim(); -const codexAvailable = codexPath.length > 0; +const codexRuns = + codexPath.length > 0 && + spawnSync('codex', ['--version'], { + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'pipe'], + timeout: 10_000, + }).status === 0; +const codexAvailable = codexRuns; describe.skipIf(!codexAvailable)( 'codex exec resume — flag semantics (live CLI smoke; closes #1270 regex-only gap)', diff --git a/test/gbrain-detect-install.test.ts b/test/gbrain-detect-install.test.ts index b9c82c155..fea588916 100644 --- a/test/gbrain-detect-install.test.ts +++ b/test/gbrain-detect-install.test.ts @@ -25,7 +25,15 @@ 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'; +// +// One wrinkle: bin/gstack-gbrain-detect's shebang is `#!/usr/bin/env -S bun run`, +// so the child's PATH must contain `bun`. On dev machines bun usually lives in +// ~/.bun/bin, which we deliberately exclude (it also holds a real `gbrain` from +// `bun link`). Expose ONLY the running bun interpreter through a shim dir, +// appended last so system dirs still win for everything else. +const BUN_SHIM_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'bun-shim-')); +fs.symlinkSync(process.execPath, path.join(BUN_SHIM_DIR, 'bun')); +const SAFE_PATH = `/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:/usr/local/bin:${BUN_SHIM_DIR}`; let tmpHome: string; let tmpHomeReal: string; diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index 2fb783ffd..8d23b4be0 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -177,6 +177,13 @@ describe('gen-skill-docs', () => { // whose plain `description:` scalar contains an interior ": " (read as a nested // mapping). Parse EVERY generated frontmatter block with a strict YAML parser, // not just string-check that name:/description: exist. + // + // Bun.YAML landed in bun 1.3; older local bun has no in-process strict YAML + // parser (the repo is deliberately zero-dep for YAML). CI pins + // `bun-version: latest`, so coverage is guaranteed there — on an old local + // bun we skip rather than false-fail. + const hasBunYaml = typeof (Bun as any).YAML?.parse === 'function'; + function frontmatterBlock(content: string): string { expect(content.startsWith('---\n')).toBe(true); const end = content.indexOf('\n---', 4); @@ -184,7 +191,7 @@ describe('gen-skill-docs', () => { return content.slice(4, end); } - test('every generated SKILL.md frontmatter parses as strict YAML', () => { + test.skipIf(!hasBunYaml)('every generated SKILL.md frontmatter parses as strict YAML', () => { for (const skill of CLAUDE_GENERATED_SKILLS) { const content = fs.readFileSync(path.join(ROOT, skill.dir, 'SKILL.md'), 'utf-8'); const fm = frontmatterBlock(content); @@ -196,7 +203,7 @@ describe('gen-skill-docs', () => { } }); - test('every generated Codex (.agents/skills) frontmatter parses as strict YAML', () => { + test.skipIf(!hasBunYaml)('every generated Codex (.agents/skills) frontmatter parses as strict YAML', () => { const agentsDir = path.join(ROOT, '.agents', 'skills'); if (!fs.existsSync(agentsDir)) return; // skip if external hosts not generated for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) { diff --git a/test/gstack-memory-helpers.test.ts b/test/gstack-memory-helpers.test.ts index 7b356a99c..9f190732f 100644 --- a/test/gstack-memory-helpers.test.ts +++ b/test/gstack-memory-helpers.test.ts @@ -249,11 +249,14 @@ body // ── withErrorContext ─────────────────────────────────────────────────────── describe("withErrorContext", () => { - let savedHome: string | undefined; + // Captured ONCE at collection time, NOT in beforeEach. Saving inside + // beforeEach clobbers the real value with the previous test's temp dir + // after test #1, so the afterAll restore leaked a dead temp path into + // process.env for every test file that ran after this one. + const savedHome = process.env.GSTACK_HOME; let testHome: string; beforeEach(() => { - savedHome = process.env.GSTACK_HOME; testHome = mkdtempSync(join(tmpdir(), "gstack-test-home-")); process.env.GSTACK_HOME = testHome; }); @@ -312,18 +315,20 @@ describe("withErrorContext", () => { // ── detectEngineTier ─────────────────────────────────────────────────────── describe("detectEngineTier", () => { - let savedHome: string | undefined; - let savedGbrainHome: string | undefined; - let savedRealHome: string | undefined; - let savedPath: string | undefined; + // Captured ONCE at collection time, NOT in beforeEach. Saving inside + // beforeEach clobbers the real values with the previous test's temp dirs + // after test #1, so the afterAll restore leaked dead gstack-test-engine-* + // temp paths into GSTACK_HOME / GBRAIN_HOME / HOME for every test file + // that ran after this one (broke gstack-config + make-pdf format-gate in + // full-suite runs). + const savedHome = process.env.GSTACK_HOME; + const savedGbrainHome = process.env.GBRAIN_HOME; + const savedRealHome = process.env.HOME; + const savedPath = process.env.PATH; let testHome: string; let testGbrainHome: string; beforeEach(() => { - savedHome = process.env.GSTACK_HOME; - savedGbrainHome = process.env.GBRAIN_HOME; - savedRealHome = process.env.HOME; - savedPath = process.env.PATH; testHome = mkdtempSync(join(tmpdir(), "gstack-test-engine-")); testGbrainHome = mkdtempSync(join(tmpdir(), "gstack-test-gbrain-")); process.env.GSTACK_HOME = testHome; diff --git a/test/openclaw-native-skills.test.ts b/test/openclaw-native-skills.test.ts index 009b5e22c..539a677e7 100644 --- a/test/openclaw-native-skills.test.ts +++ b/test/openclaw-native-skills.test.ts @@ -18,8 +18,14 @@ function extractFrontmatter(content: string): string { return content.slice(4, fmEnd); } +// Bun.YAML landed in bun 1.3; older local bun has no in-process strict YAML +// parser (the repo is deliberately zero-dep for YAML). CI pins +// `bun-version: latest`, so coverage is guaranteed there — on an old local +// bun we skip rather than false-fail. +const hasBunYaml = typeof (Bun as any).YAML?.parse === 'function'; + describe('OpenClaw native skills', () => { - test('frontmatter parses as YAML and keeps only name + description', () => { + test.skipIf(!hasBunYaml)('frontmatter parses as YAML and keeps only name + description', () => { for (const skill of OPENCLAW_NATIVE_SKILLS) { const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8'); const frontmatter = extractFrontmatter(content);