Skip to content
This repository was archived by the owner on Apr 8, 2026. It is now read-only.

Commit 21d823f

Browse files
committed
test: add integration tests for worker boot, lane events, hooks
Add five runtime integration tests that exercise worker boot readiness, lane event emission, hook merging, task packet roundtrips, and config validation through the public crate surface. Constraint: Keep coverage in rust/crates/runtime/tests without adding dependencies Rejected: Fold the cases into existing integration_tests.rs | harder to isolate the requested workflow coverage Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep these tests focused on cross-module wiring; leave unit-level edge cases in module-local test suites Tested: cargo test --workspace (from rust) Not-tested: Push result on remote CI
1 parent 43058dc commit 21d823f

File tree

1 file changed

+238
-0
lines changed

1 file changed

+238
-0
lines changed
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
use std::fs;
2+
use std::path::{Path, PathBuf};
3+
use std::time::{SystemTime, UNIX_EPOCH};
4+
5+
use runtime::task_registry::{TaskRegistry, TaskStatus};
6+
use runtime::{
7+
validate_packet, ConfigLoader, HookRunner, LaneEvent, LaneEventBlocker, LaneFailureClass,
8+
RuntimeHookConfig, TaskPacket, WorkerEventKind, WorkerFailureKind, WorkerRegistry,
9+
WorkerStatus,
10+
};
11+
use serde_json::json;
12+
13+
struct TestDir {
14+
path: PathBuf,
15+
}
16+
17+
impl TestDir {
18+
fn new(prefix: &str) -> Self {
19+
let unique = SystemTime::now()
20+
.duration_since(UNIX_EPOCH)
21+
.expect("time should be after epoch")
22+
.as_nanos();
23+
let path = std::env::temp_dir().join(format!("{prefix}-{unique}"));
24+
fs::create_dir_all(&path).expect("temp dir should be created");
25+
Self { path }
26+
}
27+
28+
fn path(&self) -> &Path {
29+
&self.path
30+
}
31+
}
32+
33+
impl Drop for TestDir {
34+
fn drop(&mut self) {
35+
if self.path.exists() {
36+
let _ = fs::remove_dir_all(&self.path);
37+
}
38+
}
39+
}
40+
41+
#[test]
42+
fn worker_boot_state_progresses_from_spawning_to_ready_snapshot() {
43+
let registry = WorkerRegistry::new();
44+
let worker = registry.create("/tmp/runtime-integration-worker", &[], true);
45+
46+
assert_eq!(worker.status, WorkerStatus::Spawning);
47+
assert_eq!(worker.events.len(), 1);
48+
assert_eq!(worker.events[0].kind, WorkerEventKind::Spawning);
49+
50+
let ready = registry
51+
.observe(&worker.worker_id, "Ready for your input\n>")
52+
.expect("ready observe should succeed");
53+
54+
assert_eq!(ready.status, WorkerStatus::ReadyForPrompt);
55+
assert!(ready.last_error.is_none());
56+
assert_eq!(
57+
ready.events.last().map(|event| event.kind),
58+
Some(WorkerEventKind::ReadyForPrompt)
59+
);
60+
61+
let snapshot = registry
62+
.await_ready(&worker.worker_id)
63+
.expect("ready snapshot should succeed");
64+
assert_eq!(snapshot.worker_id, worker.worker_id);
65+
assert!(snapshot.ready);
66+
assert!(!snapshot.blocked);
67+
assert!(!snapshot.replay_prompt_ready);
68+
assert!(snapshot.last_error.is_none());
69+
}
70+
71+
#[test]
72+
fn lane_event_emission_serializes_worker_prompt_delivery_failure() {
73+
let registry = WorkerRegistry::new();
74+
let worker = registry.create("/tmp/runtime-integration-lane", &[], true);
75+
registry
76+
.observe(&worker.worker_id, "Ready for input\n>")
77+
.expect("ready observe should succeed");
78+
registry
79+
.send_prompt(&worker.worker_id, Some("Run lane event emission test"))
80+
.expect("prompt send should succeed");
81+
82+
let failed = registry
83+
.observe(
84+
&worker.worker_id,
85+
"% Run lane event emission test\nzsh: command not found: Run",
86+
)
87+
.expect("misdelivery observe should succeed");
88+
89+
let error = failed
90+
.last_error
91+
.clone()
92+
.expect("prompt delivery failure should be recorded");
93+
assert_eq!(error.kind, WorkerFailureKind::PromptDelivery);
94+
95+
let blocker = LaneEventBlocker {
96+
failure_class: LaneFailureClass::PromptDelivery,
97+
detail: error.message,
98+
};
99+
let lane_event = LaneEvent::blocked("2026-04-04T00:00:00Z", &blocker).with_data(json!({
100+
"worker_id": failed.worker_id,
101+
"worker_status": failed.status,
102+
"worker_event_kinds": failed
103+
.events
104+
.iter()
105+
.map(|event| format!("{:?}", event.kind))
106+
.collect::<Vec<_>>()
107+
}));
108+
109+
let emitted = serde_json::to_value(&lane_event).expect("lane event should serialize");
110+
assert_eq!(emitted["event"], json!("lane.blocked"));
111+
assert_eq!(emitted["status"], json!("blocked"));
112+
assert_eq!(emitted["failureClass"], json!("prompt_delivery"));
113+
assert!(emitted["detail"]
114+
.as_str()
115+
.expect("detail should be a string")
116+
.contains("worker prompt landed in shell"));
117+
assert_eq!(emitted["data"]["worker_status"], json!("ready_for_prompt"));
118+
assert!(emitted["data"]["worker_event_kinds"]
119+
.as_array()
120+
.expect("worker event kinds should be an array")
121+
.iter()
122+
.any(|value| value == "PromptMisdelivery"));
123+
}
124+
125+
#[test]
126+
fn hook_merge_runs_loaded_config_hooks_and_overlay_once_each() {
127+
let temp = TestDir::new("runtime-hooks-integration");
128+
let cwd = temp.path().join("project");
129+
let home = temp.path().join("home").join(".claw");
130+
fs::create_dir_all(cwd.join(".claw")).expect("project config dir should exist");
131+
fs::create_dir_all(&home).expect("home config dir should exist");
132+
133+
fs::write(
134+
home.join("settings.json"),
135+
r#"{
136+
"hooks": {
137+
"PreToolUse": ["printf 'config pre'"]
138+
}
139+
}"#,
140+
)
141+
.expect("home settings should be written");
142+
fs::write(
143+
cwd.join(".claw").join("settings.local.json"),
144+
r#"{
145+
"hooks": {
146+
"PostToolUse": ["printf 'config post'"]
147+
}
148+
}"#,
149+
)
150+
.expect("project settings should be written");
151+
152+
let loaded = ConfigLoader::new(&cwd, &home)
153+
.load()
154+
.expect("config should load");
155+
let overlay = RuntimeHookConfig::new(
156+
vec![
157+
"printf 'config pre'".to_string(),
158+
"printf 'overlay pre'".to_string(),
159+
],
160+
vec![],
161+
vec![],
162+
);
163+
164+
let runner = HookRunner::new(loaded.hooks().merged(&overlay));
165+
let result = runner.run_pre_tool_use("Read", r#"{"path":"README.md"}"#);
166+
167+
assert_eq!(
168+
result.messages(),
169+
&["config pre".to_string(), "overlay pre".to_string()]
170+
);
171+
assert!(!result.is_failed());
172+
assert!(!result.is_denied());
173+
}
174+
175+
#[test]
176+
fn task_packet_roundtrip_validates_and_creates_registry_task() {
177+
let packet = TaskPacket {
178+
objective: "Ship runtime integration coverage".to_string(),
179+
scope: "runtime/tests".to_string(),
180+
repo: "claw-code-parity".to_string(),
181+
branch_policy: "origin/main only".to_string(),
182+
acceptance_tests: vec!["cargo test --workspace".to_string()],
183+
commit_policy: "single verified commit".to_string(),
184+
reporting_contract: "print verification summary and sha".to_string(),
185+
escalation_policy: "escalate only on destructive ambiguity".to_string(),
186+
};
187+
188+
let serialized = serde_json::to_string(&packet).expect("packet should serialize");
189+
let roundtrip: TaskPacket =
190+
serde_json::from_str(&serialized).expect("packet should deserialize");
191+
let validated = validate_packet(roundtrip.clone()).expect("packet should validate");
192+
193+
let registry = TaskRegistry::new();
194+
let task = registry
195+
.create_from_packet(validated.into_inner())
196+
.expect("task should be created from packet");
197+
registry
198+
.set_status(&task.task_id, TaskStatus::Running)
199+
.expect("status should update");
200+
201+
let stored = registry.get(&task.task_id).expect("task should be stored");
202+
assert_eq!(stored.prompt, packet.objective);
203+
assert_eq!(stored.description.as_deref(), Some("runtime/tests"));
204+
assert_eq!(stored.task_packet, Some(packet));
205+
assert_eq!(stored.status, TaskStatus::Running);
206+
}
207+
208+
#[test]
209+
fn config_validation_rejects_invalid_hook_entries_before_merge() {
210+
let temp = TestDir::new("runtime-config-validation");
211+
let cwd = temp.path().join("project");
212+
let home = temp.path().join("home").join(".claw");
213+
let project_settings = cwd.join(".claw").join("settings.json");
214+
fs::create_dir_all(cwd.join(".claw")).expect("project config dir should exist");
215+
fs::create_dir_all(&home).expect("home config dir should exist");
216+
217+
fs::write(
218+
home.join("settings.json"),
219+
r#"{"hooks":{"PreToolUse":["printf 'base'"]}}"#,
220+
)
221+
.expect("home settings should be written");
222+
fs::write(
223+
&project_settings,
224+
r#"{"hooks":{"PreToolUse":["printf 'project'",42]}}"#,
225+
)
226+
.expect("project settings should be written");
227+
228+
let error = ConfigLoader::new(&cwd, &home)
229+
.load()
230+
.expect_err("invalid hooks should fail validation");
231+
let rendered = error.to_string();
232+
233+
assert!(rendered.contains(&format!(
234+
"{}: hooks: field PreToolUse must contain only strings",
235+
project_settings.display()
236+
)));
237+
assert!(!rendered.contains("merged settings.hooks"));
238+
}

0 commit comments

Comments
 (0)