Source: database/repositories/testFixtureRepo.js

/**
 * @module database/repositories/testFixtureRepo
 * @description Data-driven test fixture CRUD (CAP-001).
 *
 * A fixture is a CSV / JSON row-set bound to a specific test version. The
 * runner replays the test once per row, substituting `{{column}}` placeholders
 * in `playwrightCode` from the row values. Rows are persisted as a JSON blob
 * in the `rows` column (parsed lazily on read via `rowToFixture`); the column
 * is `TEXT` rather than the dialect-specific `JSON` type so the same SQL
 * works on SQLite and Postgres without an adapter shim.
 *
 * Rows are keyed on `(testId, version)` where `version` mirrors
 * `tests.codeVersion` — a new code version (e.g. after an AI fix bumps the
 * test body) starts with a fresh fixture slot. Old fixtures stay around for
 * run-history replay rather than being cascade-deleted on version bump.
 */

import { getDatabase } from "../sqlite.js";

/**
 * Map a raw `test_fixtures` row to its public shape, parsing the JSON `rows`
 * column into an array. Returns `undefined` for missing rows so callers can
 * use the same `if (!fixture) return …` pattern as the sibling repos.
 *
 * @param {Object|undefined} row
 * @returns {Object|undefined}
 */
function rowToFixture(row) {
  if (!row) return undefined;
  return { ...row, rows: row.rows ? JSON.parse(row.rows) : [] };
}

/**
 * Upsert a fixture row. Creates on first call, replaces on subsequent calls
 * at the same `(testId, version)` — re-uploading at the same code version is
 * intentionally a replace rather than an append so the fixture panel's
 * history table never grows unbounded for a single version.
 *
 * @param {Object} entry
 * @param {string} entry.testId
 * @param {number} entry.version
 * @param {"csv"|"json"} entry.format
 * @param {Array<Object>} entry.rows
 * @returns {Object} The freshly persisted fixture row.
 */
export function upsertFixture({ testId, version, format, rows }) {
  const db = getDatabase();
  const createdAt = new Date().toISOString();
  db.prepare(`
    INSERT INTO test_fixtures (testId, version, format, rows, createdAt)
    VALUES (@testId, @version, @format, @rows, @createdAt)
    ON CONFLICT(testId, version) DO UPDATE SET
      format = excluded.format,
      rows = excluded.rows,
      createdAt = excluded.createdAt
  `).run({
    testId,
    version,
    format,
    rows: JSON.stringify(rows || []),
    createdAt,
  });
  return getFixture(testId, version);
}

/**
 * Get a fixture row for a specific test + version.
 *
 * @param {string} testId
 * @param {number} version
 * @returns {Object|undefined}
 */
export function getFixture(testId, version) {
  const db = getDatabase();
  return rowToFixture(
    db.prepare("SELECT * FROM test_fixtures WHERE testId = ? AND version = ?").get(testId, version),
  );
}

/**
 * List all fixtures for a test, newest version first. Powers the history
 * table in the Test Detail fixture panel.
 *
 * @param {string} testId
 * @returns {Array<Object>}
 */
export function listFixtures(testId) {
  const db = getDatabase();
  return db.prepare(
    "SELECT * FROM test_fixtures WHERE testId = ? ORDER BY version DESC",
  ).all(testId).map(rowToFixture);
}