Files
copilotkit__copilotkit/sdk-python/tests/test_intelligence.py
Maximiliano Korp 078ee356e0 fix(sdk-python): align ZIP bounds and the second-304 error code with the other SDKs
D4 - Python enforced only total archive bytes, entry count, and declared total
uncompressed bytes. It had no per-file byte bound, no compression-method
allowlist, and no artifact path-length bound, while TypeScript and C# enforce
`maxFileBytes` 10 MB, `maxFiles` 1000, stored/deflate only, and `maxPathLength`
512. Executed against the real SDKs on identical bundles at default limits,
Python accepted a 15 MB member, 3001 files, an LZMA member, and a 610-character
path that TypeScript rejected. All four are now refused with the same
`LEARNING_BLOB_INTEGRITY_FAILURE`/`validation` code and the peer messages. The
pre-existing `max_archive_entries` knob is kept for compatibility and both
bounds are enforced.

`_extract` also re-labelled every specific bound violation as "Invalid skill ZIP
archive", because `IntelligenceError` subclasses `RuntimeError` and its own
generic handler swallowed it. Typed errors are now re-raised first.

D5 - the second unconditional 304 raised `LEARNING_BLOB_INTEGRITY_FAILURE` in
the `validation` category, contradicting the shared golden fixture's
`expectations.secondUnconditional304Code` and both peer SDKs. It now raises
`IntelligenceCacheMissError`, which carries `LEARNING_SDK_CACHE_CORRUPT` in the
`internal` category, and the test asserts the fixture field rather than a
restated constant. `errors.*.invalidatesCache` is likewise asserted from the
fixture.

Collision and identity keys now route through the shared pinned folding helper
instead of `str.casefold()`, so Python cannot drift from TypeScript and C# on
code points added after the interpreter's Unicode version.
2026-07-24 16:33:54 -07:00

971 lines
35 KiB
Python

