"""trigger_org_node_structured's real-call coverage (2026-08, work-graph-node-execution-deblackbox-requirements.md FR-4 -- the migration from Dynamic Workflow scripts to flat `claude --agent` calls for the three call sites in work_graph_orchestrate.py/e2e_pipeline.py that used to render one-off `.js` Workflow scripts purely to get "agent identity + schema-shaped output"). Gated behind RUN_REAL_AGENT_E2E=1 (CLAUDE.md Testing constraint) -- costs real API usage, skipped by default, same as src/test_agent_e2e.py. Run: RUN_REAL_AGENT_E2E=1 python3 -m unittest src.test_org_node_trigger_e2e -v **Real result, recorded 2026-08-13** (one real call, `--agent validator --model claude-haiku-4-5`, target org_node's persona = agents/validator/SOUL.md via .claude/agents/validator.md): all three FR-4 questions confirmed together in a single real subprocess call (spied via `wraps=subprocess.run` so the production `trigger_org_node_structured` code path runs completely for real, while this test still gets to inspect the raw CompletedProcess underneath it): 1. **Persona loaded correctly** -- the self-identify answer named itself an independent QA/verification agent (PASS/FAIL/INCONCLUSIVE judgment framework, "does not write or fix the Coder's business code"), matching agents/validator/SOUL.md's own red lines, not the generic Claude Code assistant persona. Same technique U-031 used to validate the (now-retired) Dynamic Workflow `agentType` path -- ask the agent to self-identify, check for persona-specific vocabulary, not a canned/generic answer. 2. **`--model` override took effect for real** -- `claude --agent ` combined with `--model claude-haiku-4-5` is a genuinely different CLI combination from anything DSL-internal Workflow options ever validated (05 §5.4.1.5's "two different paths" lesson) -- confirmed empirically, not assumed: the raw JSON response's own `modelUsage` key names "claude-haiku-4-5" as the model that actually served the request, not validator's role-declared default (`claude-sonnet-5`, roles/validator/ role.json). 3. **```json fence extraction is stable on real model output** -- the real free-text response (identity narrative + a trailing ```json fenced block, since `--json-schema` is never combined with `--agent`, see 05 §5.4.1.1) parsed cleanly via `extract_json_payload`/`trigger_org_node_ structured`'s required-keys matching, returning the schema-shaped dict with no manual fallback needed -- the same extraction algorithm `work_graph_plan.py::plan_work_graph()` already validated for real in production (`_extract_graph_manifest`), now proven for `_JSON_FENCE_ INSTRUCTION`'s more generic prompt wording too. """ import json import subprocess import tempfile import unittest from unittest.mock import patch from src import agent_profile, org_node_trigger, project_scaffold, schema from src.test_support import skip_unless_env _SELF_IDENTIFY_SCHEMA = { "type": "object", "required": ["job", "never_does"], "properties": { "job": {"type": "string", "description": "one short sentence describing your role"}, "never_does": {"type": "string", "description": "one short sentence: something you never do"}, }, } @skip_unless_env("RUN_REAL_AGENT_E2E") class RealTriggerOrgNodeStructuredTest(unittest.TestCase): def test_identity_model_override_and_json_extraction_all_real(self): with tempfile.TemporaryDirectory() as tmp: subprocess.run(["git", "init"], cwd=tmp, check=True, capture_output=True) conn = schema.connect(":memory:") schema.create_schema(conn) project_scaffold.create_project(conn, tmp, init_git_if_missing=False) agent_profile.deploy_agent_definitions(tmp) real_run = subprocess.run captured = {} def spying_run(*args, **kwargs): result = real_run(*args, **kwargs) captured["result"] = result return result # wraps the real subprocess.run rather than replacing it -- this # call goes out to the real `claude` CLI exactly like production # code would, this test just also gets to inspect the raw # CompletedProcess trigger_org_node_structured's own return value # (the extracted payload alone) doesn't expose. with patch("src.org_node_trigger.subprocess.run", side_effect=spying_run): extracted = org_node_trigger.trigger_org_node_structured( "validator", "In one short sentence each: what is your job, and what do you never do? " "Answer only, take no other action.", tmp, _SELF_IDENTIFY_SCHEMA, model="claude-haiku-4-5", timeout=120, ) # (3) JSON fence extraction worked on real model output -- a dict # with both required keys, not None/a StructuredOutputExtractionError. self.assertIn("job", extracted) self.assertIn("never_does", extracted) # (1) persona genuinely loaded -- validator's own red lines # (agents/validator/SOUL.md), not a generic assistant answer. identity_text = extracted["job"] + " " + extracted["never_does"] self.assertTrue( any(kw in identity_text for kw in ( "QA", "验收", "PASS", "FAIL", "INCONCLUSIVE", "independent", "verif", "Verif", )), "validator's answer doesn't look like the migrated QA identity: " + identity_text, ) # (2) --model override genuinely took effect -- the real response's # own modelUsage key names the override model, not validator's # role-declared default (claude-sonnet-5, roles/validator/role.json). raw_payload = json.loads(captured["result"].stdout) model_usage = raw_payload.get("modelUsage") or {} self.assertIn( "claude-haiku-4-5", model_usage, "--model override not reflected in the real response's modelUsage: " + json.dumps(raw_payload)[:2000], ) if __name__ == "__main__": unittest.main()