Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions crates/traverse-runtime/tests/executor_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,126 @@ fn wasm_executor_emit_event_handles_module_without_memory_export() -> Result<(),
Ok(())
}

#[test]
fn core_transition_action_status_emits_status_transitioned_on_accepted_transition()
-> Result<(), String> {
let wasm_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../examples/core-transition-action-status/artifacts/core-transition-action-status.wasm",
);
let wasm_bytes = std::fs::read(&wasm_path).map_err(|e| {
format!(
"failed to read {}: {e} (run examples/core-transition-action-status/build-fixture.sh first)",
wasm_path.display()
)
})?;

let executor = WasmExecutor::new().map_err(|e| format!("{e:?}"))?;
let input = json!({
"action_item_id": "item-emit-001",
"current_status": "open",
"requested_status": "in_progress",
"actor_id": "user-ada",
"owner_id": "user-ada",
"transition_config": {
"version": "1.0",
"allowed_transitions": {
"open": ["in_progress", "cancelled", "snoozed"],
"in_progress": ["blocked", "done", "cancelled", "snoozed"],
"blocked": ["in_progress", "cancelled"],
"snoozed": ["open", "in_progress", "cancelled"],
"done": [],
"cancelled": []
},
"owner_only": true
}
});

let output = executor
.run_bytes_with_capability(
&wasm_bytes,
&input,
"core.transition-action-status",
&[EventReference {
event_id: "core.action-item.status-transitioned".to_string(),
version: "1.0.0".to_string(),
}],
ServiceType::Subscribable,
)
.map_err(|e| format!("{e:?}"))?;

assert_eq!(output.value["allowed"], json!(true));
assert_eq!(output.value["reason_code"], json!("ok"));
assert_eq!(output.value["new_status"], json!("in_progress"));
assert_eq!(
output.emitted_events.len(),
1,
"accepted transition must emit exactly one governed event"
);
let event = &output.emitted_events[0];
assert_eq!(event.event_type, "core.action-item.status-transitioned");
assert_eq!(event.version, "1.0.0");
assert_eq!(event.data["action_item_id"], json!("item-emit-001"));
assert_eq!(event.data["from_status"], json!("open"));
assert_eq!(event.data["to_status"], json!("in_progress"));
assert_eq!(event.data["actor_id"], json!("user-ada"));
Ok(())
}

#[test]
fn core_transition_action_status_does_not_emit_on_rejected_transition() -> Result<(), String> {
let wasm_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../examples/core-transition-action-status/artifacts/core-transition-action-status.wasm",
);
let wasm_bytes = std::fs::read(&wasm_path).map_err(|e| {
format!(
"failed to read {}: {e} (run examples/core-transition-action-status/build-fixture.sh first)",
wasm_path.display()
)
})?;

let executor = WasmExecutor::new().map_err(|e| format!("{e:?}"))?;
let input = json!({
"action_item_id": "item-emit-002",
"current_status": "done",
"requested_status": "open",
"actor_id": "user-ada",
"owner_id": "user-ada",
"transition_config": {
"version": "1.0",
"allowed_transitions": {
"open": ["in_progress", "cancelled", "snoozed"],
"in_progress": ["blocked", "done", "cancelled", "snoozed"],
"blocked": ["in_progress", "cancelled"],
"snoozed": ["open", "in_progress", "cancelled"],
"done": [],
"cancelled": []
},
"owner_only": true
}
});

let output = executor
.run_bytes_with_capability(
&wasm_bytes,
&input,
"core.transition-action-status",
&[EventReference {
event_id: "core.action-item.status-transitioned".to_string(),
version: "1.0.0".to_string(),
}],
ServiceType::Subscribable,
)
.map_err(|e| format!("{e:?}"))?;

assert_eq!(output.value["allowed"], json!(false));
assert_eq!(output.value["reason_code"], json!("illegal_transition"));
assert!(
output.emitted_events.is_empty(),
"rejected transition must not emit"
);
Ok(())
}

// --- helpers ---

