Back to feed

sledtools/pika branch #94

pika-cloud-guest-writer-contract

Add guest lifecycle contract tests

Target branch: master

Merge Commit: 8d773a6a6166e1754b5263c764de73766c851af9

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

6 passed

head 32f33f0730ef330ec2da957fab90d02db007d82f · queued 2026-03-26 01:33:20 · 6 lane(s)

queued 8s · ran 30s

check-notifications · success check-agent-contracts · success check-pikachat · success check-pikachat-typescript · success check-pikachat-openclaw-e2e · success check-fixture · success

Summary

This branch introduces contract tests for the pika-cloud guest lifecycle, ensuring that generated shell scripts (managed OpenClaw guest) and the Incus guest image source (Nix/Python) both conform to a shared status, event, and terminal-result schema. Four new test functions pin the exact field names, state transitions, and helper signatures emitted by the code-generation layer, preventing silent contract drift between the server-side guest orchestration and the CI executor's Linux image. A shared assert_contains_all helper is added in both test modules to keep assertions concise.

Tutorial Steps

Add `assert_contains_all` test helper in `managed_openclaw_guest`

Intent: Introduce a reusable assertion helper that checks a generated string contains every expected substring, producing a clear error message on failure. This avoids repeating individual `assert!(haystack.contains(...))` lines across multiple contract tests.

Affected files: crates/pika-server/src/managed_openclaw_guest.rs

