Ctrl K
ComunidadGuías…

seed-fixture.mjs

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

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env node
/**
 * seed-fixture.mjs — materialize a realistic user-data fixture into an install.
 *
 * Fixture states live in test-fixtures/upgrade/<state>/ and mirror the
 * user-layer files a real install of that era contains. Returns a SHA-256
 * manifest so callers can assert byte-identity later.
 *
 * Usage:
 *   node seed-fixture.mjs <targetDir> [--state state-v1.18]
 *   node seed-fixture.mjs --self-test
 */
import { createHash } from 'crypto';
import { cpSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, existsSync, mkdirSync } from 'fs';
import { tmpdir } from 'os';
import { join, dirname, relative, sep } from 'path';
import { fileURLToPath } from 'url';
import { isMainModule } from './lib/is-main-module.mjs';
import { walkTree, isNestedCheckout } from './lib/walk-tree.mjs';

const ROOT = dirname(fileURLToPath(import.meta.url));
const FIXTURES = join(ROOT, 'test-fixtures', 'upgrade');
export const DEFAULT_STATE = 'state-v1.18';

function walk(dir, base = dir) {
  // walkTree sorts every level, so files[] and the manifest key order are
  // deterministic across platforms (readdir returns filesystem order, which
  // varies). `links: 'follow'` keeps the statSync semantics this had.
  return walkTree(dir, { links: 'follow' })
    .map((p) => relative(base, p).split(sep).join('/'));
}

export function listStates() {
  if (!existsSync(FIXTURES)) return [];
  // Sort for deterministic state ordering across platforms (readdirSync
  // returns filesystem order, which varies).
  //
  // A nested checkout is filtered here rather than by walkTree, because these
  // entries are not walked — each becomes a walk ROOT, which walkTree
  // deliberately never tests (a gate run from inside a worktree must check that
  // worktree). A stray clone or worktree parked among the fixture states would
  // otherwise become an allowlisted `--state` value pointing at a whole second
  // repository (#3762).
  return readdirSync(FIXTURES).sort()
    .filter((n) => statSync(join(FIXTURES, n)).isDirectory() && !isNestedCheckout(join(FIXTURES, n)));
}

// Strict allowlist: a state must be one of the real fixture subdirectories.
// Membership in listStates() (basenames only) inherently rejects both unknown
// states and traversal strings ('../x', 'state/../..'), so no join() ever
// escapes FIXTURES.
function assertKnownState(state) {
  const states = listStates();
  if (!states.includes(state)) {
    throw new Error(`Unknown fixture state: ${state} (have: ${states.join(', ') || 'none'})`);
  }
}

export function seedFixture(targetDir, { state = DEFAULT_STATE } = {}) {
  if (!existsSync(targetDir) || !statSync(targetDir).isDirectory()) throw new Error(`targetDir is not an existing directory: ${targetDir}`);
  assertKnownState(state);
  const src = join(FIXTURES, state);
  // expected.json is harness metadata, not user data — never seed it.
  const files = walk(src).filter((f) => f !== 'expected.json');
  const manifest = {};
  for (const f of files) {
    const dest = join(targetDir, f);
    mkdirSync(dirname(dest), { recursive: true });
    cpSync(join(src, f), dest);
    manifest[f] = createHash('sha256').update(readFileSync(dest)).digest('hex');
  }
  return { state, files, manifest };
}

export function loadExpectations(state = DEFAULT_STATE) {
  assertKnownState(state);
  return JSON.parse(readFileSync(join(FIXTURES, state, 'expected.json'), 'utf-8'));
}

function selfTest() {
  let failed = 0;
  const check = (ok, msg) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${msg}`); if (!ok) failed++; };
  const states = listStates();
  // Guard against a vacuous pass: zero fixture states would run zero checks and
  // still print "green". listStates() legitimately returns [] for discovery, so
  // the assertion lives here in the test, not in listStates() itself.
  check(states.length > 0, `discovered at least one fixture state (found ${states.length})`);
  for (const state of states) {
    const dir = mkdtempSync(join(tmpdir(), 'seed-fixture-'));
    try {
      const { files, manifest } = seedFixture(dir, { state });
      check(files.length >= 8, `${state}: seeds ${files.length} files (>=8)`);
      check(!files.includes('expected.json'), `${state}: expected.json not seeded`);
      const REQUIRED = ['cv.md', 'config/profile.yml', 'modes/_profile.md', 'portals.yml', 'data/applications.md'];
      for (const r of REQUIRED) check(files.includes(r), `${state}: required file seeded: ${r}`);
      const again = seedFixture(dir, { state });
      check(JSON.stringify(again.manifest) === JSON.stringify(manifest), `${state}: manifest is deterministic`);
      const exp = loadExpectations(state);
      check(Number.isInteger(exp.tracker_rows) && exp.tracker_rows > 0, `${state}: expected.json parses with tracker_rows`);
      const tracker = readFileSync(join(dir, 'data/applications.md'), 'utf-8');
      const rows = tracker.split('\n').filter((l) => /^\|\s*\d+\s*\|/.test(l)).length;
      check(rows === exp.tracker_rows, `${state}: tracker has ${rows} rows == expected ${exp.tracker_rows}`);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  }
  console.log(failed ? `${failed} check(s) failed` : 'seed-fixture self-test green');
  process.exit(failed ? 1 : 0);
}

if (isMainModule(import.meta.url)) {
  const args = process.argv.slice(2);
  if (args.includes('--help') || args.includes('-h')) {
    console.log(`Usage:
  node seed-fixture.mjs <targetDir> [--state name]
  node seed-fixture.mjs --self-test
  node seed-fixture.mjs --help

Options:
  --state <name>  Fixture state to materialize (default: state-v1.18)
  --self-test     Run internal fixture integrity checks
  --help, -h      Print this usage block and exit`);
    process.exit(0);
  }
  if (args[0] === '--self-test') selfTest();
  else if (args[0] && !args[0].startsWith('--')) {
    const stateIdx = args.indexOf('--state');
    if (stateIdx > -1 && (args[stateIdx + 1] === undefined || args[stateIdx + 1].startsWith('--'))) {
      console.error('Missing value for --state');
      process.exit(1);
    }
    try {
      const res = seedFixture(args[0], stateIdx > -1 ? { state: args[stateIdx + 1] } : {});
      console.log(JSON.stringify(res, null, 2));
    } catch (err) {
      // Concise, actionable message — never leak a raw stack trace to the CLI.
      console.error(`seed-fixture: ${err.message}`);
      process.exit(1);
    }
  } else {
    console.error('Usage: node seed-fixture.mjs <targetDir> [--state name] | --self-test');
    process.exit(1);
  }
}