← Files Codex ReplayARCHIVED FILE

tests/test_controller_batch_ui.py

17.8 KB · Oct 2, 2026 · 00:19 UTC

↓ Download file

"""Multiple-thread behavior over the controller's existing configuration and reports."""

from __future__ import annotations

import importlib.util
import subprocess
import unittest
from pathlib import Path

_spec = importlib.util.spec_from_file_location(
    "replay_range_test_harness", Path(__file__).with_name("test_controller_range_ui.py")
)
if _spec is None or _spec.loader is None:
    raise AssertionError("Cannot load the local controller test harness.")
_harness = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_harness)
CONTROLLER = _harness.CONTROLLER
HARNESS = (
    _harness.HARNESS.replace(
        "return {state, initialize, selectThread,",
        "return {state, initialize, selectThread, toggleThreadSelection, configureSelectedThreads, "
        "selectBatchThread, startThreadBatch, aggregateModelResults, selectThreadResult, "
        "renderAggregateResults, renderThreadStep,",
    )
    .replace(
        "schedulePoll = () => {};",
        "schedulePoll = () => { state.scheduled = (state.scheduled || 0) + 1; };",
    )
    .replace("async function main() {", "async function rangeMain() {")
    .replace(
        "main().catch((error) => { process.stderr.write(error.stack); process.exitCode = 1; });", ""
    )
    + r"""
const threads = [thread, {...thread, imported_thread_id: "thread-2", title: "Second thread", source_path: "/tmp/second.jsonl"}];
let batchStarts = 0, reportReads = 0;
function batchController() {
  const c = make();
  const original = c.setTool(async (name, args) => {
    if (name === "list_threads") return {threads, total: threads.length};
    if (name === "prepare_run" && args.configurations) {
      return {ready: true, run_config: args, prepare_token: "batch-token"};
    }
    if (name === "start_run" && args.configurations) {
      batchStarts += 1;
      const launched = args.configurations.flatMap(config => config.models.map(model => {
        const run = {...config, model, run_id: `batch-${++runCounter}`, controller_session_id: "session-1",
          batch_id: "batch-1", status: "running", phase: "queued"};
        runs.set(run.run_id, run);
        return {run, model};
      }));
      return {runs: launched};
    }
    if (name === "get_report") {
      reportReads += 1;
      return {report: {original_request: runs.get(args.run_id).request,
        evaluation: {totals: {A: .9, B: .8}, candidate_mapping: {A: "codex", B: "claude"}},
        estimated_cost: {claude: {usd: 2}, codex: {usd: 1}}}};
    }
    const result = await original(name, args);
    if (["inspect_thread", "prepare_run"].includes(name)) {
      result.replay = {...result.replay, ...threads.find(item => item.imported_thread_id === args.thread_id)};
    }
    return result;
  });
  return c;
}
async function selected() {
  const c = batchController();
  await c.initialize();
  c.toggleThreadSelection("thread-1", 1);
  c.toggleThreadSelection("thread-2", 2);
  await c.configureSelectedThreads();
  return c;
}
async function main() {
  const scenario = process.argv[2];
  if (scenario === "failed_reload") {
    runs.set("failed-child", {run_id: "failed-child", controller_session_id: "session-1", batch_id: "failed-batch",
      thread_id: "thread-1", model: "model-1", status: "failed", launch_failed: true});
    const c = batchController();
    const original = c.setTool(async (name, args) => {
      const result = await original(name, args);
      if (name === "get_state") result.state.batch = {id: "failed-batch", thread_count: 2,
        models: ["model-1"], starting: false,
        threads: threads.map(item => ({thread_id: item.imported_thread_id, thread_title: item.title})),
        errors: threads.map(item => ({thread_id: item.imported_thread_id, thread_title: item.title, model: "model-1", error: "Launch failed"}))};
      return result;
    });
    await c.initialize();
    assert.equal(c.state.step, "run");
    assert.equal(c.state.batchId, "failed-batch");
    assert.equal(c.state.runErrors.length, 2);
    assert.equal(c.state.runs.length, 0);
    assert.equal(c.canNavigateTo("thread"), false);
    assert.equal(c.canNavigateTo("configure"), false);
    await c.startThreadBatch();
    assert.equal(batchStarts, 0);
    await c.refreshRun();
    assert.equal(c.state.batchId, "failed-batch");
    assert.equal(c.state.runs.length, 0);
    assert.equal(c.aggregateModelResults()[0].failed, 2);
    return;
  }
  if (scenario === "manifest_membership") {
    availableModels.push({id: "model-2", label: "Other model"});
    for (const [index, item] of threads.entries()) runs.set(`manifest-${index}`, {
      run_id: `manifest-${index}`, controller_session_id: "session-1", batch_id: "batch-1",
      thread_id: item.imported_thread_id, thread_title: item.title, model: "model-1",
      status: index ? "failed" : "completed", request: "Original request",
    });
    const c = batchController();
    const original = c.setTool(async (name, args) => {
      const result = await original(name, args);
      if (name === "get_state") result.state.batch = {id: "batch-1", thread_count: 2,
        models: ["model-1", "model-2"], starting: false,
        threads: threads.map(item => ({thread_id: item.imported_thread_id, thread_title: item.title})),
        errors: threads.map(item => ({thread_id: item.imported_thread_id, thread_title: item.title, model: "model-2", error: "Launch failed"}))};
      return result;
    });
    await c.initialize();
    await c.refreshRun();
    assert.deepEqual(c.state.selectedModels, ["model-1", "model-2"]);
    assert.equal(c.state.runErrors.length, 2);
    assert.equal(c.state.selectedThreads.length, 2);
    const failed = c.aggregateModelResults().find(entry => entry.model === "model-2");
    assert.deepEqual([failed.total, failed.ready, failed.failed], [2, 0, 2]);
    assert.ok(c.renderAggregateResults().includes("2 of 2 threads finished"));
    assert.ok(c.renderAggregateResults().includes("1 of 2 comparisons ready"));
    assert.equal(c.renderAggregateResults().includes("1 of 1 comparisons ready"), false);
    return;
  }
  if (scenario === "selection") {
    const c = batchController();
    await c.initialize();
    await c.configureSelectedThreads();
    assert.equal(c.state.step, "thread");
    assert.equal(calls.some(item => item.name === "inspect_thread"), false);
    c.toggleThreadSelection("thread-1", 1);
    await c.configureSelectedThreads();
    assert.equal(c.state.selectedThreadId, "thread-1");
    assert.ok(c.renderConfigureStep().includes("Split into chunks"));
    await c.changeReplayMode("chunks");
    c.state.step = "thread";
    await c.configureSelectedThreads();
    assert.equal(c.state.replayMode, "chunks");
    c.state.step = "thread";
    c.toggleThreadSelection("thread-1", 1);
    await c.configureSelectedThreads();
    assert.equal(c.state.step, "thread");
    assert.equal(c.state.selectedThreads.length, 0);
    assert.equal(c.canNavigateTo("configure"), false);
    return;
  }
  if (scenario === "chunks_to_batch") {
    const c = batchController();
    await c.initialize();
    c.toggleThreadSelection("thread-1", 1);
    await c.configureSelectedThreads();
    c.state.reviewDraft.request = "Edited whole thread task";
    await c.changeReplayMode("chunks");
    await c.toggleDivider("u2");
    await c.editReplayUnit(c.replayUnits()[1].key);
    c.state.reviewDraft.request = "Only the later chunk";
    assert.equal(c.state.reviewDraft.message_uuid, "u2");
    c.state.step = "thread";
    c.toggleThreadSelection("thread-2", 2);
    await c.configureSelectedThreads();
    assert.equal(c.state.replayMode, "whole");
    assert.equal(c.state.reviewDraft.request, "Edited whole thread task");
    assert.equal(c.state.reviewDraft.message_uuid, "u1");
    await c.startThreadBatch();
    assert.equal(c.state.runs[0].run.request, "Edited whole thread task");
    assert.equal(c.state.runs[0].run.message_uuid, "u1");
    assert.equal("start_message_uuid" in c.state.runs[0].run, false);
    return;
  }
  const c = await selected();
  assert.equal(c.state.selectedThreads.length, 2);
  assert.equal(c.renderConfigureStep().includes("Split into chunks"), false);
  c.state.reviewDraft.request = "Edited first request";
  c.state.classifications["seed.txt"] = "exclude";
  c.state.attributionEdits = ["seed.txt"];
  await c.selectBatchThread("thread-2");
  c.state.reviewDraft.request = "Edited second request";
  await c.selectBatchThread("thread-1");
  assert.equal(c.state.reviewDraft.request, "Edited first request");
  assert.equal(c.state.classifications["seed.txt"], "exclude");
  assert.deepEqual(c.state.selectedModels, ["model-1"]);
  if (scenario === "inspection_failure") {
    const original = c.setTool(async (name, args) => {
      if (name === "inspect_thread" && args.thread_id === "thread-2") throw new Error("Temporary inspection failure");
      return original(name, args);
    });
    await c.selectBatchThread("thread-2");
    assert.equal(c.state.reviewDraft.request, "Edited second request");
    assert.ok(c.state.error.includes("Temporary inspection failure"));
    c.saveDraft();
    const reloaded = batchController();
    const reloadTool = reloaded.setTool(async (name, args) => {
      if (name === "inspect_thread") throw new Error("Inspection still unavailable after reload");
      return reloadTool(name, args);
    });
    await reloaded.initialize();
    assert.equal(reloaded.state.reviewDraft.request, "Edited second request");
    assert.deepEqual(reloaded.state.reviewDraft.models, ["model-1"]);
    return;
  }
  if (scenario === "draft_reload") {
    c.saveDraft();
    const reloaded = batchController();
    await reloaded.initialize();
    assert.equal(reloaded.state.selectedThreads.length, 2);
    assert.equal(reloaded.state.reviewDraft.request, "Edited first request");
    await reloaded.selectBatchThread("thread-2");
    assert.equal(reloaded.state.reviewDraft.request, "Edited second request");
    return;
  }
  if (scenario === "blocked") {
    const original = c.setTool(async (name, args) => {
      if (name === "prepare_run" && args.configurations) return {ready: false,
        preparations: [{thread_id: "thread-1", preparation: {ready: true}},
          {thread_id: "thread-2", preparation: {ready: false, blockers: ["Historical files changed"]}}]};
      return original(name, args);
    });
    await c.startThreadBatch();
    assert.equal(batchStarts, 0);
    assert.equal(c.state.selectedThreadId, "thread-2");
    assert.ok(c.renderConfigureStep().includes("Historical files changed"));
    return;
  }
  if (scenario === "lost_launch") {
    const original = c.setTool(async (name, args) => {
      const result = await original(name, args);
      if (name === "start_run") throw new Error("Lost launch response");
      if (name === "get_state") result.state.batch = {id: "batch-1", thread_count: 2, models: ["model-1"], starting: false,
        threads: threads.map(item => ({thread_id: item.imported_thread_id, thread_title: item.title})), errors: []};
      return result;
    });
    await c.startThreadBatch();
    assert.equal(c.state.batchId, "batch-1");
    assert.equal(c.state.step, "run");
    await c.refreshRun();
    assert.equal(c.state.runs.length, 2);
    await c.startThreadBatch();
    assert.equal(batchStarts, 1);
    return;
  }
  if (scenario === "sibling_review") {
    c.state.models.push({id: "model-2", label: "Other model"});
    c.setSelectedModels(["model-1", "model-2"]);
    const original = c.setTool(async (name, args) => {
      const result = await original(name, args);
      if (name === "get_report") {
        const run = runs.get(args.run_id);
        const siblingFinished = [...runs.values()].some(other => other.thread_id === run.thread_id &&
          other.model === "model-2" && other.status === "completed");
        if (siblingFinished) result.report.evaluation.totals = {A: .7, B: .8};
      }
      return result;
    });
    await c.startThreadBatch();
    const [first, sibling, unrelated] = c.state.runs;
    const complete = entry => runs.set(entry.id, {...runs.get(entry.id), status: "completed"});
    complete(first);
    await c.refreshRun();
    c.selectThreadResult("thread-1");
    assert.equal(first.report.evaluation.totals.A, .9);
    assert.equal(reportReads, 1);
    complete(unrelated);
    await c.refreshRun();
    assert.equal(reportReads, 2);
    assert.equal(first.report.evaluation.totals.A, .9);
    complete(sibling);
    await c.refreshRun();
    assert.equal(reportReads, 4);
    assert.equal(first.report.evaluation.totals.A, .7);
    assert.equal(c.state.runId, first.id);
    await c.refreshRun();
    assert.equal(reportReads, 4);
    const fourth = c.state.runs[3];
    let failOnce = true;
    const beforeRetry = c.state.scheduled || 0;
    const beforeFailure = c.setTool(async (name, args) => {
      if (name === "get_report" && args.run_id === unrelated.id && failOnce) {
        failOnce = false;
        throw new Error("Temporary report read failure");
      }
      return beforeFailure(name, args);
    });
    complete(fourth);
    await c.refreshRun({continuePolling: true});
    assert.equal(unrelated.needsReportRefresh, true);
    assert.ok(c.state.scheduled > beforeRetry);
    assert.ok(c.state.error.includes("Retrying automatically"));
    await c.refreshRun();
    assert.equal(unrelated.needsReportRefresh, false);
    assert.equal(c.state.error, "");
    return;
  }
  await c.startThreadBatch();
  assert.equal(batchStarts, 1);
  assert.equal(c.state.runs.length, 2);
  assert.deepEqual(c.state.runs.map(entry => entry.run.request), ["Edited first request", "Edited second request"]);
  assert.equal(c.state.batchId, "batch-1");
  const first = c.state.runs[0], second = c.state.runs[1];
  runs.get(first.id).status = "completed";
  await c.refreshRun();
  assert.equal(c.state.step, "run");
  assert.equal(c.canNavigateTo("results"), true);
  assert.equal(reportReads, 1);
  assert.ok(c.renderAggregateResults().includes("1 of 2 threads finished"));
  let aggregate = c.aggregateModelResults()[0];
  assert.deepEqual([aggregate.ready, aggregate.paired, aggregate.claude, aggregate.codex], [1, 1, 2, 1]);
  assert.deepEqual(aggregate.quality, {wins: 1, ties: 0, losses: 0, unavailable: 0});
  c.selectThreadResult("thread-1");
  assert.equal(c.state.step, "results");
  assert.equal(c.state.runId, first.id);
  await c.refreshRun();
  assert.equal(reportReads, 1);
  assert.equal(c.state.runId, first.id);
  if (scenario === "partial_reload") {
    const reloaded = batchController();
    await reloaded.initialize();
    assert.equal(reloaded.state.batchId, "batch-1");
    assert.equal(reloaded.state.runs.length, 2);
    await reloaded.refreshRun();
    assert.equal(reloaded.canNavigateTo("results"), true);
    assert.ok(reloaded.renderAggregateResults().includes("1 of 2 threads finished"));
    reloaded.selectThreadResult("thread-1");
    assert.ok(reloaded.renderResultsStep().includes("Edited first request"));
    assert.equal(batchStarts, 1);
    return;
  }
  runs.get(second.id).status = "completed";
  await c.refreshRun();
  assert.equal(reportReads, 2);
  assert.equal(c.state.runId, first.id);
  assert.equal(c.state.batchView, "individual");
  assert.ok(c.renderResultsStep().includes("Edited first request"));
  assert.ok(c.renderAggregateResults().includes("2 of 2 threads finished"));
  assert.deepEqual(c.aggregateModelResults()[0].quality, {wins: 2, ties: 0, losses: 0, unavailable: 0});
  second.report.historical_usage_shared = true;
  first.report.estimated_cost.codex.usd = null;
  aggregate = c.aggregateModelResults()[0];
  assert.equal(aggregate.paired, 0);
  assert.equal(c.renderAggregateResults().includes("$0.0000"), false);
  first.report.estimated_cost = {claude: {usd: 0}, codex: {usd: 0}};
  assert.equal(c.aggregateModelResults()[0].paired, 1);
  assert.ok(c.renderAggregateResults().includes("Cost difference</dt><dd>Unavailable"));
  first.report.evaluation = {totals: {A: .899, B: .901}, candidate_mapping: {A: "codex", B: "claude"}};
  second.report.evaluation = {totals: {A: .8, B: .9}, candidate_mapping: {A: "codex", B: "claude"}};
  assert.deepEqual(c.aggregateModelResults()[0].quality, {wins: 0, ties: 1, losses: 1, unavailable: 0});
}
main().catch((error) => { process.stderr.write(error.stack); process.exitCode = 1; });
"""
)


