Files
gstack/test/setup-crlf-repair.test.ts
6e250405fe v1.91.90.0 fix(windows): LF for nested scripts with a setup CRLF repair, Stop hook slug fast path, setup-gbrain smoke test via temp file (Wave 9 Lane G) (#3142)
.gitattributes pins nested bin/, hook shims, lib/cso/images and scripts/app
to LF, and setup rewrites copies an older checkout left with CRLF, leaving
locally edited files alone with a warning (#3110). The timeline Stop hook
reads the slug from gstack-slug's cache and hands a miss to a detached,
locked, negatively cached repair child (#3103). The /setup-gbrain smoke test
writes its page to a temp file instead of piping into gbrain put (#1675).

Co-authored-by: Mr-B-1 <214900604+Mr-B-1@users.noreply.github.com>
Co-authored-by: Andrei Bacu <63974644+Peaky8linders@users.noreply.github.com>
Co-authored-by: Elliot Drel <156480527+ElliotDrel@users.noreply.github.com>
2026-10-11 17:59:13 +00:00

159 lines
6.3 KiB
TypeScript

/**
* setup's CRLF repair (#3110).
*
* Git for Windows checks out with core.autocrlf=true, so a script with no
* `eol` rule was written with CRLF. Adding the rule later does not fix the
* copy: git never rewrites a stat-clean file whose blob did not change. The
* fixture makes that state with a real autocrlf=true clone, adds the rule
* upstream, pulls, and runs setup's _repair_crlf_checkout under the real
* install-registry lock. The repaired hook must have LF bytes and run under
* bash; a locally edited CRLF file is named in a warning and left alone.
*/
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
const ROOT = path.resolve(import.meta.dir, '..');
const SETUP_SRC = fs.readFileSync(path.join(ROOT, 'setup'), 'utf-8');
const HOOK_SHIM = fs.readFileSync(path.join(ROOT, 'autoplan', 'bin', 'phase-publication-hook'), 'utf-8');
const HOOK_REL = 'autoplan/bin/phase-publication-hook';
const EDITED_REL = 'autoplan/bin/edited-hook';
function extractFn(name: string): string {
const start = SETUP_SRC.indexOf(`${name}() {`);
const end = SETUP_SRC.indexOf('\n}\n', start);
if (start < 0 || end < 0) throw new Error(`Could not locate ${name}() in setup`);
return SETUP_SRC.slice(start, end + 2);
}
let tmp: string;
let upstream: string;
let clone: string;
let stateRoot: string;
let env: Record<string, string | undefined>;
function run(cmd: string, args: string[], cwd: string, input?: string) {
return spawnSync(cmd, args, { cwd, env, input, encoding: 'utf-8', timeout: 30_000 });
}
function git(cwd: string, ...args: string[]): string {
const r = run('git', args, cwd);
if (r.status !== 0) throw new Error(`git ${args.join(' ')}: ${r.stderr}`);
return r.stdout;
}
function write(dir: string, rel: string, body: string, mode = 0o644): void {
fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true });
fs.writeFileSync(path.join(dir, rel), body, { mode });
}
function repair() {
return run('bash', ['-c', [
'set -e',
'QUIET=0',
'log() { [ "$QUIET" -eq 0 ] && echo "$@" || true; }',
`SOURCE_GSTACK_DIR=${JSON.stringify(clone)}`,
`GSTACK_STATE_ROOT=${JSON.stringify(stateRoot)}`,
`. ${JSON.stringify(path.join(ROOT, 'bin', 'gstack-install-registry.sh'))}`,
extractFn('_repair_crlf_checkout'),
'_repair_crlf_checkout',
].join('\n')], clone);
}
function runHook() {
return run('bash', [path.join(clone, HOOK_REL)], clone, JSON.stringify({ tool_name: 'Bash', tool_input: { command: 'ls' } }));
}
beforeAll(() => {
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-crlf-'));
upstream = path.join(tmp, 'upstream');
clone = path.join(tmp, 'clone');
stateRoot = path.join(tmp, 'state');
const gitconfig = path.join(tmp, 'gitconfig');
fs.writeFileSync(gitconfig, '[user]\n\tname = t\n\temail = t@test\n[commit]\n\tgpgsign = false\n[init]\n\tdefaultBranch = main\n');
env = {
...process.env,
HOME: tmp,
GIT_CONFIG_GLOBAL: gitconfig,
GIT_CONFIG_NOSYSTEM: '1',
PATH: `${path.dirname(process.execPath)}${path.delimiter}${process.env.PATH ?? ''}`,
};
fs.mkdirSync(upstream);
git(upstream, 'init', '-q');
write(upstream, '.gitattributes', '.gitattributes text eol=lf\n*.ts text eol=lf\n');
write(upstream, HOOK_REL, HOOK_SHIM, 0o755);
write(upstream, `${HOOK_REL}.ts`, "process.stdout.write('{}');\n");
write(upstream, EDITED_REL, '#!/usr/bin/env bash\nset -euo pipefail\necho upstream\n', 0o755);
git(upstream, 'add', '-A');
git(upstream, 'commit', '-qm', 'old release: no eol rule for nested bin/');
git(tmp, 'clone', '-q', '-c', 'core.autocrlf=true', upstream, clone);
write(upstream, '.gitattributes', '.gitattributes text eol=lf\n*.ts text eol=lf\n**/bin/* text eol=lf\n');
git(upstream, 'commit', '-qam', 'new release: nested bin/ is LF');
git(clone, 'pull', '-q', '--ff-only');
const edited = path.join(clone, EDITED_REL);
fs.writeFileSync(edited, fs.readFileSync(edited, 'utf-8') + 'echo local edit\r\n');
});
afterAll(() => {
fs.rmSync(tmp, { recursive: true, force: true });
});
describe('setup CRLF repair (#3110)', () => {
test('fixture precondition: the pulled rule left the CRLF copy, and Linux bash cannot run it', () => {
expect(fs.readFileSync(path.join(clone, HOOK_REL), 'utf-8')).toContain('\r\n');
expect(git(clone, 'check-attr', 'eol', '--', HOOK_REL)).toContain('eol: lf');
expect(git(clone, 'status', '--porcelain', '--untracked-files=no')).toBe(` M ${EDITED_REL}\n`);
const r = runHook();
expect(r.status).not.toBe(0);
expect(r.stderr).toContain('pipefail');
});
test('rewrites the unmodified file as LF, keeps the edited one, and the hook runs', () => {
const r = repair();
expect(r.status).toBe(0);
expect(r.stdout).toContain('Rewrote 1 file(s)');
const repaired = fs.readFileSync(path.join(clone, HOOK_REL));
expect(repaired.includes('\r')).toBe(false);
expect(repaired.toString('utf-8')).toBe(git(clone, 'show', `HEAD:${HOOK_REL}`));
expect(fs.statSync(path.join(clone, HOOK_REL)).mode & 0o111).not.toBe(0);
expect(git(clone, 'status', '--porcelain', '--untracked-files=no')).toBe(` M ${EDITED_REL}\n`);
const edited = fs.readFileSync(path.join(clone, EDITED_REL), 'utf-8');
expect(edited).toContain('echo local edit\r\n');
expect(edited).toContain('\r\n');
expect(r.stderr).toContain(EDITED_REL);
expect(r.stderr).toContain(`cd ${clone} && git stash && git stash pop`);
expect(r.stderr).toContain('docs/troubleshooting.md#setup-crlf-repair');
expect(fs.existsSync(path.join(stateRoot, 'installs.tsv.lock'))).toBe(false);
const hook = runHook();
expect(hook.stderr).toBe('');
expect(hook.status).toBe(0);
expect(hook.stdout.trim()).toBe('{}');
});
test('a second run rewrites nothing', () => {
const r = repair();
expect(r.status).toBe(0);
expect(r.stdout).not.toContain('Rewrote');
expect(r.stderr).toContain(EDITED_REL);
});
test('the printed fix rewrites the edited file as LF and keeps the edit', () => {
git(clone, 'stash', '-q');
git(clone, 'stash', 'pop', '-q');
const edited = fs.readFileSync(path.join(clone, EDITED_REL), 'utf-8');
expect(edited.includes('\r')).toBe(false);
expect(edited).toContain('echo local edit\n');
const r = repair();
expect(r.stdout + r.stderr).toBe('');
});
});