Back to feed

sledtools/pika branch #64

pika-orch-incus-cleanup-11

Let PikaciRunStore own run path layout

Target branch: master

Merge Commit: 6235230486574e745ef5b374428596a4a9eb99d8

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

2 passed

head 1c6b4bf0d43cad0140d0e808710c3386817916bc · queued 2026-03-25 20:48:48 · 2 lane(s)

queued 14s · ran 24s

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

Summary

This branch moves the CI run path layout logic out of the forge module and into PikaciRunStore, making the store the single owner of how run directories, job directories, and log paths are constructed on disk. The free function pikaci_state_root is deleted from forge.rs and replaced by a private state_root_for_repo helper inside pikaci_store.rs. New test-only accessor methods (run_dir, job_dir, host_log_path, guest_log_path, prepared_outputs_path) are added to PikaciRunStore so that tests no longer manually concatenate path segments, and all call sites in forge.rs and web.rs are updated accordingly.

Tutorial Steps

Move state root computation into PikaciRunStore

Intent: Eliminate the public pikaci_state_root free function from forge.rs and relocate the identical logic into a private state_root_for_repo function inside pikaci_store.rs. This makes PikaciRunStore the sole authority over where CI state lives on disk.

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

Evidence
@@ -100,22 +101,6 @@ impl Drop for MirrorLockGuard {
-pub fn pikaci_state_root(repo: &ForgeRepoConfig) -> PathBuf {
-    let repo_slug = repo
-        .repo
-        .chars()
-        .map(|ch| match ch {
-            'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => ch,
-            _ => '-',
-        })
-        .collect::<String>();
-    canonical_git_dir(repo)
-        .parent()
-        .unwrap_or_else(|| Path::new("."))
-        .join("pikaci-state")
-        .join(repo_slug)
-}
@@ -44,3 +70,19 @@ pub fn require_pikaci_run_store(...)
+fn state_root_for_repo(repo: &ForgeRepoConfig) -> PathBuf {
+    let repo_slug = repo
+        .repo
+        .chars()
+        .map(|ch| match ch {
+            'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => ch,
+            _ => '-',
+        })
+        .collect::<String>();
+    Path::new(&repo.canonical_git_dir)
+        .parent()
+        .unwrap_or_else(|| Path::new("."))
+        .join("pikaci-state")
+        .join(repo_slug)
+}

The pikaci_state_root function previously lived in forge.rs and was the only place that knew how to derive the on-disk state root from a ForgeRepoConfig. It is now a private function state_root_for_repo inside pikaci_store.rs.

A subtle but important change: the old code called canonical_git_dir(repo) to obtain the base path and then walked up to the parent. The new code reads repo.canonical_git_dir directly (Path::new(&repo.canonical_git_dir)), removing the dependency on forge::canonical_git_dir and the use crate::forge import entirely.

PikaciRunStore::from_forge_repo now delegates to this private helper:

pub fn from_forge_repo(repo: &ForgeRepoConfig) -> Self {
    Self {
        state_root: state_root_for_repo(repo),
    }
}

Add test-only path accessor methods to PikaciRunStore

Intent: Provide a set of #[cfg(test)] helper methods on PikaciRunStore that encode the directory conventions for runs, jobs, and log files. This eliminates duplicated manual path joins scattered across test code.

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

Evidence
@@ -28,6 +27,33 @@ impl PikaciRunStore {
+    #[cfg(test)]
+    pub fn run_dir(&self, run_id: &str) -> PathBuf {
+        self.state_root.join("runs").join(run_id)
+    }
+
+    #[cfg(test)]
+    pub fn prepared_outputs_path(&self, run_id: &str) -> PathBuf {
+        self.run_dir(run_id).join("prepared-outputs.json")
+    }
+
+    #[cfg(test)]
+    pub fn job_dir(&self, run_id: &str, job_id: &str) -> PathBuf {
+        self.run_dir(run_id).join("jobs").join(job_id)
+    }
+
+    #[cfg(test)]
+    pub fn host_log_path(&self, run_id: &str, job_id: &str) -> PathBuf {
+        self.job_dir(run_id, job_id).join("host.log")
+    }
+
+    #[cfg(test)]
+    pub fn guest_log_path(&self, run_id: &str, job_id: &str) -> PathBuf {
+        self.job_dir(run_id, job_id)
+            .join("artifacts")
+            .join("guest.log")
+    }

Five new methods are added, all gated behind #[cfg(test)]:

MethodReturns
run_dir(run_id)<state_root>/runs/<run_id>
prepared_outputs_path(run_id)<run_dir>/prepared-outputs.json
job_dir(run_id, job_id)<run_dir>/jobs/<job_id>
host_log_path(run_id, job_id)<job_dir>/host.log
guest_log_path(run_id, job_id)<job_dir>/artifacts/guest.log

These are deliberately test-only because production code reads paths through the pikaci crate's load_run_record / load_logs functions. The accessors exist solely to keep test assertions and fixture setup DRY and consistent with the canonical layout.

Update forge.rs production code to use PikaciRunStore

Intent: Replace the inline call to the now-removed pikaci_state_root with a PikaciRunStore construction, keeping the same runtime behavior while routing through the store's API.

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

Evidence
@@ -343,9 +328,11 @@ where
-    let structured_pikaci_state_root = structured_pikaci_target
-        .as_ref()
-        .map(|_| pikaci_state_root(repo));
+    let structured_pikaci_state_root = structured_pikaci_target.as_ref().map(|_| {
+        PikaciRunStore::from_forge_repo(repo)
+            .state_root()
+            .to_path_buf()
+    });

In the command-execution path, the old code called pikaci_state_root(repo) directly. The replacement constructs a PikaciRunStore and calls .state_root().to_path_buf(). The observable result is identical—a PathBuf pointing at the same directory—but the path derivation now goes through the store, ensuring a single source of truth.

A new use crate::pikaci_store::PikaciRunStore; import is added at the top of forge.rs to support this.

Migrate forge.rs tests to PikaciRunStore accessors

Intent: Replace hand-built path joins in forge test assertions with the new PikaciRunStore helper methods, making the tests resilient to future layout changes.

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

Evidence
@@ -1610,10 +1598,13 @@ mod tests {
-        let state_root = super::pikaci_state_root(&forge_repo);
-        assert!(state_root.join("runs/pikaci-run-123/run.json").is_file());
-        assert!(state_root
-            .join("runs/pikaci-run-123/jobs/job-one/host.log")
+        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
+            .host_log_path("pikaci-run-123", "job-one")
             .is_file());
@@ -1679,8 +1670,11 @@ mod tests {
-        let state_root = super::pikaci_state_root(&forge_repo);
-        assert!(state_root.join("runs/pikaci-run-456/run.json").is_file());
+        let run_store = PikaciRunStore::from_forge_repo(&forge_repo);
+        assert!(run_store
+            .run_dir("pikaci-run-456")
+            .join("run.json")
+            .is_file());

Two test functions are updated:

  1. The first test previously asserted on state_root.join("runs/pikaci-run-123/run.json") and state_root.join("runs/pikaci-run-123/jobs/job-one/host.log"). It now uses run_store.run_dir("pikaci-run-123").join("run.json") and run_store.host_log_path("pikaci-run-123", "job-one").

  2. The second test similarly replaces state_root.join("runs/pikaci-run-456/run.json") with run_store.run_dir("pikaci-run-456").join("run.json").

Note that run.json is still joined manually because there is no run_json_path accessor—this file is loaded by the pikaci crate's load_run_record, not by PikaciRunStore directly, so a dedicated accessor was not warranted.

Migrate web.rs test fixtures to PikaciRunStore accessors

Intent: Refactor the web module's test fixture builder and an additional test to use PikaciRunStore methods instead of raw path manipulation, completing the migration across all test code.

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

Evidence
@@ -4325,16 +4325,13 @@ mod tests {
     fn write_pikaci_run_fixture(config: &Config, run_id: &str) {
-        let state_root = PikaciRunStore::from_config(config)
-            .expect("pikaci run store")
-            .state_root()
-            .to_path_buf();
-        let run_dir = state_root.join("runs").join(run_id);
-        let job_dir = run_dir.join("jobs").join("job-one");
-        let prepared_outputs_path = run_dir.join("prepared-outputs.json");
+        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");
+        let prepared_outputs_path = run_store.prepared_outputs_path(run_id);
         fs::create_dir_all(&job_dir).expect("create pikaci fixture dir");
-        let host_log = job_dir.join("host.log");
-        let guest_log = job_dir.join("artifacts").join("guest.log");
+        let host_log = run_store.host_log_path(run_id, "job-one");
+        let guest_log = run_store.guest_log_path(run_id, "job-one");
@@ -5763,10 +5760,7 @@ mod tests {
         let prepared_outputs_path = PikaciRunStore::from_config(&config)
             .expect("pikaci run store")
-            .state_root()
-            .join("runs")
-            .join("pikaci-run-invalid-prepared")
-            .join("prepared-outputs.json");
+            .prepared_outputs_path("pikaci-run-invalid-prepared");

The write_pikaci_run_fixture helper was the most path-heavy call site in the test suite. It previously obtained state_root and manually built every path:

// Before
let run_dir = state_root.join("runs").join(run_id);
let job_dir = run_dir.join("jobs").join("job-one");
let host_log = job_dir.join("host.log");
let guest_log = job_dir.join("artifacts").join("guest.log");

After the change, every path is derived through PikaciRunStore:

// After
let run_dir = run_store.run_dir(run_id);
let job_dir = run_store.job_dir(run_id, "job-one");
let host_log = run_store.host_log_path(run_id, "job-one");
let guest_log = run_store.guest_log_path(run_id, "job-one");

A separate test that corrupts prepared-outputs.json also benefits, collapsing a four-segment join chain into a single prepared_outputs_path call. The net effect across both files is the elimination of all ad-hoc "runs" / "jobs" / "artifacts" string literals from test code.

Diff