uagent-testkit 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - run: pip install -e ".[test]"
21
+ - run: pytest -q
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .pytest_cache/
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ agent_storage.json
9
+ *_data.json
10
+ grant-application.md
11
+ .env
12
+ .env.*
@@ -0,0 +1,15 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
@@ -0,0 +1,255 @@
1
+ Metadata-Version: 2.4
2
+ Name: uagent-testkit
3
+ Version: 0.1.0
4
+ Summary: Test Fetch.ai uAgents without a network, a wallet, or a sleep().
5
+ Project-URL: Homepage, https://github.com/pgalyen1987/fetchai_harness
6
+ Project-URL: Repository, https://github.com/pgalyen1987/fetchai_harness
7
+ Project-URL: Issues, https://github.com/pgalyen1987/fetchai_harness/issues
8
+ Author-email: pgalyen1987 <pgalyen87@gmail.com>
9
+ License-Expression: Apache-2.0
10
+ License-File: LICENSE
11
+ Keywords: agentverse,asi,fetchai,pytest,testing,uagents
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Framework :: Pytest
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Testing
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: uagents>=0.22
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'test'
25
+ Requires-Dist: pytest>=8.0; extra == 'test'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # uagent-testkit
29
+
30
+ Test [Fetch.ai uAgents](https://github.com/fetchai/uAgents) without a network, a wallet, or a `sleep()`.
31
+
32
+ ```python
33
+ from uagent_testkit import harness
34
+
35
+ async def test_echo():
36
+ h = harness(my_agent)
37
+ result = await h.deliver(Ping(text="hi"))
38
+
39
+ assert result.reply(Pong).text == "pong:hi"
40
+ assert h.storage["seen"] == 1
41
+ ```
42
+
43
+ ## Why
44
+
45
+ The uAgents framework has no testing story. To exercise a handler today you generally
46
+ have to stand the agent up for real — which means Almanac registration, endpoint
47
+ resolution, a funded wallet on some paths, and `asyncio.sleep()` calls sprinkled through
48
+ the test to wait for messages that may never arrive. That is slow, flaky, and awkward to
49
+ run in CI.
50
+
51
+ Worse, `Agent._handle_message` catches every exception a handler raises and writes it to
52
+ a log. A handler that crashes on every message still looks like a green test. The most
53
+ common agent bug is invisible to the most common agent test.
54
+
55
+ `uagent-testkit` routes messages straight to the registered handler with a context that
56
+ records instead of transmitting. No sockets, no registration, no waiting.
57
+
58
+ - **Handler exceptions are raised**, not swallowed into a log.
59
+ - **Storage is in-memory**, so state doesn't leak between tests via the JSON file the
60
+ stock `KeyValueStore` writes to your working directory.
61
+ - **Reply contracts are enforced** — if `@on_message(replies=Pong)` doesn't send a
62
+ `Pong`, that's an assertion failure instead of a log line nobody reads.
63
+ - **Interval and lifecycle handlers run on demand**, so an `@on_interval(period=3600)`
64
+ is testable without waiting an hour.
65
+ - **Multi-agent conversations replay deterministically**, with a loop guard instead of
66
+ a hung test.
67
+
68
+ ## Install
69
+
70
+ ```bash
71
+ pip install uagent-testkit
72
+ ```
73
+
74
+ Requires Python 3.10+ and `uagents>=0.22`. The pytest fixtures register automatically.
75
+
76
+ ## Testing one agent
77
+
78
+ ```python
79
+ from uagents import Agent, Context, Model
80
+ from uagent_testkit import harness
81
+
82
+ class Ping(Model):
83
+ text: str
84
+
85
+ class Pong(Model):
86
+ text: str
87
+
88
+ agent = Agent(name="echo", seed="echo seed")
89
+
90
+ @agent.on_message(model=Ping, replies=Pong)
91
+ async def on_ping(ctx: Context, sender: str, msg: Ping):
92
+ ctx.storage.set("seen", (ctx.storage.get("seen") or 0) + 1)
93
+ await ctx.send(sender, Pong(text=f"pong:{msg.text}"))
94
+ ```
95
+
96
+ ```python
97
+ async def test_ping():
98
+ h = harness(agent)
99
+ result = await h.deliver(Ping(text="hi"), sender="agent1qexample")
100
+
101
+ result.assert_replied_with(Pong)
102
+ result.assert_reply_contract() # honoured its replies= declaration
103
+ assert result.reply(Pong).text == "pong:hi"
104
+ assert result.sent[0].destination == "agent1qexample"
105
+ assert h.storage["seen"] == 1
106
+ ```
107
+
108
+ ### Asserting on failure
109
+
110
+ ```python
111
+ async def test_bad_input_is_handled():
112
+ result = await h.deliver(Ping(text=""), raise_errors=False)
113
+ assert isinstance(result.error, ValueError)
114
+ assert "empty" in result.logs.errors[0]
115
+ ```
116
+
117
+ By default `deliver()` re-raises whatever the handler raised, so a broken handler fails
118
+ the test loudly. Pass `raise_errors=False` when the failure is the thing under test.
119
+
120
+ ### Sender verification
121
+
122
+ Handlers without `allow_unverified=True` are only reachable from a verified agent
123
+ address. The harness enforces that, so a test can't pass for a flow production
124
+ rejects:
125
+
126
+ ```python
127
+ from uagent_testkit import UnverifiedSender, user_sender
128
+
129
+ async def test_strict_handler_refuses_users():
130
+ with pytest.raises(UnverifiedSender):
131
+ await h.deliver(Ping(text="hi"), sender=user_sender())
132
+ ```
133
+
134
+ ### Intervals and lifecycle
135
+
136
+ ```python
137
+ await h.startup() # runs @on_event("startup") handlers
138
+ result = await h.tick() # runs @on_interval handlers once, ignoring period
139
+ result = await h.tick(only="heartbeat") # just one, by function name
140
+ await h.shutdown()
141
+ ```
142
+
143
+ ## Testing agents against each other
144
+
145
+ ```python
146
+ from uagent_testkit import AgentNetwork
147
+
148
+ async def test_conversation():
149
+ net = AgentNetwork(alice, bob)
150
+ transcript = await net.send(Ping(text="hello"), to=bob, sender=alice)
151
+
152
+ transcript.assert_delivered(Pong, to=alice.address)
153
+ assert transcript.of(Pong)[0].text == "pong:hello"
154
+ assert net.harness(alice).storage["last_pong"] == "pong:hello"
155
+ ```
156
+
157
+ Every reply addressed to another agent in the network is delivered in turn until the
158
+ conversation goes quiet. Messages sent to addresses outside the network are collected on
159
+ `transcript.undelivered` rather than silently dropped:
160
+
161
+ ```python
162
+ transcript.assert_all_delivered() # fails if anything went to an unknown address
163
+ ```
164
+
165
+ Agents that answer each other forever raise `ConversationTooLong` after `max_rounds`
166
+ deliveries instead of hanging the suite. This catches echo loops, which are easy to
167
+ write and expensive to discover in production:
168
+
169
+ ```python
170
+ # a keyword responder whose answer contains its own trigger word
171
+ if "menu" in msg.text().lower():
172
+ await ctx.send(sender, chat("here is the menu")) # -> contains "menu"
173
+ ```
174
+
175
+ Two agents like that will talk to each other indefinitely. On a live network that is
176
+ two agents spamming each other; here it fails in milliseconds with the transcript
177
+ so far attached.
178
+
179
+ ## Protocols and the chat protocol
180
+
181
+ Agents assembled from `Protocol` objects work the same way, including the shared chat
182
+ protocol from `uagents_core` that Agentverse and Agent Launch agents use:
183
+
184
+ ```python
185
+ chat = Protocol(spec=chat_protocol_spec)
186
+
187
+ @chat.on_message(ChatMessage)
188
+ async def on_chat(ctx: Context, sender: str, msg: ChatMessage):
189
+ ...
190
+
191
+ agent.include(chat)
192
+ ```
193
+
194
+ ```python
195
+ async def test_chat():
196
+ h = harness(agent)
197
+ result = await h.deliver(
198
+ ChatMessage(
199
+ timestamp=datetime.now(timezone.utc),
200
+ msg_id=uuid4(),
201
+ content=[TextContent(type="text", text="show me the menu")],
202
+ )
203
+ )
204
+ assert result.replies(ChatAcknowledgement)
205
+ assert result.replies(ChatMessage)[0].text() == "here is what we have"
206
+ ```
207
+
208
+ ## pytest fixtures
209
+
210
+ Installing the package registers two factory fixtures:
211
+
212
+ ```python
213
+ def test_with_fixture(agent_harness, agent_network):
214
+ h = agent_harness(my_agent)
215
+ net = agent_network(alice, bob)
216
+ ```
217
+
218
+ ## API
219
+
220
+ | | |
221
+ |---|---|
222
+ | `harness(agent)` | Wrap an agent. Swaps in in-memory storage and merges the internal protocol. |
223
+ | `h.deliver(msg, sender=…, raise_errors=True)` | Route a message to its handler. Returns `Delivery`. |
224
+ | `h.tick(only=None)` | Run `@on_interval` handlers once. |
225
+ | `h.startup()` / `h.shutdown()` | Run `@on_event` handlers. |
226
+ | `h.storage` | In-memory store, subscriptable. |
227
+ | `h.handles(Model)` | Whether a handler is registered for a type. |
228
+ | `Delivery.sent` | List of `Sent(destination, schema_digest, body)`. |
229
+ | `Delivery.reply(Model)` | The single reply of that type; raises otherwise. |
230
+ | `Delivery.replies(Model, to=…)` | Replies of that type to the sender; `to=ANY` for any destination. |
231
+ | `user_sender()` | A user (non-agent) address, for `allow_unverified` paths. |
232
+ | `Delivery.error` / `.logs` | Handler exception and captured log records. |
233
+ | `Delivery.assert_replied_with` / `assert_silent` / `assert_no_errors` / `assert_reply_contract` | Chainable assertions. |
234
+ | `AgentNetwork(*agents)` | Multi-agent network. |
235
+ | `net.send(msg, to=…, sender=…, max_rounds=20)` | Play out a conversation. Returns `Transcript`. |
236
+ | `Transcript.of(Model)` / `.to(addr)` / `.undelivered` | Inspect what was exchanged. |
237
+
238
+ ## Scope
239
+
240
+ This exercises your handler logic: routing, replies, storage, protocol contracts, and
241
+ inter-agent flow. It deliberately does not simulate Almanac resolution, envelope
242
+ signing, or on-chain settlement — those are integration concerns and mocking them would
243
+ give you false confidence. Keep a thin integration test on a real testnet for those, and
244
+ use this for everything else.
245
+
246
+ ## Running the tests
247
+
248
+ ```bash
249
+ python -m venv .venv && .venv/bin/pip install -e ".[test]"
250
+ .venv/bin/python -m pytest
251
+ ```
252
+
253
+ ## License
254
+
255
+ Apache-2.0
@@ -0,0 +1,228 @@
1
+ # uagent-testkit
2
+
3
+ Test [Fetch.ai uAgents](https://github.com/fetchai/uAgents) without a network, a wallet, or a `sleep()`.
4
+
5
+ ```python
6
+ from uagent_testkit import harness
7
+
8
+ async def test_echo():
9
+ h = harness(my_agent)
10
+ result = await h.deliver(Ping(text="hi"))
11
+
12
+ assert result.reply(Pong).text == "pong:hi"
13
+ assert h.storage["seen"] == 1
14
+ ```
15
+
16
+ ## Why
17
+
18
+ The uAgents framework has no testing story. To exercise a handler today you generally
19
+ have to stand the agent up for real — which means Almanac registration, endpoint
20
+ resolution, a funded wallet on some paths, and `asyncio.sleep()` calls sprinkled through
21
+ the test to wait for messages that may never arrive. That is slow, flaky, and awkward to
22
+ run in CI.
23
+
24
+ Worse, `Agent._handle_message` catches every exception a handler raises and writes it to
25
+ a log. A handler that crashes on every message still looks like a green test. The most
26
+ common agent bug is invisible to the most common agent test.
27
+
28
+ `uagent-testkit` routes messages straight to the registered handler with a context that
29
+ records instead of transmitting. No sockets, no registration, no waiting.
30
+
31
+ - **Handler exceptions are raised**, not swallowed into a log.
32
+ - **Storage is in-memory**, so state doesn't leak between tests via the JSON file the
33
+ stock `KeyValueStore` writes to your working directory.
34
+ - **Reply contracts are enforced** — if `@on_message(replies=Pong)` doesn't send a
35
+ `Pong`, that's an assertion failure instead of a log line nobody reads.
36
+ - **Interval and lifecycle handlers run on demand**, so an `@on_interval(period=3600)`
37
+ is testable without waiting an hour.
38
+ - **Multi-agent conversations replay deterministically**, with a loop guard instead of
39
+ a hung test.
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pip install uagent-testkit
45
+ ```
46
+
47
+ Requires Python 3.10+ and `uagents>=0.22`. The pytest fixtures register automatically.
48
+
49
+ ## Testing one agent
50
+
51
+ ```python
52
+ from uagents import Agent, Context, Model
53
+ from uagent_testkit import harness
54
+
55
+ class Ping(Model):
56
+ text: str
57
+
58
+ class Pong(Model):
59
+ text: str
60
+
61
+ agent = Agent(name="echo", seed="echo seed")
62
+
63
+ @agent.on_message(model=Ping, replies=Pong)
64
+ async def on_ping(ctx: Context, sender: str, msg: Ping):
65
+ ctx.storage.set("seen", (ctx.storage.get("seen") or 0) + 1)
66
+ await ctx.send(sender, Pong(text=f"pong:{msg.text}"))
67
+ ```
68
+
69
+ ```python
70
+ async def test_ping():
71
+ h = harness(agent)
72
+ result = await h.deliver(Ping(text="hi"), sender="agent1qexample")
73
+
74
+ result.assert_replied_with(Pong)
75
+ result.assert_reply_contract() # honoured its replies= declaration
76
+ assert result.reply(Pong).text == "pong:hi"
77
+ assert result.sent[0].destination == "agent1qexample"
78
+ assert h.storage["seen"] == 1
79
+ ```
80
+
81
+ ### Asserting on failure
82
+
83
+ ```python
84
+ async def test_bad_input_is_handled():
85
+ result = await h.deliver(Ping(text=""), raise_errors=False)
86
+ assert isinstance(result.error, ValueError)
87
+ assert "empty" in result.logs.errors[0]
88
+ ```
89
+
90
+ By default `deliver()` re-raises whatever the handler raised, so a broken handler fails
91
+ the test loudly. Pass `raise_errors=False` when the failure is the thing under test.
92
+
93
+ ### Sender verification
94
+
95
+ Handlers without `allow_unverified=True` are only reachable from a verified agent
96
+ address. The harness enforces that, so a test can't pass for a flow production
97
+ rejects:
98
+
99
+ ```python
100
+ from uagent_testkit import UnverifiedSender, user_sender
101
+
102
+ async def test_strict_handler_refuses_users():
103
+ with pytest.raises(UnverifiedSender):
104
+ await h.deliver(Ping(text="hi"), sender=user_sender())
105
+ ```
106
+
107
+ ### Intervals and lifecycle
108
+
109
+ ```python
110
+ await h.startup() # runs @on_event("startup") handlers
111
+ result = await h.tick() # runs @on_interval handlers once, ignoring period
112
+ result = await h.tick(only="heartbeat") # just one, by function name
113
+ await h.shutdown()
114
+ ```
115
+
116
+ ## Testing agents against each other
117
+
118
+ ```python
119
+ from uagent_testkit import AgentNetwork
120
+
121
+ async def test_conversation():
122
+ net = AgentNetwork(alice, bob)
123
+ transcript = await net.send(Ping(text="hello"), to=bob, sender=alice)
124
+
125
+ transcript.assert_delivered(Pong, to=alice.address)
126
+ assert transcript.of(Pong)[0].text == "pong:hello"
127
+ assert net.harness(alice).storage["last_pong"] == "pong:hello"
128
+ ```
129
+
130
+ Every reply addressed to another agent in the network is delivered in turn until the
131
+ conversation goes quiet. Messages sent to addresses outside the network are collected on
132
+ `transcript.undelivered` rather than silently dropped:
133
+
134
+ ```python
135
+ transcript.assert_all_delivered() # fails if anything went to an unknown address
136
+ ```
137
+
138
+ Agents that answer each other forever raise `ConversationTooLong` after `max_rounds`
139
+ deliveries instead of hanging the suite. This catches echo loops, which are easy to
140
+ write and expensive to discover in production:
141
+
142
+ ```python
143
+ # a keyword responder whose answer contains its own trigger word
144
+ if "menu" in msg.text().lower():
145
+ await ctx.send(sender, chat("here is the menu")) # -> contains "menu"
146
+ ```
147
+
148
+ Two agents like that will talk to each other indefinitely. On a live network that is
149
+ two agents spamming each other; here it fails in milliseconds with the transcript
150
+ so far attached.
151
+
152
+ ## Protocols and the chat protocol
153
+
154
+ Agents assembled from `Protocol` objects work the same way, including the shared chat
155
+ protocol from `uagents_core` that Agentverse and Agent Launch agents use:
156
+
157
+ ```python
158
+ chat = Protocol(spec=chat_protocol_spec)
159
+
160
+ @chat.on_message(ChatMessage)
161
+ async def on_chat(ctx: Context, sender: str, msg: ChatMessage):
162
+ ...
163
+
164
+ agent.include(chat)
165
+ ```
166
+
167
+ ```python
168
+ async def test_chat():
169
+ h = harness(agent)
170
+ result = await h.deliver(
171
+ ChatMessage(
172
+ timestamp=datetime.now(timezone.utc),
173
+ msg_id=uuid4(),
174
+ content=[TextContent(type="text", text="show me the menu")],
175
+ )
176
+ )
177
+ assert result.replies(ChatAcknowledgement)
178
+ assert result.replies(ChatMessage)[0].text() == "here is what we have"
179
+ ```
180
+
181
+ ## pytest fixtures
182
+
183
+ Installing the package registers two factory fixtures:
184
+
185
+ ```python
186
+ def test_with_fixture(agent_harness, agent_network):
187
+ h = agent_harness(my_agent)
188
+ net = agent_network(alice, bob)
189
+ ```
190
+
191
+ ## API
192
+
193
+ | | |
194
+ |---|---|
195
+ | `harness(agent)` | Wrap an agent. Swaps in in-memory storage and merges the internal protocol. |
196
+ | `h.deliver(msg, sender=…, raise_errors=True)` | Route a message to its handler. Returns `Delivery`. |
197
+ | `h.tick(only=None)` | Run `@on_interval` handlers once. |
198
+ | `h.startup()` / `h.shutdown()` | Run `@on_event` handlers. |
199
+ | `h.storage` | In-memory store, subscriptable. |
200
+ | `h.handles(Model)` | Whether a handler is registered for a type. |
201
+ | `Delivery.sent` | List of `Sent(destination, schema_digest, body)`. |
202
+ | `Delivery.reply(Model)` | The single reply of that type; raises otherwise. |
203
+ | `Delivery.replies(Model, to=…)` | Replies of that type to the sender; `to=ANY` for any destination. |
204
+ | `user_sender()` | A user (non-agent) address, for `allow_unverified` paths. |
205
+ | `Delivery.error` / `.logs` | Handler exception and captured log records. |
206
+ | `Delivery.assert_replied_with` / `assert_silent` / `assert_no_errors` / `assert_reply_contract` | Chainable assertions. |
207
+ | `AgentNetwork(*agents)` | Multi-agent network. |
208
+ | `net.send(msg, to=…, sender=…, max_rounds=20)` | Play out a conversation. Returns `Transcript`. |
209
+ | `Transcript.of(Model)` / `.to(addr)` / `.undelivered` | Inspect what was exchanged. |
210
+
211
+ ## Scope
212
+
213
+ This exercises your handler logic: routing, replies, storage, protocol contracts, and
214
+ inter-agent flow. It deliberately does not simulate Almanac resolution, envelope
215
+ signing, or on-chain settlement — those are integration concerns and mocking them would
216
+ give you false confidence. Keep a thin integration test on a real testnet for those, and
217
+ use this for everything else.
218
+
219
+ ## Running the tests
220
+
221
+ ```bash
222
+ python -m venv .venv && .venv/bin/pip install -e ".[test]"
223
+ .venv/bin/python -m pytest
224
+ ```
225
+
226
+ ## License
227
+
228
+ Apache-2.0
File without changes
@@ -0,0 +1,62 @@
1
+ """A two-agent quote flow, written the way the uAgents docs write agents.
2
+
3
+ Note the module-level `Agent(...)` calls — the standard idiom. A trader asks an
4
+ oracle for a price; the oracle answers from its cache or records a miss.
5
+ """
6
+
7
+ from uagents import Agent, Context, Model
8
+
9
+
10
+ class QuoteRequest(Model):
11
+ symbol: str
12
+
13
+
14
+ class Quote(Model):
15
+ symbol: str
16
+ price: float
17
+
18
+
19
+ class QuoteUnavailable(Model):
20
+ symbol: str
21
+ reason: str
22
+
23
+
24
+ PRICES = {"FET": 1.42, "BNB": 604.10}
25
+
26
+
27
+ oracle = Agent(name="oracle", seed="uagent-testkit example oracle seed")
28
+ trader = Agent(name="trader", seed="uagent-testkit example trader seed")
29
+
30
+
31
+ @oracle.on_message(model=QuoteRequest, replies={Quote, QuoteUnavailable})
32
+ async def on_quote_request(ctx: Context, sender: str, msg: QuoteRequest):
33
+ symbol = msg.symbol.upper()
34
+ served = ctx.storage.get("served") or 0
35
+ ctx.storage.set("served", served + 1)
36
+
37
+ price = PRICES.get(symbol)
38
+ if price is None:
39
+ misses = ctx.storage.get("misses") or []
40
+ ctx.storage.set("misses", [*misses, symbol])
41
+ await ctx.send(
42
+ sender, QuoteUnavailable(symbol=symbol, reason="symbol not tracked")
43
+ )
44
+ return
45
+
46
+ await ctx.send(sender, Quote(symbol=symbol, price=price))
47
+
48
+
49
+ @trader.on_message(model=Quote)
50
+ async def on_quote(ctx: Context, sender: str, msg: Quote):
51
+ ctx.storage.set("last_price", msg.price)
52
+
53
+
54
+ @trader.on_message(model=QuoteUnavailable)
55
+ async def on_quote_unavailable(ctx: Context, sender: str, msg: QuoteUnavailable):
56
+ ctx.storage.set("last_error", msg.reason)
57
+
58
+
59
+ @trader.on_interval(period=60.0, messages=QuoteRequest)
60
+ async def poll_price(ctx: Context):
61
+ """Would fire once a minute in production; the harness runs it on demand."""
62
+ await ctx.send(oracle.address, QuoteRequest(symbol="FET"))
@@ -0,0 +1,67 @@
1
+ """The same flow, tested end to end.
2
+
3
+ Every test here executes in under 5ms; the only real cost in the file is deriving
4
+ the agents' keys from their seeds once at import.
5
+ """
6
+
7
+ from uagent_testkit import AgentNetwork, harness
8
+
9
+ from .quote_agents import (
10
+ Quote,
11
+ QuoteRequest,
12
+ QuoteUnavailable,
13
+ oracle,
14
+ trader,
15
+ )
16
+
17
+
18
+ async def test_oracle_answers_a_known_symbol():
19
+ h = harness(oracle)
20
+ result = await h.deliver(QuoteRequest(symbol="FET"))
21
+
22
+ assert result.reply(Quote).price == 1.42
23
+ result.assert_reply_contract()
24
+ assert h.storage["served"] == 1
25
+
26
+
27
+ async def test_oracle_reports_an_unknown_symbol():
28
+ h = harness(oracle)
29
+ result = await h.deliver(QuoteRequest(symbol="DOGE"))
30
+
31
+ assert result.reply(QuoteUnavailable).reason == "symbol not tracked"
32
+ assert h.storage["misses"] == ["DOGE"]
33
+
34
+
35
+ async def test_symbols_are_normalised():
36
+ h = harness(oracle)
37
+ result = await h.deliver(QuoteRequest(symbol="bnb"))
38
+
39
+ assert result.reply(Quote).symbol == "BNB"
40
+
41
+
42
+ async def test_full_round_trip_between_two_agents():
43
+ net = AgentNetwork(trader, oracle)
44
+ transcript = await net.send(
45
+ QuoteRequest(symbol="FET"), to=oracle, sender=trader
46
+ )
47
+
48
+ transcript.assert_delivered(Quote, to=trader.address)
49
+ transcript.assert_all_delivered()
50
+ assert net.harness(trader).storage["last_price"] == 1.42
51
+
52
+
53
+ async def test_failure_path_reaches_the_trader():
54
+ net = AgentNetwork(trader, oracle)
55
+ transcript = await net.send(
56
+ QuoteRequest(symbol="NOPE"), to=oracle, sender=trader
57
+ )
58
+
59
+ transcript.assert_delivered(QuoteUnavailable, to=trader.address)
60
+ assert net.harness(trader).storage["last_error"] == "symbol not tracked"
61
+
62
+
63
+ async def test_the_minute_poll_without_waiting_a_minute():
64
+ h = harness(trader)
65
+ result = await h.tick(only="poll_price")
66
+
67
+ assert result.reply(QuoteRequest, to=oracle.address).symbol == "FET"