Editing a scenario and saving (with or without changes) returned 500:
function pg_advisory_xact_lock(smallint, bigint) does not exist
Postgres only ships (int4, int4) and (bigint) variants. The two-arg call
passed `m = hash(uuid) & 0xFFFFFFFF` which can reach 2^32-1, so psycopg
promoted it to bigint and no overload matched.
Switched to the single-arg bigint form. While there, replaced Python's
built-in hash() with hashlib.blake2b(...) — the built-in is randomised
per process via PYTHONHASHSEED, so gunicorn workers were computing
different lock keys for the same scenario and the lock wasn't actually
serialising across workers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The +New test modal capped at max-w-2xl rendered the 15-column MITRE matrix
in a 672px frame with no height cap, so the matrix spilled to the right of
the dialog, the form bottom dropped below the viewport, and neither scroll
direction worked — buttons were unreachable.
- Modal: add a `size` prop (default 2xl, back-compat) with a `7xl` preset.
Cap height at calc(100vh-2rem), make the header sticky, and wrap children
in a min-w-0 flex-1 overflow-y-auto body so tall content scrolls inside.
- MitreTagPicker: move overflow-x-auto from the grid itself to a dedicated
scroller wrapper, and add `min-w-0` so the constraint propagates from the
modal body. The grid's 1680px intrinsic min-width previously prevented
the parent's overflow-x-auto from kicking in.
- AdminTestsPage: switch the form layout from `grid gap-3` to `flex flex-col
gap-3 min-w-0` and set the modal size to 7xl. The CSS Grid form was
propagating min-width: auto to all its items, which let the picker drag
the body past the modal width.
- AdminScenariosPage: bump the modal to size 3xl for breathing room around
the catalogue picker.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spec-reviewer + code-reviewer findings applied:
Must-fix
- Filter combinator AND-semantics: tactic+technique+subtechnique now intersect
(one IN subquery per facet) instead of being pooled into one OR. Reviewers
flagged both the wrong default semantics and the theoretical UUID-collision
risk of pooling tactic/technique/sub UUIDs into a shared list across
three columns.
- Front-end mutation cache hygiene: updateMeta + setTests both
`onSettled: invalidate` so a partial failure leaves the cache consistent.
Should-fix
- Per-scenario pg_advisory_xact_lock on set_scenario_tests — serialises
concurrent reorders, mirrors M4 /mitre/sync pattern.
- Backend/front consistency on duplicate tests in a scenario: the
UNIQUE(scenario_id, position) constraint already allows the same
test_template multiple times (chained ops), so the catalogue picker no
longer excludes already-picked items.
Nice-to-have
- N+1 eradicated in test_template view rendering: _to_views_batch
builds {uuid → MitreRow} maps in 3 queries up-front; list endpoint
now issues 4 queries total regardless of list size.
- Wire-level item length caps on tags (64) and expected_iocs (255)
via Annotated[str, StringConstraints(...)] — returns 400 instead of
bubbling up StringDataRightTruncation.
- 4 new pytest covering the AND-filter, extra="forbid" rejection,
empty mitre_tags clearing, and the 65-char tag cap. Total now
81 pytest + 38 e2e pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 4 Playwright tests: API CRUD round-trip, scenario reorder via PUT, SPA
list + opsec filter, SPA scenario list rendering with ordered tests.
- afterAll restores the stable admin (admin@metamorph.local) per the
test_admin memory rule.
- CHANGELOG M5 section + Fixed subsections for the LogRecord 'name'
collision and the React `currentTarget` vs `target` quirk.
- README status bumps to M0-M5.
- tasks/lessons.md captures the new patterns (sentinel pattern for
partial-update, FK ordering in /diag/reset, dnd-kit stable IDs).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Service `app/services/test_templates.py`: CRUD with MITRE tag resolution
(kind, external_id) → polymorphic join, filters by tactic/technique/
subtechnique/opsec/tag, `_UNSET` sentinel for partial-update semantics.
- Service `app/services/scenario_templates.py`: ordered test list, reorder
via full-replace (atomic w.r.t. UNIQUE(position) constraint), soft-delete.
- REST endpoints on /api/v1/test-templates and /scenario-templates with
pydantic schemas + perm gating (test_template.* and scenario_template.*).
- /diag/reset truncates the 4 new tables before MITRE (FK ordering).
- 19 pytest covering CRUD, MITRE tag merge, soft-delete chaining, perm
enforcement, and reorder atomicity.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>