From 317e1f0f8e86d3c2ef092471538f737da1f80d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20Harjam=C3=A4ki?= Date: Fri, 3 Jul 2026 14:05:29 +0300 Subject: [PATCH 1/3] fix(typing): add missing types to tests and resolve ruff errors --- tests/test_api.py | 7 ++++--- tests/test_contract_registry.py | 7 ++++--- tests/test_function_boundary.py | 3 ++- tests/test_models.py | 9 +++++---- tests/test_service_factory.py | 5 +++-- tests/test_telemetry.py | 22 +++++++++++++--------- tests/test_workflow.py | 9 +++++---- 7 files changed, 36 insertions(+), 26 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 4be7dbd..6aa26c2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,3 +1,4 @@ +from typing import Any from unittest.mock import patch from fastapi.testclient import TestClient @@ -8,11 +9,11 @@ class FailingExternalService: - def run(self, envelope) -> str: + def run(self: "Any", envelope: "Any") -> str: raise WorkflowAgentServiceError("sensitive provider detail") -def test_workflow_api_emits_canonical_events(envelope) -> None: +def test_workflow_api_emits_canonical_events(envelope: "Any") -> None: client = TestClient(create_app(Settings())) response = client.post("/api/v1/workflows", json=envelope.model_dump(mode="json")) @@ -56,7 +57,7 @@ def test_invalid_foundry_endpoint_is_not_ready() -> None: assert client.get("/health/ready").status_code == 503 -def test_workflow_api_sanitizes_external_service_failures(envelope) -> None: +def test_workflow_api_sanitizes_external_service_failures(envelope: "Any") -> None: with patch( "cas_reference_product.app.build_workflow_agent_service", return_value=FailingExternalService(), diff --git a/tests/test_contract_registry.py b/tests/test_contract_registry.py index df6672d..126ceb1 100644 --- a/tests/test_contract_registry.py +++ b/tests/test_contract_registry.py @@ -12,7 +12,8 @@ def load_json(path: Path) -> dict[str, Any]: - return json.loads(path.read_text(encoding="utf-8")) + from typing import cast + return cast(dict[str, Any], json.loads(path.read_text(encoding="utf-8"))) def contract_registry() -> Registry[Any]: @@ -37,11 +38,11 @@ def test_vendored_contract_release_matches_manifest_hashes() -> None: assert hashlib.sha256(content).hexdigest() == entry["sha256"] -def test_prompt_envelope_serialization_conforms_to_v010_registry(envelope) -> None: +def test_prompt_envelope_serialization_conforms_to_v010_registry(envelope: "Any") -> None: assert_valid("prompt-envelope.schema.json", envelope.model_dump(mode="json")) -def test_run_event_serialization_conforms_to_v010_registry(envelope) -> None: +def test_run_event_serialization_conforms_to_v010_registry(envelope: "Any") -> None: result = WorkflowOrchestrator(LocalWorkflowAgentService(), envelope.repo).execute(envelope) for event in result.events: diff --git a/tests/test_function_boundary.py b/tests/test_function_boundary.py index 4736f5d..a2d4e19 100644 --- a/tests/test_function_boundary.py +++ b/tests/test_function_boundary.py @@ -1,3 +1,4 @@ +from typing import Any import json import pytest @@ -5,7 +6,7 @@ from cas_reference_product.ingress import InvalidIngressRequest, create_worker_message -def test_ingress_validates_and_serializes_canonical_envelope(envelope) -> None: +def test_ingress_validates_and_serializes_canonical_envelope(envelope: "Any") -> None: message = create_worker_message(envelope.model_dump_json().encode()) assert json.loads(message)["runId"] == envelope.runId diff --git a/tests/test_models.py b/tests/test_models.py index a9b4179..6de2020 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,10 +1,11 @@ +from typing import Any import pytest from pydantic import ValidationError from cas_reference_product.models import Actor, PromptEnvelope, RunEvent, TraceContext -def test_prompt_envelope_rejects_extra_properties(envelope) -> None: +def test_prompt_envelope_rejects_extra_properties(envelope: "Any") -> None: payload = envelope.model_dump() payload["secret"] = "not-allowed" @@ -12,7 +13,7 @@ def test_prompt_envelope_rejects_extra_properties(envelope) -> None: PromptEnvelope.model_validate(payload) -def test_prompt_envelope_matches_cas_contract_metadata(envelope) -> None: +def test_prompt_envelope_matches_cas_contract_metadata(envelope: "Any") -> None: payload = envelope.model_dump(mode="json") assert payload["kind"] == "PromptEnvelope" @@ -28,7 +29,7 @@ def test_prompt_envelope_matches_cas_contract_metadata(envelope) -> None: ["No secrets", "No secrets"], ], ) -def test_prompt_envelope_enforces_cas_contract_constraints(envelope, constraints) -> None: +def test_prompt_envelope_enforces_cas_contract_constraints(envelope: "Any", constraints: "Any") -> None: payload = envelope.model_dump() payload["constraints"] = constraints @@ -67,6 +68,6 @@ def test_prompt_envelope_enforces_cas_contract_constraints(envelope, constraints ), ], ) -def test_contract_models_reject_explicit_null_optional_fields(model, payload) -> None: +def test_contract_models_reject_explicit_null_optional_fields(model: "Any", payload: "Any") -> None: with pytest.raises(ValidationError): model.model_validate(payload) diff --git a/tests/test_service_factory.py b/tests/test_service_factory.py index 0c3c664..84cbe06 100644 --- a/tests/test_service_factory.py +++ b/tests/test_service_factory.py @@ -1,3 +1,4 @@ +from typing import Any from unittest.mock import patch import pytest @@ -39,7 +40,7 @@ def test_foundry_service_rejects_invalid_project_endpoint() -> None: FoundryWorkflowAgentService(settings) -def test_foundry_service_uses_next_gen_agent_reference(envelope) -> None: +def test_foundry_service_uses_next_gen_agent_reference(envelope: "Any") -> None: settings = Settings( environment="prod", workflow_backend="foundry", @@ -71,7 +72,7 @@ def test_foundry_service_uses_next_gen_agent_reference(envelope) -> None: assert result == "Foundry result" -def test_foundry_service_sanitizes_sdk_failure(envelope) -> None: +def test_foundry_service_sanitizes_sdk_failure(envelope: "Any") -> None: settings = Settings( environment="prod", workflow_backend="foundry", diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py index c5a4fce..d98a440 100644 --- a/tests/test_telemetry.py +++ b/tests/test_telemetry.py @@ -1,3 +1,4 @@ +from typing import Any """Tests for Phase 2 — Telemetry Hardening (TEL-01 through TEL-04).""" from unittest.mock import MagicMock, patch @@ -110,12 +111,13 @@ def test_all_loop_stages_share_one_trace_without_prompt_or_output_attributes() - pass spans = exporter.get_finished_spans() - assert {span.attributes["cas.stage"] for span in spans} == {stage.value for stage in LoopStage} + stage_values = {stage.value for stage in LoopStage} + assert {span.attributes["cas.stage"] for span in spans if span.attributes} == stage_values assert len({span.context.trace_id for span in spans}) == 1 assert all( "prompt" not in key and "output" not in key for span in spans - for key in span.attributes + for key in (span.attributes or {}) ) @@ -140,7 +142,7 @@ def test_install_propagator_sets_w3c_propagator() -> None: # --------------------------------------------------------------------------- -def test_workflow_endpoint_creates_span(in_memory_exporter: InMemorySpanExporter, envelope) -> None: +def test_workflow_endpoint_creates_span(in_memory_exporter: InMemorySpanExporter, envelope: "Any") -> None: client = TestClient(create_app(Settings())) response = client.post("/api/v1/workflows", json=envelope.model_dump(mode="json")) @@ -157,12 +159,13 @@ def test_workflow_endpoint_creates_span(in_memory_exporter: InMemorySpanExporter # --------------------------------------------------------------------------- -def test_workflow_span_attributes(in_memory_exporter: InMemorySpanExporter, envelope) -> None: +def test_workflow_span_attributes(in_memory_exporter: InMemorySpanExporter, envelope: "Any") -> None: client = TestClient(create_app(Settings())) client.post("/api/v1/workflows", json=envelope.model_dump(mode="json")) spans = in_memory_exporter.get_finished_spans() api_span = next(s for s in spans if s.name == "cas.api.workflows.execute") + assert api_span.attributes assert api_span.attributes.get("cas.correlation_id") == envelope.correlationId assert api_span.attributes.get("cas.run_id") == envelope.runId assert api_span.attributes.get("cas.intent") == envelope.intent @@ -174,7 +177,7 @@ def test_workflow_span_attributes(in_memory_exporter: InMemorySpanExporter, enve def test_workflow_span_events_started_and_completed( - in_memory_exporter: InMemorySpanExporter, envelope + in_memory_exporter: InMemorySpanExporter, envelope: "Any" ) -> None: client = TestClient(create_app(Settings())) client.post("/api/v1/workflows", json=envelope.model_dump(mode="json")) @@ -188,12 +191,12 @@ def test_workflow_span_events_started_and_completed( def test_workflow_span_events_started_and_failed( - in_memory_exporter: InMemorySpanExporter, envelope + in_memory_exporter: InMemorySpanExporter, envelope: "Any" ) -> None: from cas_reference_product.workflow import WorkflowAgentServiceError class FailingService: - def run(self, _env) -> str: + def run(self: "Any", _env: "Any") -> str: raise WorkflowAgentServiceError("backend down") with patch( @@ -213,7 +216,7 @@ def run(self, _env) -> str: def test_span_event_carries_correlation_id( - in_memory_exporter: InMemorySpanExporter, envelope + in_memory_exporter: InMemorySpanExporter, envelope: "Any" ) -> None: client = TestClient(create_app(Settings())) client.post("/api/v1/workflows", json=envelope.model_dump(mode="json")) @@ -221,6 +224,7 @@ def test_span_event_carries_correlation_id( spans = in_memory_exporter.get_finished_spans() api_span = next(s for s in spans if s.name == "cas.api.workflows.execute") started_event = next(e for e in api_span.events if e.name == "workflow.started") + assert started_event.attributes assert started_event.attributes.get("cas.correlation_id") == envelope.correlationId assert started_event.attributes.get("cas.run_id") == envelope.runId @@ -231,7 +235,7 @@ def test_span_event_carries_correlation_id( def test_w3c_traceparent_propagated_inbound( - in_memory_exporter: InMemorySpanExporter, envelope + in_memory_exporter: InMemorySpanExporter, envelope: "Any" ) -> None: """Request with a W3C traceparent header links the API span as a child.""" incoming_traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" diff --git a/tests/test_workflow.py b/tests/test_workflow.py index fb6d538..3b7b60d 100644 --- a/tests/test_workflow.py +++ b/tests/test_workflow.py @@ -1,3 +1,4 @@ +from typing import Any from datetime import UTC, datetime from unittest.mock import patch @@ -7,16 +8,16 @@ class SuccessfulService: - def run(self, envelope) -> str: + def run(self: "Any", envelope: "Any") -> str: return f"processed:{envelope.promptId}" class FailingService: - def run(self, envelope) -> str: + def run(self: "Any", envelope: "Any") -> str: raise RuntimeError("expected") -def test_orchestrator_returns_traceable_events(envelope) -> None: +def test_orchestrator_returns_traceable_events(envelope: "Any") -> None: fixed = datetime(2026, 6, 11, 10, 0, tzinfo=UTC) # Patch current_traceparent so this unit test is provider-independent. with patch( @@ -36,6 +37,6 @@ def test_orchestrator_returns_traceable_events(envelope) -> None: assert all(event.traceContext == envelope.traceContext for event in result.events) -def test_orchestrator_propagates_failure(envelope) -> None: +def test_orchestrator_propagates_failure(envelope: "Any") -> None: with pytest.raises(RuntimeError, match="expected"): WorkflowOrchestrator(FailingService(), envelope.repo).execute(envelope) From 71a2fb48e06d99b566ff31cfc9a00046b0d292b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20Harjam=C3=A4ki?= Date: Fri, 3 Jul 2026 17:56:53 +0300 Subject: [PATCH 2/3] chore(contracts,hygiene): add contract-compat check; untrack .claude worktrees Pins cas-contracts v0.1.0 assertion; ignores and untracks .claude/worktrees. Co-Authored-By: Claude Opus 4.8 --- .claude/worktrees/agent-a663d9be60cbd74bf | 1 - .claude/worktrees/agent-af9dfbbffaac749ab | 1 - .github/workflows/ci.yml | 4 ++ .gitignore | 3 ++ tests/test_contract_registry.py | 50 ++++++++++++++++++++++- 5 files changed, 55 insertions(+), 4 deletions(-) delete mode 160000 .claude/worktrees/agent-a663d9be60cbd74bf delete mode 160000 .claude/worktrees/agent-af9dfbbffaac749ab diff --git a/.claude/worktrees/agent-a663d9be60cbd74bf b/.claude/worktrees/agent-a663d9be60cbd74bf deleted file mode 160000 index c7df179..0000000 --- a/.claude/worktrees/agent-a663d9be60cbd74bf +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c7df17942e08d569d5b2445106807ea9896a96ba diff --git a/.claude/worktrees/agent-af9dfbbffaac749ab b/.claude/worktrees/agent-af9dfbbffaac749ab deleted file mode 160000 index c7df179..0000000 --- a/.claude/worktrees/agent-af9dfbbffaac749ab +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c7df17942e08d569d5b2445106807ea9896a96ba diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 832614e..60a9213 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,10 @@ jobs: - run: python -m pip install -e ".[dev]" - run: python -m ruff check . - run: python -m mypy + - name: Contract compatibility (pinned cas-contracts v0.1) + # Consumer-side gate: fails red if the pinned CAS contract version or the + # vendored v0.1 schema release drifts from what the Pydantic models emit. + run: python -m pytest tests/test_contract_registry.py -q --tb=short -o addopts="" - run: python -m pytest - run: python -m cas_reference_product.evidence diff --git a/.gitignore b/.gitignore index 1267023..7cca7ad 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ build/ .env .foundry/results/ +# Claude Code session worktrees — never commit these +.claude/worktrees/ + diff --git a/tests/test_contract_registry.py b/tests/test_contract_registry.py index 126ceb1..cbaa3a1 100644 --- a/tests/test_contract_registry.py +++ b/tests/test_contract_registry.py @@ -3,12 +3,27 @@ from pathlib import Path from typing import Any +import pytest from jsonschema import Draft202012Validator from referencing import Registry, Resource +from cas_reference_product.models import LifecycleMetadata from cas_reference_product.workflow import LocalWorkflowAgentService, WorkflowOrchestrator -CONTRACT_ROOT = Path(__file__).parent / "contracts" / "cas-contracts" / "v0.1.0" +# The cas-contracts schema version this consumer pins to. Its Pydantic models +# (cas_reference_product.models.LifecycleMetadata.schemaVersion) emit this exact +# value, so the pin below and the models must stay in lockstep. +PINNED_SCHEMA_VERSION = "0.1.0" + +CONTRACT_ROOT = Path(__file__).parent / "contracts" / "cas-contracts" / f"v{PINNED_SCHEMA_VERSION}" +# Sibling source-of-truth checkout (local polyrepo). Absent in isolated CI. +UPSTREAM_ROOT = ( + Path(__file__).resolve().parents[2] + / "cas-contracts" + / "registry" + / "releases" + / f"v{PINNED_SCHEMA_VERSION}" +) def load_json(path: Path) -> dict[str, Any]: @@ -32,12 +47,43 @@ def assert_valid(schema_name: str, instance: dict[str, Any]) -> None: def test_vendored_contract_release_matches_manifest_hashes() -> None: manifest = load_json(CONTRACT_ROOT / "manifest.json") - assert manifest["version"] == "0.1.0" + assert manifest["version"] == PINNED_SCHEMA_VERSION for entry in manifest["schemas"]: content = (CONTRACT_ROOT / entry["path"]).read_bytes() assert hashlib.sha256(content).hexdigest() == entry["sha256"] +def test_pinned_version_is_the_version_models_emit() -> None: + """The version this consumer pins must equal both the schemaVersion const the + vendored schema enforces and the schemaVersion the Pydantic models emit. + + Fails red if the vendored release is bumped without updating models.LifecycleMetadata. + """ + common = load_json(CONTRACT_ROOT / "common.schema.json") + schema_const = common["$defs"]["lifecycleMetadata"]["properties"]["schemaVersion"]["const"] + assert schema_const == PINNED_SCHEMA_VERSION + + model_default = LifecycleMetadata.model_fields["schemaVersion"].default + assert model_default == PINNED_SCHEMA_VERSION + + +@pytest.mark.skipif( + not UPSTREAM_ROOT.exists(), + reason="sibling cas-contracts checkout not present (expected in isolated CI)", +) +def test_vendored_release_matches_upstream_source_of_truth() -> None: + """Local-only drift guard: vendored copy must equal the sibling cas-contracts + release byte-for-byte. Skipped in isolated CI where the sibling is not checked out. + """ + for path in CONTRACT_ROOT.glob("*.json"): + upstream = UPSTREAM_ROOT / path.name + assert upstream.exists(), f"upstream missing {path.name}" + assert ( + hashlib.sha256(path.read_bytes()).hexdigest() + == hashlib.sha256(upstream.read_bytes()).hexdigest() + ), f"vendored {path.name} drifted from upstream cas-contracts {PINNED_SCHEMA_VERSION}" + + def test_prompt_envelope_serialization_conforms_to_v010_registry(envelope: "Any") -> None: assert_valid("prompt-envelope.schema.json", envelope.model_dump(mode="json")) From e678dad5e9464e721dbcb43adddbd77542e87faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20Harjam=C3=A4ki?= Date: Fri, 3 Jul 2026 18:07:39 +0300 Subject: [PATCH 3/3] style: resolve ruff errors in tests (import order, E402, E501) Move misplaced module docstring above imports in test_telemetry.py, sort imports, and wrap over-long function signatures. Co-Authored-By: Claude Opus 4.8 --- tests/test_function_boundary.py | 2 +- tests/test_models.py | 5 ++++- tests/test_telemetry.py | 10 +++++++--- tests/test_workflow.py | 2 +- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/test_function_boundary.py b/tests/test_function_boundary.py index a2d4e19..bd24059 100644 --- a/tests/test_function_boundary.py +++ b/tests/test_function_boundary.py @@ -1,5 +1,5 @@ -from typing import Any import json +from typing import Any import pytest diff --git a/tests/test_models.py b/tests/test_models.py index 6de2020..5d96ba4 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,4 +1,5 @@ from typing import Any + import pytest from pydantic import ValidationError @@ -29,7 +30,9 @@ def test_prompt_envelope_matches_cas_contract_metadata(envelope: "Any") -> None: ["No secrets", "No secrets"], ], ) -def test_prompt_envelope_enforces_cas_contract_constraints(envelope: "Any", constraints: "Any") -> None: +def test_prompt_envelope_enforces_cas_contract_constraints( + envelope: "Any", constraints: "Any" +) -> None: payload = envelope.model_dump() payload["constraints"] = constraints diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py index d98a440..7daa363 100644 --- a/tests/test_telemetry.py +++ b/tests/test_telemetry.py @@ -1,6 +1,6 @@ -from typing import Any """Tests for Phase 2 — Telemetry Hardening (TEL-01 through TEL-04).""" +from typing import Any from unittest.mock import MagicMock, patch from fastapi.testclient import TestClient @@ -142,7 +142,9 @@ def test_install_propagator_sets_w3c_propagator() -> None: # --------------------------------------------------------------------------- -def test_workflow_endpoint_creates_span(in_memory_exporter: InMemorySpanExporter, envelope: "Any") -> None: +def test_workflow_endpoint_creates_span( + in_memory_exporter: InMemorySpanExporter, envelope: "Any" +) -> None: client = TestClient(create_app(Settings())) response = client.post("/api/v1/workflows", json=envelope.model_dump(mode="json")) @@ -159,7 +161,9 @@ def test_workflow_endpoint_creates_span(in_memory_exporter: InMemorySpanExporter # --------------------------------------------------------------------------- -def test_workflow_span_attributes(in_memory_exporter: InMemorySpanExporter, envelope: "Any") -> None: +def test_workflow_span_attributes( + in_memory_exporter: InMemorySpanExporter, envelope: "Any" +) -> None: client = TestClient(create_app(Settings())) client.post("/api/v1/workflows", json=envelope.model_dump(mode="json")) diff --git a/tests/test_workflow.py b/tests/test_workflow.py index 3b7b60d..3f0b6cf 100644 --- a/tests/test_workflow.py +++ b/tests/test_workflow.py @@ -1,5 +1,5 @@ -from typing import Any from datetime import UTC, datetime +from typing import Any from unittest.mock import patch import pytest