Skip to content

Commit b2c143e

Browse files
OgeonX-AiAitomates
andauthored
test: port stale contract coverage (#22)
Add current API lifecycle, evidence-boundary, CLI, and tracestate tests recovered from an obsolete Claude worktree. Excludes duplicate telemetry coverage and the coverage-omitting configuration. Co-authored-by: Kim Harjamäki <kim.harjamaki@prosimo.fi>
1 parent e252d11 commit b2c143e

3 files changed

Lines changed: 237 additions & 1 deletion

File tree

tests/test_api.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,27 @@ def run(self: "Any", envelope: "Any") -> str:
1313
raise WorkflowAgentServiceError("sensitive provider detail")
1414

1515

16+
def test_lifespan_invokes_configure_telemetry() -> None:
17+
with patch("cas_reference_product.app.configure_telemetry") as configure:
18+
with TestClient(create_app(Settings())):
19+
configure.assert_called_once()
20+
21+
22+
def test_workflow_api_returns_503_when_service_is_none(envelope: "Any") -> None:
23+
with patch(
24+
"cas_reference_product.app.build_workflow_agent_service",
25+
return_value=None,
26+
):
27+
with TestClient(create_app(Settings())) as client:
28+
response = client.post(
29+
"/api/v1/workflows",
30+
json=envelope.model_dump(mode="json"),
31+
)
32+
33+
assert response.status_code == 503
34+
assert response.json() == {"detail": "Workflow backend is not ready"}
35+
36+
1637
def test_workflow_api_emits_canonical_events(envelope: "Any") -> None:
1738
client = TestClient(create_app(Settings()))
1839

tests/test_evidence.py

Lines changed: 187 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import hashlib
22
import json
3+
import sys
34
from pathlib import Path
5+
from unittest.mock import patch
46

57
import pytest
68

7-
from cas_reference_product.evidence import DEFAULT_BUNDLE, EvidenceVerificationError, verify_bundle
9+
from cas_reference_product.evidence import (
10+
DEFAULT_BUNDLE,
11+
EvidenceVerificationError,
12+
_load_json,
13+
main,
14+
verify_bundle,
15+
)
816

917

1018
def copy_bundle(tmp_path: Path) -> Path:
@@ -22,6 +30,26 @@ def write_json(path: Path, payload: dict[str, object]) -> None:
2230
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
2331

2432

33+
def rebuild_evidence_digests(bundle: Path) -> None:
34+
descriptor_path = bundle / "bundle.json"
35+
descriptor = json.loads(descriptor_path.read_text(encoding="utf-8"))
36+
37+
for section_name in ("sourceProvenance", "contractRegistry", "evaluation", "platformWhatIf"):
38+
section = descriptor[section_name]
39+
section["sha256"] = hashlib.sha256((bundle / section["path"]).read_bytes()).hexdigest()
40+
41+
artifact_manifest_path = bundle / "artifact-manifest.json"
42+
artifact_manifest = json.loads(artifact_manifest_path.read_text(encoding="utf-8"))
43+
manifest_entries = {item["uri"]: item for item in artifact_manifest["artifacts"]}
44+
for artifact in descriptor["artifacts"]:
45+
digest = hashlib.sha256((bundle / artifact["path"]).read_bytes()).hexdigest()
46+
artifact["sha256"] = digest
47+
manifest_entries[artifact["uri"]]["sha256"] = digest
48+
49+
write_json(artifact_manifest_path, artifact_manifest)
50+
write_json(descriptor_path, descriptor)
51+
52+
2553
def test_committed_immutable_evidence_bundle_verifies() -> None:
2654
verify_bundle()
2755

@@ -124,3 +152,161 @@ def test_evaluation_response_digest_is_mandatory(tmp_path: Path) -> None:
124152

125153
with pytest.raises(EvidenceVerificationError, match="evaluation fixture digest mismatch"):
126154
verify_bundle(bundle)
155+
156+
157+
def test_load_json_rejects_non_object_json(tmp_path: Path) -> None:
158+
path = tmp_path / "array.json"
159+
path.write_text("[1, 2, 3]", encoding="utf-8")
160+
161+
with pytest.raises(EvidenceVerificationError, match="must contain a JSON object"):
162+
_load_json(path)
163+
164+
165+
def test_bundle_artifacts_must_be_a_non_empty_list(tmp_path: Path) -> None:
166+
bundle = copy_bundle(tmp_path)
167+
descriptor_path = bundle / "bundle.json"
168+
descriptor = json.loads(descriptor_path.read_text(encoding="utf-8"))
169+
descriptor["artifacts"] = []
170+
write_json(descriptor_path, descriptor)
171+
172+
with pytest.raises(
173+
EvidenceVerificationError,
174+
match="bundle artifacts must be a non-empty list",
175+
):
176+
verify_bundle(bundle)
177+
178+
179+
def test_bundle_artifact_entry_must_be_an_object(tmp_path: Path) -> None:
180+
bundle = copy_bundle(tmp_path)
181+
descriptor_path = bundle / "bundle.json"
182+
descriptor = json.loads(descriptor_path.read_text(encoding="utf-8"))
183+
descriptor["artifacts"] = ["not-an-object"]
184+
write_json(descriptor_path, descriptor)
185+
186+
with pytest.raises(EvidenceVerificationError, match="bundle artifact entries must be objects"):
187+
verify_bundle(bundle)
188+
189+
190+
def test_artifact_missing_path_or_sha256_raises(tmp_path: Path) -> None:
191+
bundle = copy_bundle(tmp_path)
192+
descriptor_path = bundle / "bundle.json"
193+
descriptor = json.loads(descriptor_path.read_text(encoding="utf-8"))
194+
descriptor["artifacts"] = [{"uri": "urn:test", "path": None, "sha256": None}]
195+
write_json(descriptor_path, descriptor)
196+
197+
with pytest.raises(EvidenceVerificationError, match="artifact path and sha256 must be strings"):
198+
verify_bundle(bundle)
199+
200+
201+
def test_artifact_sha256_with_invalid_pattern_raises(tmp_path: Path) -> None:
202+
bundle = copy_bundle(tmp_path)
203+
descriptor_path = bundle / "bundle.json"
204+
descriptor = json.loads(descriptor_path.read_text(encoding="utf-8"))
205+
descriptor["artifacts"] = [
206+
{"uri": "urn:test", "path": "artifact-manifest.json", "sha256": "invalid"}
207+
]
208+
write_json(descriptor_path, descriptor)
209+
210+
with pytest.raises(EvidenceVerificationError, match="has an invalid SHA-256 digest"):
211+
verify_bundle(bundle)
212+
213+
214+
def test_artifact_path_traversal_outside_bundle_root_raises(tmp_path: Path) -> None:
215+
bundle = copy_bundle(tmp_path)
216+
descriptor_path = bundle / "bundle.json"
217+
descriptor = json.loads(descriptor_path.read_text(encoding="utf-8"))
218+
descriptor["artifacts"] = [
219+
{"uri": "urn:test", "path": "../../outside.json", "sha256": "a" * 64}
220+
]
221+
write_json(descriptor_path, descriptor)
222+
223+
with pytest.raises(EvidenceVerificationError, match="escapes the bundle root"):
224+
verify_bundle(bundle)
225+
226+
227+
def test_invalid_git_sha_in_source_provenance_raises(tmp_path: Path) -> None:
228+
bundle = copy_bundle(tmp_path)
229+
provenance_path = bundle / "artifacts" / "source-provenance.json"
230+
provenance = json.loads(provenance_path.read_text(encoding="utf-8"))
231+
provenance["repositories"][0]["sha"] = "not-a-git-sha"
232+
write_json(provenance_path, provenance)
233+
rebuild_evidence_digests(bundle)
234+
235+
with pytest.raises(EvidenceVerificationError, match="invalid immutable source reference"):
236+
verify_bundle(bundle)
237+
238+
239+
@pytest.mark.parametrize(
240+
("field", "value", "message"),
241+
[
242+
("summary", {"failed": 1, "passed": 0, "total": 1}, "did not pass exactly one case"),
243+
("suiteId", "wrong-suite", "unexpected golden path evaluation suite"),
244+
],
245+
)
246+
def test_evaluation_metadata_mismatch_raises(
247+
tmp_path: Path, field: str, value: object, message: str
248+
) -> None:
249+
bundle = copy_bundle(tmp_path)
250+
evaluation_path = bundle / "artifacts" / "eval-evidence.json"
251+
evaluation = json.loads(evaluation_path.read_text(encoding="utf-8"))
252+
evaluation[field] = value
253+
write_json(evaluation_path, evaluation)
254+
rebuild_evidence_digests(bundle)
255+
256+
with pytest.raises(EvidenceVerificationError, match=message):
257+
verify_bundle(bundle)
258+
259+
260+
def test_evaluation_response_digest_mismatch_raises(tmp_path: Path) -> None:
261+
bundle = copy_bundle(tmp_path)
262+
evaluation_path = bundle / "artifacts" / "eval-evidence.json"
263+
evaluation = json.loads(evaluation_path.read_text(encoding="utf-8"))
264+
evaluation["evidence"][0]["execution"]["responseDigest"] = f"sha256:{'0' * 64}"
265+
write_json(evaluation_path, evaluation)
266+
rebuild_evidence_digests(bundle)
267+
268+
with pytest.raises(EvidenceVerificationError, match="evaluation response digest mismatch"):
269+
verify_bundle(bundle)
270+
271+
272+
def test_available_container_with_invalid_digest_raises(tmp_path: Path) -> None:
273+
bundle = copy_bundle(tmp_path)
274+
descriptor_path = bundle / "bundle.json"
275+
descriptor = json.loads(descriptor_path.read_text(encoding="utf-8"))
276+
descriptor["containerImage"] = {"status": "available", "digest": "invalid"}
277+
write_json(descriptor_path, descriptor)
278+
279+
with pytest.raises(EvidenceVerificationError, match="requires a valid digest"):
280+
verify_bundle(bundle)
281+
282+
283+
def test_verification_result_outcome_not_passed_raises(tmp_path: Path) -> None:
284+
bundle = copy_bundle(tmp_path)
285+
result_path = bundle / "verification-result.json"
286+
result = json.loads(result_path.read_text(encoding="utf-8"))
287+
result["outcome"] = "failed"
288+
write_json(result_path, result)
289+
290+
with pytest.raises(EvidenceVerificationError, match="canonical VerificationResult must pass"):
291+
verify_bundle(bundle)
292+
293+
294+
def test_main_returns_zero_on_valid_bundle() -> None:
295+
with patch.object(sys, "argv", ["evidence"]):
296+
assert main() == 0
297+
298+
299+
def test_main_returns_one_on_invalid_bundle_path() -> None:
300+
with patch.object(sys, "argv", ["evidence", "/nonexistent/path/bundle"]):
301+
assert main() == 1
302+
303+
304+
def test_main_returns_one_on_verification_failure(tmp_path: Path) -> None:
305+
bundle = copy_bundle(tmp_path)
306+
descriptor_path = bundle / "bundle.json"
307+
descriptor = json.loads(descriptor_path.read_text(encoding="utf-8"))
308+
descriptor["platformWhatIf"]["deploymentClaim"] = "deployed"
309+
write_json(descriptor_path, descriptor)
310+
311+
with patch.object(sys, "argv", ["evidence", str(bundle)]):
312+
assert main() == 1

tests/test_workflow.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import pytest
66

7+
from cas_reference_product.models import Actor, PromptEnvelope, TraceContext
78
from cas_reference_product.workflow import WorkflowOrchestrator
89

910

@@ -40,3 +41,31 @@ def test_orchestrator_returns_traceable_events(envelope: "Any") -> None:
4041
def test_orchestrator_propagates_failure(envelope: "Any") -> None:
4142
with pytest.raises(RuntimeError, match="expected"):
4243
WorkflowOrchestrator(FailingService(), envelope.repo).execute(envelope)
44+
45+
46+
def test_event_includes_tracestate_when_present(envelope: "Any") -> None:
47+
envelope_with_tracestate = PromptEnvelope(
48+
correlationId=envelope.correlationId,
49+
promptId=envelope.promptId,
50+
runId=envelope.runId,
51+
repo=envelope.repo,
52+
actor=Actor(id="developer", type="human"),
53+
timestamp=envelope.timestamp,
54+
traceContext=TraceContext(
55+
traceparent="00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
56+
tracestate="vendor1=value1",
57+
),
58+
intent=envelope.intent,
59+
prompt=envelope.prompt,
60+
constraints=envelope.constraints,
61+
)
62+
63+
with patch(
64+
"cas_reference_product.workflow.current_traceparent",
65+
side_effect=lambda fallback: fallback,
66+
):
67+
result = WorkflowOrchestrator(
68+
SuccessfulService(), envelope.repo
69+
).execute(envelope_with_tracestate)
70+
71+
assert all(event.traceContext.tracestate == "vendor1=value1" for event in result.events)

0 commit comments

Comments
 (0)