diff --git a/packages/client/src/launchdarkly_ai_server/skills.py b/packages/client/src/launchdarkly_ai_server/skills.py index f4b18afc..378c9d28 100644 --- a/packages/client/src/launchdarkly_ai_server/skills.py +++ b/packages/client/src/launchdarkly_ai_server/skills.py @@ -279,8 +279,8 @@ async def get_skills(refs: Sequence[SkillReference | str]) -> list[Skill]: Returns: The skills found, in input order. Entries that are missing, at the - wrong version, or fail verification are omitted, and a warning logs how - many. + wrong version, or fail verification are omitted. A warning logs how + many failed verification; misses are not counted. Raises: TypeError: If *refs* is a single string; pass ``[key]`` instead. @@ -295,14 +295,18 @@ async def get_skills(refs: Sequence[SkillReference | str]) -> list[Skill]: store = require_store() - requests = list(refs) skills: list[Skill] = [] - for ref in requests: + # Only what the store served counts toward the summary: a miss or an outage + # is not a verification failure, and the summary would report it as one. + served = 0 + for ref in refs: key, wanted = reference_target(ref) - skill = resolve_from_store(store, key, wanted).skill - if skill is not None: - skills.append(skill) - log_withholding_summary("requested skills", len(requests), len(skills)) + resolution = resolve_from_store(store, key, wanted) + if resolution.skill is not None: + skills.append(resolution.skill) + if resolution.reason in ("ok", "integrity_failure"): + served += 1 + log_withholding_summary("requested skills the store served", served, len(skills)) return skills diff --git a/packages/client/tests/test_skills.py b/packages/client/tests/test_skills.py index fa720992..e16659ad 100644 --- a/packages/client/tests/test_skills.py +++ b/packages/client/tests/test_skills.py @@ -1536,6 +1536,50 @@ async def test_a_key_with_no_usable_version_is_still_withheld( assert await all_skills() == [] assert len([r for r in caplog.records if r.levelname == "WARNING"]) == 1 + async def test_get_skills_for_keys_the_store_does_not_hold_is_silent( + self, store: Any, caplog: pytest.LogCaptureFixture + ) -> None: + """An empty store at boot is not an integrity problem.""" + with caplog.at_level("WARNING", logger="launchdarkly_ai_server.skills_core"): + assert await get_skills(["a", "b"]) == [] + assert [r for r in caplog.records if r.levelname == "WARNING"] == [] + + async def test_a_get_skills_pin_miss_is_silent( + self, store: Any, make_raw_skill: Any, caplog: pytest.LogCaptureFixture + ) -> None: + store.put(make_raw_skill(key="a", version=2)) + with caplog.at_level("WARNING", logger="launchdarkly_ai_server.skills_core"): + assert await get_skills([SkillReference(key="a", version=1)]) == [] + assert [r for r in caplog.records if r.levelname == "WARNING"] == [] + + async def test_a_get_skills_wrong_version_answer_is_not_counted( + self, make_raw_skill: Any, caplog: pytest.LogCaptureFixture + ) -> None: + skills_module._set_store(_WrongVersionAnsweringStore(make_raw_skill)) + with caplog.at_level("WARNING", logger="launchdarkly_ai_server.skills_core"): + assert await get_skills([SkillReference(key="a", version=1)]) == [] + assert [r for r in caplog.records if r.levelname == "WARNING"] == [] + + async def test_a_raising_store_is_not_counted( + self, caplog: pytest.LogCaptureFixture + ) -> None: + skills_module._set_store(_RaisingStore()) + with caplog.at_level("WARNING", logger="launchdarkly_ai_server.skills_core"): + assert await get_skills(["a"]) == [] + assert [r for r in caplog.records if r.levelname == "WARNING"] == [] + + async def test_get_skills_counts_only_what_the_store_served( + self, store: Any, make_raw_skill: Any, caplog: pytest.LogCaptureFixture + ) -> None: + store.put(make_raw_skill(key="good")) + store.put(self._tampered(make_raw_skill, key="bad")) + with caplog.at_level("WARNING", logger="launchdarkly_ai_server.skills_core"): + skills = await get_skills(["good", "bad", "missing"]) + assert [s.key for s in skills] == ["good"] + warnings = [r for r in caplog.records if r.levelname == "WARNING"] + assert len(warnings) == 1 + assert "1 of 2" in warnings[0].getMessage() + async def test_a_fully_resolved_run_is_silent( self, store: Any, make_raw_skill: Any, caplog: pytest.LogCaptureFixture ) -> None: