v3.22.0 (review fixes): the ownership reconciliation and the import guards

The suite/script ownership invariant could break silently: a suites import
with mode "replace" wiped the suites while their scripts stayed behind
(suite_id pointing at gone suites - invisible in the UI), a scripts import
with mode "replace" left every suite's member list dangling, and a script
whose suite_id disagreed with its listing suite stayed inconsistent.

- server: reconcile_scripts_to_suites() repairs the invariant - a script a
  suite lists adopts that suite (the member list is authoritative), a script
  no suite lists joins "Imported scripts"; migrate_scripts_to_suites()
  delegates to it (the startup migration is the same pass) and the suites
  import runs it after loading, so a "replace" adopts the wiped suites'
  scripts instead of losing them.
- the scripts import refuses mode "replace" while any suite still lists
  scripts (400 naming the dangling references; merge is the normal path).
- nits from the review: a legacy numeric `event` is no longer parsed as hex
  in the step summary/error check (0x0A displayed as 0x10), and an integral
  float memory quota (a JSON client's 200.0) is accepted.
- tests: the reconciliation (missing suite, listed-but-mismatched,
  idempotent), the scripts-replace guard (refused/allowed), the
  suites-replace adoption, the float quota, the numeric-event summary.
  Tests: 838 frontend + 800 Python; sw cache simple-v402.
This commit is contained in:
2026-10-06 01:15:20 +03:00
parent 40c1e0f148
commit b2cb42caf5
8 changed files with 166 additions and 27 deletions
+10 -2
View File
@@ -820,7 +820,10 @@ policy) and `POST /api/test/scripts/copy` (`{id, suite_id, name?}`) clones it
into the target. `POST /api/test/scripts/import` takes
`{scripts: [...], mode: "merge"|"replace", suite_id?}` (the old localStorage
export shape; invalid entries are reported in `errors` instead of failing the
whole import; `suite_id` attaches the imported scripts to that suite).
whole import; `suite_id` attaches the imported scripts to that suite). A
`replace` is **refused while any suite still lists scripts** — the wipe would
leave dangling references; delete those scripts first, or import with
`merge`.
### `GET /api/test/suites` and the suite store
@@ -852,7 +855,12 @@ removes it - refused while the suite still holds scripts - and
`{suites: [...], scripts: [...], mode: "merge"|"replace"}`: the scripts are
imported first (ids preserved; a collision gets a fresh uuid and the suites'
references are remapped), then the suites; an unresolvable reference is
reported in `errors` and the suite is skipped.
reported in `errors` and the suite is skipped. After the import the
ownership invariant is reconciled: a script the imported suites list adopts
its listing suite, and a script no suite lists any more — the `replace` mode
wipes the suites that owned them — is **adopted into "Imported scripts"**
instead of becoming invisible (the same reconciliation runs at startup for a
v3.21.0 store).
### `POST /api/test/run` (single script or suite)