Ctrl K
ComunidadGuías…

scripts/mutate-verify-dirty.mjs

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

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env node
//
// Mutation verification WITHOUT requiring a clean tree.
//
// Use this while a fix is still uncommitted. It materializes the current dirty
// bytes into a disposable clone, commits that synthetic tree locally, and runs
// every mutation there. The Owner checkout is never a mutation target, so a
// killed process, a test that writes another file/ref/config entry, or a failed
// restore cannot destroy the work being verified. `.opencode/`, legacy
// `.claude/worktrees/`, and ignored runtime data are never copied into it.
//
// Usage:
//   node scripts/mutate-verify-dirty.mjs <spec.json>
//
// Spec format is identical to mutate-verify.mjs. Exit code 0 only when every
// mutation matched its expectation.

import {
  readFileSync, writeFileSync,
} from 'node:fs';
import { execFileSync } from 'node:child_process';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
  harnessEnv, workspaceSnapshot, acquireMutationLock, assertContainedRegularFile,
  materializeMutationWorkspace, repoGit, parseMutationReporterOutput,
} from './mutation-harness-utils.mjs';

const SOURCE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
let ROOT = SOURCE_ROOT;
const specPath = process.argv[2];
if (!specPath) {
  console.error('usage: node scripts/mutate-verify-dirty.mjs <spec.json>');
  process.exit(2);
}

const spec = JSON.parse(readFileSync(specPath, 'utf8'));
if (!Array.isArray(spec.tests) || spec.tests.length === 0) {
  console.error('RESULT=SPEC_FAILURE tests must be a non-empty array');
  process.exit(2);
}
if (!Array.isArray(spec.mutations) || spec.mutations.length === 0) {
  console.error('RESULT=SPEC_FAILURE mutations must be a non-empty array');
  process.exit(2);
}
for (const testPath of spec.tests) {
  if (typeof testPath !== 'string' || testPath.startsWith('-')) {
    console.error(`RESULT=SPEC_FAILURE invalid test path: ${testPath}`);
    process.exit(2);
  }
}
const files = [...new Set(spec.mutations.map(m => m.file))];

const assertMutableTarget = (file, root = ROOT) => assertContainedRegularFile(root, file);

// Guard 3's equivalent, and it runs FIRST. An anchor matching zero times makes
// `.replace()` a no-op, and a no-op mutation necessarily SURVIVES — which reads as
// "the guard has a hole" when nothing was ever mutated.
let fatal = false;
for (const m of spec.mutations) {
  const target = assertMutableTarget(m.file, SOURCE_ROOT);
  const src = readFileSync(target, 'utf8');
  const hits = src.split(m.anchor).length - 1;
  if (hits !== 1) {
    console.log(`ANCHOR_BAD hits=${hits} :: ${m.name.slice(0, 70)}`);
    fatal = true;
  }
}
if (fatal) { console.log('RESULT=ANCHOR_FAILURE'); process.exit(2); }

// The lock belongs to the Owner repository, not the disposable clone.  Taking it
// only after materialization lets two dirty runs clone the same Owner and then
// acquire different `.git/` locks concurrently.
let releaseMutationLock;
try { releaseMutationLock = acquireMutationLock(SOURCE_ROOT); }
catch (err) { console.log(`RESULT=LOCK_FAILURE ${err.message}`); process.exit(2); }

let materialized;
try { materialized = materializeMutationWorkspace(SOURCE_ROOT); }
catch (err) {
  console.log(`RESULT=WORKSPACE_FAILURE ${err.message}`);
  process.exit(2);
}
ROOT = materialized.root;
process.once('exit', materialized.cleanup);

// Re-check anchors on the exact materialized bytes which the test child will
// execute. The source checkout is never mutated by this harness.
for (const m of spec.mutations) {
  const target = assertMutableTarget(m.file);
  const src = readFileSync(target, 'utf8');
  const hits = src.split(m.anchor).length - 1;
  if (hits !== 1) {
    console.log(`ANCHOR_BAD_MATERIALIZED hits=${hits} :: ${m.name.slice(0, 70)}`);
    fatal = true;
  }
}
if (fatal) { console.log('RESULT=ANCHOR_FAILURE'); process.exit(2); }

function runTests() {
  const args = [
    '--import', './scripts/mutation-network-deny.mjs',
    '--import', './test/setup-env.mjs',
    '--test-reporter=./scripts/mutation-harness-utils.mjs',
    '--test', ...spec.tests,
  ];
  try {
    return {
      stdout: execFileSync(process.execPath, args, {
        cwd: ROOT, env: harnessEnv(), encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
        timeout: 15 * 60 * 1000,
        maxBuffer: 64 * 1024 * 1024,
      }),
      stderr: '',
      executionFailure: null,
    };
  } catch (e) {
    return {
      stdout: e.stdout || '',
      stderr: e.stderr || '',
      executionFailure: { status: e.status, signal: e.signal, killed: e.killed === true, code: e.code },
    };
  }
}

