Skip to content

Commit 4652531

Browse files
OgeonX-AiAitomates
andauthored
feat(contracts): verify registry-fetch smoke check + update vendored $id to resolvable Pages URL (#9)
* test(registry-check): add failing test for registry-fetch smoke check - RED phase: test_registry_check.py covers all-200 success, one-404 failure, network-unavailable distinction, HTTP 5xx errors, and CLI exit-code paths for the not-yet-implemented cas_evals.registry_check - REQ-1.4.11 * feat(registry-check): implement registry-fetch smoke check module - GREEN phase: check_registry_urls() performs live GETs against the Pages registry, raising RegistryCheckError naming the first failing path on non-200, HTTPError, or network-unavailable conditions - main() is the python -m cas_evals.registry_check CLI entry point, checking index.json, v0.1/manifest.json, v0.1/common.schema.json, and v0.1/evaluation-result.schema.json by default; exits non-zero on failure - Follows reference_product.py's urllib pattern: capped response reads (MAX_RESPONSE_BYTES), typed exception-to-domain-error translation - REQ-1.4.11 * feat(contracts): verify registry-fetch smoke check + update vendored $id to resolvable Pages URL - Update vendored v0.1.0 common/evaluation-result schema $id values from the dead schemas.coding-autopilot.dev namespace to the resolvable Pages registry URL, matching the portfolio-wide convention from cas-contracts plan 32-01 - Recompute sha256 in provenance.json for the re-vendored files - Update verify_vendored_contract()'s two hardcoded $id equality checks to match the new value - Wire a new registry-smoke CI job invoking cas_evals.registry_check against the live Pages registry, isolated from the offline verify matrix so a transient network failure doesn't block unrelated merges - Regenerate releases/v0.2.0/manifest.json: its provenanceDigest is a sha256 of provenance.json, which changed as a direct consequence of the re-vendor above (Rule 3 auto-fix: this is a blocking downstream effect of the provenance.json edit required by this task, not new scope) - REQ-1.4.11 --------- Co-authored-by: Kim Harjamäki <kim.harjamaki@prosimo.fi>
1 parent 4fe936c commit 4652531

8 files changed

Lines changed: 204 additions & 7 deletions

File tree

.github/workflows/ci.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,25 @@ jobs:
2626
- run: python -m cas_evals.cli benchmarks/v0.2/golden.json
2727
- run: python -m cas_evals.cli benchmarks/v0.2/adversarial.json
2828
- run: python -m cas_evals.release --check
29+
30+
registry-smoke:
31+
# Separate from the offline `verify` matrix job because this job needs
32+
# network egress and should not block the offline unit-test matrix on
33+
# transient network failures. As of this job's introduction, the
34+
# cas-contracts PR from plan 32-01 (rewriting schema $id to this same
35+
# Pages registry URL) has not yet merged. This job checks paths that
36+
# already resolve today regardless of that PR's merge status -- the
37+
# v0.1/v1.0/v1.1 registry lines were published by prior phases' pages.yml
38+
# work. Merging 32-01 only changes the $id field text embedded inside
39+
# those already-resolving files, not which URLs exist.
40+
runs-on: ubuntu-latest
41+
timeout-minutes: 5
42+
permissions:
43+
contents: read
44+
steps:
45+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
46+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
47+
with:
48+
python-version: "3.11"
49+
- run: python -m pip install -e .
50+
- run: python -m cas_evals.registry_check

releases/v0.2.0/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"releaseVersion": "v0.2.0",
44
"releasedAt": "2026-06-11T12:00:00Z",
55
"sharedContract": {
6-
"provenanceDigest": "sha256:3d82b533691c779e9cf2361f0491a2d8873ef71e22f279c5cfdfc784515be9a5",
6+
"provenanceDigest": "sha256:d206cb050c42c80d10d37b418f6a6bbb9da08aba0ae76e77d3344108613829a2",
77
"release": "https://github.com/Coding-Autopilot-System/cas-contracts/releases/tag/v0.1.0",
88
"repository": "https://github.com/Coding-Autopilot-System/cas-contracts",
99
"tag": "v0.1.0"

src/cas_evals/contracts.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ def verify_vendored_contract() -> dict[str, Any]:
6161

6262
common = _load_json(VENDOR_DIR / "common.schema.json")
6363
evaluation = _load_json(VENDOR_DIR / "evaluation-result.schema.json")
64-
if common.get("$id") != "https://schemas.coding-autopilot.dev/v0.1/common.schema.json":
64+
if common.get("$id") != "https://coding-autopilot-system.github.io/cas-contracts/registry/v0.1/common.schema.json":
6565
raise ContractValidationError("unexpected common schema identity")
66-
if evaluation.get("$id") != "https://schemas.coding-autopilot.dev/v0.1/evaluation-result.schema.json":
66+
if evaluation.get("$id") != "https://coding-autopilot-system.github.io/cas-contracts/registry/v0.1/evaluation-result.schema.json":
6767
raise ContractValidationError("unexpected evaluation schema identity")
6868
if evaluation["allOf"][0].get("$ref") != "common.schema.json#/$defs/lifecycleMetadata":
6969
raise ContractValidationError("evaluation schema does not reference the vendored common schema")

src/cas_evals/registry_check.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Registry-fetch smoke check for the live cas-contracts GitHub Pages registry.
2+
3+
Performs live HTTP GETs against known-published registry paths and asserts
4+
each responds with HTTP 200. This is deliberately network-dependent and
5+
distinct from cas_evals.contracts.verify_vendored_contract(), which only
6+
checks bytes on disk and never touches the network. The two mechanisms are
7+
additive: verify_vendored_contract() proves the pinned offline copy is intact;
8+
this module proves the live registry actually resolves today.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import sys
14+
from urllib.error import HTTPError, URLError
15+
from urllib.request import Request, urlopen
16+
17+
DEFAULT_BASE_URL = "https://coding-autopilot-system.github.io/cas-contracts/registry"
18+
DEFAULT_PATHS = (
19+
"index.json",
20+
"v0.1/manifest.json",
21+
"v0.1/common.schema.json",
22+
"v0.1/evaluation-result.schema.json",
23+
)
24+
MAX_RESPONSE_BYTES = 2_000_000
25+
26+
27+
class RegistryCheckError(RuntimeError):
28+
"""Raised when a registry-fetch smoke check fails."""
29+
30+
31+
def check_registry_urls(
32+
base_url: str,
33+
paths: list[str] | tuple[str, ...],
34+
timeout_seconds: float = 10,
35+
) -> list[tuple[str, int]]:
36+
"""Fetch each path joined onto base_url and assert HTTP 200.
37+
38+
Returns a list of (path, status_code) tuples when every URL responds
39+
with 200. Raises RegistryCheckError naming the first failing path if any
40+
URL returns non-200, times out, or the network is unavailable.
41+
"""
42+
results: list[tuple[str, int]] = []
43+
for path in paths:
44+
url = f"{base_url.rstrip('/')}/{path.lstrip('/')}"
45+
request = Request(url, method="GET")
46+
try:
47+
with urlopen(request, timeout=timeout_seconds) as response:
48+
status = response.status
49+
response.read(MAX_RESPONSE_BYTES + 1)
50+
except HTTPError as error:
51+
raise RegistryCheckError(f"{path} returned HTTP {error.code}") from error
52+
except (URLError, TimeoutError, OSError) as error:
53+
raise RegistryCheckError(f"{path} is unavailable (network error)") from error
54+
if status != 200:
55+
raise RegistryCheckError(f"{path} returned HTTP {status}")
56+
results.append((path, status))
57+
return results
58+
59+
60+
def main(argv: list[str] | None = None) -> int:
61+
"""CLI entry point: python -m cas_evals.registry_check [--base-url URL]."""
62+
args = list(sys.argv[1:] if argv is None else argv)
63+
base_url = DEFAULT_BASE_URL
64+
if "--base-url" in args:
65+
index = args.index("--base-url")
66+
base_url = args[index + 1]
67+
68+
try:
69+
results = check_registry_urls(base_url, DEFAULT_PATHS)
70+
except RegistryCheckError as error:
71+
print(f"FAIL: {error}")
72+
return 1
73+
74+
for path, status in results:
75+
print(f"{path}: {status}")
76+
return 0
77+
78+
79+
if __name__ == "__main__":
80+
sys.exit(main())

tests/test_registry_check.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import io
2+
import unittest
3+
from contextlib import redirect_stdout
4+
from unittest.mock import MagicMock, patch
5+
from urllib.error import HTTPError, URLError
6+
7+
from cas_evals.registry_check import RegistryCheckError, check_registry_urls, main
8+
9+
10+
def _fake_response(status_code):
11+
response = MagicMock()
12+
response.status = status_code
13+
response.read.return_value = b""
14+
response.__enter__.return_value = response
15+
response.__exit__.return_value = False
16+
return response
17+
18+
19+
class RegistryCheckTests(unittest.TestCase):
20+
def test_all_paths_return_200(self):
21+
with patch("cas_evals.registry_check.urlopen", return_value=_fake_response(200)) as urlopen:
22+
results = check_registry_urls(
23+
"https://coding-autopilot-system.github.io/cas-contracts/registry",
24+
["index.json", "v0.1/manifest.json"],
25+
)
26+
self.assertEqual(
27+
results,
28+
[("index.json", 200), ("v0.1/manifest.json", 200)],
29+
)
30+
self.assertEqual(urlopen.call_count, 2)
31+
32+
def test_one_path_returns_404(self):
33+
responses = [_fake_response(200), _fake_response(404)]
34+
35+
def side_effect(*args, **kwargs):
36+
return responses.pop(0)
37+
38+
with patch("cas_evals.registry_check.urlopen", side_effect=side_effect):
39+
with self.assertRaises(RegistryCheckError) as ctx:
40+
check_registry_urls(
41+
"https://coding-autopilot-system.github.io/cas-contracts/registry",
42+
["index.json", "v0.1/manifest.json"],
43+
)
44+
self.assertIn("v0.1/manifest.json", str(ctx.exception))
45+
self.assertIn("404", str(ctx.exception))
46+
47+
def test_network_unavailable_raises_distinguishable_error(self):
48+
with patch("cas_evals.registry_check.urlopen", side_effect=URLError("no route to host")):
49+
with self.assertRaises(RegistryCheckError) as ctx:
50+
check_registry_urls(
51+
"https://coding-autopilot-system.github.io/cas-contracts/registry",
52+
["index.json"],
53+
)
54+
self.assertIn("unavailable", str(ctx.exception).lower())
55+
self.assertNotIn("404", str(ctx.exception))
56+
57+
def test_http_error_raises_registry_check_error(self):
58+
error = HTTPError("https://example.com/index.json", 500, "Internal Server Error", {}, None)
59+
with patch("cas_evals.registry_check.urlopen", side_effect=error):
60+
with self.assertRaises(RegistryCheckError) as ctx:
61+
check_registry_urls(
62+
"https://coding-autopilot-system.github.io/cas-contracts/registry",
63+
["index.json"],
64+
)
65+
self.assertIn("500", str(ctx.exception))
66+
67+
def test_main_exits_zero_on_success(self):
68+
with patch("cas_evals.registry_check.check_registry_urls") as mocked:
69+
mocked.return_value = [
70+
("index.json", 200),
71+
("v0.1/manifest.json", 200),
72+
("v0.1/common.schema.json", 200),
73+
("v0.1/evaluation-result.schema.json", 200),
74+
]
75+
buffer = io.StringIO()
76+
with redirect_stdout(buffer):
77+
exit_code = main([])
78+
self.assertEqual(exit_code, 0)
79+
output = buffer.getvalue()
80+
self.assertIn("index.json", output)
81+
self.assertIn("200", output)
82+
83+
def test_main_exits_nonzero_on_failure(self):
84+
with patch(
85+
"cas_evals.registry_check.check_registry_urls",
86+
side_effect=RegistryCheckError("v0.1/manifest.json returned HTTP 404"),
87+
):
88+
buffer = io.StringIO()
89+
with redirect_stdout(buffer):
90+
exit_code = main([])
91+
self.assertEqual(exit_code, 1)
92+
93+
94+
if __name__ == "__main__":
95+
unittest.main()

vendor/cas-contracts/v0.1.0/common.schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"$schema": "https://json-schema.org/draft/2020-12/schema",
3-
"$id": "https://schemas.coding-autopilot.dev/v0.1/common.schema.json",
3+
"$id": "https://coding-autopilot-system.github.io/cas-contracts/registry/v0.1/common.schema.json",
44
"title": "CAS Common Definitions",
55
"$defs": {
66
"actor": {

vendor/cas-contracts/v0.1.0/evaluation-result.schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"$schema": "https://json-schema.org/draft/2020-12/schema",
3-
"$id": "https://schemas.coding-autopilot.dev/v0.1/evaluation-result.schema.json",
3+
"$id": "https://coding-autopilot-system.github.io/cas-contracts/registry/v0.1/evaluation-result.schema.json",
44
"title": "EvaluationResult",
55
"type": "object",
66
"allOf": [

vendor/cas-contracts/v0.1.0/provenance.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
"schemas": {
66
"common.schema.json": {
77
"blobSha": "0eec265131a301f924a5ca7fb61718f5bdb14012",
8-
"sha256": "c7ce72a6f5da8394e48f2421820588a8142546962e05152997bd1e6ced994928",
8+
"sha256": "6c86075df043ec924f6b7aa004c6a694916a6c3d7a822dda4235f1ee8368f92a",
99
"source": "https://raw.githubusercontent.com/Coding-Autopilot-System/cas-contracts/v0.1.0/schemas/v0.1/common.schema.json"
1010
},
1111
"evaluation-result.schema.json": {
1212
"blobSha": "719f46a6ee9024fa4462094c3d0c21d838c20f17",
13-
"sha256": "be6d3216c95cfa6d2ccda908ff089010765b1c70223a920bfe3cb70a0cd24df5",
13+
"sha256": "ecb222d70b389e2d47de5bb1344dd4e5ae1043c0e76f8608aa99f42e491ce384",
1414
"source": "https://raw.githubusercontent.com/Coding-Autopilot-System/cas-contracts/v0.1.0/schemas/v0.1/evaluation-result.schema.json"
1515
}
1616
}

0 commit comments

Comments
 (0)