From 8a8e496acf6dec4f4ebc8aeca2c2d997e1bf1926 Mon Sep 17 00:00:00 2001 From: Jacob Molz Date: Tue, 26 May 2026 10:37:21 -0400 Subject: [PATCH] fix: avoid swallowing prompt manager interrupts --- tests/unit/test_prompt_manager.py | 40 ++++++++++++++++++- .../trustgraph/template/prompt_manager.py | 13 +++--- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/tests/unit/test_prompt_manager.py b/tests/unit/test_prompt_manager.py index 3e73ab9c..22b735ac 100644 --- a/tests/unit/test_prompt_manager.py +++ b/tests/unit/test_prompt_manager.py @@ -7,7 +7,7 @@ including template rendering, term merging, JSON validation, and error handling. import pytest import json -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock from trustgraph.template.prompt_manager import PromptManager, PromptConfiguration, Prompt @@ -344,6 +344,42 @@ class TestPromptManager: assert pm.terms == {} # Default empty terms assert len(pm.prompts) == 0 + def test_load_config_does_not_swallow_keyboard_interrupt(self, monkeypatch): + """KeyboardInterrupt should propagate out of config parsing.""" + pm = PromptManager() + + def interrupt(_value): + raise KeyboardInterrupt + + monkeypatch.setattr("trustgraph.template.prompt_manager.json.loads", interrupt) + + with pytest.raises(KeyboardInterrupt): + pm.load_config({"system": json.dumps("Test")}) + + @pytest.mark.asyncio + async def test_json_parse_does_not_swallow_system_exit(self): + """SystemExit should propagate out of JSON response parsing.""" + pm = PromptManager() + config = { + "system": json.dumps("Test"), + "template-index": json.dumps(["json_response"]), + "template.json_response": json.dumps({ + "prompt": "Generate JSON", + "response-type": "json" + }) + } + pm.load_config(config) + + def exit_parse(_text): + raise SystemExit(2) + + pm.parse_json = exit_parse + mock_llm = AsyncMock() + mock_llm.return_value = "{}" + + with pytest.raises(SystemExit): + await pm.invoke("json_response", {}, mock_llm) + @pytest.mark.unit class TestPromptManagerJsonl: @@ -585,4 +621,4 @@ not json at all assert len(result) == 2 assert result[0] == {"any": "structure"} - assert result[1] == {"completely": "different"} \ No newline at end of file + assert result[1] == {"completely": "different"} diff --git a/trustgraph-flow/trustgraph/template/prompt_manager.py b/trustgraph-flow/trustgraph/template/prompt_manager.py index 546a7faf..976d3695 100644 --- a/trustgraph-flow/trustgraph/template/prompt_manager.py +++ b/trustgraph-flow/trustgraph/template/prompt_manager.py @@ -31,12 +31,12 @@ class PromptManager: try: system = json.loads(config["system"]) - except: + except (KeyError, TypeError, json.JSONDecodeError): system = "Be helpful." try: ix = json.loads(config["template-index"]) - except: + except (KeyError, TypeError, json.JSONDecodeError): ix = [] prompts = {} @@ -68,8 +68,8 @@ class PromptManager: try: self.system_template = ibis.Template(self.config.system_template) - except: - raise RuntimeError("Error in system template") + except Exception as e: + raise RuntimeError(f"Error in system template: {e}") self.templates = {} for k, v in self.prompts.items(): @@ -136,8 +136,6 @@ class PromptManager: terms = self.terms | self.prompts[id].terms | input - resp_type = self.prompts[id].response_type - return self.templates[id].render(terms) async def invoke(self, id, input, llm): @@ -161,7 +159,7 @@ class PromptManager: if resp_type == "json": try: obj = self.parse_json(resp) - except: + except (json.JSONDecodeError, TypeError): logger.error(f"JSON parse failed: {resp}") raise RuntimeError("JSON parse fail") @@ -195,4 +193,3 @@ class PromptManager: return objects raise RuntimeError(f"Response type {resp_type} not known") -