Skip to content

Commit c247f64

Browse files
OgeonX-AiAitomatesclaude
authored
fix: audit sweep to resolve typing and lint errors (#7)
* fix(typing): add missing types to tests and resolve ruff errors * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Kim Harjamäki <kim.harjamaki@prosimo.fi> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 989f1d5 commit c247f64

11 files changed

Lines changed: 98 additions & 30 deletions
Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 0 additions & 1 deletion
This file was deleted.

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ jobs:
2121
- run: python -m pip install -e ".[dev]"
2222
- run: python -m ruff check .
2323
- run: python -m mypy
24+
- name: Contract compatibility (pinned cas-contracts v0.1)
25+
# Consumer-side gate: fails red if the pinned CAS contract version or the
26+
# vendored v0.1 schema release drifts from what the Pydantic models emit.
27+
run: python -m pytest tests/test_contract_registry.py -q --tb=short -o addopts=""
2428
- run: python -m pytest
2529
- run: python -m cas_reference_product.evidence
2630

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ build/
1010
.env
1111
.foundry/results/
1212

13+
# Claude Code session worktrees — never commit these
14+
.claude/worktrees/
15+

tests/test_api.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from typing import Any
12
from unittest.mock import patch
23

34
from fastapi.testclient import TestClient
@@ -8,11 +9,11 @@
89

910

1011
class FailingExternalService:
11-
def run(self, envelope) -> str:
12+
def run(self: "Any", envelope: "Any") -> str:
1213
raise WorkflowAgentServiceError("sensitive provider detail")
1314

1415

15-
def test_workflow_api_emits_canonical_events(envelope) -> None:
16+
def test_workflow_api_emits_canonical_events(envelope: "Any") -> None:
1617
client = TestClient(create_app(Settings()))
1718

1819
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:
5657
assert client.get("/health/ready").status_code == 503
5758

5859

59-
def test_workflow_api_sanitizes_external_service_failures(envelope) -> None:
60+
def test_workflow_api_sanitizes_external_service_failures(envelope: "Any") -> None:
6061
with patch(
6162
"cas_reference_product.app.build_workflow_agent_service",
6263
return_value=FailingExternalService(),

tests/test_contract_registry.py

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,32 @@
33
from pathlib import Path
44
from typing import Any
55

6+
import pytest
67
from jsonschema import Draft202012Validator
78
from referencing import Registry, Resource
89

10+
from cas_reference_product.models import LifecycleMetadata
911
from cas_reference_product.workflow import LocalWorkflowAgentService, WorkflowOrchestrator
1012

11-
CONTRACT_ROOT = Path(__file__).parent / "contracts" / "cas-contracts" / "v0.1.0"
13+
# The cas-contracts schema version this consumer pins to. Its Pydantic models
14+
# (cas_reference_product.models.LifecycleMetadata.schemaVersion) emit this exact
15+
# value, so the pin below and the models must stay in lockstep.
16+
PINNED_SCHEMA_VERSION = "0.1.0"
17+
18+
CONTRACT_ROOT = Path(__file__).parent / "contracts" / "cas-contracts" / f"v{PINNED_SCHEMA_VERSION}"
19+
# Sibling source-of-truth checkout (local polyrepo). Absent in isolated CI.
20+
UPSTREAM_ROOT = (
21+
Path(__file__).resolve().parents[2]
22+
/ "cas-contracts"
23+
/ "registry"
24+
/ "releases"
25+
/ f"v{PINNED_SCHEMA_VERSION}"
26+
)
1227

1328

1429
def load_json(path: Path) -> dict[str, Any]:
15-
return json.loads(path.read_text(encoding="utf-8"))
30+
from typing import cast
31+
return cast(dict[str, Any], json.loads(path.read_text(encoding="utf-8")))
1632

1733

1834
def contract_registry() -> Registry[Any]:
@@ -31,17 +47,48 @@ def assert_valid(schema_name: str, instance: dict[str, Any]) -> None:
3147
def test_vendored_contract_release_matches_manifest_hashes() -> None:
3248
manifest = load_json(CONTRACT_ROOT / "manifest.json")
3349

34-
assert manifest["version"] == "0.1.0"
50+
assert manifest["version"] == PINNED_SCHEMA_VERSION
3551
for entry in manifest["schemas"]:
3652
content = (CONTRACT_ROOT / entry["path"]).read_bytes()
3753
assert hashlib.sha256(content).hexdigest() == entry["sha256"]
3854

3955

40-
def test_prompt_envelope_serialization_conforms_to_v010_registry(envelope) -> None:
56+
def test_pinned_version_is_the_version_models_emit() -> None:
57+
"""The version this consumer pins must equal both the schemaVersion const the
58+
vendored schema enforces and the schemaVersion the Pydantic models emit.
59+
60+
Fails red if the vendored release is bumped without updating models.LifecycleMetadata.
61+
"""
62+
common = load_json(CONTRACT_ROOT / "common.schema.json")
63+
schema_const = common["$defs"]["lifecycleMetadata"]["properties"]["schemaVersion"]["const"]
64+
assert schema_const == PINNED_SCHEMA_VERSION
65+
66+
model_default = LifecycleMetadata.model_fields["schemaVersion"].default
67+
assert model_default == PINNED_SCHEMA_VERSION
68+
69+
70+
@pytest.mark.skipif(
71+
not UPSTREAM_ROOT.exists(),
72+
reason="sibling cas-contracts checkout not present (expected in isolated CI)",
73+
)
74+
def test_vendored_release_matches_upstream_source_of_truth() -> None:
75+
"""Local-only drift guard: vendored copy must equal the sibling cas-contracts
76+
release byte-for-byte. Skipped in isolated CI where the sibling is not checked out.
77+
"""
78+
for path in CONTRACT_ROOT.glob("*.json"):
79+
upstream = UPSTREAM_ROOT / path.name
80+
assert upstream.exists(), f"upstream missing {path.name}"
81+
assert (
82+
hashlib.sha256(path.read_bytes()).hexdigest()
83+
== hashlib.sha256(upstream.read_bytes()).hexdigest()
84+
), f"vendored {path.name} drifted from upstream cas-contracts {PINNED_SCHEMA_VERSION}"
85+
86+
87+
def test_prompt_envelope_serialization_conforms_to_v010_registry(envelope: "Any") -> None:
4188
assert_valid("prompt-envelope.schema.json", envelope.model_dump(mode="json"))
4289

4390

44-
def test_run_event_serialization_conforms_to_v010_registry(envelope) -> None:
91+
def test_run_event_serialization_conforms_to_v010_registry(envelope: "Any") -> None:
4592
result = WorkflowOrchestrator(LocalWorkflowAgentService(), envelope.repo).execute(envelope)
4693

4794
for event in result.events:

tests/test_function_boundary.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import json
2+
from typing import Any
23

34
import pytest
45

56
from cas_reference_product.ingress import InvalidIngressRequest, create_worker_message
67

78

8-
def test_ingress_validates_and_serializes_canonical_envelope(envelope) -> None:
9+
def test_ingress_validates_and_serializes_canonical_envelope(envelope: "Any") -> None:
910
message = create_worker_message(envelope.model_dump_json().encode())
1011

1112
assert json.loads(message)["runId"] == envelope.runId

tests/test_models.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1+
from typing import Any
2+
13
import pytest
24
from pydantic import ValidationError
35

46
from cas_reference_product.models import Actor, PromptEnvelope, RunEvent, TraceContext
57

68

7-
def test_prompt_envelope_rejects_extra_properties(envelope) -> None:
9+
def test_prompt_envelope_rejects_extra_properties(envelope: "Any") -> None:
810
payload = envelope.model_dump()
911
payload["secret"] = "not-allowed"
1012

1113
with pytest.raises(ValidationError):
1214
PromptEnvelope.model_validate(payload)
1315

1416

15-
def test_prompt_envelope_matches_cas_contract_metadata(envelope) -> None:
17+
def test_prompt_envelope_matches_cas_contract_metadata(envelope: "Any") -> None:
1618
payload = envelope.model_dump(mode="json")
1719

1820
assert payload["kind"] == "PromptEnvelope"
@@ -28,7 +30,9 @@ def test_prompt_envelope_matches_cas_contract_metadata(envelope) -> None:
2830
["No secrets", "No secrets"],
2931
],
3032
)
31-
def test_prompt_envelope_enforces_cas_contract_constraints(envelope, constraints) -> None:
33+
def test_prompt_envelope_enforces_cas_contract_constraints(
34+
envelope: "Any", constraints: "Any"
35+
) -> None:
3236
payload = envelope.model_dump()
3337
payload["constraints"] = constraints
3438

