fix: win32 NUL discard for the Go build check; unique Health Stack names (rule 23 grant)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-31 07:58:43 +00:00
1 parent 6d2a9e1ad9
commit 2147aeffc3
3 files changed
+47 -5

No files matched your search

+6 -4
View File
@@ -15,7 +15,6 @@
*/
import { createHash } from 'node:crypto';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { slugFromEnvironment } from './bin-context';
import { resolveStateRoot } from './state-root';
@@ -61,14 +60,17 @@ export function healthStack(claudeMd: string): Check[] {
if (start < 0) return [];
const checks: Check[] = [];
const seen = new Map<string, number>();
const taken = new Set<string>();
for (const line of lines.slice(start + 1)) {
if (/^#{1,6}\s/.test(line)) break;
const m = /^\s*[-*]\s*([A-Za-z0-9_-]+):\s*(.+?)\s*$/.exec(line);
if (!m || ['test', TESTS].includes(m[1].toLowerCase())) continue;
const key = m[1].toLowerCase();
const n = (seen.get(key) ?? 0) + 1;
let n = seen.get(key) ?? 0;
let name = key;
while (taken.has(name)) name = `${key}-${++n + 1}`;
seen.set(key, n);
const name = n === 1 ? key : `${key}-${n}`;
taken.add(name);
checks.push({ name, command: m[2].replace(/`/g, ''), source: 'CLAUDE.md Health Stack' });
}
return checks;
@@ -106,7 +108,7 @@ export function manifestChecks(root: string): Check[] {
const cargo = read(path.join(root, 'Cargo.toml'));
if (cargo !== null) checks.push({ name: 'build', command: 'cargo check --all-targets', source: 'Cargo.toml' });
if (cargo !== null && (/^\[(workspace\.)?lints\.clippy\]/m.test(cargo) || ['clippy.toml', '.clippy.toml'].some(f => fs.existsSync(path.join(root, f))))) checks.push({ name: 'lint', command: 'cargo clippy --all-targets', source: 'Cargo.toml' });
if (fs.existsSync(path.join(root, 'go.mod'))) checks.push({ name: 'vet', command: 'go vet ./...', source: 'go.mod' }, { name: 'build', command: `go build -o ${os.devNull} ./...`, source: 'go.mod' });
if (fs.existsSync(path.join(root, 'go.mod'))) checks.push({ name: 'vet', command: 'go vet ./...', source: 'go.mod' }, { name: 'build', command: `go build -o ${process.platform === 'win32' ? 'NUL' : '/dev/null'} ./...`, source: 'go.mod' });
const seen = new Set<string>();
return checks.filter(c => (seen.has(c.name) ? false : (seen.add(c.name), true)));
}
+39
View File
@@ -0,0 +1,39 @@
/**
* The Go build check gstack-checks detects runs through the same bash invocation pregate's supervise uses (Git Bash on
* Windows), so its discard path must survive bash: os.devNull on Windows is \\.\nul, which bash mangles to \.nul.
* Kept out of test/gstack-checks.test.ts so a lane with Go can run it on its own. Skips where Go is not installed.
*/
import { afterAll, 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 { manifestChecks } from '../lib/checks';
import { bashCommandInvocation } from '../lib/script-invocation';
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-checks-go-'));
afterAll(() => fs.rmSync(tmp, { recursive: true, force: true }));
const write = (rel: string, text: string) => { fs.mkdirSync(path.dirname(path.join(tmp, rel)), { recursive: true }); fs.writeFileSync(path.join(tmp, rel), text); };
const files = (dir: string): string[] => fs.readdirSync(dir, { withFileTypes: true }).flatMap(e => e.isDirectory() ? files(path.join(dir, e.name)).map(f => `${e.name}/${f}`) : [e.name]).sort();
describe('the detected Go build check', () => {
// Value: protects=the build check passes and writes nothing on a one-main module and a three-main module;
// fails_when=the discard path is mangled (a false FAIL on several mains) or dropped (a binary lands in the tree);
// why_new=the -o discard is new; seam=the production bash invocation; no_claim=Go toolchains without go on PATH
test.skipIf(!Bun.which('go'))('builds every package through bash and leaves the tree unchanged', () => {
// One root main package (go build would write ./x) and three main packages (a mangled -o path fails with several).
for (const [mod, mains] of [['one', ['main.go']], ['many', ['main.go', 'cmd/a/main.go', 'cmd/b/main.go']]] as const) {
const dir = path.join(tmp, mod);
write(`${mod}/go.mod`, `module example.com/${mod}\n\ngo 1.21\n`);
for (const rel of mains) write(`${mod}/${rel}`, 'package main\n\nfunc main() {}\n');
const before = files(dir);
const build = manifestChecks(dir).find(c => c.name === 'build')!;
const inv = bashCommandInvocation(build.command);
if (!inv.ok) throw new Error(inv.error);
const r = spawnSync(inv.cmd, inv.argv, { cwd: dir, encoding: 'utf8', timeout: 120_000, env: { ...process.env, GOWORK: 'off' } });
expect(`${r.stdout}${r.stderr}`).toBe('');
expect(r.status).toBe(0);
expect(files(dir)).toEqual(before);
}
}, 150_000);
});
+2 -1
View File
@@ -60,6 +60,7 @@ describe('detection', () => {
.toEqual([['typecheck', 'tsc --noEmit'], ['shell', 'shellcheck *.sh']]);
// A repeated key keeps both checks under their own names, and a tests row is pregate's, so status never reads one verdict for two checks.
expect(healthStack('## Health Stack\n- lint: eslint .\n- lint: stylelint "**/*.css"\n- lint: oxlint\n- tests: npm test\n').map(c => c.name)).toEqual(['lint', 'lint-2', 'lint-3']);
expect(healthStack('## Health Stack\n- lint: eslint .\n- lint-2: oxlint\n- lint: stylelint x\n').map(c => c.name)).toEqual(['lint', 'lint-2', 'lint-3']);
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-checks-manifest-'));
dirs.push(dir);
fs.writeFileSync(path.join(dir, 'tsconfig.json'), '{}');
@@ -68,7 +69,7 @@ describe('detection', () => {
fs.writeFileSync(path.join(dir, 'bun.lock'), '');
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ scripts: { lint: 'eslint .' }, devDependencies: { typescript: '5' } }));
fs.writeFileSync(path.join(dir, 'go.mod'), 'module x\n');
expect(manifestChecks(dir).map(c => c.command)).toEqual(['bunx tsc --noEmit', 'bun run lint', 'go vet ./...', `go build -o ${os.devNull} ./...`]);
expect(manifestChecks(dir).map(c => c.command)).toEqual(['bunx tsc --noEmit', 'bun run lint', 'go vet ./...', `go build -o ${process.platform === 'win32' ? 'NUL' : '/dev/null'} ./...`]);
// Clippy is a rustup component Cargo.toml does not declare: it runs only when the repo opts in.
const rs = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-checks-rs-'));
dirs.push(rs);