import { afterEach, describe, expect, test } from "bun:test";
import { Database } from "bun:sqlite";
import { copyFileSync, mkdtempSync, rmSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
  COMPLETED_SESSION_LIMIT,
  createFactoryAdapter as createFactorySummaryAdapter,
  loadFactoryRunDetail,
  type FactoryAdapterOptions,
  type FactoryPanelData,
  type FactoryRunView,
} from "./factory";

const SCHEMA = `
CREATE TABLE sessions (
  adw_id        TEXT PRIMARY KEY,
  adw_name      TEXT,
  repo          TEXT,
  repo_name     TEXT,
  run_slug      TEXT,
  request       TEXT,
  status        TEXT,
  engineer      TEXT,
  started_at    TEXT,
  ended_at      TEXT,
  total_tokens  INTEGER DEFAULT 0,
  total_cost    REAL DEFAULT 0,
  archived      INTEGER DEFAULT 0
);
CREATE TABLE phases (
  phase_id      TEXT PRIMARY KEY,
  adw_id        TEXT REFERENCES sessions,
  seq           INTEGER,
  name TEXT, kind TEXT, owner TEXT, description TEXT,
  status        TEXT DEFAULT 'fail',
  attempt       INTEGER DEFAULT 0,
  retries       INTEGER DEFAULT 0,
  error         TEXT,
  started_at    TEXT,
  ended_at      TEXT
);
CREATE TABLE events (
  event_id      TEXT PRIMARY KEY,
  adw_id        TEXT REFERENCES sessions,
  phase_id      TEXT REFERENCES phases,
  parent_id     TEXT,
  type          TEXT,
  name          TEXT,
  payload_json  TEXT,
  tokens        INTEGER,
  started_at    TEXT,
  ended_at      TEXT
);
CREATE TABLE envelopes (
  envelope_id   TEXT PRIMARY KEY,
  adw_id        TEXT REFERENCES sessions,
  phase_id      TEXT REFERENCES phases,
  agent         TEXT,
  output_type   TEXT,
  payload_json  TEXT,
  valid         INTEGER,
  attempt       INTEGER,
  created_at    TEXT
);
CREATE TABLE gate_results (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  adw_id        TEXT REFERENCES sessions,
  phase_id      TEXT REFERENCES phases,
  attempt       INTEGER,
  gate          TEXT,
  passed        INTEGER,
  violations_json TEXT,
  checks_json   TEXT,
  created_at    TEXT
);
CREATE TABLE phase_diffs (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  adw_id        TEXT NOT NULL,
  phase_id      TEXT NOT NULL,
  attempt       INTEGER,
  files_json    TEXT,
  insertions    INTEGER,
  deletions     INTEGER,
  diff_text     TEXT,
  truncated     INTEGER,
  created_at    TEXT NOT NULL
);
CREATE TABLE processes (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  adw_id        TEXT REFERENCES sessions,
  kind          TEXT,
  name          TEXT,
  pid           INTEGER,
  command       TEXT,
  started_at    TEXT,
  ended_at      TEXT
);
CREATE TABLE agent_sessions (
  adw_id        TEXT REFERENCES sessions,
  agent         TEXT,
  coding_agent  TEXT,
  model         TEXT,
  color         TEXT,
  session_id    TEXT,
  context_tokens INTEGER,
  context_window INTEGER,
  created_at    TEXT,
  last_used_at  TEXT,
  PRIMARY KEY (adw_id, agent)
);
CREATE TABLE agent_attempts (
  attempt_id TEXT PRIMARY KEY,
  adw_id TEXT,
  phase_id TEXT,
  parent_id TEXT,
  agent TEXT,
  session_id TEXT,
  command TEXT,
  returncode INTEGER,
  signal INTEGER,
  timed_out INTEGER NOT NULL DEFAULT 0,
  stderr_path TEXT,
  tokens INTEGER,
  usage_json TEXT,
  error TEXT,
  started_at TEXT NOT NULL,
  ended_at TEXT
);
CREATE TABLE decisions (
  decision_id  TEXT PRIMARY KEY,
  adw_id       TEXT NOT NULL,
  phase        TEXT,
  question     TEXT NOT NULL,
  options      TEXT NOT NULL DEFAULT '[]',
  free_text    INTEGER NOT NULL DEFAULT 0,
  context      TEXT NOT NULL DEFAULT '',
  status       TEXT NOT NULL DEFAULT 'pending',
  answer_value TEXT,
  answer_text  TEXT,
  answered_by  TEXT,
  created_at   TEXT NOT NULL,
  answered_at  TEXT
);
CREATE INDEX IF NOT EXISTS idx_decisions_status ON decisions(status);
`;

interface FixtureRun {
  adwId: string;
  repo: string;
  status: "running" | "success" | "fail";
  request: string;
  runSlug?: string | null;
  repoName?: string | null;
  adwName?: string;
  engineer?: string;
  startedAt: string;
  endedAt?: string | null;
  totalTokens?: number;
  totalCost?: number;
  phases: Array<{
    phaseId: string;
    seq: number;
    name: string;
    kind?: string;
    owner?: string;
    status?: string;
    startedAt?: string;
    endedAt?: string | null;
  }>;
  events: Array<{
    eventId: string;
    phaseId: string;
    type: string;
    name?: string;
    payloadJson?: string | null;
    tokens?: number | null;
    startedAt?: string;
  }>;
}

const REPO_A = "/home/user/Projects/overdeck";
const REPO_B = "/home/user/Projects/multideal";

const tmpDirs: string[] = [];

afterEach(() => {
  while (tmpDirs.length > 0) {
    const dir = tmpDirs.pop();
    if (dir) rmSync(dir, { recursive: true, force: true });
  }
});

