scripts/mutation-harness-utils.mjs
Rama master · réplica de lectura del repositorio original.
import {
accessSync, constants, lstatSync, readFileSync, readlinkSync,
realpathSync, mkdirSync, writeFileSync, unlinkSync, rmdirSync, renameSync,
cpSync, existsSync, mkdtempSync, rmSync, readdirSync,
} from 'node:fs';
import { execFileSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { dirname, isAbsolute, join, relative, resolve, sep, delimiter } from 'node:path';
import { tmpdir } from 'node:os';
const GIT_CANDIDATES = [
'/usr/bin/git', '/usr/local/bin/git', '/opt/homebrew/bin/git',
'/run/current-system/sw/bin/git', '/nix/var/nix/profiles/default/bin/git',
];
const executable = path => {
try { accessSync(path, constants.X_OK); return true; } catch { return false; }
};
const gitCandidate = GIT_CANDIDATES.find(executable);
if (!gitCandidate) throw new Error('mutation harness requires git at a trusted absolute path');
export const REAL_GIT = realpathSync(gitCandidate);
const SAFE_TMP_ROOT = (() => {
try {
if (lstatSync('/tmp').isDirectory()) return '/tmp';
} catch {}
return tmpdir();
})();
const SAFE_HOME = '/nonexistent/windsurfapi-harness-home';
const TRUSTED_TOOL_DIRS = [...new Set([
dirname(REAL_GIT),
dirname(process.execPath),
'/usr/bin', '/bin', '/usr/sbin', '/sbin',
].filter(path => {
try { return lstatSync(path).isDirectory(); } catch { return false; }
}))];
const TRUSTED_PATH = TRUSTED_TOOL_DIRS.join(delimiter);
/**
* Return a hermetic child environment for mutation suites.
*
* This intentionally starts from a fixed allowlist rather than copying
* process.env. Mutation tests must never inherit account credentials, proxy
* routing, application auth switches, or a real DATA_DIR from the operator's
* shell. `overrides` is retained for call-site compatibility, but ambient
* values are deliberately ignored; the suite itself can set any test fixture
* state after it starts.
*/
export function harnessEnv(_overrides = {}) {
const env = {
PATH: TRUSTED_PATH,
HOME: SAFE_HOME,
XDG_CONFIG_HOME: SAFE_HOME,
XDG_DATA_HOME: SAFE_TMP_ROOT,
XDG_CACHE_HOME: SAFE_TMP_ROOT,
LANG: 'C',
LC_ALL: 'C',
TZ: 'UTC',
TMPDIR: SAFE_TMP_ROOT,
TMP: SAFE_TMP_ROOT,
TEMP: SAFE_TMP_ROOT,
NO_COLOR: '1',
WINDSURFAPI_SKIP_DOTENV: '1',
GIT_OPTIONAL_LOCKS: '0',
GIT_NO_REPLACE_OBJECTS: '1',
GIT_TERMINAL_PROMPT: '0',
};
env.GIT_CONFIG_NOSYSTEM = '1';
env.GIT_CONFIG_GLOBAL = '/dev/null';
env.GIT_CONFIG_SYSTEM = '/dev/null';
delete env.NODE_TEST_CONTEXT;
delete env.FORCE_COLOR;
return env;
}
/**
* A machine-readable Node test reporter. Parsing the human `spec` reporter is
* unsafe: a test can print a forged `ℹ pass 999` line and make a truncated run
* look complete. The runner sends structured events to this reporter; ordinary
* test stdout/stderr is never yielded, so only the runner can author these
* records. The final (file-less) summary is the authoritative aggregate.
*/
export default async function* mutationReporter(source) {
for await (const event of source) {
if (event?.type === 'test:summary' && event.data?.file == null && event.data?.counts) {
yield `@@MUTATION_SUMMARY ${JSON.stringify(event.data)}\n`;
} else if (event?.type === 'test:fail') {
// Keep the structured failure details for infrastructure classification
// (network-deny and uncaught child failures), without exposing arbitrary
// test output as a count signal.
let data;
try { data = JSON.stringify(event.data); } catch { data = '{}'; }
yield `@@MUTATION_FAIL ${data}\n`;
}
}
}
/** Parse only records emitted by mutationReporter; never count user text. */
export function parseMutationReporterOutput(stdout = '', stderr = '', executionFailure = null) {
let summary = null;
let summaryCount = 0;
const failures = [];
for (const line of String(stdout).split('\n')) {
if (line.startsWith('@@MUTATION_SUMMARY ')) {
summaryCount++;
try {
const candidate = JSON.parse(line.slice('@@MUTATION_SUMMARY '.length));
// The reporter emits exactly one aggregate summary. Treat duplicate or
// malformed records as infrastructure failure instead of silently letting
// the last line win (a forged/partial stream must never become evidence).
if (summary == null && candidate && typeof candidate === 'object') summary = candidate;
} catch {}
} else if (line.startsWith('@@MUTATION_FAIL ')) {
try { failures.push(JSON.parse(line.slice('@@MUTATION_FAIL '.length))); } catch {}
}
}
const counts = summary?.counts || {};
const count = (name) => Number.isInteger(counts[name]) && counts[name] >= 0
? counts[name] : null;
const pass = count('passed');
const fail = count('failed');
const tests = count('tests');
const skipped = count('skipped');
const cancelled = count('cancelled');
const todo = count('todo');
const countsComplete = [pass, fail, tests, skipped, cancelled, todo].every(Number.isInteger);
const summarySuccess = summary?.success;
const summaryShape = summary != null
&& summaryCount === 1
&& countsComplete
&& (summarySuccess === true || summarySuccess === false);
const arithmetic = countsComplete
&& tests === pass + fail + skipped + cancelled + todo;
const abnormalExecution = executionFailure != null && (
executionFailure.signal != null
|| executionFailure.killed === true
|| executionFailure.status == null
|| ['ENOBUFS', 'ETIMEDOUT', 'EPIPE'].includes(executionFailure.code)
);
const failureText = `${stdout}\n${stderr}\n${failures.map(JSON.stringify).join('\n')}`;
const infrastructureFailure = !summaryShape
|| !arithmetic
|| abnormalExecution
// A successful runner followed by a non-zero process exit is an abnormal
// child termination, not a caught mutation.
|| (executionFailure?.status != null && executionFailure.status !== 0 && summarySuccess === true)
// A forged/partial reporter stream can otherwise claim success with a
// failure count (or claim failure while all tests passed). Keep the
// runner's success bit correlated with the aggregate counts.
|| (summarySuccess !== (fail === 0 && cancelled === 0))
|| /\b(?:NETWORK_)?STUB_MISS\b|unexpected git:|failureType:\s*['"](?:uncaughtException|unhandledRejection)['"]|(?:uncaughtException|unhandledRejection)\s*:/i.test(failureText);
const failedNames = failures
.map(item => typeof item?.name === 'string' ? item.name : '')
.filter(Boolean);
return {
raw: String(stdout),
pass,
fail,
tests,
skipped,
cancelled,
todo,
infrastructureFailure,
executionFailure,
summary,
summaryCount,
failedNames: [...new Set(failedNames)],
};
}
export function repoGitRaw(root, args) {
return execFileSync(REAL_GIT, args, {
cwd: root,
env: harnessEnv(),
encoding: 'utf8',
});
}
export function repoGit(root, args) {
return repoGitRaw(root, args).trim();
}
function fileDigest(path) {
let stat;
try { stat = lstatSync(path); }
catch (err) { if (err?.code === 'ENOENT') return 'missing'; throw err; }
if (stat.isSymbolicLink()) return `symlink:${stat.mode}:${readlinkSync(path)}`;
if (!stat.isFile()) return `mode:${stat.mode}:size:${stat.size}`;
return `file:${stat.mode}:sha256:${createHash('sha256').update(readFileSync(path)).digest('hex')}`;
}
/** Snapshot Git state and bytes of every tracked/untracked/ignored path except noise. */
export function workspaceSnapshot(root) {
const status = repoGitRaw(root, [
'status', '--porcelain=v1', '--untracked-files=all', '-z',
]);
// Include ignored files too: an ignored runtime file can be mutated without
// appearing in ordinary status, which would otherwise let a test pass while
// leaving owner data changed. Skip only known dependency/index trees whose
// contents are external noise rather than repository state.
const ignoredNoise = path => path.startsWith('node_modules/')
|| path.startsWith('.opencode/node_modules/')
|| path.startsWith('.codegraph/')
|| path.startsWith('.claude/worktrees/');
// `ls-files -coi` means "only ignored" and therefore drops ordinary tracked
// and untracked paths. Take the visible set and the ignored set separately;
// a dirty tracked file must be represented by bytes, not merely by its status.
const paths = [...new Set([
...repoGitRaw(root, ['ls-files', '-co', '--exclude-standard', '-z'])
.split('\0').filter(Boolean),
...repoGitRaw(root, ['ls-files', '-oi', '--exclude-standard', '-z'])
.split('\0').filter(Boolean),
])]
.filter(path => !ignoredNoise(path)).sort();
const files = paths.map(path => [path, fileDigest(resolve(root, path))]);
const head = repoGit(root, ['rev-parse', 'HEAD']);
const headSymref = (() => {
try { return repoGitRaw(root, ['symbolic-ref', '-q', 'HEAD']); } catch { return ''; }
})();
const index = createHash('sha256').update(repoGitRaw(root, ['diff', '--cached', '--binary', '--no-ext-diff'])).digest('hex');
const indexEntries = createHash('sha256').update(repoGitRaw(root, ['ls-files', '--stage', '-z'])).digest('hex');
// Do not enumerate a hand-picked set of namespaces. Tests which write
// refs/custom/*, refs/notes/*, refs/replace/*, or a future namespace must be
// observable just like heads/tags/remotes. `refs` is the complete namespace.
const refs = createHash('sha256').update(repoGitRaw(root, [
'for-each-ref', '--format=%(refname)%00%(objectname)%00%(symref)', 'refs',
])).digest('hex');
const reflogs = createHash('sha256').update(repoGitRaw(root, [
'reflog', '--all', '--date=raw', '--format=%H%x00%gD%x00%gs%x00%gd',
])).digest('hex');
const remotes = createHash('sha256').update(repoGitRaw(root, ['remote', '-v'])).digest('hex');
const gitDir = resolve(root, repoGit(root, ['rev-parse', '--git-dir']));
const commonDir = resolve(root, repoGit(root, ['rev-parse', '--git-common-dir']));
const config = fileDigest(join(gitDir, 'config'));
const adminNames = [
'HEAD', 'FETCH_HEAD', 'ORIG_HEAD', 'MERGE_HEAD', 'CHERRY_PICK_HEAD',
'REVERT_HEAD', 'BISECT_HEAD', 'BISECT_LOG', 'BISECT_NAMES',
'BISECT_START', 'BISECT_EXPECTED_REV', 'AUTO_MERGE', 'REBASE_HEAD',
'MERGE_MSG', 'SQUASH_MSG', 'packed-refs', 'shallow', 'index.lock',
'config', 'config.worktree', 'commondir', 'gitdir',
];
const admin = [...new Set([gitDir, commonDir])].flatMap(dir => adminNames.map(name => [
`${dir}/${name}`, fileDigest(join(dir, name)),
]));
// The named list above covers known pseudorefs. This second digest covers
// future pseudorefs, sequencer/rebase state, hooks, info/exclude, lock files,
// and custom ref directories without descending into the object database.
const adminTrees = [...new Set([gitDir, commonDir])].map(dir => [
`${dir}/admin`, directoryDigest(dir, (relPath) => {
const top = relPath.split('/')[0];
// The raw index file contains a mutable stat cache. A correct
// `git checkout HEAD -- <target>` restore can refresh those cache bytes
// even when every staged entry and cached diff is identical. Index
// semantics are already captured above by `ls-files --stage` plus
// `diff --cached`; keep `index.lock` visible, but ignore only the raw
// cache-bearing `index` file here.
return top === 'objects' || top === 'refs' || top === 'logs' || relPath === 'index';
}),
`${dir}/refs`, directoryDigest(join(dir, 'refs')),
`${dir}/logs`, directoryDigest(join(dir, 'logs')),
]);
const logs = [...new Set([gitDir, commonDir])].map(dir => [
`${dir}/logs`, directoryDigest(join(dir, 'logs')),
]);
return JSON.stringify({
status, head, headSymref, index, indexEntries, refs, reflogs, remotes, config,
admin, adminTrees, logs, files,
});
}
function directoryDigest(path, skip = () => false) {
const hash = createHash('sha256');
let rootStat;
try { rootStat = lstatSync(path); }
catch (err) {
if (err?.code !== 'ENOENT') throw err;
hash.update('missing\0');
return hash.digest('hex');
}
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
hash.update(`${fileDigest(path)}\0`);
return hash.digest('hex');
}
// Missing, empty and mode-changed directories are distinct states. The old
// empty hash made creation of `.git/sequencer/`, an empty ref namespace, or
// another administrative directory invisible to the side-effect guard.
hash.update(`directory:${rootStat.mode}\0`);
const walk = (current, relPath = '') => {
let entries;
try { entries = readdirSync(current, { withFileTypes: true }); }
catch (err) { if (err?.code === 'ENOENT') { hash.update('vanished\0'); return; } throw err; }
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
const child = join(current, entry.name);
const childRel = relPath ? `${relPath}/${entry.name}` : entry.name;
if (skip(childRel, entry)) continue;
let childStat;
try { childStat = lstatSync(child); }
catch (err) {
if (err?.code === 'ENOENT') { hash.update(`${childRel}\0vanished\0`); continue; }
throw err;
}
hash.update(`${childRel}\0mode:${childStat.mode}\0`);
if (childStat.isDirectory() && !childStat.isSymbolicLink()) walk(child, childRel);
else hash.update(`${fileDigest(child)}\0`);
}
};
walk(path);
return hash.digest('hex');
}
/**
* Materialize the current checkout into a disposable, self-contained clone.
*
* Mutation suites are allowed to exercise Git, ignored files, fixture remotes,
* and arbitrary test-created state. Running them in the Owner checkout and
* merely comparing a post-run hash is not isolation: a non-target side effect
* has no saved bytes to restore. This helper clones the committed history,
* overlays every visible current tracked/untracked file (except Owner-only
* OpenCode and legacy Claude worktree state), and gives the clone a temporary
* local origin. The caller owns cleanup().
*/
export function materializeMutationWorkspace(sourceRoot) {
const parent = mkdtempSync(join(SAFE_TMP_ROOT, 'windsurfapi-mutation-workspace-'));
const scratch = join(parent, 'repo');
const remote = join(parent, 'origin.git');
const run = (args, options = {}) => execFileSync(REAL_GIT, args, {
cwd: options.cwd || scratch,
env: harnessEnv(),
encoding: 'utf8',
stdio: options.stdio || ['ignore', 'pipe', 'pipe'],
});
const forbidden = path => path === '.opencode' || path.startsWith('.opencode/')
|| path === '.claude/worktrees' || path.startsWith('.claude/worktrees/');
const requiredHarnessFiles = [
'scripts/mutation-harness-utils.mjs',
'scripts/mutation-network-deny.mjs',
];
try {
execFileSync(REAL_GIT, ['clone', '--local', '--no-hardlinks', sourceRoot, scratch], {
cwd: parent, env: harnessEnv(), encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
});
const visible = new Set(repoGitRaw(sourceRoot, ['ls-files', '-co', '--exclude-standard', '-z'])
.split('\0').filter(Boolean).filter(path => !forbidden(path)));
// These two files are the evidence boundary itself. Copy current Owner
// bytes even when an older HEAD or ignore rule would otherwise omit them,
// and fail closed when either preload/reporter is unavailable.
for (const path of requiredHarnessFiles) {
try { assertContainedRegularFile(sourceRoot, path); }
catch { throw new Error(`materialized mutation workspace requires regular ${path}`); }
visible.add(path);
}
for (const path of visible) {
const source = resolve(sourceRoot, path);
if (!existsSync(source)) continue; // deleted tracked paths are removed below
const target = resolve(scratch, path);
mkdirSync(dirname(target), { recursive: true });
cpSync(source, target, { dereference: false, force: true });
}
const deleted = repoGitRaw(sourceRoot, ['ls-files', '-d', '-z'])
.split('\0').filter(Boolean).filter(path => !forbidden(path));
for (const path of deleted) rmSync(resolve(scratch, path), { force: true, recursive: true });
// `git clone` materializes tracked HEAD before the overlay. Filtering only
// overlay paths therefore is not enough: a historically tracked Owner state
// directory would survive in the disposable clone. Remove both forbidden
// trees explicitly, whether they came from HEAD, the index, or current bytes.
rmSync(join(scratch, '.opencode'), { recursive: true, force: true });
const claudeRoot = join(scratch, '.claude');
try {
const claudeStat = lstatSync(claudeRoot);
if (claudeStat.isSymbolicLink()) {
// Never traverse a checkout-controlled parent symlink while deleting.
rmSync(claudeRoot, { force: true });
} else if (claudeStat.isDirectory()) {
rmSync(join(claudeRoot, 'worktrees'), { recursive: true, force: true });
}
} catch (err) {
if (err?.code !== 'ENOENT') throw err;
}
// The Owner's OpenCode state is intentionally never copied or committed.
mkdirSync(join(scratch, '.git', 'info'), { recursive: true });
writeFileSync(join(scratch, '.git', 'info', 'exclude'), '.opencode/\n.claude/worktrees/\n', { encoding: 'utf8' });
run(['config', 'user.name', 'WindsurfAPI mutation harness']);
run(['config', 'user.email', 'mutation-harness@example.invalid']);
// The preload and reporter must also exist in the synthetic restore point,
// even if an older checkout's ignore rules hide a newly introduced file.
run(['add', '-f', '--', ...requiredHarnessFiles]);
run(['add', '-A']);
try { run(['diff', '--cached', '--quiet'], { stdio: 'ignore' }); }
catch { run(['commit', '-m', 'mutation harness materialized workspace']); }
execFileSync(REAL_GIT, ['init', '--bare', remote], {
cwd: parent, env: harnessEnv(), encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
});
run(['remote', 'set-url', 'origin', remote]);
run(['push', '--force', 'origin', 'HEAD:refs/heads/master']);
run(['fetch', 'origin', 'master']);
return {
root: scratch,
cleanup: () => rmSync(parent, { recursive: true, force: true }),
};
} catch (err) {
rmSync(parent, { recursive: true, force: true });
throw err;
}
}
function pidAlive(pid) {
try { process.kill(pid, 0); return true; }
catch (err) { return err?.code !== 'ESRCH'; }
}
/** Acquire one repository-wide mutation lock; cooperative parallel runs fail closed. */
export function acquireMutationLock(root) {
const commonDirRaw = repoGit(root, ['rev-parse', '--git-common-dir']);
const commonDir = resolve(root, commonDirRaw);
const lockDir = join(commonDir, 'codex', 'mutation-harness.lock');
mkdirSync(dirname(lockDir), { recursive: true });
const owner = `${process.pid}\n`;
let createdLockDir = false;
try {
mkdirSync(lockDir, { mode: 0o700 });
createdLockDir = true;
writeFileSync(join(lockDir, 'owner'), owner, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
} catch (err) {
let stale = false;
try {
const pid = Number(readFileSync(join(lockDir, 'owner'), 'utf8').trim());
stale = Number.isSafeInteger(pid) && pid > 0 && !pidAlive(pid);
} catch {}
if (!stale) {
// If mkdir succeeded but owner creation failed for an unrelated reason,
// do not leave an empty lock directory that blocks every future run. Only
// remove a directory proven to have been created by this process and only
// when it still has no owner file.
if (createdLockDir) {
try { accessSync(join(lockDir, 'owner')); }
catch { try { rmdirSync(lockDir); } catch {} }
}
throw new Error(`mutation harness lock is already held: ${lockDir}`);
}
// Claim recovery atomically before touching the stale directory. Without
// O_EXCL two contenders can both observe the dead owner; one can recreate
// a fresh lock while the other removes that new owner's directory (ABA).
const claimPath = join(lockDir, 'recovery');
try {
writeFileSync(claimPath, `${process.pid}\n`, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
} catch {
throw new Error(`could not claim stale mutation harness lock: ${lockDir}`);
}
let verifyPid = 0;
try {
verifyPid = Number(readFileSync(join(lockDir, 'owner'), 'utf8').trim());
} catch {
throw new Error(`could not verify stale mutation harness lock: ${lockDir}`);
}
if (!Number.isSafeInteger(verifyPid) || verifyPid <= 0 || pidAlive(verifyPid)) {
throw new Error(`mutation harness lock changed during stale recovery: ${lockDir}`);
}
const quarantine = `${lockDir}.stale-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
try {
renameSync(lockDir, quarantine);
unlinkSync(join(quarantine, 'owner'));
unlinkSync(join(quarantine, 'recovery'));
rmdirSync(quarantine);
} catch {
throw new Error(`could not quarantine stale mutation harness lock: ${lockDir}`);
}
mkdirSync(lockDir, { mode: 0o700 });
writeFileSync(join(lockDir, 'owner'), owner, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
}
let released = false;
const release = () => {
if (released) return;
released = true;
try {
if (readFileSync(join(lockDir, 'owner'), 'utf8') !== owner) return;
unlinkSync(join(lockDir, 'owner'));
rmdirSync(lockDir);
} catch {}
};
process.once('exit', release);
return release;
}
export function assertContainedRegularFile(root, file) {
if (typeof file !== 'string' || !file || file.startsWith('-') || file.includes('\0')) throw new Error(`invalid mutation target: ${file}`);
// macOS commonly exposes temporary directories through `/var` while
// realpath returns `/private/var`. Canonicalize the root as well as the
// target or a valid in-repo file is falsely classified as escaping.
const canonicalRoot = realpathSync(root);
const lexical = resolve(canonicalRoot, file);
const rel = relative(canonicalRoot, lexical);
if (!rel || rel === '..' || rel.startsWith(`..${sep}`) || /^[A-Za-z]:[\\/]/.test(rel)) {
throw new Error(`mutation target escapes repository: ${file}`);
}
const stat = lstatSync(lexical);
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`mutation target is not a regular file: ${file}`);
const real = relative(canonicalRoot, realpathSync(lexical));
if (!real || real === '..' || real.startsWith(`..${sep}`) || /^[A-Za-z]:[\\/]/.test(real)) {
throw new Error(`mutation target resolves outside repository: ${file}`);
}
return lexical;
}