verifaied 0.31.0.dev78__tar.gz → 0.32.0.dev80__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.
Files changed (76) hide show
  1. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/PKG-INFO +15 -5
  2. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/README.md +14 -4
  3. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/pyproject.toml +1 -1
  4. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/client.py +51 -10
  5. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/record.py +50 -6
  6. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/testcmd.py +298 -33
  7. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_client.py +118 -1
  8. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_record.py +5 -2
  9. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_testcmd.py +752 -4
  10. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/uv.lock +1 -1
  11. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/.gitignore +0 -0
  12. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/LICENSE +0 -0
  13. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/__init__.py +0 -0
  14. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/__main__.py +0 -0
  15. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/agent_mcp.py +0 -0
  16. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/agent_steps.py +0 -0
  17. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/analyzer.py +0 -0
  18. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/audit.py +0 -0
  19. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/cli.py +0 -0
  20. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/collect_browser.js +0 -0
  21. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/collect_browser.py +0 -0
  22. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/config.py +0 -0
  23. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/failure_text.py +0 -0
  24. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/instrument.py +0 -0
  25. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/instrument_snippets.py +0 -0
  26. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/instrumenter.js +0 -0
  27. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/instrumenter.py +0 -0
  28. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/prompts.py +0 -0
  29. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/proxy.py +0 -0
  30. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/recorder_driver.js +0 -0
  31. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/recorder_observer.js +0 -0
  32. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/recorder_picker.js +0 -0
  33. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/recorder_refs.js +0 -0
  34. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/recorder_spec.js +0 -0
  35. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/replay_harvest.ts +0 -0
  36. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/repo_config.py +0 -0
  37. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/run_reporter.js +0 -0
  38. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/runner_dir.py +0 -0
  39. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/skill/SKILL.md +0 -0
  40. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/skill/reference/commands.md +0 -0
  41. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/skillcmd.py +0 -0
  42. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/source_index.py +0 -0
  43. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/stepcheck.py +0 -0
  44. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/subprocess_util.py +0 -0
  45. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/suite_report.py +0 -0
  46. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/uploader.py +0 -0
  47. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/src/verifaied/varcmd.py +0 -0
  48. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/__init__.py +0 -0
  49. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/conftest.py +0 -0
  50. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_agent_mcp.py +0 -0
  51. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_audit.py +0 -0
  52. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_browser_settings_parity.py +0 -0
  53. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_caption_parity.py +0 -0
  54. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_check.py +0 -0
  55. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_check_done.py +0 -0
  56. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_clear_manual.py +0 -0
  57. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_cli.py +0 -0
  58. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_collect_browser.py +0 -0
  59. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_config.py +0 -0
  60. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_console_label_parity.py +0 -0
  61. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_instrument.py +0 -0
  62. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_instrument_detect.py +0 -0
  63. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_instrument_snippets.py +0 -0
  64. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_instrumenter.py +0 -0
  65. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_pause_parity.py +0 -0
  66. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_prompt_parity.py +0 -0
  67. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_proxy.py +0 -0
  68. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_replay_harvest_runner.py +0 -0
  69. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_repo_config.py +0 -0
  70. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_skill_parity.py +0 -0
  71. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_skillcmd.py +0 -0
  72. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_source_index.py +0 -0
  73. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_stepcheck_parity.py +0 -0
  74. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_suite_report.py +0 -0
  75. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_uploader.py +0 -0
  76. {verifaied-0.31.0.dev78 → verifaied-0.32.0.dev80}/tests/test_varcmd.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: verifaied
3
- Version: 0.31.0.dev78
3
+ Version: 0.32.0.dev80
4
4
  Summary: Find what's untested in your code — locally, no account required
5
5
  Project-URL: Homepage, https://pypi.org/project/verifaied/
6
6
  Author: Kyle Richards
@@ -1054,11 +1054,18 @@ cloud can stop it. A request that fails is reported with the reason and
1054
1054
  the studio carries on — one bad pre-step chain is not a reason for it to
1055
1055
  die.
1056
1056
 