import asyncio
import base64
import hashlib
import io
import json
import shutil
import stat
import threading
import time
import unicodedata
import zipfile
from concurrent.futures import ThreadPoolExecutor
from dataclasses import FrozenInstanceError
from pathlib import Path
import pytest
from copilotkit import (
AsyncCopilotKitIntelligence as ExportedAsyncCopilotKitIntelligence,
CopilotKitIntelligence as ExportedCopilotKitIntelligence,
IntelligenceSkillDescriptor as ExportedIntelligenceSkillDescriptor,
IntelligenceSkillFileDescriptor as ExportedIntelligenceSkillFileDescriptor,
IntelligenceSkillManifestDescriptor as ExportedIntelligenceSkillManifestDescriptor,
)
CONTAINER = "55555555-5555-4555-8555-555555555555"
INTRO = "99999999-9999-4999-8999-999999999999"
QUIZ = "88888888-8888-4888-8888-888888888888"
BAD = "77777777-7777-4777-8777-777777777777"
VERSION_ONE = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"
VERSION_TWO = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
from copilotkit.intelligence import (
_ERROR_CATEGORIES,
_ERROR_CODES,
AsyncCopilotKitIntelligence,
CopilotKitIntelligence,
IntelligenceAccessDeniedError,
IntelligenceCacheMissError,
IntelligenceError,
IntelligenceIntegrityError,
IntelligenceRequest,
IntelligenceResponse,
IntelligenceSkillDescriptor,
IntelligenceSkillFileDescriptor,
IntelligenceSkillManifestDescriptor,
IntelligenceSkillSet,
IntelligenceUnavailableError,
)
def golden_registry_fixture():
path = (
Path(__file__).parents[2]
/ "packages/intelligence/conformance/registry-sdk-v1.json"
)
return json.loads(path.read_text(encoding="utf-8"))
def canonical_error_vocabulary():
path = (
Path(__file__).parents[2]
/ "packages/intelligence/conformance/learning-platform-v1.json"
)
corpus = json.loads(path.read_text(encoding="utf-8"))
properties = corpus["schemas"]["LearningPlatformErrorResponseV1"]["properties"][
"error"
]["properties"]
return properties["code"]["enum"], properties["category"]["enum"]
def canonical_error_response(code, category):
return {
"error": {
"code": code,
"message": "Canonical Learning Platform error.",
"category": category,
"retryable": True,
},
"requestId": "request-canonical",
"traceId": "trace-canonical",
}
def golden_client(tmp_path, transport):
golden = golden_registry_fixture()
return CopilotKitIntelligence(
api_key="secret-token",
project_namespace=golden["identity"]["projectNamespace"],
base_url=golden["identity"]["baseUrl"],
cache_dir=tmp_path,
transport=transport,
)
def test_clients_are_exported_from_the_package_root():
assert ExportedCopilotKitIntelligence is CopilotKitIntelligence
assert ExportedAsyncCopilotKitIntelligence is AsyncCopilotKitIntelligence
def test_skill_descriptors_are_immutable_verified_projection_views(tmp_path):
first_archive = bundle(
{
"root/SKILL.md": "# Introduction",
"root/reference.md": "Verified reference",
}
)
second_archive = bundle({"root/SKILL.md": "# Quiz"})
payload = projection(
[
(INTRO, VERSION_ONE, first_archive),
(QUIZ, VERSION_TWO, second_archive),
]
)
payload["entries"][0]["description"] = "Introduces the course"
result = client(tmp_path, QueueTransport(response(payload))).skills.get(CONTAINER)
assert ExportedIntelligenceSkillDescriptor is IntelligenceSkillDescriptor
assert (
ExportedIntelligenceSkillManifestDescriptor
is IntelligenceSkillManifestDescriptor
)
assert ExportedIntelligenceSkillFileDescriptor is IntelligenceSkillFileDescriptor
assert isinstance(result.skill_descriptors, tuple)
assert [descriptor.skill_id for descriptor in result.skill_descriptors] == [
INTRO,
QUIZ,
]
descriptor = result.skill_descriptors[0]
expected_directory = (result.path / "skills" / f"000000-{INTRO}" / "root").resolve()
assert descriptor == IntelligenceSkillDescriptor(
skill_id=INTRO,
version_id=VERSION_ONE,
position=0,
name="Skill 0",
description="Introduces the course",
directory=expected_directory,
manifest=IntelligenceSkillManifestDescriptor(
agent_skills_profile="agentskills:v1",
manifest_sha256=payload["entries"][0]["manifestSha256"],
files=(
IntelligenceSkillFileDescriptor(
path="SKILL.md",
role="instructions",
media_type="text/markdown",
byte_length=len(b"# Introduction"),
raw_sha256=hashlib.sha256(b"# Introduction").hexdigest(),
),
IntelligenceSkillFileDescriptor(
path="reference.md",
role="resource",
media_type="text/markdown",
byte_length=len(b"Verified reference"),
raw_sha256=hashlib.sha256(b"Verified reference").hexdigest(),
),
),
),
)
assert descriptor.directory == descriptor.directory.resolve()
assert descriptor.directory.is_dir()
assert descriptor.manifest.files == tuple(descriptor.manifest.files)
with pytest.raises(FrozenInstanceError):
descriptor.name = "Changed"
with pytest.raises(FrozenInstanceError):
descriptor.manifest.manifest_sha256 = "0" * 64
with pytest.raises(FrozenInstanceError):
descriptor.manifest.files[0].role = "script"
def test_skill_descriptors_are_not_returned_before_full_verification(tmp_path):
archive = bundle()
payload = projection([(INTRO, VERSION_ONE, archive)])
entry = payload["entries"][0]
entry["manifest"]["files"][0]["rawSha256"] = "0" * 64
manifest_without_hash = {
key: value
for key, value in entry["manifest"].items()
if key != "manifestSha256"
}
bad_manifest_sha = hashlib.sha256(
json.dumps(
manifest_without_hash,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
).encode()
).hexdigest()
entry["manifest"]["manifestSha256"] = bad_manifest_sha
entry["manifestSha256"] = bad_manifest_sha
returned = None
with pytest.raises(IntelligenceIntegrityError, match="integrity verification"):
returned = client(tmp_path, QueueTransport(response(payload))).skills.get(
CONTAINER
)
assert returned is None
def test_legacy_skill_set_constructor_remains_compatible(tmp_path):
legacy = IntelligenceSkillSet(
CONTAINER,
"revision",
"0" * 64,
(),
tmp_path,
"cached",
False,
)
assert legacy.skill_descriptors == ()
def test_shared_golden_projection_uses_canonical_v1_http_contract(tmp_path):
golden = golden_registry_fixture()
archive = base64.b64decode(golden["bundle"]["base64"], validate=True)
transport = QueueTransport(
response(golden["projection"]), IntelligenceResponse(200, {}, archive)
)
result = golden_client(tmp_path, transport).skills.get(
golden["identity"]["learningContainerId"]
)
assert result.freshness == golden["expectations"]["initialFreshness"]
assert transport.requests[0].url == (
golden["identity"]["baseUrl"] + golden["http"]["projectionPath"]
)
assert (
transport.requests[0].headers["Authorization"]
== golden["http"]["authorization"]
)
assert (result.skills[0].path / golden["bundle"]["filePath"]).read_text() == (
golden["bundle"]["fileContents"]
)
def test_noncanonical_container_id_fails_before_python_transport(tmp_path):
transport = QueueTransport(response({}))
with pytest.raises(IntelligenceIntegrityError) as raised:
golden_client(tmp_path, transport).skills.get("not-a-uuid")
assert raised.value.code == "LEARNING_REGISTRY_UNRECOVERABLE"
assert raised.value.category == "validation"
assert transport.requests == []
def test_canonical_conflict_preserves_cache_and_error_metadata(tmp_path):
golden = golden_registry_fixture()
archive = base64.b64decode(golden["bundle"]["base64"], validate=True)
online = QueueTransport(
response(golden["projection"]), IntelligenceResponse(200, {}, archive)
)
sdk = golden_client(tmp_path, online)
sdk.skills.get(golden["identity"]["learningContainerId"])
conflict = golden["errors"]["canonicalConflict"]
denied = golden_client(
tmp_path,
QueueTransport(response(conflict["body"], status=conflict["status"])),
)
with pytest.raises(IntelligenceError) as raised:
denied.skills.get(golden["identity"]["learningContainerId"])
assert raised.value.code == conflict["body"]["error"]["code"]
assert raised.value.category == conflict["body"]["error"]["category"]
assert raised.value.retryable is False
assert raised.value.request_id == conflict["body"]["requestId"]
assert raised.value.trace_id == conflict["body"]["traceId"]
assert (
denied.skills.get_cached(golden["identity"]["learningContainerId"]).freshness
== golden["expectations"]["explicitCacheFreshness"]
)
def test_shared_golden_etag_304_is_fresh_only_after_cache_verification(tmp_path):
golden = golden_registry_fixture()
archive = base64.b64decode(golden["bundle"]["base64"], validate=True)
golden_client(
tmp_path,
QueueTransport(
response(golden["projection"]), IntelligenceResponse(200, {}, archive)
),
).skills.get(golden["identity"]["learningContainerId"])
transport = QueueTransport(IntelligenceResponse(304, {}, b""))
result = golden_client(tmp_path, transport).skills.get(
golden["identity"]["learningContainerId"]
)
assert result.freshness == golden["expectations"]["validated304Freshness"]
assert (
transport.requests[0].headers["If-None-Match"] == golden["http"]["ifNoneMatch"]
)
def test_shared_golden_denial_invalidates_verified_cache(tmp_path):
golden = golden_registry_fixture()
archive = base64.b64decode(golden["bundle"]["base64"], validate=True)
golden_client(
tmp_path,
QueueTransport(
response(golden["projection"]), IntelligenceResponse(200, {}, archive)
),
).skills.get(golden["identity"]["learningContainerId"])
denial = golden["errors"]["canonicalDenial"]
denied = golden_client(
tmp_path, QueueTransport(response(denial["body"], status=denial["status"]))
)
with pytest.raises(IntelligenceError) as raised:
denied.skills.get(golden["identity"]["learningContainerId"])
assert raised.value.code == denial["body"]["error"]["code"]
assert raised.value.request_id == denial["body"]["requestId"]
with pytest.raises(IntelligenceCacheMissError):
denied.skills.get_cached(golden["identity"]["learningContainerId"])
@pytest.mark.parametrize("scenario_name", ["unknownCode", "malformed"])
def test_shared_golden_noncanonical_errors_fail_loudly(tmp_path, scenario_name):
golden = golden_registry_fixture()
scenario = golden["errors"][scenario_name]
sdk = golden_client(
tmp_path,
QueueTransport(response(scenario["body"], status=scenario["status"])),
)
with pytest.raises(IntelligenceError) as raised:
sdk.skills.get(golden["identity"]["learningContainerId"])
assert raised.value.code == golden["expectations"]["nonCanonicalErrorCode"]
assert raised.value.category == "dependency"
def test_embedded_error_allowlists_equal_the_canonical_conformance_enums():
codes, categories = canonical_error_vocabulary()
assert _ERROR_CODES == frozenset(codes)
assert _ERROR_CATEGORIES == frozenset(categories)
def test_every_canonical_error_code_and_category_round_trips(tmp_path):
codes, categories = canonical_error_vocabulary()
for code in codes:
sdk = client(
tmp_path,
QueueTransport(
response(canonical_error_response(code, "dependency"), status=500)
),
)
with pytest.raises(IntelligenceError) as raised:
sdk.skills.get(CONTAINER)
assert raised.value.code == code
assert raised.value.category == "dependency"
for category in categories:
sdk = client(
tmp_path,
QueueTransport(
response(
canonical_error_response("LEARNING_JOB_LAUNCH_FAILED", category),
status=500,
)
),
)
with pytest.raises(IntelligenceError) as raised:
sdk.skills.get(CONTAINER)
assert raised.value.code == "LEARNING_JOB_LAUNCH_FAILED"
assert raised.value.category == category
@pytest.mark.parametrize(
"code",
[
"LEARNING_CANDIDATE_STALE_PARENT",
"LEARNING_CANDIDATE_SUBJECT_MISMATCH",
"LEARNING_CANDIDATE_GATES_INCOMPLETE",
],
)
def test_obsolete_error_aliases_fail_closed_as_noncanonical(tmp_path, code):
sdk = client(
tmp_path,
QueueTransport(
response(canonical_error_response(code, "conflict"), status=500)
),
)
with pytest.raises(IntelligenceError) as raised:
sdk.skills.get(CONTAINER)
assert raised.value.code == "LEARNING_REGISTRY_UNRECOVERABLE"
assert raised.value.category == "dependency"
def bundle(files=None, *, symlink=None):
output = io.BytesIO()
with zipfile.ZipFile(output, "w") as archive:
for name, contents in (files or {"root/SKILL.md": "# Skill"}).items():
archive.writestr(name, contents)
if symlink:
info = zipfile.ZipInfo(symlink)
info.create_system = 3
info.external_attr = (stat.S_IFLNK | 0o777) << 16
archive.writestr(info, "root/SKILL.md")
return output.getvalue()
def projection(entries, *, container=CONTAINER, revision="rev-1", revoked=False):
projected = []
for position, (skill_id, version_id, archive) in enumerate(entries):
digest = hashlib.sha256(archive).hexdigest()
with zipfile.ZipFile(io.BytesIO(archive)) as zipped:
manifest_files = []
for member in zipped.infolist():
if member.is_dir():
continue
contents = zipped.read(member)
relative = member.filename.partition("/")[2] or member.filename
manifest_files.append(
{
"path": relative,
"role": (
"instructions" if relative == "SKILL.md" else "resource"
),
"mediaType": "text/markdown",
"byteLength": len(contents),
"rawSha256": hashlib.sha256(contents).hexdigest(),
}
)
manifest_without_hash = {
"manifestVersion": 1,
"agentSkillsProfile": "agentskills:v1",
"files": manifest_files,
"bundleSha256": digest,
"bundleByteLength": len(archive),
"provenance": {},
}
manifest = {
**manifest_without_hash,
"manifestSha256": hashlib.sha256(
json.dumps(
manifest_without_hash,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
).encode()
).hexdigest(),
}
projected.append(
{
"skillId": skill_id,
"versionId": version_id,
"position": position,
"name": f"Skill {position}",
"description": None,
"bundleLocator": {
"schemaVersion": 1,
"backendId": "primary",
"provider": "awsS3",
"resource": "skill-bundles",
"key": f"objects/{skill_id}.zip",
"providerVersion": None,
"etag": None,
"applicationSha256": digest,
"providerChecksum": None,
"byteLength": len(archive),
"contentType": "application/zip",
},
"bundleSha256": digest,
"manifestSha256": manifest["manifestSha256"],
"bundleByteLength": len(archive),
"approvalMethod": "manual",
"manifest": manifest,
"downloadUrl": f"/bundles/{skill_id}/{version_id}",
"testBundleBase64": base64.b64encode(archive).decode(),
}
)
return {
"schemaVersion": 1,
"learningContainerId": container,
"registryRevision": revision,
"skillSetHash": hashlib.sha256(
b"".join(archive for _, _, archive in entries)
+ (b"revoked" if revoked else b"active")
).hexdigest(),
"etag": revision,
"entries": projected,
"publishedAt": "2026-07-16T18:00:00.000Z",
"revoked": revoked,
}
class QueueTransport:
def __init__(self, *responses):
self.responses = list(responses)
self.requests = []
self.lock = threading.Lock()
self.bundles = {}
def __call__(self, request: IntelligenceRequest):
with self.lock:
self.requests.append(request)
if "/bundles/" in request.url and request.url in self.bundles:
return IntelligenceResponse(200, {}, self.bundles[request.url])
response = self.responses.pop(0)
if isinstance(response, Exception):
raise response
if isinstance(response, IntelligenceResponse) and response.status == 200:
try:
payload = json.loads(response.body)
except (UnicodeError, json.JSONDecodeError):
payload = None
if isinstance(payload, dict) and isinstance(payload.get("entries"), list):
for entry in payload["entries"]:
if isinstance(entry, dict) and "testBundleBase64" in entry:
self.bundles[
"https://registry.example" + entry["downloadUrl"]
] = base64.b64decode(entry["testBundleBase64"], validate=True)
return response
def response(payload, status=200, headers=None):
return IntelligenceResponse(
status=status,
headers=headers or {},
body=json.dumps(payload).encode() if payload is not None else b"",
)
def client(tmp_path, transport, **kwargs):
return CopilotKitIntelligence(
api_key="secret",
project_namespace="acme/course",
base_url="https://registry.example",
cache_dir=tmp_path,
transport=transport,
**kwargs,
)
def test_sync_get_authenticates_projects_and_materializes_ordered_skills(tmp_path):
payload = projection(
[(INTRO, VERSION_ONE, bundle()), (QUIZ, VERSION_TWO, bundle())]
)
transport = QueueTransport(response(payload, headers={"ETag": '"rev-1"'}))
result = client(tmp_path, transport).skills.get(CONTAINER)
assert transport.requests[0].url == (
f"https://registry.example/v1/learning-containers/{CONTAINER}/skills"
)
assert transport.requests[0].headers["Authorization"] == "Bearer secret"
assert (
transport.requests[0].headers["X-CopilotKit-Project-Namespace"] == "acme/course"
)
assert [skill.skill_id for skill in result.skills] == [INTRO, QUIZ]
assert [skill.position for skill in result.skills] == [0, 1]
assert result.freshness == "fresh"
assert (result.path / f"skills/000000-{INTRO}/root/SKILL.md").is_file()
assert (result.path / f"skills/000001-{QUIZ}/root/SKILL.md").is_file()
namespace = hashlib.sha256(b"acme/course").hexdigest()
assert (
result.path
== tmp_path / "v1" / namespace / CONTAINER / "sets" / payload["skillSetHash"]
)
assert json.loads((result.path / ".copilotkit-skill-set.json").read_text())[
"entries"
]
assert (
json.loads(
(result.path.parent.parent / ".copilotkit-current.json").read_text()
)["skillSetHash"]
== payload["skillSetHash"]
)
@pytest.mark.asyncio
async def test_async_get_has_transport_parity_and_does_not_block_loop(tmp_path):
archive = bundle()
payload = projection([(INTRO, VERSION_ONE, archive)])
def slow_transport(request):
time.sleep(0.08)
return (
IntelligenceResponse(200, {}, archive)
if "/bundles/" in request.url
else response(payload)
)
sdk = AsyncCopilotKitIntelligence(
api_key="secret",
project_namespace="acme/course",
cache_dir=tmp_path,
transport=slow_transport,
)
ticks = 0
async def ticker():
nonlocal ticks
while ticks < 3:
await asyncio.sleep(0.01)
ticks += 1
result, _ = await asyncio.gather(sdk.skills.get(CONTAINER), ticker())
assert ticks == 3
assert result.skills[0].skill_id == INTRO
@pytest.mark.asyncio
async def test_async_client_accepts_an_async_authenticated_transport(tmp_path):
archive = bundle()
payload = projection([(INTRO, VERSION_ONE, archive)])
requests = []
async def async_transport(request):
requests.append(request)
await asyncio.sleep(0)
return (
IntelligenceResponse(200, {}, archive)
if "/bundles/" in request.url
else response(payload)
)
sdk = AsyncCopilotKitIntelligence(
api_key="secret",
project_namespace="acme/course",
cache_dir=tmp_path,
transport=async_transport,
skills_path="/custom/{learning_container_id}",
)
result = await sdk.skills.get(CONTAINER)
assert result.skills[0].skill_id == INTRO
assert requests[0].url.endswith(f"/custom/{CONTAINER}")
assert requests[0].headers["Authorization"] == "Bearer secret"
cached = await sdk.skills.get_cached(CONTAINER)
assert cached.path == result.path
assert cached.freshness == "cached"
def test_conditional_304_requires_complete_verified_cache(tmp_path):
payload = projection([(INTRO, VERSION_ONE, bundle())])
first = QueueTransport(response(payload))
sdk = client(tmp_path, first)
original = sdk.skills.get(CONTAINER)
second = QueueTransport(IntelligenceResponse(304, {}, b""))
result = client(tmp_path, second).skills.get(CONTAINER)
assert second.requests[0].headers["If-None-Match"] == "rev-1"
assert result.path == original.path
assert result.freshness == "fresh"
def test_corrupt_cache_followed_by_304_forces_unconditional_refetch(tmp_path):
payload = projection([(INTRO, VERSION_ONE, bundle())])
sdk = client(tmp_path, QueueTransport(response(payload)))
cached = sdk.skills.get(CONTAINER)
(cached.path / f"skills/000000-{INTRO}/root/SKILL.md").write_text("corrupt")
transport = QueueTransport(IntelligenceResponse(304, {}, b""), response(payload))
repaired = client(tmp_path, transport).skills.get(CONTAINER)
assert "If-None-Match" in transport.requests[0].headers
assert "If-None-Match" not in transport.requests[1].headers
assert (
repaired.path / f"skills/000000-{INTRO}/root/SKILL.md"
).read_text() == "# Skill"
@pytest.mark.parametrize(
("status", "error"),
[(401, IntelligenceAccessDeniedError), (403, IntelligenceAccessDeniedError)],
)
def test_denials_raise_canonical_errors_and_block_old_cache(tmp_path, status, error):
payload = projection([(INTRO, VERSION_ONE, bundle())])
client(tmp_path, QueueTransport(response(payload))).skills.get(CONTAINER)
canonical = golden_registry_fixture()["errors"]["canonicalDenial"]["body"]
denied = client(tmp_path, QueueTransport(response(canonical, status=status)))
with pytest.raises(error):
denied.skills.get(CONTAINER)
with pytest.raises(IntelligenceCacheMissError):
denied.skills.get_cached(CONTAINER)
def test_transient_get_never_falls_back_but_cached_access_is_explicit(tmp_path):
payload = projection([(INTRO, VERSION_ONE, bundle())])
client(tmp_path, QueueTransport(response(payload))).skills.get(CONTAINER)
failing = client(tmp_path, QueueTransport(OSError("offline")))
with pytest.raises(IntelligenceUnavailableError):
failing.skills.get(CONTAINER)
assert failing.skills.get_cached(CONTAINER).freshness == "cached"
def test_malformed_success_does_not_invalidate_previous_verified_cache(tmp_path):
valid = projection([(INTRO, VERSION_ONE, bundle())])
client(tmp_path, QueueTransport(response(valid))).skills.get(CONTAINER)
malformed = projection([(INTRO, VERSION_ONE, bundle())])
malformed["skillSetHash"] = "F" * 64
sdk = client(tmp_path, QueueTransport(response(malformed)))
with pytest.raises(IntelligenceIntegrityError):
sdk.skills.get(CONTAINER)
assert sdk.skills.get_cached(CONTAINER).freshness == "cached"
def test_empty_and_revoked_sets_are_valid_and_replace_current(tmp_path):
empty = projection([], revision="rev-empty")
empty_result = client(tmp_path, QueueTransport(response(empty))).skills.get(
CONTAINER
)
assert empty_result.skills == ()
revoked = projection([], revision="rev-revoked", revoked=True)
revoked_result = client(tmp_path, QueueTransport(response(revoked))).skills.get(
CONTAINER
)
assert revoked_result.skills == ()
assert revoked_result.revoked is True
assert (
client(tmp_path, QueueTransport()).skills.get_cached(CONTAINER).revoked is True
)
def test_registry_revision_is_excluded_from_shared_set_cache_key(tmp_path):
archive = bundle()
first = projection([(INTRO, VERSION_ONE, archive)], revision="rev-1")
second = projection([(INTRO, VERSION_ONE, archive)], revision="rev-2")
sdk = client(tmp_path, QueueTransport(response(first), response(second)))
first_result = sdk.skills.get(CONTAINER)
second_result = sdk.skills.get(CONTAINER)
assert first_result.path == second_result.path
assert second_result.registry_revision == "rev-2"
assert sdk.skills.get_cached(CONTAINER).registry_revision == "rev-2"
def test_reused_set_hash_must_match_immutable_projected_skill_content(tmp_path):
archive = bundle()
first = projection([(INTRO, VERSION_ONE, archive)], revision="rev-1")
second = projection([(INTRO, VERSION_TWO, archive)], revision="rev-2")
second["skillSetHash"] = first["skillSetHash"]
sdk = client(tmp_path, QueueTransport(response(first), response(second)))
sdk.skills.get(CONTAINER)
replaced = sdk.skills.get(CONTAINER)
assert replaced.skills[0].version == VERSION_TWO
@pytest.mark.parametrize(
"bad_files",
[
{"../SKILL.md": "x"},
{"/root/SKILL.md": "x"},
{"root\\SKILL.md": "x"},
{"a/SKILL.md": "x", "b/file": "x"},
{"root/SKILL.md": "x", "root/readme": "x", "root/README": "x"},
{
"root/SKILL.md": "x",
"root/" + unicodedata.normalize("NFD", "café"): "x",
"root/café": "x",
},
],
)
def test_zip_path_root_case_and_unicode_violations_fail_loudly(tmp_path, bad_files):
with pytest.raises(IntelligenceIntegrityError):
transport = QueueTransport(
response(projection([(BAD, VERSION_ONE, bundle(bad_files))]))
)
client(tmp_path, transport).skills.get(CONTAINER)
@pytest.mark.parametrize(
("left", "right"),
[
("references/Straße.txt", "references/STRASSE.txt"),
("references/İ.txt", "references/i̇.txt"),
("file.md", "file.md"),
("SKILL2.md", "skill2.md"),
],
)
def test_unicode_default_case_fold_path_collisions_are_rejected(tmp_path, left, right):
"""Every one of these pairs is declared invalid by the conformance corpus."""
archive = bundle(
{"root/SKILL.md": "# Skill", f"root/{left}": "a", f"root/{right}": "b"}
)
payload = projection([(INTRO, VERSION_ONE, archive)])
with pytest.raises(IntelligenceIntegrityError) as raised:
client(tmp_path, QueueTransport(response(payload))).skills.get(CONTAINER)
assert raised.value.code == "LEARNING_BLOB_INTEGRITY_FAILURE"
assert raised.value.category == "validation"
@pytest.mark.parametrize(
("name", "files", "compression", "message"),
[
(
"per-file byte bound",
{"root/SKILL.md": "# Skill", "root/big.bin": "A" * 2_048},
zipfile.ZIP_STORED,
"Bundle file exceeds the configured byte limit",
),
(
"file count bound",
{"root/SKILL.md": "# Skill", **{f"root/f{n}.txt": "x" for n in range(8)}},
zipfile.ZIP_STORED,
"Invalid or oversized ZIP directory",
),
(
"compression allowlist",
{"root/SKILL.md": "# Skill"},
zipfile.ZIP_LZMA,
"Unsupported ZIP compression method",
),
(
"path length bound",
{"root/SKILL.md": "# Skill", "root/" + "d/" * 40 + "x.txt": "z"},
zipfile.ZIP_STORED,
"Unsafe artifact path",
),
],
)
def test_zip_bounds_match_the_typescript_and_dotnet_sdks(
tmp_path, name, files, compression, message
):
"""Python enforced only the total-archive bounds; TS and C# also bound
per-file bytes, file count, compression method, and path length."""
output = io.BytesIO()
with zipfile.ZipFile(output, "w", compression) as archive:
for member, contents in files.items():
archive.writestr(member, contents)
payload = projection([(INTRO, VERSION_ONE, output.getvalue())])
with pytest.raises(IntelligenceIntegrityError, match=message) as raised:
client(
tmp_path,
QueueTransport(response(payload)),
max_files=4,
max_file_bytes=1_024,
max_path_length=64,
).skills.get(CONTAINER)
assert raised.value.code == "LEARNING_BLOB_INTEGRITY_FAILURE"
assert name
def test_default_zip_bounds_equal_the_shared_v1_limits(tmp_path):
"""The defaults, not just explicit overrides, must match TypeScript and C#."""
sdk = client(tmp_path, QueueTransport())
assert sdk.skills._max_archive_bytes == 50 * 1024 * 1024
assert sdk.skills._max_uncompressed_bytes == 100 * 1024 * 1024
assert sdk.skills._max_files == 1_000
assert sdk.skills._max_file_bytes == 10 * 1024 * 1024
assert sdk.skills._max_path_length == 512
def test_second_unconditional_304_uses_the_shared_golden_code(tmp_path):
golden = golden_registry_fixture()
payload = projection([(INTRO, VERSION_ONE, bundle())])
installed = client(tmp_path, QueueTransport(response(payload))).skills.get(
CONTAINER
)
shutil.rmtree(installed.path)
transport = QueueTransport(
IntelligenceResponse(304, {}, b""), IntelligenceResponse(304, {}, b"")
)
with pytest.raises(IntelligenceError) as raised:
client(tmp_path, transport).skills.get(CONTAINER)
assert raised.value.code == golden["expectations"]["secondUnconditional304Code"]
assert raised.value.category == "internal"
assert len(transport.requests) == 2
assert "If-None-Match" not in transport.requests[1].headers
@pytest.mark.parametrize("scenario", ["canonicalConflict", "canonicalDenial"])
def test_golden_invalidates_cache_field_decides_cached_consumption(tmp_path, scenario):
"""Drives the assertion from errors.*.invalidatesCache instead of restating it."""
golden = golden_registry_fixture()
expected = golden["errors"][scenario]
assert isinstance(expected["invalidatesCache"], bool)
payload = projection([(INTRO, VERSION_ONE, bundle())])
client(tmp_path, QueueTransport(response(payload))).skills.get(CONTAINER)
sdk = client(
tmp_path,
QueueTransport(response(expected["body"], status=expected["status"])),
)
with pytest.raises(IntelligenceError) as raised:
sdk.skills.get(CONTAINER)
assert raised.value.code == expected["body"]["error"]["code"]
if expected["invalidatesCache"]:
with pytest.raises(IntelligenceCacheMissError):
sdk.skills.get_cached(CONTAINER)
else:
assert (
sdk.skills.get_cached(CONTAINER).freshness
== golden["expectations"]["explicitCacheFreshness"]
)
def test_zip_links_missing_skill_md_and_archive_bounds_are_rejected(tmp_path):
cases = [
bundle({"root/file.txt": "x"}),
bundle({"root/SKILL.md": "x"}, symlink="root/link"),
bundle({"root/SKILL.md": "x", **{f"root/{n}": "x" for n in range(5)}}),
bundle({"root/SKILL.md": "x" * 64}),
]
for archive in cases:
with pytest.raises(IntelligenceIntegrityError):
client(
tmp_path,
QueueTransport(response(projection([(BAD, VERSION_ONE, archive)]))),
max_archive_entries=4,
max_uncompressed_bytes=32,
).skills.get(CONTAINER)
@pytest.mark.parametrize(
"mutation", ["hash", "length", "order", "manifest", "uppercase-hash"]
)
def test_hash_length_order_manifest_and_projection_are_verified(tmp_path, mutation):
payload = projection(
[(INTRO, VERSION_ONE, bundle()), (QUIZ, VERSION_ONE, bundle())]
)
if mutation == "hash":
payload["entries"][0]["bundleSha256"] = "0" * 64
elif mutation == "length":
payload["entries"][0]["bundleByteLength"] += 1
elif mutation == "order":
payload["entries"][1]["position"] = 0
elif mutation == "manifest":
payload["entries"][0]["manifest"]["files"][0]["rawSha256"] = "0" * 64
else:
payload["skillSetHash"] = "F" * 64
with pytest.raises(IntelligenceIntegrityError):
client(tmp_path, QueueTransport(response(payload))).skills.get(CONTAINER)
def test_missing_canonical_bundle_locator_and_legacy_wire_aliases_are_rejected(
tmp_path,
):
missing = projection([(INTRO, VERSION_ONE, bundle())])
del missing["entries"][0]["bundleLocator"]
legacy = projection([(INTRO, VERSION_ONE, bundle())])
entry = legacy["entries"][0]
entry["version"] = entry.pop("versionId")
entry["bundle"] = {"data": entry.pop("testBundleBase64")}
for payload in (missing, legacy):
with pytest.raises(IntelligenceIntegrityError):
client(tmp_path, QueueTransport(response(payload))).skills.get(CONTAINER)
def test_atomic_race_reuses_fully_validated_winner(tmp_path):
payload = projection([(INTRO, VERSION_ONE, bundle())])
transport = QueueTransport(response(payload), response(payload))
sdk = client(tmp_path, transport)
with ThreadPoolExecutor(max_workers=2) as pool:
results = list(pool.map(lambda _: sdk.skills.get(CONTAINER), range(2)))
assert results[0].path == results[1].path
assert not list(results[0].path.parent.glob("*.staging-*"))
assert (results[0].path / f"skills/000000-{INTRO}/root/SKILL.md").is_file()