Ctrl K
ComunidadGuías…

scripts/mutation-harness-utils.mjs

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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;
}