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

Commit c195113

Browse files
committed
test: add integration tests for worker boot, lane events, hooks
Add a focused runtime integration test file that exercises worker boot state, lane event emission, hook config merging and execution, task packet roundtrips, and config validation through the public runtime APIs. Constraint: Keep the change scoped to integration coverage without touching runtime behavior Rejected: Reusing broader untracked workspace changes | would mix unrelated work into this request Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep these tests aligned with the public runtime re-exports and cross-module wiring paths Tested: cargo test -p runtime --test runtime_workflows; cargo test --workspace Not-tested: No additional lint-only pass beyond rustfmt
1 parent eea7651 commit c195113

1 file changed

Lines changed: 230 additions & 0 deletions

File tree

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
//! Integration tests for runtime workflows that span multiple subsystems.
2+
3+
use std::fs;
4+
use std::path::PathBuf;
5+
use std::time::{SystemTime, UNIX_EPOCH};
6+
7+
use runtime::task_registry::TaskRegistry;
8+
use runtime::{
9+
validate_packet, ConfigLoader, HookRunner, LaneEvent, LaneEventBlocker, LaneFailureClass,
10+
RuntimeFeatureConfig, RuntimeHookConfig, TaskPacket, WorkerEventKind, WorkerRegistry,
11+
WorkerStatus,
12+
};
13+
use serde_json::json;
14+
15+
fn temp_dir(prefix: &str) -> PathBuf {
16+
let nanos = SystemTime::now()
17+
.duration_since(UNIX_EPOCH)
18+
.expect("time should be after epoch")
19+
.as_nanos();
20+
std::env::temp_dir().join(format!("runtime-{prefix}-{nanos}"))
21+
}
22+
23+
#[test]
24+
fn worker_boot_state_tracks_trust_resolution_and_ready_snapshot() {
25+
let registry = WorkerRegistry::new();
26+
let cwd = "/tmp/runtime-worker-boot-integration";
27+
let worker = registry.create(cwd, &[cwd.to_string()], true);
28+
29+
let spawning = registry
30+
.observe(
31+
&worker.worker_id,
32+
"Do you trust the files in this folder?\nAllow and continue",
33+
)
34+
.expect("trust prompt should be observed");
35+
assert_eq!(spawning.status, WorkerStatus::Spawning);
36+
assert!(spawning.trust_gate_cleared);
37+
38+
registry
39+
.observe(&worker.worker_id, "Ready for your input\n>")
40+
.expect("ready cue should be observed");
41+
42+
let snapshot = registry
43+
.await_ready(&worker.worker_id)
44+
.expect("snapshot should be available");
45+
assert!(snapshot.ready);
46+
assert!(!snapshot.blocked);
47+
assert_eq!(snapshot.status, WorkerStatus::ReadyForPrompt);
48+
assert_eq!(snapshot.last_error, None);
49+
50+
let stored = registry
51+
.get(&worker.worker_id)
52+
.expect("worker should exist");
53+
let event_kinds: Vec<_> = stored.events.iter().map(|event| event.kind).collect();
54+
assert_eq!(
55+
event_kinds,
56+
vec![
57+
WorkerEventKind::Spawning,
58+
WorkerEventKind::TrustRequired,
59+
WorkerEventKind::TrustResolved,
60+
WorkerEventKind::ReadyForPrompt,
61+
]
62+
);
63+
}
64+
65+
#[test]
66+
fn lane_event_emission_serializes_prompt_misdelivery_failures() {
67+
let registry = WorkerRegistry::new();
68+
let worker = registry.create("/tmp/runtime-lane-event-integration", &[], true);
69+
registry
70+
.observe(&worker.worker_id, "Ready for input\n>")
71+
.expect("worker should become ready");
72+
registry
73+
.send_prompt(&worker.worker_id, Some("Run lane event parity checks"))
74+
.expect("prompt dispatch should succeed");
75+
76+
let failed = registry
77+
.observe(
78+
&worker.worker_id,
79+
"% Run lane event parity checks\nzsh: command not found: Run",
80+
)
81+
.expect("prompt misdelivery should be classified");
82+
assert_eq!(failed.status, WorkerStatus::ReadyForPrompt);
83+
let failure = failed
84+
.last_error
85+
.as_ref()
86+
.expect("misdelivery should record a failure");
87+
88+
let blocker = LaneEventBlocker {
89+
failure_class: LaneFailureClass::PromptDelivery,
90+
detail: failure.message.clone(),
91+
};
92+
let emitted = serde_json::to_value(
93+
LaneEvent::failed("2026-04-04T00:00:00Z", &blocker).with_data(json!({
94+
"worker_id": failed.worker_id,
95+
"attempts": failed.prompt_delivery_attempts,
96+
"replay_prompt_ready": failed.replay_prompt.is_some(),
97+
})),
98+
)
99+
.expect("lane event should serialize");
100+
101+
assert_eq!(emitted["event"], json!("lane.failed"));
102+
assert_eq!(emitted["status"], json!("failed"));
103+
assert_eq!(emitted["failureClass"], json!("prompt_delivery"));
104+
assert_eq!(emitted["data"]["attempts"], json!(1));
105+
assert_eq!(emitted["data"]["replay_prompt_ready"], json!(true));
106+
assert!(emitted["detail"]
107+
.as_str()
108+
.expect("detail should be serialized")
109+
.contains("worker prompt landed in shell"));
110+
}
111+
112+
#[test]
113+
fn hook_merge_runs_deduped_commands_across_hook_stages() {
114+
let base = RuntimeHookConfig::new(
115+
vec!["printf 'base-pre'".to_string()],
116+
vec!["printf 'base-post'".to_string()],
117+
vec!["printf 'base-failure'".to_string()],
118+
);
119+
let overlay = RuntimeHookConfig::new(
120+
vec![
121+
"printf 'base-pre'".to_string(),
122+
"printf 'overlay-pre'".to_string(),
123+
],
124+
vec!["printf 'overlay-post'".to_string()],
125+
vec![
126+
"printf 'base-failure'".to_string(),
127+
"printf 'overlay-failure'".to_string(),
128+
],
129+
);
130+
let runner = HookRunner::from_feature_config(
131+
&RuntimeFeatureConfig::default().with_hooks(base.merged(&overlay)),
132+
);
133+
134+
let pre = runner.run_pre_tool_use("Read", r#"{"path":"README.md"}"#);
135+
let post = runner.run_post_tool_use("Read", r#"{"path":"README.md"}"#, "ok", false);
136+
let failure = runner.run_post_tool_use_failure("Read", r#"{"path":"README.md"}"#, "boom");
137+
138+
assert_eq!(
139+
pre.messages(),
140+
&["base-pre".to_string(), "overlay-pre".to_string()]
141+
);
142+
assert_eq!(
143+
post.messages(),
144+
&["base-post".to_string(), "overlay-post".to_string()]
145+
);
146+
assert_eq!(
147+
failure.messages(),
148+
&["base-failure".to_string(), "overlay-failure".to_string(),]
149+
);
150+
}
151+
152+
#[test]
153+
fn task_packet_roundtrip_survives_validation_and_registry_storage() {
154+
let packet = TaskPacket {
155+
objective: "Ship worker boot integration coverage".to_string(),
156+
scope: "runtime/tests".to_string(),
157+
repo: "claw-code-parity".to_string(),
158+
branch_policy: "origin/main only".to_string(),
159+
acceptance_tests: vec![
160+
"cargo test -p runtime --test runtime_workflows".to_string(),
161+
"cargo test --workspace".to_string(),
162+
],
163+
commit_policy: "single verified commit".to_string(),
164+
reporting_contract: "print verification evidence and sha".to_string(),
165+
escalation_policy: "escalate only on destructive ambiguity".to_string(),
166+
};
167+
168+
let serialized = serde_json::to_string(&packet).expect("packet should serialize");
169+
let decoded: TaskPacket = serde_json::from_str(&serialized).expect("packet should decode");
170+
let validated = validate_packet(decoded).expect("packet should validate");
171+
172+
let registry = TaskRegistry::new();
173+
let task = registry
174+
.create_from_packet(validated.into_inner())
175+
.expect("packet-backed task should be created");
176+
registry
177+
.update(&task.task_id, "Keep runtime hook semantics stable")
178+
.expect("task update should succeed");
179+
registry
180+
.append_output(&task.task_id, "tests passed")
181+
.expect("task output should append");
182+
183+
let stored = registry.get(&task.task_id).expect("task should be stored");
184+
assert_eq!(stored.prompt, packet.objective);
185+
assert_eq!(stored.description.as_deref(), Some("runtime/tests"));
186+
assert_eq!(stored.task_packet, Some(packet));
187+
assert_eq!(stored.messages.len(), 1);
188+
assert_eq!(
189+
stored.messages[0].content,
190+
"Keep runtime hook semantics stable"
191+
);
192+
assert_eq!(
193+
registry.output(&task.task_id).expect("output should exist"),
194+
"tests passed"
195+
);
196+
}
197+
198+
#[test]
199+
fn config_validation_rejects_invalid_hook_shapes_before_runtime_use() {
200+
let root = temp_dir("config-validation");
201+
let cwd = root.join("project");
202+
let home = root.join("home").join(".claw");
203+
fs::create_dir_all(cwd.join(".claw")).expect("project config dir");
204+
fs::create_dir_all(&home).expect("home config dir");
205+
206+
let bad_settings = cwd.join(".claw").join("settings.local.json");
207+
fs::write(
208+
home.join("settings.json"),
209+
r#"{"hooks":{"PreToolUse":["printf 'base'"]}}"#,
210+
)
211+
.expect("write base settings");
212+
fs::write(
213+
&bad_settings,
214+
r#"{"hooks":{"PreToolUse":["printf 'overlay'",42]}}"#,
215+
)
216+
.expect("write invalid local settings");
217+
218+
let error = ConfigLoader::new(&cwd, &home)
219+
.load()
220+
.expect_err("invalid hook shapes should fail validation");
221+
let rendered = error.to_string();
222+
223+
assert!(rendered.contains(&format!(
224+
"{}: hooks: field PreToolUse must contain only strings",
225+
bad_settings.display()
226+
)));
227+
assert!(!rendered.contains("merged settings.hooks"));
228+
229+
fs::remove_dir_all(root).expect("cleanup temp dir");
230+
}

0 commit comments

Comments
 (0)