pyrtan / sourcev0.14.0 · main

pyrtan / tests / test_examples.py

7,701 bytes · last changed 2026-10-27 in Chat: strangers and bots, Omegle-style, on a hub and across a follow; release 0.10.0

1"""The example trees, end to end, against recorded responses."""
2
3import pytest
4
5from pyrtan.tree import check
6
7
8def test_examples_check_clean(examples):
9 for tree in examples.trees():
10 errors = [p for p in check(tree) if p.level == "error"]
11 assert not errors, (tree.name, errors)
12
13
14def test_top_story(runner):
15 out = runner.run_intent("What's the top story on Hacker News?")
16 assert out.status == "done"
17 assert out.say == "Top of Hacker News: Pyrtan 0.1 — https://example.org/p"
18 assert out.output["stories"][0]["by"] == "alice"
19
20
21def test_top_n_stories_fills_in_self_post_url(runner):
22 out = runner.run_intent("top 3 stories on hacker news")
23 assert out.status == "done"
24 stories = out.output["stories"]
25 assert stories[1]["url"] == "https://news.ycombinator.com/item?id=102"
26 assert stories[2] is None # a comment is not a story: that item failed, the run did not
27 assert any(s.node == "not-a-story" for s in out.trace.steps)
28
29
30def test_count_is_bounded(runner):
31 out = runner.run_intent("top 99 stories on hacker news")
32 assert out.status == "error" and out.reason == "bad-input"
33
34
35def test_item_missing(runner):
36 out = runner.run_intent("hacker news item 999")
37 assert out.status == "failed" and out.reason == "no-such-item"
38
39
40def test_small_talk_is_one_decide_on_what_the_trigger_fixed(runner):
41 for sentence, topic, starts in [("how are you doing?", "how", "Deterministic"), ("Are you human", "who", "A bot"), ("what can you do", "what", "Only what"), ("see ya", "bye", "Bye")]:
42 out = runner.run_intent(sentence)
43 assert out.status == "done" and out.tree == "small-talk" and out.output == {"topic": topic} and out.say.startswith(starts), sentence
44 assert [s.node for s in out.trace.steps if s.kind != "intent"] == ["topic", topic]
45 # A greeting alone is filler: it reads as nothing, and no tree answers it.
46 assert runner.run_intent("hello").reason == "no-match"
47
48
49def test_weather_two_hops(runner):
50 out = runner.run_intent("weather in Oslo")
51 assert out.status == "done"
52 assert out.say == "Oslo, Norway: 4.2°C, raining, humidity 80%, wind 12.3 km/h."
53 assert [s.chosen for s in out.trace.steps if s.kind == "decide"] == ["exists True", "type 'number'", "between [51, 67]"]
54
55
56def test_weather_units_input(runner):
57 out = runner.run_tree("weather", {"place": "Oslo", "units": "fahrenheit"})
58 assert out.status == "done" and "39.6°F, clear" in out.say
59 out = runner.run_tree("weather", {"place": "Oslo", "units": "kelvin"})
60 assert out.status == "error" and out.reason == "bad-input"
61
62
63def test_weather_unknown_place(runner):
64 out = runner.run_intent("how cold is it in Nowhereville")
65 assert out.status == "failed" and out.reason == "unknown-place"
66
67
68def test_site_status_branches(runner):
69 assert runner.run_intent("is Example.com up").status == "done"
70 down = runner.run_intent("is broken.example up")
71 assert down.status == "failed" and down.reason == "server-error"
72 odd = runner.run_intent("check status of teapot.example")
73 assert odd.status == "done" and "refuses" in odd.say
74
75
76def test_site_status_rejects_bad_host(runner):
77 # The {host:host} slot refuses what is not a host name, so the trigger
78 # does not fire at all, and the reading says which trigger refused.
79 out = runner.run_intent("is http://x up")
80 assert out.status == "error" and out.reason == "no-match"
81 assert out.output["reading"]["considered"] == ["is {host:host} up"]
82 out = runner.run_tree("site-status", {"host": "http://x"})
83 assert out.status == "error" and out.reason == "bad-input" # the input's own pattern still guards a direct run
84
85
86def test_github_release(runner):
87 ok = runner.run_intent("latest release of b-i-t-8/pyrtan")
88 assert ok.status == "done" and ok.output["tag"] == "v0.1.0"
89 none = runner.run_intent("latest b-i-t-8/nothing release")
90 assert none.status == "failed" and none.reason == "no-release"
91 busy = runner.run_intent("latest release of b-i-t-8/busy")
92 assert busy.reason == "rate-limited"
93
94
95def test_fixture_fails_loudly_on_unforeseen_request(runner):
96 out = runner.run_intent("weather in Paris")
97 assert out.status == "error" and out.reason == "transport"
98
99
100def test_disk_space_over_a_shell(runner):
101 out = runner.run_intent("how full is the disk")
102 assert out.status == "done" and out.say == "/ is 71% full." and out.output == {"mounted": "/", "percent": 71}
103 assert out.trace.steps[1].summary == "shell df -P / -> exit 0; percent=71, mounted=/"
104 assert runner.run_intent("disk space on /data").reason == "disk-full"
105 failed = runner.run_intent("disk space on /nowhere")
106 assert failed.status == "failed" and failed.reason == "df-failed" and "No such file" in failed.say
107
108
109def test_log_errors_from_a_file(runner):
110 out = runner.run_intent("errors in the log")
111 assert out.status == "done" and out.say.startswith("2 error line(s) in the last 5 of /var/log/syslog; latest: ") and out.output["errors"] == 2
112 assert runner.run_intent("errors in /var/log/quiet.log").say == "No error lines in the last 1 of /var/log/quiet.log."
113 assert runner.run_intent("errors in /var/log/nothing.log").reason == "no-such-file"
114 # The path from the sentence is checked against the tree's paths before anything is read.
115 out = runner.run_intent("errors in /etc/passwd")
116 assert out.status == "error" and out.reason == "path-not-allowed" and [s.kind for s in out.trace.steps] == ["intent"]
117
118
119def test_room_temperature_from_a_queue(runner):
120 assert runner.run_intent("temperature in the kitchen").say == "kitchen: 21.5°C."
121 assert runner.run_intent("how warm is the attic").say == "attic: 31°C, warm."
122 assert runner.run_intent("temperature in the garage").reason == "no-reading"
123 gap = runner.run_intent("temperature in the hall") # a payload that is not JSON: a gap, not a guess
124 assert gap.status == "error" and gap.reason == "gap" and gap.trace.gaps[0]["node"] == "reading"
125 assert runner.run_tree("room-temperature", {"room": "a-b"}).status == "error" # fixture has nothing for it
126
127
128def test_rendered_headline_through_a_browser(runner):
129 out = runner.run_intent("headline of https://app.example/dashboard")
130 assert out.status == "done" and out.say == "Dashboard — Welcome back, Barry" and out.output["heading"] == "Welcome back, Barry"
131 assert out.trace.steps[1].summary == "browser https://app.example/dashboard -> 200; heading=Welcome back, Barry"
132 assert runner.run_intent("what does https://app.example/empty say").reason == "no-heading"
133 assert runner.run_intent("headline of https://app.example/gone").reason == "unexpected-response"
134
135
136def test_modem_check_over_serial(runner):
137 assert runner.run_intent("is the modem up").say == "The modem on /dev/ttyUSB0 is there."
138 silent = runner.run_intent("is the modem on /dev/ttyUSB1 up")
139 assert silent.status == "error" and silent.reason == "transport" and "no answer within 3 s" in silent.say
140 assert runner.run_intent("is the modem on /dev/ttyACM0 up").reason == "modem-error"
141 assert runner.run_intent("is the modem on /dev/ttyS0 up").reason == "device-not-allowed"
142
143
144def test_capabilities_name_the_sources_and_lists(examples):
145 caps = {t["name"]: t for t in examples.capabilities()}
146 assert caps["disk-space"]["sources"] == ["shell"] and caps["disk-space"]["commands"] == ["df"] and "paths" not in caps["disk-space"]
147 assert caps["room-temperature"]["sources"] == ["queue"] and caps["room-temperature"]["topics"] == ["home/+/temperature"]
148 assert caps["weather"]["sources"] == ["http"] and caps["modem-check"]["devices"] == ["/dev/ttyUSB*", "/dev/ttyACM*"]