Spaces:
Running
Running
File size: 7,179 Bytes
d999bba | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | """Regression tests for the command surface (2026-06-15).
The command surface merges the legacy "Quick add" + "Ask ShopStack"
inputs into a single typed input. The user types one of:
* "add milk" β add_to_list
* "i bought rice" β log_purchase
* "we have onion" β add_stock
* "we finished bread" β mark_consumed
* anything else β ask (free-form Ask ShopStack)
These tests pin the intent parser contract so the routes don't
silently change.
"""
from __future__ import annotations
from shopstack.services.command_surface import (
COMMON_STAPLE_CHIPS,
CommandIntent,
parse_intent,
_normalise_canonical_name,
)
class TestNormaliseCanonicalName:
"""Helper that converts free-text to a canonical slug."""
def test_simple_lowercase(self):
assert _normalise_canonical_name("Milk") == "milk"
def test_multi_word_underscored(self):
assert _normalise_canonical_name("Wheat Flour") == "wheat_flour"
def test_punctuation_stripped(self):
assert _normalise_canonical_name("Milk (1L)") == "milk_1l"
def test_collapses_whitespace(self):
assert _normalise_canonical_name(" toor dal ") == "toor_dal"
def test_empty_returns_empty(self):
assert _normalise_canonical_name("") == ""
class TestParseIntentAddToList:
def test_simple_add(self):
intent = parse_intent("add milk")
assert intent.action == "add_to_list"
assert intent.canonical_name == "milk"
def test_need_synonym(self):
intent = parse_intent("need bread")
assert intent.action == "add_to_list"
assert intent.canonical_name == "bread"
def test_buy_synonym(self):
intent = parse_intent("buy atta")
assert intent.action == "add_to_list"
assert intent.canonical_name == "wheat_flour" or intent.canonical_name == "atta"
def test_get_synonym(self):
intent = parse_intent("get eggs")
assert intent.action == "add_to_list"
assert intent.canonical_name == "eggs"
def test_quantity_in_name(self):
intent = parse_intent("add 2L milk")
assert intent.action == "add_to_list"
# The quantity is part of the canonical name; downstream
# dispatchers can split it later if needed.
assert "milk" in intent.canonical_name
class TestParseIntentLogPurchase:
def test_i_bought(self):
intent = parse_intent("I bought rice")
assert intent.action == "log_purchase"
assert intent.canonical_name == "rice"
def test_just_bought(self):
intent = parse_intent("just bought bread")
assert intent.action == "log_purchase"
assert intent.canonical_name == "bread"
def test_bought_past_tense(self):
intent = parse_intent("bought onion")
assert intent.action == "log_purchase"
assert intent.canonical_name == "onion"
def test_purchased_synonym(self):
intent = parse_intent("purchased cooking_oil")
assert intent.action == "log_purchase"
assert intent.canonical_name == "cooking_oil"
class TestParseIntentAddStock:
def test_we_have(self):
intent = parse_intent("we have onion")
assert intent.action == "add_stock"
assert intent.canonical_name == "onion"
def test_at_home(self):
intent = parse_intent("at home 2kg rice")
assert intent.action == "add_stock"
assert "rice" in intent.canonical_name
def test_stocked_synonym(self):
intent = parse_intent("stocked up on dal")
assert intent.action == "add_stock"
assert "dal" in intent.canonical_name
class TestParseIntentMarkConsumed:
def test_we_finished(self):
intent = parse_intent("we finished bread")
assert intent.action == "mark_consumed"
assert intent.canonical_name == "bread"
def test_consume_synonym(self):
intent = parse_intent("consume milk")
assert intent.action == "mark_consumed"
assert intent.canonical_name == "milk"
def test_ate(self):
intent = parse_intent("ate tomato")
assert intent.action == "mark_consumed"
assert intent.canonical_name == "tomato"
class TestParseIntentAskFallback:
"""If nothing matches, fall through to 'ask' (free-form)."""
def test_question_mark(self):
intent = parse_intent("do we have milk?")
assert intent.action == "ask"
assert intent.canonical_name == ""
assert intent.raw == "do we have milk?"
def test_long_sentence(self):
intent = parse_intent("what did we buy last week?")
assert intent.action == "ask"
assert intent.canonical_name == ""
def test_empty(self):
intent = parse_intent("")
assert intent.action == "unknown"
assert intent.canonical_name == ""
def test_whitespace_only(self):
intent = parse_intent(" ")
assert intent.action == "unknown"
assert intent.canonical_name == ""
class TestCommonStapleChips:
"""The chip row shows 10 most common Indian household staples."""
def test_contains_milk(self):
assert "milk" in COMMON_STAPLE_CHIPS
def test_contains_eggs(self):
assert "eggs" in COMMON_STAPLE_CHIPS
def test_contains_rice(self):
assert "rice" in COMMON_STAPLE_CHIPS
def test_size_is_reasonable(self):
# 10 chips is a balance between useful and not overwhelming
assert 5 <= len(COMMON_STAPLE_CHIPS) <= 12
def test_all_canonical_names(self):
for chip in COMMON_STAPLE_CHIPS:
assert chip.replace("_", "").isalnum(), f"chip '{chip}' has unexpected chars"
class TestDispatchHandlerContract:
"""The dispatch function delegates to a registered handler.
These tests use a stub handler (no DB) to verify the routing.
"""
def test_dispatch_routes_to_action_handler(self):
from shopstack.services import command_surface as cs
def add_to_list_handler(canonical_name, *, intent=None):
return cs.CommandResult(
success=True,
action="add_to_list",
canonical_name=canonical_name,
message=f"added: {canonical_name}",
)
def ask_handler(canonical_name, *, intent=None):
return cs.CommandResult(
success=True,
action="ask",
canonical_name=canonical_name,
message=f"asked: {intent.raw if intent else canonical_name}",
)
cs.register_handler("add_to_list", add_to_list_handler)
cs.register_handler("ask", ask_handler)
# "add milk" β add_to_list handler
intent = parse_intent("add milk")
result = cs.dispatch(intent)
assert result.success
assert result.action == "add_to_list"
assert result.canonical_name == "milk"
# Unmatched text β ask fall-through
intent = parse_intent("what did we buy last week?")
result = cs.dispatch(intent)
assert result.success
assert result.action == "ask"
assert intent.raw == "what did we buy last week?"
|