// Only records emitted by the structured reporter count.  Test-authored TAP,
// ANSI text, and a partial stream are intentionally ignored or fail closed.
const countsWithEvidence = ({ stdout, stderr, executionFailure }) =>
  parseMutationReporterOutput(stdout, stderr, executionFailure);

// Guard 2's equivalent: a SURVIVED verdict means nothing unless the suite was
// green first, at the exact count the spec declares.
const initialTree = workspaceSnapshot(ROOT);
const base = countsWithEvidence(runTests());
console.log(`BASELINE pass=${base.pass} fail=${base.fail} tests=${base.tests} declared=${spec.expectBaselinePass ?? '(none)'}`);
if (base.fail !== 0 || base.tests === 0 || base.infrastructureFailure
    || base.tests !== base.pass || base.skipped !== 0 || base.cancelled !== 0 || base.todo !== 0) {
  console.log('RESULT=BASELINE_UNTRUSTWORTHY');
  process.exit(2);
}
if (spec.expectBaselinePass != null && base.pass !== spec.expectBaselinePass) {
  console.log('RESULT=BASELINE_MISMATCH');
  process.exit(2);
}
if (workspaceSnapshot(ROOT) !== initialTree) {
  console.log('RESULT=BASELINE_SIDE_EFFECT');
  process.exit(2);
}

// The restore point is the synthetic HEAD in the disposable workspace. Unlike
// the old in-place cp backup, this covers every mutation target and lets the
// entire workspace be discarded after any non-target side effect.
const baselineTree = initialTree;
const restore = () => {
  for (const f of files) repoGit(ROOT, ['checkout', 'HEAD', '--', f]);
};
if (workspaceSnapshot(ROOT) !== baselineTree) {
  console.log('RESULT=BASELINE_SIDE_EFFECT');
  process.exit(2);
}

let restoring = false;
const emergency = (why) => {
  if (restoring) return;
  restoring = true;
  try { restore(); console.log(`EMERGENCY_RESTORE ok (${why})`); }
  catch (e) { console.log(`EMERGENCY_RESTORE FAILED (${why}): ${e.message}; disposable workspace will be removed`); }
  process.exit(130);
};
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(sig, () => emergency(sig));
process.on('uncaughtException', (e) => emergency(`uncaught: ${e.message}`));

let bad = 0;
for (const m of spec.mutations) {
  const p = assertMutableTarget(m.file);
  writeFileSync(p, readFileSync(p, 'utf8').replace(m.anchor, m.replacement));
  let r;
  try { r = countsWithEvidence(runTests()); }
  finally { restore(); }
  const complete = r.tests === base.tests
    && r.pass + r.fail === base.tests
    && r.skipped === 0 && r.cancelled === 0 && r.todo === 0;
  const trustworthy = complete && !r.infrastructureFailure;
  if (workspaceSnapshot(ROOT) !== baselineTree) {
    console.log(`UNEXPECTED SIDE_EFFECT after ${m.name}`);
    console.log('RESULT=MUTATION_SIDE_EFFECT');
    process.exit(2);
  }
  if (!trustworthy) {
    console.log(`UNEXPECTED UNTRUSTWORTHY fail=${r.fail} pass=${r.pass} tests=${r.tests}/${base.tests}${r.infrastructureFailure ? ' infra=1' : ''} :: ${m.name.slice(0, 90)}`);
    console.log('RESULT=MUTATION_UNTRUSTWORTHY');
    process.exit(2);
  }
  const caught = r.fail > 0;
  const ok = caught === (m.expectCaught !== false);
  if (!ok) bad++;
  const verdict = caught ? 'CAUGHT' : 'SURVIVED';
  console.log(`${ok ? 'OK' : 'UNEXPECTED'} ${verdict} fail=${r.fail} pass=${r.pass} tests=${r.tests}/${base.tests}${r.infrastructureFailure ? ' infra=1' : ''} :: ${m.name.slice(0, 90)}`);
}
restore();
if (workspaceSnapshot(ROOT) !== baselineTree) {
  console.log('RESULT=FINAL_SIDE_EFFECT');
  process.exit(2);
}
releaseMutationLock?.();
console.log(`RESULT=${bad === 0 ? 'ALL_AS_EXPECTED' : `${bad}_UNEXPECTED`} total=${spec.mutations.length}`);
process.exit(bad === 0 ? 0 : 1);