test(graph): cover agent node inputs end to end and at the runtime guards

Two offline chain tests materialize an echo-inputs graph agent whose
script reports the width it sees: a parent with inputs.width gets the
parent's value back, a parent without inputs gets the child's YAML
default. Two agents.rs tests pin the exact runtime guard texts for a
config-only target and a reserved initial_prompt key.

Task T5 of plans/map-subgraph-branches-design.md §3.12.
This commit is contained in:
Dark-Alex-17 committed 2026-09-11 10:56:27 -06:00
1 parent 10b8fe5cae
commit e26543f21a
2 files changed
+187

No files matched your search

+105
View File
@@ -1484,6 +1484,46 @@ nodes:
.unwrap();
}
const ECHO_INPUTS_AGENT: &str = "echo-inputs";
/// Materializes a graph agent whose only script echoes the `width` it
/// finds in `GRAPH_STATE` back as `output`. Its YAML default is
/// `width: 1`, so a parent that seeds `width` through `inputs:` is
/// observable against a parent that does not.
fn materialize_echo_inputs_agent() {
let graph_path = paths::agent_graph_file(ECHO_INPUTS_AGENT);
let agent_dir = graph_path.parent().unwrap();
fs::create_dir_all(agent_dir).unwrap();
fs::write(
agent_dir.join("echo.py"),
"#!/usr/bin/env python3\nimport os, json\n\
state = json.loads(os.environ.get(\"GRAPH_STATE\", \"{}\"))\n\
print(json.dumps({\"output\": str(state[\"width\"])}))\n",
)
.unwrap();
fs::write(
&graph_path,
format!(
r#"
name: {ECHO_INPUTS_AGENT}
start: echo
initial_state:
width: 1
nodes:
echo:
type: script
script: echo.py
timeout: 30
next: done
done:
type: end
output: "{{{{output}}}}"
"#
),
)
.unwrap();
}
const DOUBLER_GRAPH: &str = r#"
name: t
start: doubler
@@ -1784,6 +1824,71 @@ nodes:
assert!(peers.0.is_finished(&peers.1.0));
}
fn echo_inputs_parent_graph(inputs_block: &str) -> String {
format!(
r#"
name: t
start: worker
nodes:
worker:
type: agent
agent: {ECHO_INPUTS_AGENT}
prompt: "p"
{inputs_block} state_updates:
output: "{{{{output}}}}"
"#
)
}
async fn run_echo_inputs_chain(parent_yaml: &str) -> StateManager {
let h = Harness::new(parent_yaml);
let mut state = StateManager::new(HashMap::from([("n".to_string(), json!(3))]));
let mut ctx = silent_ctx();
h.run("worker", None, &mut state, &mut ctx)
.await
.unwrap_or_else(|e| panic!("chain failed: {e:#}"));
state
}
/// The child's `width` default is overlaid by the parent's `inputs`
/// before the child graph starts, so its script sees the parent's value.
#[tokio::test]
#[serial]
async fn agent_node_inputs_seed_the_child_graph_state() {
if !cmd_available("python3") {
eprintln!("skipping: python3 not available");
return;
}
let _guard = TestConfigDirGuard::new();
materialize_echo_inputs_agent();
let parent = echo_inputs_parent_graph(" inputs:\n width: \"{{n}}\"\n");
let state = run_echo_inputs_chain(&parent).await;
assert_eq!(state.state().get("output"), Some(&json!("3")));
}
/// Control for the test above: the same child with no `inputs` on the
/// parent node keeps its YAML default, so an override in the sibling
/// test cannot be the child ignoring its own `initial_state`.
#[tokio::test]
#[serial]
async fn agent_node_without_inputs_leaves_the_child_graph_defaults() {
if !cmd_available("python3") {
eprintln!("skipping: python3 not available");
return;
}
let _guard = TestConfigDirGuard::new();
materialize_echo_inputs_agent();
let parent = echo_inputs_parent_graph("");
let state = run_echo_inputs_chain(&parent).await;
assert_eq!(state.state().get("output"), Some(&json!("1")));
}
#[tokio::test]
async fn run_item_chain_reaches_flagged_agent_after_script_step() {
if !cmd_available("python3") {