From f3e398aa500a1e92bab6800865df108f1ed8111d Mon Sep 17 00:00:00 2001 From: Ivan Lim Date: Mon, 6 Jul 2026 02:37:35 +0800 Subject: [PATCH] test(raises): narrow 13 broad pytest.raises(Exception) to ground-truth types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stack A / PR-A2. Each narrowed type was observed from the running code via the synced test env, not guessed: - Frozen dataclasses -> dataclasses.FrozenInstanceError: ImmutableScope + ContextBinding (test_immutability.py x5), Artifact (test_emit_artifact.py), RunRef (test_kernel_re_exports.py). - Frozen pydantic models -> pydantic.ValidationError: KVStoreContext (test_kvstore.py), ValidationResult + ToolCallRejected (test_capability_hooks.py x2). - test_programmatic_device_execution.py: the lifecycle wraps the device's SandboxExecutionError and re-raises TaskExecutionError (shepherd_core.errors), so the assertion now pins TaskExecutionError (the propagated type), not the mock's inner cause. - test_mcp_stdio_lifecycle.py: spawning a missing binary -> FileNotFoundError. - test_static_fragment_corpus.py: the core-0 trace validators raise TraceValidationError; verified across all 18 parametrized corpus cases. Left intentionally broad (documented, not a miss): test_lane_c_run.py:84 asserts the raised error is NOT the removed LC-3 fence (a negative-match on the message), so the type is polymorphic by design. Tested-by: uv run ruff check (clean, 8 files); pytest on each affected file/test (immutability 10, kvstore 54, capability_hooks 22, emit_artifact 13, kernel_re_exports 8, device-cleanup 1, mcp invalid-command 1, corpus 18) — all pass. --- .../tests/kernel/test_static_fragment_corpus.py | 3 ++- .../integration/three_layer/test_immutability.py | 12 ++++++------ .../meta/tests/unit/test_capability_hooks.py | 5 +++-- shepherd/packages/meta/tests/unit/test_kvstore.py | 3 ++- .../providers/tests/unit/test_mcp_stdio_lifecycle.py | 2 +- .../lifecycle/test_programmatic_device_execution.py | 3 ++- .../runtime/tests/unit/nucleus/test_emit_artifact.py | 4 ++-- .../tests/unit/trace/test_kernel_re_exports.py | 4 +++- 8 files changed, 21 insertions(+), 15 deletions(-) diff --git a/shepherd/packages/kernel-v3-reference/tests/kernel/test_static_fragment_corpus.py b/shepherd/packages/kernel-v3-reference/tests/kernel/test_static_fragment_corpus.py index aa24002..6b8f97e 100644 --- a/shepherd/packages/kernel-v3-reference/tests/kernel/test_static_fragment_corpus.py +++ b/shepherd/packages/kernel-v3-reference/tests/kernel/test_static_fragment_corpus.py @@ -14,6 +14,7 @@ from shepherd_kernel_v3_reference.trace.machine import run_trace from shepherd_kernel_v3_reference.trace.records import EffectCapture, SelectionClosed from shepherd_kernel_v3_reference.trace.validate import ( + TraceValidationError, validate_core0_trace, validate_core0_trace_prefix, validate_core_a_trace, @@ -266,7 +267,7 @@ def test_static_fragment_direct_kernel_and_trace_corpus(case: CorpusCase) -> Non if case.core0: _validate_complete_or_prefix(direct, traced.trace, core0=True) else: - with pytest.raises(Exception): + with pytest.raises(TraceValidationError): _validate_complete_or_prefix(direct, traced.trace, core0=True) _validate_complete_or_prefix(direct, traced.trace, core0=False) diff --git a/shepherd/packages/meta/tests/integration/three_layer/test_immutability.py b/shepherd/packages/meta/tests/integration/three_layer/test_immutability.py index ec1179e..3aa89ea 100644 --- a/shepherd/packages/meta/tests/integration/three_layer/test_immutability.py +++ b/shepherd/packages/meta/tests/integration/three_layer/test_immutability.py @@ -5,7 +5,7 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import FrozenInstanceError, dataclass from typing import Self import pytest @@ -28,13 +28,13 @@ async def test_immutable_scope_is_frozen(self) -> None: scope = ImmutableScope() # Attempting to mutate should raise FrozenInstanceError - with pytest.raises(Exception): # dataclass.FrozenInstanceError + with pytest.raises(FrozenInstanceError): scope._id = "new_id" - with pytest.raises(Exception): + with pytest.raises(FrozenInstanceError): scope._bindings = () - with pytest.raises(Exception): + with pytest.raises(FrozenInstanceError): scope._stream = scope._stream async def test_context_binding_is_frozen(self) -> None: @@ -43,10 +43,10 @@ async def test_context_binding_is_frozen(self) -> None: binding = ContextBinding(name="test", context=ctx) # Attempting to mutate should raise FrozenInstanceError - with pytest.raises(Exception): + with pytest.raises(FrozenInstanceError): binding.name = "new_name" - with pytest.raises(Exception): + with pytest.raises(FrozenInstanceError): binding.context = ctx async def test_immutable_scope_with_binding_returns_new_instance(self) -> None: diff --git a/shepherd/packages/meta/tests/unit/test_capability_hooks.py b/shepherd/packages/meta/tests/unit/test_capability_hooks.py index be8bc36..3b5c0d3 100644 --- a/shepherd/packages/meta/tests/unit/test_capability_hooks.py +++ b/shepherd/packages/meta/tests/unit/test_capability_hooks.py @@ -16,6 +16,7 @@ from __future__ import annotations import pytest +from pydantic import ValidationError from shepherd_contexts import WorkspaceRef from shepherd_core.effects import ToolCallRejected from shepherd_core.types import ( @@ -100,7 +101,7 @@ def test_result_is_frozen(self): tool_call = ToolCall(id="tc_1", name="Read", params={}) result = ValidationResult(allowed=True, tool=tool_call) # Attempting to modify should raise an error - with pytest.raises(Exception): + with pytest.raises(ValidationError): result.allowed = False @@ -143,7 +144,7 @@ def test_frozen(self): reason="Not allowed", ) - with pytest.raises(Exception): + with pytest.raises(ValidationError): effect.tool_name = "Edit" diff --git a/shepherd/packages/meta/tests/unit/test_kvstore.py b/shepherd/packages/meta/tests/unit/test_kvstore.py index 83d7264..387dcb3 100644 --- a/shepherd/packages/meta/tests/unit/test_kvstore.py +++ b/shepherd/packages/meta/tests/unit/test_kvstore.py @@ -14,6 +14,7 @@ """ import pytest +from pydantic import ValidationError from shepherd_contexts import KVStoreContext, SessionState from shepherd_core.context import compute_composite_reversibility, is_execution_context from shepherd_core.effects import TaskCompleted, TaskStarted @@ -41,7 +42,7 @@ def test_create_with_data(self): def test_model_is_frozen(self): """Model should be frozen (cannot reassign attributes).""" store = KVStoreContext.create({"key": "value"}) - with pytest.raises(Exception): # Pydantic frozen model + with pytest.raises(ValidationError): store.data = {"new": "value"} def test_str_returns_empty(self): diff --git a/shepherd/packages/providers/tests/unit/test_mcp_stdio_lifecycle.py b/shepherd/packages/providers/tests/unit/test_mcp_stdio_lifecycle.py index 7888b85..a4c853d 100644 --- a/shepherd/packages/providers/tests/unit/test_mcp_stdio_lifecycle.py +++ b/shepherd/packages/providers/tests/unit/test_mcp_stdio_lifecycle.py @@ -126,7 +126,7 @@ class TestErrorHandling: async def test_start_invalid_command(self): """Starting with a non-existent command should raise an error.""" bridge = StdioMCPBridge() - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): await bridge.start("/nonexistent/binary/xyz", ["--bad"]) # Cleanup should be safe even after failed start await bridge.stop() diff --git a/shepherd/packages/runtime/tests/unit/lifecycle/test_programmatic_device_execution.py b/shepherd/packages/runtime/tests/unit/lifecycle/test_programmatic_device_execution.py index 4745186..4f3212f 100644 --- a/shepherd/packages/runtime/tests/unit/lifecycle/test_programmatic_device_execution.py +++ b/shepherd/packages/runtime/tests/unit/lifecycle/test_programmatic_device_execution.py @@ -12,6 +12,7 @@ import pytest from shepherd_core.effects import TaskCompleted, TaskStarted +from shepherd_core.errors import TaskExecutionError from shepherd_core.foundation.protocols.device import ( DeviceCapabilities, EffectBundle, @@ -364,7 +365,7 @@ async def test_cleanup_called_after_device_error(self) -> None: executor=instance.execute, task_name="GreetTask", ) as lifecycle: - with pytest.raises(Exception): + with pytest.raises(TaskExecutionError): await lifecycle.run_executor() device.cleanup.assert_called_once() diff --git a/shepherd/packages/runtime/tests/unit/nucleus/test_emit_artifact.py b/shepherd/packages/runtime/tests/unit/nucleus/test_emit_artifact.py index 48e35d3..e754825 100644 --- a/shepherd/packages/runtime/tests/unit/nucleus/test_emit_artifact.py +++ b/shepherd/packages/runtime/tests/unit/nucleus/test_emit_artifact.py @@ -10,7 +10,7 @@ import asyncio import json -from dataclasses import dataclass +from dataclasses import FrozenInstanceError, dataclass import pytest from shepherd_core.schema import SINGLE_OUTPUT_KEY @@ -217,7 +217,7 @@ async def with_secret_artifact() -> str: def test_artifact_is_frozen_dataclass() -> None: artifact = Artifact(kind="report", name="x.txt", content="hi", metadata={"k": "v"}) - with pytest.raises(Exception): + with pytest.raises(FrozenInstanceError): artifact.kind = "other" # type: ignore[misc] diff --git a/shepherd/packages/runtime/tests/unit/trace/test_kernel_re_exports.py b/shepherd/packages/runtime/tests/unit/trace/test_kernel_re_exports.py index 10b183c..368fe6b 100644 --- a/shepherd/packages/runtime/tests/unit/trace/test_kernel_re_exports.py +++ b/shepherd/packages/runtime/tests/unit/trace/test_kernel_re_exports.py @@ -12,6 +12,8 @@ from __future__ import annotations +from dataclasses import FrozenInstanceError + import pytest @@ -32,7 +34,7 @@ def test_runref_is_frozen_dataclass_with_string_id() -> None: assert ref.to_payload()["schema"] == RUN_REF_SCHEMA assert RunRef.from_payload(ref.to_payload()) == ref # Frozen: cannot reassign id. - with pytest.raises(Exception): + with pytest.raises(FrozenInstanceError): ref.id = "run_other" # type: ignore[misc] # Hashable; equality structural. assert ref == RunRef(id="run_01HZX")