verifaied 0.30.0.dev77__tar.gz → 0.31.0.dev78__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.30.0.dev77 → verifaied-0.31.0.dev78}/.gitignore +5 -0
  2. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/PKG-INFO +69 -4
  3. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/README.md +68 -3
  4. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/pyproject.toml +1 -1
  5. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/cli.py +97 -0
  6. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/client.py +46 -5
  7. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/skill/reference/commands.md +6 -0
  8. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/testcmd.py +603 -30
  9. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/conftest.py +7 -0
  10. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_cli.py +44 -0
  11. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_client.py +119 -2
  12. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_testcmd.py +951 -33
  13. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/uv.lock +1 -1
  14. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/LICENSE +0 -0
  15. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/__init__.py +0 -0
  16. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/__main__.py +0 -0
  17. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/agent_mcp.py +0 -0
  18. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/agent_steps.py +0 -0
  19. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/analyzer.py +0 -0
  20. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/audit.py +0 -0
  21. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/collect_browser.js +0 -0
  22. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/collect_browser.py +0 -0
  23. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/config.py +0 -0
  24. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/failure_text.py +0 -0
  25. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/instrument.py +0 -0
  26. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/instrument_snippets.py +0 -0
  27. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/instrumenter.js +0 -0
  28. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/instrumenter.py +0 -0
  29. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/prompts.py +0 -0
  30. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/proxy.py +0 -0
  31. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/record.py +0 -0
  32. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/recorder_driver.js +0 -0
  33. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/recorder_observer.js +0 -0
  34. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/recorder_picker.js +0 -0
  35. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/recorder_refs.js +0 -0
  36. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/recorder_spec.js +0 -0
  37. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/replay_harvest.ts +0 -0
  38. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/repo_config.py +0 -0
  39. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/run_reporter.js +0 -0
  40. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/runner_dir.py +0 -0
  41. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/skill/SKILL.md +0 -0
  42. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/skillcmd.py +0 -0
  43. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/source_index.py +0 -0
  44. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/stepcheck.py +0 -0
  45. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/subprocess_util.py +0 -0
  46. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/suite_report.py +0 -0
  47. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/uploader.py +0 -0
  48. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/src/verifaied/varcmd.py +0 -0
  49. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/__init__.py +0 -0
  50. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_agent_mcp.py +0 -0
  51. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_audit.py +0 -0
  52. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_browser_settings_parity.py +0 -0
  53. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_caption_parity.py +0 -0
  54. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_check.py +0 -0
  55. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_check_done.py +0 -0
  56. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_clear_manual.py +0 -0
  57. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_collect_browser.py +0 -0
  58. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_config.py +0 -0
  59. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_console_label_parity.py +0 -0
  60. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_instrument.py +0 -0
  61. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_instrument_detect.py +0 -0
  62. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_instrument_snippets.py +0 -0
  63. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_instrumenter.py +0 -0
  64. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_pause_parity.py +0 -0
  65. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_prompt_parity.py +0 -0
  66. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_proxy.py +0 -0
  67. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_record.py +0 -0
  68. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_replay_harvest_runner.py +0 -0
  69. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_repo_config.py +0 -0
  70. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_skill_parity.py +0 -0
  71. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_skillcmd.py +0 -0
  72. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_source_index.py +0 -0
  73. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_stepcheck_parity.py +0 -0
  74. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_suite_report.py +0 -0
  75. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_uploader.py +0 -0
  76. {verifaied-0.30.0.dev77 → verifaied-0.31.0.dev78}/tests/test_varcmd.py +0 -0
@@ -25,6 +25,8 @@ frontend/coverage/
25
25
  # Environment
26
26
  **/.env
27
27
  **/.env.*
28
+ # The e2e/bt stacks' template carries no secrets and is what the docs point at.
29
+ !e2e/.env.example
28
30
 
29
31
  # Docker
30
32
  pgdata/
@@ -48,6 +50,9 @@ backend/coverage-e2e.json
48
50
 
49
51
  # Keys
50
52
  *.pem
53
+ # ...except the throwaway GitHub App key the offline test stacks sign with
54
+ # (paired with no App; see its header).
55
+ !e2e/fixtures/github-app/private-key.pem
51
56
 
52
57
  TODO.txt
53
58
  TODO.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: verifaied
