From edd30678f4116421ffab1c94b1e891134003fb58 Mon Sep 17 00:00:00 2001 From: Rita Agafonova <36133358+rita-aga@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:35:55 -0400 Subject: [PATCH 1/5] feat(observe): expose action params and hints in spec detail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /observe/specs/{entity} returned actions without their params or agent hints, though both are parsed from the IOA spec. Agent platforms need them to generate typed client APIs from the live spec (Code Mode pattern: the schema-derived API replaces prose documentation in prompts). Additive, serde-defaulted fields — existing consumers are unaffected. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DKJ5oR9ELxyVmzjioKrXHJ --- crates/temper-server/src/observe/mod.rs | 14 ++++++++++++++ crates/temper-server/src/observe/specs.rs | 13 ++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/crates/temper-server/src/observe/mod.rs b/crates/temper-server/src/observe/mod.rs index fbd3e3cfd..3465d7942 100644 --- a/crates/temper-server/src/observe/mod.rs +++ b/crates/temper-server/src/observe/mod.rs @@ -74,6 +74,20 @@ pub struct ActionDetail { pub guards: Vec, /// Effects (Debug representation). pub effects: Vec, + /// Parameters this action accepts, in spec order. + #[serde(default)] + pub params: Vec, + /// Agent-facing hint from the spec (empty when the spec has none). + #[serde(default)] + pub hint: String, +} + +/// A single action parameter: name plus its declared type ("string" default). +#[derive(Serialize, Deserialize)] +pub struct ActionParamDetail { + pub name: String, + #[serde(rename = "type")] + pub param_type: String, } /// Detail of a single invariant. diff --git a/crates/temper-server/src/observe/specs.rs b/crates/temper-server/src/observe/specs.rs index dab48e848..69905e8c4 100644 --- a/crates/temper-server/src/observe/specs.rs +++ b/crates/temper-server/src/observe/specs.rs @@ -8,7 +8,9 @@ use crate::authz::{observe_tenant_scope, require_observe_auth}; use crate::registry::VerificationStatus; use crate::state::ServerState; -use super::{ActionDetail, InvariantDetail, SpecDetail, SpecSummary, StateVarDetail}; +use super::{ + ActionDetail, ActionParamDetail, InvariantDetail, SpecDetail, SpecSummary, StateVarDetail, +}; mod load_dir; mod load_inline; @@ -115,6 +117,15 @@ pub(crate) async fn handle_get_spec_detail( to: a.to.clone(), guards: a.guard.iter().map(|g| format!("{g:?}")).collect(), effects: a.effect.iter().map(|e| format!("{e:?}")).collect(), + params: a + .params + .iter() + .map(|p| ActionParamDetail { + name: p.name().to_string(), + param_type: p.param_type().to_string(), + }) + .collect(), + hint: a.hint.clone().unwrap_or_default(), }) .collect(), invariants: automaton From d36829303134f3ede5550f221d432a5ac058c066 Mon Sep 17 00:00:00 2001 From: Rita Agafonova <36133358+rita-aga@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:27:49 -0400 Subject: [PATCH 2/5] test(server): declare observe feature requirement for spec_validate_endpoint (ARN-281) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The integration test hits /api/specs/validate-ioa, which only exists with the 'observe' feature — under default features the route 404s and the test target has been failing on pristine main. Declare required-features = ["observe"] on the test target (the canonical Cargo mechanism), and add an explicit CI step running it with the feature enabled so it never silently drops out of coverage. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Vw8tCkfnUW8p149yUNfSEE --- .github/workflows/ci.yml | 5 +++++ crates/temper-server/Cargo.toml | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c29f86dc..7a65c0196 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -265,6 +265,11 @@ jobs: - name: cargo test --workspace -- --skip dst_ run: cargo test --workspace -- --skip dst_ + # spec_validate_endpoint declares required-features = ["observe"], so the + # default-features workspace run above skips it; run it here explicitly. + - name: cargo test observe-gated endpoint tests + run: cargo test -p temper-server --features observe --test spec_validate_endpoint + # ───────────────────────────────────────────────────── # Gate 3b: DST/platform coverage (matrixed) # ───────────────────────────────────────────────────── diff --git a/crates/temper-server/Cargo.toml b/crates/temper-server/Cargo.toml index 6ce2378df..0ed9c51e5 100644 --- a/crates/temper-server/Cargo.toml +++ b/crates/temper-server/Cargo.toml @@ -6,6 +6,10 @@ license.workspace = true rust-version.workspace = true description = "HTTP server assembly for Temper entity services" +[[test]] +name = "spec_validate_endpoint" +required-features = ["observe"] + [features] default = [] observe = ["temper-verify"] From 0f3caa40a7b4d3d3def2d005dea174fb67f02eee Mon Sep 17 00:00:00 2001 From: Rita Agafonova <36133358+rita-aga@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:48:30 -0400 Subject: [PATCH 3/5] test(observe): lock the params/hint wire contract; document ActionParamDetail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review findings from three independent reviewers (grok-4.5, codex gpt-5.6-sol xhigh, fresh-context Claude reviewer) on PR #413: - The spec-detail test now asserts the params/hint contract end to end: bare-named params with default "string" types in spec order, hint passthrough, absent hint as empty string, and the raw wire shape (params as objects under the JSON key "type"). - Typed params (uint64 etc.) get a focused wire-shape test — ActionParam accessors are covered in temper-spec; this locks what those values serialize into. - Doc comments on ActionParamDetail's public fields, documenting the open-set type contract for generated clients. - The CI observe step now also runs the observe module unit tests, which the default-features workspace run never compiles. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Vw8tCkfnUW8p149yUNfSEE --- .github/workflows/ci.yml | 9 ++- crates/temper-server/src/observe/mod.rs | 5 ++ crates/temper-server/src/observe/mod_test.rs | 60 ++++++++++++++++++++ 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a65c0196..af6b77cdb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -266,9 +266,12 @@ jobs: run: cargo test --workspace -- --skip dst_ # spec_validate_endpoint declares required-features = ["observe"], so the - # default-features workspace run above skips it; run it here explicitly. - - name: cargo test observe-gated endpoint tests - run: cargo test -p temper-server --features observe --test spec_validate_endpoint + # default-features workspace run above skips it; the observe module's + # unit tests are also only compiled with the feature. Run both here. + - name: cargo test observe-gated tests + run: | + cargo test -p temper-server --features observe --test spec_validate_endpoint + cargo test -p temper-server --features observe --lib observe:: # ───────────────────────────────────────────────────── # Gate 3b: DST/platform coverage (matrixed) diff --git a/crates/temper-server/src/observe/mod.rs b/crates/temper-server/src/observe/mod.rs index 3465d7942..8a0c58f78 100644 --- a/crates/temper-server/src/observe/mod.rs +++ b/crates/temper-server/src/observe/mod.rs @@ -85,7 +85,12 @@ pub struct ActionDetail { /// A single action parameter: name plus its declared type ("string" default). #[derive(Serialize, Deserialize)] pub struct ActionParamDetail { + /// Parameter name exactly as declared in the spec's `params` list. pub name: String, + /// Declared parameter type, serialized under the JSON key `"type"`. + /// Bare-named params carry `"string"`; typed params carry the spec's + /// type name (e.g. `"uint64"`). This is an OPEN set — generated clients + /// must not assume a closed enum. #[serde(rename = "type")] pub param_type: String, } diff --git a/crates/temper-server/src/observe/mod_test.rs b/crates/temper-server/src/observe/mod_test.rs index b5bf6abd0..9c7a44460 100644 --- a/crates/temper-server/src/observe/mod_test.rs +++ b/crates/temper-server/src/observe/mod_test.rs @@ -874,6 +874,66 @@ async fn test_get_spec_detail_found() { assert_eq!(detail.entity_type, "Order"); assert!(!detail.states.is_empty()); assert!(!detail.actions.is_empty()); + + // The params/hint contract (consumed by generated typed clients): + // AddItem declares two bare-named params (default "string" type, spec + // order preserved) and a hint; RemoveItem declares no hint (empty, not + // null). + let add_item = detail + .actions + .iter() + .find(|a| a.name == "AddItem") + .expect("Order fixture declares AddItem"); + let param_pairs: Vec<(&str, &str)> = add_item + .params + .iter() + .map(|p| (p.name.as_str(), p.param_type.as_str())) + .collect(); + assert_eq!( + param_pairs, + vec![("ProductId", "string"), ("Quantity", "string")] + ); + assert!(add_item.hint.starts_with("Add a product")); + let remove_item = detail + .actions + .iter() + .find(|a| a.name == "RemoveItem") + .expect("Order fixture declares RemoveItem"); + assert_eq!(remove_item.params.len(), 1); + assert_eq!(remove_item.params[0].name, "ItemId"); + assert_eq!(remove_item.hint, ""); + + // Wire-level contract: the type field serializes under the key "type", + // and params are objects, not bare strings. + let raw: serde_json::Value = serde_json::from_slice(&body).unwrap(); + let raw_add_item = raw["actions"] + .as_array() + .unwrap() + .iter() + .find(|a| a["name"] == "AddItem") + .unwrap(); + assert_eq!(raw_add_item["params"][0]["name"], "ProductId"); + assert_eq!(raw_add_item["params"][0]["type"], "string"); + assert_eq!(raw_add_item["hint"], add_item.hint); +} + +#[test] +fn action_param_detail_serializes_typed_params_under_type_key() { + // Typed params (e.g. `{ name = "sleep_seconds", type = "uint64" }` in a + // spec) reach ActionParamDetail through ActionParam::name()/param_type(); + // this locks the wire shape those values serialize into. + let detail = ActionParamDetail { + name: "sleep_seconds".to_string(), + param_type: "uint64".to_string(), + }; + let json = serde_json::to_value(&detail).unwrap(); + assert_eq!( + json, + serde_json::json!({"name": "sleep_seconds", "type": "uint64"}) + ); + let back: ActionParamDetail = serde_json::from_value(json).unwrap(); + assert_eq!(back.name, "sleep_seconds"); + assert_eq!(back.param_type, "uint64"); } #[tokio::test] From 30c65a87d6e4253734c0530f38a2f029843a8acb Mon Sep 17 00:00:00 2001 From: Rita Agafonova <36133358+rita-aga@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:35:27 -0400 Subject: [PATCH 4/5] test(server): raise wasm_dispatch status waits to the file's standard 45s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two tests waited only 5s for async wasm dispatch to reach Done while every sibling in the file waits 20-45s; on loaded CI runners 5s flakes (observed on this PR's Tests job; unrelated diff). Same deadline-poll helper, longer budget — completed dispatch returns immediately either way. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Vw8tCkfnUW8p149yUNfSEE --- crates/temper-server/tests/wasm_dispatch.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/temper-server/tests/wasm_dispatch.rs b/crates/temper-server/tests/wasm_dispatch.rs index 50c4b29ff..2619d3b1b 100644 --- a/crates/temper-server/tests/wasm_dispatch.rs +++ b/crates/temper-server/tests/wasm_dispatch.rs @@ -354,7 +354,7 @@ async fn persisted_wasm_modules_are_lazy_compiled_on_first_invoke() { "EchoTest", "echo-lazy-1", &["Done", "Failed"], - Duration::from_secs(5), + Duration::from_secs(45), ) .await; assert_eq!(final_status, "Done"); @@ -423,7 +423,7 @@ async fn persisted_wasm_modules_with_legacy_db_blob_fallback_execute_after_start "EchoTest", "echo-legacy-hash", &["Done", "Failed"], - Duration::from_secs(5), + Duration::from_secs(45), ) .await; assert_eq!(final_status, "Done"); From 7a9027a721222f98cc02d20021619e18018812f7 Mon Sep 17 00:00:00 2001 From: Rita Agafonova <36133358+rita-aga@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:37:41 -0400 Subject: [PATCH 5/5] =?UTF-8?q?docs(ci):=20correct=20the=20observe-step=20?= =?UTF-8?q?comment=20=E2=80=94=20workspace=20unification=20already=20enabl?= =?UTF-8?q?es=20the=20feature;=20the=20explicit=20step=20guarantees=20rath?= =?UTF-8?q?er=20than=20rescues=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer finding (fresh-context Claude): resolver-v2 feature unification via temper-cli/temper-mcp means the workspace run compiles observe already; the explicit step's value is making that coverage non-incidental. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Vw8tCkfnUW8p149yUNfSEE --- .github/workflows/ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af6b77cdb..a82ad9f7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -265,9 +265,11 @@ jobs: - name: cargo test --workspace -- --skip dst_ run: cargo test --workspace -- --skip dst_ - # spec_validate_endpoint declares required-features = ["observe"], so the - # default-features workspace run above skips it; the observe module's - # unit tests are also only compiled with the feature. Run both here. + # spec_validate_endpoint declares required-features = ["observe"]. The + # workspace run above usually still covers it via feature unification + # (temper-cli/temper-mcp enable temper-server's observe feature), but + # that coverage is incidental — it evaporates if those edges change. + # Run the observe-gated tests explicitly so coverage is guaranteed. - name: cargo test observe-gated tests run: | cargo test -p temper-server --features observe --test spec_validate_endpoint