feat(#62): IMP-46 cache fingerprint forwarding u1~u4 (router kwarg + step12 forward + 8 scenarios)
This commit is contained in:
@@ -86,7 +86,7 @@ def test_router_returns_none_when_route_not_ai_adaptation(monkeypatch):
|
||||
def test_router_returns_cached_when_cache_hit(monkeypatch):
|
||||
monkeypatch.setattr(router_mod.settings, "ai_fallback_enabled", True)
|
||||
cached = _make_proposal()
|
||||
monkeypatch.setattr(router_mod, "read_proposal", lambda key: cached)
|
||||
monkeypatch.setattr(router_mod, "read_proposal", lambda key, **_: cached)
|
||||
client = MagicMock(spec=AiFallbackClient)
|
||||
result = route_ai_fallback(**_call_kwargs(), client=client)
|
||||
assert result is cached
|
||||
@@ -99,16 +99,97 @@ def test_router_validates_cached_proposal(monkeypatch):
|
||||
proposal_kind=ProposalKind.BUILDER_OPTIONS_PATCH,
|
||||
payload={"unknown_key": "x"},
|
||||
)
|
||||
monkeypatch.setattr(router_mod, "read_proposal", lambda key: bad_cached)
|
||||
monkeypatch.setattr(router_mod, "read_proposal", lambda key, **_: bad_cached)
|
||||
client = MagicMock(spec=AiFallbackClient)
|
||||
with pytest.raises(AiFallbackValidationError):
|
||||
route_ai_fallback(**_call_kwargs(), client=client)
|
||||
client.request_proposal.assert_not_called()
|
||||
|
||||
|
||||
def test_router_forwards_fingerprints_and_misses_on_mismatch(monkeypatch):
|
||||
"""Scope: router-level (IMP-46 #62 Axis R u2).
|
||||
|
||||
When caller supplies ``fingerprints`` and the cache layer returns
|
||||
``None`` (simulating a strict-equality mismatch against the stored
|
||||
contract/partial/catalog SHA), the router proceeds to call the
|
||||
client. The exact ``fingerprints`` dict supplied by the caller must
|
||||
appear in the kwargs passed to ``read_proposal``.
|
||||
"""
|
||||
monkeypatch.setattr(router_mod.settings, "ai_fallback_enabled", True)
|
||||
captured: dict = {}
|
||||
|
||||
def _spy_read_proposal(key, *, fingerprints=None):
|
||||
captured["key"] = key
|
||||
captured["fingerprints"] = fingerprints
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(router_mod, "read_proposal", _spy_read_proposal)
|
||||
proposal = _make_proposal()
|
||||
client = MagicMock(spec=AiFallbackClient)
|
||||
client.request_proposal.return_value = proposal
|
||||
supplied = {"contract_sha": "aaa", "partial_sha": "bbb", "catalog_sha": "ccc"}
|
||||
result = route_ai_fallback(
|
||||
**_call_kwargs(), client=client, fingerprints=supplied
|
||||
)
|
||||
assert result is proposal
|
||||
assert captured["fingerprints"] == supplied
|
||||
client.request_proposal.assert_called_once()
|
||||
|
||||
|
||||
def test_router_forwards_fingerprints_and_hits_on_match(monkeypatch):
|
||||
"""Scope: router-level (IMP-46 #62 Axis R u2).
|
||||
|
||||
When caller supplies ``fingerprints`` and the cache layer returns a
|
||||
cached proposal (simulating strict-equality match against stored
|
||||
SHA bundle), the router short-circuits without calling the client.
|
||||
The forwarded kwarg must equal the caller-supplied dict exactly.
|
||||
"""
|
||||
monkeypatch.setattr(router_mod.settings, "ai_fallback_enabled", True)
|
||||
cached = _make_proposal()
|
||||
captured: dict = {}
|
||||
|
||||
def _spy_read_proposal(key, *, fingerprints=None):
|
||||
captured["fingerprints"] = fingerprints
|
||||
return cached
|
||||
|
||||
monkeypatch.setattr(router_mod, "read_proposal", _spy_read_proposal)
|
||||
client = MagicMock(spec=AiFallbackClient)
|
||||
supplied = {"contract_sha": "xxx", "partial_sha": "yyy", "catalog_sha": "zzz"}
|
||||
result = route_ai_fallback(
|
||||
**_call_kwargs(), client=client, fingerprints=supplied
|
||||
)
|
||||
assert result is cached
|
||||
assert captured["fingerprints"] == supplied
|
||||
client.request_proposal.assert_not_called()
|
||||
|
||||
|
||||
def test_router_forwards_fingerprints_none_for_legacy_callers(monkeypatch):
|
||||
"""Scope: router-level (IMP-46 #62 Axis R u2).
|
||||
|
||||
Legacy callers that omit the ``fingerprints`` kwarg must result in
|
||||
``read_proposal`` being invoked with ``fingerprints=None`` (cache
|
||||
layer skips fingerprint comparison — legacy no-invalidation
|
||||
behaviour preserved).
|
||||
"""
|
||||
monkeypatch.setattr(router_mod.settings, "ai_fallback_enabled", True)
|
||||
cached = _make_proposal()
|
||||
captured: dict = {}
|
||||
|
||||
def _spy_read_proposal(key, *, fingerprints=None):
|
||||
captured["fingerprints"] = fingerprints
|
||||
return cached
|
||||
|
||||
monkeypatch.setattr(router_mod, "read_proposal", _spy_read_proposal)
|
||||
client = MagicMock(spec=AiFallbackClient)
|
||||
result = route_ai_fallback(**_call_kwargs(), client=client)
|
||||
assert result is cached
|
||||
assert captured["fingerprints"] is None
|
||||
client.request_proposal.assert_not_called()
|
||||
|
||||
|
||||
def test_router_calls_client_and_returns_validated_proposal(monkeypatch):
|
||||
monkeypatch.setattr(router_mod.settings, "ai_fallback_enabled", True)
|
||||
monkeypatch.setattr(router_mod, "read_proposal", lambda key: None)
|
||||
monkeypatch.setattr(router_mod, "read_proposal", lambda key, **_: None)
|
||||
proposal = _make_proposal()
|
||||
client = MagicMock(spec=AiFallbackClient)
|
||||
client.request_proposal.return_value = proposal
|
||||
@@ -121,7 +202,7 @@ def test_router_calls_client_and_returns_validated_proposal(monkeypatch):
|
||||
|
||||
def test_router_propagates_validation_error(monkeypatch):
|
||||
monkeypatch.setattr(router_mod.settings, "ai_fallback_enabled", True)
|
||||
monkeypatch.setattr(router_mod, "read_proposal", lambda key: None)
|
||||
monkeypatch.setattr(router_mod, "read_proposal", lambda key, **_: None)
|
||||
bad = AiFallbackProposal(
|
||||
proposal_kind=ProposalKind.BUILDER_OPTIONS_PATCH,
|
||||
payload={"unknown_key": "x"},
|
||||
@@ -134,7 +215,7 @@ def test_router_propagates_validation_error(monkeypatch):
|
||||
|
||||
def test_router_propagates_budget_exceeded(monkeypatch):
|
||||
monkeypatch.setattr(router_mod.settings, "ai_fallback_enabled", True)
|
||||
monkeypatch.setattr(router_mod, "read_proposal", lambda key: None)
|
||||
monkeypatch.setattr(router_mod, "read_proposal", lambda key, **_: None)
|
||||
client = MagicMock(spec=AiFallbackClient)
|
||||
client.request_proposal.side_effect = AiFallbackBudgetExceeded("over")
|
||||
with pytest.raises(AiFallbackBudgetExceeded):
|
||||
@@ -143,7 +224,7 @@ def test_router_propagates_budget_exceeded(monkeypatch):
|
||||
|
||||
def test_router_propagates_circuit_open(monkeypatch):
|
||||
monkeypatch.setattr(router_mod.settings, "ai_fallback_enabled", True)
|
||||
monkeypatch.setattr(router_mod, "read_proposal", lambda key: None)
|
||||
monkeypatch.setattr(router_mod, "read_proposal", lambda key, **_: None)
|
||||
client = MagicMock(spec=AiFallbackClient)
|
||||
client.request_proposal.side_effect = AiFallbackCircuitOpen("tripped")
|
||||
with pytest.raises(AiFallbackCircuitOpen):
|
||||
|
||||
Reference in New Issue
Block a user