3
- Version: 0.30.0.dev77
3
+ Version: 0.31.0.dev78
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
@@ -476,9 +476,16 @@ verifaied test run --all --env stage
476
476
  # skips the flow's Pause steps and the holds after its checks.
477
477
  verifaied test run "Settings/API tokens" --video watch
478
478
 
479
+ # On a busy CI machine: replay a failed test once more, from a fresh reset.
480
+ verifaied test run --all --retries 1
481
+
479
482
  # What's recorded, and how each last went on this branch.
480
483
  verifaied test list
481
484
 
485
+ # Wrote or pulled a spec by hand? Give every spec on disk its row, its
486
+ # chain and its flowchart. Nothing is deleted; --dry-run sends nothing.
487
+ verifaied test sync
488
+
482
489
  # Or leave a recorder running and drive the whole thing from the web.
483
490
  verifaied test studio
484
491
  ```
@@ -719,6 +726,48 @@ recording of one flow — the composition happens in a throwaway file at run
719
726
  time, which is what makes editing `Auth/Log in` fix every test that starts
720
727
  by logging in. You can also pick pre-steps on the **New test** page.
721
728
 
729
+ ### Hand-written specs — `test sync`
730
+
731
+ A spec doesn't have to come out of the recorder. Every spec names itself
732
+ in a header above its import, and the recorder writes that header on
733
+ every save:
734
+
735
+ ```ts
736
+ // verifaied-test: Settings/API tokens/Create a token
737
+ // verifaied-pre: Login/Paid user
738
+ import { test, expect } from '@playwright/test';
739
+
740
+ test('Create a token', async ({ page }) => {
741
+ await page.getByRole('link', { name: 'Settings' }).click();
742
+ // …
743
+ });
744
+ ```
745
+
746
+ `verifaied-test` is the test's `<feature path>/<name>`, once.
747
+ `verifaied-pre` is a pre-step, one line each, in the order they run. Write
748
+ the file yourself — or pull one somebody else wrote — and `verifaied test
749
+ sync` makes verifAIed agree with it: the test is created if it is new,
750
+ marked recorded, given its chain, and its flowchart is drawn on the
751
+ detail page from the file.
752
+
753
+ ```bash
754
+ verifaied test sync # create and update rows from .verifaied/tests/
755
+ verifaied test sync --dry-run # say what would change, send nothing
756
+ ```
757
+
758
+ Everything is checked before anything is sent, and one problem means
759
+ nothing is sent at all: a spec with no header, a spec that isn't at the
760
+ path its name gives (`.verifaied/tests/<feature path>/<name>.spec.ts`,
761
+ slugged), a pre-step that isn't a spec on disk, a loop, or a test whose
762
+ file verifAIed already has somewhere else — a renamed test keeps the file
763
+ it was recorded to. Pre-steps are synced before the tests that name them,
764
+ so one run is always enough. A test that already agrees with its file
765
+ costs no request.
766
+
767
+ Sync never deletes anything. A test whose spec isn't on disk is listed at
768
+ the end — it may be on another branch, or renamed — so you can delete it
769
+ on the web if it really is gone.
770
+
722
771
  ### Environments
723
772
 
724
773
  A recorded flow runs against a real app *somewhere*, and somewhere is the
@@ -1052,7 +1101,9 @@ replay and lays over the file.
1052
1101
  - `0` — recorded, or replayed cleanly
1053
1102
  - `*` — the replay's own exit code when the spec failed (`test run --all`
1054
1103
  reports the worst across the run)
1055
- - `2` — the replay was clean but its final coverage upload failed
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)
1056
1107
  - `1` — usage/config error (no token, bad `--repo`, unknown test, no
1057
1108
  Playwright)
1058
1109
 
@@ -1082,10 +1133,24 @@ chain leaves the browser, or at `base_url` itself when there is no chain
1082
1133
  either),
1083
1134
  `--description <text>`, `--pre "<Feature>/<Name>"` (repeatable;
1084
1135
  replaces the stored chain — omit it to keep what the test already has),
1085
- and `--no-replay` to skip the replay. `run` takes `--all` in place of a
1086
- test name. `studio` takes `--no-replay` and takes no test name at all —
1136
+ and `--no-replay` to skip the replay. `run` takes `--all` or `--folder
1137
+ "<folder path>"` in place of a test name, `--video fast|watch|off`, and
1138
+ `--retries <n>` (default 0): a test whose replay **failed** is reset and
1139
+ replayed again, up to `n` more times, and the summary says `passed on
1140
+ retry 1 of 2` when a later attempt passes. Retries are for infrastructure
1141
+ — a sign-in that hung, a page slow to load on a loaded machine — not for a
1142
+ flow that is wrong, which fails every attempt. Each attempt is its own
1143
+ run; the **last** one is the verdict, the coverage and the video and trace
1144
+ the test keeps (a failed attempt that is retried keeps its verdict and its
1145
+ failure text but uploads no video or trace). A refusal — a missing spec, a
1146
+ variable this machine cannot supply, a reset that failed — is never
1147
+ retried, and neither is a clean run whose coverage upload failed (`2`). `studio` takes `--no-replay` and takes no test name at all —
1087
1148
  what it records is whatever the web asks for.
1088
1149
 
1150
+ `sync` takes `--repo`, `--branch` (the branch a new test is stamped with),
1151
+ `--root`, `--api-url`, `--token` and `--dry-run`. It exits `1` when a spec
1152
+ needs fixing (and sends nothing) and `2` when the API refuses a request.
1153
+
1089
1154
  ## `verifaied skill install` — hand the loop to your agent
1090
1155
 
1091
1156
  The CLI ships the agent skill for using it. One command writes it where a
@@ -430,9 +430,16 @@ verifaied test run --all --env stage
430
430
  # skips the flow's Pause steps and the holds after its checks.
431
431
  verifaied test run "Settings/API tokens" --video watch
432
432
 
433
+ # On a busy CI machine: replay a failed test once more, from a fresh reset.
434
+ verifaied test run --all --retries 1
435
+
433
436
  # What's recorded, and how each last went on this branch.
434
437
  verifaied test list
435
438
 
439
+ # Wrote or pulled a spec by hand? Give every spec on disk its row, its
440
+ # chain and its flowchart. Nothing is deleted; --dry-run sends nothing.
441
+ verifaied test sync
442
+
436
443
  # Or leave a recorder running and drive the whole thing from the web.
437
444
  verifaied test studio
438
445
  ```
