// Run only with Sage's separate mutant CLEAR. Each source mutation restores in finally.
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { spawnSync } from 'node:child_process';
const root = process.cwd(), home = path.join(root, 'agents/magica/work/queue-331');
const file = path.join(root, 'packages/bus/src/model-keys.mjs'), original = fs.readFileSync(file, 'utf8');
const cases = [
  { name: 'revoke-skips-confirming-GET', from: "await this.#replace(before, before.filter(k => !keys.includes(k)));", to: "this.#sentinel(before); await this.#client.putKeys(before.filter(k => !keys.includes(k)));", failure: 'revoke confirms absence then polls delayed client refusal before local removal' },
  { name: 'reconcile-removes-foreign', from: "const removed = before.filter(k => k.startsWith(this.#prefix) && !k.startsWith(this.#prefix + 'sentinel-') && !retained.includes(k));", to: "const removed = before.filter(k => !k.startsWith(this.#prefix + 'sentinel-') && !retained.includes(k));", failure: 'reconcile restores missing recorded keys, removes own unknown, and preserves foreign and sentinel' },
  { name: 'preflight-accepts-401', from: "if (status < 200 || status >= 300) fail('proxy-key-refused');", to: "if (status !== 401 && (status < 200 || status >= 300)) fail('proxy-key-refused');", failure: 'proxy-key-refused: preflight rejects HTTP 401 with exactly one probe' },
  { name: 'reconcile-ignores-revoking', from: "const retained = records.filter(e => e.record.state !== 'revoking').flatMap(e => [e.record.key, ...e.record.retiring.map(r => r.key)]);\n        const revoking = records.filter(e => e.record.state === 'revoking');", to: "const retained = records.flatMap(e => [e.record.key, ...e.record.retiring.map(r => r.key)]);\n        const revoking = [];", failure: 'reconcile revoking never restores key and completes delayed revocation after restart' },
];
os.setPriority(0, 19);
const baseEnv = { ...process.env };
for (const name of Object.keys(baseEnv)) if (/(TOKEN|SECRET|PASSWORD|API_KEY|AUTH_SOCK|BUS_CAP)/.test(name)) delete baseEnv[name];
try {
  for (const mutant of cases) {
    if (original.split(mutant.from).length !== 2) throw Error('mutation anchor not unique: ' + mutant.name);
    fs.writeFileSync(file, original.replace(mutant.from, mutant.to));
    const scratch = spawnSync('mktemp', ['-d', '/mnt/storage/scratch/tmp/XXXXXX'], { encoding: 'utf8' }).stdout.trim();
    if (!/^\/mnt\/storage\/scratch\/tmp\/[A-Za-z0-9]{6}$/.test(scratch)) throw Error('short scratch creation failed');
    fs.mkdirSync(path.join(scratch, 'home')); fs.mkdirSync(path.join(scratch, 'config'));
    const log = spawnSync(process.execPath, ['--test', '--test-reporter=tap', path.join(root, 'packages/bus/tests/model-keys.test.mjs')], {
      cwd: root, encoding: 'utf8', timeout: 180000, maxBuffer: 8 * 1024 * 1024,
      env: { ...baseEnv, TMPDIR: scratch, HOME: path.join(scratch, 'home'), MOSAIC_CONFIG: path.join(scratch, 'config/config.json') },
    });
    const output = (log.stdout ?? '') + (log.stderr ?? '');
    fs.writeFileSync(path.join(home, 'mutant-' + mutant.name + '.log'), output);
    const namedFailure = output.split('\n').some(line => /^not ok /.test(line) && line.includes(mutant.failure));
    if (log.status === 0 || !namedFailure || log.error) throw Error('mutant did not fail its named behavior: ' + mutant.name);
    process.stdout.write(mutant.name + ' killed; nice=' + os.getPriority(0) + '\n');
    fs.writeFileSync(file, original);
  }
} finally { fs.writeFileSync(file, original); }
