test(scheduler-core): REPL step/continue equivalence to batch simulation (task 12.4)

Adds the final task of "Core: Interactive REPL" (Req 5 AC 22): proves
the REPL's own documented invariant -- "step/continue to completion
produces byte-identical output to batch --sim" (design.md SimDriver
Invariants).

test_repl_equivalence.c runs the same sched_sim_driver_run() on the
same 3-session/9-decision fixture twice: once batch (interactive=
false), once REPL-driven via freopen()'d scripted "step 1\ncontinue\n"
stdin into the real repl_command_loop()/readline() path (same
mechanism test_repl_execution_control.c established) -- then
structurally decodes both runs' --sim-output and compares decision
content. Test-only; zero production code changed.

RED-phase probe (planted in xce_repl_on_event(), reverted, zero
residue) confirmed the comparison is real: corrupting one job's id
under interactive mode broke exactly this test plus the 3 other REPL
tests that share the same hook, none else.

Scheduler suite: 37/37 clean (was 36/36). Task group 12 (Interactive
REPL, tasks 12.1-12.4) is now complete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JPPXeKSUXUqcqvzNXY1rzJ
This commit is contained in:
Rongsong Shen 2026-08-07 10:49:08 +00:00
parent 2329dbf2a8
commit 829fbe41dc
Signed by: rshen
SSH key fingerprint: SHA256:8Vf2pZ3h6rJa7EhGK+WaAQ9Mta/apZvteW6g9U/WKKk
4 changed files with 717 additions and 1 deletions

View file

