shopstack / tests /test_command_surface.py
pranaysuyash's picture
Sync ShopStack HEAD 6f8adfc
d999bba verified
Raw
History Blame Contribute Delete
7.18 kB
"""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?"