function createFixtureDb(runs: FixtureRun[]): string {
  const dir = mkdtempSync(join(tmpdir(), "factory-adapter-"));
  tmpDirs.push(dir);
  const path = join(dir, "sssf.db");
  const db = new Database(path);
  db.exec("PRAGMA journal_mode = WAL");
  db.exec("PRAGMA synchronous = NORMAL");
  db.exec("PRAGMA busy_timeout = 5000");
  db.exec(SCHEMA);

  for (const run of runs) {
    db.query(
      `INSERT INTO sessions
         (adw_id, adw_name, repo, repo_name, run_slug, request, status, engineer, started_at, ended_at, total_tokens, total_cost)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
    ).run(
      run.adwId,
      run.adwName ?? "adw_test",
      run.repo,
      run.repoName ?? null,
      run.runSlug ?? null,
      run.request,
      run.status,
      run.engineer ?? "engineer",
      run.startedAt,
      run.endedAt ?? null,
      run.totalTokens ?? 0,
      run.totalCost ?? 0,
    );

    for (const phase of run.phases) {
      db.query(
        `INSERT INTO phases
           (phase_id, adw_id, seq, name, kind, owner, status, started_at, ended_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
      ).run(
        phase.phaseId,
        run.adwId,
        phase.seq,
        phase.name,
        phase.kind ?? "agent",
        phase.owner ?? "coder",
        phase.status ?? (run.status === "running" ? "running" : "success"),
        phase.startedAt ?? run.startedAt,
        phase.endedAt ?? run.endedAt ?? null,
      );
    }

    for (const event of run.events) {
      db.query(
        `INSERT INTO events
           (event_id, adw_id, phase_id, type, name, payload_json, tokens, started_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
      ).run(
        event.eventId,
        run.adwId,
        event.phaseId,
        event.type,
        event.name ?? event.type,
        event.payloadJson ?? null,
        event.tokens ?? null,
        event.startedAt ?? run.startedAt,
      );
    }
  }

  db.close();
  return path;
}

function insertFixtureEvents(
  dbPath: string,
  events: Array<{ adwId: string; event: FixtureRun["events"][number] }>,
): void {
  const db = new Database(dbPath);
  for (const { adwId, event } of events) {
    db.query(
      `INSERT INTO events
         (event_id, adw_id, phase_id, type, name, payload_json, tokens, started_at)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
    ).run(
      event.eventId,
      adwId,
      event.phaseId,
      event.type,
      event.name ?? event.type,
      event.payloadJson ?? null,
      event.tokens ?? null,
      event.startedAt ?? "2026-08-05T10:00:00Z",
    );
  }
  db.close();
}

function insertFixtureRun(dbPath: string, run: FixtureRun): void {
  appendToFixtureDb(dbPath, [run]);
}

function appendToFixtureDb(
  dbPath: string,
  runs: FixtureRun[],
): void {
  const db = new Database(dbPath);
  for (const run of runs) {
    db.query(
      `INSERT INTO sessions
         (adw_id, adw_name, repo, repo_name, run_slug, request, status, engineer, started_at, ended_at, total_tokens, total_cost)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
    ).run(
      run.adwId,
      run.adwName ?? "adw_test",
      run.repo,
      run.repoName ?? null,
      run.runSlug ?? null,
      run.request,
      run.status,
      run.engineer ?? "engineer",
      run.startedAt,
      run.endedAt ?? null,
      run.totalTokens ?? 0,
      run.totalCost ?? 0,
    );

    for (const phase of run.phases) {
      db.query(
        `INSERT INTO phases
           (phase_id, adw_id, seq, name, kind, owner, status, started_at, ended_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
      ).run(
        phase.phaseId,
        run.adwId,
        phase.seq,
        phase.name,
        phase.kind ?? "agent",
        phase.owner ?? "coder",
        phase.status ?? (run.status === "running" ? "running" : "success"),
        phase.startedAt ?? run.startedAt,
        phase.endedAt ?? run.endedAt ?? null,
      );
    }

    for (const event of run.events) {
      db.query(
        `INSERT INTO events
           (event_id, adw_id, phase_id, type, name, payload_json, tokens, started_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
      ).run(
        event.eventId,
        run.adwId,
        event.phaseId,
        event.type,
        event.name ?? event.type,
        event.payloadJson ?? null,
        event.tokens ?? null,
        event.startedAt ?? run.startedAt,
      );
    }
  }
  db.close();
}

function statFromPath(path: string): { dev: number; ino: number } {
  const stat = statSync(path);
  return { dev: Number(stat.dev), ino: Number(stat.ino) };
}

function createFactoryAdapter(opts: FactoryAdapterOptions = {}) {
  const adapter = createFactorySummaryAdapter(opts);
  return {
    ...adapter,
    async poll() {
      const result = await adapter.poll();
      return {
        ...result,
        panels: result.panels.map((panel) => panel.id !== "factory-runs" ? panel : {
          ...panel,
          data: {
            ...(panel.data as FactoryPanelData),
            runs: (panel.data as FactoryPanelData).runs.map((run) =>
              loadFactoryRunDetail(run.adwId, opts) ?? run),
          },
        }),
      };
    },
  };
}

function panelData(result: Awaited<ReturnType<ReturnType<typeof createFactoryAdapter>["poll"]>>): FactoryPanelData {
  const panel = result.panels.find((entry) => entry.id === "factory-runs");
  expect(panel).toBeDefined();
  return panel!.data as FactoryPanelData;
}

function runByAdwId(data: FactoryPanelData, adwId: string): FactoryRunView {
  const run = data.runs.find((entry) => entry.adwId === adwId);
  expect(run).toBeDefined();
  return run!;
}

describe("createFactoryAdapter", () => {
  test("poll retains summaries while detail loads from SQLite on demand", async () => {
    const dbPath = createFixtureDb([{
      adwId: "adw-summary", repo: REPO_A, status: "success", request: "bounded summary",
      startedAt: "2026-08-05T10:00:00Z",
      phases: [{ phaseId: "phase-summary", seq: 1, name: "build" }],
      events: [{ eventId: "event-summary", phaseId: "phase-summary", type: "phase_end" }],
    }]);

    const summary = runByAdwId(
      panelData(await createFactorySummaryAdapter({ dbPath }).poll()),
      "adw-summary",
    );
    expect(summary.detailAvailable).toBe(true);
    expect(summary.phases).toEqual([]);
    expect(summary.events).toEqual([]);

    const detail = loadFactoryRunDetail("adw-summary", { dbPath });
    expect(detail?.phases).toHaveLength(1);
    expect(detail?.events).toHaveLength(1);
  });

  test("keeps core run data when optional trace tables are absent", async () => {
    const dbPath = createFixtureDb([{
      adwId: "adw-core-only", repo: REPO_A, status: "success", request: "read old schema",
      startedAt: "2026-08-05T10:00:00Z",
      phases: [{ phaseId: "phase-core", seq: 1, name: "build" }],
      events: [{ eventId: "event-core", phaseId: "phase-core", type: "phase_end" }],
    }]);
    const db = new Database(dbPath);
    for (const table of ["envelopes", "gate_results", "phase_diffs", "processes", "agent_sessions", "agent_attempts", "decisions"]) {
      db.exec(`DROP TABLE ${table}`);
    }
    db.close();

    const data = panelData(await createFactoryAdapter({ dbPath }).poll());
    const run = runByAdwId(data, "adw-core-only");
    expect(data.dbPresent).toBe(true);
    expect(run.phases).toHaveLength(1);
    expect(run.events).toHaveLength(1);
    expect(run.attempts).toEqual([]);
    expect(run.gates).toEqual([]);
    expect(run.diffs).toEqual([]);
    expect(run.processes).toEqual([]);
    expect(run.unavailableTables).toEqual(["agent_attempts", "gate_results", "phase_diffs", "processes"]);
  });

  test("propagates runSlug from sessions.run_slug", async () => {
    const dbPath = createFixtureDb([{
      adwId: "adw-slugged",
      repo: REPO_A,
      status: "success",
      request: "docs/specs/2026-08-09-incident-dispatch-selector-options-design.md",
      runSlug: "incident-dispatch-selector-options-design",
      startedAt: "2026-08-05T10:00:00Z",
      phases: [],
      events: [],
    }]);
    const run = runByAdwId(panelData(await createFactoryAdapter({ dbPath }).poll()), "adw-slugged");
    expect(run.runSlug).toBe("incident-dispatch-selector-options-design");
  });

  test("returns null runSlug when sessions.run_slug column is absent", async () => {
    const dir = mkdtempSync(join(tmpdir(), "factory-adapter-legacy-slug-"));
    tmpDirs.push(dir);
    const path = join(dir, "sssf.db");
    const legacySchema = SCHEMA.replace("  run_slug      TEXT,\n", "");
    const db = new Database(path);
    db.exec("PRAGMA journal_mode = WAL");
    db.exec(legacySchema);
    db.query(
      `INSERT INTO sessions
         (adw_id, adw_name, repo, request, status, engineer, started_at, ended_at, total_tokens, total_cost)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
    ).run(
      "adw-legacy-slug",
      "adw_test",
      REPO_A,
      "legacy request",
      "success",
      "engineer",
      "2026-08-05T10:00:00Z",
      null,
      0,
      0,
    );
    db.close();

    const run = runByAdwId(panelData(await createFactoryAdapter({ dbPath: path }).poll()), "adw-legacy-slug");
    expect(run.runSlug).toBeNull();
  });

  test("propagates repoName from sessions.repo_name", async () => {
    const dbPath = createFixtureDb([{
      adwId: "adw-named",
      repo: `${REPO_A}/.worktrees/cluster-machines-hero`,
      repoName: "overdeck",
      status: "running",
      request: "hero work",
      startedAt: "2026-08-05T10:00:00Z",
      phases: [],
      events: [],
    }]);
    const run = runByAdwId(panelData(await createFactoryAdapter({ dbPath }).poll()), "adw-named");
    expect(run.repo).toBe(`${REPO_A}/.worktrees/cluster-machines-hero`);
    expect(run.repoName).toBe("overdeck");
  });

  test("returns null repoName when sessions.repo_name column is absent", async () => {
    const dir = mkdtempSync(join(tmpdir(), "factory-adapter-legacy-repo-name-"));
    tmpDirs.push(dir);
    const path = join(dir, "sssf.db");
    const legacySchema = SCHEMA.replace("  repo_name     TEXT,\n", "");
    const db = new Database(path);
    db.exec("PRAGMA journal_mode = WAL");
    db.exec(legacySchema);
    db.query(
      `INSERT INTO sessions
         (adw_id, adw_name, repo, run_slug, request, status, engineer, started_at, ended_at, total_tokens, total_cost)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
    ).run(
      "adw-legacy-repo-name",
      "adw_test",
      `${REPO_A}/.worktrees/cluster-machines-hero`,
      null,
      "legacy request",
      "running",
      "engineer",
      "2026-08-05T10:00:00Z",
      null,
      0,
      0,
    );
    db.close();

    const run = runByAdwId(panelData(await createFactoryAdapter({ dbPath: path }).poll()), "adw-legacy-repo-name");
    expect(run.repoName).toBeNull();
    expect(run.repo).toBe(`${REPO_A}/.worktrees/cluster-machines-hero`);
  });

  test("discovers an optional trace table created in the same db file after polling", async () => {
    const dbPath = createFixtureDb([{
      adwId: "adw-late-attempts", repo: REPO_A, status: "running", request: "add attempts",
      startedAt: "2026-08-05T10:00:00Z",
      phases: [{ phaseId: "phase-late-attempts", seq: 1, name: "build" }],
      events: [],
    }]);
    const db = new Database(dbPath);
    db.exec("DROP TABLE agent_attempts");
    db.close();

    const adapter = createFactoryAdapter({ dbPath });
    const first = panelData(await adapter.poll());
    expect(first.dbPresent).toBe(true);
    expect(runByAdwId(first, "adw-late-attempts").attempts).toEqual([]);
    expect(runByAdwId(first, "adw-late-attempts").unavailableTables).toContain("agent_attempts");

    const migratedDb = new Database(dbPath);
    migratedDb.exec(`CREATE TABLE agent_attempts (
      attempt_id TEXT PRIMARY KEY, adw_id TEXT, phase_id TEXT, parent_id TEXT, agent TEXT,
      session_id TEXT, command TEXT, returncode INTEGER, signal INTEGER,
      timed_out INTEGER NOT NULL DEFAULT 0, stderr_path TEXT, tokens INTEGER,
      usage_json TEXT, error TEXT, started_at TEXT NOT NULL, ended_at TEXT
    )`);
    migratedDb.query(`INSERT INTO agent_attempts
      (attempt_id, adw_id, phase_id, agent, command, timed_out, started_at)
      VALUES (?, ?, ?, ?, ?, ?, ?)`)
      .run("attempt-late", "adw-late-attempts", "phase-late-attempts", "coder", "bun test", 0,
        "2026-08-05T10:00:10Z");
    migratedDb.close();

    const second = panelData(await adapter.poll());
    expect(runByAdwId(second, "adw-late-attempts").attempts).toEqual([
      expect.objectContaining({ attemptId: "attempt-late", command: "bun test" }),
    ]);
    expect(runByAdwId(second, "adw-late-attempts").unavailableTables).not.toContain("agent_attempts");
  });

  test("discovers an optional column added in the same db file after polling", async () => {
    const dbPath = createFixtureDb([{
      adwId: "adw-late-host", repo: REPO_A, status: "running", request: "add host",
      startedAt: "2026-08-05T10:00:00Z",
      phases: [{ phaseId: "phase-late-host", seq: 1, name: "build" }],
      events: [],
    }]);
    const db = new Database(dbPath);
    db.query(`INSERT INTO agent_attempts
      (attempt_id, adw_id, phase_id, agent, command, timed_out, started_at)
      VALUES (?, ?, ?, ?, ?, ?, ?)`)
      .run("attempt-late-host", "adw-late-host", "phase-late-host", "coder", "bun test", 0,
        "2026-08-05T10:00:10Z");
    db.close();

    const adapter = createFactoryAdapter({ dbPath });
    const firstRun = runByAdwId(panelData(await adapter.poll()), "adw-late-host");
    expect(firstRun.attempts[0]?.host).toBeNull();

    const migratedDb = new Database(dbPath);
    migratedDb.exec("ALTER TABLE agent_attempts ADD COLUMN host TEXT");
    migratedDb.query("UPDATE agent_attempts SET host = ? WHERE attempt_id = ?")
      .run("e14", "attempt-late-host");
    migratedDb.close();

    const secondRun = runByAdwId(panelData(await adapter.poll()), "adw-late-host");
    expect(secondRun.attempts[0]?.host).toBe("e14");
  });

  test("prefers attempt model and exposes account while legacy attempt columns stay optional", async () => {
    const dbPath = createFixtureDb([{
      adwId: "adw-attempt-identity", repo: REPO_A, status: "running", request: "show identity",
      startedAt: "2026-08-05T10:00:00Z",
      phases: [{ phaseId: "phase-attempt-identity", seq: 1, name: "build", owner: "coder" }],
      events: [],
    }]);
    const db = new Database(dbPath);
    db.query(`INSERT INTO agent_sessions
      (adw_id, agent, coding_agent, model, session_id, created_at, last_used_at)
      VALUES (?, ?, ?, ?, ?, ?, ?)`).run(
        "adw-attempt-identity", "coder", "pi", "session/fallback", "session-1",
        "2026-08-05T10:00:00Z", "2026-08-05T10:00:00Z",
      );
    db.query(`INSERT INTO agent_attempts
      (attempt_id, adw_id, phase_id, agent, command, timed_out, started_at)
      VALUES (?, ?, ?, ?, ?, ?, ?)`).run(
        "attempt-before-migration", "adw-attempt-identity", "phase-attempt-identity", "coder",
        "pi legacy", 0, "2026-08-05T10:00:10Z",
      );
    db.close();

    const legacyRun = runByAdwId(panelData(await createFactoryAdapter({ dbPath }).poll()), "adw-attempt-identity");
    expect(legacyRun.attempts[0]).toEqual(expect.objectContaining({
      attemptId: "attempt-before-migration", account: null, model: "session/fallback",
    }));

    const migratedDb = new Database(dbPath);
    migratedDb.exec("ALTER TABLE agent_attempts ADD COLUMN model TEXT");
    migratedDb.exec("ALTER TABLE agent_attempts ADD COLUMN account TEXT");
    migratedDb.query("UPDATE agent_attempts SET model = ?, account = ? WHERE attempt_id = ?").run(
      "openai-codex/gpt-5.6-terra", "account-123", "attempt-before-migration",
    );
    migratedDb.close();

    const migratedRun = runByAdwId(panelData(await createFactoryAdapter({ dbPath }).poll()), "adw-attempt-identity");
    expect(migratedRun.attempts[0]).toEqual(expect.objectContaining({
      account: "account-123", model: "openai-codex/gpt-5.6-terra",
    }));
  });

  test("computes idle time only for running attempts with heartbeat data", async () => {
    const dbPath = createFixtureDb([{
      adwId: "adw-heartbeat", repo: REPO_A, status: "running", request: "stay alive",
      startedAt: "2026-08-05T10:00:00Z",
      phases: [{ phaseId: "phase-heartbeat", seq: 1, name: "build" }],
      events: [],
    }]);
    const db = new Database(dbPath);
    db.exec("ALTER TABLE agent_attempts ADD COLUMN last_output_at TEXT");
    db.query(`INSERT INTO agent_attempts
      (attempt_id, adw_id, phase_id, agent, timed_out, started_at, last_output_at)
      VALUES (?, ?, ?, ?, ?, ?, ?)`)
      .run("attempt-running", "adw-heartbeat", "phase-heartbeat", "coder", 0,
        "2026-08-05T10:00:00Z", "2026-08-05T10:01:00Z");
    db.query(`INSERT INTO agent_attempts
      (attempt_id, adw_id, phase_id, agent, timed_out, started_at, last_output_at, ended_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
      .run("attempt-ended", "adw-heartbeat", "phase-heartbeat", "coder", 0,
        "2026-08-05T10:00:00Z", "2026-08-05T10:00:30Z", "2026-08-05T10:01:30Z");
    db.close();

    const now = Date.parse("2026-08-05T10:02:00Z");
    const run = runByAdwId(panelData(await createFactoryAdapter({ dbPath, now: () => now }).poll()), "adw-heartbeat");
    expect(run.attempts[0]).toEqual(expect.objectContaining({
      lastOutputAt: "2026-08-05T10:01:00Z", idleMs: 60_000,
    }));
    expect(run.attempts[1]).toEqual(expect.objectContaining({
      lastOutputAt: "2026-08-05T10:00:30Z", idleMs: null,
    }));
  });

  test("surfaces attempts, phase durations, and parsed or raw gate checks from legacy columns", async () => {
    const dbPath = createFixtureDb([{
      adwId: "adw-trace", repo: REPO_A, status: "success", request: "trace it",
      startedAt: "2026-08-05T10:00:00Z", endedAt: "2026-08-05T10:02:00Z",
      phases: [{ phaseId: "phase-trace", seq: 1, name: "build", startedAt: "2026-08-05T10:00:00Z", endedAt: "2026-08-05T10:01:00Z" }],
      events: [],
    }]);
    const db = new Database(dbPath);
    db.query(`INSERT INTO agent_attempts
      (attempt_id, adw_id, phase_id, agent, session_id, command, returncode, signal, timed_out,
       stderr_path, tokens, error, started_at, ended_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
      .run("attempt-1", "adw-trace", "phase-trace", "coder", "session-1", "bun test", 1, null, 0,
        "logs/stderr.txt", 42, "failed", "2026-08-05T10:00:10Z", "2026-08-05T10:00:15Z");
    db.query("INSERT INTO gate_results (adw_id, phase_id, attempt, gate, passed, violations_json, checks_json) VALUES (?, ?, ?, ?, ?, ?, ?)")
      .run("adw-trace", "phase-trace", 1, "tests", 0, '["test output tail"]', '[{"command":"bun test","returncode":1,"output_path":"logs/test.txt"}]');
    db.query("INSERT INTO gate_results (adw_id, phase_id, attempt, gate, passed, checks_json) VALUES (?, ?, ?, ?, ?, ?)")
      .run("adw-trace", "phase-trace", 1, "lint", 0, "not-json");
    db.close();

    const run = runByAdwId(panelData(await createFactoryAdapter({ dbPath }).poll()), "adw-trace");
    expect(run.phases[0]?.durationMs).toBe(60_000);
    expect(run.attempts).toEqual([expect.objectContaining({
      attemptId: "attempt-1", durationMs: 5_000, host: null, account: null, command: "bun test",
      timeoutKind: null, toolCalls: [],
    })]);
    expect(run.gates[0]?.checks).toEqual([{ command: "bun test", returncode: 1, output_path: "logs/test.txt" }]);
    expect(run.gates[0]?.violations).toEqual(["test output tail"]);
    expect(run.gates[1]?.checks).toBe("not-json");
    expect(run.unavailableTables).toEqual([]);
  });

  test("surfaces phase diffs and owned processes", async () => {
    const dbPath = createFixtureDb([{
      adwId: "adw-diff", repo: REPO_A, status: "success", request: "diff it",
      startedAt: "2026-08-05T10:00:00Z",
      phases: [{ phaseId: "phase-diff", seq: 1, name: "build" }],
      events: [],
    }]);
    const db = new Database(dbPath);
    db.query(`INSERT INTO phase_diffs
      (adw_id, phase_id, attempt, files_json, insertions, deletions, diff_text, truncated, created_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
      .run("adw-diff", "phase-diff", 1,
        '[{"path":"src/a.ts","status":"M","insertions":5,"deletions":7},"src/b.ts",{"nopath":1}]', 16, 20,
        "diff --git a/src/a.ts b/src/a.ts", 0, "2026-08-05T10:01:00Z");
    db.query(`INSERT INTO phase_diffs
      (adw_id, phase_id, attempt, files_json, insertions, deletions, diff_text, truncated, created_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
      .run("adw-diff", "phase-diff", 2, "{malformed", 1, 0, "diff", 1, "2026-08-05T10:02:00Z");
    db.query(`INSERT INTO processes (adw_id, kind, name, pid, command, started_at, ended_at)
      VALUES (?, ?, ?, ?, ?, ?, ?)`)
      .run("adw-diff", "adw", "", 4242, "factory adw_build spec.md",
        "2026-08-05T10:00:00Z", "2026-08-05T10:03:00Z");
    db.query(`INSERT INTO processes (adw_id, kind, name, pid, command, started_at, ended_at)
      VALUES (?, ?, ?, ?, ?, ?, ?)`)
      .run("adw-diff", "agent", "coder", 4243, "cursor-agent", "2026-08-05T10:00:10Z", null);
    db.close();

    const run = runByAdwId(panelData(await createFactoryAdapter({ dbPath }).poll()), "adw-diff");
    expect(run.diffs).toEqual([
      {
        phaseId: "phase-diff", attempt: 1, files: [
          { path: "src/a.ts", status: "M", insertions: 5, deletions: 7 },
          { path: "src/b.ts", status: null, insertions: null, deletions: null },
        ], insertions: 16, deletions: 20,
        diffText: "diff --git a/src/a.ts b/src/a.ts", truncated: false, createdAt: "2026-08-05T10:01:00Z",
      },
      {
        phaseId: "phase-diff", attempt: 2, files: [], insertions: 1, deletions: 0,
        diffText: "diff", truncated: true, createdAt: "2026-08-05T10:02:00Z",
      },
    ]);
    expect(run.processes).toEqual([
      {
        kind: "adw", name: "", pid: 4242, command: "factory adw_build spec.md",
        startedAt: "2026-08-05T10:00:00Z", endedAt: "2026-08-05T10:03:00Z", durationMs: 180_000,
      },
      {
        kind: "agent", name: "coder", pid: 4243, command: "cursor-agent",
        startedAt: "2026-08-05T10:00:10Z", endedAt: null, durationMs: null,
      },
    ]);
    expect(run.unavailableTables).toEqual([]);
  });

  test("uses empty violations for malformed gate JSON", async () => {
    const dbPath = createFixtureDb([{
      adwId: "adw-bad-violations", repo: REPO_A, status: "fail", request: "read failure",
      startedAt: "2026-08-05T10:00:00Z",
      phases: [{ phaseId: "phase-bad-violations", seq: 1, name: "verify" }],
      events: [],
    }]);
    const db = new Database(dbPath);
    db.query("INSERT INTO gate_results (adw_id, phase_id, attempt, gate, passed, violations_json, checks_json) VALUES (?, ?, ?, ?, ?, ?, ?)")
      .run("adw-bad-violations", "phase-bad-violations", 1, "quality:test", 0, "{malformed", "[]");
    db.close();

    const run = runByAdwId(panelData(await createFactoryAdapter({ dbPath }).poll()), "adw-bad-violations");
    expect(run.gates[0]?.violations).toEqual([]);
  });

  test("uses empty violations when the legacy gate table has no violations column", async () => {
    const dbPath = createFixtureDb([{
      adwId: "adw-legacy-gate", repo: REPO_A, status: "fail", request: "read legacy gate",
      startedAt: "2026-08-05T10:00:00Z",
      phases: [{ phaseId: "phase-legacy-gate", seq: 1, name: "verify" }],
      events: [],
    }]);
    const db = new Database(dbPath);
    db.exec("DROP TABLE gate_results");
    db.exec(`CREATE TABLE gate_results (
      id INTEGER PRIMARY KEY AUTOINCREMENT, adw_id TEXT, phase_id TEXT,
      attempt INTEGER, gate TEXT, passed INTEGER, checks_json TEXT, created_at TEXT
    )`);
    db.query("INSERT INTO gate_results (adw_id, phase_id, attempt, gate, passed, checks_json) VALUES (?, ?, ?, ?, ?, ?)")
      .run("adw-legacy-gate", "phase-legacy-gate", 1, "quality:test", 0, "[]");
    db.close();

    const run = runByAdwId(panelData(await createFactoryAdapter({ dbPath }).poll()), "adw-legacy-gate");
    expect(run.gates[0]?.violations).toEqual([]);
  });

  test("surfaces ordered tool calls with safe parsed args and timeout kind", async () => {
    const dbPath = createFixtureDb([{
      adwId: "adw-tools", repo: REPO_A, status: "success", request: "trace tools",
      startedAt: "2026-08-05T10:00:00Z", endedAt: "2026-08-05T10:02:00Z",
      phases: [{ phaseId: "phase-tools", seq: 1, name: "plan", owner: "planner" }],
      events: [],
    }]);
    const db = new Database(dbPath);
    db.exec("ALTER TABLE agent_attempts ADD COLUMN timeout_kind TEXT");
    db.exec(`CREATE TABLE tool_calls (
      tool_call_id TEXT PRIMARY KEY, attempt_id TEXT NOT NULL, seq INTEGER NOT NULL,
      tool_name TEXT, args_json TEXT, started_at TEXT, ended_at TEXT,
      duration_ms INTEGER, ok INTEGER, result_excerpt TEXT
    )`);
    db.query(`INSERT INTO agent_attempts
      (attempt_id, adw_id, phase_id, agent, timed_out, timeout_kind, started_at, ended_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(
        "attempt-tools", "adw-tools", "phase-tools", "planner", 1, "idle",
        "2026-08-05T10:00:01Z", "2026-08-05T10:01:01Z",
      );
    db.query(`INSERT INTO tool_calls
      (tool_call_id, attempt_id, seq, tool_name, args_json, started_at, ended_at,
       duration_ms, ok, result_excerpt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
        "call-second", "attempt-tools", 1, "read", "{malformed",
        "2026-08-05T10:00:03Z", "2026-08-05T10:00:04Z", 1000, 0, "failed",
      );
    db.query(`INSERT INTO tool_calls
      (tool_call_id, attempt_id, seq, tool_name, args_json, started_at, ended_at,
       duration_ms, ok, result_excerpt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
        "call-first", "attempt-tools", 0, "ls", '{"path":"specs"}',
        "2026-08-05T10:00:01Z", "2026-08-05T10:00:02Z", 1000, 1, "file.md",
      );
    db.close();

    const run = runByAdwId(panelData(await createFactoryAdapter({ dbPath }).poll()), "adw-tools");
    expect(run.attempts[0]?.timeoutKind).toBe("idle");
    expect(run.attempts[0]?.toolCalls).toEqual([
      {
        toolCallId: "call-first", seq: 0, toolName: "ls", args: { path: "specs" },
        startedAt: "2026-08-05T10:00:01Z", endedAt: "2026-08-05T10:00:02Z",
        durationMs: 1000, ok: true, resultExcerpt: "file.md",
      },
      {
        toolCallId: "call-second", seq: 1, toolName: "read", args: null,
        startedAt: "2026-08-05T10:00:03Z", endedAt: "2026-08-05T10:00:04Z",
        durationMs: 1000, ok: false, resultExcerpt: "failed",
      },
    ]);
  });

  test("parses estimated usage and typed provider failures from usage_json", async () => {
    const dbPath = createFixtureDb([{
      adwId: "adw-gpt-usage", repo: REPO_A, status: "fail", request: "show gpt metadata",
      startedAt: "2026-08-05T10:00:00Z",
      phases: [{ phaseId: "phase-gpt-usage", seq: 1, name: "build", owner: "builder" }],
      events: [],
    }]);
    const db = new Database(dbPath);
    db.query(`INSERT INTO agent_attempts
      (attempt_id, adw_id, phase_id, agent, timed_out, tokens, usage_json, error, started_at, ended_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
        "attempt-gpt", "adw-gpt-usage", "phase-gpt-usage", "builder", 0, 30,
        JSON.stringify({
          total_tokens: 30,
          usage_estimated: true,
          billing_status: "unavailable",
          max_tokens: 16000,
          context_window: 120000,
          provider_failure: {
            kind: "capped",
            detail: "usage cap hit",
            retry_after_seconds: 900,
            resume_at: "2026-08-11T09:00:00Z",
          },
        }),
        "pi provider failure [capped]: usage cap hit",
        "2026-08-05T10:00:10Z", "2026-08-05T10:00:12Z",
      );
    db.close();

    const run = runByAdwId(panelData(await createFactoryAdapter({ dbPath }).poll()), "adw-gpt-usage");
    expect(run.attempts[0]).toEqual(expect.objectContaining({
      usage: expect.objectContaining({
        totalTokens: 30,
        usageEstimated: true,
        billingStatus: "unavailable",
        maxTokens: 16000,
        contextWindow: 120000,
      }),
      providerFailure: {
        kind: "capped",
        detail: "usage cap hit",
        retryAfterSeconds: 900,
        resumeAt: "2026-08-11T09:00:00Z",
      },
    }));
  });

  test("returns an empty panel when the db file is absent", async () => {
    const dir = mkdtempSync(join(tmpdir(), "factory-missing-"));
    tmpDirs.push(dir);
    const adapter = createFactoryAdapter({ dbPath: join(dir, "missing.db") });
    const result = await adapter.poll();
    const data = panelData(result);
    expect(data.dbPresent).toBe(false);
    expect(data.runs).toEqual([]);
    expect(result.items).toEqual([]);
  });

  test("projects completed and running runs from two repos with phase spend and tokens", async () => {
    const dbPath = createFixtureDb([
      {
        adwId: "adw-complete-a",
        repo: REPO_A,
        status: "success",
        request: "ship feature A",
        startedAt: "2026-08-05T10:00:00Z",
        endedAt: "2026-08-05T10:30:00Z",
        totalTokens: 12_000,
        totalCost: 1.25,
        phases: [
          { phaseId: "phase-a-1", seq: 1, name: "plan", kind: "engineer", status: "success" },
          { phaseId: "phase-a-2", seq: 2, name: "build", kind: "agent", status: "success" },
        ],
        events: [
          {
            eventId: "evt-a-1",
            phaseId: "phase-a-2",
            type: "agent_end",
            tokens: 4_000,
            payloadJson: JSON.stringify({ cost: 0.5, usage: { total_tokens: 4000, total_cost: 0.5 } }),
          },
          {
            eventId: "evt-a-2",
            phaseId: "phase-a-2",
            type: "agent_end",
            tokens: 2_000,
            payloadJson: JSON.stringify({ cost: 0.25 }),
          },
          {
            eventId: "evt-a-bad",
            phaseId: "phase-a-2",
            type: "agent_end",
            tokens: 100,
            payloadJson: "{not-json",
          },
          { eventId: "evt-a-log", phaseId: "phase-a-1", type: "log", payloadJson: JSON.stringify({ msg: "ok" }) },
        ],
      },
      {
        adwId: "adw-running-b",
        repo: REPO_B,
        status: "running",
        request: "investigate failure",
        startedAt: "2026-08-05T11:00:00Z",
        phases: [
          { phaseId: "phase-b-1", seq: 1, name: "triage", kind: "engineer", status: "success" },
          { phaseId: "phase-b-2", seq: 2, name: "fix", kind: "agent", status: "running" },
        ],
        events: [
          {
            eventId: "evt-b-1",
            phaseId: "phase-b-2",
            type: "tool_call",
            payloadJson: JSON.stringify({ tool: "bash", ok: true }),
          },
        ],
      },
    ]);

    const adapter = createFactoryAdapter({ dbPath });
    const result = await adapter.poll();
    const data = panelData(result);

    expect(data.dbPresent).toBe(true);
    expect(data.runs).toHaveLength(2);

    const completed = runByAdwId(data, "adw-complete-a");
    expect(completed.repo).toBe(REPO_A);
    expect(completed.status).toBe("success");
    expect(completed.phases).toHaveLength(2);
    expect(completed.phases[0]!.seq).toBe(1);
    expect(completed.phases[1]!.seq).toBe(2);
    expect(completed.phases[1]!.tokens).toBe(6_100);
    expect(completed.phases[1]!.spend).toBeCloseTo(0.75);
    expect(completed.events.map((event) => event.eventId)).toEqual([
      "evt-a-1",
      "evt-a-2",
      "evt-a-bad",
      "evt-a-log",
    ]);

    const running = runByAdwId(data, "adw-running-b");
    expect(running.repo).toBe(REPO_B);
    expect(running.status).toBe("running");
    expect(running.phases).toHaveLength(2);
    expect(running.events).toHaveLength(1);

    const repos = new Set(data.runs.map((run) => run.repo));
    expect(repos.has(REPO_A)).toBe(true);
    expect(repos.has(REPO_B)).toBe(true);

    expect(result.items.some((item) => item.kind === "progress" && item.id === "factory-running-adw-running-b")).toBe(true);
    expect(result.items.some((item) => item.kind === "halt")).toBe(false);
  });

  test("emits a halt item for failed runs without invented actions", async () => {
    const dbPath = createFixtureDb([
      {
        adwId: "adw-failed",
        repo: REPO_A,
        status: "fail",
        request: "broken deploy",
        startedAt: "2026-08-05T09:00:00Z",
        endedAt: "2026-08-05T09:15:00Z",
        phases: [{ phaseId: "phase-f-1", seq: 1, name: "deploy", status: "fail" }],
        events: [{ eventId: "evt-f-1", phaseId: "phase-f-1", type: "error" }],
      },
    ]);

    const adapter = createFactoryAdapter({ dbPath });
    const result = await adapter.poll();
    const halt = result.items.find((item) => item.kind === "halt");
    expect(halt).toBeDefined();
    expect(halt!.severity).toBe("act");
    expect(halt!.actions).toEqual([]);
  });

  test("pages the global rowid cursor beyond 500 events without duplicates on repeat polls", async () => {
    const events = Array.from({ length: 600 }, (_, index) => ({
      eventId: `evt-page-${index}`,
      phaseId: "phase-page",
      type: index % 50 === 0 ? "agent_end" : "log",
      tokens: index % 50 === 0 ? 10 : null,
      payloadJson: index % 50 === 0 ? JSON.stringify({ cost: 0.01 }) : null,
    }));

    const dbPath = createFixtureDb([
      {
        adwId: "adw-page",
        repo: REPO_A,
        status: "success",
        request: "large trace",
        startedAt: "2026-08-05T08:00:00Z",
        endedAt: "2026-08-05T08:30:00Z",
        phases: [{ phaseId: "phase-page", seq: 1, name: "work", status: "success" }],
        events,
      },
    ]);

    const adapter = createFactoryAdapter({ dbPath });
    const first = await adapter.poll();
    const second = await adapter.poll();

    const firstRun = runByAdwId(panelData(first), "adw-page");
    const secondRun = runByAdwId(panelData(second), "adw-page");
    expect(firstRun.events).toHaveLength(600);
    expect(secondRun.events).toHaveLength(600);
    expect(secondRun.events.map((event) => event.rowid)).toEqual(firstRun.events.map((event) => event.rowid));
    expect(new Set(secondRun.events.map((event) => event.eventId)).size).toBe(600);
  });

  test("does not skip cross-session events when a new session bootstraps after an incremental event", async () => {
    const eventsA = Array.from({ length: 100 }, (_, index) => ({
      eventId: `evt-a-${index}`,
      phaseId: "phase-a",
      type: "log",
    }));

    const dbPath = createFixtureDb([
      {
        adwId: "adw-a",
        repo: REPO_A,
        status: "running",
        request: "session A",
        startedAt: "2026-08-05T10:00:00Z",
        phases: [{ phaseId: "phase-a", seq: 1, name: "work", status: "running" }],
        events: eventsA,
      },
    ]);

    const adapter = createFactoryAdapter({ dbPath });
    const first = await adapter.poll();
    expect(runByAdwId(panelData(first), "adw-a").events).toHaveLength(100);

    insertFixtureEvents(dbPath, [
      { adwId: "adw-a", event: { eventId: "evt-a-101", phaseId: "phase-a", type: "log" } },
    ]);
    insertFixtureRun(dbPath, {
      adwId: "adw-b",
      repo: REPO_B,
      status: "running",
      request: "session B",
      startedAt: "2026-08-05T10:05:00Z",
      phases: [{ phaseId: "phase-b", seq: 1, name: "work", status: "running" }],
      events: [{ eventId: "evt-b-102", phaseId: "phase-b", type: "log" }],
    });

    const second = await adapter.poll();
    const runA = runByAdwId(panelData(second), "adw-a");
    const runB = runByAdwId(panelData(second), "adw-b");
    expect(runA.events.map((event) => event.eventId)).toContain("evt-a-101");
    expect(runB.events.map((event) => event.eventId)).toEqual(["evt-b-102"]);
  });

  test("preserves open db and cursor across mtime/size changes but rebuilds on inode replacement", async () => {
    const dbPath = createFixtureDb([
      {
        adwId: "adw-live",
        repo: REPO_A,
        status: "running",
        request: "live db",
        startedAt: "2026-08-05T10:00:00Z",
        phases: [{ phaseId: "phase-live", seq: 1, name: "work", status: "running" }],
        events: [{ eventId: "evt-live-1", phaseId: "phase-live", type: "log" }],
      },
    ]);

    let statSnapshot = statFromPath(dbPath);
    const adapter = createFactoryAdapter({
      dbPath,
      statImpl: () => statSnapshot,
    });

    const first = await adapter.poll();
    expect(runByAdwId(panelData(first), "adw-live").events.map((event) => event.eventId)).toEqual([
      "evt-live-1",
    ]);

    // Simulate live-db writes: real file grows/changes mtime, but dev+ino stay stable.
    insertFixtureEvents(dbPath, [
      { adwId: "adw-live", event: { eventId: "evt-live-2", phaseId: "phase-live", type: "log" } },
    ]);

    const second = await adapter.poll();
    expect(runByAdwId(panelData(second), "adw-live").events.map((event) => event.eventId)).toEqual([
      "evt-live-1",
      "evt-live-2",
    ]);

    const replacementPath = createFixtureDb([
      {
        adwId: "adw-replaced",
        repo: REPO_B,
        status: "running",
        request: "replaced db",
        startedAt: "2026-08-05T11:00:00Z",
        phases: [{ phaseId: "phase-replaced", seq: 1, name: "new", status: "running" }],
        events: [{ eventId: "evt-replaced", phaseId: "phase-replaced", type: "log" }],
      },
    ]);
    rmSync(dbPath, { force: true });
    for (const suffix of ["-wal", "-shm"]) {
      try {
        rmSync(`${dbPath}${suffix}`, { force: true });
      } catch {
        // sidecar may be absent
      }
    }
    copyFileSync(replacementPath, dbPath);
    statSnapshot = statFromPath(dbPath);

    const third = await adapter.poll();
    const data = panelData(third);
    expect(data.runs.some((run) => run.adwId === "adw-live")).toBe(false);
    expect(runByAdwId(data, "adw-replaced").events.map((event) => event.eventId)).toEqual(["evt-replaced"]);
  });

  test("rebuilds safely after db replacement and schema reset", async () => {
    const dir = mkdtempSync(join(tmpdir(), "factory-replace-"));
    tmpDirs.push(dir);
    const dbPath = join(dir, "sssf.db");

    const initialPath = createFixtureDb([
      {
        adwId: "adw-old",
        repo: REPO_A,
        status: "success",
        request: "old run",
        startedAt: "2026-08-05T07:00:00Z",
        endedAt: "2026-08-05T07:10:00Z",
        phases: [{ phaseId: "phase-old", seq: 1, name: "old", status: "success" }],
        events: [{ eventId: "evt-old", phaseId: "phase-old", type: "log" }],
      },
    ]);
    copyFileSync(initialPath, dbPath);

    const adapter = createFactoryAdapter({ dbPath });
    const before = await adapter.poll();
    expect(runByAdwId(panelData(before), "adw-old").events).toHaveLength(1);

    rmSync(dbPath);
    const replacementPath = createFixtureDb([
      {
        adwId: "adw-new",
        repo: REPO_B,
        status: "running",
        request: "fresh run",
        startedAt: "2026-08-05T12:00:00Z",
        phases: [{ phaseId: "phase-new", seq: 1, name: "new", status: "running" }],
        events: [{ eventId: "evt-new", phaseId: "phase-new", type: "phase_start" }],
      },
    ]);
    copyFileSync(replacementPath, dbPath);

    const after = await adapter.poll();
    const data = panelData(after);
    expect(data.runs.some((run) => run.adwId === "adw-old")).toBe(false);
    expect(runByAdwId(data, "adw-new").events.map((event) => event.eventId)).toEqual(["evt-new"]);
  });

  test("opens the database readonly and never writes", () => {
    const dbPath = createFixtureDb([
      {
        adwId: "adw-ro",
        repo: REPO_A,
        status: "success",
        request: "readonly check",
        startedAt: "2026-08-05T06:00:00Z",
        endedAt: "2026-08-05T06:05:00Z",
        phases: [{ phaseId: "phase-ro", seq: 1, name: "ro", status: "success" }],
        events: [],
      },
    ]);

    const db = new Database(dbPath, { readonly: true });
    db.exec("PRAGMA busy_timeout = 5000");
    expect(() => {
      db.query("INSERT INTO sessions (adw_id, status) VALUES ('hack', 'fail')").run();
    }).toThrow();
    db.close();
  });

  test("documents the completed-session cap consistent with the visualizer", () => {
    expect(COMPLETED_SESSION_LIMIT).toBe(200);
  });

  test("projects pending decisions as inbox items with factory answer actions", async () => {
    const dbPath = createFixtureDb([
      {
        adwId: "adw-decide",
        repo: REPO_A,
        status: "running",
        request: "needs input",
        adwName: "adw_plan",
        startedAt: "2026-08-05T10:00:00Z",
        phases: [{ phaseId: "phase-decide", seq: 1, name: "plan", status: "running" }],
        events: [],
      },
    ]);
    const db = new Database(dbPath);
    db.query(
      `INSERT INTO decisions
         (decision_id, adw_id, phase, question, options, free_text, context, status, created_at)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
    ).run(
      "dec-1",
      "adw-decide",
      "phase-decide",
      "Ship with feature flag?",
      JSON.stringify([
        { value: "yes", label: "Enable flag", recommended: true },
        { value: "no", label: "Disable flag" },
      ]),
      0,
      "phase plan blocked on flag default",
      "pending",
      "2026-08-05T10:05:00Z",
    );
    db.close();

    const adapter = createFactoryAdapter({ dbPath });
    const result = await adapter.poll();
    const item = result.items.find((entry) => entry.id === "factory-decision-dec-1");
    expect(item).toBeDefined();
    expect(item!.source).toBe("factory");
    expect(item!.kind).toBe("decision");
    expect(item!.severity).toBe("act");
    expect(item!.project).toBe("overdeck");
    expect(item!.title).toBe("Ship with feature flag?");
    expect(item!.detail).toBe("phase plan blocked on flag default");
    expect(item!.decision).toEqual({
      question: "Ship with feature flag?",
      options: [
        { label: "Enable flag", recommended: true },
        { label: "Disable flag" },
      ],
      freeText: false,
      context: "phase plan blocked on flag default",
      waitingSince: "2026-08-05T10:05:00Z",
    });
    expect(item!.actions).toEqual([
      {
        verb: "factory.decision.answer",
        args: { decisionId: "dec-1", choice: "yes" },
        label: "Enable flag",
        recommended: true,
      },
      {
        verb: "factory.decision.answer",
        args: { decisionId: "dec-1", choice: "no" },
        label: "Disable flag",
      },
    ]);
  });

  test("does not project answered or canceled decisions", async () => {
    const dbPath = createFixtureDb([
      {
        adwId: "adw-done",
        repo: REPO_A,
        status: "success",
        request: "done",
        startedAt: "2026-08-05T09:00:00Z",
        endedAt: "2026-08-05T09:30:00Z",
        phases: [{ phaseId: "phase-done", seq: 1, name: "work", status: "success" }],
        events: [],
      },
    ]);
    const db = new Database(dbPath);
    db.query(
      `INSERT INTO decisions
         (decision_id, adw_id, question, options, free_text, context, status, created_at, answered_at)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
    ).run("dec-answered", "adw-done", "old?", "[]", 0, "", "answered", "2026-08-05T09:10:00Z", "2026-08-05T09:11:00Z");
    db.query(
      `INSERT INTO decisions
         (decision_id, adw_id, question, options, free_text, context, status, created_at)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
    ).run("dec-canceled", "adw-done", "canceled?", "[]", 0, "", "canceled", "2026-08-05T09:12:00Z");
    db.close();

    const adapter = createFactoryAdapter({ dbPath });
    const result = await adapter.poll();
    expect(result.items.some((entry) => entry.kind === "decision")).toBe(false);
  });

  test("projects pending decisions across multiple repos", async () => {
    const dbPath = createFixtureDb([
      {
        adwId: "adw-a",
        repo: REPO_A,
        status: "running",
        request: "repo a",
        startedAt: "2026-08-05T10:00:00Z",
        phases: [{ phaseId: "phase-a", seq: 1, name: "work", status: "running" }],
        events: [],
      },
      {
        adwId: "adw-b",
        repo: REPO_B,
        status: "running",
        request: "repo b",
        startedAt: "2026-08-05T10:01:00Z",
        phases: [{ phaseId: "phase-b", seq: 1, name: "work", status: "running" }],
        events: [],
      },
    ]);
    const db = new Database(dbPath);
    db.query(
      `INSERT INTO decisions
         (decision_id, adw_id, question, options, free_text, context, status, created_at)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
    ).run("dec-a", "adw-a", "A?", "[]", 1, "ctx a", "pending", "2026-08-05T10:02:00Z");
    db.query(
      `INSERT INTO decisions
         (decision_id, adw_id, question, options, free_text, context, status, created_at)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
    ).run("dec-b", "adw-b", "B?", "[]", 1, "ctx b", "pending", "2026-08-05T10:03:00Z");
    db.close();

    const adapter = createFactoryAdapter({ dbPath });
    const result = await adapter.poll();
    const decisions = result.items.filter((entry) => entry.kind === "decision");
    expect(decisions).toHaveLength(2);
    expect(decisions.map((entry) => entry.project).sort()).toEqual(["multideal", "overdeck"]);
  });

  test("panel runs carry the full decision history including answered decisions", async () => {
    const dbPath = createFixtureDb([
      {
        adwId: "adw-history",
        repo: REPO_A,
        status: "running",
        request: "history",
        startedAt: "2026-08-05T10:00:00Z",
        phases: [{ phaseId: "phase-h", seq: 1, name: "work", status: "running" }],
        events: [],
      },
    ]);
    const db = new Database(dbPath);
    db.query(
      `INSERT INTO decisions
         (decision_id, adw_id, phase, question, options, free_text, context, status, created_at)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
    ).run(
      "dec-open",
      "adw-history",
      "phase-h",
      "Open question?",
      JSON.stringify([{ value: "go", label: "Go", recommended: true }]),
      0,
      "still waiting",
      "pending",
      "2026-08-05T10:05:00Z",
    );
    db.query(
      `INSERT INTO decisions
         (decision_id, adw_id, question, options, free_text, context, status,
          answer_value, answered_by, created_at, answered_at)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
    ).run(
      "dec-closed",
      "adw-history",
      "Closed question?",
      JSON.stringify([{ value: "yes", label: "Yes" }]),
      0,
      "",
      "answered",
      "yes",
      "deck-web",
      "2026-08-05T10:01:00Z",
      "2026-08-05T10:02:00Z",
    );
    db.close();

    const adapter = createFactoryAdapter({ dbPath });
    const result = await adapter.poll();
    const run = runByAdwId(panelData(result), "adw-history");
    expect(run.decisions).toEqual([
      {
        decisionId: "dec-closed",
        phase: null,
        question: "Closed question?",
        options: [{ value: "yes", label: "Yes" }],
        freeText: false,
        context: "",
        status: "answered",
        answerValue: "yes",
        answerText: null,
        answeredBy: "deck-web",
        createdAt: "2026-08-05T10:01:00Z",
        answeredAt: "2026-08-05T10:02:00Z",
      },
      {
        decisionId: "dec-open",
        phase: "phase-h",
        question: "Open question?",
        options: [{ value: "go", label: "Go", recommended: true }],
        freeText: false,
        context: "still waiting",
        status: "pending",
        answerValue: null,
        answerText: null,
        answeredBy: null,
        createdAt: "2026-08-05T10:05:00Z",
        answeredAt: null,
      },
    ]);
  });
});
