Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions tests/unit/protocol/fixtures/cache-keys.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
{
"version": "1.0.0",
"generator": "cachekit-py v0.12.0",
"ci_verification": "Vendored (sha256-pinned) and byte-verified against CacheKeyGenerator in cachekit-py default CI: tests/unit/protocol/test_cache_key_vectors.py",
"note": "Keys include __main__ module because vectors were generated at top level. Cross-SDK implementations should substitute their own module path \u2014 only the args hash portion must match.",
"key_format": "ns:{namespace}:func:{module}.{qualname}:args:{blake2b_256_hex}:{ic_flag}{serializer_code}",
"hash_algorithm": "blake2b-256 of msgpack([normalized_args, sorted_kwargs])",
"vectors": [
{
"name": "single_integer",
"namespace": "test",
"function_module": "__main__",
"function_qualname": "get_user",
"args": [
42
],
"kwargs": {},
"integrity_checking": true,
"serializer_type": "std",
"expected_key": "ns:test:func:__main__.get_user:args:3870b2ea5735ae639ded9450ef117768db676f037bec636503796c5b81095153:1s"
},
{
"name": "single_string",
"namespace": "test",
"function_module": "__main__",
"function_qualname": "get_user",
"args": [
"hello"
],
"kwargs": {},
"integrity_checking": true,
"serializer_type": "std",
"expected_key": "ns:test:func:__main__.get_user:args:07ed6e7b87ff98f70efe4f4a8f082fa79415b9f670be68f3ada1f42244b5b6de:1s"
},
{
"name": "multiple_args",
"namespace": "test",
"function_module": "__main__",
"function_qualname": "process",
"args": [
1,
"two",
3.0
],
"kwargs": {},
"integrity_checking": true,
"serializer_type": "std",
"expected_key": "ns:test:func:__main__.process:args:465e6bc8edd493c64748a0c405106db14d2bb481989bf1849481f1343982e179:1s"
},
{
"name": "kwargs_only",
"namespace": "test",
"function_module": "__main__",
"function_qualname": "get_user",
"args": [],
"kwargs": {
"user_id": 42,
"include_profile": true
},
"integrity_checking": true,
"serializer_type": "std",
"expected_key": "ns:test:func:__main__.get_user:args:14570849f0d99524f4149319d37fa0c298278b8f1724c12d6cb7631efaf86f0e:1s"
},
{
"name": "mixed_args_kwargs",
"namespace": "test",
"function_module": "__main__",
"function_qualname": "get_user",
"args": [
"alice"
],
"kwargs": {
"age": 30
},
"integrity_checking": true,
"serializer_type": "std",
"expected_key": "ns:test:func:__main__.get_user:args:573b0961d0bf4e7207c6628a3f9e42c97a0cd01e276d8904ec5af6c877cd599e:1s"
},
{
"name": "no_namespace",
"namespace": null,
"function_module": "__main__",
"function_qualname": "get_user",
"args": [
1
],
"kwargs": {},
"integrity_checking": true,
"serializer_type": "std",
"expected_key": "func:__main__.get_user:args:386979f533ce537f0c42d385c8174948ebd58566ad81b32bebb830a187cb4387:1s"
},
{
"name": "empty_args",
"namespace": "test",
"function_module": "__main__",
"function_qualname": "get_user",
"args": [],
"kwargs": {},
"integrity_checking": true,
"serializer_type": "std",
"expected_key": "ns:test:func:__main__.get_user:args:f9cf3864b6e929eb73f84cf6d69409e0bd7575f8cf6feafe3a543b0f7267b2b2:1s"
},
{
"name": "none_arg",
"namespace": "test",
"function_module": "__main__",
"function_qualname": "get_user",
"args": [
null
],
"kwargs": {},
"integrity_checking": true,
"serializer_type": "std",
"expected_key": "ns:test:func:__main__.get_user:args:073b96f1817ee2b2a26b6cee56401999e4dc2d8836d596927d0aad3e338c2f0b:1s"
},
{
"name": "boolean_args",
"namespace": "test",
"function_module": "__main__",
"function_qualname": "process",
"args": [
true,
false
],
"kwargs": {},
"integrity_checking": true,
"serializer_type": "std",
"expected_key": "ns:test:func:__main__.process:args:982385ac5333a35e2d5d68638aafe47ba31d36c1f3cd60d0ed57396e3c638334:1s"
},
{
"name": "nested_dict",
"namespace": "test",
"function_module": "__main__",
"function_qualname": "get_user",
"args": [
{
"user": {
"name": "alice",
"ids": [
1,
2,
3
]
}
}
],
"kwargs": {},
"integrity_checking": true,
"serializer_type": "std",
"expected_key": "ns:test:func:__main__.get_user:args:14a3433f54d70b4cc452c3a140f8c3a0979acb3dc2baf7238dc4e6675265f3a8:1s"
}
]
}
77 changes: 77 additions & 0 deletions tests/unit/protocol/test_cache_key_vectors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Byte-verification of auto-mode cache keys against the protocol test vectors.

