Back to feed

sledtools/pika branch #71

pika-orch-incus-cleanup-13

Centralize pika-news run fixture writing

Target branch: master

Merge Commit: a41e432427a7d1e1edcb126db88d93c09b981624

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 #90 success

2 passed

head e3167141c008e47a5cf08e5e43084df85b907101 · queued 2026-03-25 21:02:28 · 2 lane(s)

queued 1m 05s · ran 23s

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

Summary

This branch centralizes the construction and writing of pika-news CI run fixtures used across tests. Previously, each test in forge.rs and web.rs manually assembled raw JSON strings and created directory trees with shell heredocs or inline fs::write calls to set up RunRecord and job log fixtures. The branch introduces two test-only structs — TestPikaciRunFixture and TestPikaciJobFixture — in pikaci_store.rs, along with a write_fixture method on PikaciRunStore. All test call-sites are migrated to use this centralized fixture API, eliminating duplicated raw JSON serialization, fragile shell-script fixture creation, and manual directory layout management.

Tutorial Steps

Introduce test-only fixture structs in pikaci_store.rs

Intent: Create reusable, type-safe structs (`TestPikaciRunFixture` and `TestPikaciJobFixture`) that represent the data needed to write a complete CI run fixture to disk, replacing scattered raw JSON and manual file operations across tests.

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

Evidence
@@ -3,6 +3,11 @@ use std::path::{Path, PathBuf};
 use anyhow::{anyhow, Result};
 use pikaci::{load_logs, load_run_bundle, load_run_record, LogKind, Logs, RunBundle, RunRecord};
 
+#[cfg(test)]
+use pikaci::{JobRecord, PreparedOutputsRecord, RemoteLinuxVmExecutionRecord, RunStatus};
+#[cfg(test)]
+use std::fs;
@@ -10,6 +15,80 @@ pub struct PikaciRunStore {
     state_root: PathBuf,
 }
 
