Back to feed

sledtools/pika branch #102

pika-cloud-artifact-paths

Factor guest startup artifact paths

Target branch: master

Merge Commit: 31efc617d60ba398fdb8038f979eb37f625959ed

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

5 passed

head 1160bce899f4b9eec97c9175ccf5e76c1aef96f7 · queued 2026-03-26 01:57:29 · 5 lane(s)

queued 14s · ran 1m 59s

check-notifications · success check-agent-contracts · success check-pikachat · success check-apple-host-sanity · success check-fixture · success

Summary

This branch extracts three artifact path fields (status_path, events_path, result_path) from the flat GuestStartupArtifacts struct into a dedicated RuntimeArtifactPaths struct defined in the paths module. The new struct carries its own Default implementation, serde defaults, and a validate_canonical_paths method, and is embedded into GuestStartupArtifacts via #[serde(flatten)] so the on-wire JSON shape is unchanged. The refactor consolidates path-validation logic that was previously inlined in GuestStartupArtifacts::validate_canonical_paths, making RuntimeArtifactPaths independently reusable across crates while keeping serialization backward-compatible.

Tutorial Steps

Introduce the RuntimeArtifactPaths struct

Intent: Create a self-contained struct that owns the three runtime artifact paths (events, status, result), each with serde defaults pointing at the canonical constants, a Default impl, and a validate_canonical_paths method that can be called independently of GuestStartupArtifacts.

Affected files: crates/pika-cloud/src/paths.rs

Evidence
@@ -17,6 +17,56 @@ pub const RUNTIME_ARTIFACTS_DIR: &str = ARTIFACTS_DIR;
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+pub struct RuntimeArtifactPaths {
+    #[serde(default = "events_path")]
+    pub events_path: String,
+    #[serde(default = "status_path")]
+    pub status_path: String,
+    #[serde(default = "result_path")]
+    pub result_path: String,
+}
@@ +31,0 +31,25 @@
+impl RuntimeArtifactPaths {
+    pub fn validate_canonical_paths(&self, field_prefix: &str) -> Result<(), String> {
+        let canonical = Self::default();
+        for (field, actual, expected) in [
+            ("status_path", self.status_path.as_str(), canonical.status_path.as_str()),
+            ("events_path", self.events_path.as_str(), canonical.events_path.as_str()),
+            ("result_path", self.result_path.as_str(), canonical.result_path.as_str()),
+        ] {
+            if actual != expected {
+                return Err(format!(
+                    "{field_prefix}.{field} must use canonical path {expected:?}, got {actual:?}"
+                ));
+            }
+        }
+        Ok(())
+    }
+}

A new RuntimeArtifactPaths struct is added to crates/pika-cloud/src/paths.rs alongside the existing RuntimePaths. It groups three fields that were previously scattered across GuestStartupArtifacts:

  • events_path
  • status_path
  • result_path

Each field uses #[serde(default = "…")] referencing the same private helper functions (events_path(), status_path(), result_path()) that already existed in the module, so deserialization from JSON that omits these keys still produces the canonical values.

The struct also owns a validate_canonical_paths method that iterates over all three fields and returns a descriptive error if any path deviates from the canonical default. The field_prefix parameter lets callers control how the error message is scoped (e.g. "artifacts.status_path").

Export RuntimeArtifactPaths from the crate root

Intent: Make the new struct available to downstream crates (like pika-server) through the existing public re-export block.

Affected files: crates/pika-cloud/src/lib.rs

Evidence
@@ -20,7 +20,7 @@ pub use paths::{
     ARTIFACTS_DIR, EVENTS_PATH, GUEST_LOG_PATH as CLOUD_GUEST_LOG_PATH, GUEST_REQUEST_PATH,
-    LOGS_DIR, RESULT_PATH, RUNTIME_STATE_DIR, RuntimePaths, STATUS_PATH,
+    LOGS_DIR, RESULT_PATH, RUNTIME_STATE_DIR, RuntimeArtifactPaths, RuntimePaths, STATUS_PATH,
 };

RuntimeArtifactPaths is added to the pub use paths::{…} re-export in crates/pika-cloud/src/lib.rs. This is the only change needed to surface the type publicly because it was already defined in the paths submodule.

Embed RuntimeArtifactPaths into GuestStartupArtifacts with serde(flatten)

Intent: Replace the three individual path fields on GuestStartupArtifacts with a single RuntimeArtifactPaths field, using #[serde(flatten)] to preserve JSON wire compatibility.

Affected files: crates/pika-cloud/src/lib.rs

Evidence
@@ -314,9 +314,8 @@ pub struct GuestStartupArtifacts {
     pub startup_plan_path: String,
     pub identity_seed_path: String,
-    pub status_path: String,
-    pub events_path: String,
-    pub result_path: String,
+    #[serde(flatten)]
+    pub runtime_artifacts: RuntimeArtifactPaths,
     pub log_path: String,
     pub pid_path: String,
 }
