diff --git a/.kiro/specs/scheduler-core/tasks.md b/.kiro/specs/scheduler-core/tasks.md index 755f00f6..efba58fd 100644 --- a/.kiro/specs/scheduler-core/tasks.md +++ b/.kiro/specs/scheduler-core/tasks.md @@ -190,11 +190,12 @@ - _Boundary: ReplayFormat — not (P), same file as 7.2/7.3 (see 7.2)_ - _Verified: 2026-08-04 — SchedAdminAction enum + SchedAdminRequest/SchedAdminReply structs added matching requirements.md Req 5 AC 10 verbatim. schedAdmin@42 (OPCode), schedadminmsg@31 (Request.body), schedadminreplymsg@19 (RequestReply.data) — all three the exact next-available ordinals, verified live before dispatch and re-verified byte-identical below by review. Deliberately schema-only per this task's own text — the controller dispatch logic (cluster.c's case OPCode_schedAdmin:) is task 10.1's job, not this one; confirmed cluster.c untouched. Round-trip tests cover all 3 SchedAdminAction values and both ok=true/ok=false SchedAdminReply cases with an asymmetric ok/recording combination, ruling out a Bool-field bit-packing mixup. 1084/1084 controller_unit_test, full clean project build, confirmed independently three times, review APPROVED first pass. Closes out Group 7 (Foundation: Replay Wire Format) entirely — tasks 7.1-7.4 all complete._ -- [ ] 7.5 Replay format round-trip test suite +- [x] 7.5 Replay format round-trip test suite - Encode and decode every new message type introduced in this group, asserting field-for-field equality after round-trip - Observable: the full round-trip test suite passes for every new struct, including the internal-state version-mismatch negative test - _Requirements: 5.20_ - _Depends: 7.2, 7.3, 7.4_ + - _Verified: 2026-08-04 — audit of all ~19 new types/enums from tasks 7.1-7.4 found one genuine gap: ReplayEvent's own envelope union (spanning 11 members across 7.1+7.2) was never actually constructed/encoded in any test, despite its individual payload types each having their own round-trip test. Fixed with 3 new tests (jobStateChange/jobUpdate/endSession variants). All other 18 types independently re-verified genuinely field-for-field, non-shallow. Decided against creating design.md's originally-named test_replay_format.c: tasks 7.1-7.4 independently, consistently (4-for-4) chose the existing test_e2e_capnp.c instead, matching task 4.3's core_events.c-vs-core.c precedent for design.md file-naming being non-binding guidance. Deliberately did not add Request.body/RequestReply.data union-wiring tests for OPCode_schedAdmin, matching universal codebase precedent (opcode-dispatch union members are always tested via functional dispatch tests, never bare schema round-trips, and the real dispatch logic doesn't exist until task 10.1). Internal-state version-mismatch negative test re-verified real and correctly scoped (schema-tag level only, matching the Observable). Zero schema/typemap changes -- audit-only, as scoped. 1087/1087 controller_unit_test, review APPROVED first pass with every claim independently re-derived. Closes out Group 7 (Foundation: Replay Wire Format) entirely -- tasks 7.1-7.5 all complete._ - [ ] 8. Core: Snapshot Serialization diff --git a/src/controller/testsuite/test_e2e_capnp.c b/src/controller/testsuite/test_e2e_capnp.c index e867c8ea..586f553f 100644 --- a/src/controller/testsuite/test_e2e_capnp.c +++ b/src/controller/testsuite/test_e2e_capnp.c @@ -1495,6 +1495,212 @@ static void test_sched_admin_reply_round_trip(void) printf("test_sched_admin_reply_round_trip: PASS\n"); } +/* ------------------------------------------------------------------ */ +/* Test 18: ReplayEvent's `body` union round-trips through the */ +/* jobStateChange variant (scheduler-core task 7.5 gap-fill, Req 5 */ +/* AC 20) -- GENUINE COVERAGE GAP found during the 7.5 completeness */ +/* audit: ReplayEvent (task 7.1's envelope struct, extended with the */ +/* jobStateChange/jobUpdate union members by task 7.2) has a real C */ +/* struct (replay_event_t, typemap.h) and a working encode_ReplayEvent_ */ +/* ptr/decode_ReplayEvent/free_ReplayEvent codec bridge -- exactly the */ +/* same shape as ReplayDecision, which DID get a union-wrapping test */ +/* (Test 14) -- yet no test anywhere ever constructed a ReplayEvent or */ +/* exercised its `body` union. This mirrors Test 14's precedent: the */ +/* payload type itself (JobStateChangeEvent) is already proven in */ +/* isolation by Test 9, so this test's job is narrower -- prove the */ +/* union GROUP mechanism on ReplayEvent itself (ts alongside body_which */ +/* alongside the payload) actually round-trips end to end. */ +/* ------------------------------------------------------------------ */ + +static void test_replay_event_job_state_change_round_trip(void) +{ + job_state_change_event_t change; + memset(&change, 0, sizeof(change)); + change.jobid = 61616ULL; + change.old_state = 1u; + change.new_state = 5u; + change.exit_code = 0; + + replay_event_t src; + memset(&src, 0, sizeof(src)); + src.ts = 70001ULL; + src.body_which = ReplayEvent_body_jobStateChange; + src.body.job_state_change = &change; + + struct capn c; + capn_init_malloc(&c); + struct capn_segment *cs = capn_root(&c).seg; + + ReplayEvent_ptr p; + encode_ReplayEvent_ptr(cs, &p, &src); + capn_setp(capn_root(&c), 0, p.p); + + int64_t sz = capn_size(&c); + TEST_ASSERT_TRUE(sz > 0); + uint8_t *buf = (uint8_t *)malloc((size_t)sz); + TEST_ASSERT_NOT_NULL(buf); + capn_write_mem(&c, buf, (size_t)sz, 0); + capn_free(&c); + + struct capn d; + TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d, buf, (size_t)sz, 0)); + ReplayEvent_ptr dp; + dp.p = capn_getp(capn_root(&d), 0, 1); + struct ReplayEvent wire; + read_ReplayEvent(&wire, dp); + replay_event_t dst; + memset(&dst, 0, sizeof(dst)); + decode_ReplayEvent(&dst, &wire); + + TEST_ASSERT_EQUAL_UINT64(70001ULL, dst.ts); + TEST_ASSERT_EQUAL_INT((int)ReplayEvent_body_jobStateChange, (int)dst.body_which); + TEST_ASSERT_NOT_NULL(dst.body.job_state_change); + TEST_ASSERT_EQUAL_UINT64(61616ULL, dst.body.job_state_change->jobid); + TEST_ASSERT_EQUAL_UINT32(1u, dst.body.job_state_change->old_state); + TEST_ASSERT_EQUAL_UINT32(5u, dst.body.job_state_change->new_state); + TEST_ASSERT_EQUAL_INT32(0, dst.body.job_state_change->exit_code); + + free_ReplayEvent(&dst); + capn_free(&d); + free(buf); + printf("test_replay_event_job_state_change_round_trip: PASS\n"); +} + +/* ------------------------------------------------------------------ */ +/* Test 19: ReplayEvent's `body` union round-trips through the */ +/* jobUpdate variant (scheduler-core task 7.5 gap-fill, Req 5 AC 20) -- */ +/* same rationale as Test 18. JobUpdateEvent's own deep field tree */ +/* (submit/allocs/pidinfo/pend_reasons) is already proven field-for- */ +/* field by Test 10 -- this test only needs a light population to prove */ +/* the UNION WRAPPING (ts + body_which discriminant alongside the */ +/* pointer payload) survives the wire, not to re-prove JobUpdateEvent's */ +/* own internals again. */ +/* ------------------------------------------------------------------ */ + +static void test_replay_event_job_update_round_trip(void) +{ + job_submit_data_t submit; + memset(&submit, 0, sizeof(submit)); + submit.queue = "batch"; + submit.user = "bob"; + submit.job_name = "replay-event-update-job"; + + xce_job_t job; + memset(&job, 0, sizeof(job)); + job.jobid = 9101ULL; + job.submit = &submit; + job.state = 3u; + job.run_time = 45; + + job_update_event_t update; + memset(&update, 0, sizeof(update)); + update.jobid = 9101ULL; + update.changed_fields = 0x1u; + update.job = &job; + + replay_event_t src; + memset(&src, 0, sizeof(src)); + src.ts = 70002ULL; + src.body_which = ReplayEvent_body_jobUpdate; + src.body.job_update = &update; + + struct capn c; + capn_init_malloc(&c); + struct capn_segment *cs = capn_root(&c).seg; + + ReplayEvent_ptr p; + encode_ReplayEvent_ptr(cs, &p, &src); + capn_setp(capn_root(&c), 0, p.p); + + int64_t sz = capn_size(&c); + TEST_ASSERT_TRUE(sz > 0); + uint8_t *buf = (uint8_t *)malloc((size_t)sz); + TEST_ASSERT_NOT_NULL(buf); + capn_write_mem(&c, buf, (size_t)sz, 0); + capn_free(&c); + + struct capn d; + TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d, buf, (size_t)sz, 0)); + ReplayEvent_ptr dp; + dp.p = capn_getp(capn_root(&d), 0, 1); + struct ReplayEvent wire; + read_ReplayEvent(&wire, dp); + replay_event_t dst; + memset(&dst, 0, sizeof(dst)); + decode_ReplayEvent(&dst, &wire); + + TEST_ASSERT_EQUAL_UINT64(70002ULL, dst.ts); + TEST_ASSERT_EQUAL_INT((int)ReplayEvent_body_jobUpdate, (int)dst.body_which); + TEST_ASSERT_NOT_NULL(dst.body.job_update); + TEST_ASSERT_EQUAL_UINT64(9101ULL, dst.body.job_update->jobid); + TEST_ASSERT_EQUAL_UINT32(0x1u, dst.body.job_update->changed_fields); + TEST_ASSERT_NOT_NULL(dst.body.job_update->job); + TEST_ASSERT_EQUAL_UINT64(9101ULL, dst.body.job_update->job->jobid); + TEST_ASSERT_EQUAL_UINT32(3u, dst.body.job_update->job->state); + TEST_ASSERT_NOT_NULL(dst.body.job_update->job->submit); + TEST_ASSERT_EQUAL_STRING("batch", dst.body.job_update->job->submit->queue); + TEST_ASSERT_EQUAL_STRING("bob", dst.body.job_update->job->submit->user); + + free_ReplayEvent(&dst); + capn_free(&d); + free(buf); + printf("test_replay_event_job_update_round_trip: PASS\n"); +} + +/* ------------------------------------------------------------------ */ +/* Test 20: ReplayEvent's `body` union round-trips through the */ +/* endSession variant (scheduler-core task 7.5 gap-fill, Req 5 AC 20) -- */ +/* the Void, no-payload marker arm. Structurally distinct from every */ +/* other ReplayEvent.body case (no pointer payload to encode/decode -- */ +/* the generated encode_/decode_/free_ switch hits its `default: break;` */ +/* branch instead of a per-type codec call), so it is a genuinely */ +/* separate code path worth its own round trip: proves the discriminant */ +/* alone (without a payload) still survives the wire faithfully, per the */ +/* schema's documented "recording stop marker" framing role (Req 5 */ +/* AC 20 final-message-of-stream framing). */ +/* ------------------------------------------------------------------ */ + +static void test_replay_event_end_session_round_trip(void) +{ + replay_event_t src; + memset(&src, 0, sizeof(src)); + src.ts = 70003ULL; + src.body_which = ReplayEvent_body_endSession; + + struct capn c; + capn_init_malloc(&c); + struct capn_segment *cs = capn_root(&c).seg; + + ReplayEvent_ptr p; + encode_ReplayEvent_ptr(cs, &p, &src); + capn_setp(capn_root(&c), 0, p.p); + + int64_t sz = capn_size(&c); + TEST_ASSERT_TRUE(sz > 0); + uint8_t *buf = (uint8_t *)malloc((size_t)sz); + TEST_ASSERT_NOT_NULL(buf); + capn_write_mem(&c, buf, (size_t)sz, 0); + capn_free(&c); + + struct capn d; + TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d, buf, (size_t)sz, 0)); + ReplayEvent_ptr dp; + dp.p = capn_getp(capn_root(&d), 0, 1); + struct ReplayEvent wire; + read_ReplayEvent(&wire, dp); + replay_event_t dst; + memset(&dst, 0, sizeof(dst)); + decode_ReplayEvent(&dst, &wire); + + TEST_ASSERT_EQUAL_UINT64(70003ULL, dst.ts); + TEST_ASSERT_EQUAL_INT((int)ReplayEvent_body_endSession, (int)dst.body_which); + + free_ReplayEvent(&dst); + capn_free(&d); + free(buf); + printf("test_replay_event_end_session_round_trip: PASS\n"); +} + /* ------------------------------------------------------------------ */ /* Suite entry point */ /* ------------------------------------------------------------------ */ @@ -1521,4 +1727,7 @@ void test_e2e_capnp(void) RUN_TEST(test_snapshot_data_round_trip); RUN_TEST(test_sched_admin_request_round_trip); RUN_TEST(test_sched_admin_reply_round_trip); + RUN_TEST(test_replay_event_job_state_change_round_trip); + RUN_TEST(test_replay_event_job_update_round_trip); + RUN_TEST(test_replay_event_end_session_round_trip); }