lisa-harness-e2e 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.
- lisa_harness_e2e-0.1.0/.gitignore +33 -0
- lisa_harness_e2e-0.1.0/PKG-INFO +13 -0
- lisa_harness_e2e-0.1.0/README.md +269 -0
- lisa_harness_e2e-0.1.0/packs.yml +17 -0
- lisa_harness_e2e-0.1.0/pyproject.toml +41 -0
- lisa_harness_e2e-0.1.0/src/e2etest/__init__.py +1 -0
- lisa_harness_e2e-0.1.0/src/e2etest/bundle.py +67 -0
- lisa_harness_e2e-0.1.0/src/e2etest/cli.py +63 -0
- lisa_harness_e2e-0.1.0/src/e2etest/driver.py +249 -0
- lisa_harness_e2e-0.1.0/src/e2etest/environment.py +134 -0
- lisa_harness_e2e-0.1.0/src/e2etest/oracles.py +420 -0
- lisa_harness_e2e-0.1.0/src/e2etest/packs_venv.py +59 -0
- lisa_harness_e2e-0.1.0/src/e2etest/scenario.py +146 -0
- lisa_harness_e2e-0.1.0/src/e2etest/scripted.py +236 -0
- lisa_harness_e2e-0.1.0/tests/test_harness.py +337 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.pyo
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Virtual environments
|
|
11
|
+
.venv/
|
|
12
|
+
|
|
13
|
+
# IDE
|
|
14
|
+
.vscode/
|
|
15
|
+
|
|
16
|
+
# OS
|
|
17
|
+
.DS_Store
|
|
18
|
+
Thumbs.db
|
|
19
|
+
|
|
20
|
+
# Testing
|
|
21
|
+
.import_linter_cache/
|
|
22
|
+
.pytest_cache/
|
|
23
|
+
.ruff_cache/
|
|
24
|
+
.mypy_cache/
|
|
25
|
+
.lisa-tmp/
|
|
26
|
+
coverage/
|
|
27
|
+
.coverage
|
|
28
|
+
.coverage.*
|
|
29
|
+
htmlcov/
|
|
30
|
+
|
|
31
|
+
# Outputs
|
|
32
|
+
artifacts/
|
|
33
|
+
.e2e/
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: lisa-harness-e2e
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: End-to-end scenario harness: drives a full Lisa runtime through pack workflows
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: lisa-harness-kernel
|
|
7
|
+
Requires-Dist: lisa-harness-runtime
|
|
8
|
+
Requires-Dist: lisa-harness-sdk
|
|
9
|
+
Requires-Dist: python-dotenv>=1.0
|
|
10
|
+
Requires-Dist: pyyaml>=6.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# e2etest — live end-to-end pack scenarios (`lisa-e2e`)
|
|
2
|
+
|
|
3
|
+
`e2etest` drives a **real** `HostRuntime` with a **live model** through a
|
|
4
|
+
scripted pack workflow, then judges the outcome from on-disk evidence. It is
|
|
5
|
+
the layer above [`packstest`](../packstest/README.md):
|
|
6
|
+
|
|
7
|
+
| Layer | Drives | Model | Answers |
|
|
8
|
+
| --- | --- | --- | --- |
|
|
9
|
+
| `packstest` evals | one tool in-process | none | "Does the tool honor its contract?" |
|
|
10
|
+
| `lisa-e2e` scenarios | the whole runtime + `handle_message` | live | "Can Lisa actually complete the workflow?" |
|
|
11
|
+
|
|
12
|
+
Use `packstest` for fast, deterministic regressions. Use `lisa-e2e` when the
|
|
13
|
+
question is whether routing, handoffs, approvals, policy, memory, sandboxing,
|
|
14
|
+
and the pack's tools work together for a real model.
|
|
15
|
+
|
|
16
|
+
See the [documentation index](../docs/README.md) and [testing](../docs/TESTING.md).
|
|
17
|
+
The workspace installs `lisa-e2e`; the framework's current publish workflow
|
|
18
|
+
does not publish the `lisa-harness-e2e` distribution.
|
|
19
|
+
|
|
20
|
+
## How a run works
|
|
21
|
+
|
|
22
|
+
```mermaid
|
|
23
|
+
flowchart LR
|
|
24
|
+
S[scenario.yml] --> V[packs venv<br/>editable installs]
|
|
25
|
+
S --> R[run dir<br/>isolated data dir + fixture repo]
|
|
26
|
+
V --> D[driver: build_runtime + handle_message]
|
|
27
|
+
R --> D
|
|
28
|
+
D -->|turns, /approve, /deny| D
|
|
29
|
+
D --> O[oracles over evidence]
|
|
30
|
+
O --> B[verdict.json + transcript.md]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
1. **Packs venv** — `.e2e/cache/packs-venv-<hash>` holds editable installs of
|
|
34
|
+
kernel/sdk/mcp_server plus the scenario's `packs`, sourced from
|
|
35
|
+
[`packs.yml`](packs.yml). Editable means a pack fix is live on the next run.
|
|
36
|
+
It is rebuilt only when the pack set or source paths change.
|
|
37
|
+
2. **Run dir** — `.e2e/runs/<scenario-id>/<UTC stamp>/` (gitignored). Contains
|
|
38
|
+
a fresh `data/` dir used as `LISA_DATA_DIR`, the fixture repo copied to
|
|
39
|
+
`workspace/<name>` with a git history and a bare `remote.git`, and any
|
|
40
|
+
`fixture.data_files` tree copied into the storage sandbox (`data/data/files`).
|
|
41
|
+
Only `config/models.yaml` and `.env` are read from the base data dir
|
|
42
|
+
(`~/.lisa`, override with `LISA_E2E_BASE_DATA_DIR`).
|
|
43
|
+
3. **Driver** — boots the runtime in-process, sends each scripted turn, then
|
|
44
|
+
follows up until the turn finishes:
|
|
45
|
+
- approval pending → `/approve <id>` or `/deny <id>` per the scenario rules
|
|
46
|
+
(the real approval path, not a policy bypass);
|
|
47
|
+
- yield → `yield_reply`, only if `max_yield_replies` allows;
|
|
48
|
+
- pause/stop exit → `continue_message`, only if `max_continues` allows.
|
|
49
|
+
4. **Oracles** — assertions over the thread log, memory DB, code-change
|
|
50
|
+
artifacts, runtime log, and git. They never trust the model's prose.
|
|
51
|
+
5. **Bundle** — `verdict.json` (first failure first), `transcript.md`
|
|
52
|
+
(exchanges + per-turn receipts), `outcome.json`, and the whole `data/` dir.
|
|
53
|
+
|
|
54
|
+
## Running the existing scenarios
|
|
55
|
+
|
|
56
|
+
From the framework checkout:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uv sync --frozen --extra dev # once
|
|
60
|
+
uv run --frozen lisa-e2e run sdk/packs/recovery/tests/e2e/skip_all.scenario.yml
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This bundled scenario needs the configured base roster/provider **and**
|
|
64
|
+
external bash/bwrap/markdown/personas sources named in its `packs` list.
|
|
65
|
+
Inspect the scenario and `packs.yml` before running; a scenario located
|
|
66
|
+
in this checkout is not necessarily self-contained.
|
|
67
|
+
|
|
68
|
+
| Scenario | Workflow |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `coding/review-fix-commit` | knowledge-capturing review → worktree → patch → required check → commit, verified by re-running the tests outside Lisa |
|
|
71
|
+
| `coding/feature-design-implement` | product review → feature design doc → multi-file implementation in a worktree → required check → commit, judged by hidden acceptance tests |
|
|
72
|
+
| `coding/roadmap-chunk-implement` | open-ended product review → roadmap → model-proposed design overridden by operator pushback → terse "start the first chunk" that must recover the agreed design → checked commit, same hidden acceptance tests |
|
|
73
|
+
| `finances/categorize-and-close` | import bank + card exports → survey + proposed categories → user decisions (incl. a new category) → final ledger, verified by re-reading the ledger outside Lisa |
|
|
74
|
+
|
|
75
|
+
Options:
|
|
76
|
+
|
|
77
|
+
- `--rebuild-venv` — force a packs-venv rebuild (after editing `packs.yml` or a
|
|
78
|
+
pack's dependencies; source edits do not need it).
|
|
79
|
+
- `--runs-dir DIR` — write bundles elsewhere.
|
|
80
|
+
|
|
81
|
+
Exit codes: `0` pass, `1` an oracle failed, `2` setup error (bad scenario,
|
|
82
|
+
missing base `models.yaml`, venv build failure).
|
|
83
|
+
|
|
84
|
+
A run takes minutes and costs real tokens. Live runs vary between attempts, so
|
|
85
|
+
confirm a fix with more than one passing run before calling it done.
|
|
86
|
+
|
|
87
|
+
## Debugging a failed run
|
|
88
|
+
|
|
89
|
+
1. Read `verdict.json` → `first_failure` (oracle, args, detail).
|
|
90
|
+
2. Read `transcript.md` for the exchange sequence, approvals, and the
|
|
91
|
+
per-turn receipt list.
|
|
92
|
+
3. Go to the raw evidence in `data/`:
|
|
93
|
+
- `logs/runtime.jsonl` — warnings/errors (operation timeouts, validation
|
|
94
|
+
failures, truncation retries);
|
|
95
|
+
- `data/threads/*.current.jsonl` — every turn's transcript, tool calls with
|
|
96
|
+
exact arguments, `turn_input` requests, provenance;
|
|
97
|
+
- `state/code-change-artifacts.json` — WorkProduct state, check records;
|
|
98
|
+
- `data/memory.db` — what memory actually stored.
|
|
99
|
+
4. Reproduce the failing piece **deterministically** before fixing: replay the
|
|
100
|
+
exact tool arguments from the thread against `git`/the tool, or boot the
|
|
101
|
+
runtime and call the pool directly (no model, seconds):
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
rt = build_runtime(host_kind="e2e", data_dir=..., session_context=..., in_process_mcp=True)
|
|
105
|
+
await rt.start()
|
|
106
|
+
await rt.resources.mcp_tool_pool.call_tool("memory.add", {...})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
5. Fix the root cause (pack, kernel, or runtime), add a regression test at the
|
|
110
|
+
lowest layer that reproduces it, then rerun the scenario.
|
|
111
|
+
|
|
112
|
+
Never loosen an oracle to get green. If the harness mis-modelled a contract,
|
|
113
|
+
fix the harness and add a test in [`tests/`](tests).
|
|
114
|
+
|
|
115
|
+
## Writing a scenario
|
|
116
|
+
|
|
117
|
+
Scenarios live **in the pack**, next to its tests:
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
<pack>/tests/e2e/
|
|
121
|
+
conftest.py # collect_ignore_glob = ["fixtures/*"]
|
|
122
|
+
<name>.scenario.yml
|
|
123
|
+
session_context.md # template; ${...} placeholders are substituted
|
|
124
|
+
fixtures/<repo>/ # optional seed repo (planted defect, sample data)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`conftest.py` keeps pytest from collecting fixture code (which is deliberately
|
|
128
|
+
broken). Give fixture repos a realistic `.gitignore`.
|
|
129
|
+
|
|
130
|
+
### Scenario file reference
|
|
131
|
+
|
|
132
|
+
```yaml
|
|
133
|
+
id: <pack>/<workflow> # bundle dir name; defaults to the file stem
|
|
134
|
+
session_context: session_context.md
|
|
135
|
+
fixture:
|
|
136
|
+
repo: fixtures/<repo> # optional; becomes a git repo with a bare origin
|
|
137
|
+
data_files: fixtures/<tree> # optional; copied into <data_dir>/data/files
|
|
138
|
+
packs: [lisa-pack-...] # keys of e2etest/packs.yml installed into the venv
|
|
139
|
+
model:
|
|
140
|
+
script: model/<name>.py # optional; scripted model instead of the live LLM
|
|
141
|
+
policy: policy.yaml # optional; default is empty `mode: ask`
|
|
142
|
+
env: {NAME: value} # optional extra process env
|
|
143
|
+
turns: # user messages, in order
|
|
144
|
+
- "..."
|
|
145
|
+
approvals:
|
|
146
|
+
approve: [tool_id, ...] # "*" approves everything not denied
|
|
147
|
+
deny: [tool_id, ...] # unlisted targets are denied
|
|
148
|
+
max_decisions: 20
|
|
149
|
+
budgets:
|
|
150
|
+
max_messages: 16 # every message sent, including /approve
|
|
151
|
+
max_continues: 0 # replies to pause exits
|
|
152
|
+
max_yield_replies: 0 # replies to model questions
|
|
153
|
+
wall_clock_s: 1800
|
|
154
|
+
continue_message: continue
|
|
155
|
+
yield_reply: Proceed with your best judgment.
|
|
156
|
+
oracles:
|
|
157
|
+
<phase>: # free-form grouping shown in the verdict
|
|
158
|
+
- <oracle_name>: {args}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`session_context.md` placeholders: `${WORKSPACE}` (fixture repo path),
|
|
162
|
+
`${WORKSPACE_PARENT}` (where coding worktrees go), `${RUN_DIR}`. Mirror the
|
|
163
|
+
real session the pack targets, but name it uniquely (`name: e2e-<pack>`) so
|
|
164
|
+
live policy rules keyed on a workspace id do not apply.
|
|
165
|
+
|
|
166
|
+
### Scripted model (token-free repros)
|
|
167
|
+
|
|
168
|
+
`model.script` replaces every provider gateway with a Python module, so a
|
|
169
|
+
scenario replays a fixed tool sequence in seconds at zero token cost.
|
|
170
|
+
Everything below the model (routing, policy, approvals, packs, sandbox,
|
|
171
|
+
persistence) is production code. Use it to pin down a bug a live run
|
|
172
|
+
surfaced, not to judge model behavior.
|
|
173
|
+
|
|
174
|
+
The module defines `respond(turn: ScriptTurn) -> Reply` and, optionally,
|
|
175
|
+
`auxiliary(turn)` for tool-less requests (distillers, classifiers, titles).
|
|
176
|
+
Those otherwise receive `{}` or empty text, which some operations log as
|
|
177
|
+
errors. `ScriptTurn` exposes `tool_names`, `user_text`, `count(name)`, and
|
|
178
|
+
`last(name)`. `last(name).data` is the JSON object inside that tool's
|
|
179
|
+
result, so ids and digests flow from real tool output. Return
|
|
180
|
+
`Reply.call(tool, **args)`, `Reply.calls(...)`, or `Reply.text(...)`; raise
|
|
181
|
+
`ScriptError` when the transcript is not what the script expects. Add
|
|
182
|
+
`"model/*"` to the pack's `conftest.py` `collect_ignore_glob`. See
|
|
183
|
+
`coding/tests/e2e/edit_after_check.scenario.yml` in the coding pack.
|
|
184
|
+
|
|
185
|
+
### Oracle reference
|
|
186
|
+
|
|
187
|
+
Conversation hygiene:
|
|
188
|
+
|
|
189
|
+
| Oracle | Args | Passes when |
|
|
190
|
+
| --- | --- | --- |
|
|
191
|
+
| `not_aborted` | — | no budget was exceeded |
|
|
192
|
+
| `no_turn_errors` | — | no exchange errored |
|
|
193
|
+
| `no_operator_exit` | — | no pause/stop/terminate exit |
|
|
194
|
+
| `no_unanswered_yield` | — | the conversation did not end on a question |
|
|
195
|
+
| `no_runtime_errors` | `ignore: [regex]` | no error-level record in `runtime.jsonl` |
|
|
196
|
+
| `max_exchanges` | `n` | at most `n` messages were sent |
|
|
197
|
+
|
|
198
|
+
Capability and knowledge:
|
|
199
|
+
|
|
200
|
+
| Oracle | Args | Passes when |
|
|
201
|
+
| --- | --- | --- |
|
|
202
|
+
| `receipt` | `capability` (id or list), `min` | ≥ `min` effect receipts for those capabilities |
|
|
203
|
+
| `memory_recorded` | `mentions_any`, `min` | ≥ `min` stored memories mention any term |
|
|
204
|
+
|
|
205
|
+
Code change (coding pack WorkProduct + git; needs a `fixture.repo`):
|
|
206
|
+
|
|
207
|
+
| Oracle | Args | Passes when |
|
|
208
|
+
| --- | --- | --- |
|
|
209
|
+
| `workproduct_committed` | — | an artifact has a `commit_sha` |
|
|
210
|
+
| `branch_prefix` | `prefix` | the committed branch starts with `prefix` |
|
|
211
|
+
| `main_untouched` | — | local and remote `main` equal the fixture's initial commit |
|
|
212
|
+
| `diff_touches` | `paths`, `exact`, `allow` | the commit changes `paths` (and nothing outside `paths` + `allow` when `exact`) |
|
|
213
|
+
| `checks_current` | — | every required check passed at the artifact revision and committed tree |
|
|
214
|
+
| `worktree_clean` | — | the owned worktree is clean and at the commit |
|
|
215
|
+
| `baseline_fails` | `argv`, `timeout_s` | `argv` fails on the fixture baseline (the defect is real) |
|
|
216
|
+
| `independent_check` | `argv`, `timeout_s`, `restore` | `argv` passes on a fresh checkout of the commit, outside Lisa, with `restore` paths reset to the fixture baseline |
|
|
217
|
+
|
|
218
|
+
`baseline_fails` and `independent_check` run with cwd at the checkout and
|
|
219
|
+
expand `${SCENARIO_DIR}` and `${RUN_DIR}` in `argv`, so hidden acceptance
|
|
220
|
+
tests can live beside the scenario where the model cannot see or edit them.
|
|
221
|
+
|
|
222
|
+
Storage sandbox (packs whose work product lives under `data/files`):
|
|
223
|
+
|
|
224
|
+
| Oracle | Args | Passes when |
|
|
225
|
+
| --- | --- | --- |
|
|
226
|
+
| `data_check` | `argv`, `timeout_s` | `argv` exits 0 with cwd at the storage root; `${SCENARIO_DIR}`, `${FILES}`, `${RUN_DIR}` expand |
|
|
227
|
+
|
|
228
|
+
An unknown oracle name fails the run, so typos surface immediately.
|
|
229
|
+
|
|
230
|
+
### Design rules for good scenarios
|
|
231
|
+
|
|
232
|
+
- **Assert outcomes, not prose.** Prefer receipts, stored records, git state,
|
|
233
|
+
and an independent check over text matching.
|
|
234
|
+
- **Prove the fixture.** Pair every "fixed" oracle with a baseline oracle that
|
|
235
|
+
shows the starting state was actually broken.
|
|
236
|
+
- **Allow tests, judge with the originals.** A fix may add regression tests:
|
|
237
|
+
list test files under `diff_touches.allow` and pair it with an
|
|
238
|
+
`independent_check` that `restore`s them, so edited tests cannot mask an
|
|
239
|
+
unfixed defect.
|
|
240
|
+
- **Verify outside Lisa.** End-state checks (`independent_check`, a re-read of
|
|
241
|
+
the ledger/file) must not reuse the tool path under test.
|
|
242
|
+
- **Exercise approvals for real.** Approve the effectful tools the workflow
|
|
243
|
+
needs; deny the ones it must not reach (e.g. `coding_push`).
|
|
244
|
+
- **Default to strict budgets.** `max_continues: 0` and `max_yield_replies: 0`
|
|
245
|
+
make stalls and capability gaps fail loudly instead of being papered over.
|
|
246
|
+
- **Keep fixtures tiny and deterministic.** Stdlib-only test commands, no
|
|
247
|
+
network, one planted defect per workflow.
|
|
248
|
+
- **Judge features with hidden tests.** For feature work, keep the acceptance
|
|
249
|
+
suite outside the fixture (`${SCENARIO_DIR}/acceptance/`) and pair a
|
|
250
|
+
`baseline_fails` with an `independent_check` on it.
|
|
251
|
+
|
|
252
|
+
### Adding a new pack to the harness
|
|
253
|
+
|
|
254
|
+
1. Add the pack's source path to [`packs.yml`](packs.yml) (relative to that
|
|
255
|
+
file). The `packs` list in a scenario may only name keys from it.
|
|
256
|
+
2. If the scenario needs an assertion the oracle table lacks, add it to
|
|
257
|
+
[`oracles.py`](src/e2etest/oracles.py) with the `@oracle("name")`
|
|
258
|
+
decorator and a test in [`tests/test_harness.py`](tests/test_harness.py)
|
|
259
|
+
that covers both pass and fail.
|
|
260
|
+
|
|
261
|
+
## Gotchas
|
|
262
|
+
|
|
263
|
+
- The coding pack only runs checks under a bwrap floor with
|
|
264
|
+
`execution: sandbox` and `persistence: direct-write`, and the worktree parent
|
|
265
|
+
(`${WORKSPACE_PARENT}`) must be bound `rw`.
|
|
266
|
+
- The base `.env` is loaded without overriding the process env; location
|
|
267
|
+
overrides (`LISA_PROFILE`, `LISA_THREAD_DIR`, `LISA_SESSION_CONTEXT_PATH`,
|
|
268
|
+
`LISA_MCP_LOG_PATH`) are cleared so a run can never touch live state.
|
|
269
|
+
- Run one scenario per process; the runtime keeps process-global registries.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Pack sources installed (editable) into the e2e packs venv.
|
|
2
|
+
# Paths are relative to this file. Editable installs mean a pack fix is live on
|
|
3
|
+
# the next scenario run without a reinstall.
|
|
4
|
+
framework:
|
|
5
|
+
- ../kernel
|
|
6
|
+
- ../sdk
|
|
7
|
+
- ../mcp_server
|
|
8
|
+
packs:
|
|
9
|
+
lisa-pack-bash: ../../lisa-packs/bash
|
|
10
|
+
lisa-pack-bwrap: ../../lisa-packs/bwrap
|
|
11
|
+
lisa-pack-coding: ../../packs-wip/coding
|
|
12
|
+
lisa-pack-finances: ../../packs-wip/finances
|
|
13
|
+
lisa-pack-markdown: ../../lisa-packs/markdown
|
|
14
|
+
lisa-pack-personas: ../../lisa-packs/personas
|
|
15
|
+
lisa-pack-recovery: ../sdk/packs/recovery
|
|
16
|
+
lisa-pack-search: ../../lisa-packs/search
|
|
17
|
+
lisa-pack-serper: ../../lisa-packs/serper
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[tool.hatch.build.targets.wheel]
|
|
6
|
+
packages = ["src/e2etest"]
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "lisa-harness-e2e"
|
|
10
|
+
version = "0.1.0"
|
|
11
|
+
description = "End-to-end scenario harness: drives a full Lisa runtime through pack workflows"
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
|
|
14
|
+
dependencies = [
|
|
15
|
+
"lisa-harness-kernel",
|
|
16
|
+
"lisa-harness-sdk",
|
|
17
|
+
"lisa-harness-runtime",
|
|
18
|
+
"python-dotenv>=1.0",
|
|
19
|
+
"pyyaml>=6.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
lisa-e2e = "e2etest.cli:main"
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = [
|
|
27
|
+
"pytest>=8.0.0",
|
|
28
|
+
"pytest-asyncio>=0.23.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
pythonpath = ["src"]
|
|
33
|
+
testpaths = ["tests"]
|
|
34
|
+
|
|
35
|
+
[tool.uv]
|
|
36
|
+
package = true
|
|
37
|
+
|
|
38
|
+
[tool.uv.sources]
|
|
39
|
+
lisa-harness-kernel = { workspace = true }
|
|
40
|
+
lisa-harness-sdk = { workspace = true }
|
|
41
|
+
lisa-harness-runtime = { workspace = true }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""End-to-end scenario harness for Lisa packs (drives a full ``HostRuntime``)."""
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Write a run bundle: verdict, raw outcome, and a readable transcript."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import asdict
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from e2etest.oracles import Evidence, OracleResult
|
|
11
|
+
from e2etest.scenario import Scenario
|
|
12
|
+
|
|
13
|
+
_TEXT_LIMIT = 4000
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def verdict(scenario: Scenario, evidence: Evidence, results: list[OracleResult]) -> dict[str, Any]:
|
|
17
|
+
failed = [r for r in results if not r.passed]
|
|
18
|
+
return {
|
|
19
|
+
"scenario": scenario.scenario_id,
|
|
20
|
+
"scenario_file": str(scenario.source),
|
|
21
|
+
"run_dir": str(evidence.paths.run_dir),
|
|
22
|
+
"passed": not failed,
|
|
23
|
+
"first_failure": asdict(failed[0]) if failed else None,
|
|
24
|
+
"exchanges": len(evidence.outcome.exchanges),
|
|
25
|
+
"aborted": evidence.outcome.aborted,
|
|
26
|
+
"results": [asdict(r) for r in results],
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _transcript(evidence: Evidence) -> str:
|
|
31
|
+
lines = ["# Transcript", ""]
|
|
32
|
+
for ex in evidence.outcome.exchanges:
|
|
33
|
+
lines.append(f"## #{ex.index} {ex.kind} ({ex.duration_s}s)")
|
|
34
|
+
lines += ["", "```text", ex.message, "```", ""]
|
|
35
|
+
flags = []
|
|
36
|
+
if ex.approval:
|
|
37
|
+
flags.append(
|
|
38
|
+
f"approval: {ex.approval.get('target_id')} ({ex.approval.get('request_id')})"
|
|
39
|
+
)
|
|
40
|
+
if ex.operator_exit:
|
|
41
|
+
flags.append(f"operator_exit: {ex.operator_exit}")
|
|
42
|
+
if ex.yielded_question:
|
|
43
|
+
flags.append(f"yielded: {ex.yielded_question}")
|
|
44
|
+
flags += [f"error: {e}" for e in ex.errors]
|
|
45
|
+
lines += [f"- {f}" for f in flags]
|
|
46
|
+
text = ex.text if len(ex.text) <= _TEXT_LIMIT else ex.text[:_TEXT_LIMIT] + "\n…[truncated]"
|
|
47
|
+
lines += ["", text, ""]
|
|
48
|
+
lines += ["# Thread entries", ""]
|
|
49
|
+
for entry in evidence.thread_entries:
|
|
50
|
+
receipts = [r.get("capability_id") for r in entry.get("effect_receipts") or ()]
|
|
51
|
+
provenance = entry.get("provenance") or {}
|
|
52
|
+
lines.append(
|
|
53
|
+
f"- turn {entry.get('turn')} mode={entry.get('mode_id')} "
|
|
54
|
+
f"handoff={entry.get('handoff_to') or '-'} model={provenance.get('model_id', '-')} "
|
|
55
|
+
f"exit={provenance.get('operator_exit') or '-'} receipts={receipts}"
|
|
56
|
+
)
|
|
57
|
+
return "\n".join(lines) + "\n"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def write_bundle(run_dir: Path, payload: dict[str, Any], evidence: Evidence) -> Path:
|
|
61
|
+
(run_dir / "outcome.json").write_text(
|
|
62
|
+
json.dumps(evidence.outcome.to_json(), indent=1), encoding="utf-8"
|
|
63
|
+
)
|
|
64
|
+
(run_dir / "transcript.md").write_text(_transcript(evidence), encoding="utf-8")
|
|
65
|
+
path = run_dir / "verdict.json"
|
|
66
|
+
path.write_text(json.dumps(payload, indent=1), encoding="utf-8")
|
|
67
|
+
return path
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""``lisa-e2e``: run one scenario end to end and write a verdict bundle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from e2etest.bundle import verdict, write_bundle
|
|
11
|
+
from e2etest.driver import run_conversation
|
|
12
|
+
from e2etest.environment import prepare_run
|
|
13
|
+
from e2etest.oracles import Evidence, evaluate
|
|
14
|
+
from e2etest.packs_venv import ensure_packs_venv
|
|
15
|
+
from e2etest.scenario import load_scenario
|
|
16
|
+
|
|
17
|
+
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
18
|
+
_EXIT_FAIL = 1
|
|
19
|
+
_EXIT_SETUP = 2
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _parser() -> argparse.ArgumentParser:
|
|
23
|
+
parser = argparse.ArgumentParser(prog="lisa-e2e", description=__doc__)
|
|
24
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
25
|
+
run = sub.add_parser("run", help="Run a scenario against a live model.")
|
|
26
|
+
run.add_argument("scenario", type=Path)
|
|
27
|
+
run.add_argument("--runs-dir", type=Path, default=_REPO_ROOT / ".e2e" / "runs")
|
|
28
|
+
run.add_argument("--rebuild-venv", action="store_true")
|
|
29
|
+
return parser
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _run(args: argparse.Namespace) -> int:
|
|
33
|
+
scenario = load_scenario(args.scenario)
|
|
34
|
+
venv = ensure_packs_venv(
|
|
35
|
+
scenario.packs, _REPO_ROOT / ".e2e" / "cache", rebuild=args.rebuild_venv
|
|
36
|
+
)
|
|
37
|
+
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
|
|
38
|
+
run_dir = (args.runs_dir / scenario.scenario_id.replace("/", "__") / stamp).resolve()
|
|
39
|
+
paths = prepare_run(scenario, run_dir, venv)
|
|
40
|
+
print(f"run dir: {run_dir}", flush=True)
|
|
41
|
+
outcome = run_conversation(scenario, paths, venv)
|
|
42
|
+
evidence = Evidence(paths, outcome)
|
|
43
|
+
payload = verdict(scenario, evidence, evaluate(scenario.oracles, evidence))
|
|
44
|
+
path = write_bundle(run_dir, payload, evidence)
|
|
45
|
+
for result in payload["results"]:
|
|
46
|
+
mark = "PASS" if result["passed"] else "FAIL"
|
|
47
|
+
first = result["detail"].splitlines()[0] if result["detail"] else ""
|
|
48
|
+
print(f"{mark} {result['phase']}.{result['name']}: {first}")
|
|
49
|
+
print(f"verdict: {'PASS' if payload['passed'] else 'FAIL'} → {path}")
|
|
50
|
+
return 0 if payload["passed"] else _EXIT_FAIL
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main(argv: list[str] | None = None) -> int:
|
|
54
|
+
args = _parser().parse_args(argv)
|
|
55
|
+
try:
|
|
56
|
+
return _run(args)
|
|
57
|
+
except (OSError, ValueError, RuntimeError) as exc:
|
|
58
|
+
print(f"setup error: {type(exc).__name__}: {exc}", file=sys.stderr)
|
|
59
|
+
return _EXIT_SETUP
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
sys.exit(main())
|