pyrtan / sourcev0.14.0 · main

pyrtan / tests / test_grow.py

3,981 bytes · last changed 2026-10-25 in Read sentences with rules: rewrites, typed slots, synonyms, typos, pyrtan match

1"""An unanswered sentence becomes a proposed tree, a scaffold that checks,
2and an entry in the unanswered log."""
3
4import pytest
5
6from pyrtan.grow import Proposal, Unanswered, no_match_outcome, propose
7from pyrtan.intent import Trigger
8from pyrtan.tree import check, load_tree_text
9
10
11@pytest.mark.parametrize(
12 "text, name, trigger, inputs",
13 [
14 ("top 3 stories on hacker news", "top-stories-hacker-news", "top {count:int} stories on hacker news", {"count": 3}),
15 ("is example.com up", "host-up", "is {host:host} up", {"host": "example.com"}),
16 ("what's the latest release of astral-sh/uv", "latest-release", "what's the latest release of {target:word}", {"target": "astral-sh/uv"}),
17 ("convert 12.5 km to miles", "convert-km-miles", "convert {value:float} km to miles", {"value": 12.5}),
18 ("turn on the lights", "turn-lights", "turn on the lights", {}),
19 ("Weather in Oslo?", "weather-oslo", "Weather in Oslo", {}),
20 ("ping 1 then 2", "ping-then", "ping {count:int} then {count2:int}", {"count": 1, "count2": 2}),
21 ],
22)
23def test_propose_names_slots_and_matches_the_sentence(text, name, trigger, inputs):
24 p = propose(text)
25 assert (p.name, p.trigger) == (name, trigger)
26 assert Trigger(p.trigger).match(text) == inputs
27 assert set(p.inputs) == set(inputs)
28
29
30def test_propose_survives_trigger_syntax_and_bare_numbers():
31 p = propose("{weird} [tokens] | here")
32 Trigger(p.trigger) # compiles
33 assert "{" not in p.trigger.replace("{count", "")
34 p = propose("42")
35 assert p.trigger == "42" and p.inputs == {}
36 assert propose("").name == "new-tree"
37
38
39def test_given_name_wins():
40 assert propose("my thing", name="custom").name == "custom"
41
42
43@pytest.mark.parametrize("text", ["top 3 stories on hacker news", "is example.com up", "what's the latest release of astral-sh/uv", 'say "hi" to {everyone}', "turn on the lights"])
44def test_scaffold_loads_and_checks_clean(text):
45 p = propose(text)
46 tree = load_tree_text(p.scaffold())
47 assert tree.name == p.name
48 assert [t.pattern for t in tree.triggers] == [p.trigger]
49 assert [x for x in check(tree) if x.level == "error"] == []
50 assert set(tree.inputs) == set(p.inputs)
51
52
53def test_host_slot_opens_hosts_and_builds_the_url():
54 tree = load_tree_text(propose("is example.com up").scaffold())
55 assert tree.hosts == ["*"]
56 assert "inputs.host" in tree.node("fetch").get("http")["url"]
57
58
59def test_command_quotes_for_a_shell():
60 assert propose("top 3 stories on hacker news").command() == 'pyrtan new top-stories-hacker-news --from "top 3 stories on hacker news"'
61 assert propose("what's up").command("trees") == 'pyrtan new up --from "what\'s up" --dir trees'
62 assert propose("costs $5").command().endswith("--from 'costs $5'")
63
64
65def test_no_match_outcome_points_at_the_next_tree():
66 out = no_match_outcome("tell me a joke", "Nothing here")
67 assert out["say"].startswith("Nothing here answers 'tell me a joke'. To write the tree that would: pyrtan new joke --from")
68 assert out["output"]["unanswered"] == "tell me a joke"
69 assert out["output"]["proposal"]["name"] == "joke"
70
71
72def test_unanswered_counts_and_bounds():
73 log = Unanswered(limit=2)
74 first = log.record("Tell me a joke")
75 assert first["count"] == 1 and first["proposal"]["name"] == "joke"
76 assert log.record("tell me a joke!")["count"] == 2 # same sentence, normalised
77 log.record("second")
78 log.record("third")
79 texts = [e["text"] for e in log.to_list()]
80 assert texts == ["third", "second"] # the least recently seen went
81 log = Unanswered()
82 log.record("rare")
83 log.record("common")
84 log.record("common")
85 assert [e["count"] for e in log.to_list()] == [2, 1] # most seen first
86
87
88def test_proposal_round_trips_through_its_dict():
89 p = propose("is example.com up")
90 d = p.to_dict()
91 again = Proposal(d["text"], d["name"], d["trigger"], d["inputs"])
92 assert again.scaffold() == p.scaffold()