Evidence
@@ -748,6 +748,15 @@ mod tests {
     use super::*;
     use pika_cloud::{EVENTS_PATH, RESULT_PATH, STATUS_PATH};
 
+    fn assert_contains_all(haystack: &str, needles: &[&str]) {
+        for needle in needles {
+            assert!(
+                haystack.contains(needle),
+                "expected generated guest script to contain `{needle}`"
+            );
+        }
+    }

A small utility function assert_contains_all is added inside the existing mod tests block. It iterates over a slice of expected substrings and asserts each one is present in haystack, formatting a descriptive panic message that includes the missing needle.

This keeps subsequent contract tests compact—callers pass a single &[&str] slice instead of writing N separate assertions.

Pin the shared status and event contract in the OpenClaw autostart script

Intent: Verify that `guest_autostart_script()` emits the canonical `write_status()` and `append_event()` shell helpers with the exact field names (`schema_version`, `state`, `updated_at`, `message`, `seq`, `timestamp`, `kind`) and the correct lifecycle state transitions (`booted → starting → ready → completed/failed`). A negative assertion ensures the obsolete `"passed"` status is never emitted.

Affected files: crates/pika-server/src/managed_openclaw_guest.rs

Evidence
@@ -769,4 +778,58 @@ mod tests {
+    #[test]
+    fn guest_autostart_script_pins_shared_status_and_event_contract() {
+        let script = guest_autostart_script();
+
+        assert_contains_all(
+            &script,
+            &[
+                "write_status() {",
+                "schema_version: $schema_version",
+                "state: $state",
+                "updated_at: $updated_at",
+                "message: $message",
+                "write_status \"booted\" \"managed OpenClaw guest booted\"",
+                "write_status \"starting\" \"waiting for OpenClaw health\"",
+                "write_status \"starting\" \"OpenClaw health ready; publishing keypackage\"",
+                "write_status \"ready\" \"managed OpenClaw guest ready\"",
+                "write_status \"completed\" \"$message\" \"$details_json\"",
+                "write_status \"failed\" \"$reason\" \"$details_json\"",
+                "append_event() {",
+                "seq: $seq",
+                "timestamp: $timestamp",
+                "kind: $kind",
+                "message: $message",
+                "append_event \"booted\" \"managed OpenClaw guest booted\"",
+                "append_event \"ready\" \"managed OpenClaw guest ready\"",
+                "append_event \"completed\" \"$message\" \"$details_json\"",
+                "append_event \"failed\" \"$reason\" \"$details_json\"",
+            ],
+        );
+        assert!(!script.contains("\"status\": \"passed\""));

The test guest_autostart_script_pins_shared_status_and_event_contract calls guest_autostart_script() and checks for:

  1. Helper function signatureswrite_status() { and append_event() { must appear verbatim.
  2. Schema fields – Each helper must emit schema_version, state/kind, updated_at/timestamp, and message.
  3. Lifecycle transitions – Every expected invocation (booted, starting, ready, completed, failed) with its human-readable message must be present.
  4. Negative guard – The legacy "passed" status string must not appear.

If the code-generation logic in guest_autostart_script() ever renames a field or changes a state string, this test will catch it immediately.

Pin the shared terminal result contract in the OpenClaw autostart script

Intent: Ensure the `write_result()` shell helper and its call-sites use the agreed-upon fields (`schema_version`, `status`, `finished_at`, `message`, `exit_code`) and only emit `completed` or `failed`—never `passed`.

Affected files: crates/pika-server/src/managed_openclaw_guest.rs

Evidence
@@ -769,4 +778,58 @@ mod tests {
+    #[test]
+    fn guest_autostart_script_pins_shared_terminal_result_contract() {
+        let script = guest_autostart_script();
+
+        assert_contains_all(
+            &script,
+            &[
+                "write_result() {",
+                "schema_version: $schema_version",
+                "status: $status",
+                "finished_at: $finished_at",
+                "message: $message",
+                "exit_code: $exit_code",
+                "write_result \"completed\" \"$exit_code\" \"$message\" \"$details_json\"",
+                "write_result \"failed\" \"$exit_code\" \"$reason\" \"$details_json\"",
+            ],
+        );
+        assert!(!script.contains("write_result \"passed\""));

A companion test guest_autostart_script_pins_shared_terminal_result_contract focuses specifically on the write_result() helper. It validates:

  • The function signature exists.
  • All required JSON fields (schema_version, status, finished_at, message, exit_code) are present.
  • Both terminal invocations (completed and failed) are emitted with the correct argument ordering.
  • The obsolete write_result "passed" pattern is absent.

Together with the previous test, this locks down the full lifecycle and result reporting surface of the generated shell script.

Add `assert_contains_all` and `incus_guest_image_source` helpers in the executor test module

Intent: Mirror the assertion helper in the `pikaci` crate's executor tests and add a convenience function that statically includes the Nix/Python guest image source so contract tests can inspect it at compile time.

Affected files: crates/pikaci/src/executor.rs

Evidence
@@ -2470,6 +2470,19 @@ mod tests {
+    fn assert_contains_all(haystack: &str, needles: &[&str]) {
+        for needle in needles {
+            assert!(
+                haystack.contains(needle),
+                "expected Incus guest image source to contain `{needle}`"
+            );
+        }
+    }
+
+    fn incus_guest_image_source() -> &'static str {
+        include_str!("../../../nix/incus/pikaci-image.nix")
+    }

assert_contains_all is duplicated here (rather than extracted into a shared test utility crate) because the error message is tailored to the Incus context ("expected Incus guest image source to contain …").

incus_guest_image_source() uses include_str! to embed nix/incus/pikaci-image.nix at compile time, giving the tests a zero-cost, always-in-sync snapshot of the guest image definition to assert against.

Pin the shared status and event contract in the Incus guest image

Intent: Verify that the Python lifecycle helpers inside the Nix-defined Incus guest image use the same field names and state transitions as the OpenClaw guest, ensuring both guest runtimes conform to one canonical contract.

Affected files: crates/pikaci/src/executor.rs

Evidence
@@ -2563,6 +2576,57 @@ mod tests {
+    #[test]
+    fn remote_linux_incus_image_pins_shared_status_and_event_contract() {
+        let source = incus_guest_image_source();
+
+        assert_contains_all(
+            source,
+            &[
+                "def write_status(state_dir: pathlib.Path, state: str, message: str, **fields) -> None:",
+                "\"schema_version\": LIFECYCLE_SCHEMA_VERSION",
+                "\"state\": state",
+                "\"updated_at\": finished_at()",
+                "\"message\": message",
+                "def append_event(events_path: pathlib.Path, kind: str, message: str, **fields) -> None:",
+                "\"seq\": EVENT_SEQ",
+                "\"kind\": kind",
+                "\"timestamp\": finished_at()",
+                "\"message\": message",
+                "write_status(state_dir, \"booted\", \"incus guest booted\")",
+                "append_event(events_path, \"booted\", \"incus guest booted\")",
+                "\"launching guest command\"",
+                "lifecycle_state = \"completed\" if exit_code == 0 else \"failed\"",
+                "write_status(state_dir, \"failed\", f\"Incus guest bootstrap failed: {exc}\")",
+                "append_event(events_path, \"failed\", f\"Incus guest bootstrap failed: {exc}\")",
+            ],
+        );
+        assert!(!source.contains("lifecycle_state = \"passed\""));
+        assert!(!source.contains("\"status\": \"passed\""));

This test reads the Incus guest image Nix source (which embeds Python) and checks that the Python write_status and append_event functions:

  • Have the expected signatures (including **fields for extensibility).
  • Emit the canonical JSON fields: schema_version, state, updated_at, message for status; seq, kind, timestamp, message for events.
  • Use the agreed lifecycle states at every call-site (booted, starting/launching, completed, failed).
  • Never use the retired "passed" state.

This is the cross-runtime counterpart to the OpenClaw shell-script test—both must pass for the contract to be considered stable.

Pin the shared terminal result contract in the Incus guest image

Intent: Lock down the `write_result()` Python function and its call-sites in the Incus image, ensuring the terminal result JSON matches the same schema used by the OpenClaw guest.

Affected files: crates/pikaci/src/executor.rs

Evidence
@@ -2563,6 +2576,57 @@ mod tests {
+    #[test]
+    fn remote_linux_incus_image_pins_shared_terminal_result_contract() {
+        let source = incus_guest_image_source();
+
+        assert_contains_all(
+            source,
+            &[
+                "def write_result(state_dir: pathlib.Path, exit_code: int, message: str) -> None:",
+                "status = \"completed\" if exit_code == 0 else \"failed\"",
+                "state_dir / \"result.json\"",
+                "\"schema_version\": 1",
+                "\"status\": status",
+                "\"exit_code\": exit_code",
+                "\"finished_at\": finished_at()",
+                "\"message\": message",
+                "write_result(state_dir, exit_code, message)",
+                "write_result(state_dir, 2, f\"Incus guest bootstrap failed: {exc}\")",
+            ],
+        );
+        assert!(!source.contains("status = \"passed\""));

The final test remote_linux_incus_image_pins_shared_terminal_result_contract completes the coverage matrix:

Status + EventsTerminal Result
OpenClaw (shell)guest_autostart_script_pins_shared_status_and_event_contractguest_autostart_script_pins_shared_terminal_result_contract
Incus (Python/Nix)remote_linux_incus_image_pins_shared_status_and_event_contractremote_linux_incus_image_pins_shared_terminal_result_contract

It asserts that write_result() writes to result.json with schema_version: 1, derives status from exit_code, and includes finished_at and message. The negative guard rejects status = "passed".

With all four tests in place, any change to either guest runtime's lifecycle reporting will trigger a compile-time test failure, keeping the two implementations in lockstep.

Diff