Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/scripts/issue-refresh-router-workflow.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,15 @@ test('workflow baselines retain readiness guards and pin original-event identity
// #2160 permits repeated parameterized names while retaining numeric TAP identity checks.
// #1891 adds only the observational Windows suite/discovery, including trusted Bash/jq prerequisites,
// Git/bin root resolution and its native Windows path contract step.
// #2179 adds safe temp observations and post-suite representative diagnostics/artifacts;
// the original suite block is unchanged, with collector contracts registered alongside it.
// #2192 adds only compatible Race cache restore/refresh; other jobs and guards stay exact.
// Normalize to LF for Windows and archives; retain strict full-file hash comparisons.
const baseline = {
'.github/workflows/pr-check.yml': '33259c75e4f7dfdfca3143fc5efa0521a8ca289f06eca0d3efb429fbad7565bf',
'.github/workflows/issue-validation.yml': 'd996ffae8e1341390b818e294a1a34a70f1b06f5eabda03cb96c0f065f7115b8',
'.github/workflows/issue-policy.yml': '5f8bfbd9ab354f6b60d3ef35d30d5f4ee43a97eb83f61d8e0a955da37947adfd',
'.github/workflows/ci.yml': '0266f252c818bd1d5ea07810473510aa1e905a6a7abb9dea599039419a0e32ec',
'.github/workflows/ci.yml': 'a355a3d0e4a0a1ef032f52b3f53f385357597cd6902e855c9526de3c9f726b7e',
};
for (const [path, expected] of Object.entries(baseline)) {
const source = path.endsWith('/pr-check.yml') ? read(path).replace(/^ if \(!Array\.isArray\(issue\.labels\).+\n throw new Error\('issue labels are malformed'\);\n }\n/m, '').replace(/^ if \(!Array\.isArray\(issue\.assignees\).+\n throw new Error\('issue assignees are malformed'\);\n }\n if \(issue\.assignees\.length === 0\) \{\n errors\.push\(.+\);\n }\n/m, '') : read(path);
Expand Down
98 changes: 98 additions & 0 deletions .github/scripts/windows-bash-temp-diagnostics.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { spawnSync } from 'node:child_process';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const aliases = ['HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'TEMP', 'TMP', 'TMPDIR'];
const context = ['GITHUB_SHA', 'GITHUB_RUN_ID', 'GITHUB_RUN_ATTEMPT', 'RUNNER_OS', 'ImageVersion', 'ImageOS'];
const runtime = ['SYSTEMROOT', 'WINDIR', 'OS', 'COMSPEC', 'PATH', 'PATHEXT', 'MSYSTEM',
'MSYS_NO_PATHCONV', 'MSYS2_ARG_CONV_EXCL', 'MSYS2_ENV_CONV_EXCL'];
const usage = 'Usage: windows-bash-temp-diagnostics.mjs snapshot FILE | collect BASELINE FILE\n';
function select(env, names) {
const upper = Object.fromEntries(Object.entries(env).map(([key, value]) => [key.toUpperCase(), value]));
return Object.fromEntries(names.map(key => [key, upper[key.toUpperCase()] ?? null]));
}
const supplied = env => Object.fromEntries(Object.entries(env).filter(([, value]) => value !== null));
export function snapshot(env = process.env) {
return { version: 1, aliases: select(env, aliases), context: select(env, context), node: process.version };
}
function validate(baseline) {
if (baseline?.version !== 1 || !aliases.every(key => baseline.aliases?.[key] === null || typeof baseline.aliases?.[key] === 'string')) {
throw new Error('Invalid diagnostic baseline');
}
}
const shell = [
'printf "BASH_VERSION=%s\\n" "$BASH_VERSION"',
'printf "POSIX_TMP_WINDOWS="',
'if "$DIAG_CYGPATH" -w /tmp; then printf "MAP_EXIT=0\\n"; else printf "MAP_EXIT=%s\\n" "$?"; fi',
'if [ -d /tmp ]; then printf "POSIX_TMP_EXISTS=yes\\n"; else printf "POSIX_TMP_EXISTS=no\\n"; fi',
'for key in TEMP TMP TMPDIR; do',
' if [[ -v $key ]]; then',
' printf "%s=%s\\n" "$key" "${!key}"',
' if "$DIAG_CYGPATH" -u "${!key}"; then printf "%s_MAP_EXIT=0\\n" "$key"; else printf "%s_MAP_EXIT=%s\\n" "$key" "$?"; fi',
' if [ -d "${!key}" ]; then printf "%s_EXISTS=yes\\n" "$key"; else printf "%s_EXISTS=no\\n" "$key"; fi',
' else printf "%s=<unset>\\n" "$key"; fi',
'done',
].join('\n');

// Called after the suite: never recreate a missing alias or mapped /tmp.
// execute is the process boundary; contract tests do not start Bash before CI's suite.
export function collect(baseline, parent, env = process.env, execute = spawnSync) {
validate(baseline);
const bash = env.CODEX_TEST_TRUSTED_BASH;
if (!bash || !path.isAbsolute(bash)) throw new Error('Absolute CODEX_TEST_TRUSTED_BASH required');
const base = { ...supplied(select(env, runtime)), BASH_ENV: '', ENV: '', LC_ALL: 'C',
DIAG_CYGPATH: path.join(path.dirname(bash), 'cygpath.exe').replaceAll('\\', '/') };
const original = { version: 1, aliases: select(baseline.aliases, aliases),
context: select(baseline.context ?? {}, context), node: baseline.node ?? null };
const result = { version: 1, representativeOnly: true, goTempDirMeasured: false,
startupFilesDisabled: true, baseline: original, current: snapshot(env), bash, cases: [] };
const observe = (name, environment) => {
const native = select(environment, ['TEMP', 'TMP', 'TMPDIR']);
const directories = Object.fromEntries(Object.entries(native).map(([key, value]) => [key, value === null ? null : existsSync(value)]));
const child = execute(bash, ['--noprofile', '--norc', '-c', shell], {
env: environment, encoding: 'utf8', timeout: 10000, windowsHide: true,
});
result.cases.push({ name, environment, directories, at: new Date().toISOString(),
exit: child.status, signal: child.signal, error: child.error?.message ?? null,
stdout: child.stdout ?? '', stderr: child.stderr ?? '' });
};
try {
// Observe existing aliases before allocating any diagnostic profile.
observe('original-aliases', { ...base, ...supplied(original.aliases) });
observe('suite-aliases', { ...base, ...supplied(select(env, aliases)) });
result.scratch = mkdtempSync(path.join(parent, 'bash-temp-'));
const fixture = { ...base, PATH: '', MSYS_NO_PATHCONV: '1' };
for (const [dir, keys] of Object.entries({ home: ['HOME', 'USERPROFILE'],
config: ['APPDATA', 'XDG_CONFIG_HOME', 'ENGRAM_CONFIG_DIR', 'CURL_HOME', 'CODEX_HOME', 'CLAUDE_CONFIG_DIR'],
data: ['LOCALAPPDATA', 'XDG_DATA_HOME', 'ENGRAM_DATA_DIR'], tmp: ['TEMP', 'TMP', 'TMPDIR'] })) {
const value = path.join(result.scratch, dir);
mkdirSync(value);
for (const key of keys) fixture[key] = value;
}
observe('fixture-like-no-gate', fixture);
} catch (error) {
result.setupError = error.message;
} finally {
if (result.scratch) rmSync(result.scratch, { recursive: true, force: true });
result.scratchRemoved = !result.scratch || !existsSync(result.scratch);
}
return result;
}
function main(args) {
if (args.length === 1 && args[0] === '--help') { process.stdout.write(usage); return; }
if (!(args[0] === 'snapshot' && args.length === 2) && !(args[0] === 'collect' && args.length === 3)) {
process.stderr.write(usage); process.exitCode = 2; return;
}
try {
if (args[0] === 'snapshot') {
mkdirSync(path.dirname(args[1]), { recursive: true });
writeFileSync(args[1], JSON.stringify(snapshot(), null, 2) + '\n');
} else {
const result = collect(JSON.parse(readFileSync(args[1], 'utf8')), path.dirname(args[2]));
writeFileSync(args[2], JSON.stringify(result, null, 2) + '\n');
if (result.setupError || result.cases.length !== 3 || result.cases.some(c => c.exit !== 0 || c.error) || !result.scratchRemoved) process.exitCode = 1;
}
} catch (error) { process.stderr.write(error.message + '\n'); process.exitCode = 1; }
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main(process.argv.slice(2));
128 changes: 128 additions & 0 deletions .github/scripts/windows-bash-temp-diagnostics.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
const script = fileURLToPath(new URL('./windows-bash-temp-diagnostics.mjs', import.meta.url));
const usage = 'Usage: windows-bash-temp-diagnostics.mjs snapshot FILE | collect BASELINE FILE\n';
function fixture(t) {
const parent = path.resolve('odd');
mkdirSync(parent, { recursive: true });
const root = mkdtempSync(path.join(parent, 'temp-contract-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
return root;
}
const cli = (args, env = process.env) => spawnSync(process.execPath, [script, ...args], { env, encoding: 'utf8', timeout: 10000 });

test('snapshot CLI records only safe aliases/context, including unset values', t => {
const root = fixture(t), output = path.join(root, 'baseline.json');
const result = cli(['snapshot', output], { SystemRoot: process.env.SystemRoot, HOME: root, TEMP: root, GITHUB_SHA: 'revision', GITHUB_TOKEN: 'never-record' });
assert.equal(result.status, 0, result.stderr);
assert.equal(result.stdout, ''); assert.equal(result.stderr, '');
const data = JSON.parse(readFileSync(output, 'utf8'));
assert.equal(data.version, 1); assert.equal(data.aliases.HOME, root);
assert.equal(data.aliases.TMPDIR, null); assert.equal(data.context.GITHUB_SHA, 'revision');
assert.doesNotMatch(JSON.stringify(data), /never-record|GITHUB_TOKEN/);
assert.deepEqual(readdirSync(root), ['baseline.json']);
});

test('CLI help has exact stdout and no stderr', () => {
const result = cli(['--help']);
assert.equal(result.status, 0); assert.equal(result.stdout, usage); assert.equal(result.stderr, '');
});

test('CLI rejects invalid arguments without altering existing output or creating scratch', t => {
const root = fixture(t), output = path.join(root, 'result.json');
writeFileSync(output, 'sentinel');
const result = cli(['unknown', output]);
assert.equal(result.status, 2); assert.equal(result.stdout, ''); assert.equal(result.stderr, usage);
assert.equal(readFileSync(output, 'utf8'), 'sentinel'); assert.deepEqual(readdirSync(root), ['result.json']);
});

test('representative matrix preserves every child exit and exact streams, environment and cleanup', async t => {
const { collect, snapshot } = await import('./windows-bash-temp-diagnostics.mjs');
const root = fixture(t), calls = [];
const env = { SystemRoot: process.env.SystemRoot, TEMP: root, TMP: root, TMPDIR: root, CODEX_TEST_TRUSTED_BASH: script, SECRET_TOKEN: 'never-record' };
const baseline = snapshot({ TEMP: 'original-temp' });
baseline.aliases.BASH_ENV = 'never-execute'; baseline.aliases.SECRET_TOKEN = 'never-record';
const result = collect(baseline, root, env, (exe, args, options) => {
calls.push({ exe, args, options });
return { status: 31, signal: null, stdout: 'raw\r\nstdout\n', stderr: 'bash.exe: warning: could not find /tmp, please create!\r\n' };
});
assert.deepEqual(result.cases.map(c => c.name), ['original-aliases', 'suite-aliases', 'fixture-like-no-gate']);
assert.equal(result.representativeOnly, true); assert.equal(result.goTempDirMeasured, false);
assert.equal(calls.length, 3);
for (const [i, c] of result.cases.entries()) {
assert.equal(c.exit, 31); assert.equal(c.signal, null); assert.equal(c.error, null);
assert.equal(c.stdout, 'raw\r\nstdout\n'); assert.equal(c.stderr, 'bash.exe: warning: could not find /tmp, please create!\r\n');
assert.equal(calls[i].options.timeout, 10000);
assert.deepEqual(calls[i].args.slice(0, 3), ['--noprofile', '--norc', '-c']);
assert.equal(calls[i].options.env.BASH_ENV, ''); assert.equal(calls[i].options.env.ENV, '');
assert.doesNotMatch(JSON.stringify(c.environment), /SECRET_TOKEN|never-record/);
}
assert.doesNotMatch(JSON.stringify(result), /never-record|never-execute|SECRET_TOKEN/);
assert.equal(calls[0].options.env.TEMP, 'original-temp');
assert.equal(Object.hasOwn(calls[0].options.env, 'TMPDIR'), false);
assert.equal(calls[1].options.env.TEMP, root);
assert.equal(calls[2].options.env.PATH, ''); assert.equal(calls[2].options.env.MSYS_NO_PATHCONV, '1');
assert.equal(result.scratchRemoved, true); assert.equal(existsSync(result.scratch), false);
assert.deepEqual(readdirSync(root), []);
});

test('Bash receives a slash-delimited sibling executable with spaces even when PATH is empty', async t => {
const { collect, snapshot } = await import('./windows-bash-temp-diagnostics.mjs');
const root = fixture(t), calls = [];
const bash = path.join(root, 'Git With Spaces', 'usr', 'bin', 'bash.exe');
const expected = path.join(path.dirname(bash), 'cygpath.exe').replaceAll('\\', '/');
const result = collect(snapshot({}), root, { CODEX_TEST_TRUSTED_BASH: bash }, (exe, args, options) => {
calls.push({ exe, args, options });
return { status: 0, signal: null, stdout: 'mapping observation\r\n', stderr: '' };
});
assert.equal(calls.length, 3);
for (const [i, call] of calls.entries()) {
assert.equal(call.exe, bash);
assert.equal(call.options.env.DIAG_CYGPATH, expected);
assert.doesNotMatch(call.options.env.DIAG_CYGPATH, /\\\\/);
assert.ok(call.options.env.DIAG_CYGPATH.includes('/Git With Spaces/'));
assert.ok(call.args[3].includes('"$DIAG_CYGPATH" -w /tmp'));
assert.equal(result.cases[i].environment.DIAG_CYGPATH, expected);
assert.equal(result.cases[i].exit, 0);
assert.equal(result.cases[i].stdout, 'mapping observation\r\n');
assert.equal(result.cases[i].stderr, '');
}
assert.equal(calls[2].options.env.PATH, '');
assert.equal(result.scratchRemoved, true); assert.deepEqual(readdirSync(root), []);
});

test('timeout retains partial stdout/stderr and cleans private scratch', async t => {
const { collect, snapshot } = await import('./windows-bash-temp-diagnostics.mjs');
const root = fixture(t);
const result = collect(snapshot({}), root, { CODEX_TEST_TRUSTED_BASH: script }, () => ({ status: null, signal: 'SIGTERM', error: new Error('ETIMEDOUT'), stdout: 'partial', stderr: 'original error' }));
assert.equal(result.cases.length, 3);
for (const c of result.cases) {
assert.equal(c.exit, null); assert.equal(c.signal, 'SIGTERM'); assert.equal(c.error, 'ETIMEDOUT');
assert.equal(c.stdout, 'partial'); assert.equal(c.stderr, 'original error');
}
assert.equal(result.scratchRemoved, true); assert.deepEqual(readdirSync(root), []);
});

test('profile setup error retains earlier observations without repairing missing parents', async t => {
const { collect, snapshot } = await import('./windows-bash-temp-diagnostics.mjs');
const root = fixture(t), missing = path.join(root, 'missing');
const result = collect(snapshot({}), missing, { CODEX_TEST_TRUSTED_BASH: script }, () => ({ status: 0, signal: null, stdout: 'recorded', stderr: 'warning' }));
assert.equal(result.cases.length, 2); assert.match(result.setupError, /ENOENT/);
assert.equal(result.cases[0].stdout, 'recorded'); assert.equal(result.cases[1].stderr, 'warning');
assert.equal(result.scratchRemoved, true); assert.equal(existsSync(missing), false);
assert.deepEqual(readdirSync(root), []);
});

test('CLI setup failure leaves existing output and directory contents unchanged', t => {
const root = fixture(t), baseline = path.join(root, 'invalid.json'), output = path.join(root, 'result.json');
writeFileSync(baseline, '{}'); writeFileSync(output, 'sentinel');
const result = cli(['collect', baseline, output]);
assert.equal(result.status, 1); assert.equal(result.stdout, '');
assert.equal(result.stderr, 'Invalid diagnostic baseline\n');
assert.equal(readFileSync(output, 'utf8'), 'sentinel');
assert.deepEqual(readdirSync(root).sort(), ['invalid.json', 'result.json']);
});
Loading
Loading