@@ -67,6 +71,6 @@ def test_prompt_envelope_enforces_cas_contract_constraints(envelope, constraints
6771
),
6872
],
6973
)
70-
def test_contract_models_reject_explicit_null_optional_fields(model, payload) -> None:
74+
def test_contract_models_reject_explicit_null_optional_fields(model: "Any", payload: "Any") -> None:
7175
with pytest.raises(ValidationError):
7276
model.model_validate(payload)

tests/test_service_factory.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from typing import Any
12
from unittest.mock import patch
23

34
import pytest
@@ -39,7 +40,7 @@ def test_foundry_service_rejects_invalid_project_endpoint() -> None:
3940
FoundryWorkflowAgentService(settings)
4041

4142

42-
def test_foundry_service_uses_next_gen_agent_reference(envelope) -> None:
43+
def test_foundry_service_uses_next_gen_agent_reference(envelope: "Any") -> None:
4344
settings = Settings(
4445
environment="prod",
4546
workflow_backend="foundry",
@@ -71,7 +72,7 @@ def test_foundry_service_uses_next_gen_agent_reference(envelope) -> None:
7172
assert result == "Foundry result"
7273

7374

74-
def test_foundry_service_sanitizes_sdk_failure(envelope) -> None:
75+
def test_foundry_service_sanitizes_sdk_failure(envelope: "Any") -> None:
7576
settings = Settings(
7677
environment="prod",
7778
workflow_backend="foundry",

tests/test_telemetry.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Tests for Phase 2 — Telemetry Hardening (TEL-01 through TEL-04)."""
22

3+
from typing import Any
34
from unittest.mock import MagicMock, patch
45

56
from fastapi.testclient import TestClient
@@ -110,12 +111,13 @@ def test_all_loop_stages_share_one_trace_without_prompt_or_output_attributes() -
110111
pass
111112

112113
spans = exporter.get_finished_spans()
113-
assert {span.attributes["cas.stage"] for span in spans} == {stage.value for stage in LoopStage}
114+
stage_values = {stage.value for stage in LoopStage}
115+
assert {span.attributes["cas.stage"] for span in spans if span.attributes} == stage_values
114116
assert len({span.context.trace_id for span in spans}) == 1
115117
assert all(
116118
"prompt" not in key and "output" not in key
117119
for span in spans
118-
for key in span.attributes
120+
for key in (span.attributes or {})
119121
)
120122

121123

@@ -140,7 +142,9 @@ def test_install_propagator_sets_w3c_propagator() -> None:
140142
# ---------------------------------------------------------------------------
141143

142144

143-
def test_workflow_endpoint_creates_span(in_memory_exporter: InMemorySpanExporter, envelope) -> None:
145+
def test_workflow_endpoint_creates_span(
146+
in_memory_exporter: InMemorySpanExporter, envelope: "Any"
147+
) -> None:
144148
client = TestClient(create_app(Settings()))
145149
response = client.post("/api/v1/workflows", json=envelope.model_dump(mode="json"))
146150

@@ -157,12 +161,15 @@ def test_workflow_endpoint_creates_span(in_memory_exporter: InMemorySpanExporter
157161
# ---------------------------------------------------------------------------
158162

159163

160-
def test_workflow_span_attributes(in_memory_exporter: InMemorySpanExporter, envelope) -> None:
164+
def test_workflow_span_attributes(
165+
in_memory_exporter: InMemorySpanExporter, envelope: "Any"
166+
) -> None:
161167
client = TestClient(create_app(Settings()))
162168
client.post("/api/v1/workflows", json=envelope.model_dump(mode="json"))
163169

164170
spans = in_memory_exporter.get_finished_spans()
165171
api_span = next(s for s in spans if s.name == "cas.api.workflows.execute")
172+
assert api_span.attributes
166173
assert api_span.attributes.get("cas.correlation_id") == envelope.correlationId
167174
assert api_span.attributes.get("cas.run_id") == envelope.runId
168175
assert api_span.attributes.get("cas.intent") == envelope.intent
@@ -174,7 +181,7 @@ def test_workflow_span_attributes(in_memory_exporter: InMemorySpanExporter, enve
174181

175182

176183
def test_workflow_span_events_started_and_completed(
177-
in_memory_exporter: InMemorySpanExporter, envelope
184+
in_memory_exporter: InMemorySpanExporter, envelope: "Any"
178185
) -> None:
179186
client = TestClient(create_app(Settings()))
180187
client.post("/api/v1/workflows", json=envelope.model_dump(mode="json"))
@@ -188,12 +195,12 @@ def test_workflow_span_events_started_and_completed(
188195

189196

190197
def test_workflow_span_events_started_and_failed(
191-
in_memory_exporter: InMemorySpanExporter, envelope
198+
in_memory_exporter: InMemorySpanExporter, envelope: "Any"
192199
) -> None:
193200
from cas_reference_product.workflow import WorkflowAgentServiceError
194201

195202
class FailingService:
196-
def run(self, _env) -> str:
203+
def run(self: "Any", _env: "Any") -> str:
197204
raise WorkflowAgentServiceError("backend down")
198205

199206
with patch(
@@ -213,14 +220,15 @@ def run(self, _env) -> str:
213220

214221

215222
def test_span_event_carries_correlation_id(
216-
in_memory_exporter: InMemorySpanExporter, envelope
223+
in_memory_exporter: InMemorySpanExporter, envelope: "Any"
217224
) -> None:
218225
client = TestClient(create_app(Settings()))
219226
client.post("/api/v1/workflows", json=envelope.model_dump(mode="json"))
220227

221228
spans = in_memory_exporter.get_finished_spans()
222229
api_span = next(s for s in spans if s.name == "cas.api.workflows.execute")
223230
started_event = next(e for e in api_span.events if e.name == "workflow.started")
231+
assert started_event.attributes
224232
assert started_event.attributes.get("cas.correlation_id") == envelope.correlationId
225233
assert started_event.attributes.get("cas.run_id") == envelope.runId
226234

@@ -231,7 +239,7 @@ def test_span_event_carries_correlation_id(
231239

232240

233241
def test_w3c_traceparent_propagated_inbound(
234-
in_memory_exporter: InMemorySpanExporter, envelope
242+
in_memory_exporter: InMemorySpanExporter, envelope: "Any"
235243
) -> None:
236244
"""Request with a W3C traceparent header links the API span as a child."""
237245
incoming_traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"

0 commit comments

Comments
 (0)