@@ -673,6 +680,48 @@ recording of one flow — the composition happens in a throwaway file at run
673
680
  time, which is what makes editing `Auth/Log in` fix every test that starts
674
681
  by logging in. You can also pick pre-steps on the **New test** page.
675
682
 
683
+ ### Hand-written specs — `test sync`
684
+
685
+ A spec doesn't have to come out of the recorder. Every spec names itself
686
+ in a header above its import, and the recorder writes that header on
687
+ every save:
688
+
689
+ ```ts
690
+ // verifaied-test: Settings/API tokens/Create a token
691
+ // verifaied-pre: Login/Paid user
692
+ import { test, expect } from '@playwright/test';
693
+
694
+ test('Create a token', async ({ page }) => {
695
+ await page.getByRole('link', { name: 'Settings' }).click();
696
+ // …
697
+ });
698
+ ```
699
+
700
+ `verifaied-test` is the test's `<feature path>/<name>`, once.
701
+ `verifaied-pre` is a pre-step, one line each, in the order they run. Write
702
+ the file yourself — or pull one somebody else wrote — and `verifaied test
703
+ sync` makes verifAIed agree with it: the test is created if it is new,
704
+ marked recorded, given its chain, and its flowchart is drawn on the
705
+ detail page from the file.
706
+
707
+ ```bash
708
+ verifaied test sync # create and update rows from .verifaied/tests/
709
+ verifaied test sync --dry-run # say what would change, send nothing
710
+ ```
711
+
712
+ Everything is checked before anything is sent, and one problem means
713
+ nothing is sent at all: a spec with no header, a spec that isn't at the
714
+ path its name gives (`.verifaied/tests/<feature path>/<name>.spec.ts`,
715
+ slugged), a pre-step that isn't a spec on disk, a loop, or a test whose
716
+ file verifAIed already has somewhere else — a renamed test keeps the file
717
+ it was recorded to. Pre-steps are synced before the tests that name them,
718
+ so one run is always enough. A test that already agrees with its file
719
+ costs no request.
720
+
721
+ Sync never deletes anything. A test whose spec isn't on disk is listed at
722
+ the end — it may be on another branch, or renamed — so you can delete it
723
+ on the web if it really is gone.
724
+
676
725
  ### Environments
677
726
 
678
727
  A recorded flow runs against a real app *somewhere*, and somewhere is the
