Back to feed

sledtools/pika branch #73

pika-orch-incus-cleanup-15

Unify pika-news test fixture builders

Target branch: master

Merge Commit: 074589e888690771b55d964a73164c278271c135

branch: merged tutorial: ready ci: success
Open CI Details

Continuous Integration

CI: success

Compact status on the review page, with full logs on the CI page.

Open CI Details

Latest run #92 success

2 passed

head 4beb7fb174a985ca5d314ebb4c655736d127d951 · queued 2026-03-25 21:18:11 · 2 lane(s)

queued 6s · ran 28s

check-notifications · success check-agent-contracts · success

Summary

This branch unifies duplicated test fixture builders in the pika-news crate. Previously, both forge.rs and pikaci_store.rs contained inline helper functions that manually constructed JobRecord, RunRecord, and RunLifecycleEvent values for tests. The change consolidates all construction logic into methods on the existing TestPikaciJobFixture and TestPikaciRunFixture structs in pikaci_store.rs, then replaces every ad-hoc builder call in forge.rs (and the duplicated record-building code in pikaci_store.rs itself) with calls to those shared methods. This eliminates roughly 120 lines of repetitive boilerplate and ensures that any future schema change to JobRecord or RunRecord only needs to be updated in one place.

Tutorial Steps

Add conversion and event-builder methods to TestPikaciJobFixture

Intent: Give TestPikaciJobFixture the ability to produce a full JobRecord (with caller-supplied or default log paths) and to emit RunLifecycleEvent::JobStarted / JobFinished events, so every test site can delegate to a single source of truth.

Affected files: crates/pika-news/src/pikaci_store.rs

