orchestrator-llm/tests/plan_compatibility.rs
flemming-it 3bb8fc9e70 feat: deterministic stub orchestrator (v0.1.0)
The orchestrator-llm module turns a goal text into a Plan JSON
that the F∆I hub can apply. v0.1.0 is a deterministic stub —
it always emits a three-step plan (explain, explain-stub-notice,
save_flow with debug.echo) regardless of the goal.

Why ship a stub: it validates the module → plan → apply pipeline
end-to-end before adding LLM integration. The output is a
structurally valid Plan that round-trips cleanly through
fai_hub::plan::Plan deserialization.

v0.2.0 will replace the stub body with an actual LLM call once
the F∆I platform exposes outbound HTTP to permitted modules via
wasi-http.

Files:
- module.yaml — capability orchestrator.plan@0.1.0, no permissions
- wit/world.wit — copy of fai:platform@0.1.0
- src/lib.rs — wit_bindgen Guest impl that delegates to plan.rs
- src/plan.rs — pure plan-builder (host + WASM compatible)
- tests/plan_compatibility.rs — plan JSON shape assertions
- rust-toolchain.toml — pin to Rust 1.86 with wasm32-wasip2 target
- README.md — usage and status

Signed-off-by: flemming-it <sf@flemming.it>
2026-04-29 17:43:46 +02:00

47 lines
1.7 KiB
Rust

//! Verify that plan JSON emitted by orchestrator-llm parses
//! cleanly as a `fai_hub::plan::Plan`. This protects against
//! schema drift between this module and the hub.
//!
//! Note: this test runs only on the host target. The WASM build
//! does not include test harnesses.
#![cfg(not(target_arch = "wasm32"))]
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use orchestrator_llm::build_plan;
use serde_json::Value;
#[test]
fn emitted_plan_has_required_top_level_fields() {
let json = build_plan("test goal");
let parsed: Value = serde_json::from_str(&json).expect("plan must be valid json");
assert!(parsed["schema_version"].is_u64());
assert!(parsed["goal"].is_string());
assert!(parsed["steps"].is_array());
}
#[test]
fn each_step_has_kind_tag() {
let json = build_plan("test goal");
let parsed: Value = serde_json::from_str(&json).unwrap();
for step in parsed["steps"].as_array().unwrap() {
assert!(step["kind"].is_string(), "step missing kind tag: {step}");
}
}
#[test]
fn save_flow_step_yaml_parses_as_flow_definition() {
// The save_flow step embeds a YAML flow body. Verify it
// round-trips through serde_yaml.
let json = build_plan("anything");
let parsed: Value = serde_json::from_str(&json).unwrap();
let save_flow_step = parsed["steps"]
.as_array()
.unwrap()
.iter()
.find(|s| s["kind"] == "save_flow")
.expect("expected a save_flow step");
let yaml_text = save_flow_step["yaml"].as_str().unwrap();
let flow: Value = serde_yaml::from_str(yaml_text).expect("embedded yaml must parse");
assert_eq!(flow["name"], "orchestrator-stub-greeting");
assert!(flow["steps"].is_array());
}