From 9b2ab6e4ed3094f87fa0c441f9044f755d1267f6 Mon Sep 17 00:00:00 2001 From: Trecek Date: Sun, 28 Jun 2026 10:06:26 -0700 Subject: [PATCH 1/3] feat: add _validate_result fail-closed envelope gate to open_kitchen and load_recipe Implements T5-P5-A4-WP1: adds _validate_result helper to server/tools/_types.py that enforces output shape invariants (required keys, non-None, success==True, non-empty content) before serialization. Wires validator into open_kitchen (named- recipe branch only) and load_recipe (unified exit, gated on result.valid). Returns serialized ToolFailureEnvelope on violation. Co-Authored-By: Claude Sonnet 4.6 --- src/autoskillit/server/tools/_types.py | 55 +++++++++++ src/autoskillit/server/tools/tools_kitchen.py | 15 +++ src/autoskillit/server/tools/tools_recipe.py | 14 +++ tests/server/AGENTS.md | 1 + tests/server/test_tools_kitchen_gate.py | 69 +++++++++++++- tests/server/test_tools_load_recipe.py | 38 ++++++++ tests/server/test_tools_types_module.py | 1 + tests/server/test_tools_types_validate.py | 95 +++++++++++++++++++ 8 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 tests/server/test_tools_types_validate.py diff --git a/src/autoskillit/server/tools/_types.py b/src/autoskillit/server/tools/_types.py index b686e421ab..c02d7eeb12 100644 --- a/src/autoskillit/server/tools/_types.py +++ b/src/autoskillit/server/tools/_types.py @@ -7,6 +7,7 @@ from __future__ import annotations +import json from typing import Any, Literal, TypedDict from autoskillit.core import ModelTotalEntry, RetryReason @@ -23,6 +24,7 @@ "ToolFailureEnvelope", "server_failure_envelope", "input_failure_envelope", + "_validate_result", ] @@ -244,3 +246,56 @@ def input_failure_envelope( stage=stage, retriable=False, ) + + +def _validate_result( + result: dict[str, Any], + *, + required_keys: frozenset[str], + tool_name: str, + retriable: bool = False, +) -> str | None: + """Validate a tool result dict and return a fail-closed envelope on violation. + + Returns ``None`` when all invariants hold, or a ``json.dumps``-serialized + ``ToolFailureEnvelope`` on the first violation detected. + """ + _stage = f"validate_result:{tool_name}" + for key in sorted(required_keys): + if key not in result: + return json.dumps( + ToolFailureEnvelope( + success=False, + error=f"Missing required key: {key}", + stage=_stage, + retriable=retriable, + ) + ) + if result[key] is None: + return json.dumps( + ToolFailureEnvelope( + success=False, + error=f"Required key is None: {key}", + stage=_stage, + retriable=retriable, + ) + ) + if "success" in result and result["success"] is not True: + return json.dumps( + ToolFailureEnvelope( + success=False, + error=f"Result reports failure: success={result['success']!r}", + stage=_stage, + retriable=retriable, + ) + ) + if "content" in result and not result["content"]: + return json.dumps( + ToolFailureEnvelope( + success=False, + error="Result content is empty", + stage=_stage, + retriable=retriable, + ) + ) + return None diff --git a/src/autoskillit/server/tools/tools_kitchen.py b/src/autoskillit/server/tools/tools_kitchen.py index 6e4aeaf645..129d410cd6 100644 --- a/src/autoskillit/server/tools/tools_kitchen.py +++ b/src/autoskillit/server/tools/tools_kitchen.py @@ -68,6 +68,7 @@ _check_dispatch_feasibility, filter_steps_by_post_prune, ) +from autoskillit.server.tools._types import _validate_result logger = get_logger(__name__) @@ -888,6 +889,20 @@ async def open_kitchen( if warning: result["hook_warning"] = warning.strip() + _required_keys = frozenset({"success", "content", "valid"}) + if ingredients_only: + _required_keys = _required_keys - {"content"} + _validation_err = _validate_result( + result, required_keys=_required_keys, tool_name="open_kitchen" + ) + if _validation_err is not None: + logger.warning( + "open_kitchen_fail_closed", + tool="open_kitchen", + stage="validate_result", + ) + return _validation_err + return json.dumps(result) text = ( diff --git a/src/autoskillit/server/tools/tools_recipe.py b/src/autoskillit/server/tools/tools_recipe.py index 357f7ae9c6..026f39cebc 100644 --- a/src/autoskillit/server/tools/tools_recipe.py +++ b/src/autoskillit/server/tools/tools_recipe.py @@ -29,6 +29,7 @@ _promote_capability_keys, ) from autoskillit.server.tools._cancellation_shield import _cancellation_shield +from autoskillit.server.tools._types import _validate_result logger = get_logger(__name__) @@ -244,6 +245,19 @@ async def load_recipe( ) if ingredients_only: result = strip_ingredients_only_keys(result) + _required_keys: frozenset[str] = frozenset() + if not ingredients_only and result.get("valid", False): + _required_keys = frozenset({"content"}) + _validation_err = _validate_result( + result, required_keys=_required_keys, tool_name="load_recipe" + ) + if _validation_err is not None: + logger.warning( + "load_recipe_fail_closed", + tool="load_recipe", + stage="validate_result", + ) + return _validation_err return json.dumps(result) except Exception as exc: logger.error("load_recipe unhandled exception", exc_info=True) diff --git a/tests/server/AGENTS.md b/tests/server/AGENTS.md index d61efbac3e..d6a3673b32 100644 --- a/tests/server/AGENTS.md +++ b/tests/server/AGENTS.md @@ -133,6 +133,7 @@ Server tool handler unit tests — kitchen, execution, CI, clone, workspace tool | `test_tools_status_quota_and_db.py` | Tests for server status tools: quota events, telemetry writing, and DB access | | `test_tools_status_summaries.py` | Tests for server status tools: token and timing summaries | | `test_tools_types_module.py` | Tests for server/tools/_types.py TypedDict imports and failure envelope factory functions | +| `test_tools_types_validate.py` | Tests for _validate_result helper in server/tools/_types.py — shape invariant enforcement and fail-closed envelope validation | | `test_tools_workspace.py` | Tests for autoskillit server workspace tools | | `test_tools_agents.py` | Tests for agent pack registry, MCP resources, and `unlock_agent_pack` | | `test_track_response_size.py` | Tests for the track_response_size decorator in autoskillit.server._notify | diff --git a/tests/server/test_tools_kitchen_gate.py b/tests/server/test_tools_kitchen_gate.py index 588f54fd8b..a63abf3074 100644 --- a/tests/server/test_tools_kitchen_gate.py +++ b/tests/server/test_tools_kitchen_gate.py @@ -3,7 +3,7 @@ from __future__ import annotations import json -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -341,3 +341,70 @@ async def test_close_kitchen_drains_orphaned_github_api_entries(tmp_path, monkey data = json.loads(orphan_path.read_text()) assert data["total_requests"] == 1 assert not log._entries + + +@pytest.mark.anyio +async def test_open_kitchen_fail_closed_empty_content(monkeypatch, tmp_path): + """open_kitchen returns fail-closed envelope when content is empty.""" + monkeypatch.chdir(tmp_path) + ctx = _make_mock_ctx() + ctx.gate.enabled = True + ctx.gate_infrastructure_ready = True + ctx.recipes.load_and_validate.return_value = { + "valid": True, + "content": "", + "dispatch_feasible": True, + } + ctx.recipes.find.return_value = MagicMock(path=tmp_path / "r.yaml") + ctx.config.providers = MagicMock() + ctx.backend = MagicMock() + ctx.backend.name = "test" + with ( + patch("autoskillit.server.tools.tools_kitchen._get_ctx", return_value=ctx), + patch("autoskillit.server.tools.tools_kitchen.logger"), + patch( + "autoskillit.server.tools.tools_kitchen._prime_quota_cache", + new_callable=AsyncMock, + ), + patch("autoskillit.server.tools.tools_kitchen._write_hook_config"), + ): + from autoskillit.server.tools.tools_kitchen import open_kitchen + + raw = await open_kitchen(name="test-recipe") + + parsed = json.loads(raw) + assert parsed["success"] is False + assert "content" in parsed["error"].lower() + + +@pytest.mark.anyio +async def test_open_kitchen_fail_closed_missing_content(monkeypatch, tmp_path): + """open_kitchen returns fail-closed envelope when content key is missing.""" + monkeypatch.chdir(tmp_path) + ctx = _make_mock_ctx() + ctx.gate.enabled = True + ctx.gate_infrastructure_ready = True + ctx.recipes.load_and_validate.return_value = { + "valid": True, + "dispatch_feasible": True, + } + ctx.recipes.find.return_value = MagicMock(path=tmp_path / "r.yaml") + ctx.config.providers = MagicMock() + ctx.backend = MagicMock() + ctx.backend.name = "test" + with ( + patch("autoskillit.server.tools.tools_kitchen._get_ctx", return_value=ctx), + patch("autoskillit.server.tools.tools_kitchen.logger"), + patch( + "autoskillit.server.tools.tools_kitchen._prime_quota_cache", + new_callable=AsyncMock, + ), + patch("autoskillit.server.tools.tools_kitchen._write_hook_config"), + ): + from autoskillit.server.tools.tools_kitchen import open_kitchen + + raw = await open_kitchen(name="test-recipe") + + parsed = json.loads(raw) + assert parsed["success"] is False + assert "content" in parsed["error"] diff --git a/tests/server/test_tools_load_recipe.py b/tests/server/test_tools_load_recipe.py index fedf081408..18a9c4cfdc 100644 --- a/tests/server/test_tools_load_recipe.py +++ b/tests/server/test_tools_load_recipe.py @@ -580,3 +580,41 @@ async def test_load_recipe_surfaces_validation_failure( ) assert "errors" in result assert len(result["errors"]) > 0 + + +class TestLoadRecipeFailClosed: + """Fail-closed validation for empty and missing content.""" + + @pytest.fixture(autouse=True) + def _ensure_ctx(self, tool_ctx_kitchen_open): + pass + + @pytest.mark.anyio + async def test_load_recipe_fail_closed_empty_content(self, monkeypatch, tmp_path): + monkeypatch.chdir(tmp_path) + from autoskillit.recipe._api import _LOAD_CACHE + + _LOAD_CACHE.clear() + monkeypatch.setattr( + "autoskillit.recipe._api.load_and_validate", + lambda *a, **kw: {"valid": True, "content": "", "dispatch_feasible": True}, + ) + raw = await load_recipe(name="test-recipe") + parsed = json.loads(raw) + assert parsed["success"] is False + assert "content" in parsed["error"].lower() + + @pytest.mark.anyio + async def test_load_recipe_fail_closed_missing_content(self, monkeypatch, tmp_path): + monkeypatch.chdir(tmp_path) + from autoskillit.recipe._api import _LOAD_CACHE + + _LOAD_CACHE.clear() + monkeypatch.setattr( + "autoskillit.recipe._api.load_and_validate", + lambda *a, **kw: {"valid": True, "dispatch_feasible": True}, + ) + raw = await load_recipe(name="test-recipe") + parsed = json.loads(raw) + assert parsed["success"] is False + assert "content" in parsed["error"] diff --git a/tests/server/test_tools_types_module.py b/tests/server/test_tools_types_module.py index ce1aecf331..23ef6d402d 100644 --- a/tests/server/test_tools_types_module.py +++ b/tests/server/test_tools_types_module.py @@ -126,6 +126,7 @@ def test_tool_failure_envelope_in_all(self): assert "ToolFailureEnvelope" in types_all assert "server_failure_envelope" in types_all assert "input_failure_envelope" in types_all + assert "_validate_result" in types_all def test_tool_failure_envelope_distinct_from_kitchen_envelope(self): from autoskillit.server.tools._types import ToolFailureEnvelope diff --git a/tests/server/test_tools_types_validate.py b/tests/server/test_tools_types_validate.py new file mode 100644 index 0000000000..76913a204a --- /dev/null +++ b/tests/server/test_tools_types_validate.py @@ -0,0 +1,95 @@ +"""Unit tests for _validate_result helper in server/tools/_types.""" + +import json + +import pytest + +from autoskillit.server.tools._types import _validate_result + +pytestmark = [pytest.mark.layer("server"), pytest.mark.small] + + +class TestValidateResult: + def test_pass_all_keys_present(self): + result = {"success": True, "content": "recipe body", "valid": True} + assert ( + _validate_result( + result, + required_keys=frozenset({"success", "content", "valid"}), + tool_name="test", + ) + is None + ) + + def test_missing_key_returns_failure(self): + result = {"success": True} + raw = _validate_result( + result, + required_keys=frozenset({"success", "content"}), + tool_name="test", + ) + assert raw is not None + parsed = json.loads(raw) + assert parsed["success"] is False + assert "content" in parsed["error"] + assert parsed["stage"] == "validate_result:test" + + def test_none_value_returns_failure(self): + result = {"success": True, "content": None} + raw = _validate_result( + result, + required_keys=frozenset({"success", "content"}), + tool_name="test", + ) + assert raw is not None + parsed = json.loads(raw) + assert parsed["success"] is False + assert "content" in parsed["error"] + assert "None" in parsed["error"] + + def test_empty_content_returns_failure(self): + result = {"success": True, "content": ""} + raw = _validate_result( + result, + required_keys=frozenset({"success"}), + tool_name="test", + ) + assert raw is not None + parsed = json.loads(raw) + assert parsed["success"] is False + assert "content" in parsed["error"].lower() + + def test_success_false_returns_failure(self): + result = {"success": False, "content": "something"} + raw = _validate_result( + result, + required_keys=frozenset({"content"}), + tool_name="test", + ) + assert raw is not None + parsed = json.loads(raw) + assert parsed["success"] is False + assert "success" in parsed["error"].lower() + + def test_retriable_true_propagates(self): + result = {"success": True} + raw = _validate_result( + result, + required_keys=frozenset({"success", "missing"}), + tool_name="test", + retriable=True, + ) + assert raw is not None + parsed = json.loads(raw) + assert parsed["retriable"] is True + + def test_retriable_false_default(self): + result = {"success": True} + raw = _validate_result( + result, + required_keys=frozenset({"success", "missing"}), + tool_name="test", + ) + assert raw is not None + parsed = json.loads(raw) + assert parsed["retriable"] is False From 757aa9fa51abeee2f3abee0183a13d8373f4640c Mon Sep 17 00:00:00 2001 From: Trecek Date: Sun, 28 Jun 2026 10:21:09 -0700 Subject: [PATCH 2/3] fix: correct test failures from _validate_result implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump tools_kitchen.py line-limit exemption 1290→1310 for new validator block - Update _LEGACY_JSON_WRITES allowlist with shifted json.dumps line numbers - Fix open_kitchen fail-closed tests: use correct patch targets (autoskillit.server._get_ctx / autoskillit.server.logger), add _apply_triage_gate mock, fix assertions to match recipe_validation path - Fix load_recipe fail-closed tests: use object-form monkeypatch to avoid arch re-export bypass violation, fix _LOAD_CACHE import to _api_cache, add _apply_triage_gate mock so _validate_result is reached Co-Authored-By: Claude Sonnet 4.6 --- tests/arch/test_subpackage_isolation.py | 2 +- tests/infra/test_schema_version_convention.py | 10 ++--- tests/server/test_tools_kitchen_gate.py | 37 ++++++++++--------- tests/server/test_tools_load_recipe.py | 32 +++++++++------- 4 files changed, 45 insertions(+), 36 deletions(-) diff --git a/tests/arch/test_subpackage_isolation.py b/tests/arch/test_subpackage_isolation.py index 82f66d0799..e6a2729e86 100644 --- a/tests/arch/test_subpackage_isolation.py +++ b/tests/arch/test_subpackage_isolation.py @@ -967,7 +967,7 @@ def test_data_directories_are_not_python_packages() -> None: "co-located with the execution engine that calls them", ), "tools_kitchen.py": ( - 1290, + 1310, "REQ-CNST-010-E7: kitchen tool handlers — open_kitchen and lock_ingredients require " "inline validation helpers (_check_override_keys, _build_ingredient_key_suggestions) " "for ingredient key validation; splitting would cross import-layer boundaries; " diff --git a/tests/infra/test_schema_version_convention.py b/tests/infra/test_schema_version_convention.py index ff548013fe..a04feccad8 100644 --- a/tests/infra/test_schema_version_convention.py +++ b/tests/infra/test_schema_version_convention.py @@ -119,11 +119,11 @@ def _is_yaml_dump(node: ast.expr) -> bool: # _lifespan.py — hooks.json self-heal on startup drift (co-owned with Claude plugin system) ("src/autoskillit/server/_lifespan.py", 90), # tools_kitchen.py — hook config, quota guard, git_ops_policy, ingredient locks overlay - ("src/autoskillit/server/tools/tools_kitchen.py", 204), - ("src/autoskillit/server/tools/tools_kitchen.py", 223), - ("src/autoskillit/server/tools/tools_kitchen.py", 257), - ("src/autoskillit/server/tools/tools_kitchen.py", 1013), - ("src/autoskillit/server/tools/tools_kitchen.py", 1073), + ("src/autoskillit/server/tools/tools_kitchen.py", 205), + ("src/autoskillit/server/tools/tools_kitchen.py", 224), + ("src/autoskillit/server/tools/tools_kitchen.py", 258), + ("src/autoskillit/server/tools/tools_kitchen.py", 1028), + ("src/autoskillit/server/tools/tools_kitchen.py", 1088), # tools_pipeline_tracker.py — tracker_data dict ("src/autoskillit/server/tools/tools_pipeline_tracker.py", 166), # tools_status.py — mcp_data dict diff --git a/tests/server/test_tools_kitchen_gate.py b/tests/server/test_tools_kitchen_gate.py index a63abf3074..b30f387a7c 100644 --- a/tests/server/test_tools_kitchen_gate.py +++ b/tests/server/test_tools_kitchen_gate.py @@ -345,23 +345,24 @@ async def test_close_kitchen_drains_orphaned_github_api_entries(tmp_path, monkey @pytest.mark.anyio async def test_open_kitchen_fail_closed_empty_content(monkeypatch, tmp_path): - """open_kitchen returns fail-closed envelope when content is empty.""" + """open_kitchen returns fail-closed when content is empty (caught at validity gate).""" monkeypatch.chdir(tmp_path) ctx = _make_mock_ctx() ctx.gate.enabled = True ctx.gate_infrastructure_ready = True - ctx.recipes.load_and_validate.return_value = { - "valid": True, - "content": "", - "dispatch_feasible": True, - } + _load_result = {"valid": True, "content": "", "dispatch_feasible": True} + ctx.recipes.load_and_validate.return_value = _load_result ctx.recipes.find.return_value = MagicMock(path=tmp_path / "r.yaml") ctx.config.providers = MagicMock() ctx.backend = MagicMock() ctx.backend.name = "test" with ( - patch("autoskillit.server.tools.tools_kitchen._get_ctx", return_value=ctx), - patch("autoskillit.server.tools.tools_kitchen.logger"), + patch("autoskillit.server._get_ctx", return_value=ctx), + patch("autoskillit.server.logger"), + patch( + "autoskillit.server.tools.tools_kitchen._apply_triage_gate", + new=AsyncMock(return_value=_load_result), + ), patch( "autoskillit.server.tools.tools_kitchen._prime_quota_cache", new_callable=AsyncMock, @@ -374,27 +375,29 @@ async def test_open_kitchen_fail_closed_empty_content(monkeypatch, tmp_path): parsed = json.loads(raw) assert parsed["success"] is False - assert "content" in parsed["error"].lower() + assert parsed.get("kitchen") == "failed" @pytest.mark.anyio async def test_open_kitchen_fail_closed_missing_content(monkeypatch, tmp_path): - """open_kitchen returns fail-closed envelope when content key is missing.""" + """open_kitchen returns fail-closed when content key is missing (caught at validity gate).""" monkeypatch.chdir(tmp_path) ctx = _make_mock_ctx() ctx.gate.enabled = True ctx.gate_infrastructure_ready = True - ctx.recipes.load_and_validate.return_value = { - "valid": True, - "dispatch_feasible": True, - } + _load_result = {"valid": True, "dispatch_feasible": True} + ctx.recipes.load_and_validate.return_value = _load_result ctx.recipes.find.return_value = MagicMock(path=tmp_path / "r.yaml") ctx.config.providers = MagicMock() ctx.backend = MagicMock() ctx.backend.name = "test" with ( - patch("autoskillit.server.tools.tools_kitchen._get_ctx", return_value=ctx), - patch("autoskillit.server.tools.tools_kitchen.logger"), + patch("autoskillit.server._get_ctx", return_value=ctx), + patch("autoskillit.server.logger"), + patch( + "autoskillit.server.tools.tools_kitchen._apply_triage_gate", + new=AsyncMock(return_value=_load_result), + ), patch( "autoskillit.server.tools.tools_kitchen._prime_quota_cache", new_callable=AsyncMock, @@ -407,4 +410,4 @@ async def test_open_kitchen_fail_closed_missing_content(monkeypatch, tmp_path): parsed = json.loads(raw) assert parsed["success"] is False - assert "content" in parsed["error"] + assert parsed.get("kitchen") == "failed" diff --git a/tests/server/test_tools_load_recipe.py b/tests/server/test_tools_load_recipe.py index 18a9c4cfdc..5322e8ad14 100644 --- a/tests/server/test_tools_load_recipe.py +++ b/tests/server/test_tools_load_recipe.py @@ -587,19 +587,22 @@ class TestLoadRecipeFailClosed: @pytest.fixture(autouse=True) def _ensure_ctx(self, tool_ctx_kitchen_open): - pass + self.ctx = tool_ctx_kitchen_open @pytest.mark.anyio async def test_load_recipe_fail_closed_empty_content(self, monkeypatch, tmp_path): monkeypatch.chdir(tmp_path) - from autoskillit.recipe._api import _LOAD_CACHE + from autoskillit.recipe._api_cache import _LOAD_CACHE _LOAD_CACHE.clear() - monkeypatch.setattr( - "autoskillit.recipe._api.load_and_validate", - lambda *a, **kw: {"valid": True, "content": "", "dispatch_feasible": True}, - ) - raw = await load_recipe(name="test-recipe") + test_result = {"valid": True, "content": "", "dispatch_feasible": True} + monkeypatch.setattr(self.ctx.recipes, "load_and_validate", lambda *a, **kw: test_result) + monkeypatch.setattr(self.ctx.recipes, "find", lambda *a, **kw: None) + with patch( + "autoskillit.server.tools.tools_recipe._apply_triage_gate", + new=AsyncMock(return_value=test_result), + ): + raw = await load_recipe(name="test-recipe") parsed = json.loads(raw) assert parsed["success"] is False assert "content" in parsed["error"].lower() @@ -607,14 +610,17 @@ async def test_load_recipe_fail_closed_empty_content(self, monkeypatch, tmp_path @pytest.mark.anyio async def test_load_recipe_fail_closed_missing_content(self, monkeypatch, tmp_path): monkeypatch.chdir(tmp_path) - from autoskillit.recipe._api import _LOAD_CACHE + from autoskillit.recipe._api_cache import _LOAD_CACHE _LOAD_CACHE.clear() - monkeypatch.setattr( - "autoskillit.recipe._api.load_and_validate", - lambda *a, **kw: {"valid": True, "dispatch_feasible": True}, - ) - raw = await load_recipe(name="test-recipe") + test_result = {"valid": True, "dispatch_feasible": True} + monkeypatch.setattr(self.ctx.recipes, "load_and_validate", lambda *a, **kw: test_result) + monkeypatch.setattr(self.ctx.recipes, "find", lambda *a, **kw: None) + with patch( + "autoskillit.server.tools.tools_recipe._apply_triage_gate", + new=AsyncMock(return_value=test_result), + ): + raw = await load_recipe(name="test-recipe") parsed = json.loads(raw) assert parsed["success"] is False assert "content" in parsed["error"] From a409a5f731d2943f3ca38f0945acbb054e840a26 Mon Sep 17 00:00:00 2001 From: Trecek Date: Sun, 28 Jun 2026 10:53:37 -0700 Subject: [PATCH 3/3] fix(review): add error message assertions to open_kitchen fail-closed tests test_open_kitchen_fail_closed_empty_content and test_open_kitchen_fail_closed_missing_content previously only checked `kitchen == "failed"` without verifying the error key exists or contains meaningful content. Adding assertions on `parsed["error"]` makes the tests more robust against regressions in the failure response schema. Co-Authored-By: Claude Sonnet 4.6 --- tests/server/test_tools_kitchen_gate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/server/test_tools_kitchen_gate.py b/tests/server/test_tools_kitchen_gate.py index b30f387a7c..06af45c6a3 100644 --- a/tests/server/test_tools_kitchen_gate.py +++ b/tests/server/test_tools_kitchen_gate.py @@ -376,6 +376,8 @@ async def test_open_kitchen_fail_closed_empty_content(monkeypatch, tmp_path): parsed = json.loads(raw) assert parsed["success"] is False assert parsed.get("kitchen") == "failed" + assert "error" in parsed + assert "failed validation" in parsed["error"] @pytest.mark.anyio @@ -411,3 +413,5 @@ async def test_open_kitchen_fail_closed_missing_content(monkeypatch, tmp_path): parsed = json.loads(raw) assert parsed["success"] is False assert parsed.get("kitchen") == "failed" + assert "error" in parsed + assert "failed validation" in parsed["error"]