+#[cfg(test)]
+#[derive(Clone, Debug)]
+pub struct TestPikaciJobFixture {
@@ -10,6 +15,80 @@
+#[cfg(test)]
+#[derive(Clone, Debug)]
+pub struct TestPikaciRunFixture {
@@ -10,6 +15,80 @@
+impl TestPikaciJobFixture {
+    pub fn passed_remote_linux(job_id: &str, description: &str) -> Self {
@@ -10,6 +15,80 @@
+impl TestPikaciRunFixture {
+    pub fn passed(run_id: &str, target_id: Option<&str>, target_description: Option<&str>) -> Self {

Two new #[cfg(test)] structs are added to pikaci_store.rs:

  • TestPikaciJobFixture holds all fields needed for a single CI job fixture: id, description, status, executor type, log content, timing, exit code, and optional RemoteLinuxVmExecutionRecord. A convenience constructor passed_remote_linux() creates a fully-populated passing job with sensible defaults.

  • TestPikaciRunFixture holds run-level fields (run_id, status, target info, timestamps) plus a Vec<TestPikaciJobFixture> for jobs and an optional PreparedOutputsRecord. The passed() constructor creates a minimal passing run.

Both structs are gated behind #[cfg(test)] so they have zero impact on production builds. The conditional imports pull in JobRecord, PreparedOutputsRecord, RemoteLinuxVmExecutionRecord, RunStatus, and std::fs only during testing.

Add run_record_path helper and write_fixture method to PikaciRunStore

Intent: Provide a single method on `PikaciRunStore` that takes a `TestPikaciRunFixture`, creates the necessary directory structure, writes log files, serializes the `RunRecord` and optional `PreparedOutputsRecord` to disk — replacing all ad-hoc fixture writing code.

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

Evidence
@@ -32,6 +111,11 @@ impl PikaciRunStore {
     pub fn run_dir(&self, run_id: &str) -> PathBuf {
         self.state_root.join("runs").join(run_id)
     }
 
+    #[cfg(test)]
+    pub fn run_record_path(&self, run_id: &str) -> PathBuf {
+        self.run_dir(run_id).join("run.json")
+    }
@@ -54,6 +138,91 @@ impl PikaciRunStore {
+    #[cfg(test)]
+    pub fn write_fixture(&self, fixture: &TestPikaciRunFixture) -> Result<()> {

Two new #[cfg(test)] methods are added to PikaciRunStore:

  1. run_record_path(&self, run_id) -> PathBuf — returns the canonical path to run.json for a given run. This encapsulates the run_dir(id).join("run.json") pattern that was previously inlined at each assertion site.

  2. write_fixture(&self, fixture: &TestPikaciRunFixture) -> Result<()> — the core of this branch. It:

    • Creates the run directory via fs::create_dir_all.
    • If the fixture has prepared_outputs, serializes it to the standard prepared-outputs path.
    • For each TestPikaciJobFixture, creates job directories, writes host.log and guest.log with the fixture's log content, and builds a JobRecord with correct paths.
    • Constructs a full RunRecord from the fixture's fields plus the assembled jobs and optional prepared-outputs path.
    • Serializes the RunRecord as JSON to run.json.

This method uses the real pikaci types (RunRecord, JobRecord) for serialization, ensuring the fixture data matches production schema exactly — a significant improvement over hand-crafted JSON strings.

Migrate forge.rs tests to use centralized fixtures

Intent: Replace inline shell-script heredocs that wrote `run.json` and log files inside wrapper scripts with pre-created fixtures using `TestPikaciRunFixture` and `TestPikaciJobFixture`, and update assertions to use `run_record_path`.

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

Evidence
@@ -1237,7 +1237,7 @@ mod tests {
-    use crate::pikaci_store::PikaciRunStore;
+    use crate::pikaci_store::{PikaciRunStore, TestPikaciJobFixture, TestPikaciRunFixture};
@@ -1524,6 +1524,18 @@ mod tests {
     fn staged_pikaci_lane_reports_run_id_and_human_log_summary() {
         let (_root, forge_repo, seed) = setup_repo();
+        let run_store = PikaciRunStore::from_forge_repo(&forge_repo);
+        let mut fixture = TestPikaciRunFixture::passed(
+            "pikaci-run-123",
+            Some("pre-merge-pika-rust"),
+            Some("Run staged pika rust"),
+        );
+        fixture.jobs.push(TestPikaciJobFixture::passed_remote_linux(
+            "job-one", "job one",
+        ));
+        run_store
+            .write_fixture(&fixture)
+            .expect("write persisted run fixture");
@@ -1536,13 +1548,6 @@ mod tests {
-                "run_root=\"${PIKACI_STATE_ROOT:?missing-state-root}/runs/pikaci-run-123/jobs/job-one\"\n",
-                "mkdir -p \"$run_root\"\n",
-                "printf 'host log\\n' > \"$run_root/host.log\"\n",
-                "printf 'guest log\\n' > \"$run_root/guest.log\"\n",
-                "cat > \"${PIKACI_STATE_ROOT}/runs/pikaci-run-123/run.json\" <<EOF_RUN\n",
-                "{\"run_id\":\"pikaci-run-123\",...}\n",
-                "EOF_RUN\n",
@@ -1598,11 +1603,7 @@ mod tests {
-        let run_store = PikaciRunStore::from_forge_repo(&forge_repo);
-        assert!(run_store
-            .run_dir("pikaci-run-123")
-            .join("run.json")
-            .is_file());
+        assert!(run_store.run_record_path("pikaci-run-123").is_file());

Two tests in forge.rs are updated:

staged_pikaci_lane_reports_run_id_and_human_log_summary

Before: The wrapper shell script contained ~10 lines of heredoc that created directories, wrote log files, and emitted a raw JSON run.json inline — a single long escaped JSON string with every RunRecord field.

After: The fixture is created before the wrapper script runs, using TestPikaciRunFixture::passed() with a TestPikaciJobFixture::passed_remote_linux() job. The shell script only needs to emit the streaming events (run_started, job_started, etc.) — it no longer manages any persistent state.

The post-execution assertion changes from run_store.run_dir(...).join("run.json").is_file() to the cleaner run_store.run_record_path(...).is_file().

explicit_structured_pikaci_target_does_not_depend_on_wrapper_name

The same pattern applies: fixture creation moves from the shell script heredoc to a TestPikaciRunFixture::passed() call (with no jobs, matching the original fixture). The run_store is created at the top of the test instead of after execution, and assertions use run_record_path.

Migrate web.rs test helper to use centralized fixtures

Intent: Replace the `write_pikaci_run_fixture` helper in web.rs, which manually constructed directories, wrote raw JSON strings, and wired up log/prepared-output paths, with calls to the new fixture API.

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

Evidence
@@ -4235,7 +4235,7 @@ mod tests {
-    use crate::pikaci_store::PikaciRunStore;
+    use crate::pikaci_store::{PikaciRunStore, TestPikaciJobFixture, TestPikaciRunFixture};
@@ -4341,28 +4341,31 @@ mod tests {
     fn write_pikaci_run_fixture(config: &Config, run_id: &str) {
         let run_store = PikaciRunStore::from_config(config).expect("pikaci run store");
-        let run_dir = run_store.run_dir(run_id);
-        let job_dir = run_store.job_dir(run_id, "job-one");
-        ...
-        fs::write(run_dir.join("run.json"), run_json).expect("write run fixture");
+        let mut fixture = TestPikaciRunFixture::passed(
+            run_id,
+            Some("pre-merge-pika-rust"),
+            Some("Run staged pika rust"),
+        );
+        let mut job = TestPikaciJobFixture::passed_remote_linux("job-one", "job one");
+        job.remote_linux_vm_execution = Some(pikaci::RemoteLinuxVmExecutionRecord {
+            backend: pikaci::RemoteLinuxVmBackend::Incus,
+            incus_image: Some(pikaci::RemoteLinuxVmImageRecord {
+                project: "pika-managed-agents".to_string(),
+                alias: "pikaci/dev".to_string(),
+                fingerprint: Some("abc123".to_string()),
+            }),
+            phases: Vec::new(),
+        });
+        fixture.jobs.push(job);
+        fixture.prepared_outputs = Some(
+            serde_json::from_str(r#"{...}"#).expect("parse prepared outputs fixture"),
+        );
+        run_store.write_fixture(&fixture).expect("write run fixture");

The write_pikaci_run_fixture helper in web.rs was the most complex fixture site — it manually created directories, wrote host/guest logs, serialized a prepared-outputs JSON file, and constructed a run.json with an interpolated format!() string that embedded file paths.

After the change, the helper:

  1. Creates a TestPikaciRunFixture::passed() with the run id and target metadata.
  2. Creates a TestPikaciJobFixture::passed_remote_linux() and sets its remote_linux_vm_execution field to an Incus backend with image metadata — this is the one fixture that exercises the VM execution detail path.
  3. Sets fixture.prepared_outputs by parsing a static JSON string into the typed PreparedOutputsRecord.
  4. Calls run_store.write_fixture(&fixture) which handles all directory creation, log writing, and JSON serialization.

The result is that write_pikaci_run_fixture drops from ~20 lines of manual I/O to ~15 lines of structured fixture assembly, with the benefit that all path construction and serialization is handled by the single write_fixture method. The prepared-outputs JSON is still a raw string, but it is now parsed into a typed struct before being passed to the fixture, catching schema mismatches at test time rather than silently writing invalid JSON.

Diff