faultbench 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.
- faultbench-0.1.0/.env.example +4 -0
- faultbench-0.1.0/.github/workflows/ci.yml +27 -0
- faultbench-0.1.0/.gitignore +12 -0
- faultbench-0.1.0/.python-version +1 -0
- faultbench-0.1.0/CHANGELOG.md +4 -0
- faultbench-0.1.0/CLAUDE.md +52 -0
- faultbench-0.1.0/LICENSE +175 -0
- faultbench-0.1.0/PKG-INFO +137 -0
- faultbench-0.1.0/README.md +108 -0
- faultbench-0.1.0/conftest.py +4 -0
- faultbench-0.1.0/docs/ARCHITECTURE.md +50 -0
- faultbench-0.1.0/docs/DECISIONS.md +51 -0
- faultbench-0.1.0/docs/JOURNEY.md +82 -0
- faultbench-0.1.0/docs/STATUS.md +58 -0
- faultbench-0.1.0/docs/TESTING.md +133 -0
- faultbench-0.1.0/docs/WORLDS.md +157 -0
- faultbench-0.1.0/examples/bank/README.md +26 -0
- faultbench-0.1.0/examples/bank/bank_rules.py +37 -0
- faultbench-0.1.0/examples/bank/worlds/bank.yaml +39 -0
- faultbench-0.1.0/examples/shop/README.md +71 -0
- faultbench-0.1.0/examples/shop/agent.py +94 -0
- faultbench-0.1.0/examples/shop/conftest.py +21 -0
- faultbench-0.1.0/examples/shop/demo.py +127 -0
- faultbench-0.1.0/examples/shop/shop_rules.py +26 -0
- faultbench-0.1.0/examples/shop/test_openai_agents.py +40 -0
- faultbench-0.1.0/examples/shop/test_refunds.py +44 -0
- faultbench-0.1.0/examples/shop/worlds/shop.yaml +45 -0
- faultbench-0.1.0/examples/stripe/README.md +26 -0
- faultbench-0.1.0/examples/stripe/stripe_rules.py +69 -0
- faultbench-0.1.0/examples/stripe/worlds/stripe.yaml +69 -0
- faultbench-0.1.0/pyproject.toml +61 -0
- faultbench-0.1.0/src/faultbench/__init__.py +3 -0
- faultbench-0.1.0/src/faultbench/cli.py +60 -0
- faultbench-0.1.0/src/faultbench/faults/__init__.py +16 -0
- faultbench-0.1.0/src/faultbench/faults/clock.py +32 -0
- faultbench-0.1.0/src/faultbench/faults/injector.py +151 -0
- faultbench-0.1.0/src/faultbench/faults/profile.py +104 -0
- faultbench-0.1.0/src/faultbench/integrations/__init__.py +6 -0
- faultbench-0.1.0/src/faultbench/integrations/pydantic_ai.py +55 -0
- faultbench-0.1.0/src/faultbench/pytest_plugin.py +300 -0
- faultbench-0.1.0/src/faultbench/server/__init__.py +6 -0
- faultbench-0.1.0/src/faultbench/server/__main__.py +41 -0
- faultbench-0.1.0/src/faultbench/server/mcp_server.py +298 -0
- faultbench-0.1.0/src/faultbench/server/operations.py +62 -0
- faultbench-0.1.0/src/faultbench/trace/__init__.py +6 -0
- faultbench-0.1.0/src/faultbench/trace/queries.py +55 -0
- faultbench-0.1.0/src/faultbench/trace/recorder.py +58 -0
- faultbench-0.1.0/src/faultbench/world/__init__.py +29 -0
- faultbench-0.1.0/src/faultbench/world/engine.py +252 -0
- faultbench-0.1.0/src/faultbench/world/schema.py +154 -0
- faultbench-0.1.0/src/faultbench/world/seed.py +79 -0
- faultbench-0.1.0/tests/__init__.py +0 -0
- faultbench-0.1.0/tests/test_bank_example.py +66 -0
- faultbench-0.1.0/tests/test_errors.py +46 -0
- faultbench-0.1.0/tests/test_faults.py +193 -0
- faultbench-0.1.0/tests/test_http.py +36 -0
- faultbench-0.1.0/tests/test_integration_pydantic_ai.py +30 -0
- faultbench-0.1.0/tests/test_keyless.py +19 -0
- faultbench-0.1.0/tests/test_plugin.py +68 -0
- faultbench-0.1.0/tests/test_rules.py +130 -0
- faultbench-0.1.0/tests/test_runs.py +47 -0
- faultbench-0.1.0/tests/test_server.py +136 -0
- faultbench-0.1.0/tests/test_smoke.py +27 -0
- faultbench-0.1.0/tests/test_stripe_example.py +62 -0
- faultbench-0.1.0/tests/test_trace.py +80 -0
- faultbench-0.1.0/tests/test_world.py +289 -0
- faultbench-0.1.0/tests/worlds/mini.yaml +16 -0
- faultbench-0.1.0/uv.lock +2484 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.11", "3.12"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- name: Install uv
|
|
17
|
+
uses: astral-sh/setup-uv@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- name: Install
|
|
21
|
+
run: uv sync --all-extras
|
|
22
|
+
- name: Lint
|
|
23
|
+
run: uv run ruff check .
|
|
24
|
+
- name: Format check
|
|
25
|
+
run: uv run ruff format --check .
|
|
26
|
+
- name: Test (no API key needed)
|
|
27
|
+
run: uv run pytest -q
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.11
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# faultbench
|
|
2
|
+
|
|
3
|
+
Fake, stateful worlds with fault injection for testing tool-using AI agents.
|
|
4
|
+
A user declares services and records in `world.yaml`; faultbench serves them as MCP
|
|
5
|
+
tools, injects latency/errors/clock skew on demand, records every call, and lets
|
|
6
|
+
pytest assert on the world's end state.
|
|
7
|
+
|
|
8
|
+
**Read `docs/STATUS.md` before doing anything.** It says which milestone is active
|
|
9
|
+
and what the next session should do. Then read `docs/ARCHITECTURE.md` for the
|
|
10
|
+
component you are touching and `docs/DECISIONS.md` so you don't re-argue settled choices.
|
|
11
|
+
|
|
12
|
+
## Non-goals (do not build these)
|
|
13
|
+
- No simulated user / persona engine. Users bring LangWatch Scenario, DeepEval or openevals.
|
|
14
|
+
- No LLM judge or transcript scoring. Assert on world state and the trace only.
|
|
15
|
+
- No dashboard or web UI. Stay inside pytest and the CLI.
|
|
16
|
+
- No clever YAML. Logic goes in Python handlers, not in templates or conditionals.
|
|
17
|
+
- No support for every framework at once. Pydantic AI first, one more via HTTP, then stop.
|
|
18
|
+
|
|
19
|
+
## Stack
|
|
20
|
+
Python 3.11+, uv, pydantic v2, official `mcp` SDK, faker, pytest, ruff. No database.
|
|
21
|
+
Target: under 3,000 lines for v0.1.
|
|
22
|
+
|
|
23
|
+
## Commands
|
|
24
|
+
uv sync --all-extras # install
|
|
25
|
+
uv run pytest # harness unit tests
|
|
26
|
+
uv run pytest examples/shop # example agent tests (needs a model API key)
|
|
27
|
+
uv run ruff check . && uv run ruff format .
|
|
28
|
+
uv run faultbench serve examples/shop/worlds/shop.yaml
|
|
29
|
+
|
|
30
|
+
## Session protocol
|
|
31
|
+
1. Read STATUS.md, ARCHITECTURE.md (relevant section), DECISIONS.md.
|
|
32
|
+
2. Propose a short plan for this session's milestone; wait for approval.
|
|
33
|
+
3. One component per session. Do not start the next milestone.
|
|
34
|
+
4. Write tests alongside code in `tests/`.
|
|
35
|
+
5. Before ending: run `uv run pytest` and `uv run ruff check .`, update STATUS.md
|
|
36
|
+
(state + "next session should"), append to DECISIONS.md if anything was decided,
|
|
37
|
+
and update docs/JOURNEY.md: flip the finished milestone to ✅ with a 2-line plain-language
|
|
38
|
+
"Built." note (what exists now, not how), move the "Now" marker to the next one. Commit.
|
|
39
|
+
|
|
40
|
+
## Layout
|
|
41
|
+
src/faultbench/world/ schema.py engine.py seed.py # YAML -> stateful tables
|
|
42
|
+
src/faultbench/faults/ profile.py injector.py clock.py # latency, errors, fake time
|
|
43
|
+
src/faultbench/server/ mcp_server.py operations.py # world -> MCP tools
|
|
44
|
+
src/faultbench/trace/ recorder.py queries.py # JSONL trace of every call
|
|
45
|
+
src/faultbench/pytest_plugin.py # fixtures, markers, --runs
|
|
46
|
+
src/faultbench/cli.py # `faultbench serve`
|
|
47
|
+
examples/shop/ the refund agent demo (world, rules, agent, tests)
|
|
48
|
+
tests/ unit tests for faultbench itself
|
|
49
|
+
docs/ ARCHITECTURE, DECISIONS, STATUS, user docs
|
|
50
|
+
|
|
51
|
+
Background (why this exists, market landscape, 12-weekend plan):
|
|
52
|
+
https://claude.ai/code/artifact/058e73f3-39fa-4d36-8f96-f1b29efe8de2
|
faultbench-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: faultbench
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fake, stateful worlds with fault injection for testing tool-using AI agents. Exposed over MCP, asserted from pytest.
|
|
5
|
+
Author: mithun
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: ai-agents,fault-injection,mcp,pytest,simulation,testing
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Framework :: Pytest
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Testing
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: faker>=25.0
|
|
19
|
+
Requires-Dist: mcp>=2.2
|
|
20
|
+
Requires-Dist: pydantic>=2.7
|
|
21
|
+
Requires-Dist: pyyaml>=6.0
|
|
22
|
+
Provides-Extra: examples
|
|
23
|
+
Requires-Dist: pydantic-ai>=2.0; extra == 'examples'
|
|
24
|
+
Provides-Extra: openai-agents
|
|
25
|
+
Requires-Dist: openai-agents>=0.2; extra == 'openai-agents'
|
|
26
|
+
Provides-Extra: pydantic-ai
|
|
27
|
+
Requires-Dist: pydantic-ai>=2.0; extra == 'pydantic-ai'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# faultbench
|
|
31
|
+
|
|
32
|
+
**Fake, stateful worlds with fault injection for testing tool-using AI agents.**
|
|
33
|
+
|
|
34
|
+
Declare your services in a YAML file. faultbench serves them as MCP tools your agent can call,
|
|
35
|
+
makes them slow or broken on purpose, records every call, and lets you assert on the world's
|
|
36
|
+
end state from pytest, twenty runs at a time.
|
|
37
|
+
|
|
38
|
+
> Status: pre-alpha, but it works end to end. Write a world, serve it over MCP (stdio or
|
|
39
|
+
> HTTP), inject faults, and assert on state from pytest across N seeded runs.
|
|
40
|
+
> **Guides:** [write a world](docs/WORLDS.md) · [test an agent](docs/TESTING.md) ·
|
|
41
|
+
> examples: [`examples/shop`](examples/shop) (refund agent), [`examples/bank`](examples/bank).
|
|
42
|
+
> Validated against **Pydantic AI** and the **OpenAI Agents SDK** (any MCP framework works).
|
|
43
|
+
|
|
44
|
+
## The problem
|
|
45
|
+
|
|
46
|
+
Your support agent handles "return my order and refund me." It works when you test by hand.
|
|
47
|
+
In production the refund API times out once, the agent retries, and a customer is refunded twice.
|
|
48
|
+
You can't make the real payments API time out on command, so you never tested it.
|
|
49
|
+
|
|
50
|
+
## What it looks like
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
# world.yaml
|
|
54
|
+
services:
|
|
55
|
+
orders:
|
|
56
|
+
records: { order: { id: str, status: enum[placed, delivered, returned], total: float } }
|
|
57
|
+
operations: { get_order: { kind: get, record: order } }
|
|
58
|
+
payments:
|
|
59
|
+
records: { refund: { id: str, order_id: str, amount: float } }
|
|
60
|
+
operations: { issue_refund: { kind: create, record: refund } }
|
|
61
|
+
faults:
|
|
62
|
+
payments.issue_refund: { errors: { timeout: 0.10 } }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from faultbench.integrations.pydantic_ai import run_agent # or wire any MCP framework
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.world("world.yaml")
|
|
70
|
+
@pytest.mark.faults("world.yaml") # inject the faults: block above
|
|
71
|
+
@pytest.mark.runs(20)
|
|
72
|
+
@pytest.mark.min_pass_rate(0.95)
|
|
73
|
+
async def test_refund_issued_exactly_once(world, mcp_server, trace):
|
|
74
|
+
order = world.orders.pick(status="delivered")
|
|
75
|
+
await run_agent(
|
|
76
|
+
"openai:gpt-5-mini",
|
|
77
|
+
f"Return order {order.id} and refund me",
|
|
78
|
+
mcp=mcp_server,
|
|
79
|
+
system_prompt="You are a refund agent.",
|
|
80
|
+
)
|
|
81
|
+
assert len(world.refunds.where(order_id=order.id)) == 1 # a timeout+retry breaks this
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
faultbench: pass rate over runs
|
|
86
|
+
test_refund_issued_exactly_once: 17/20 passed (85%) min_pass_rate=95% -> FAIL
|
|
87
|
+
run4: get_order → create_return → issue_refund!timeout → issue_refund
|
|
88
|
+
run11: get_order → create_return → issue_refund!timeout → issue_refund
|
|
89
|
+
run18: get_order → create_return → issue_refund!timeout → issue_refund
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The timeout fired *after* the refund was written, the agent retried, and the customer was
|
|
93
|
+
refunded twice — the production bug you couldn't trigger on the real payments API, now a red
|
|
94
|
+
test with the trace that explains it.
|
|
95
|
+
|
|
96
|
+
## What it can and can't model
|
|
97
|
+
faultbench models services as **flat records** (fields: `str/int/float/bool/datetime/enum/ref`)
|
|
98
|
+
with built-in CRUD plus **custom Python operations** for anything else.
|
|
99
|
+
|
|
100
|
+
- **Fits well:** entities with enums and `ref` relationships; CRUD and list-by-field; business
|
|
101
|
+
rules, state machines, and multi-record writes as custom handlers; array *inputs* via a custom
|
|
102
|
+
op that flattens into a related record type; money as integer minor-units. (See
|
|
103
|
+
[`examples/stripe`](examples/stripe) — a Stripe-style payments API with partial-refund rules.)
|
|
104
|
+
- **Caveat:** records are flat — there are **no nested objects or array fields**. Model a
|
|
105
|
+
one-to-many as a related record type + a `ref` (invoice ← line items); a GET returns the
|
|
106
|
+
parent without children inline, so if your agent's correctness depends on a nested *response*
|
|
107
|
+
shape, the fake's shape differs.
|
|
108
|
+
- **Not in 0.1:** pagination/cursors, non-equality filters, auth, webhooks, per-request
|
|
109
|
+
idempotency (that last is a bug faultbench helps you *catch*, not prevent). Generated seed
|
|
110
|
+
values are type-correct but not domain-aware — set realistic values in a handler or your test.
|
|
111
|
+
|
|
112
|
+
## Security
|
|
113
|
+
A world file can name Python to import and run (custom handlers, `handler: module.func`), so
|
|
114
|
+
loading or serving one executes that code. Only use world files you trust, like any script.
|
|
115
|
+
|
|
116
|
+
## Not in scope
|
|
117
|
+
Simulated users, LLM judges, dashboards. Use LangWatch Scenario / DeepEval for users and your own
|
|
118
|
+
judge for scoring; faultbench is the environment.
|
|
119
|
+
|
|
120
|
+
## Quickstart
|
|
121
|
+
|
|
122
|
+
uv sync --all-extras
|
|
123
|
+
uv run pytest # the harness test suite (no API key)
|
|
124
|
+
uv run faultbench serve examples/shop/worlds/shop.yaml # serve a world over MCP (stdio)
|
|
125
|
+
uv run faultbench serve examples/bank/worlds/bank.yaml --http # ...or over HTTP
|
|
126
|
+
|
|
127
|
+
Then write your own: [docs/WORLDS.md](docs/WORLDS.md) and [docs/TESTING.md](docs/TESTING.md).
|
|
128
|
+
|
|
129
|
+
## Development
|
|
130
|
+
See `CLAUDE.md` for the session protocol and `docs/` for architecture, decisions and status.
|
|
131
|
+
|
|
132
|
+
uv sync --all-extras
|
|
133
|
+
uv run pytest
|
|
134
|
+
uv run pytest examples/shop # the example agent test (needs a model API key)
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
Apache 2.0
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# faultbench
|
|
2
|
+
|
|
3
|
+
**Fake, stateful worlds with fault injection for testing tool-using AI agents.**
|
|
4
|
+
|
|
5
|
+
Declare your services in a YAML file. faultbench serves them as MCP tools your agent can call,
|
|
6
|
+
makes them slow or broken on purpose, records every call, and lets you assert on the world's
|
|
7
|
+
end state from pytest, twenty runs at a time.
|
|
8
|
+
|
|
9
|
+
> Status: pre-alpha, but it works end to end. Write a world, serve it over MCP (stdio or
|
|
10
|
+
> HTTP), inject faults, and assert on state from pytest across N seeded runs.
|
|
11
|
+
> **Guides:** [write a world](docs/WORLDS.md) · [test an agent](docs/TESTING.md) ·
|
|
12
|
+
> examples: [`examples/shop`](examples/shop) (refund agent), [`examples/bank`](examples/bank).
|
|
13
|
+
> Validated against **Pydantic AI** and the **OpenAI Agents SDK** (any MCP framework works).
|
|
14
|
+
|
|
15
|
+
## The problem
|
|
16
|
+
|
|
17
|
+
Your support agent handles "return my order and refund me." It works when you test by hand.
|
|
18
|
+
In production the refund API times out once, the agent retries, and a customer is refunded twice.
|
|
19
|
+
You can't make the real payments API time out on command, so you never tested it.
|
|
20
|
+
|
|
21
|
+
## What it looks like
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
# world.yaml
|
|
25
|
+
services:
|
|
26
|
+
orders:
|
|
27
|
+
records: { order: { id: str, status: enum[placed, delivered, returned], total: float } }
|
|
28
|
+
operations: { get_order: { kind: get, record: order } }
|
|
29
|
+
payments:
|
|
30
|
+
records: { refund: { id: str, order_id: str, amount: float } }
|
|
31
|
+
operations: { issue_refund: { kind: create, record: refund } }
|
|
32
|
+
faults:
|
|
33
|
+
payments.issue_refund: { errors: { timeout: 0.10 } }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from faultbench.integrations.pydantic_ai import run_agent # or wire any MCP framework
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.world("world.yaml")
|
|
41
|
+
@pytest.mark.faults("world.yaml") # inject the faults: block above
|
|
42
|
+
@pytest.mark.runs(20)
|
|
43
|
+
@pytest.mark.min_pass_rate(0.95)
|
|
44
|
+
async def test_refund_issued_exactly_once(world, mcp_server, trace):
|
|
45
|
+
order = world.orders.pick(status="delivered")
|
|
46
|
+
await run_agent(
|
|
47
|
+
"openai:gpt-5-mini",
|
|
48
|
+
f"Return order {order.id} and refund me",
|
|
49
|
+
mcp=mcp_server,
|
|
50
|
+
system_prompt="You are a refund agent.",
|
|
51
|
+
)
|
|
52
|
+
assert len(world.refunds.where(order_id=order.id)) == 1 # a timeout+retry breaks this
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
faultbench: pass rate over runs
|
|
57
|
+
test_refund_issued_exactly_once: 17/20 passed (85%) min_pass_rate=95% -> FAIL
|
|
58
|
+
run4: get_order → create_return → issue_refund!timeout → issue_refund
|
|
59
|
+
run11: get_order → create_return → issue_refund!timeout → issue_refund
|
|
60
|
+
run18: get_order → create_return → issue_refund!timeout → issue_refund
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The timeout fired *after* the refund was written, the agent retried, and the customer was
|
|
64
|
+
refunded twice — the production bug you couldn't trigger on the real payments API, now a red
|
|
65
|
+
test with the trace that explains it.
|
|
66
|
+
|
|
67
|
+
## What it can and can't model
|
|
68
|
+
faultbench models services as **flat records** (fields: `str/int/float/bool/datetime/enum/ref`)
|
|
69
|
+
with built-in CRUD plus **custom Python operations** for anything else.
|
|
70
|
+
|
|
71
|
+
- **Fits well:** entities with enums and `ref` relationships; CRUD and list-by-field; business
|
|
72
|
+
rules, state machines, and multi-record writes as custom handlers; array *inputs* via a custom
|
|
73
|
+
op that flattens into a related record type; money as integer minor-units. (See
|
|
74
|
+
[`examples/stripe`](examples/stripe) — a Stripe-style payments API with partial-refund rules.)
|
|
75
|
+
- **Caveat:** records are flat — there are **no nested objects or array fields**. Model a
|
|
76
|
+
one-to-many as a related record type + a `ref` (invoice ← line items); a GET returns the
|
|
77
|
+
parent without children inline, so if your agent's correctness depends on a nested *response*
|
|
78
|
+
shape, the fake's shape differs.
|
|
79
|
+
- **Not in 0.1:** pagination/cursors, non-equality filters, auth, webhooks, per-request
|
|
80
|
+
idempotency (that last is a bug faultbench helps you *catch*, not prevent). Generated seed
|
|
81
|
+
values are type-correct but not domain-aware — set realistic values in a handler or your test.
|
|
82
|
+
|
|
83
|
+
## Security
|
|
84
|
+
A world file can name Python to import and run (custom handlers, `handler: module.func`), so
|
|
85
|
+
loading or serving one executes that code. Only use world files you trust, like any script.
|
|
86
|
+
|
|
87
|
+
## Not in scope
|
|
88
|
+
Simulated users, LLM judges, dashboards. Use LangWatch Scenario / DeepEval for users and your own
|
|
89
|
+
judge for scoring; faultbench is the environment.
|
|
90
|
+
|
|
91
|
+
## Quickstart
|
|
92
|
+
|
|
93
|
+
uv sync --all-extras
|
|
94
|
+
uv run pytest # the harness test suite (no API key)
|
|
95
|
+
uv run faultbench serve examples/shop/worlds/shop.yaml # serve a world over MCP (stdio)
|
|
96
|
+
uv run faultbench serve examples/bank/worlds/bank.yaml --http # ...or over HTTP
|
|
97
|
+
|
|
98
|
+
Then write your own: [docs/WORLDS.md](docs/WORLDS.md) and [docs/TESTING.md](docs/TESTING.md).
|
|
99
|
+
|
|
100
|
+
## Development
|
|
101
|
+
See `CLAUDE.md` for the session protocol and `docs/` for architecture, decisions and status.
|
|
102
|
+
|
|
103
|
+
uv sync --all-extras
|
|
104
|
+
uv run pytest
|
|
105
|
+
uv run pytest examples/shop # the example agent test (needs a model API key)
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
Apache 2.0
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Five components. The agent under test never knows it is talking to a fake; it sees MCP tools.
|
|
4
|
+
|
|
5
|
+
Agent --MCP--> mcp_server --> faults.injector --> world.engine
|
|
6
|
+
|
|
|
7
|
+
+--> trace.recorder
|
|
8
|
+
pytest_plugin ----> engine (assert state), injector (configure), recorder (read trace)
|
|
9
|
+
|
|
10
|
+
## 1. World engine (`world/`)
|
|
11
|
+
- `schema.py`: pydantic models for `world.yaml` (World, Service, RecordType, Field, Operation, SeedSpec).
|
|
12
|
+
Field types: `str`, `int`, `float`, `bool`, `datetime`, `enum[a, b, c]`, `ref[service.record]`.
|
|
13
|
+
- `engine.py`: `World.load(path) -> World`. Builds one in-memory `Table` per record type.
|
|
14
|
+
`Table` API: `get(id)`, `all()`, `where(**eq)`, `pick(**eq)` (deterministic first match),
|
|
15
|
+
`insert(record)`, `update(id, **fields)`, `delete(id)`.
|
|
16
|
+
`World` API: `snapshot() -> dict`, `diff(snapshot) -> Diff`, `revision: int` (bumps on every write).
|
|
17
|
+
- `seed.py`: faker-based generators per field type, seeded by `world.seed`. CSV/JSON loaders.
|
|
18
|
+
- Invariant: same YAML + same seed => identical initial state, byte for byte.
|
|
19
|
+
|
|
20
|
+
## 2. Fault layer (`faults/`)
|
|
21
|
+
- `profile.py`: pydantic models for the `faults:` block. Keyed by `service.operation` or `default`.
|
|
22
|
+
Fields: `latency_ms: [lo, hi]`, `errors: {kind: probability}`, `rate_limit: {calls, per_seconds}`,
|
|
23
|
+
conditional errors (e.g. `not_found_if_newer_than: 2h`).
|
|
24
|
+
- `injector.py`: wraps an operation callable. Seeded RNG (`seed + run_index`). Error catalogue:
|
|
25
|
+
`timeout` (actually sleeps past the client deadline), `http_500`, `http_429` (with retry-after),
|
|
26
|
+
`malformed_json`, `empty_result`, `not_found`. Every decision is recorded to the trace.
|
|
27
|
+
- `clock.py`: `FakeClock` with `now()`, `advance(seconds)`. The engine and injector read time only from it.
|
|
28
|
+
- Invariant: same profile + same seed => identical fault sequence.
|
|
29
|
+
|
|
30
|
+
## 3. MCP server (`server/`)
|
|
31
|
+
- `mcp_server.py`: builds a `mcp.server.Server` from a World + FaultProfile + Recorder.
|
|
32
|
+
One tool per operation, JSON schema derived from the record type. Transports: stdio, streamable HTTP.
|
|
33
|
+
- `operations.py`: built-in kinds `get`, `list`, `create`, `update`, `delete`; `custom` resolves
|
|
34
|
+
`handler: "module.func"` and calls `func(world, clock, **args)`.
|
|
35
|
+
|
|
36
|
+
## 4. Trace (`trace/`)
|
|
37
|
+
- `recorder.py`: JSON Lines, one event per tool call:
|
|
38
|
+
`{run, seq, ts, tool, args, fault, latency_ms, ok, result|error, world_rev}`.
|
|
39
|
+
- `queries.py`: `Trace.count(tool)`, `calls_to(tool)`, `faults()`, `failures()`.
|
|
40
|
+
|
|
41
|
+
## 5. pytest plugin (`pytest_plugin.py`)
|
|
42
|
+
- Markers: `@pytest.mark.world("path.yaml")`, `@pytest.mark.faults("path.yaml" | dict)`,
|
|
43
|
+
`@pytest.mark.runs(N)`, `@pytest.mark.min_pass_rate(0.9)`.
|
|
44
|
+
- Fixtures: `world`, `faults`, `clock`, `mcp_url` (server subprocess per test), `trace`.
|
|
45
|
+
- `--runs=N` overrides the marker; report shows `runs / passed / failed / pass rate` and a
|
|
46
|
+
per-failure trace summary. `min_pass_rate` turns the rate into pass/fail for CI.
|
|
47
|
+
|
|
48
|
+
## Flow of one test
|
|
49
|
+
pytest builds World + FaultProfile -> starts MCP server subprocess -> yields `mcp_url`
|
|
50
|
+
-> test runs the agent -> test asserts on `world` and `trace` -> server stops, world discarded.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Decisions
|
|
2
|
+
|
|
3
|
+
One line each. Append; do not rewrite history. Format: date, decision, reason.
|
|
4
|
+
|
|
5
|
+
- 2026-09-19 — Name: `faultbench`. Reason: describes the thing (fake world + benchmarking); free on PyPI/GitHub at time of check.
|
|
6
|
+
- 2026-09-19 — License: Apache 2.0. Reason: matches peers (Scenario, DeepEval, tau2-bench), patent grant, leaves room for a hosted product later.
|
|
7
|
+
- 2026-09-19 — Language: Python 3.11+, packaged with uv. Reason: largest agent ecosystem; pytest is the natural runner.
|
|
8
|
+
- 2026-09-19 — Scope: fake world + faults + trace + pytest. Explicitly NOT simulated users, judges, dashboards. Reason: those markets are full; the environment layer is empty (see market doc).
|
|
9
|
+
- 2026-09-19 — Fake services are hand-written from YAML, not LLM-emulated. Reason: determinism is the feature. `kind: emulated` may come later on request.
|
|
10
|
+
- 2026-09-19 — Logic lives in Python handlers (`kind: custom`), never in YAML templating. Reason: every YAML feature is something users must learn and something that breaks sharing.
|
|
11
|
+
- 2026-09-19 — Server runs as a subprocess per test, stdio by default, HTTP when the agent is remote. Reason: isolation over speed at this stage.
|
|
12
|
+
- 2026-09-19 — Tests report a pass rate over N seeded runs; `min_pass_rate` converts to pass/fail for CI. Reason: agents are probabilistic; a single pass/fail lies.
|
|
13
|
+
- 2026-09-19 — Toy agent uses Pydantic AI. Reason: small, typed, speaks MCP natively, model-agnostic.
|
|
14
|
+
- 2026-09-19 — Time is read only from `FakeClock`. Reason: "order is 2 hours old" must be testable without waiting.
|
|
15
|
+
- 2026-09-19 — Installed `mcp` is 2.x (2.2.0): `FastMCP` is now `mcp.server.mcpserver.MCPServer` (`@server.tool()`, `server.run("stdio"|"streamable-http")`). Weekend 3 targets this API; `pyproject` `mcp>=1.2` should be tightened to `>=2` before the server milestone.
|
|
16
|
+
- 2026-09-19 — Pydantic AI is 2.x (2.45): MCP client is `MCPToolset(FastMCPClient(StdioTransport(...)))`, not `MCPServerStdio`. Agent connects via `async with agent:`.
|
|
17
|
+
- 2026-09-19 — Example agent model is set via `FAULTBENCH_DEMO_MODEL` (default `openai:gpt-5-mini`; the key on hand this session was OpenAI). Provider prefix picks the required key. Reason: keep the example key-agnostic rather than pinning one provider.
|
|
18
|
+
- 2026-09-19 — Schema pydantic root is `WorldSpec` (with `ServiceSpec`/`RecordType`/`FieldType`/`OperationSpec`/`SeedSpec`), not `World`. Reason: avoid two classes named `World` — `engine.World` is the runtime, stateful world.
|
|
19
|
+
- 2026-09-19 — Tables are reached on the runtime `World` by record-type name (`world.orders`), so record names must be unique across the world (validated). `shop.yaml` records are named as collections (`orders`, `refunds`). Reason: matches the documented target test `world.orders.pick(...)` / `world.refunds.where(...)` with no pluralization magic ("no clever" rule).
|
|
20
|
+
- 2026-09-19 — Seeded `datetime` fields are drawn from a FIXED absolute window (2020–2025), never a now-relative one, and stored as ISO strings. Reason: the byte-identical invariant must hold on any day; verified identical across two separate processes.
|
|
21
|
+
- 2026-09-19 — Seeding does not bump `revision`; a freshly loaded world is at revision 0. Only insert/update/delete bump it. Reason: revision should count agent-caused writes, not initial state.
|
|
22
|
+
- 2026-09-19 — MCP tool schemas are derived by giving a generated wrapper function an explicit `__signature__` (mcp 2.x `func_metadata` reads `inspect.signature`), one tool per operation. Reason: operations are known only at runtime from YAML; this reuses the SDK's schema generation instead of hand-building JSON schema.
|
|
23
|
+
- 2026-09-19 — Tool params: get/delete → `id`; update → `id` + optional fields; create → record fields (optional, id auto-assigned); list → the operation's `filter` fields (optional); custom → the handler's params minus `world`/`clock`. Create/list fields are optional so the agent can call with only what it knows (e.g. `issue_refund(order_id, amount)`). Reason: keep the toy agent working unchanged.
|
|
24
|
+
- 2026-09-19 — Business-rule failures (ValueError/LookupError/KeyError from an op or handler) are converted to MCP `ToolError`; other exceptions propagate as crashes. Reason: the agent sees a clean, readable tool error for "not found"/"not delivered" without a server traceback, while real bugs stay loud.
|
|
25
|
+
- 2026-09-19 — Custom handler modules resolve by bare name; the server puts the world file's directory and its parent on `sys.path`. Reason: lets `shop.yaml` (in worlds/) reference `shop_rules` (in examples/shop/) without packaging.
|
|
26
|
+
- 2026-09-19 — `examples/shop/tools_dict.py` deleted; the agent now spawns `python -m faultbench.server <world.yaml>`. Reason: the generated server replaces the milestone-1 throwaway.
|
|
27
|
+
- 2026-09-19 — Fault timing models reality: `timeout` runs the op (side effect happens) THEN fails; `http_500`/`http_429`/`not_found` reject before running; `empty_result`/`malformed_json` corrupt the response without running. Reason: the double-refund bug requires the write to have happened before the client gives up and retries.
|
|
28
|
+
- 2026-09-19 — Injected timeouts/errors are surfaced to the client as MCP `ToolError`, not a real transport timeout. Reason: a retrying agent re-calls the op (reproducing the double write) without tests depending on real wall-clock deadlines; verified live (agent double-refunded order 2 at run_index 4).
|
|
29
|
+
- 2026-09-19 — Fault RNG is `Random(f"{seed}:{run_index}")`, one per server build; `seed` comes from the world. Reason: same profile + seed + run_index => identical fault sequence, and `run_index` walks distinct deterministic runs (for weekend-6 `--runs=N`).
|
|
30
|
+
- 2026-09-19 — Conditional faults (`not_found_if_newer_than`) and rate limits are parsed but NOT enforced in weekend 4; deferred to weekend 7 with the clock/seed-window eligibility work. Reason: keep milestone 4 to probabilistic faults + the demo; shop.yaml still loads.
|
|
31
|
+
- 2026-09-19 — pytest plugin fixture for the server is `mcp_server` (an in-process MCPServer sharing the `world` object), NOT `mcp_url`. Reason: our transport is stdio/in-memory, not HTTP; sharing the World object lets tests assert on state directly. `mcp_url`/HTTP is deferred to weekend 8.
|
|
32
|
+
- 2026-09-19 — The `mcp_server` fixture builds with `clock=None` (return-window check off), even though a `clock` fixture exists. Reason: threading the clock activates the 30-day window, but seed dates (2020–2025) are outside 30 days of the default clock (2025-06-01), so the refund test would fail; aligning them is weekend-7 eligibility work.
|
|
33
|
+
- 2026-09-19 — Example agent test (`examples/shop/test_refunds.py`) connects the agent to the fixture's in-process server via `run_agent(..., server=...)`, and lives outside `testpaths` so `uv run pytest` stays key-free; `uv run pytest examples/shop` runs it (skipped without a key, `.env` loaded by conftest). Reason: harness CI needs no API key; the real-agent test is opt-in.
|
|
34
|
+
- 2026-09-19 — Each of the N runs is a normal pytest item (`run_index` parametrised 0..N-1), not an internal loop. Reason: pytest-asyncio runs each item normally, so async tests work without fighting the pyfunc_call hook.
|
|
35
|
+
- 2026-09-19 — With `min_pass_rate(r)`, individual run failures are flipped to "passed" in `pytest_runtest_makereport` and the session fails (in `pytest_sessionfinish`) only if the aggregate rate < r. Without `min_pass_rate`, each run's pass/fail stands. Reason: DECISION that a single pass/fail lies — the aggregate is the CI verdict; but a strict test (no marker) should still go red on any failure.
|
|
36
|
+
- 2026-09-19 — Trace event per call carries the injector's decision via a `call(..., on_decision=)` hook; `build_server(..., recorder=)` owns writing the full event (adds ok/result-or-error/world_rev). Reason: one event per call assembled server-side, since fault/latency are known before the op and result/rev after.
|
|
37
|
+
- 2026-09-19 — Seed datetimes fall in the 20 days ending at `WORLD_EPOCH` (2025-06-01), and `faults.clock.DEFAULT_NOW == WORLD_EPOCH` (drift-guarded by a test, no cross-layer import). Reason: the deferred clock/seed alignment — so `create_return`'s 30-day window has eligible orders at the default clock. Threaded the clock into `mcp_server`/`serve_stdio`.
|
|
38
|
+
- 2026-09-19 — Injector check order per call: latency → conditional not-found → rate limit → probabilistic error → execute. Conditional (`not_found_if_newer_than`) and rate limits are deterministic (data/clock-driven, no RNG); the record's timestamp is supplied by the server (first datetime field of the target record, get/update/delete only). Reason: conditional/limit faults model real, non-random behaviour and must be reproducible from the clock.
|
|
39
|
+
- 2026-09-19 — HTTP transport is streamable-HTTP via `serve_http` / `faultbench serve --http`. The `mcp_url` pytest fixture serves the world in a subprocess and exposes end state through the world's state-file mirror (`.snapshot()`), since an out-of-process server can't share the `world` object. Reason: HTTP is for remote/other-framework agents; in-process `mcp_server` stays the fast path for state assertions.
|
|
40
|
+
- 2026-09-19 — Framework-neutrality is proven at the MCP protocol level: the reference `mcp.Client` (not Pydantic AI) drives the world over HTTP in CI, keyless. Reason: a real second agent framework needs another dependency + API key + flaky LLM calls; protocol-level proof is deterministic and per the "one more via HTTP, then stop" non-goal.
|
|
41
|
+
- 2026-09-19 — Second example world is `examples/bank/` (accounts + transfers, custom transfer rule). Reason: proves the world format generalises beyond the shop; validated keylessly in `tests/` so it can't rot.
|
|
42
|
+
- 2026-09-19 — User docs live in `docs/WORLDS.md` (authoring) and `docs/TESTING.md` (markers/fixtures/CLI); the README links them. Reason: separate the "how do I use it" guides from the internal ARCHITECTURE/DECISIONS/STATUS docs.
|
|
43
|
+
- 2026-09-19 — Framework adapters live in `faultbench.integrations.<framework>` as optional imports (each needs its own extra, e.g. `faultbench[pydantic-ai]`); the core stays framework-neutral. The Pydantic AI adapter's `run_agent(model, prompt, mcp=...)` accepts the in-process `mcp_server` or the `mcp_url` string. Reason: smooth the first-run experience without making pydantic-ai a core dependency.
|
|
44
|
+
- 2026-09-19 — Repo-local git identity is `mithun <mithunmsman@gmail.com>` (global untouched); the whole existing history was rewritten to this author before any push. Publishing (GitHub push, PyPI) is held for the `mit-hun-k` account token and explicit user confirmation — outward-facing/irreversible. Reason: correct attribution for the new owner; no accidental publish.
|
|
45
|
+
- 2026-09-19 — `faultbench.__version__` and `pyproject` version must match, guarded by `test_version_matches_pyproject`. Reason: they drifted (code 0.0.1 vs wheel 0.1.0) and shipped the wrong `--version`.
|
|
46
|
+
- 2026-09-19 — Pre-launch validation with a real second agent framework: the OpenAI Agents SDK drives the shop world over HTTP (`examples/shop/test_openai_agents.py`, opt-in; `openai-agents` extra). Fault-free it does a clean refund; under a forced timeout it retried and wrote 3 phantom refunds while reporting failure — faultbench caught them. Reason: substantiates "works with any framework" with a genuine rival agent, not just the reference client.
|
|
47
|
+
- 2026-09-19 — The server mirrors world state to the state file after EVERY attempt, including faulted ones (a timeout runs the op then fails). Found during the OpenAI Agents SDK validation: the success-only mirror under-reported write-then-timeout over HTTP — the exact bug class faultbench targets. Guarded by `test_faulted_write_is_mirrored_to_state_file`.
|
|
48
|
+
- 2026-09-19 — Records using `enum[a, b, c]` must be written in YAML block style; flow style (`{ status: enum[a, b, c] }`) is a YAML parse error (commas/brackets). Documented in WORLDS.md. Reason: real papercut hit while authoring a world.
|
|
49
|
+
- 2026-09-19 — Enum values are validated on writes: `create`/`update` reject a value outside the field's members (`run_builtin._check_enums`), and enum tool params advertise their members as a JSON-schema enum (Literal). Reason: real APIs reject bad enums; a docs-only tester found `status="banana"` was silently accepted.
|
|
50
|
+
- 2026-09-19 — Documented (README + WORLDS.md) that world files execute Python via custom handlers, so only trusted world files should be loaded/served. Reason: loading a world runs `handler: module.func` imports; users must know.
|
|
51
|
+
- 2026-09-19 — Documented the keyless testing path (`await mcp_server.call_tool(...)` + assert on `world`/`trace`), guarded by `tests/test_keyless.py`. Reason: docs-only tester couldn't test operations/faults without an LLM; the free deterministic loop was undocumented.
|