prettyplay 0.0.2__py3-none-any.whl
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.
- prettyplay/.usages/lifecycle.md +117 -0
- prettyplay/.usages/steps.md +169 -0
- prettyplay/__init__.py +18 -0
- prettyplay/cache/.usages/addressing.md +44 -0
- prettyplay/cache/.usages/budgets.md +51 -0
- prettyplay/cache/.usages/storage.md +32 -0
- prettyplay/cache/__init__.py +8 -0
- prettyplay/cache/budgets.py +93 -0
- prettyplay/cache/models.py +55 -0
- prettyplay/cache/store.py +276 -0
- prettyplay/cache/text.py +36 -0
- prettyplay/config/.usages/configuration.md +246 -0
- prettyplay/config/__init__.py +6 -0
- prettyplay/config/loader.py +336 -0
- prettyplay/config/models.py +288 -0
- prettyplay/driver/.usages/error_kinds.md +26 -0
- prettyplay/driver/.usages/plumbing.md +52 -0
- prettyplay/driver/__init__.py +11 -0
- prettyplay/driver/errors.py +76 -0
- prettyplay/driver/page.py +175 -0
- prettyplay/driver/session.py +330 -0
- prettyplay/engine/.usages/generation.md +157 -0
- prettyplay/engine/.usages/healing.md +61 -0
- prettyplay/engine/__init__.py +19 -0
- prettyplay/engine/attempts.py +80 -0
- prettyplay/engine/classification.py +70 -0
- prettyplay/engine/compliance.py +125 -0
- prettyplay/engine/execution.py +48 -0
- prettyplay/engine/generator.py +1110 -0
- prettyplay/engine/groups/.usages/recovery.md +71 -0
- prettyplay/engine/groups/__init__.py +7 -0
- prettyplay/engine/groups/diagnosis.py +128 -0
- prettyplay/engine/groups/outcome.py +55 -0
- prettyplay/engine/groups/recovery.py +491 -0
- prettyplay/engine/healer.py +149 -0
- prettyplay/engine/polling/.usages/settle.md +55 -0
- prettyplay/engine/polling/__init__.py +6 -0
- prettyplay/engine/polling/settle.py +98 -0
- prettyplay/engine/polling/window.py +72 -0
- prettyplay/engine/renderer/.usages/memory.md +24 -0
- prettyplay/engine/renderer/.usages/rendering.md +44 -0
- prettyplay/engine/renderer/.usages/validation.md +20 -0
- prettyplay/engine/renderer/__init__.py +7 -0
- prettyplay/engine/renderer/memory.py +37 -0
- prettyplay/engine/renderer/render.py +184 -0
- prettyplay/engine/renderer/validation.py +76 -0
- prettyplay/engine/steering/.usages/steering.md +95 -0
- prettyplay/engine/steering/__init__.py +5 -0
- prettyplay/engine/steering/steering.py +765 -0
- prettyplay/engine/text.py +41 -0
- prettyplay/executor.py +654 -0
- prettyplay/failures/.usages/taxonomy.md +131 -0
- prettyplay/failures/__init__.py +25 -0
- prettyplay/failures/errors.py +359 -0
- prettyplay/groups.py +276 -0
- prettyplay/llm/.usages/classification.md +31 -0
- prettyplay/llm/.usages/providers.md +127 -0
- prettyplay/llm/.usages/transport.md +81 -0
- prettyplay/llm/__init__.py +38 -0
- prettyplay/llm/_request.py +823 -0
- prettyplay/llm/anthropic_provider.py +468 -0
- prettyplay/llm/models.py +273 -0
- prettyplay/llm/openai_provider.py +444 -0
- prettyplay/llm/provider.py +279 -0
- prettyplay/reporting/.usages/hooks.md +61 -0
- prettyplay/reporting/__init__.py +6 -0
- prettyplay/reporting/hooks.py +47 -0
- prettyplay/reporting/reporter.py +70 -0
- prettyplay/runtime.py +81 -0
- prettyplay/scenario.py +469 -0
- prettyplay-0.0.2.dist-info/METADATA +515 -0
- prettyplay-0.0.2.dist-info/RECORD +75 -0
- prettyplay-0.0.2.dist-info/WHEEL +5 -0
- prettyplay-0.0.2.dist-info/licenses/LICENSE +28 -0
- prettyplay-0.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""The error-text policy of the engine: full typed failure text for reports and requests."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
#: The header of the bulky page-state section Playwright appends to a failed
|
|
6
|
+
#: expectation message — column zero; the section runs to the message end.
|
|
7
|
+
_ARIA_SNAPSHOT_HEADER = re.compile(r"^Aria snapshot:[ \t]*$", re.MULTILINE)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _strip_aria_snapshot(text: str) -> str:
|
|
11
|
+
"""Drop the aria-snapshot section Playwright appends to a failed check message.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
text: the full message text of the step-code exception.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
The text before the column-zero ``Aria snapshot:`` header, trailing
|
|
18
|
+
whitespace trimmed; text without the header returns unchanged.
|
|
19
|
+
"""
|
|
20
|
+
header = _ARIA_SNAPSHOT_HEADER.search(text)
|
|
21
|
+
|
|
22
|
+
return text[: header.start()].rstrip() if header is not None else text
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def format_step_error(exc: Exception) -> str:
|
|
26
|
+
"""Format the full failure text of a step-code exception.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
exc: the exception raised by the failed step or candidate branch.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The full formatted failure text — a failed check yields its message
|
|
33
|
+
with no prefix, the trailing aria-snapshot section excluded; any
|
|
34
|
+
other failure carries its type name; a message-less check yields an
|
|
35
|
+
empty text, a message-less error the bare type name.
|
|
36
|
+
"""
|
|
37
|
+
text = _strip_aria_snapshot(str(exc))
|
|
38
|
+
if isinstance(exc, AssertionError):
|
|
39
|
+
return text
|
|
40
|
+
|
|
41
|
+
return f"{type(exc).__name__}: {text}" if text else type(exc).__name__
|
prettyplay/executor.py
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
"""The step cycle: cache hit — settle; miss — generate or the strict stop; failure — heal, recover, steer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from typing import TYPE_CHECKING, NoReturn
|
|
8
|
+
|
|
9
|
+
from .cache import CachedStep, RunBudgets, StepCache, StepIdentity, normalize_step_text
|
|
10
|
+
from .config import PrettyConfig
|
|
11
|
+
from .driver import PageFacade
|
|
12
|
+
from .engine import StepGenerator, StepHealer, classify_step_failure, format_step_error, run_step_code
|
|
13
|
+
from .engine.attempts import OUTCOME_ORIGINAL, StepAttempt
|
|
14
|
+
from .engine.execution import bind_step_inputs
|
|
15
|
+
from .engine.groups import GroupRecovery, GroupStepOutcome
|
|
16
|
+
from .engine.polling import SettleWindow, settle
|
|
17
|
+
from .engine.renderer import PreparedStep, StepMemory, render_step, validate_step_result
|
|
18
|
+
from .engine.steering import StepSteering
|
|
19
|
+
from .failures import FailureVerdict, IncurableStepError, LLMUnavailableError, ProductDefectError
|
|
20
|
+
from .llm import LLMProvider, ScenarioStep
|
|
21
|
+
from .reporting import StepReporter
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from .groups import StepGroup
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger("prettyplay")
|
|
27
|
+
|
|
28
|
+
#: Strict-mode cache miss — generation is forbidden, so the miss is incurable.
|
|
29
|
+
_STRICT_MISS_REASON = "strict mode forbids generation — the step is missing from the cache"
|
|
30
|
+
|
|
31
|
+
#: Strict-mode terminal failure produced by the quiet verdict skip.
|
|
32
|
+
_STRICT_NO_VERDICT_REASON = "the step failed in strict mode without an llm verdict"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _read_url(page: PageFacade) -> str:
|
|
36
|
+
"""Read the page URL for the replay bracket; never kills the step.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
page: the page facade handle of the current test.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
The current URL of the page, or the empty string when the read
|
|
43
|
+
fails — the step itself proceeds normally.
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
return page.url
|
|
47
|
+
except Exception:
|
|
48
|
+
return ""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class StepExecutor:
|
|
52
|
+
"""The owner of the step cycle of one test.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
cache_key: the context key of the owning test object.
|
|
56
|
+
_cache: the step cache of the test.
|
|
57
|
+
_generator: the generation engine of the cycle.
|
|
58
|
+
_healer: the healing engine of the cycle — ordinary steps only.
|
|
59
|
+
_steering: the interactive steering of terminally stuck steps.
|
|
60
|
+
_recovery: the group recovery engine of failed group steps.
|
|
61
|
+
_reporter: the visibility point of the test.
|
|
62
|
+
_config: project settings; the strict switch picks the replay-only
|
|
63
|
+
path, the interactive switch the steering gate.
|
|
64
|
+
_provider: the LLM port implementation of the strict classification.
|
|
65
|
+
_scenario: the typed records of the previous steps of this test.
|
|
66
|
+
_memory: the per-test memory of captured observations — constructed
|
|
67
|
+
here once, read by every render, grown only at acceptance points.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__( # noqa: PLR0913, PLR0917 — the signature is fixed by the root cell contract
|
|
71
|
+
self,
|
|
72
|
+
cache_key: str,
|
|
73
|
+
cache: StepCache,
|
|
74
|
+
generator: StepGenerator,
|
|
75
|
+
healer: StepHealer,
|
|
76
|
+
steering: StepSteering,
|
|
77
|
+
recovery: GroupRecovery,
|
|
78
|
+
budgets: RunBudgets, # noqa: ARG002 — spent by the engine; kept for contract symmetry
|
|
79
|
+
reporter: StepReporter,
|
|
80
|
+
config: PrettyConfig,
|
|
81
|
+
provider: LLMProvider,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Keep the collaborators of the step cycle, reset the scenario context and build the test memory.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
cache_key: the context key of the owning test object.
|
|
87
|
+
cache: the step cache of the test.
|
|
88
|
+
generator: the generation engine of the cycle.
|
|
89
|
+
healer: the healing engine of the cycle — invoked only for
|
|
90
|
+
ordinary steps of a non-strict run.
|
|
91
|
+
steering: the interactive steering of terminally stuck steps —
|
|
92
|
+
invoked only on a non-strict interactive run.
|
|
93
|
+
recovery: the group recovery engine of failed group steps —
|
|
94
|
+
invoked only on a non-strict run for a step executed inside
|
|
95
|
+
a group.
|
|
96
|
+
budgets: the per-test attempt registry; attempts are spent
|
|
97
|
+
by the engine, not by the executor.
|
|
98
|
+
reporter: the visibility point of the test.
|
|
99
|
+
config: project settings; the strict switch picks the
|
|
100
|
+
replay-only path, the interactive switch the steering gate,
|
|
101
|
+
and the classification prompt feeds the strict path.
|
|
102
|
+
provider: the LLM port implementation of the strict classification;
|
|
103
|
+
a lightweight object — no SDK client, no credentials.
|
|
104
|
+
"""
|
|
105
|
+
self.cache_key = cache_key
|
|
106
|
+
self._cache = cache
|
|
107
|
+
self._generator = generator
|
|
108
|
+
self._healer = healer
|
|
109
|
+
self._steering = steering
|
|
110
|
+
self._recovery = recovery
|
|
111
|
+
self._reporter = reporter
|
|
112
|
+
self._config = config
|
|
113
|
+
self._provider = provider
|
|
114
|
+
self._scenario: list[ScenarioStep] = [] # test scenario context
|
|
115
|
+
self._memory = StepMemory() # the per-test memory of captured observations
|
|
116
|
+
|
|
117
|
+
def execute( # noqa: PLR0913, PLR0917 — the signature is fixed by the root cell contract
|
|
118
|
+
self,
|
|
119
|
+
step_text: str,
|
|
120
|
+
step_type: str,
|
|
121
|
+
page: PageFacade,
|
|
122
|
+
group: StepGroup | None = None,
|
|
123
|
+
tries: int | None = None,
|
|
124
|
+
delay: float | None = None,
|
|
125
|
+
vars: dict[str, str] | None = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Run one step through the full cycle.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
step_text: the sentence of the step as written by the engineer —
|
|
131
|
+
the template source and the addressing artifact; never
|
|
132
|
+
rendered into a request.
|
|
133
|
+
step_type: the kind of the step sentence ({action, assertion}).
|
|
134
|
+
page: the live page facade of the current test.
|
|
135
|
+
group: the authoring group the step executes inside; ``None`` —
|
|
136
|
+
an ordinary step. A group step carries the group framing and
|
|
137
|
+
its failures route to the group recovery; strict mode keeps
|
|
138
|
+
the classification-only path for every step alike.
|
|
139
|
+
tries: the declared retry count of this execution; ``None`` —
|
|
140
|
+
the time-bounded settle mode; a declared count switches the
|
|
141
|
+
settle window to the count-bounded mode.
|
|
142
|
+
delay: the declared quiet start pause in seconds; ``None`` — no
|
|
143
|
+
pause; passes quietly right after the started event, before
|
|
144
|
+
the render.
|
|
145
|
+
vars: the validated call-local string inputs passed by the
|
|
146
|
+
facade/group methods; rendered into the step's
|
|
147
|
+
``PreparedStep`` and never published to memory.
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
PrettyplayError: the template is an authoring error — malformed,
|
|
151
|
+
an invalid or duplicated capture name, an unavailable name or
|
|
152
|
+
an expectation declaring captures; raised before any browser
|
|
153
|
+
execution, cache access or identity construction.
|
|
154
|
+
ProductDefectError: the healed step verdict says the expectation
|
|
155
|
+
of the step is genuinely broken in the product; in strict
|
|
156
|
+
mode, a product_defect classification of a failed cached
|
|
157
|
+
step — or an assertion step failing without a verdict.
|
|
158
|
+
IncurableStepError: the step never generated successfully, or the
|
|
159
|
+
verdict says regeneration cannot help; in strict mode, any
|
|
160
|
+
cache miss or non-product-defect classification. Never raised
|
|
161
|
+
by an interactive non-strict step the steering dialog healed.
|
|
162
|
+
LLMUnavailableError: the provider service failed; no retry.
|
|
163
|
+
ComplianceVerdictError: the compliance gate could not obtain a
|
|
164
|
+
usable verdict — the provider answer did not parse into
|
|
165
|
+
findings; the executed candidate is never cached.
|
|
166
|
+
"""
|
|
167
|
+
outcome = "failed"
|
|
168
|
+
traced = False # this step's group trace record — appended exactly once, failed or passed
|
|
169
|
+
try:
|
|
170
|
+
self._reporter.emit("on_step_started", {"step_text": step_text, "step_type": step_type})
|
|
171
|
+
if delay is not None:
|
|
172
|
+
time.sleep(delay) # the quiet pre-step pause — no event, no log record
|
|
173
|
+
|
|
174
|
+
prepared = render_step(step_text, step_type, self._memory, vars) # authoring errors precede addressing
|
|
175
|
+
identity = StepIdentity(
|
|
176
|
+
cache_key=self.cache_key,
|
|
177
|
+
step_type=step_type,
|
|
178
|
+
normalized_text=normalize_step_text(step_text),
|
|
179
|
+
)
|
|
180
|
+
window = SettleWindow(self._config.polling_timeout, self._config.polling_delay, tries)
|
|
181
|
+
attempt_history: list[StepAttempt] = [] # the per-step history — created here, dies with the step
|
|
182
|
+
group_url_before = _read_url(page) if group is not None else "" # the group bracket opens
|
|
183
|
+
cached = self._cache.load(identity)
|
|
184
|
+
|
|
185
|
+
if cached is not None:
|
|
186
|
+
url_before = group_url_before if group is not None else _read_url(page)
|
|
187
|
+
try:
|
|
188
|
+
with bind_step_inputs(self._memory.snapshot(), prepared.inputs):
|
|
189
|
+
result = settle(run_step_code, cached.code, page, window)
|
|
190
|
+
captures = validate_step_result(prepared, result)
|
|
191
|
+
except Exception as error: # the replay failed or its result violated the contract — the mode reacts
|
|
192
|
+
traced = self._replay_failure(
|
|
193
|
+
cached,
|
|
194
|
+
format_step_error(error),
|
|
195
|
+
url_before,
|
|
196
|
+
_read_url(page), # the bracket closes on the failure record 0 anchors
|
|
197
|
+
prepared,
|
|
198
|
+
step_text,
|
|
199
|
+
step_type,
|
|
200
|
+
group,
|
|
201
|
+
tries,
|
|
202
|
+
delay,
|
|
203
|
+
identity,
|
|
204
|
+
attempt_history,
|
|
205
|
+
page,
|
|
206
|
+
window,
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
# acceptance = the validated successful execution — the gate never runs on replay
|
|
210
|
+
self._memory.publish(captures)
|
|
211
|
+
elif self._config.strict:
|
|
212
|
+
raise IncurableStepError(prepared.instruction, _STRICT_MISS_REASON, "", code="")
|
|
213
|
+
else:
|
|
214
|
+
traced = self._generate_step(
|
|
215
|
+
prepared,
|
|
216
|
+
step_text,
|
|
217
|
+
step_type,
|
|
218
|
+
group,
|
|
219
|
+
tries,
|
|
220
|
+
delay,
|
|
221
|
+
identity,
|
|
222
|
+
group_url_before,
|
|
223
|
+
attempt_history,
|
|
224
|
+
page,
|
|
225
|
+
window,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
self._scenario.append(
|
|
229
|
+
ScenarioStep(
|
|
230
|
+
sentence=step_text,
|
|
231
|
+
instruction=prepared.instruction,
|
|
232
|
+
group_prompt=group.prompt if group is not None else "",
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
if group is not None and not traced:
|
|
236
|
+
# the green step's own record — the bracket closes after the execution
|
|
237
|
+
self._append_group_trace(
|
|
238
|
+
group,
|
|
239
|
+
prepared,
|
|
240
|
+
step_text,
|
|
241
|
+
step_type,
|
|
242
|
+
tries,
|
|
243
|
+
delay,
|
|
244
|
+
identity,
|
|
245
|
+
group_url_before,
|
|
246
|
+
_read_url(page),
|
|
247
|
+
"passed",
|
|
248
|
+
)
|
|
249
|
+
self._reporter.emit("on_step_passed", {"step_text": step_text, "step_type": step_type})
|
|
250
|
+
outcome = "passed"
|
|
251
|
+
except Exception as error:
|
|
252
|
+
self._reporter.emit(
|
|
253
|
+
"on_step_failed",
|
|
254
|
+
{"step_text": step_text, "step_type": step_type, "error": str(error)}, # the render, verbatim
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if isinstance(error, (ProductDefectError, IncurableStepError)) and error.verdict is not None:
|
|
258
|
+
self._reporter.emit(
|
|
259
|
+
"on_step_verdict",
|
|
260
|
+
{
|
|
261
|
+
"step_text": step_text,
|
|
262
|
+
"category": error.verdict.category,
|
|
263
|
+
"explanation": error.verdict.explanation,
|
|
264
|
+
"recommendation": error.verdict.recommendation,
|
|
265
|
+
},
|
|
266
|
+
)
|
|
267
|
+
raise
|
|
268
|
+
finally:
|
|
269
|
+
# the closing event of every step — exactly once, after every other
|
|
270
|
+
# event, on pass, on failure and on the strict miss alike
|
|
271
|
+
self._reporter.emit(
|
|
272
|
+
"on_step_finished",
|
|
273
|
+
{"step_text": step_text, "step_type": step_type, "outcome": outcome},
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def _append_group_trace( # noqa: PLR0913, PLR0917 — the record fields are fixed by the groups cell contract
|
|
277
|
+
self,
|
|
278
|
+
group: StepGroup,
|
|
279
|
+
prepared: PreparedStep,
|
|
280
|
+
step_text: str,
|
|
281
|
+
step_type: str,
|
|
282
|
+
tries: int | None,
|
|
283
|
+
delay: float | None,
|
|
284
|
+
identity: StepIdentity,
|
|
285
|
+
url_before: str,
|
|
286
|
+
url_after: str,
|
|
287
|
+
outcome_label: str,
|
|
288
|
+
) -> None:
|
|
289
|
+
"""Append this step's verbatim trace record to the traces of its group.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
group: the authoring group the step executes inside.
|
|
293
|
+
prepared: the render product of the step — its instruction and
|
|
294
|
+
call-local inputs ride the record verbatim.
|
|
295
|
+
step_text: the raw sentence of the step, verbatim — the
|
|
296
|
+
re-render source of the row mechanics.
|
|
297
|
+
step_type: action or assertion.
|
|
298
|
+
tries: the declared retry count of this execution; ``None`` —
|
|
299
|
+
the global polling settings governed the step.
|
|
300
|
+
delay: the declared start pause of this execution; ``None`` —
|
|
301
|
+
no pause.
|
|
302
|
+
identity: the cache address of the step.
|
|
303
|
+
url_before: the page URL read before the execution.
|
|
304
|
+
url_after: the page URL read after the execution — on the
|
|
305
|
+
failure or the green completion.
|
|
306
|
+
outcome_label: passed or failed — exactly one immutable record
|
|
307
|
+
per executed group step.
|
|
308
|
+
"""
|
|
309
|
+
group.traces.append(
|
|
310
|
+
GroupStepOutcome(
|
|
311
|
+
sentence=step_text,
|
|
312
|
+
instruction=prepared.instruction,
|
|
313
|
+
step_type=step_type,
|
|
314
|
+
tries=tries,
|
|
315
|
+
delay=delay,
|
|
316
|
+
vars=prepared.inputs,
|
|
317
|
+
outcome=outcome_label,
|
|
318
|
+
url_before=url_before,
|
|
319
|
+
url_after=url_after,
|
|
320
|
+
identity=identity,
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
def _replay_failure( # noqa: PLR0913, PLR0917 — the threading is fixed by the root cell contract
|
|
325
|
+
self,
|
|
326
|
+
step: CachedStep,
|
|
327
|
+
error_text: str,
|
|
328
|
+
url_before: str,
|
|
329
|
+
url_after: str,
|
|
330
|
+
prepared: PreparedStep,
|
|
331
|
+
step_text: str,
|
|
332
|
+
step_type: str,
|
|
333
|
+
group: StepGroup | None,
|
|
334
|
+
tries: int | None,
|
|
335
|
+
delay: float | None,
|
|
336
|
+
identity: StepIdentity,
|
|
337
|
+
attempt_history: list[StepAttempt],
|
|
338
|
+
page: PageFacade,
|
|
339
|
+
window: SettleWindow,
|
|
340
|
+
) -> bool:
|
|
341
|
+
"""React to a failed cached execution by mode: strict classifies, a group recovers, ordinary heals.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
step: the cached step whose code failed.
|
|
345
|
+
error_text: the full formatted error text of the failure — a
|
|
346
|
+
result-contract violation arrives as the deterministic
|
|
347
|
+
violation text.
|
|
348
|
+
url_before: the page URL read before the cached execution.
|
|
349
|
+
url_after: the page URL read on the failure — the bracket of
|
|
350
|
+
record 0 and the failed trace record.
|
|
351
|
+
prepared: the render product of the failed step — every request
|
|
352
|
+
and raised failure carries the prepared instruction, never
|
|
353
|
+
the raw template sentence.
|
|
354
|
+
step_text: the raw sentence of the step, verbatim.
|
|
355
|
+
step_type: action or assertion.
|
|
356
|
+
group: the authoring group the step executes inside;
|
|
357
|
+
``None`` — an ordinary step.
|
|
358
|
+
tries: the declared retry count of this execution.
|
|
359
|
+
delay: the declared start pause of this execution.
|
|
360
|
+
identity: the cache address of the step.
|
|
361
|
+
attempt_history: the per-step attempt history record 0 anchors.
|
|
362
|
+
page: the live page facade of the test.
|
|
363
|
+
window: the settle window of this step's execution.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Whether this step's group trace record was appended; the strict
|
|
367
|
+
path never returns, it raises.
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
ProductDefectError: the strict classification or the healed
|
|
371
|
+
verdict says the expectation is genuinely broken.
|
|
372
|
+
IncurableStepError: the strict classification, the unrecoverable
|
|
373
|
+
group verdict or the unrecoverable heal — the steering
|
|
374
|
+
decline propagates the original failure object.
|
|
375
|
+
LLMUnavailableError: the provider service failed; no retry.
|
|
376
|
+
"""
|
|
377
|
+
if self._config.strict:
|
|
378
|
+
self._strict_failure(prepared.instruction, step_type, step, error_text, page) # always raises
|
|
379
|
+
|
|
380
|
+
if group is not None:
|
|
381
|
+
# the failed group step is traced once, then the group recovery decides
|
|
382
|
+
self._append_group_trace(
|
|
383
|
+
group, prepared, step_text, step_type, tries, delay, identity, url_before, url_after, "failed"
|
|
384
|
+
)
|
|
385
|
+
# record 0 — the anchor of the recovery: the original cached code with its
|
|
386
|
+
# replay error and the replay URL pair, composed before the recovery delegation
|
|
387
|
+
attempt_history.append(
|
|
388
|
+
StepAttempt(
|
|
389
|
+
code=step.code,
|
|
390
|
+
error=error_text,
|
|
391
|
+
outcome=OUTCOME_ORIGINAL,
|
|
392
|
+
url_before=url_before,
|
|
393
|
+
url_after=url_after,
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
self._recover_or_steer(group, prepared, step_type, identity, attempt_history, page, window)
|
|
397
|
+
|
|
398
|
+
return True
|
|
399
|
+
|
|
400
|
+
# record 0 — the anchor of the healing: the original cached code with its
|
|
401
|
+
# replay error and the replay URL pair, composed before the heal delegation
|
|
402
|
+
attempt_history.append(
|
|
403
|
+
StepAttempt(
|
|
404
|
+
code=step.code,
|
|
405
|
+
error=error_text,
|
|
406
|
+
outcome=OUTCOME_ORIGINAL,
|
|
407
|
+
url_before=url_before,
|
|
408
|
+
url_after=url_after,
|
|
409
|
+
)
|
|
410
|
+
)
|
|
411
|
+
try:
|
|
412
|
+
# healed = re-executed
|
|
413
|
+
self._healer.heal(
|
|
414
|
+
step,
|
|
415
|
+
error_text,
|
|
416
|
+
prepared,
|
|
417
|
+
step_type,
|
|
418
|
+
self._scenario,
|
|
419
|
+
page,
|
|
420
|
+
attempt_history,
|
|
421
|
+
window,
|
|
422
|
+
self._memory,
|
|
423
|
+
)
|
|
424
|
+
except IncurableStepError as failure: # the only kind the steering intercept serves
|
|
425
|
+
self._steer_or_raise(failure, identity, prepared, step_type, self._scenario, None, page, attempt_history)
|
|
426
|
+
|
|
427
|
+
return False
|
|
428
|
+
|
|
429
|
+
def _generate_step( # noqa: PLR0913, PLR0917 — the threading is fixed by the root cell contract
|
|
430
|
+
self,
|
|
431
|
+
prepared: PreparedStep,
|
|
432
|
+
step_text: str,
|
|
433
|
+
step_type: str,
|
|
434
|
+
group: StepGroup | None,
|
|
435
|
+
tries: int | None,
|
|
436
|
+
delay: float | None,
|
|
437
|
+
identity: StepIdentity,
|
|
438
|
+
url_before: str,
|
|
439
|
+
attempt_history: list[StepAttempt],
|
|
440
|
+
page: PageFacade,
|
|
441
|
+
window: SettleWindow,
|
|
442
|
+
) -> bool:
|
|
443
|
+
"""Generate a missing step; a group step's terminal failure routes to the recovery.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
prepared: the render product of the step — every request carries
|
|
447
|
+
the prepared instruction with its INPUTS and RESULTS blocks;
|
|
448
|
+
the accepted captures publish inside the engine loop.
|
|
449
|
+
step_text: the raw sentence of the step, verbatim.
|
|
450
|
+
step_type: action or assertion.
|
|
451
|
+
group: the authoring group the step executes inside;
|
|
452
|
+
``None`` — an ordinary step.
|
|
453
|
+
tries: the declared retry count of this execution.
|
|
454
|
+
delay: the declared start pause of this execution.
|
|
455
|
+
identity: the cache address of the step.
|
|
456
|
+
url_before: the page URL read before the cycle — the opening
|
|
457
|
+
side of the group bracket.
|
|
458
|
+
attempt_history: the per-step attempt history the generation
|
|
459
|
+
loop grows on its failures.
|
|
460
|
+
page: the live page facade of the test.
|
|
461
|
+
window: the settle window of this step's execution.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Whether this step's group trace record was appended. A group
|
|
465
|
+
step's generation failures arrive unclassified — the recovery
|
|
466
|
+
decides; an ordinary step's terminal failure goes to the
|
|
467
|
+
steering gate.
|
|
468
|
+
|
|
469
|
+
Raises:
|
|
470
|
+
ProductDefectError: a generation failure classified as a
|
|
471
|
+
genuine product defect.
|
|
472
|
+
IncurableStepError: the generation never succeeded — the
|
|
473
|
+
steering decline propagates the original failure object.
|
|
474
|
+
LLMUnavailableError: the provider service failed; no retry.
|
|
475
|
+
"""
|
|
476
|
+
try:
|
|
477
|
+
self._generator.generate(
|
|
478
|
+
identity,
|
|
479
|
+
prepared,
|
|
480
|
+
step_type,
|
|
481
|
+
self._scenario,
|
|
482
|
+
group.prompt if group is not None else None,
|
|
483
|
+
page,
|
|
484
|
+
attempt_history,
|
|
485
|
+
window,
|
|
486
|
+
self._memory,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
return False
|
|
490
|
+
except IncurableStepError as failure: # the only kind the steering intercept serves
|
|
491
|
+
if group is None:
|
|
492
|
+
self._steer_or_raise(
|
|
493
|
+
failure, identity, prepared, step_type, self._scenario, None, page, attempt_history
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
return False
|
|
497
|
+
|
|
498
|
+
# the unclassified generation failure of a group step — the recovery decides
|
|
499
|
+
self._append_group_trace(
|
|
500
|
+
group, prepared, step_text, step_type, tries, delay, identity, url_before, _read_url(page), "failed"
|
|
501
|
+
)
|
|
502
|
+
self._recover_or_steer(group, prepared, step_type, identity, attempt_history, page, window)
|
|
503
|
+
|
|
504
|
+
return True
|
|
505
|
+
|
|
506
|
+
def _recover_or_steer( # noqa: PLR0913, PLR0917 — the threading is fixed by the root cell contract
|
|
507
|
+
self,
|
|
508
|
+
group: StepGroup,
|
|
509
|
+
prepared: PreparedStep,
|
|
510
|
+
step_type: str,
|
|
511
|
+
identity: StepIdentity,
|
|
512
|
+
attempt_history: list[StepAttempt],
|
|
513
|
+
page: PageFacade,
|
|
514
|
+
window: SettleWindow,
|
|
515
|
+
) -> None:
|
|
516
|
+
"""Delegate a failed group step to the recovery; a healed return continues the step as a success.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
group: the authoring group of the failed step.
|
|
520
|
+
prepared: the render product of the failed step — the active
|
|
521
|
+
failure's instruction rides the diagnosis; the per-test
|
|
522
|
+
memory threads into every row re-render and publication.
|
|
523
|
+
step_type: action or assertion.
|
|
524
|
+
identity: the address of the failed step.
|
|
525
|
+
attempt_history: the per-step attempt history anchored by
|
|
526
|
+
record 0 on the cached-replay path, grown by the generation
|
|
527
|
+
loop on the miss path.
|
|
528
|
+
page: the live page facade of the test.
|
|
529
|
+
window: the settle window of this step's execution.
|
|
530
|
+
"""
|
|
531
|
+
try:
|
|
532
|
+
self._recovery.recover(
|
|
533
|
+
group.prompt,
|
|
534
|
+
group.traces,
|
|
535
|
+
prepared,
|
|
536
|
+
step_type,
|
|
537
|
+
self._scenario,
|
|
538
|
+
identity,
|
|
539
|
+
attempt_history,
|
|
540
|
+
page,
|
|
541
|
+
window,
|
|
542
|
+
self._memory,
|
|
543
|
+
)
|
|
544
|
+
except IncurableStepError as failure: # the only kind the steering intercept serves
|
|
545
|
+
self._steer_or_raise(
|
|
546
|
+
failure, identity, prepared, step_type, self._scenario, group.prompt, page, attempt_history
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
def _steer_or_raise( # noqa: PLR0913, PLR0917 — the threading is fixed by the root cell contract
|
|
550
|
+
self,
|
|
551
|
+
failure: IncurableStepError,
|
|
552
|
+
identity: StepIdentity,
|
|
553
|
+
prepared: PreparedStep,
|
|
554
|
+
step_type: str,
|
|
555
|
+
previous_steps: list[ScenarioStep],
|
|
556
|
+
group_prompt: str | None,
|
|
557
|
+
page: PageFacade,
|
|
558
|
+
attempt_history: list[StepAttempt],
|
|
559
|
+
) -> CachedStep:
|
|
560
|
+
"""Offer a terminal step failure to the steering dialog before it propagates.
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
failure: the terminal failure about to propagate.
|
|
564
|
+
identity: the address of the stuck step — the healed step is
|
|
565
|
+
written back under it.
|
|
566
|
+
prepared: the render product of the stuck step — the banner and
|
|
567
|
+
every guided request carry the prepared instruction; the
|
|
568
|
+
memory threads into the validated green turn's publication.
|
|
569
|
+
step_type: action or assertion — carried into every guided
|
|
570
|
+
regeneration request.
|
|
571
|
+
previous_steps: the typed scenario records of the previous steps
|
|
572
|
+
of the test, in execution order.
|
|
573
|
+
group_prompt: the group prompt of the stuck step's group;
|
|
574
|
+
``None`` — an ordinary step.
|
|
575
|
+
page: the live page facade of the test.
|
|
576
|
+
attempt_history: the per-step attempt history already grown by
|
|
577
|
+
the engine loop — the dialog joins it and appends the
|
|
578
|
+
records of its own turns.
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
The healed cached step — the step continues as a success.
|
|
582
|
+
|
|
583
|
+
Raises:
|
|
584
|
+
IncurableStepError: the dialog was declined or never opened —
|
|
585
|
+
the original failure object, unchanged. Only a non-strict
|
|
586
|
+
interactive run opens the dialog; the intercept wraps
|
|
587
|
+
exactly ``IncurableStepError``.
|
|
588
|
+
"""
|
|
589
|
+
if not (self._config.interactive and not self._config.strict):
|
|
590
|
+
raise failure
|
|
591
|
+
|
|
592
|
+
healed = self._steering.steer(
|
|
593
|
+
failure,
|
|
594
|
+
identity,
|
|
595
|
+
prepared,
|
|
596
|
+
step_type,
|
|
597
|
+
previous_steps,
|
|
598
|
+
group_prompt,
|
|
599
|
+
page,
|
|
600
|
+
attempt_history,
|
|
601
|
+
self._memory,
|
|
602
|
+
)
|
|
603
|
+
if healed is None: # quit, EOF, SIGINT, an unreadable stdin or a dead provider
|
|
604
|
+
raise failure
|
|
605
|
+
|
|
606
|
+
return healed
|
|
607
|
+
|
|
608
|
+
def _strict_failure(
|
|
609
|
+
self,
|
|
610
|
+
instruction: str,
|
|
611
|
+
step_type: str,
|
|
612
|
+
step: CachedStep,
|
|
613
|
+
error_text: str,
|
|
614
|
+
page: PageFacade,
|
|
615
|
+
) -> NoReturn:
|
|
616
|
+
"""Turn the failure of a cached step into a terminal error by classification only.
|
|
617
|
+
|
|
618
|
+
Args:
|
|
619
|
+
instruction: the prepared instruction of the failed step — the
|
|
620
|
+
classification request and the raised failure carry it,
|
|
621
|
+
never the raw template sentence.
|
|
622
|
+
step_type: the kind of the step sentence ({action, assertion}).
|
|
623
|
+
step: the cached step whose code failed.
|
|
624
|
+
error_text: the full formatted error text of the failure.
|
|
625
|
+
page: the live page facade of the current test.
|
|
626
|
+
|
|
627
|
+
Raises:
|
|
628
|
+
Always: ProductDefectError or IncurableStepError — the strict
|
|
629
|
+
terminal verdict. Strict mode never regenerates and never
|
|
630
|
+
heals: the classification is the only LLM call; an
|
|
631
|
+
unavailable LLM is skipped quietly and the step type alone
|
|
632
|
+
picks the kind.
|
|
633
|
+
"""
|
|
634
|
+
try:
|
|
635
|
+
classification = classify_step_failure(
|
|
636
|
+
self._config, self._provider, instruction, step.code, error_text, page
|
|
637
|
+
)
|
|
638
|
+
except LLMUnavailableError:
|
|
639
|
+
logger.warning("verdict skipped: llm unavailable")
|
|
640
|
+
|
|
641
|
+
# from None: the skip is logged; the step failure itself travels in the error field
|
|
642
|
+
if step_type == "assertion":
|
|
643
|
+
raise ProductDefectError(instruction, _STRICT_NO_VERDICT_REASON, error_text, None) from None
|
|
644
|
+
raise IncurableStepError(instruction, _STRICT_NO_VERDICT_REASON, error_text, code=step.code) from None
|
|
645
|
+
|
|
646
|
+
verdict = FailureVerdict(
|
|
647
|
+
category=classification.category,
|
|
648
|
+
explanation=classification.explanation,
|
|
649
|
+
recommendation=classification.recommendation,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
if classification.category == "product_defect":
|
|
653
|
+
raise ProductDefectError(instruction, classification.explanation, error_text, verdict)
|
|
654
|
+
raise IncurableStepError(instruction, classification.explanation, error_text, code=step.code, verdict=verdict)
|