verifaied 0.23.1.dev48__tar.gz → 0.24.0.dev49__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.
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/PKG-INFO +1 -1
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/pyproject.toml +1 -1
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/client.py +35 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/testcmd.py +126 -13
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_client.py +45 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_testcmd.py +299 -4
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/uv.lock +1 -1
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/.gitignore +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/LICENSE +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/README.md +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/__init__.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/__main__.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/analyzer.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/audit.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/cli.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/config.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/failure_text.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/instrument.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/instrument_snippets.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/instrumenter.js +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/instrumenter.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/prompts.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/proxy.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/record.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/recorder_driver.js +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/recorder_observer.js +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/recorder_picker.js +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/recorder_refs.js +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/recorder_spec.js +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/repo_config.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/run_reporter.js +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/source_index.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/subprocess_util.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/suite_report.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/uploader.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/src/verifaied/varcmd.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/__init__.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/conftest.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_audit.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_check.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_check_done.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_clear_manual.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_cli.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_config.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_instrument.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_instrument_detect.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_instrument_snippets.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_instrumenter.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_prompt_parity.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_proxy.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_record.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_repo_config.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_reporter_parity.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_source_index.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_suite_report.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_uploader.py +0 -0
- {verifaied-0.23.1.dev48 → verifaied-0.24.0.dev49}/tests/test_varcmd.py +0 -0
|
@@ -741,6 +741,41 @@ def post_recording_request_status(
|
|
|
741
741
|
raise ApiError(response.status_code, _detail(response))
|
|
742
742
|
|
|
743
743
|
|
|
744
|
+
def delete_recorded_test(
|
|
745
|
+
*,
|
|
746
|
+
api_url: str,
|
|
747
|
+
token: str,
|
|
748
|
+
test_id: UUID,
|
|
749
|
+
timeout: float = 30.0,
|
|
750
|
+
) -> bool:
|
|
751
|
+
"""Remove a test row. True when it was there, False when it wasn't.
|
|
752
|
+
|
|
753
|
+
The discard's other half. The browser closes without saving, and the
|
|
754
|
+
row a first recording created is then no use to anybody — so the
|
|
755
|
+
machine that did the recording is what removes it, because deleting
|
|
756
|
+
a row for a recording that may still be running is not something the
|
|
757
|
+
service can safely do on its own.
|
|
758
|
+
|
|
759
|
+
A 404 is not an error: the row is gone, which is the state the caller
|
|
760
|
+
was asking for. Everything else raises, so a failure can be said out
|
|
761
|
+
loud rather than leaving somebody believing a test was removed.
|
|
762
|
+
"""
|
|
763
|
+
url = f"{api_url.rstrip('/')}/recorded-tests/{test_id}"
|
|
764
|
+
try:
|
|
765
|
+
response = httpx.delete(
|
|
766
|
+
url,
|
|
767
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
768
|
+
timeout=timeout,
|
|
769
|
+
)
|
|
770
|
+
except httpx.HTTPError as e:
|
|
771
|
+
raise ApiError(0, f"could not reach {url}: {e}") from e
|
|
772
|
+
if response.status_code == 404:
|
|
773
|
+
return False
|
|
774
|
+
if response.status_code >= 400:
|
|
775
|
+
raise ApiError(response.status_code, _detail(response))
|
|
776
|
+
return True
|
|
777
|
+
|
|
778
|
+
|
|
744
779
|
def check_done(
|
|
745
780
|
*,
|
|
746
781
|
api_url: str,
|
|
@@ -111,6 +111,7 @@ from rich.table import Table
|
|
|
111
111
|
from verifaied.client import (
|
|
112
112
|
ApiError,
|
|
113
113
|
create_recorded_test,
|
|
114
|
+
delete_recorded_test,
|
|
114
115
|
list_recorded_tests,
|
|
115
116
|
patch_recorded_test,
|
|
116
117
|
post_recorder_heartbeat,
|
|
@@ -3934,6 +3935,12 @@ class RecordingSession:
|
|
|
3934
3935
|
# the save path still reads it if the browser dies before the
|
|
3935
3936
|
# `cancelled` event makes it back out.
|
|
3936
3937
|
self.cancelled = False
|
|
3938
|
+
# Whether that discard also asked for the test row itself to go.
|
|
3939
|
+
# Set beside ``cancelled``, for its reason, and acted on by
|
|
3940
|
+
# whoever called ``record_one`` rather than here: this session has
|
|
3941
|
+
# no idea what a row is, and the delete has to happen after the
|
|
3942
|
+
# closing posts have landed.
|
|
3943
|
+
self.discard = False
|
|
3937
3944
|
self.error: str | None = None
|
|
3938
3945
|
self._picked: dict[str, Any] | None = None
|
|
3939
3946
|
self._last_action: dict[str, Any] | None = None
|
|
@@ -4651,15 +4658,23 @@ class RecordingSession:
|
|
|
4651
4658
|
# rather than as the deliberate stop it was.
|
|
4652
4659
|
self._cancel_all_flows()
|
|
4653
4660
|
self._send({"cmd": "finish", "id": command_id})
|
|
4654
|
-
elif kind
|
|
4661
|
+
elif kind in ("cancel_recording", "discard_recording"):
|
|
4655
4662
|
# The discard, and finish's twin in every way but the one that
|
|
4656
4663
|
# matters: nothing is written. The flag is set here rather than
|
|
4657
4664
|
# when the driver answers, because the save path has to know a
|
|
4658
4665
|
# cancel was asked for even if the browser goes away before the
|
|
4659
4666
|
# `cancelled` event gets back out. Waiting flows are cancelled
|
|
4660
4667
|
# first for exactly finish's reason.
|
|
4668
|
+
#
|
|
4669
|
+
# The two kinds travel the identical road down stdin — the
|
|
4670
|
+
# driver knows one ending, not two — and differ only in what
|
|
4671
|
+
# the *caller* does once the browser has closed and the
|
|
4672
|
+
# closing posts have landed: `discard_recording` also asks for
|
|
4673
|
+
# the row to be removed, which is only ever offered on a test
|
|
4674
|
+
# that has never produced a spec.
|
|
4661
4675
|
self._cancel_all_flows()
|
|
4662
4676
|
self.cancelled = True
|
|
4677
|
+
self.discard = kind == "discard_recording"
|
|
4663
4678
|
self._send({"cmd": "cancel", "id": command_id})
|
|
4664
4679
|
elif kind == "assert":
|
|
4665
4680
|
code = str(payload.get("code") or "")
|
|
@@ -5335,10 +5350,12 @@ def run_test_record(
|
|
|
5335
5350
|
has_pre=bool(pre),
|
|
5336
5351
|
base_url=base_url,
|
|
5337
5352
|
)
|
|
5338
|
-
#
|
|
5339
|
-
#
|
|
5340
|
-
#
|
|
5341
|
-
|
|
5353
|
+
# A terminal recording that was discarded is a deliberate 0, and the
|
|
5354
|
+
# studio is the one caller that has anywhere to report *that*
|
|
5355
|
+
# difference to — but the row a discard asks for is this caller's to
|
|
5356
|
+
# remove, exactly as it is the loop's, and it goes after the closing
|
|
5357
|
+
# posts for the same reason.
|
|
5358
|
+
outcome = record_one(
|
|
5342
5359
|
api_url=api_url,
|
|
5343
5360
|
token=token,
|
|
5344
5361
|
repo_id=repo_id,
|
|
@@ -5365,7 +5382,17 @@ def run_test_record(
|
|
|
5365
5382
|
runner=runner,
|
|
5366
5383
|
stream_runner=stream_runner,
|
|
5367
5384
|
shell_runner=shell_runner,
|
|
5368
|
-
)
|
|
5385
|
+
)
|
|
5386
|
+
if outcome.discarded:
|
|
5387
|
+
discard_test_row(
|
|
5388
|
+
api_url=api_url,
|
|
5389
|
+
token=token,
|
|
5390
|
+
test_id=UUID(str(test["id"])),
|
|
5391
|
+
label=ref_of(feature, name),
|
|
5392
|
+
console=console,
|
|
5393
|
+
err_console=err_console,
|
|
5394
|
+
)
|
|
5395
|
+
return outcome.code
|
|
5369
5396
|
|
|
5370
5397
|
|
|
5371
5398
|
def saved_flowchart(
|
|
@@ -5422,10 +5449,52 @@ class RecordOutcome(NamedTuple):
|
|
|
5422
5449
|
which the studio has to report as its own ending rather than as
|
|
5423
5450
|
``done`` — that word means the recording was saved, and a discard is
|
|
5424
5451
|
precisely the case where nothing was.
|
|
5452
|
+
|
|
5453
|
+
``discarded`` is the narrower half of that: the user asked for the
|
|
5454
|
+
test itself to be removed, not just for this attempt to be thrown
|
|
5455
|
+
away. The removal is the **caller's** — ``record_one`` never deletes
|
|
5456
|
+
a row — because the closing posts have to land first, and only the
|
|
5457
|
+
caller knows whether there is also a request row riding on it.
|
|
5425
5458
|
"""
|
|
5426
5459
|
|
|
5427
5460
|
code: int
|
|
5428
5461
|
cancelled: bool = False
|
|
5462
|
+
discarded: bool = False
|
|
5463
|
+
|
|
5464
|
+
|
|
5465
|
+
def discard_test_row(
|
|
5466
|
+
*,
|
|
5467
|
+
api_url: str,
|
|
5468
|
+
token: str,
|
|
5469
|
+
test_id: UUID,
|
|
5470
|
+
label: str,
|
|
5471
|
+
console: Console,
|
|
5472
|
+
err_console: Console,
|
|
5473
|
+
) -> None:
|
|
5474
|
+
"""Remove the row a discarded first recording left behind.
|
|
5475
|
+
|
|
5476
|
+
Fail-soft, and deliberately: the recording is already over and the
|
|
5477
|
+
browser is already closed, so a delete that cannot land is worth one
|
|
5478
|
+
yellow line and nothing more. The row simply stays a draft, which is
|
|
5479
|
+
exactly where it was a moment ago and which the user can remove from
|
|
5480
|
+
the Tests page.
|
|
5481
|
+
|
|
5482
|
+
Never raises — a caller reporting the recording's own ending must not
|
|
5483
|
+
have that ending rewritten by the tidy-up after it.
|
|
5484
|
+
"""
|
|
5485
|
+
try:
|
|
5486
|
+
removed = delete_recorded_test(api_url=api_url, token=token, test_id=test_id)
|
|
5487
|
+
except ApiError as e:
|
|
5488
|
+
err_console.print(
|
|
5489
|
+
f'[yellow]could not remove "{escape(label)}"[/yellow] — '
|
|
5490
|
+
f"it stays a draft: {escape(str(e))}"
|
|
5491
|
+
)
|
|
5492
|
+
return
|
|
5493
|
+
console.print(
|
|
5494
|
+
f'[dim]"{escape(label)}" removed from verifAIed.[/dim]'
|
|
5495
|
+
if removed
|
|
5496
|
+
else f'[dim]"{escape(label)}" was already gone.[/dim]'
|
|
5497
|
+
)
|
|
5429
5498
|
|
|
5430
5499
|
|
|
5431
5500
|
def record_one(
|
|
@@ -5658,6 +5727,21 @@ def record_one(
|
|
|
5658
5727
|
# recording, and this post is the only channel that can put
|
|
5659
5728
|
# the saved spec's flow back — PATCH never writes steps, by
|
|
5660
5729
|
# design.
|
|
5730
|
+
#
|
|
5731
|
+
# A discard of a *draft* asks for one more thing: the row goes
|
|
5732
|
+
# too. The endpoint already refuses that on a row with a spec,
|
|
5733
|
+
# and the status is checked again here so an older service
|
|
5734
|
+
# that let one through degrades to a plain cancel rather than
|
|
5735
|
+
# deleting somebody's test. The delete itself is the caller's
|
|
5736
|
+
# — the closing posts have to land first.
|
|
5737
|
+
discard = session.discard and str(test.get("status")) == "draft"
|
|
5738
|
+
if discard:
|
|
5739
|
+
session.finish([])
|
|
5740
|
+
console.print(
|
|
5741
|
+
"[yellow]recording discarded[/yellow] — nothing was "
|
|
5742
|
+
f'saved, and "{label}" is being removed.'
|
|
5743
|
+
)
|
|
5744
|
+
return RecordOutcome(0, cancelled=True, discarded=True)
|
|
5661
5745
|
session.finish(saved_flowchart(spec_path, pre_refs))
|
|
5662
5746
|
note = (
|
|
5663
5747
|
f' — nothing was saved, so "{label}" stays a draft'
|
|
@@ -6134,6 +6218,12 @@ def edit_one(
|
|
|
6134
6218
|
# Nothing is written, so the spec on disk is the one that was
|
|
6135
6219
|
# there — and the closing post has to put its flow back, because
|
|
6136
6220
|
# the live feed has been clobbering `steps` with this session's.
|
|
6221
|
+
#
|
|
6222
|
+
# ``session.discard`` is deliberately not read here. An edit is
|
|
6223
|
+
# only ever offered on a test that already has a spec, which is
|
|
6224
|
+
# exactly the row the discard is fenced off from — so a session
|
|
6225
|
+
# that somehow collected one still ends as a plain cancel and
|
|
6226
|
+
# the saved flow stands.
|
|
6137
6227
|
session.finish(saved_flowchart(spec_path, pre_refs))
|
|
6138
6228
|
console.print(
|
|
6139
6229
|
f"[yellow]edit cancelled[/yellow] — nothing was saved, so "
|
|
@@ -6805,6 +6895,11 @@ def run_test_studio(
|
|
|
6805
6895
|
# than requiring the field keeps an upgraded CLI working
|
|
6806
6896
|
# against a service that hasn't been deployed yet.
|
|
6807
6897
|
cancelled = False
|
|
6898
|
+
# Whether the recording also asked for its row to go. Held
|
|
6899
|
+
# until after the status post below, because the request
|
|
6900
|
+
# row cascades with the test it names — deleting first
|
|
6901
|
+
# would leave that post with nothing to report to.
|
|
6902
|
+
discarded = False
|
|
6808
6903
|
with keeper(busy_beat):
|
|
6809
6904
|
kind = request.get("kind")
|
|
6810
6905
|
if kind in ("run", "run_headless"):
|
|
@@ -6874,7 +6969,7 @@ def run_test_studio(
|
|
|
6874
6969
|
shell_runner=shell_runner,
|
|
6875
6970
|
)
|
|
6876
6971
|
else:
|
|
6877
|
-
|
|
6972
|
+
outcome = _record_requested(
|
|
6878
6973
|
request,
|
|
6879
6974
|
api_url=api_url,
|
|
6880
6975
|
token=token,
|
|
@@ -6894,6 +6989,8 @@ def run_test_studio(
|
|
|
6894
6989
|
stream_runner=stream_runner,
|
|
6895
6990
|
shell_runner=shell_runner,
|
|
6896
6991
|
)
|
|
6992
|
+
cancelled = outcome.cancelled
|
|
6993
|
+
discarded = outcome.discarded
|
|
6897
6994
|
# Done means "the recording was saved" — or, for a run,
|
|
6898
6995
|
# "the replay finished", or, for an amendment, "the new
|
|
6899
6996
|
# lines are in the file", whatever the exit code was.
|
|
@@ -6912,6 +7009,19 @@ def run_test_studio(
|
|
|
6912
7009
|
request_id=request_id,
|
|
6913
7010
|
status="cancelled" if cancelled else "done",
|
|
6914
7011
|
)
|
|
7012
|
+
if discarded:
|
|
7013
|
+
# **After** the status post, never before: this request
|
|
7014
|
+
# row cascades with the test it names, so deleting
|
|
7015
|
+
# first would leave the ending with nowhere to land and
|
|
7016
|
+
# the page watching a request that simply vanished.
|
|
7017
|
+
discard_test_row(
|
|
7018
|
+
api_url=api_url,
|
|
7019
|
+
token=token,
|
|
7020
|
+
test_id=UUID(str(request["recorded_test_id"])),
|
|
7021
|
+
label=ref_of(request["feature"], request["name"]),
|
|
7022
|
+
console=console,
|
|
7023
|
+
err_console=err_console,
|
|
7024
|
+
)
|
|
6915
7025
|
except RecordedTestError as e:
|
|
6916
7026
|
err_console.print(f"[red]VerifAIed error[/red]: {e}")
|
|
6917
7027
|
_fail_request(
|
|
@@ -6974,12 +7084,15 @@ def _record_requested(
|
|
|
6974
7084
|
stream_runner: StreamRunner,
|
|
6975
7085
|
shell_runner: ShellRunner,
|
|
6976
7086
|
env_name: str | None = None,
|
|
6977
|
-
) ->
|
|
6978
|
-
"""Record the one flow a heartbeat handed over.
|
|
7087
|
+
) -> RecordOutcome:
|
|
7088
|
+
"""Record the one flow a heartbeat handed over.
|
|
6979
7089
|
|
|
6980
|
-
The
|
|
6981
|
-
``done`` means the recording was saved, so a recording
|
|
6982
|
-
threw away has to be reported as its own ending instead.
|
|
7090
|
+
The outcome is the two things the loop cannot see for itself.
|
|
7091
|
+
``cancelled`` — ``done`` means the recording was saved, so a recording
|
|
7092
|
+
the user threw away has to be reported as its own ending instead.
|
|
7093
|
+
``discarded`` — the user asked for the test itself to go, which the
|
|
7094
|
+
loop does **after** posting that ending, because the request row
|
|
7095
|
+
cascades with the test it names.
|
|
6983
7096
|
|
|
6984
7097
|
``recording`` is posted **first**, before anything slow: replaying a
|
|
6985
7098
|
chain of pre-steps can take a minute, and the page that asked for
|
|
@@ -7039,7 +7152,7 @@ def _record_requested(
|
|
|
7039
7152
|
runner=runner,
|
|
7040
7153
|
stream_runner=stream_runner,
|
|
7041
7154
|
shell_runner=shell_runner,
|
|
7042
|
-
)
|
|
7155
|
+
)
|
|
7043
7156
|
|
|
7044
7157
|
|
|
7045
7158
|
def _edit_requested(
|
|
@@ -21,6 +21,7 @@ from verifaied.client import (
|
|
|
21
21
|
clear_manual_coverage,
|
|
22
22
|
create_recorded_test,
|
|
23
23
|
create_session,
|
|
24
|
+
delete_recorded_test,
|
|
24
25
|
finalize_session,
|
|
25
26
|
list_installations,
|
|
26
27
|
list_recorded_tests,
|
|
@@ -1382,6 +1383,50 @@ def test_post_recording_request_status_wraps_a_transport_failure(
|
|
|
1382
1383
|
assert "could not reach" in exc.value.message
|
|
1383
1384
|
|
|
1384
1385
|
|
|
1386
|
+
def test_delete_recorded_test_removes_the_row(httpx_mock: HTTPXMock):
|
|
1387
|
+
"""The discard's other half: the machine that did the recording is
|
|
1388
|
+
what removes the row a first recording left behind."""
|
|
1389
|
+
test_id = uuid4()
|
|
1390
|
+
httpx_mock.add_response(
|
|
1391
|
+
method="DELETE",
|
|
1392
|
+
url=f"http://api.test/recorded-tests/{test_id}",
|
|
1393
|
+
status_code=204,
|
|
1394
|
+
)
|
|
1395
|
+
assert (
|
|
1396
|
+
delete_recorded_test(api_url="http://api.test/", token="t", test_id=test_id)
|
|
1397
|
+
is True
|
|
1398
|
+
)
|
|
1399
|
+
assert httpx_mock.get_request().headers["Authorization"] == "Bearer t"
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
def test_delete_recorded_test_reads_a_404_as_already_gone(httpx_mock: HTTPXMock):
|
|
1403
|
+
"""Not an error: the row is gone, which is the state the caller was
|
|
1404
|
+
asking for."""
|
|
1405
|
+
httpx_mock.add_response(
|
|
1406
|
+
method="DELETE", status_code=404, json={"detail": "recorded test not found"}
|
|
1407
|
+
)
|
|
1408
|
+
assert (
|
|
1409
|
+
delete_recorded_test(api_url="http://api.test", token="t", test_id=uuid4())
|
|
1410
|
+
is False
|
|
1411
|
+
)
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
def test_delete_recorded_test_raises_on_anything_else(httpx_mock: HTTPXMock):
|
|
1415
|
+
httpx_mock.add_response(method="DELETE", status_code=500, json={"detail": "boom"})
|
|
1416
|
+
with pytest.raises(ApiError) as exc:
|
|
1417
|
+
delete_recorded_test(api_url="http://api.test", token="t", test_id=uuid4())
|
|
1418
|
+
assert exc.value.status_code == 500
|
|
1419
|
+
assert exc.value.message == "boom"
|
|
1420
|
+
|
|
1421
|
+
|
|
1422
|
+
def test_delete_recorded_test_wraps_a_transport_failure(httpx_mock: HTTPXMock):
|
|
1423
|
+
httpx_mock.add_exception(httpx.ConnectError("no route"))
|
|
1424
|
+
with pytest.raises(ApiError) as exc:
|
|
1425
|
+
delete_recorded_test(api_url="http://api.test", token="t", test_id=uuid4())
|
|
1426
|
+
assert exc.value.status_code == 0
|
|
1427
|
+
assert "could not reach" in exc.value.message
|
|
1428
|
+
|
|
1429
|
+
|
|
1385
1430
|
def test_post_recorder_heartbeat_declares_the_kinds_it_can_do(
|
|
1386
1431
|
httpx_mock: HTTPXMock,
|
|
1387
1432
|
):
|
|
@@ -1674,6 +1674,7 @@ class Recording:
|
|
|
1674
1674
|
rows,
|
|
1675
1675
|
results,
|
|
1676
1676
|
outcome,
|
|
1677
|
+
deleted,
|
|
1677
1678
|
):
|
|
1678
1679
|
self.code = code
|
|
1679
1680
|
self.stream = stream
|
|
@@ -1685,9 +1686,12 @@ class Recording:
|
|
|
1685
1686
|
self.rows = rows
|
|
1686
1687
|
# Command results the CLI reported back on the driver's behalf.
|
|
1687
1688
|
self.results = results
|
|
1688
|
-
# What `record_one` answered — the exit code plus the
|
|
1689
|
-
# exit code cannot say
|
|
1689
|
+
# What `record_one` answered — the exit code plus the two things an
|
|
1690
|
+
# exit code cannot say: whether the recording was discarded, and
|
|
1691
|
+
# whether the row was asked to go with it.
|
|
1690
1692
|
self.outcome = outcome
|
|
1693
|
+
# The ids `delete_recorded_test` was called with, in order.
|
|
1694
|
+
self.deleted = deleted
|
|
1691
1695
|
|
|
1692
1696
|
|
|
1693
1697
|
def _record(
|
|
@@ -1700,6 +1704,8 @@ def _record(
|
|
|
1700
1704
|
replay_code: int = 0,
|
|
1701
1705
|
extra_lines: tuple[str, ...] = (),
|
|
1702
1706
|
acks: list[dict] | None = None,
|
|
1707
|
+
delete_error: Exception | None = None,
|
|
1708
|
+
delete_found: bool = True,
|
|
1703
1709
|
**overrides,
|
|
1704
1710
|
) -> Recording:
|
|
1705
1711
|
root = _repo(tmp_path)
|
|
@@ -1714,6 +1720,13 @@ def _record(
|
|
|
1714
1720
|
queued = list(acks or [])
|
|
1715
1721
|
|
|
1716
1722
|
outcomes: list[RecordOutcome] = []
|
|
1723
|
+
deleted: list[dict] = []
|
|
1724
|
+
|
|
1725
|
+
def _delete(**kw):
|
|
1726
|
+
deleted.append(kw)
|
|
1727
|
+
if delete_error is not None:
|
|
1728
|
+
raise delete_error
|
|
1729
|
+
return delete_found
|
|
1717
1730
|
|
|
1718
1731
|
def _post(**kw):
|
|
1719
1732
|
posted.append(kw)
|
|
@@ -1760,6 +1773,7 @@ def _record(
|
|
|
1760
1773
|
side_effect=lambda **kw: results.append(kw),
|
|
1761
1774
|
),
|
|
1762
1775
|
patch("verifaied.testcmd.record_one", side_effect=_capture_outcome),
|
|
1776
|
+
patch("verifaied.testcmd.delete_recorded_test", side_effect=_delete),
|
|
1763
1777
|
_inline_poster(),
|
|
1764
1778
|
):
|
|
1765
1779
|
code = run_test_record(**kwargs)
|
|
@@ -1774,6 +1788,7 @@ def _record(
|
|
|
1774
1788
|
rows,
|
|
1775
1789
|
results,
|
|
1776
1790
|
outcomes[-1] if outcomes else None,
|
|
1791
|
+
deleted,
|
|
1777
1792
|
)
|
|
1778
1793
|
|
|
1779
1794
|
|
|
@@ -6095,9 +6110,165 @@ def test_record_discards_whatever_the_driver_exit_looked_like(tmp_path: Path):
|
|
|
6095
6110
|
assert result.patched.call_count == 0
|
|
6096
6111
|
|
|
6097
6112
|
|
|
6113
|
+
DISCARD_COMMAND = {"id": "cmd-dis", "kind": "discard_recording", "payload": {}}
|
|
6114
|
+
|
|
6115
|
+
|
|
6116
|
+
def test_record_forwards_a_discard_the_same_way_it_forwards_a_cancel(tmp_path: Path):
|
|
6117
|
+
"""One ending down stdin, not two: the driver knows how to stop, and
|
|
6118
|
+
what the row is worth afterwards is the CLI's question."""
|
|
6119
|
+
row = _test_row(status="draft")
|
|
6120
|
+
result = _record(
|
|
6121
|
+
tmp_path,
|
|
6122
|
+
test=row,
|
|
6123
|
+
rows=[row],
|
|
6124
|
+
events=(
|
|
6125
|
+
{"type": "ready"},
|
|
6126
|
+
{"type": "spec", "text": SPEC_BODY},
|
|
6127
|
+
{"type": "cancelled", "id": "cmd-dis"},
|
|
6128
|
+
{"type": "done", "spec_saved": True},
|
|
6129
|
+
),
|
|
6130
|
+
acks=[{"commands": [DISCARD_COMMAND]}],
|
|
6131
|
+
)
|
|
6132
|
+
assert result.stream.stdin == [READY_CAPTURE, {"cmd": "cancel", "id": "cmd-dis"}]
|
|
6133
|
+
assert result.results == [
|
|
6134
|
+
{
|
|
6135
|
+
"api_url": "http://api.test",
|
|
6136
|
+
"token": "t",
|
|
6137
|
+
"test_id": UUID(row["id"]),
|
|
6138
|
+
"command_id": "cmd-dis",
|
|
6139
|
+
"status": "done",
|
|
6140
|
+
"result": {"ok": True, "cancelled": True},
|
|
6141
|
+
}
|
|
6142
|
+
]
|
|
6143
|
+
|
|
6144
|
+
|
|
6145
|
+
def test_record_removes_the_row_when_a_first_recording_is_discarded(tmp_path: Path):
|
|
6146
|
+
"""The whole point of the second button: the row exists only because
|
|
6147
|
+
somebody named a flow, so "this was a mistake" takes it with it."""
|
|
6148
|
+
row = _test_row(status="draft")
|
|
6149
|
+
console = _console()
|
|
6150
|
+
result = _record(
|
|
6151
|
+
tmp_path,
|
|
6152
|
+
test=row,
|
|
6153
|
+
rows=[row],
|
|
6154
|
+
console=console,
|
|
6155
|
+
events=(
|
|
6156
|
+
{"type": "ready"},
|
|
6157
|
+
{"type": "spec", "text": SPEC_BODY},
|
|
6158
|
+
{"type": "cancelled", "id": "cmd-dis"},
|
|
6159
|
+
{"type": "done", "spec_saved": True},
|
|
6160
|
+
),
|
|
6161
|
+
acks=[{"commands": [DISCARD_COMMAND]}],
|
|
6162
|
+
)
|
|
6163
|
+
assert result.code == 0
|
|
6164
|
+
assert result.outcome == RecordOutcome(0, cancelled=True, discarded=True)
|
|
6165
|
+
assert not (result.root / row["spec_path"]).exists()
|
|
6166
|
+
assert result.patched.call_count == 0
|
|
6167
|
+
assert result.replayed.call_count == 0
|
|
6168
|
+
# Nothing on disk, so nothing for the closing flowchart to say.
|
|
6169
|
+
assert result.posted[-1]["live"] is False
|
|
6170
|
+
assert result.posted[-1]["steps"] == []
|
|
6171
|
+
assert [call["test_id"] for call in result.deleted] == [UUID(row["id"])]
|
|
6172
|
+
assert result.deleted[0]["api_url"] == "http://api.test"
|
|
6173
|
+
assert result.deleted[0]["token"] == "t"
|
|
6174
|
+
printed = [str(call.args[0]) for call in console.print.call_args_list]
|
|
6175
|
+
assert (
|
|
6176
|
+
"[yellow]recording discarded[/yellow] — nothing was saved, and "
|
|
6177
|
+
'"Settings/API tokens" is being removed.'
|
|
6178
|
+
) in printed
|
|
6179
|
+
assert '[dim]"Settings/API tokens" removed from verifAIed.[/dim]' in printed
|
|
6180
|
+
|
|
6181
|
+
|
|
6182
|
+
def test_record_degrades_a_discard_on_a_saved_test_to_a_plain_cancel(tmp_path: Path):
|
|
6183
|
+
"""The endpoint refuses this, and the CLI checks again: a re-record
|
|
6184
|
+
throwing away its attempt must never take the spec with it."""
|
|
6185
|
+
row = _test_row(status="ready")
|
|
6186
|
+
root = _repo(tmp_path)
|
|
6187
|
+
_write_spec(root, row, OLD_SPEC_BODY)
|
|
6188
|
+
before = (root / row["spec_path"]).read_bytes()
|
|
6189
|
+
console = _console()
|
|
6190
|
+
|
|
6191
|
+
result = _record(
|
|
6192
|
+
tmp_path,
|
|
6193
|
+
test=row,
|
|
6194
|
+
rows=[row],
|
|
6195
|
+
console=console,
|
|
6196
|
+
events=(
|
|
6197
|
+
{"type": "ready"},
|
|
6198
|
+
{"type": "spec", "text": SPEC_BODY},
|
|
6199
|
+
{"type": "cancelled", "id": "cmd-dis"},
|
|
6200
|
+
{"type": "done", "spec_saved": True},
|
|
6201
|
+
),
|
|
6202
|
+
acks=[{"commands": [DISCARD_COMMAND]}],
|
|
6203
|
+
)
|
|
6204
|
+
|
|
6205
|
+
assert result.outcome == RecordOutcome(0, cancelled=True, discarded=False)
|
|
6206
|
+
assert result.deleted == []
|
|
6207
|
+
assert (result.root / row["spec_path"]).read_bytes() == before
|
|
6208
|
+
assert (
|
|
6209
|
+
"[yellow]recording cancelled[/yellow] — nothing was saved, so "
|
|
6210
|
+
'"Settings/API tokens" keeps the spec it already had.'
|
|
6211
|
+
) in [str(call.args[0]) for call in console.print.call_args_list]
|
|
6212
|
+
|
|
6213
|
+
|
|
6214
|
+
def test_record_says_so_when_the_row_was_already_gone(tmp_path: Path):
|
|
6215
|
+
"""A 404 is the state the caller asked for, not a failure."""
|
|
6216
|
+
row = _test_row(status="draft")
|
|
6217
|
+
console = _console()
|
|
6218
|
+
result = _record(
|
|
6219
|
+
tmp_path,
|
|
6220
|
+
test=row,
|
|
6221
|
+
rows=[row],
|
|
6222
|
+
console=console,
|
|
6223
|
+
delete_found=False,
|
|
6224
|
+
events=(
|
|
6225
|
+
{"type": "ready"},
|
|
6226
|
+
{"type": "spec", "text": SPEC_BODY},
|
|
6227
|
+
{"type": "cancelled", "id": "cmd-dis"},
|
|
6228
|
+
{"type": "done", "spec_saved": True},
|
|
6229
|
+
),
|
|
6230
|
+
acks=[{"commands": [DISCARD_COMMAND]}],
|
|
6231
|
+
)
|
|
6232
|
+
assert result.code == 0
|
|
6233
|
+
assert '[dim]"Settings/API tokens" was already gone.[/dim]' in [
|
|
6234
|
+
str(call.args[0]) for call in console.print.call_args_list
|
|
6235
|
+
]
|
|
6236
|
+
|
|
6237
|
+
|
|
6238
|
+
def test_record_survives_a_delete_it_could_not_land(tmp_path: Path):
|
|
6239
|
+
"""The recording is already over and the browser is already closed, so
|
|
6240
|
+
a delete that fails is worth one yellow line and nothing more."""
|
|
6241
|
+
row = _test_row(status="draft")
|
|
6242
|
+
err_console = _console()
|
|
6243
|
+
result = _record(
|
|
6244
|
+
tmp_path,
|
|
6245
|
+
test=row,
|
|
6246
|
+
rows=[row],
|
|
6247
|
+
err_console=err_console,
|
|
6248
|
+
delete_error=ApiError(500, "boom"),
|
|
6249
|
+
events=(
|
|
6250
|
+
{"type": "ready"},
|
|
6251
|
+
{"type": "spec", "text": SPEC_BODY},
|
|
6252
|
+
{"type": "cancelled", "id": "cmd-dis"},
|
|
6253
|
+
{"type": "done", "spec_saved": True},
|
|
6254
|
+
),
|
|
6255
|
+
acks=[{"commands": [DISCARD_COMMAND]}],
|
|
6256
|
+
)
|
|
6257
|
+
assert result.code == 0
|
|
6258
|
+
assert [str(call.args[0]) for call in err_console.print.call_args_list] == [
|
|
6259
|
+
'[yellow]could not remove "Settings/API tokens"[/yellow] — '
|
|
6260
|
+
"it stays a draft: boom"
|
|
6261
|
+
]
|
|
6262
|
+
|
|
6263
|
+
|
|
6264
|
+
def test_record_deletes_nothing_when_the_recording_was_saved(tmp_path: Path):
|
|
6265
|
+
result = _record(tmp_path)
|
|
6266
|
+
assert result.deleted == []
|
|
6267
|
+
|
|
6268
|
+
|
|
6098
6269
|
def test_record_answers_not_cancelled_when_the_recording_was_saved(tmp_path: Path):
|
|
6099
6270
|
result = _record(tmp_path)
|
|
6100
|
-
assert result.outcome == RecordOutcome(0, cancelled=False)
|
|
6271
|
+
assert result.outcome == RecordOutcome(0, cancelled=False, discarded=False)
|
|
6101
6272
|
|
|
6102
6273
|
|
|
6103
6274
|
def test_record_ignores_a_command_kind_it_does_not_know(tmp_path: Path):
|
|
@@ -6446,6 +6617,8 @@ class Studio:
|
|
|
6446
6617
|
amends,
|
|
6447
6618
|
sleeps,
|
|
6448
6619
|
err_console,
|
|
6620
|
+
deleted,
|
|
6621
|
+
order,
|
|
6449
6622
|
):
|
|
6450
6623
|
self.code = code
|
|
6451
6624
|
# Every heartbeat, in order, including the goodbye.
|
|
@@ -6461,6 +6634,11 @@ class Studio:
|
|
|
6461
6634
|
self.amends = amends
|
|
6462
6635
|
self.sleeps = sleeps
|
|
6463
6636
|
self.err_console = err_console
|
|
6637
|
+
# The ids `delete_recorded_test` was called with, in order.
|
|
6638
|
+
self.deleted = deleted
|
|
6639
|
+
# Status posts and deletes interleaved, so "after the status post"
|
|
6640
|
+
# is something a test can actually see.
|
|
6641
|
+
self.order = order
|
|
6464
6642
|
|
|
6465
6643
|
|
|
6466
6644
|
def _studio_request(test_id: str, **overrides) -> dict:
|
|
@@ -6501,6 +6679,7 @@ def _studio(
|
|
|
6501
6679
|
edit: object = None,
|
|
6502
6680
|
amend: object = None,
|
|
6503
6681
|
status_error: Exception | None = None,
|
|
6682
|
+
delete_error: Exception | None = None,
|
|
6504
6683
|
goodbye_error: Exception | None = None,
|
|
6505
6684
|
sleeps_before_stop: int | None = None,
|
|
6506
6685
|
branches: list[str] | None = None,
|
|
@@ -6515,6 +6694,8 @@ def _studio(
|
|
|
6515
6694
|
edits: list[dict] = []
|
|
6516
6695
|
amends: list[dict] = []
|
|
6517
6696
|
sleeps: list[float] = []
|
|
6697
|
+
deleted: list[dict] = []
|
|
6698
|
+
order: list[str] = []
|
|
6518
6699
|
queued = list(acks)
|
|
6519
6700
|
err_console = _console()
|
|
6520
6701
|
|
|
@@ -6535,9 +6716,17 @@ def _studio(
|
|
|
6535
6716
|
|
|
6536
6717
|
def _status(**kw):
|
|
6537
6718
|
statuses.append(kw)
|
|
6719
|
+
order.append(f"status:{kw['status']}")
|
|
6538
6720
|
if status_error is not None and kw["status"] == "failed":
|
|
6539
6721
|
raise status_error
|
|
6540
6722
|
|
|
6723
|
+
def _delete(**kw):
|
|
6724
|
+
deleted.append(kw)
|
|
6725
|
+
order.append("delete")
|
|
6726
|
+
if delete_error is not None:
|
|
6727
|
+
raise delete_error
|
|
6728
|
+
return True
|
|
6729
|
+
|
|
6541
6730
|
def _sleep(seconds):
|
|
6542
6731
|
sleeps.append(seconds)
|
|
6543
6732
|
if sleeps_before_stop is not None and len(sleeps) >= sleeps_before_stop:
|
|
@@ -6604,10 +6793,21 @@ def _studio(
|
|
|
6604
6793
|
patch("verifaied.testcmd.replay_spec", side_effect=_replay_spec),
|
|
6605
6794
|
patch("verifaied.testcmd.edit_one", side_effect=_edit_one),
|
|
6606
6795
|
patch("verifaied.testcmd.amend_one", side_effect=_amend_one),
|
|
6796
|
+
patch("verifaied.testcmd.delete_recorded_test", side_effect=_delete),
|
|
6607
6797
|
):
|
|
6608
6798
|
code = run_test_studio(**kwargs)
|
|
6609
6799
|
return Studio(
|
|
6610
|
-
code,
|
|
6800
|
+
code,
|
|
6801
|
+
beats,
|
|
6802
|
+
statuses,
|
|
6803
|
+
records,
|
|
6804
|
+
replays,
|
|
6805
|
+
edits,
|
|
6806
|
+
amends,
|
|
6807
|
+
sleeps,
|
|
6808
|
+
err_console,
|
|
6809
|
+
deleted,
|
|
6810
|
+
order,
|
|
6611
6811
|
)
|
|
6612
6812
|
|
|
6613
6813
|
|
|
@@ -6879,6 +7079,68 @@ def test_studio_reports_a_discarded_recording_as_cancelled(tmp_path: Path):
|
|
|
6879
7079
|
assert "error" not in result.statuses[1]
|
|
6880
7080
|
|
|
6881
7081
|
|
|
7082
|
+
def test_studio_removes_the_row_after_reporting_a_discard(tmp_path: Path):
|
|
7083
|
+
"""The order is load-bearing: the request row cascades with the test
|
|
7084
|
+
it names, so deleting first would leave the ending nowhere to land."""
|
|
7085
|
+
row = _test_row(status="draft")
|
|
7086
|
+
request = _studio_request(row["id"])
|
|
7087
|
+
|
|
7088
|
+
result = _studio(
|
|
7089
|
+
tmp_path,
|
|
7090
|
+
acks=[_ack(request)],
|
|
7091
|
+
rows=[row],
|
|
7092
|
+
record=lambda **kw: RecordOutcome(0, cancelled=True, discarded=True),
|
|
7093
|
+
)
|
|
7094
|
+
|
|
7095
|
+
assert result.code == 0
|
|
7096
|
+
assert [s["status"] for s in result.statuses] == ["recording", "cancelled"]
|
|
7097
|
+
assert result.order == ["status:recording", "status:cancelled", "delete"]
|
|
7098
|
+
assert [call["test_id"] for call in result.deleted] == [
|
|
7099
|
+
UUID(request["recorded_test_id"])
|
|
7100
|
+
]
|
|
7101
|
+
assert result.deleted[0]["api_url"] == "http://api.test"
|
|
7102
|
+
|
|
7103
|
+
|
|
7104
|
+
def test_studio_deletes_nothing_for_a_cancel_that_kept_the_draft(tmp_path: Path):
|
|
7105
|
+
row = _test_row(status="draft")
|
|
7106
|
+
result = _studio(
|
|
7107
|
+
tmp_path,
|
|
7108
|
+
acks=[_ack(_studio_request(row["id"]))],
|
|
7109
|
+
rows=[row],
|
|
7110
|
+
record=lambda **kw: RecordOutcome(0, cancelled=True),
|
|
7111
|
+
)
|
|
7112
|
+
assert [s["status"] for s in result.statuses] == ["recording", "cancelled"]
|
|
7113
|
+
assert result.deleted == []
|
|
7114
|
+
|
|
7115
|
+
|
|
7116
|
+
def test_studio_keeps_going_when_a_discard_could_not_be_removed(tmp_path: Path):
|
|
7117
|
+
"""One row that would not delete is not a dead daemon: the next
|
|
7118
|
+
request is still taken."""
|
|
7119
|
+
first = _test_row(status="draft")
|
|
7120
|
+
second = _test_row(status="draft")
|
|
7121
|
+
|
|
7122
|
+
result = _studio(
|
|
7123
|
+
tmp_path,
|
|
7124
|
+
acks=[
|
|
7125
|
+
_ack(_studio_request(first["id"])),
|
|
7126
|
+
_ack(_studio_request(second["id"])),
|
|
7127
|
+
],
|
|
7128
|
+
rows=[first, second],
|
|
7129
|
+
record=lambda **kw: RecordOutcome(0, cancelled=True, discarded=True),
|
|
7130
|
+
delete_error=ApiError(500, "boom"),
|
|
7131
|
+
)
|
|
7132
|
+
|
|
7133
|
+
assert result.code == 0
|
|
7134
|
+
assert [s["status"] for s in result.statuses] == [
|
|
7135
|
+
"recording",
|
|
7136
|
+
"cancelled",
|
|
7137
|
+
"recording",
|
|
7138
|
+
"cancelled",
|
|
7139
|
+
]
|
|
7140
|
+
assert len(result.deleted) == 2
|
|
7141
|
+
assert result.err_console.print.called
|
|
7142
|
+
|
|
7143
|
+
|
|
6882
7144
|
def test_studio_fails_a_request_whose_test_was_deleted(tmp_path: Path):
|
|
6883
7145
|
result = _studio(
|
|
6884
7146
|
tmp_path,
|
|
@@ -10898,6 +11160,39 @@ def test_a_cancelled_edit_leaves_the_spec_byte_identical(tmp_path: Path):
|
|
|
10898
11160
|
assert result.posted[-1]["live"] is False
|
|
10899
11161
|
|
|
10900
11162
|
|
|
11163
|
+
def test_a_discard_collected_during_an_edit_is_just_a_cancel(tmp_path: Path):
|
|
11164
|
+
"""An edit only ever happens on a test that already has a spec, which
|
|
11165
|
+
is exactly the row a discard is fenced off from — so a session that
|
|
11166
|
+
somehow collected one still ends with the saved flow standing."""
|
|
11167
|
+
events = (
|
|
11168
|
+
{"type": "ready"},
|
|
11169
|
+
{"type": "spec", "text": EDIT_TAIL},
|
|
11170
|
+
{"type": "cancelled", "id": "c1"},
|
|
11171
|
+
{"type": "done", "spec_saved": False},
|
|
11172
|
+
)
|
|
11173
|
+
result = _edit(
|
|
11174
|
+
tmp_path,
|
|
11175
|
+
edit_from=1,
|
|
11176
|
+
events=events,
|
|
11177
|
+
acks=[
|
|
11178
|
+
{
|
|
11179
|
+
"commands": [
|
|
11180
|
+
{"id": "c1", "kind": "discard_recording", "payload": {}},
|
|
11181
|
+
]
|
|
11182
|
+
}
|
|
11183
|
+
],
|
|
11184
|
+
)
|
|
11185
|
+
assert result.code == 0
|
|
11186
|
+
assert result.spec == EDIT_SPEC
|
|
11187
|
+
assert result.patched.call_count == 0
|
|
11188
|
+
assert result.replayed.call_count == 0
|
|
11189
|
+
assert [step["label"] for step in result.posted[-1]["steps"]] == [
|
|
11190
|
+
"Open http://localhost:3000/settings",
|
|
11191
|
+
"Click 'New token'",
|
|
11192
|
+
"'Created' is visible",
|
|
11193
|
+
]
|
|
11194
|
+
|
|
11195
|
+
|
|
10901
11196
|
def test_an_edit_that_recorded_nothing_truncates_to_what_was_kept(
|
|
10902
11197
|
tmp_path: Path,
|
|
10903
11198
|
):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|