Skip to content

Commit e7b23f2

Browse files
committed
test(build): execute helper scripts in regression test (garrytan#1602)
Issue garrytan#1602's package.json fix landed in garrytan#1594, but the regression test was string-matching only. Exercise scripts/build.sh and scripts/write-version-files.sh via `bash -n` (syntax check) and a real invocation against tmpdir targets; also expand the package.json guard to cover every bunsh-incompatible construct (subshells-with-redirection, multi-redirection, brace groups, process substitution) in one place. Catches a future edit that re-introduces any of them without needing a Windows runner.
1 parent a9f347a commit e7b23f2

1 file changed

Lines changed: 87 additions & 1 deletion

File tree

test/build-script-shell-compat.test.ts

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import { describe, test, expect } from 'bun:test';
2+
import { spawnSync } from 'child_process';
23
import * as fs from 'fs';
4+
import * as os from 'os';
35
import * as path from 'path';
46

57
const ROOT = path.resolve(import.meta.dir, '..');
68
const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf-8')) as {
79
scripts: Record<string, string>;
810
};
9-
const BUILD_SCRIPT = fs.readFileSync(path.join(ROOT, 'scripts', 'build.sh'), 'utf-8');
11+
const BUILD_SCRIPT_PATH = path.join(ROOT, 'scripts', 'build.sh');
12+
const WRITE_VERSION_PATH = path.join(ROOT, 'scripts', 'write-version-files.sh');
13+
const BUILD_SCRIPT = fs.readFileSync(BUILD_SCRIPT_PATH, 'utf-8');
1014

1115
// Strip single-quoted strings so JS code emitted as `echo '{ ... }'` doesn't
1216
// trip the shell-brace-group check. Conservative: only `'...'` segments.
@@ -52,3 +56,85 @@ describe('package.json build scripts — POSIX shell compat (D-1460)', () => {
5256
expect(BUILD_SCRIPT).toContain('bash scripts/write-version-files.sh');
5357
});
5458
});
59+
60+
// ── Issue #1602: Windows build hardening ──────────────────────────────────
61+
// Static string-matching catches the package.json regression. These tests
62+
// also exercise the helper scripts so a hand-written bash bug (missing
63+
// shebang, broken loop, syntax error) is caught at `bun test` time on every
64+
// platform — no need to fully build the project to find a syntax error.
65+
66+
describe('Windows build hardening (issue #1602)', () => {
67+
test('scripts/build.sh is bash-syntax-clean (bash -n)', () => {
68+
const r = spawnSync('bash', ['-n', BUILD_SCRIPT_PATH], { timeout: 5000 });
69+
expect(r.status).toBe(0);
70+
expect((r.stderr ?? '').toString()).toBe('');
71+
});
72+
73+
test('scripts/write-version-files.sh is bash-syntax-clean (bash -n)', () => {
74+
const r = spawnSync('bash', ['-n', WRITE_VERSION_PATH], { timeout: 5000 });
75+
expect(r.status).toBe(0);
76+
expect((r.stderr ?? '').toString()).toBe('');
77+
});
78+
79+
test('scripts/write-version-files.sh writes one .version file per argument', () => {
80+
// The build chain calls this with three paths (browse/dist/.version,
81+
// design/dist/.version, make-pdf/dist/.version). Validate the loop body
82+
// by invoking with controlled targets in a tmpdir — catches a future
83+
// refactor that loses the `for/do/done` shape or drops `mkdir -p`.
84+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-version-files-'));
85+
try {
86+
const a = path.join(dir, 'a', 'dist', '.version');
87+
const b = path.join(dir, 'b', 'dist', '.version');
88+
const r = spawnSync('bash', [WRITE_VERSION_PATH, a, b], {
89+
timeout: 5000,
90+
env: { ...process.env, PATH: process.env.PATH ?? '' },
91+
});
92+
expect(r.status).toBe(0);
93+
expect(fs.existsSync(a)).toBe(true);
94+
expect(fs.existsSync(b)).toBe(true);
95+
// Both files share the same git_head value, derived once at the top
96+
// of the script.
97+
expect(fs.readFileSync(a, 'utf-8')).toBe(fs.readFileSync(b, 'utf-8'));
98+
} finally {
99+
fs.rmSync(dir, { recursive: true, force: true });
100+
}
101+
});
102+
103+
test('scripts/write-version-files.sh tolerates being run outside a git repo', () => {
104+
// The script's git rev-parse is wrapped in `if … then : else git_head=""`.
105+
// Catches a regression where that fallback gets stripped and the script
106+
// dies under `set -e` when run from a tarball.
107+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-version-no-git-'));
108+
try {
109+
const target = path.join(dir, 'dist', '.version');
110+
const r = spawnSync('bash', [WRITE_VERSION_PATH, target], {
111+
timeout: 5000,
112+
cwd: dir, // outside any git repo
113+
env: {
114+
...process.env,
115+
PATH: process.env.PATH ?? '',
116+
GIT_DIR: '/nonexistent', // force git rev-parse to fail
117+
},
118+
});
119+
expect(r.status).toBe(0);
120+
expect(fs.existsSync(target)).toBe(true);
121+
} finally {
122+
fs.rmSync(dir, { recursive: true, force: true });
123+
}
124+
});
125+
126+
test('package.json build entry contains no constructs the Bun Windows shell rejects', () => {
127+
// Comprehensive guard collecting every known bunsh-incompatible pattern
128+
// in one place. Catches a future package.json edit that re-introduces
129+
// any of them without needing a Windows runner.
130+
const build = PKG.scripts.build ?? '';
131+
// Subshells `( ... )` — bunsh rejects when paired with redirection.
132+
expect(build).not.toMatch(/\([^)]*\)\s*[<>]/);
133+
// Multiple redirections — `cmd >a 2>b` form. Bunsh rejects ">" "2>" pairs.
134+
expect(build).not.toMatch(/>\s*\S+\s+2>/);
135+
// Bash brace groups `{ cmd; }`.
136+
expect(stripSingleQuoted(build)).not.toMatch(/\{\s+[^}]*;\s*\}/);
137+
// Process substitution `<(cmd)` and `>(cmd)`.
138+
expect(build).not.toMatch(/[<>]\([^)]+\)/);
139+
});
140+
});

0 commit comments

Comments
 (0)