Evidence
@@ -52,6 +54,47 @@ impl TestPikaciJobFixture {
+ pub fn into_job_record_with_logs(
+ pub fn into_event_job_record(self) -> JobRecord {
+ pub fn job_started_event(self, run_id: &str) -> RunLifecycleEvent {
+ pub fn job_finished_event(self, run_id: &str) -> RunLifecycleEvent {

Three new methods are added to TestPikaciJobFixture:

  • into_job_record_with_logs(host_log_path, guest_log_path) — consumes the fixture and builds a complete JobRecord, accepting explicit log paths so the PikaciRunStore::seed_fixture writer can pass real filesystem paths.
  • into_event_job_record() — convenience wrapper that uses placeholder /tmp/*.log paths, suitable for lifecycle-event tests that never read log files.
  • job_started_event(run_id) / job_finished_event(run_id) — produce the corresponding RunLifecycleEvent variants, boxing the record internally.

The import block gains RunLifecycleEvent under #[cfg(test)] to support the new return types.

// pikaci_store.rs — new imports
#[cfg(test)]
use pikaci::{
    JobRecord, PreparedOutputsRecord, RemoteLinuxVmExecutionRecord,
    RunLifecycleEvent, RunStatus,
};

Add conversion and event-builder methods to TestPikaciRunFixture

Intent: Provide TestPikaciRunFixture with into_run_record, run_started_event, and run_finished_event so that run-level record and event construction is also centralized.

Affected files: crates/pika-news/src/pikaci_store.rs

Evidence
@@ -87,6 +130,55 @@ impl TestPikaciRunFixture {
+ pub fn into_run_record(self, jobs: Vec<JobRecord>) -> RunRecord {
+ pub fn run_started_event(&self) -> RunLifecycleEvent {
+ pub fn run_finished_event(self) -> RunLifecycleEvent {

Three methods mirror the job-level pattern:

  • into_run_record(jobs) — consumes the fixture and fills every RunRecord field, accepting a pre-built Vec<JobRecord> for the jobs list. Fields that have no fixture counterpart (e.g. plan_path, all prepared_output_* transport fields) default to None or empty vecs.
  • run_started_event(&self) — borrows the fixture (note: &self, not consuming) so callers can clone and reuse the fixture for both start and finish events.
  • run_finished_event(self) — consumes the fixture, delegates to into_run_record(Vec::new()), and wraps the result in RunLifecycleEvent::RunFinished.

This means a test that needs both events can .clone() the fixture for the start event and then move it into the finish event.

Replace inline record construction in PikaciRunStore::seed_fixture

Intent: Eliminate the 40-line manual JobRecord and RunRecord construction inside the store's seeding logic by delegating to the new fixture methods.

Affected files: crates/pika-news/src/pikaci_store.rs

Evidence
@@ -166,56 +258,16 @@ impl PikaciRunStore {
-            jobs.push(JobRecord {
+            jobs.push(job.clone().into_job_record_with_logs(
-        let run = RunRecord {
+        let mut run = fixture.clone().into_run_record(jobs);
+        run.prepared_outputs_path = prepared_outputs_path

Inside PikaciRunStore::seed_fixture, the old code manually destructured every field of TestPikaciJobFixture to build a JobRecord, and every field of TestPikaciRunFixture to build a RunRecord. Both blocks are replaced:

// Before (job)
jobs.push(JobRecord {
    id: job.id.clone(),
    description: job.description.clone(),
    // … 12 more fields …
});

// After
jobs.push(job.clone().into_job_record_with_logs(
    host_log_path.display().to_string(),
    guest_log_path.display().to_string(),
));

For the run record, the only field that cannot come from the fixture is prepared_outputs_path (computed from a local temp path), so it is set via mutation after construction:

let mut run = fixture.clone().into_run_record(jobs);
run.prepared_outputs_path = prepared_outputs_path
    .as_ref()
    .map(|(path, _)| path.display().to_string());

This drops ~40 lines of boilerplate from the production store-seeding path.

Remove duplicated fixture helpers from forge.rs tests

Intent: Delete the local fixture_job_event, fixture_run_event, and inlined RunLifecycleEvent construction in forge.rs, replacing them with the shared fixture builders.

Affected files: crates/pika-news/src/forge.rs

Evidence
@@ -1228,7 +1228,7 @@ mod tests {
-    use pikaci::{JobRecord, RunLifecycleEvent, RunRecord, RunStatus};
+    use pikaci::RunStatus;
@@ -1254,71 +1254,7 @@ mod tests {
-    fn fixture_job_event(
-    fn fixture_run_event(
-    fn event_line(event: RunLifecycleEvent) -> String {
+    fn event_line(event: pikaci::RunLifecycleEvent) -> String {

The forge.rs test module had its own fixture_job_event (15 lines) and fixture_run_event (30 lines) helpers plus the matching use imports. All are removed:

  1. Imports narrowedJobRecord, RunLifecycleEvent, and RunRecord are dropped from the use statement; only RunStatus remains because tests still reference it for struct-update expressions.
  2. fixture_job_event deleted — 15-line function that built a JobRecord with placeholder fields.
  3. fixture_run_event deleted — 30-line function that built a RunRecord with all transport fields set to None.
  4. event_line updated — the helper still exists but now qualifies the type as pikaci::RunLifecycleEvent instead of relying on the removed import.

Migrate structured-pikaci success test to shared fixtures

Intent: Replace the four hand-built RunLifecycleEvent values in the structured pikaci success integration test with calls to the fixture builders.

Affected files: crates/pika-news/src/forge.rs

Evidence
@@ -1608,29 +1544,16 @@ mod tests {
+        let run_started = event_line(fixture.clone().run_started_event());
+        let job_started = event_line(
+            TestPikaciJobFixture::passed_remote_linux("job-one", "job one")
+                .job_started_event("pikaci-run-123"),
+        let run_finished = event_line(fixture.clone().run_finished_event());

The structured_pikaci_events test previously constructed all four lifecycle events inline. Now it uses the fixture variable (a TestPikaciRunFixture already in scope) and TestPikaciJobFixture::passed_remote_linux:

let run_started = event_line(fixture.clone().run_started_event());
let job_started = event_line(
    TestPikaciJobFixture::passed_remote_linux("job-one", "job one")
        .job_started_event("pikaci-run-123"),
);
let job_finished = event_line(
    TestPikaciJobFixture::passed_remote_linux("job-one", "job one")
        .job_finished_event("pikaci-run-123"),
);
let run_finished = event_line(fixture.clone().run_finished_event());

The same pattern applies to the second integration test (structured_custom_lane), which only needs run_started and run_finished and drops from 13 lines to 2.

Migrate structured-pikaci failure test to shared fixtures with struct-update syntax

Intent: Show how tests that need non-default field values (e.g. Failed status, custom message) use Rust's struct-update syntax with the shared fixture as a base.

Affected files: crates/pika-news/src/forge.rs

Evidence
@@ -1815,28 +1725,33 @@ mod tests {
+                TestPikaciJobFixture {
+                    status: RunStatus::Failed,
+                    exit_code: Some(1),
+                    message: Some("cargo test failed in crate pika_core".to_string()),
+                    ..TestPikaciJobFixture::passed_remote_linux("job-one", "job one")
+                }
+                .job_finished_event("pikaci-run-123"),
+                TestPikaciRunFixture {
+                    status: RunStatus::Failed,
+                    message: Some("prepared-output helper exited 1".to_string()),
+                    ..TestPikaciRunFixture::passed(

The structured_pikaci_failure_log_keeps_job_and_run_messages test verifies rendering of failed events. Instead of calling the deleted fixture_job_event(RunStatus::Failed, …) helper, it now uses Rust struct-update syntax:

TestPikaciJobFixture {
    status: RunStatus::Failed,
    exit_code: Some(1),
    message: Some("cargo test failed in crate pika_core".to_string()),
    ..TestPikaciJobFixture::passed_remote_linux("job-one", "job one")
}
.job_finished_event("pikaci-run-123")

This pattern keeps the deviation (failed status, exit code, message) visually prominent while inheriting all other defaults from the canonical constructor. The same approach is used for the TestPikaciRunFixture failure case, making the test both shorter and more intention-revealing.

Diff