"""U-005 tests: c_org_node_trigger's subprocess wrapper, mocked at the subprocess boundary (CLAUDE.md Testing constraint). Real end-to-end coverage is in src/test_agent_e2e.py (RUN_REAL_AGENT_E2E=1). Run: python3 -m unittest src.test_org_node_trigger -v """ import json import subprocess import unittest from unittest.mock import patch from src import org_node_trigger def _completed_process(returncode=0, stdout_obj=None, stderr=""): stdout = json.dumps(stdout_obj) if stdout_obj is not None else "" return subprocess.CompletedProcess(args=["claude"], returncode=returncode, stdout=stdout, stderr=stderr) class TriggerOrgNodeTest(unittest.TestCase): def test_rejects_unknown_org_node_without_shelling_out(self): with patch("src.org_node_trigger.subprocess.run") as run: with self.assertRaises(ValueError): org_node_trigger.trigger_org_node("not-a-real-node", "hi", "/tmp/some-project") run.assert_not_called() def test_happy_path_returns_result_text(self): with patch("src.org_node_trigger.subprocess.run") as run: run.return_value = _completed_process(stdout_obj={"is_error": False, "result": "hello from planner"}) answer = org_node_trigger.trigger_org_node("planner", "say hi", "/tmp/some-project") self.assertEqual(answer, "hello from planner") def test_invokes_claude_with_expected_args_and_cwd(self): with patch("src.org_node_trigger.subprocess.run") as run: run.return_value = _completed_process(stdout_obj={"is_error": False, "result": "ok"}) org_node_trigger.trigger_org_node("coder", "do the thing", "/tmp/some-project") args, kwargs = run.call_args argv = args[0] self.assertEqual(argv[:3], ["claude", "--agent", "coder"]) self.assertIn("-p", argv) self.assertIn("do the thing", argv) self.assertIn("--permission-mode", argv) self.assertIn("auto", argv) self.assertNotIn("--bare", argv) self.assertNotIn("--dangerously-skip-permissions", argv) self.assertNotIn("bypassPermissions", argv) self.assertEqual(kwargs["cwd"], "/tmp/some-project") self.assertEqual(kwargs["stdin"], subprocess.DEVNULL) def test_nonzero_exit_raises(self): with patch("src.org_node_trigger.subprocess.run") as run: run.return_value = _completed_process(returncode=1, stdout_obj=None, stderr="boom") with self.assertRaises(org_node_trigger.OrgNodeTriggerError): org_node_trigger.trigger_org_node("planner", "hi", "/tmp/some-project") def test_is_error_true_raises(self): with patch("src.org_node_trigger.subprocess.run") as run: run.return_value = _completed_process(stdout_obj={"is_error": True, "result": ""}) with self.assertRaises(org_node_trigger.OrgNodeTriggerError): org_node_trigger.trigger_org_node("validator", "hi", "/tmp/some-project") _SCHEMA = {"type": "object", "required": ["ok", "issues"], "properties": { "ok": {"type": "boolean"}, "issues": {"type": "array"}, }} def _fenced(payload): return "Here you go:\n\n```json\n" + json.dumps(payload) + "\n```\n\nDone." class TriggerOrgNodeStructuredTest(unittest.TestCase): def test_rejects_unknown_org_node_without_shelling_out(self): with patch("src.org_node_trigger.subprocess.run") as run: with self.assertRaises(ValueError): org_node_trigger.trigger_org_node_structured( "not-a-real-node", "hi", "/tmp/some-project", _SCHEMA ) run.assert_not_called() def test_extracts_bare_json_result(self): payload = {"ok": True, "issues": []} with patch("src.org_node_trigger.subprocess.run") as run: run.return_value = _completed_process(stdout_obj={"is_error": False, "result": json.dumps(payload)}) extracted = org_node_trigger.trigger_org_node_structured( "validator", "check this", "/tmp/some-project", _SCHEMA ) self.assertEqual(extracted, payload) def test_extracts_a_single_fenced_json_block_with_prose_around_it(self): payload = {"ok": False, "issues": ["bug 1"]} with patch("src.org_node_trigger.subprocess.run") as run: run.return_value = _completed_process(stdout_obj={"is_error": False, "result": _fenced(payload)}) extracted = org_node_trigger.trigger_org_node_structured( "validator", "check this", "/tmp/some-project", _SCHEMA ) self.assertEqual(extracted, payload) def test_multiple_fenced_blocks_returns_first_matching_one(self): unrelated = "```json\n" + json.dumps({"unrelated": True}) + "\n```\n\n" payload = {"ok": True, "issues": []} text = unrelated + _fenced(payload) with patch("src.org_node_trigger.subprocess.run") as run: run.return_value = _completed_process(stdout_obj={"is_error": False, "result": text}) extracted = org_node_trigger.trigger_org_node_structured( "validator", "check this", "/tmp/some-project", _SCHEMA ) self.assertEqual(extracted, payload) def test_no_matching_json_anywhere_raises_extraction_error(self): with patch("src.org_node_trigger.subprocess.run") as run: run.return_value = _completed_process( stdout_obj={"is_error": False, "result": "sorry, I can't produce that"} ) with self.assertRaises(org_node_trigger.StructuredOutputExtractionError): org_node_trigger.trigger_org_node_structured( "validator", "check this", "/tmp/some-project", _SCHEMA ) def test_appends_json_fence_instruction_and_schema_to_prompt(self): with patch("src.org_node_trigger.subprocess.run") as run: run.return_value = _completed_process( stdout_obj={"is_error": False, "result": json.dumps({"ok": True, "issues": []})} ) org_node_trigger.trigger_org_node_structured("validator", "check this", "/tmp/some-project", _SCHEMA) argv = run.call_args[0][0] prompt_arg = argv[argv.index("-p") + 1] self.assertIn("check this", prompt_arg) self.assertIn("```json fenced code block", prompt_arg) self.assertIn(json.dumps(_SCHEMA), prompt_arg) def test_model_override_passed_through_as_cli_flag(self): with patch("src.org_node_trigger.subprocess.run") as run: run.return_value = _completed_process( stdout_obj={"is_error": False, "result": json.dumps({"ok": True, "issues": []})} ) org_node_trigger.trigger_org_node_structured( "validator", "check this", "/tmp/some-project", _SCHEMA, model="claude-fable-5", ) argv = run.call_args[0][0] self.assertIn("--model", argv) self.assertEqual(argv[argv.index("--model") + 1], "claude-fable-5") def test_no_model_means_no_model_flag(self): with patch("src.org_node_trigger.subprocess.run") as run: run.return_value = _completed_process( stdout_obj={"is_error": False, "result": json.dumps({"ok": True, "issues": []})} ) org_node_trigger.trigger_org_node_structured("validator", "check this", "/tmp/some-project", _SCHEMA) argv = run.call_args[0][0] self.assertNotIn("--model", argv) def test_nonzero_exit_with_usage_limit_phrase_raises_usage_limit_error_with_type(self): with patch("src.org_node_trigger.subprocess.run") as run: run.return_value = _completed_process( returncode=1, stdout_obj=None, stderr="Claude AI usage limit reached, you've hit your session limit" ) with self.assertRaises(org_node_trigger.UsageLimitReachedError) as ctx: org_node_trigger.trigger_org_node_structured( "validator", "check this", "/tmp/some-project", _SCHEMA ) self.assertEqual(ctx.exception.quota_limit_type, "session") def test_nonzero_exit_without_usage_limit_phrase_raises_generic_error(self): with patch("src.org_node_trigger.subprocess.run") as run: run.return_value = _completed_process(returncode=1, stdout_obj=None, stderr="boom") with self.assertRaises(org_node_trigger.OrgNodeTriggerError): org_node_trigger.trigger_org_node_structured( "validator", "check this", "/tmp/some-project", _SCHEMA ) def test_is_error_with_weekly_limit_phrase_raises_usage_limit_error(self): with patch("src.org_node_trigger.subprocess.run") as run: run.return_value = _completed_process( stdout_obj={"is_error": True, "result": "You've hit your weekly limit"} ) with self.assertRaises(org_node_trigger.UsageLimitReachedError) as ctx: org_node_trigger.trigger_org_node_structured( "validator", "check this", "/tmp/some-project", _SCHEMA ) self.assertEqual(ctx.exception.quota_limit_type, "weekly") def test_is_error_with_opus_quota_phrase_raises_usage_limit_error(self): with patch("src.org_node_trigger.subprocess.run") as run: run.return_value = _completed_process( stdout_obj={"is_error": True, "result": "You have reached your opus quota limit for this period"} ) with self.assertRaises(org_node_trigger.UsageLimitReachedError) as ctx: org_node_trigger.trigger_org_node_structured( "validator", "check this", "/tmp/some-project", _SCHEMA ) self.assertEqual(ctx.exception.quota_limit_type, "opus") def test_is_error_without_usage_limit_phrase_raises_generic_error(self): with patch("src.org_node_trigger.subprocess.run") as run: run.return_value = _completed_process(stdout_obj={"is_error": True, "result": "something else failed"}) with self.assertRaises(org_node_trigger.OrgNodeTriggerError): org_node_trigger.trigger_org_node_structured( "validator", "check this", "/tmp/some-project", _SCHEMA ) def test_invokes_claude_with_expected_cwd_and_stdin(self): with patch("src.org_node_trigger.subprocess.run") as run: run.return_value = _completed_process( stdout_obj={"is_error": False, "result": json.dumps({"ok": True, "issues": []})} ) org_node_trigger.trigger_org_node_structured("coder", "do the thing", "/tmp/some-project", _SCHEMA) args, kwargs = run.call_args argv = args[0] self.assertEqual(argv[:3], ["claude", "--agent", "coder"]) self.assertIn("--permission-mode", argv) self.assertIn("auto", argv) self.assertNotIn("--bare", argv) self.assertNotIn("--dangerously-skip-permissions", argv) self.assertNotIn("bypassPermissions", argv) self.assertEqual(kwargs["cwd"], "/tmp/some-project") self.assertEqual(kwargs["stdin"], subprocess.DEVNULL) if __name__ == "__main__": unittest.main()