fn native_capability(id: &str) -> ExecutorCapability {
Expand Down
Binary file not shown.
58 changes: 37 additions & 21 deletions examples/core-transition-action-status/contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@
"id": "core.transition-action-status",
"namespace": "core",
"name": "transition-action-status",
"version": "1.2.0",
"version": "1.3.0",
"lifecycle": "active",
"owner": {
"team": "loop",
"contact": "founders@loop.dev"
},
"summary": "Deterministic state machine for action item status transitions (open \u2192 in_progress \u2192 blocked \u2192 snoozed \u2192 done / cancelled).",
"description": "Pure state-machine for action-item status transitions under a caller-supplied transition_config (allowed_transitions + owner_only).\n\nImplemented and smoke-tested matrix (use_cases):\n- open \u2192 in_progress (ok)\n- done \u2192 open illegal (illegal_transition)\n- open \u2192 snoozed (ok)\n- non-owner denied (not_owner)\n- in_progress \u2192 blocked (ok)\n- in_progress \u2192 done (ok)\n- blocked \u2192 in_progress (ok)\n- snoozed \u2192 open (ok)\n- open \u2192 cancelled (ok)\n- cancelled \u2192 open illegal (covers current_status=cancelled)\n- missing current_status \u2192 invalid_status\n\nKnown limitations (intentional in 1.2.0):\n- Transition graph is entirely caller-supplied; no built-in default policy\n- Does not persist status changes\n- Declares governed event core.action-item.status-transitioned@1.0.0 on accepted transitions; the WASM guest does not call the host emit ABI (declaration-only honesty with the published event product)",
"summary": "Deterministic state machine for action item status transitions (open in_progress blocked snoozed done / cancelled).",
"description": "Action-item status transitions under a caller-supplied transition_config (allowed_transitions + owner_only).\n\nImplemented and smoke-tested matrix (use_cases):\n- open in_progress (ok)\n- done open illegal (illegal_transition)\n- open snoozed (ok)\n- non-owner denied (not_owner)\n- in_progress blocked (ok)\n- in_progress done (ok)\n- blocked in_progress (ok)\n- snoozed open (ok)\n- open cancelled (ok)\n- cancelled open illegal (covers current_status=cancelled)\n- missing current_status invalid_status\n\nOn accepted transitions (reason_code=ok), the WASM guest calls traverse_host::emit_event for core.action-item.status-transitioned@1.0.0 with payload {action_item_id, from_status, to_status, actor_id}.\n\nKnown limitations (intentional in 1.3.0):\n- Transition graph is entirely caller-supplied; no built-in default policy\n- Does not persist status changes",
"use_cases": [
{
"scenario": "As an owner, I want to move an item from open to in_progress, so that active work is reflected accurately.",
"input_example": {
"action_item_id": "item-001",
"current_status": "open",
"requested_status": "in_progress",
"actor_id": "user-ada",
Expand Down Expand Up @@ -54,16 +55,17 @@
"new_status": "in_progress",
"reason_code": "ok",
"evaluation_trace": [
"open \u2192 in_progress is allowed",
"open in_progress is allowed",
"actor is owner"
]
},
"happy": true,
"persona_ref": "meeting-organizer"
},
{
"scenario": "As a system, I want an illegal transition (done \u2192 open) rejected, so that completed work cannot silently reopen.",
"scenario": "As a system, I want an illegal transition (done open) rejected, so that completed work cannot silently reopen.",
"input_example": {
"action_item_id": "item-002",
"current_status": "done",
"requested_status": "open",
"actor_id": "user-ada",
Expand Down Expand Up @@ -111,6 +113,7 @@
{
"scenario": "As an owner, I want to snooze an open item, so that I can pause follow-up without losing the item.",
"input_example": {
"action_item_id": "item-003",
"current_status": "open",
"requested_status": "snoozed",
"actor_id": "user-ada",
Expand Down Expand Up @@ -149,7 +152,7 @@
"new_status": "snoozed",
"reason_code": "ok",
"evaluation_trace": [
"open \u2192 snoozed is allowed",
"open snoozed is allowed",
"actor is owner"
]
},
Expand All @@ -159,6 +162,7 @@
{
"scenario": "As a system, I want a non-owner blocked when owner_only is true, so that only the owner can change status.",
"input_example": {
"action_item_id": "item-004",
"current_status": "open",
"requested_status": "in_progress",
"actor_id": "user-bob",
Expand Down Expand Up @@ -204,8 +208,9 @@
"persona_ref": "runtime-engineer"
},
{
"scenario": "As an owner, I want in_progress \u2192 blocked, so that blockers are an explicit status, not tribal knowledge.",
"scenario": "As an owner, I want in_progress blocked, so that blockers are an explicit status, not tribal knowledge.",
"input_example": {
"action_item_id": "item-005",
"current_status": "in_progress",
"requested_status": "blocked",
"actor_id": "user-ada",
Expand Down Expand Up @@ -244,15 +249,16 @@
"new_status": "blocked",
"reason_code": "ok",
"evaluation_trace": [
"in_progress \u2192 blocked is allowed"
"in_progress blocked is allowed"
]
},
"happy": true,
"persona_ref": "meeting-organizer"
},
{
"scenario": "As an owner, I want in_progress \u2192 done, so that finished work can leave the active queue.",
"scenario": "As an owner, I want in_progress done, so that finished work can leave the active queue.",
"input_example": {
"action_item_id": "item-006",
"current_status": "in_progress",
"requested_status": "done",
"actor_id": "user-ada",
Expand Down Expand Up @@ -291,15 +297,16 @@
"new_status": "done",
"reason_code": "ok",
"evaluation_trace": [
"in_progress \u2192 done is allowed"
"in_progress done is allowed"
]
},
"happy": true,
"persona_ref": "meeting-organizer"
},
{
"scenario": "As an owner, I want blocked \u2192 in_progress, so that cleared blockers resume cleanly.",
"scenario": "As an owner, I want blocked in_progress, so that cleared blockers resume cleanly.",
"input_example": {
"action_item_id": "item-007",
"current_status": "blocked",
"requested_status": "in_progress",
"actor_id": "user-ada",
Expand Down Expand Up @@ -338,15 +345,16 @@
"new_status": "in_progress",
"reason_code": "ok",
"evaluation_trace": [
"blocked \u2192 in_progress is allowed"
"blocked in_progress is allowed"
]
},
"happy": true,
"persona_ref": "meeting-organizer"
},
{
"scenario": "As an owner, I want snoozed \u2192 open, so that snoozed items re-enter the follow-up pool.",
"scenario": "As an owner, I want snoozed open, so that snoozed items re-enter the follow-up pool.",
"input_example": {
"action_item_id": "item-008",
"current_status": "snoozed",
"requested_status": "open",
"actor_id": "user-ada",
Expand Down Expand Up @@ -385,15 +393,16 @@
"new_status": "open",
"reason_code": "ok",
"evaluation_trace": [
"snoozed \u2192 open is allowed"
"snoozed open is allowed"
]
},
"happy": true,
"persona_ref": "meeting-organizer"
},
{
"scenario": "As an owner, I want open \u2192 cancelled, so that abandoned work can exit without fake completion.",
"scenario": "As an owner, I want open cancelled, so that abandoned work can exit without fake completion.",
"input_example": {
"action_item_id": "item-009",
"current_status": "open",
"requested_status": "cancelled",
"actor_id": "user-ada",
Expand Down Expand Up @@ -432,7 +441,7 @@
"new_status": "cancelled",
"reason_code": "ok",
"evaluation_trace": [
"open \u2192 cancelled is allowed"
"open cancelled is allowed"
]
},
"happy": true,
Expand All @@ -441,6 +450,7 @@
{
"scenario": "As a system, I want cancelled items to reject further transitions, so that cancelled work stays terminal.",
"input_example": {
"action_item_id": "item-010",
"current_status": "cancelled",
"requested_status": "open",
"actor_id": "user-ada",
Expand Down Expand Up @@ -488,6 +498,7 @@
{
"scenario": "As a system, I want missing current_status rejected with invalid_status, so that incomplete transition requests fail closed.",
"input_example": {
"action_item_id": "item-011",
"current_status": "",
"requested_status": "open",
"actor_id": "user-ada",
Expand Down Expand Up @@ -537,12 +548,16 @@
"schema": {
"type": "object",
"required": [
"action_item_id",
"current_status",
"requested_status",
"actor_id",
"transition_config"
],
"properties": {
"action_item_id": {
"type": "string"
},
"current_status": {
"type": "string",
"enum": [
Expand Down Expand Up @@ -647,7 +662,7 @@
},
{
"kind": "event_emission",
"description": "Declares domain fact core.action-item.status-transitioned when a transition is accepted (ok)"
"description": "Emits core.action-item.status-transitioned@1.0.0 via traverse_host::emit_event when a transition is accepted (ok)"
}
],
"emits": [
Expand Down Expand Up @@ -689,25 +704,26 @@
"source": "ai-assisted",
"author": "loop-founders + traverse-capability-author",
"created_at": "2026-08-08T06:00:00Z",
"spec_ref": "core.transition-action-status@1.2.0",
"spec_ref": "core.transition-action-status@1.3.0",
"adr_refs": [
"persona-council-review-2026-08-08"
],
"exception_refs": []
},
"evidence": [
{
"evidence_id": "core-transition-action-status-1.2.0-contract-validation",
"evidence_id": "core-transition-action-status-1.3.0-contract-validation",
"type": "contract_validation",
"status": "passed"
}
],
"service_type": "stateless",
"service_type": "subscribable",
"permitted_targets": [
"local",
"edge",
"browser",
"cloud"
],
"artifact_type": "native"
"artifact_type": "native",
"event_trigger": "core.transition-action-status.triggered"
}
10 changes: 5 additions & 5 deletions examples/core-transition-action-status/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
"kind": "capability_package",
"schema_version": "1.0.0",
"package_id": "core.transition-action-status-agent",
"version": "1.2.0",
"summary": "Capability package for core.transition-action-status@1.2.0.",
"version": "1.3.0",
"summary": "Capability package for core.transition-action-status@1.3.0.",
"capability_ref": {
"id": "core.transition-action-status",
"version": "1.2.0",
"version": "1.3.0",
"contract_path": "./contract.json"
},
"workflow_refs": [
{
"workflow_id": "core.transition-action-status",
"workflow_version": "1.2.0"
"workflow_version": "1.3.0"
}
],
"source": {
Expand All @@ -23,7 +23,7 @@
"binary": {
"path": "./artifacts/core-transition-action-status.wasm",
"format": "wasm",
"expected_digest": "fnv1a64:f392f46763fdd2af",
"expected_digest": "fnv1a64:3ac908d226fb7954",
"abi_version": "1.0.0"
},
"constraints": {
Expand Down
Loading
Loading