class ControllerBatchTests(unittest.TestCase):
    def check_scenario(self, scenario: str) -> None:
        result = subprocess.run(
            ["node", "-e", HARNESS, str(CONTROLLER), scenario],
            capture_output=True,
            text=True,
            check=False,
        )
        self.assertEqual(result.returncode, 0, result.stdout + result.stderr)

    def test_one_thread_keeps_chunking_and_zero_selection_cannot_continue(self) -> None:
        self.check_scenario("selection")

    def test_per_thread_edits_survive_switching_and_reload(self) -> None:
        self.check_scenario("draft_reload")

    def test_batch_blocker_opens_the_affected_thread_without_launching(self) -> None:
        self.check_scenario("blocked")

    def test_partial_reports_and_aggregates_update_without_changing_the_open_report(self) -> None:
        self.check_scenario("results")

    def test_reload_recovers_partial_results_without_starting_again(self) -> None:
        self.check_scenario("partial_reload")

    def test_reload_retains_all_failed_batch_errors_and_does_not_start_again(self) -> None:
        self.check_scenario("failed_reload")

    def test_lost_launch_response_recovers_runs_without_starting_again(self) -> None:
        self.check_scenario("lost_launch")

    def test_late_model_sibling_refreshes_its_shared_review_without_reloading_other_threads(
        self,
    ) -> None:
        self.check_scenario("sibling_review")

    def test_manifest_keeps_models_with_only_launch_failures_after_reload(self) -> None:
        self.check_scenario("manifest_membership")

    def test_failed_reinspection_preserves_edited_thread_draft(self) -> None:
        self.check_scenario("inspection_failure")

    def test_converting_chunks_to_multiple_threads_restores_the_whole_thread_draft(self) -> None:
        self.check_scenario("chunks_to_batch")


if __name__ == "__main__":
    unittest.main()

SHA-256: e4fbfb7548e0cfd1d8666e6e6490f3c391d4d013f63b59c071f05854d303fd9e