Files
gstack/test/bun-isolation-launch.test.ts
Garry Tanandcapy-ai[bot] 9dc1dfb729 v1.91.227.0 fix(windows): shell bins pass NUL as Bun's empty config when MSYS path conversion is off (#3415) (#3431)
* fix(windows): shell bins pass NUL as Bun's empty config when MSYS path conversion is off (#3415)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(windows): detect Git Bash/MSYS2/Cygwin from OSTYPE, as gstack-doctor does, so uname stubs in setup fixtures stay untouched

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (#3415)

- Choose NUL only when cygpath is also on PATH, so an OSTYPE=msys inherited
  by a POSIX bash keeps /dev/null instead of a cwd-relative NUL file.
- gstack-developer-profile --log-session reports a Bun failure instead of
  skipping it as invalid input, like gstack-timeline-log.
- learnings-injection tests check that their anchored bun -e block exists.
- CHANGELOG names the callers this fix does not cover; curation reason fixed.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes, cycle 2 (#3415)

- timeline-log and developer-profile validators print their verdict; any
  non-zero Bun exit is a reported failure (a Bun crash on Windows exits 3).
- timeline-log keeps the original event when ts stamping fails.
- The definition ratchet scopes a local GSTACK_BUN_CONFIG to its function.
- The shell-bins fixture clears its preload marker per test.
- CHANGELOG states the callers that discard the report and the Bun 1.3.x case.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: CHANGELOG notes Bun 1.3.10 evidence and remaining literal /dev/null callers (#3415)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-26 04:32:27 +00:00

89 lines
4.5 KiB
TypeScript

/**
* GSTA-56: each way gstack starts Bun keeps a hostile working directory's
* .env and bunfig.toml out, on every platform the free lanes run, including
* windows-latest under Git Bash. There MSYS rewrites --config=/dev/null to
* --config=nul for the native bun.exe, which Bun 1.4.2 reads as an empty
* config; a native Windows spawn has no /dev/null, so TypeScript callers use
* bunIsolationArgs() (lib/bun-isolation.ts) instead of spelling the flag.
* With conversion off (MSYS_NO_PATHCONV=1) bun.exe gets /dev/null itself and
* fails with EINVAL, so gstack's shell files resolve GSTACK_BUN_CONFIG to NUL
* there (#3415).
*/
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { bunIsolationArgs } from '../lib/bun-isolation';
import { bashCommandInvocation } from '../lib/script-invocation';
const PRINT = 'console.log("env=" + (process.env.GSTACK_LAUNCH_PROBE ?? "unset"))';
const CONFIG_DEF = 'GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac';
const NO_PATHCONV = { MSYS_NO_PATHCONV: '1', MSYS2_ARG_CONV_EXCL: '*' };
let project = '';
let preloadMarker = '';
function bash(command: string, extraEnv: Record<string, string> = {}) {
const inv = bashCommandInvocation(command);
if (!inv.ok) throw new Error(inv.error);
return spawnSync(inv.cmd, inv.argv, { cwd: project, encoding: 'utf8', timeout: 60_000, env: { ...process.env, ...extraEnv } });
}
function expectIsolated(r: { status: number | null; stdout: string; stderr: string }, expected = 'env=unset') {
expect(r.stderr.trim()).toBe('');
expect(r.status).toBe(0);
expect(r.stdout.trim()).toBe(expected);
expect(fs.existsSync(preloadMarker)).toBe(false);
}
beforeAll(() => {
project = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-launch-')));
preloadMarker = path.join(project, 'preload-ran');
fs.writeFileSync(path.join(project, '.env'), 'GSTACK_LAUNCH_PROBE=from-dotenv\n');
fs.writeFileSync(path.join(project, 'preload.ts'), `require('fs').writeFileSync(${JSON.stringify(preloadMarker)}, 'ran');\n`);
fs.writeFileSync(path.join(project, 'bunfig.toml'), 'preload = ["./preload.ts"]\n');
fs.writeFileSync(path.join(project, 'probe.ts'), PRINT + '\n');
fs.writeFileSync(path.join(project, 'shebang-probe'), `#!/usr/bin/env -S bun --no-env-file --config=/dev/null\n${PRINT}\n`);
fs.chmodSync(path.join(project, 'shebang-probe'), 0o755);
});
afterAll(() => { if (project) fs.rmSync(project, { recursive: true, force: true }); });
describe('GSTA-56: isolated Bun launches on this platform', () => {
test('control: a bare bun in the fixture loads its .env and runs its preload', () => {
const r = spawnSync(process.execPath, ['probe.ts'], { cwd: project, encoding: 'utf8', timeout: 60_000 });
expect(r.stdout).toContain('env=from-dotenv');
expect(fs.existsSync(preloadMarker)).toBe(true);
fs.rmSync(preloadMarker);
});
test('a script with the isolated shebang, executed from bash', () => {
expectIsolated(bash('./shebang-probe'));
});
test('a bash caller spelling bun --no-env-file --config=/dev/null', () => {
expectIsolated(bash(`bun --no-env-file --config=/dev/null -e '${PRINT}'`));
expectIsolated(bash('bun --no-env-file --config=/dev/null run probe.ts'));
});
test('a shipped shell file spelling, with MSYS path conversion off', () => {
expectIsolated(bash(`${CONFIG_DEF}; bun --no-env-file --config="\${GSTACK_BUN_CONFIG:-/dev/null}" -e '${PRINT}'`, NO_PATHCONV));
expectIsolated(bash(`${CONFIG_DEF}; bun --no-env-file --config="\${GSTACK_BUN_CONFIG:-/dev/null}" run probe.ts`, NO_PATHCONV));
});
test.if(process.platform === 'win32')('control: with MSYS path conversion off, bun.exe cannot open --config=/dev/null', () => {
const r = bash(`bun --no-env-file --config=/dev/null -e '${PRINT}'`, NO_PATHCONV);
expect(r.status).not.toBe(0);
expect(r.stderr).toContain('EINVAL');
expect(fs.existsSync(preloadMarker)).toBe(false);
});
test('a TypeScript spawn with bunIsolationArgs()', () => {
expectIsolated(spawnSync(process.execPath, [...bunIsolationArgs(), 'probe.ts'], { cwd: project, encoding: 'utf8', timeout: 60_000 }));
});
test("negative control: the user's shell environment still reaches an isolated launch", () => {
expectIsolated(bash('./shebang-probe', { GSTACK_LAUNCH_PROBE: 'from-shell' }), 'env=from-shell');
});
});