@@ -1006,7 +1055,9 @@ replay and lays over the file.
1006
1055
  - `0` — recorded, or replayed cleanly
1007
1056
  - `*` — the replay's own exit code when the spec failed (`test run --all`
1008
1057
  reports the worst across the run)
1009
- - `2` — the replay was clean but its final coverage upload failed
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)
1010
1061
  - `1` — usage/config error (no token, bad `--repo`, unknown test, no
1011
1062
  Playwright)
1012
1063
 
@@ -1036,10 +1087,24 @@ chain leaves the browser, or at `base_url` itself when there is no chain
1036
1087
  either),
1037
1088
  `--description <text>`, `--pre "<Feature>/<Name>"` (repeatable;
1038
1089
  replaces the stored chain — omit it to keep what the test already has),
1039
- and `--no-replay` to skip the replay. `run` takes `--all` in place of a
1040
- test name. `studio` takes `--no-replay` and takes no test name at all —
1090
+ and `--no-replay` to skip the replay. `run` takes `--all` or `--folder
1091
+ "<folder path>"` in place of a test name, `--video fast|watch|off`, and
1092
+ `--retries <n>` (default 0): a test whose replay **failed** is reset and
1093
+ replayed again, up to `n` more times, and the summary says `passed on
1094
+ retry 1 of 2` when a later attempt passes. Retries are for infrastructure
1095
+ — a sign-in that hung, a page slow to load on a loaded machine — not for a
1096
+ flow that is wrong, which fails every attempt. Each attempt is its own
1097
+ run; the **last** one is the verdict, the coverage and the video and trace
1098
+ the test keeps (a failed attempt that is retried keeps its verdict and its
1099
+ failure text but uploads no video or trace). A refusal — a missing spec, a
1100
+ variable this machine cannot supply, a reset that failed — is never
1101
+ retried, and neither is a clean run whose coverage upload failed (`2`). `studio` takes `--no-replay` and takes no test name at all —
1041
1102
  what it records is whatever the web asks for.
1042
1103
 
1104
+ `sync` takes `--repo`, `--branch` (the branch a new test is stamped with),
1105
+ `--root`, `--api-url`, `--token` and `--dry-run`. It exits `1` when a spec
1106
+ needs fixing (and sends nothing) and `2` when the API refuses a request.
1107
+
1043
1108
  ## `verifaied skill install` — hand the loop to your agent
1044
1109
 
1045
1110
  The CLI ships the agent skill for using it. One command writes it where a
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "verifaied"
3
- version = "0.30.0.dev77"
3
+ version = "0.31.0.dev78"
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"
@@ -67,6 +67,7 @@ from verifaied.testcmd import (
67
67
  run_test_record,
68
68
  run_test_run,
69
69
  run_test_studio,
70
+ run_test_sync,
70
71
  )
71
72
  from verifaied.uploader import (
72
73
  UploaderError,
@@ -2053,6 +2054,16 @@ def test_run_command(
2053
2054
  "replay_type_delay_ms / replay_pauses for this run."
2054
2055
  ),
2055
2056
  ),
2057
+ retries: int = typer.Option(
2058
+ 0,
2059
+ "--retries",
2060
+ min=0,
2061
+ help=(
2062
+ "Replay a test whose run failed up to this many more times, each "
2063
+ "from a fresh reset. For flaky infrastructure; the last attempt "
2064
+ "is the verdict and coverage recorded. Refusals are never retried."
2065
+ ),
2066
+ ),
2056
2067
  ) -> None:
2057
2068
  """Replay a recorded test, capturing video, trace and coverage.
2058
2069
 
@@ -2078,6 +2089,10 @@ def test_run_command(
2078
2089
  reset that fails stops the run; so does a variable this machine
2079
2090
  cannot supply, before anything is reset.
2080
2091
 
2092
+ ``--retries N`` replays a test whose run failed up to N more times,
2093
+ each from a fresh reset. The last attempt is the verdict and the
2094
+ coverage the test keeps; a refusal is never retried.
2095
+
2081
2096
  Exit codes:
2082
2097
 
2083
2098
  0 every test replayed cleanly
@@ -2109,6 +2124,7 @@ def test_run_command(
2109
2124
  proxy_target=proxy_target,
2110
2125
  proxy_port=proxy_port,
2111
2126
  video=video.value if video is not None else None,
2127
+ retries=retries,
2112
2128
  )
2113
2129
  except (RecordedTestError, UploaderError) as e:
2114
2130
  err_console.print(f"[red]VerifAIed error[/red]: {e}")
@@ -2323,6 +2339,87 @@ def test_list_command(
2323
2339
  raise typer.Exit(code=2) from e
2324
2340
 
2325
2341
 
2342
+ @test_app.command("sync")
2343
+ def test_sync_command(
2344
+ root: Path | None = typer.Option(
2345
+ None,
2346
+ "--root",
2347
+ help="Repo root whose .verifaied/tests/ is synced (default: cwd).",
2348
+ ),
2349
+ dry_run: bool = typer.Option(
2350
+ False,
2351
+ "--dry-run",
2352
+ help="Say what would be created and updated, and send nothing.",
2353
+ ),
2354
+ repo: str | None = typer.Option(
2355
+ None,
2356
+ "--repo",
2357
+ "-r",
2358
+ help=(
2359
+ "Repository to sync. Accepts a UUID, an `owner/name` slug, or "
2360
+ "omit to auto-detect from `git remote get-url origin`."
2361
+ ),
2362
+ ),
2363
+ branch: str | None = typer.Option(
2364
+ None,
2365
+ "--branch",
2366
+ "-b",
2367
+ help=(
2368
+ "Branch a new test is stamped with. Defaults to "
2369
+ "`git branch --show-current`."
2370
+ ),
2371
+ ),
2372
+ api_url: str | None = typer.Option(
2373
+ None,
2374
+ "--api-url",
2375
+ help=f"Backend base URL. Defaults to ${ENV_API_URL} or {DEFAULT_API_URL}.",
2376
+ ),
2377
+ token: str | None = typer.Option(
2378
+ None,
2379
+ "--token",
2380
+ help=f"API token (vr_live_...). Defaults to ${ENV_API_TOKEN}.",
2381
+ ),
2382
+ ) -> None:
2383
+ """Create or update a test row for every spec under .verifaied/tests/.
2384
+
2385
+ Each spec names itself in a header above its import —
2386
+ ``// verifaied-test: <feature path>/<name>`` — and any tests that
2387
+ replay first, one ``// verifaied-pre: <ref>`` line each, in order. A
2388
+ recorded spec carries the header already; a hand-written one gets its
2389
+ row, its chain and its flowchart from this command. Everything is
2390
+ checked before anything is sent, and a row that already agrees costs
2391
+ no request. Rows are never deleted: one whose spec is missing is
2392
+ listed for you to remove on the web.
2393
+
2394
+ Exit codes:
2395
+
2396
+ 0 every spec is synced (or would be, with --dry-run)
2397
+ 1 a spec needs fixing first (nothing was sent), or a usage error
2398
+ 2 the API refused a request
2399
+ """
2400
+ resolved_url, resolved_token, repo_id = _test_prelude(
2401
+ repo=repo, api_url=api_url, token=token
2402
+ )
2403
+ try:
2404
+ run_test_sync(
2405
+ api_url=resolved_url,
2406
+ token=resolved_token,
2407
+ repo_id=repo_id,
2408
+ root=root or Path.cwd(),
2409
+ branch=branch,
2410
+ dry_run=dry_run,
2411
+ console=console,
2412
+ )
2413
+ except (RecordedTestError, UploaderError) as e:
2414
+ err_console.print(f"[red]VerifAIed error[/red]: {e}")
2415
+ raise typer.Exit(code=1) from e
2416
+ except ApiError as e:
2417
+ err_console.print(
2418
+ f"[red]VerifAIed error[/red]: {_api_error_text(e, resolved_url)}"
2419
+ )
2420
+ raise typer.Exit(code=2) from e
2421
+
2422
+
2326
2423
  @var_app.command("set")
2327
2424
  def var_set_command(
2328
2425
  name: str = typer.Argument(
@@ -7,6 +7,8 @@ can be swapped if/when we add a streaming variant for huge uploads.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import time
11
+ from collections.abc import Callable
10
12
  from dataclasses import dataclass
11
13
  from pathlib import Path
12
14
  from typing import Any
@@ -16,6 +18,36 @@ import httpx
16
18
 
17
19
  from verifaied.uploader import Payload
18
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.
26
+ UPLOAD_BACKOFF: tuple[float, ...] = (2.0, 6.0)
27
+ # Indirected so tests can skip the waits.
28
+ _sleep: Callable[[float], None] = time.sleep
29
+
30
+
31
+ def _send_with_retry(send: Callable[[], httpx.Response]) -> httpx.Response:
32
+ """Send an upload, retrying what a retry can fix.
33
+
34
+ A connection that failed or timed out, or a 5xx, is tried again after
35
+ each :data:`UPLOAD_BACKOFF` delay; the last attempt's answer — or its
36
+ exception — is what the caller sees. A 4xx is returned at once: the
37
+ request itself is wrong (a bad token, a payload too big) and sending
38
+ it again only repeats the refusal.
39
+ """
40
+ for delay in UPLOAD_BACKOFF:
41
+ try:
42
+ response = send()
43
+ except httpx.TransportError:
44
+ _sleep(delay)
45
+ continue
46
+ if response.status_code < 500:
47
+ return response
48
+ _sleep(delay)
49
+ return send()
50
+
19
51
 
20
52
  class RepoNotFoundError(Exception):
21
53
  """The owner/name slug isn't connected to verifAIed under this