@ -376,11 +376,12 @@
- _Depends: 12.1_
- _Verified: 2026-08-07 — new repl_params.c (fourth sibling in the repl.c/repl_break.c/repl_info.c family, repl_internal.h's own "Why a split at all" section extended again): `snapshot [FILE]` and `set speed|output|verify ...` (repl_cmd_snapshot()/repl_cmd_set()), reusing sim_record.c's sim_record_write_header_and_snapshot() (task 11.5) unchanged for both the one-shot standalone snapshot and `set output`'s live-stream preamble -- neither hand-rolls capnp encoding. `set speed <N>` mutates BOTH sim->sim_speed_num/sim_speed_den (the driver-level shadow copy) AND sim->core.speed_num/speed_den via the SAME atomic_store_explicit() pair sim_set_virtual_clock() (sim.c) uses at startup -- confirmed by reading core_clock.c's xce_core_now() that it reads the atomic pair fresh, uncached, on every call, so sim_loop.c needed zero new call sites (verified: `sim->sim_speed_num`/`sim->sim_speed_den` alone are never read by sim_loop.c or core_clock.c -- only the atomic `core.speed_num`/`core.speed_den` pair is; the shadow copy exists purely for the REPL/`info config` to read without reaching into `core`, per sim.h's own doc comment -- both fields are kept in sync by repl_set_speed() so this distinction never surfaces as a bug). `info config` (repl_info.c, alongside every other `info` dump) added as the read-only counterpart. `snapshot`'s auto-generated filename (`<basename>-snap-<event_index>.replay`) uses a documented fixed basename fallback ("sim") since xce_sched_sim_driver_t retains no string form of the replay path (sim.h has only a bare FILE*) -- a deliberate, documented deviation matching repl_count_jobs()'s own AC22-pseudocode-deviation precedent, judged not worth adding a retained-path field to sim.h/sim.c for. sim_loop.c untouched, still exactly 800/800 lines. New test_repl_snapshot_params.c (7 tests): the central Observable proved end to end via a forked child (`step 1`/`snapshot FILE`/`quit` against a real fixture) followed by a SECOND, real sched_sim_driver_start() call in the parent against the file the child wrote, asserting success, an exact reloaded snapshot_ts (2000, frozen clock), the pre-session job present and the post-session job absent (proves the snapshot captured the position at command time, not the whole file); plus auto-filename generation, `set speed`/`set verify` state+malformed-input unit tests, `set output` open/preamble-readback/off round trip, `info config` field-format assertions, and a `help` listing regression check. Every other sim-family white-box test file (`test_sim_equivalence.c`/`test_sim_event_loop.c`/`test_repl_breakpoints_inspection.c`/`test_sim_record_output.c`/`test_repl_execution_control.c`/`test_sim_alt_events.c`/`test_sim_verify.c`) needed a one-line `#include "../src/repl_params.c"` addition alongside their existing `repl.c` include -- repl.c's repl_dispatch() now unconditionally references repl_cmd_snapshot()/repl_cmd_set(), the same "unconditional extern forces every white-box TU to add the new source file" pattern this file family already established. RED phase: initial build failed exactly as expected (`meson setup` error "File src/repl_params.c does not exist") after wiring the test file, repl_internal.h extern declarations, and both meson.build files but before writing the implementation; after implementation, a real bug surfaced and was self-fixed before commit (an accidental `*/` inside a prose doc comment -- `xce_scheduler_t*/record_dir` -- prematurely closed a block comment, a variant of this codebase's known recurring `-Wcomment` pitfall). Synthetic RED-phase probe on the finished implementation: removed both `atomic_store_explicit()` calls from `repl_set_speed()` (leaving only the shadow-copy assignment), rebuilt, ran the suite: exactly 1 of 7 tests failed (`test_set_speed_updates_shadow_and_core_atomics`, "Expected 7 Was 0"), all 6 others stayed green; reverted, rebuilt, reconfirmed clean. test_sim_isolation.c extended to scan repl_params.c too (9-file precedent, seventh sched_sim_isolation_lib archive member). Final: 36/36 scheduler suite (was 35/35), whole-project build clean._
- [ ] 12.4 REPL equivalence end-to-end test
- [x] 12.4 REPL equivalence end-to-end test
- Run a fixture file through the REPL using step/continue to completion and through batch simulation mode, and assert both produce the same decision sequence
- Observable: both runs produce byte-identical decision sequences
- _Requirements: 5.22_
- _Depends: 12.2, 12.3_
- _Verified: 2026-08-07 — TEST-ONLY task, no new production code. New test_repl_equivalence.c (683 lines) runs the SAME sched_sim_driver_run() (sim_loop.c) against ONE hand-built `.replay` fixture (two queues at different priorities "high"=5/"normal"=1, four jobs split across THREE ReplaySession boundaries) twice: once in batch mode (interactive=false, straight through), once REPL-driven (interactive=true, stdin freopen()'d from a scripted "step 1\ncontinue\n" -- exactly 2 lines, sufficient and safe regardless of session count since CONTINUE mode never re-reads stdin once entered, avoiding the readline()-EOF-triggers-exit(0) trap). This is a DIFFERENT comparison than task 11.6's test_sim_equivalence.c (which compares normal-mode driver vs simulation driver) -- here both sides are the identical simulation driver, differing only in `sim->interactive`. Both runs set `sched_sim_args_t.output_path` directly (confirmed unconditional on `interactive` by reading sim.c's `args->output_path != NULL` gate), so both write a full `.replay`-format decision stream to their own file with no `set output` REPL command needed. Since the fixture has THREE session boundaries (not task 11.6's one), decoding uses a new decode_all_sessions() that scans the whole output file structurally via sim.c's shared `msg_root_has_shape()` (ReplaySession's distinct (1,2) word/pointer-count shape) rather than a fixed message-skip count. Comparator reuses sim_verify.c's leaf-level structural comparators (`sim_verify_job_alloc_eq()`/`sim_verify_job_preason_eq()`, both `static` but visible via white-box #include) through a duplicated `replay_decision_content_eq()`, same technique test_sim_equivalence.c's own function of that name establishes. Non-tautology guards: session 1 asserted to have EXACTLY 2 decisions (an unambiguous floor -- only 2 jobs could possibly be pending that early), job 501 independently pinned as present in session 1's batch-side content, total batch decisions asserted >= 4, and every decision's type independently pinned as PEND (matching sim_picker_thread_fn()'s documented DISPATCH gap) BEFORE the cross-run equality check -- so a buggy always-`return 1` comparator could not pass silently. Observed (not hand-predicted) that a PEND'd job persists as a picker candidate into later sessions (session 1: jobs 501/502 -> 2 decisions; session 2: 501/503/502 -> 3; session 3: 501/503/502/504 -> 4; total 9 on both sides), confirming the fixture is genuinely non-trivial without needing a fragile hardcoded per-session prediction. Synthetic RED-phase probe: analysis first showed that defeating `repl_should_pause()` (task 12.1's own already-verified probe target) would NOT turn this test red, because that only changes *when* stdin is read/control returns, never *what* gets processed -- every session still eventually completes regardless of pause timing, and this file's own CONTINUE-is-sticky script design tolerates any pause schedule. The correct probe for THIS invariant ("never alters ... what events are processed") had to touch content, and only under `sim->interactive`: temporarily added one line to `xce_repl_on_event()` (repl_break.c) -- which takes a non-const `xce_core_event_t*` and fires (via repl.h's `sim->interactive`-gated wrapper) strictly before an event joins the batch `xce_core_push_events()` later applies -- corrupting a JOBS_PUSH job's jobid (`+= 1000000`) whenever interactive. Rebuilt, ran the full scheduler suite: exactly 4/37 failed (this new test plus three pre-existing REPL tests that also exercise JOBS_PUSH under interactive mode -- sched_repl_execution_control_test, sched_repl_snapshot_params_test, sched_repl_breakpoints_inspection_test -- all correctly detecting the same real divergence), all 33 others stayed green. This test's own failure output: "REPL step/continue-to-completion must produce a byte-identical decision to batch mode at this session and position" (jobid 501 on the batch side vs 1000501 on the REPL side, directly visible in the two runs' printed PEND lines). Reverted (diff-confirmed byte-identical to the pre-probe file), rebuilt, reconfirmed 37/37 scheduler suite (was 36/36), whole-project build clean.
- [ ] 13. Run full cross-phase HA soak regression
- Run sequential and chaotic HA soak rounds against the complete feature (all three phases integrated), confirming zero job loss, divergence, or liveness stall attributable to this feature across the full run