@@ -326,9 +325,7 @@ impl Default for GuestStartupArtifacts {
-            status_path: STATUS_PATH.to_string(),
-            events_path: EVENTS_PATH.to_string(),
-            result_path: RESULT_PATH.to_string(),
+            runtime_artifacts: RuntimeArtifactPaths::default(),

The three loose fields (status_path, events_path, result_path) are removed from GuestStartupArtifacts and replaced by a single runtime_artifacts: RuntimeArtifactPaths field annotated with #[serde(flatten)].

Because flatten inlines the inner struct's fields into the parent during serialization and deserialization, the JSON representation is identical to before — consumers that read or write {"status_path": …, "events_path": …, "result_path": …} see no change. The Default impl delegates to RuntimeArtifactPaths::default().

Delegate path validation to RuntimeArtifactPaths

Intent: Remove the duplicated validation loop from GuestStartupArtifacts::validate_canonical_paths and call the new struct's own method instead.

Affected files: crates/pika-cloud/src/lib.rs

Evidence
@@ -349,21 +346,6 @@ impl GuestStartupArtifacts {
-            (
-                "status_path",
-                self.status_path.as_str(),
-                canonical.status_path.as_str(),
-            ),
-            (
-                "events_path",
-                self.events_path.as_str(),
-                canonical.events_path.as_str(),
-            ),
-            (
-                "result_path",
-                self.result_path.as_str(),
-                canonical.result_path.as_str(),
-            ),
@@ -381,6 +363,8 @@ impl GuestStartupArtifacts {
+        self.runtime_artifacts
+            .validate_canonical_paths("artifacts")?;

The 15-line validation block that checked status_path, events_path, and result_path individually inside GuestStartupArtifacts::validate_canonical_paths is deleted. In its place, a single call to self.runtime_artifacts.validate_canonical_paths("artifacts") is appended after the remaining field checks.

This keeps the same error semantics — non-canonical paths produce a descriptive Err(String) — while eliminating the duplication.

Update tests in pika-cloud and pika-server

Intent: Adjust all test assertions to access the three paths through the new runtime_artifacts field rather than directly on GuestStartupArtifacts.

Affected files: crates/pika-cloud/src/lib.rs, crates/pika-cloud/src/paths.rs, crates/pika-server/src/managed_openclaw_guest.rs

Evidence
@@ -953,9 +937,9 @@ mod tests {
-        assert_eq!(artifacts.status_path, STATUS_PATH);
-        assert_eq!(artifacts.events_path, EVENTS_PATH);
-        assert_eq!(artifacts.result_path, RESULT_PATH);
+        assert_eq!(artifacts.runtime_artifacts.status_path, STATUS_PATH);
+        assert_eq!(artifacts.runtime_artifacts.events_path, EVENTS_PATH);
+        assert_eq!(artifacts.runtime_artifacts.result_path, RESULT_PATH);
@@ -1084,7 +1068,10 @@ mod tests {
-                status_path: "/run/custom/status.json".to_string(),
+                runtime_artifacts: RuntimeArtifactPaths {
+                    status_path: "/run/custom/status.json".to_string(),
+                    ..RuntimeArtifactPaths::default()
+                },
@@ -761,9 +761,9 @@ mod tests {
-        assert_eq!(plan.artifacts.status_path, STATUS_PATH);
-        assert_eq!(plan.artifacts.events_path, EVENTS_PATH);
-        assert_eq!(plan.artifacts.result_path, RESULT_PATH);
+        assert_eq!(plan.artifacts.runtime_artifacts.status_path, STATUS_PATH);
+        assert_eq!(plan.artifacts.runtime_artifacts.events_path, EVENTS_PATH);
+        assert_eq!(plan.artifacts.runtime_artifacts.result_path, RESULT_PATH);
@@ +160,0 +160,25 @@
+    #[test]
+    fn runtime_artifact_paths_default_to_canonical_contract() {
+        let paths = RuntimeArtifactPaths::default();
+        assert_eq!(paths.status_path, STATUS_PATH);
+        assert_eq!(paths.events_path, EVENTS_PATH);
+        assert_eq!(paths.result_path, RESULT_PATH);
+    }
+
+    #[test]
+    fn runtime_artifact_paths_validate_rejects_non_canonical_paths() {

All existing tests that referenced artifacts.status_path (etc.) are updated to go through artifacts.runtime_artifacts.status_path. Two new unit tests are added in crates/pika-cloud/src/paths.rs:

  1. runtime_artifact_paths_default_to_canonical_contract — asserts that the default values match STATUS_PATH, EVENTS_PATH, and RESULT_PATH.
  2. runtime_artifact_paths_validate_rejects_non_canonical_paths — constructs a RuntimeArtifactPaths with a non-canonical status_path and verifies that validate_canonical_paths returns an error mentioning both the field prefix and the expected canonical path.

In pika-server, the guest_startup_plan_uses_shared_lifecycle_artifacts test is updated with the same accessor change, confirming that the server crate compiles and passes with the new nested field layout.

Diff