1057
+ **Stopping a run from the web.** While a run is replaying, the spinning
1058
+ Run button on the page turns into **Stop**. The studio hears it on its next
1059
+ beat, ends the replay's whole process group (SIGTERM, then SIGKILL), and
1060
+ records the run as failed with "Stopped from the web"; its video and trace
1061
+ are not kept. This uses POSIX process groups (`os.killpg`), so it needs
1062
+ macOS or Linux — the studio does not support Windows.
1063
+
1057
1064
  **Branches.** The branch is re-detected on every beat, so switching
1058
1065
  branches under a running studio moves what the web shows and what the next
1059
1066
  recording lands against. `--branch` pins it instead.
1060
1067
 
1061
- **Ctrl-C** does the safe thing at either moment. Mid-recording, the
1068
+ **Ctrl-C** (and SIGTERM, or the terminal closing) does the safe thing at either moment. Mid-recording, the
1062
1069
  interrupt is forwarded to the recorder exactly as it is for `test record`:
1063
1070
  the browser closes, the spec is saved, the replay runs, the request
1064
1071
  completes — and only then does the studio exit. At the idle prompt it
@@ -1101,9 +1108,12 @@ replay and lays over the file.
1101
1108
  - `0` — recorded, or replayed cleanly
1102
1109
  - `*` — the replay's own exit code when the spec failed (`test run --all`
1103
1110
  reports the worst across the run)
1104
- - `2` — the replay was clean but its final coverage upload failed (coverage,
1105
- video and trace uploads are each tried three times, 2 s and 6 s apart, on a
1106
- dropped connection, a timeout or a 5xx — never on a 4xx)
1111
+ - `2` — the replay was clean but its final coverage upload failed, or the
1112
+ service never received its verdict because the session could not be closed
1113
+ (coverage, video and trace uploads and the session's close are each tried
1114
+ three times, 2 s and 6 s apart, on a dropped connection, a timeout or a 5xx
1115
+ — never on a 4xx; a close that still fails prints the session id and the
1116
+ `curl` that records the verdict by hand)
1107
1117
  - `1` — usage/config error (no token, bad `--repo`, unknown test, no
1108
1118
  Playwright)
1109
1119
 
@@ -1008,11 +1008,18 @@ cloud can stop it. A request that fails is reported with the reason and
1008
1008
  the studio carries on — one bad pre-step chain is not a reason for it to
1009
1009
  die.
1010
1010
 
1011
+ **Stopping a run from the web.** While a run is replaying, the spinning
1012
+ Run button on the page turns into **Stop**. The studio hears it on its next
1013
+ beat, ends the replay's whole process group (SIGTERM, then SIGKILL), and
1014
+ records the run as failed with "Stopped from the web"; its video and trace
1015
+ are not kept. This uses POSIX process groups (`os.killpg`), so it needs
1016
+ macOS or Linux — the studio does not support Windows.
1017
+
1011
1018
  **Branches.** The branch is re-detected on every beat, so switching
1012
1019
  branches under a running studio moves what the web shows and what the next
1013
1020
  recording lands against. `--branch` pins it instead.
1014
1021
 
1015
- **Ctrl-C** does the safe thing at either moment. Mid-recording, the
1022
+ **Ctrl-C** (and SIGTERM, or the terminal closing) does the safe thing at either moment. Mid-recording, the
1016
1023
  interrupt is forwarded to the recorder exactly as it is for `test record`:
1017
1024
  the browser closes, the spec is saved, the replay runs, the request
1018
1025
  completes — and only then does the studio exit. At the idle prompt it
@@ -1055,9 +1062,12 @@ replay and lays over the file.
1055
1062
  - `0` — recorded, or replayed cleanly
1056
1063
  - `*` — the replay's own exit code when the spec failed (`test run --all`
1057
1064
  reports the worst across the run)
1058
- - `2` — the replay was clean but its final coverage upload failed (coverage,
1059
- video and trace uploads are each tried three times, 2 s and 6 s apart, on a
1060
- dropped connection, a timeout or a 5xx — never on a 4xx)
1065
+ - `2` — the replay was clean but its final coverage upload failed, or the
1066
+ service never received its verdict because the session could not be closed
1067
+ (coverage, video and trace uploads and the session's close are each tried
1068
+ three times, 2 s and 6 s apart, on a dropped connection, a timeout or a 5xx
1069
+ — never on a 4xx; a close that still fails prints the session id and the
1070
+ `curl` that records the verdict by hand)
1061
1071
  - `1` — usage/config error (no token, bad `--repo`, unknown test, no
1062
1072
  Playwright)
