srr84/agent-data-layer
0
1"""Unit tests for tool_registry: the closed menu of T1-T6 + name -> ToolSpec lookup.2 3Covers: each of the six tools registers with the exact docs/07 §3 param set (names,4types, declaration order) and its concrete return-schema type + provenance kind; lookup5of each registered name returns its ToolSpec; an unregistered / hallucinated name returns6the typed UnregisteredTool; every registered param type is a member of the M0 closed7ParamType set (no free-form / expression param anywhere — INV-C1); the T4 condition param8uses the TechSpec §3 5-key by_condition set (R-B-1); and the registry's routing keys (tool9names) are distinct and well-defined.10 11```text12spec traceability (audit map — safe to skip)13LLD §1 (M-tool_registry DoD: name -> spec; unknown -> UnregisteredTool), §2/§2a (ToolSpec/ParamSpec; six tools)14TechSpec §3 (the six tool/param schemas; T4 by_condition 5-key set)15FuncSpec FR-1 (registered tools only), FR-6/FR-7 (no raw-query param), FR-8.1 (scalar T1-T3 / set T4-T6)16Charter INV-C1 (closed typed/enum params, no free-form), INV-C3 (typed params) · R-B-1 (T4 condition keys)17```18"""19 20from __future__ import annotations21 22from typing import get_args23 24import pytest25 26from agent_data_layer.contracts.enums import ComplianceFlag, ShelfState27from agent_data_layer.contracts.errors import UnregisteredTool28from agent_data_layer.contracts.tools import (29 ComplianceResult,30 ConditionCountResult,31 ParamType,32 PriceResult,33 ProductListResult,34 StockResult,35 ToolSpec,36 WindowCountResult,37)38from agent_data_layer.registry.tool_registry import (39 TOOL_SPECS,40 get_tool_spec,41 registered_tool_names,42)43 44# The M0 closed ParamType set — the ONLY types a registered param may carry (INV-C1).45_CLOSED_PARAM_TYPES = frozenset(get_args(ParamType))46 47# The TechSpec §3 5-key by_condition set the T4 `condition` param must use (R-B-1).48_T4_CONDITION_KEYS = frozenset(49 {"OUT_OF_STOCK", "LOW_STOCK", "PRICE_MISMATCH", "EXPIRED", "PLANOGRAM_VIOLATION"}50)51 52# The expected per-tool schema, transcribed from docs/07 §3 / docs/05 §2a. Each entry:53# tool name -> (ordered (param_name, param_type) tuples, return_schema class, provenance_kind).54_EXPECTED = {55 "get_stock_state": (56 (("store_id", "StoreId"), ("product_id", "ProductId"), ("as_of", "Timestamp")),57 StockResult,58 "scalar",59 ),60 "get_price": (61 (("store_id", "StoreId"), ("product_id", "ProductId"), ("as_of", "Timestamp")),62 PriceResult,63 "scalar",64 ),65 "get_compliance_flag": (66 (("store_id", "StoreId"), ("product_id", "ProductId"), ("as_of", "Timestamp")),67 ComplianceResult,68 "scalar",69 ),70 "count_stores_with_condition": (71 (("condition", "StoreCondition"), ("as_of", "Timestamp")),72 ConditionCountResult,73 "set",74 ),75 "list_products_in_state": (76 (("store_id", "StoreId"), ("state", "ShelfState"), ("as_of", "Timestamp")),77 ProductListResult,78 "set",79 ),80 "count_events_in_window": (81 (82 ("store_id", "StoreId"),83 ("state", "ShelfState"),84 ("start", "Timestamp"),85 ("end", "Timestamp"),86 ),87 WindowCountResult,88 "set",89 ),90}91 92 93def test_exactly_six_tools_registered() -> None:94 """The closed menu holds exactly the six tools T1-T6 (FR-1, no more/fewer)."""95 assert set(TOOL_SPECS) == set(_EXPECTED)96 assert len(TOOL_SPECS) == 697 98 99@pytest.mark.parametrize("name", list(_EXPECTED))100def test_each_tool_registers_exact_param_set(name: str) -> None:101 """Each tool registers with its exact docs/07 §3 param set: names, types, order."""102 expected_params, expected_return, expected_kind = _EXPECTED[name]103 spec = TOOL_SPECS[name]104 assert spec.name == name105 actual_params = tuple((p.name, p.type) for p in spec.params)106 assert actual_params == expected_params107 assert spec.return_schema is expected_return108 assert spec.provenance_kind == expected_kind109 110 111@pytest.mark.parametrize("name", list(_EXPECTED))112def test_lookup_returns_tool_spec(name: str) -> None:113 """get_tool_spec returns the registered ToolSpec for each registered name."""114 spec = get_tool_spec(name)115 assert isinstance(spec, ToolSpec)116 assert spec is TOOL_SPECS[name]117 118 119@pytest.mark.parametrize(120 "name",121 [122 "get_inventory", # plausible-but-unregistered123 "GET_STOCK_STATE", # wrong case is a different name124 "get_stock_state ", # trailing space125 "", # empty126 "select * from events", # raw-query-shaped string is just an unknown name127 "drop_table",128 ],129)130def test_unregistered_or_hallucinated_name_returns_typed_error(name: str) -> None:131 """An unknown/hallucinated tool name returns the typed UnregisteredTool (never raises,132 never coerced to OutOfCoverage) — LLD §6, FR-1."""133 result = get_tool_spec(name)134 assert isinstance(result, UnregisteredTool)135 assert result.name == name136 137 138def test_every_registered_param_type_is_in_the_closed_set() -> None:139 """INV-C1: every registered param's type is a member of the M0 closed ParamType set —140 a bounded typed scalar or a closed enum; no free-form / expression / predicate param."""141 for spec in TOOL_SPECS.values():142 for param in spec.params:143 assert param.type in _CLOSED_PARAM_TYPES, (spec.name, param.name, param.type)144 145 146def test_no_free_form_param_name_or_open_type() -> None:147 """No registered param is a free-form passthrough: every param has a concrete name and148 a closed type, and no param is named like a raw query/expression slot (FR-6/FR-7)."""149 forbidden_names = {"query", "expression", "predicate", "filter", "raw", "sql"}150 for spec in TOOL_SPECS.values():151 for param in spec.params:152 assert param.name not in forbidden_names, (spec.name, param.name)153 assert param.name # non-empty154 assert param.type # non-empty closed type155 156 157def test_enum_params_carry_enum_domain_scalars_do_not() -> None:158 """An enum param pins enum_domain to its allowed value set; a typed scalar carries159 enum_domain=None (it has a format validator, not an enum membership set) — M0 ParamSpec."""160 enum_types = {"ShelfState", "ComplianceFlag", "StoreCondition"}161 for spec in TOOL_SPECS.values():162 for param in spec.params:163 if param.type in enum_types:164 assert param.enum_domain is not None and len(param.enum_domain) > 0, (165 spec.name,166 param.name,167 )168 else:169 assert param.enum_domain is None, (spec.name, param.name)170 171 172def test_t4_condition_uses_techspec_by_condition_5key_set() -> None:173 """R-B-1: the T4 condition param enum is the TechSpec §3 5-key by_condition set174 (OUT_OF_STOCK|LOW_STOCK|PRICE_MISMATCH|EXPIRED|PLANOGRAM_VIOLATION), consistent with175 the existing event_index + gold — NOT the M0 StoreCondition HAS_* names."""176 t4 = TOOL_SPECS["count_stores_with_condition"]177 (condition_param,) = [p for p in t4.params if p.name == "condition"]178 assert condition_param.enum_domain == _T4_CONDITION_KEYS179 180 181def test_state_enum_domains_match_m0_enums() -> None:182 """The ShelfState / ComplianceFlag param domains equal the M0 enum member-value sets183 (the registry pins the closed domain to the contract enum, not a hand-typed subset)."""184 shelf_domain = frozenset(m.value for m in ShelfState)185 compliance_domain = frozenset(m.value for m in ComplianceFlag)186 for spec in TOOL_SPECS.values():187 for param in spec.params:188 if param.type == "ShelfState":189 assert param.enum_domain == shelf_domain, (spec.name, param.name)190 if param.type == "ComplianceFlag":191 assert param.enum_domain == compliance_domain, (spec.name, param.name)192 193 194def test_routing_keys_are_disjoint_and_well_defined() -> None:195 """The registry's routing keys (tool names) are distinct and exhaustively cover the196 menu: each ToolSpec.name is unique, non-empty, and equals its dict key, so name-based197 routing is unambiguous (one name -> at most one spec). docs/05 specifies no separate198 routing-edge predicate, so disjointness here is the name-key uniqueness contract."""199 names = [spec.name for spec in TOOL_SPECS.values()]200 assert len(names) == len(set(names)) # no duplicate routing key201 assert set(names) == registered_tool_names()202 for key, spec in TOOL_SPECS.items():203 assert key == spec.name # dict key is the canonical routing key204 