feat(scheduler-sim-repl-verbosity): task 4.2 - retire redundant per-decision echo

Gates sim_print_decision() so the bare per-decision line no longer
prints under continue mode, making the Round Rollup's summary the
sole source of decision output (Requirement 2.3, closing bug-1610).
step mode and non-interactive/headless runs are completely
unaffected (Requirements 5.1, 5.4). Implemented as a file-local
inline wrapper in sim_loop.c rather than repl.h, since the gated
function is static to that file and repl.h can't see repl_state's
internals without leaking them to every other repl.h consumer.

Fixes 3 pre-existing tests (predating this spec, from scheduler-core)
that pinned the now-intentionally-changed bare-line behavior under
continue mode - each fixed differently based on its actual purpose
rather than uniformly: two kept continue mode with their anchor
switched to the Round Rollup's summary (since they specifically test
continue-mode breakpoint interruption, which step mode would
trivialize), one switched to step mode (since its actual invariant -
print-before-breakpoint-check call order - is mode-independent and
continue mode no longer has any observable anchor for it).

Review caught a second, unguarded sim_print_decision() call site in
sim_reconcile_unaccounted_jobs() (added by an earlier, separately-
merged spec) that design.md's boundary never accounted for - per the
review, this is actually the dominant real-world path for DISPATCH
decisions in replay mode, so leaving it unguarded would have defeated
Requirement 2.3's "sole source of decision output" claim for the
common case. Gated it through the same wrapper; added 2 new tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAWMhfRJFnmmnG8h97yUDd
This commit is contained in:
Rongsong Shen 2026-09-02 15:10:01 +00:00
parent 3d08a43a35
commit d9109b7c4b
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
7 changed files with 386 additions and 39 deletions

View file