@@ -72,7 +104,8 @@ def upload(
72
104
 
73
105
  Times out at 60s by default — the synchronous endpoint persists rows
74
106
  and re-runs the analyzer inline, so very large repos may push past
75
- the default httpx 5s. Callers can override for CI use.
107
+ the default httpx 5s. Callers can override for CI use. A dropped
108
+ connection, a timeout or a 5xx is tried twice more, after 2s and 6s.
76
109
 
77
110
  ``session_id`` attaches the upload to a session in the evidence ledger;
78
111
  ``driver`` says who was at the wheel for the implicit session a
@@ -101,7 +134,9 @@ def upload(
101
134
  headers = {"Authorization": f"Bearer {token}"}
102
135
 
103
136
  try:
104
- response = httpx.post(url, json=body, headers=headers, timeout=timeout)
137
+ response = _send_with_retry(
138
+ lambda: httpx.post(url, json=body, headers=headers, timeout=timeout)
139
+ )
105
140
  except httpx.HTTPError as e:
106
141
  raise ApiError(0, f"could not reach {url}: {e}") from e
107
142
 
@@ -292,21 +327,27 @@ def upload_artifact(
292
327
 
293
328
  Streamed as multipart with a generous timeout: a trace archive is tens
294
329
  of megabytes and the server hashes it on the way through, so the
295
- default 60s used elsewhere is too tight to be safe.
330
+ default 60s used elsewhere is too tight to be safe. Retried like
331
+ :func:`upload` on a dropped connection, a timeout or a 5xx.
296
332
  """
297
333
  url = f"{api_url.rstrip('/')}/sessions/{session_id}/artifacts"
298
334
  data: dict[str, str] = {"kind": kind}
299
335
  if label is not None:
300
336
  data["label"] = label
301
- try:
337
+
338
+ def send() -> httpx.Response:
339
+ # Reopened per attempt: a retry has to stream the file from the top.
302
340
  with path.open("rb") as handle:
303
- response = httpx.post(
341
+ return httpx.post(
304
342
  url,
305
343
  data=data,
306
344
  files={"file": (path.name, handle, _media_type(path))},
307
345
  headers={"Authorization": f"Bearer {token}"},
308
346
  timeout=timeout,
309
347
  )
348
+
349
+ try:
350
+ response = _send_with_retry(send)
310
351
  except OSError as e:
311
352
  raise ApiError(0, f"could not read {path}: {e}") from e
312
353
  except httpx.HTTPError as e:
@@ -135,6 +135,12 @@ a normal `@playwright/test` file: read it, edit it, commit it.
135
135
  steps, then rebuild from there; `--from 0` starts again, `--from <step
136
136
  count>` appends
137
137
  - `verifaied test studio` — leave a recorder running for the web UI to drive
138
+ - `verifaied test sync` — create or update the row for every spec under
139
+ `.verifaied/tests/`, read off each spec's header (`// verifaied-test:
140
+ <ref>` and one `// verifaied-pre: <ref>` per pre-step, above the import).
141
+ How a hand-written spec gets its row, chain and flowchart. Checks every
142
+ spec before sending anything, never deletes a row, and `--dry-run` sends
143
+ nothing
138
144
 
139
145
  `--env <name>` points any of them at another environment from
140
146
  `.verifaied/config.toml`; `--no-replay` skips the replay that a recording