Fixture: tests/unit/protocol/fixtures/cache-keys.json, vendored from
cachekit-io/protocol @ f0672c1cf2a3bbd2ba3f4760b6d1406a4357aab9
(sha256 4a0ae13dfa745a1a2505f2a10148db7d55f23f9b01636000cf477e805b86450e).
Regenerate ONLY by re-copying from the protocol repo — never by hand.

Each vector pins the full 7-segment auto-mode key
(``ns:{ns}:func:{module}.{qualname}:args:{blake2b_256_hex}:{ic_flag}{code}``)
for a given (args, kwargs, namespace, integrity_checking, serializer_type)
tuple. The vectors were generated at top level, so the module path is
``__main__`` — reproduced here by stubbing ``__module__``/``__qualname__``
on a throwaway function, which lets the test assert the FULL key, not just
the args-hash segment.

A failure here is a key-stability break to triage, never a fixture to
silently regenerate: a changed auto-mode key orphans every existing cache
entry (silent 100% miss storm, billed as misses under metered pricing).
"""

from __future__ import annotations

import hashlib
import json
from pathlib import Path
from typing import Any

import pytest

from cachekit.key_generator import CacheKeyGenerator

FIXTURE_PATH = Path(__file__).parent / "fixtures" / "cache-keys.json"
FIXTURE_SHA256 = "4a0ae13dfa745a1a2505f2a10148db7d55f23f9b01636000cf477e805b86450e"

VECTORS = json.loads(FIXTURE_PATH.read_text(encoding="utf-8"))

# Part of the conformance claim: a fixture update that adds or removes
# vectors must be a conscious change, not a silent drift.
EXPECTED_VECTOR_COUNT = 10


def test_fixture_integrity():
"""The vendored fixture is byte-identical to the pinned protocol revision."""
digest = hashlib.sha256(FIXTURE_PATH.read_bytes()).hexdigest()
assert digest == FIXTURE_SHA256, (
f"fixtures/cache-keys.json sha256 {digest} != pinned {FIXTURE_SHA256}. "
"If the protocol vectors were intentionally updated, refresh the pin AND the count."
)


def test_vector_count():
assert len(VECTORS["vectors"]) == EXPECTED_VECTOR_COUNT


@pytest.mark.parametrize("vector", VECTORS["vectors"], ids=lambda v: v["name"])
def test_cache_key_vectors(vector: dict[str, Any]):
"""CacheKeyGenerator reproduces every pinned auto-mode key byte-for-byte."""

def stub() -> None: # pragma: no cover - identity carrier, never called
pass

stub.__module__ = vector["function_module"]
stub.__qualname__ = vector["function_qualname"]

key = CacheKeyGenerator().generate_key(
stub,
tuple(vector["args"]),
vector["kwargs"],
namespace=vector["namespace"],
integrity_checking=vector["integrity_checking"],
serializer_type=vector["serializer_type"],
)
assert key == vector["expected_key"], (
f"auto-mode key drift for vector {vector['name']!r} — this breaks key "
"stability for every deployed cache entry; triage the generator change, "
"do NOT regenerate the vectors."
)
Loading