@ -1768,12 +1768,12 @@ static void test_event_breakpoint_pauses_repl_at_matching_event(void)
char aux_dir[PATH_MAX];
char script_path[PATH_MAX];
char capture_path[PATH_MAX];
char pend_marker[80];
char round_marker[96];
char jobid_buf[64];
FILE* f;
char* captured;
char* bp_pos;
char* pend_pos;
char* round_pos;
f = write_header_and_snapshot(replay_path, sizeof(replay_path),
"_evbp");
@ -1798,14 +1798,24 @@ static void test_event_breakpoint_pauses_repl_at_matching_event(void)
captured = run_group_d_and_capture(replay_path, script_path, capture_path);
/* bug-1591: sim_print_decision() (sim_loop.c) now renders jobids via
* fmt_jobid(), not raw %llu -- this marker must match that same
* human-readable form. */
/* scheduler-sim-repl-verbosity task 4.2 (Req 2.3, bug-1610): under
* `continue` mode sim_print_decision()'s bare per-decision line no
* longer prints -- the Round Rollup (repl_rollup.c) reports the
* session's decision instead, deferred until the collapsed range
* ends. This fixture has exactly one round, so that range is only
* flushed (and printed) by xce_repl_on_replay_complete()'s
* repl_round_collapse_flush() call, triggered by the script's
* trailing `quit`. Look for that round-rollup summary's
* "jobids=<...>" field instead of the old bare "PEND jobid=<...>"
* line as evidence the session's decision was eventually processed
* -- bug-1591's fmt_jobid()-vs-raw-%llu fix applies equally to
* repl_fmt_jobid_range()'s single-jobid rendering (repl.c), which is
* byte-identical to fmt_jobid()'s own non-array format. */
fmt_jobid(jobid_buf, sizeof(jobid_buf), 4242ULL);
snprintf(pend_marker, sizeof(pend_marker), "PEND jobid=%s", jobid_buf);
snprintf(round_marker, sizeof(round_marker), "jobids=%s", jobid_buf);
bp_pos = strstr(captured, "hit breakpoint 1 @ event #4: JOB_STATE_CHANGE");
pend_pos = strstr(captured, pend_marker);
bp_pos = strstr(captured, "hit breakpoint 1 @ event #4: JOB_STATE_CHANGE");
round_pos = strstr(captured, round_marker);
TEST_ASSERT_NOT_NULL_MESSAGE(
bp_pos, "the breakpoint must fire exactly at event #4 (jobid=222), "
@ -1813,11 +1823,12 @@ static void test_event_breakpoint_pauses_repl_at_matching_event(void)
TEST_ASSERT_NULL_MESSAGE(
strstr(captured, "@ event #3"),
"the breakpoint must NOT have fired on event #3 (jobid=111)");
TEST_ASSERT_NOT_NULL_MESSAGE(pend_pos,
TEST_ASSERT_NOT_NULL_MESSAGE(round_pos,
"the session's own PEND decision must "
"still have run after the resumed "
"still have run (reported via the round "
"rollup summary) after the resumed "
"breakpoint pause");
TEST_ASSERT_TRUE_MESSAGE(bp_pos < pend_pos,
TEST_ASSERT_TRUE_MESSAGE(bp_pos < round_pos,
"the event breakpoint (mid-batch, before "
"push_events) must be reported BEFORE the "
"session's own decision output");
@ -1854,10 +1865,24 @@ static void test_decision_breakpoint_pauses_after_decision_output(void)
make_tmp_dir(aux_dir, sizeof(aux_dir), "_decbpaux");
snprintf(script_path, sizeof(script_path), "%s/script.txt", aux_dir);
snprintf(capture_path, sizeof(capture_path), "%s/capture.out", aux_dir);
/* task 12.5 (bug-1594): trailing `quit` -- see the event-breakpoint
* test above for why. */
write_text_file(script_path,
"break DECISION type=PEND\ncontinue\ncontinue\nquit\n");
/* scheduler-sim-repl-verbosity task 4.2 (Req 2.3, bug-1610): `continue`
* mode now suppresses sim_print_decision()'s bare per-decision line
* entirely (the Round Rollup reports it instead, deferred until the
* collapsed range ends), so this fixture's single round would leave no
* "PEND jobid=..." text to anchor the print-before-breakpoint-check
* ordering this test exists to prove. That ordering
* (sim_print_decision() then xce_repl_on_decision()'s breakpoint
* check, sim_loop.c) is a mode-independent call-sequence invariant --
* xce_repl_check_decision() (repl.h) gates only on `sim->interactive`,
* never on REPL mode -- so `step` mode observes it exactly as well as
* `continue` did, and `step` still prints the bare per-decision line
* unconditionally (Requirement 5.1, unaffected by task 4.2). `step 1`
* pauses after session 1 completes, but the decision breakpoint below
* fires and nests its own repl_command_loop() first, so the trailing
* `quit` is read from within THAT nested loop -- same "quit ends the
* child deterministically" requirement as the `continue` scripts above,
* just reached via a different pause mechanism. */
write_text_file(script_path, "break DECISION type=PEND\nstep 1\nquit\n");
captured = run_group_d_and_capture(replay_path, script_path, capture_path);

View file

@ -526,14 +526,14 @@ static void test_pre_replay_pause_blocks_before_any_event_is_processed(void)
char aux_dir[PATH_MAX];
char script_path[PATH_MAX];
char capture_path[PATH_MAX];
char pend_marker[80];
char round_marker[96];
char jobid_buf[64];
FILE* f;
pid_t pid;
int status;
char* captured;
char* ready_pos;
char* pend_pos;
char* round_pos;
f = write_header_and_snapshot(replay_path, sizeof(replay_path),
"_prepause");
@ -574,23 +574,34 @@ static void test_pre_replay_pause_blocks_before_any_event_is_processed(void)
captured = read_whole_file(capture_path);
/* bug-1591: sim_print_decision() (sim_loop.c) now renders jobids via
* fmt_jobid(), not raw %llu -- this marker must match that same
* human-readable form. */
/* scheduler-sim-repl-verbosity task 4.2 (Req 2.3, bug-1610): under
* `continue` mode sim_print_decision()'s bare per-decision line no
* longer prints -- the Round Rollup (repl_rollup.c) reports the
* session's decision instead, deferred until the collapsed range
* ends. This fixture has exactly one round, so that range is only
* flushed (and printed) by xce_repl_on_replay_complete()'s
* repl_round_collapse_flush() call, triggered here by the script's
* trailing `quit`. Look for that round-rollup summary's
* "jobids=<...>" field instead of the old bare "PEND jobid=<...>"
* line as evidence the fixture's one session produced a live
* decision -- bug-1591's fmt_jobid()-vs-raw-%llu fix applies equally
* to repl_fmt_jobid_range()'s single-jobid rendering (repl.c), which
* is byte-identical to fmt_jobid()'s own non-array format. */
fmt_jobid(jobid_buf, sizeof(jobid_buf), 4242ULL);
snprintf(pend_marker, sizeof(pend_marker), "PEND jobid=%s", jobid_buf);
snprintf(round_marker, sizeof(round_marker), "jobids=%s", jobid_buf);
ready_pos = strstr(captured, "[sim] ready. type 'help' for commands.");
pend_pos = strstr(captured, pend_marker);
round_pos = strstr(captured, round_marker);
TEST_ASSERT_NOT_NULL_MESSAGE(ready_pos,
"pre-replay pause banner must be printed");
TEST_ASSERT_NOT_NULL_MESSAGE(pend_pos,
TEST_ASSERT_NOT_NULL_MESSAGE(round_pos,
"the fixture's one session must have "
"produced a live PEND decision");
TEST_ASSERT_TRUE_MESSAGE(ready_pos < pend_pos,
"produced a live PEND decision, reported "
"via the round rollup summary");
TEST_ASSERT_TRUE_MESSAGE(ready_pos < round_pos,
"the pre-replay pause banner must appear "
"BEFORE any decision output -- proves no "
"BEFORE any round-rollup output -- proves no "
"event was processed before the pause");
free(captured);

View file

@ -22,6 +22,24 @@
* decision uses, and that an already-accounted job produces NO duplicate
* report through this mechanism (Requirement 3.3).
*
* **Correction (post-review, scheduler-sim-repl-verbosity task 4.2
* remediation):** the reporting channel's console-print leg is now
* sim_print_decision_gate(sim, d), not a bare sim_print_decision(d) --
* review found this file's own reconciliation call site had been missed by
* task 4.2's original single-call-site scope (bug-1610). The two tests at
* the bottom of this file (test_reconcile_continue_mode_with_repl_state_
* suppresses_bare_decision_line / test_reconcile_step_mode_with_repl_
* state_still_prints_bare_decision_line) pin that gate's contract at THIS
* call site specifically, using the same repl_state_ensure()-based
* direct-call fixture technique test_repl_round_rollup.c's own fixtures
* establish. test_reconcile_reports_reconstructed_decision_through_
* existing_channel above (task 2.4's original test) deliberately leaves
* sim->repl_state NULL and continues to assert the bare line IS present --
* empirically confirmed unaffected by this remediation, since a NULL
* repl_state falls through the gate's own NULL-safety default ("never
* load-bearing, degrade to print") exactly like every other REPL hook in
* this codebase.
*
* sim_reconcile_unaccounted_jobs() is `static` and not yet called from
* anywhere in production code (task 3 wires it into
* sched_sim_driver_run()'s round-boundary sequence) -- called directly here
@ -1377,6 +1395,195 @@ test_reconcile_multiple_jobs_captured_independently_across_different_rounds(
"different_rounds: PASS\n");
}
/* ---------------------------------------------------------------------- */
/* Test 16 (scheduler-sim-repl-verbosity task 4.2 remediation, post-review: */
/* review found this file's own second production call site to */
/* sim_print_decision() -- inside sim_reconcile_unaccounted_jobs() itself */
/* -- was never gated by sim_print_decision_gate() (sim_loop.c), the ONE */
/* other call site task 4.2 originally wired. Since this reconciliation */
/* path is a PRIMARY source of DISPATCH decisions surfacing in replay mode */
/* (most live-drain-loop activity is PEND), a round where it fires used to */
/* show BOTH this bare "[sim] DISPATCH jobid=..." line AND the Round */
/* Rollup's summary counting the same dispatch -- the exact bug-1610 */
/* duplication this whole task exists to eliminate, just via this second */
/* call site. This test proves the fix: with sim->interactive == true AND */
/* a properly-allocated sim->repl_state in XCE_REPL_MODE_CONTINUE, the bare */
/* line from THIS mechanism must no longer appear -- the Round Rollup's own */
/* summary becomes the sole source of decision output here too, matching */
/* design.md's Requirement 2.3 ("the round summary is the sole source of */
/* decision output") for real this time. */
/* ---------------------------------------------------------------------- */
static void
test_reconcile_continue_mode_with_repl_state_suppresses_bare_decision_line(
void)
{
xce_sched_sim_driver_t sim;
xce_sched_job_t* job;
xce_job_alloc_t* jalloc;
replay_session_t* decoded;
sched_record_session_ctx_t* record_ctx;
char tmp[] = "/tmp/xce_simreconcile_gate_continue_XXXXXX";
int fd = mkstemp(tmp);
int saved_stdout;
off_t len;
char* captured;
fixture_init(&sim);
job = add_job(&(sim.core), 7060ULL, JOB_STAT_RUN, NULL);
TEST_ASSERT_NULL(job->accounts);
jalloc = make_test_job_alloc_with_host(7060ULL, "worker9", "slots", 3.0f);
decoded
= make_decoded_session(1000ULL, make_dispatch_entry_from_alloc(jalloc));
record_ctx = sched_record_session_ctx_new(1000ULL);
TEST_ASSERT_NOT_NULL(record_ctx);
/* The gate-relevant fixture shape (unlike test_reconcile_reports_
* reconstructed_decision_through_existing_channel above, which
* deliberately leaves repl_state NULL to prove the OTHER half of the
* gate's NULL-safety contract): a genuinely allocated repl_state in
* XCE_REPL_MODE_CONTINUE, the same repl_state_ensure() technique
* test_repl_round_rollup.c's own fixtures use for a direct-call (no
* real REPL command loop) test. */
sim.interactive = true;
sim.repl_state = repl_state_ensure(&sim);
TEST_ASSERT_NOT_NULL(sim.repl_state);
sim.repl_state->mode = XCE_REPL_MODE_CONTINUE;
TEST_ASSERT_TRUE_MESSAGE(fd >= 0, "mkstemp for stdout capture must "
"succeed");
fflush(stdout);
saved_stdout = dup(STDOUT_FILENO);
TEST_ASSERT_TRUE(saved_stdout >= 0);
TEST_ASSERT_TRUE(dup2(fd, STDOUT_FILENO) >= 0);
sim_reconcile_unaccounted_jobs(&sim, decoded, record_ctx);
fflush(stdout);
dup2(saved_stdout, STDOUT_FILENO);
close(saved_stdout);
len = lseek(fd, 0, SEEK_CUR);
TEST_ASSERT_TRUE(len >= 0);
captured = (char*)calloc(1, (size_t)len + 1);
TEST_ASSERT_NOT_NULL(captured);
TEST_ASSERT_TRUE(lseek(fd, 0, SEEK_SET) == 0);
if (len > 0) {
ssize_t nread = read(fd, captured, (size_t)len);
TEST_ASSERT_TRUE(nread == len);
}
close(fd);
/* The reconciliation itself (task 2.3) still happened -- this gate only
* ever affects the console print, nothing else in the reporting chain. */
TEST_ASSERT_EQUAL_PTR(jalloc, job->allocs);
TEST_ASSERT_NOT_NULL(job->accounts);
TEST_ASSERT_EQUAL_INT_MESSAGE(
1, record_ctx->n,
"the recording channel (sched_record_session_add_decision()) must "
"still report the reconstructed decision -- only the console print "
"is gated, not the recording");
/* THE ASSERTION THIS TEST EXISTS TO PROVE: the bare per-decision line
* must NOT appear via this call site while sim->interactive &&
* sim->repl_state->mode == XCE_REPL_MODE_CONTINUE (bug-1610, this
* remediation's own scope). */
TEST_ASSERT_NULL_MESSAGE(
strstr(captured, "[sim] DISPATCH jobid="),
"under continue mode with a genuinely allocated repl_state, "
"sim_reconcile_unaccounted_jobs()'s own reconstructed-decision "
"report must NOT print the bare per-decision line -- the Round "
"Rollup's own summary is the sole source of decision output here "
"too, closing bug-1610's duplication gap at this second call site");
free(captured);
free(sim.repl_state);
printf(
"test_reconcile_continue_mode_with_repl_state_suppresses_bare_"
"decision_line: PASS\n");
}
/* ---------------------------------------------------------------------- */
/* Test 17 (regression-safety half of Test 16): under `step` mode with a */
/* genuinely allocated repl_state, the bare per-decision line from THIS */
/* same call site must still appear exactly as before -- only `continue` */
/* mode is gated (Requirement 5.1's "step mode ... unaffected" guarantee, */
/* now proven for this second call site too, not just the drain loop's). */
/* ---------------------------------------------------------------------- */
static void
test_reconcile_step_mode_with_repl_state_still_prints_bare_decision_line(
void)
{
xce_sched_sim_driver_t sim;
xce_sched_job_t* job;
xce_job_alloc_t* jalloc;
replay_session_t* decoded;
char tmp[] = "/tmp/xce_simreconcile_gate_step_XXXXXX";
int fd = mkstemp(tmp);
int saved_stdout;
off_t len;
char* captured;
fixture_init(&sim);
job = add_job(&(sim.core), 7061ULL, JOB_STAT_RUN, NULL);
jalloc = make_test_job_alloc_with_host(7061ULL, "worker9", "slots", 3.0f);
decoded
= make_decoded_session(1000ULL, make_dispatch_entry_from_alloc(jalloc));
sim.interactive = true;
sim.repl_state = repl_state_ensure(&sim);
TEST_ASSERT_NOT_NULL(sim.repl_state);
/* XCE_REPL_MODE_STEP == 0, already repl_state_ensure()'s calloc()
* default -- set explicitly regardless, so this test's intent reads
* clearly and does not silently depend on the enum's numeric value. */
sim.repl_state->mode = XCE_REPL_MODE_STEP;
TEST_ASSERT_TRUE_MESSAGE(fd >= 0, "mkstemp for stdout capture must "
"succeed");
fflush(stdout);
saved_stdout = dup(STDOUT_FILENO);
TEST_ASSERT_TRUE(saved_stdout >= 0);
TEST_ASSERT_TRUE(dup2(fd, STDOUT_FILENO) >= 0);
sim_reconcile_unaccounted_jobs(&sim, decoded, NULL);
fflush(stdout);
dup2(saved_stdout, STDOUT_FILENO);
close(saved_stdout);
len = lseek(fd, 0, SEEK_CUR);
TEST_ASSERT_TRUE(len >= 0);
captured = (char*)calloc(1, (size_t)len + 1);
TEST_ASSERT_NOT_NULL(captured);
TEST_ASSERT_TRUE(lseek(fd, 0, SEEK_SET) == 0);
if (len > 0) {
ssize_t nread = read(fd, captured, (size_t)len);
TEST_ASSERT_TRUE(nread == len);
}
close(fd);
TEST_ASSERT_EQUAL_PTR(jalloc, job->allocs);
TEST_ASSERT_NOT_NULL(job->accounts);
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, "[sim] DISPATCH jobid="),
"while single-stepping (repl_state allocated, mode == "
"XCE_REPL_MODE_STEP), sim_reconcile_unaccounted_jobs()'s own "
"reconstructed-decision report must still print the bare "
"per-decision line exactly as before -- only continue mode is "
"gated");
free(captured);
free(sim.repl_state);
printf(
"test_reconcile_step_mode_with_repl_state_still_prints_bare_"
"decision_line: PASS\n");
}
int main(void)
{
UNITY_BEGIN();
@ -1400,5 +1607,9 @@ int main(void)
RUN_TEST(test_reconcile_no_match_leaves_pend_job_uncaptured_and_retries);
RUN_TEST(
test_reconcile_multiple_jobs_captured_independently_across_different_rounds);
RUN_TEST(
test_reconcile_continue_mode_with_repl_state_suppresses_bare_decision_line);
RUN_TEST(
test_reconcile_step_mode_with_repl_state_still_prints_bare_decision_line);
return UNITY_END();
}