1063
1073
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "verifaied"
3
- version = "0.31.0.dev78"
3
+ version = "0.32.0.dev80"
4
4
  description = "Find what's untested in your code — locally, no account required"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -18,11 +18,11 @@ import httpx
18
18
 
19
19
  from verifaied.uploader import Payload
20
20
 
21
- # Seconds to wait before the second and third attempt of an upload. A
22
- # replay's evidence is posted once, at the end, over whatever network the
23
- # machine has — a laptop roaming between access points, a CI runner
24
- # sharing one uplink with five others — and one dropped connection there
25
- # would otherwise turn a passing run into exit 2.
21
+ # Seconds to wait before the second and third attempt of an upload or of a
22
+ # session's close. A replay's evidence and its verdict are posted once, at
23
+ # the end, over whatever network the machine has — a laptop roaming between
24
+ # access points, a CI runner sharing one uplink with five others — and one
25
+ # dropped connection there would otherwise turn a passing run into exit 2.
26
26
  UPLOAD_BACKOFF: tuple[float, ...] = (2.0, 6.0)
27
27
  # Indirected so tests can skip the waits.
28
28
  _sleep: Callable[[float], None] = time.sleep
@@ -245,6 +245,11 @@ def finalize_session(
245
245
  ``failure_message`` — what the runner said there — are sent only when
246
246
  there is one, so a close from anything without a flowchart is the same
247
247
  request it always was.
248
+
249
+ Retried like :func:`upload` on a dropped connection, a timeout or a
250
+ 5xx. Safe to repeat: the service keeps the first close's end time and
251
+ writes the verdict again, so a close that landed but whose answer was
252
+ lost is only recorded twice.
248
253
  """
249
254
  url = f"{api_url.rstrip('/')}/sessions/{session_id}"
250
255
  body: dict[str, Any] = {"ended": True}
@@ -255,11 +260,13 @@ def finalize_session(
255
260
  if failure_message is not None:
256
261
  body["failure_message"] = failure_message
257
262
  try:
258
- response = httpx.patch(
259
- url,
260
- json=body,
261
- headers={"Authorization": f"Bearer {token}"},
262
- timeout=timeout,
263
+ response = _send_with_retry(
264
+ lambda: httpx.patch(
265
+ url,
266
+ json=body,
267
+ headers={"Authorization": f"Bearer {token}"},
268
+ timeout=timeout,
269
+ )
263
270
  )
264
271
  except httpx.HTTPError as e:
265
272
  raise ApiError(0, f"could not reach {url}: {e}") from e
@@ -715,6 +722,7 @@ def post_recorder_heartbeat(
715
722
  kinds: list[str] | None = None,
716
723
  environment: str | None = None,
717
724
  busy: bool = False,
725
+ working: UUID | None = None,
718
726
  timeout: float = 30.0,
719
727
  ) -> dict[str, Any]:
720
728
  """Say a studio is listening, and collect what to record next.
@@ -762,6 +770,10 @@ def post_recorder_heartbeat(
762
770
  body["environment"] = environment
763
771
  if busy:
764
772
  body["busy"] = True
773
+ if working is not None:
774
+ # Which run this busy studio is on, so the service does not stop it
775
+ # as a run whose studio went away. Sent only when named.
776
+ body["working"] = str(working)
765
777
  try:
766
778
  response = _http().post(
767
779
  request_url,
@@ -813,6 +825,35 @@ def post_recording_request_status(
813
825
  raise ApiError(response.status_code, _detail(response))
814
826
 
815
827
 
828
+ def cancel_queued_runs(
829
+ *,
830
+ api_url: str,
831
+ token: str,
832
+ repo_id: UUID,
833
+ timeout: float = 10.0,
834
+ ) -> int:
835
+ """Take back this user's queued runs in a repository; how many.
836
+
837
+ The web's "Stop queued runs", called by a studio on its way out: a run
838
+ queued for it is one nothing else will pick up, because presence is
839
+ one studio per user per repository. Runs only, never one a studio has
840
+ already taken — the endpoint's own rule. A short timeout, because the
841
+ caller is a process that has been asked to exit.
842
+ """
843
+ url = f"{api_url.rstrip('/')}/repositories/{repo_id}/recording-requests"
844
+ try:
845
+ response = httpx.delete(
846
+ url,
847
+ headers={"Authorization": f"Bearer {token}"},
848
+ timeout=timeout,
849
+ )
850
+ except httpx.HTTPError as e:
851
+ raise ApiError(0, f"could not reach {url}: {e}") from e
852
+ if response.status_code >= 400:
853
+ raise ApiError(response.status_code, _detail(response))
854
+ return int(response.json().get("cancelled", 0))
855
+
856
+
816
857
  def delete_recorded_test(
817
858
  *,
818
859
  api_url: str,
@@ -38,6 +38,7 @@ from typing import Any
38
38
  from uuid import UUID
39
39
 
40
40
  from rich.console import Console
41
+ from rich.markup import escape
41
42
 
42
43
  from verifaied.client import (
43
44
  ApiError,
@@ -1062,10 +1063,17 @@ def _close_session(
1062
1063
  err_console: Console,
1063
1064
  failed_step: int | None = None,
1064
1065
  failure_message: str | None = None,
1065
- ) -> None:
1066
- """Close the session, best-effort. Same reasoning as ``_open_session``:
1067
- the run's own verdict is what matters, and a failed PATCH must not
1068
- change it.
1066
+ ) -> bool:
1067
+ """Close the session. Returns whether the service recorded the close.
1068
+
1069
+ The close is tried three times (``finalize_session`` retries a dropped
1070
+ connection, a timeout or a 5xx). If it still fails, one line names the
1071
+ session and the exact request that closes it by hand, and False goes
1072
+ back to the caller: a replay's verdict rides this close, so a caller
1073
+ whose verdict is the point (``test run``) turns it into exit 2 rather
1074
+ than reporting a pass nobody recorded. A caller with nothing riding on
1075
+ it may ignore the answer. With no session there is nothing to close,
1076
+ which is True.
1069
1077
 
1070
1078
  ``failed_step`` is which flowchart block a failing replay died on, and
1071
1079
  ``failure_message`` is what the runner said there. Both default to None
@@ -1074,7 +1082,7 @@ def _close_session(
1074
1082
  fail.
1075
1083
  """
1076
1084
  if session_id is None:
1077
- return
1085
+ return True
1078
1086
  try:
1079
1087
  finalize_session(
1080
1088
  api_url=api_url,
@@ -1085,7 +1093,43 @@ def _close_session(
1085
1093
  failure_message=failure_message,
1086
1094
  )
1087
1095
  except ApiError as e:
1088
- err_console.print(f"[yellow]could not close the session[/yellow]: {e.message}")
1096
+ err_console.print(
1097
+ f"[yellow]could not close session {session_id}[/yellow]: "
1098
+ f"{escape(e.message)} — its verdict was not recorded, and the "
1099
+ "service will mark it abandoned. To record it now: "
1100
+ + escape(
1101
+ _close_command(
1102
+ api_url=api_url,
1103
+ session_id=session_id,
1104
+ exit_code=exit_code,
1105
+ failed_step=failed_step,
1106
+ )
1107
+ ),
1108
+ soft_wrap=True,
1109
+ )
1110
+ return False
1111
+ return True
1112
+
1113
+
1114
+ def _close_command(
1115
+ *, api_url: str, session_id: UUID, exit_code: int, failed_step: int | None
1116
+ ) -> str:
1117
+ """The ``curl`` that closes a session by hand, with the verdict it had.
1118
+
1119
+ The token is left as ``$VERIFAIED_API_TOKEN`` rather than printed: this
1120
+ line lands in CI logs. The failure text is left out — it is free text
1121
+ that would need shell quoting to survive, and the exit code and step
1122
+ are the verdict.
1123
+ """
1124
+ body: dict[str, Any] = {"ended": True, "exit_code": exit_code}
1125
+ if failed_step is not None:
1126
+ body["failed_step"] = failed_step
1127
+ return (
1128
+ f"curl -X PATCH {api_url.rstrip('/')}/sessions/{session_id} "
1129
+ '-H "Authorization: Bearer $VERIFAIED_API_TOKEN" '
1130
+ "-H 'Content-Type: application/json' "
1131
+ f"-d '{json.dumps(body, separators=(',', ':'))}'"
1132
+ )
1089
1133
 
1090
1134
 
1091
1135
  def _window_url(