Phase 23 closed as PARTIAL: fake coverage was gone, but the E2E layer itself
was still empty — no conftest.py anywhere, ~15 fixtures that never existed,
6 files crashing at import on os.environ["E2E_PASSWORD"], and CI never ran E2E.
STATUS's "10 E2E files / ~80+ cases ✅" described files that had never executed.
Infrastructure delivered:
- tests/e2e/conftest.py: ApiClient (trust_env=False — a host http_proxy turns
localhost calls into 502), operator_token(username) / new_user / admin_token
fixtures, and a session probe that calls pytest.exit(4) when the stack is
unreachable. NOT skip: skip would launder "did not run" into "passed".
- pytest.ini, tests/e2e/requirements.txt, and tests/e2e/sql/
seed_e2e_operators.sql — migrations contain NO operator seed at all, so a
fresh database has zero logins; the seed creates 6 e2e accounts with role
bindings, idempotent, password injected via `psql -v pwd`.
- CI job `e2e`: compose up -> poll /health/live (timeout = fail) -> seed ->
pytest -> always teardown. Gate D-23-D is now closed.
- 8 E2E files migrated off import-time env reads; tautological assertions
(`x == t or not x`), a counselor token standing in for content_editor, and a
bare `status_code == 200` claiming to prove INV-020 were replaced with real
permission/behavior assertions.
Six real production defects found only by running against real PostgreSQL
(H2 has zero coverage for all of them):
- D-23B-A (Critical): stale V042__add_appointment_operator_register_permission.sql
left in target/classes; src had replaced it with V042__add_service_location_
announcements.sql. Packing without clean put two V042 in the jar -> Flyway
"Found more than one migration with version 042" -> app would not start.
Guard added: testBuildOutputHasNoDuplicateMigrationVersions scans build output
(src is clean, so only this catches it).
- D-23B-B (High): non-UUID path variable reached the DB -> 500 + leaked
`invalid input syntax for type uuid`. Now rejected at the controller.
- D-23B-C (High): 6 E2E files raised KeyError at import -> never collected.
- D-23B-E (Critical): permissions = 48, role_permissions for super_admin = 18,
matchmaker = 0. V015's "grant all to super_admin" was a point-in-time snapshot;
the 30 permissions added later were never granted. On a fresh stack an admin
cannot create a service station and matchmakers can do nothing. V044 restores
super_admin to its documented definition (not an expansion); the matchmaker
permission set needs a product decision and is left as a residual.
- D-23B-I (Medium): IllegalArgumentException (e.g. invalid activity type) was
unmapped -> 500. Now BAD_REQUEST.
- D-23B-J (High): `operator does not exist: uuid = character varying` — Java
ids are String, columns are uuid, PostgreSQL does not implicitly cast in
comparisons. Fixed at the source with JDBC `stringtype=unspecified` (docker
profile); production datasource comes from deploy env and needs the same.
One judgement retracted: an initial read suggested service_appointment.register
was missing. It is not — the endpoint uses service.assisted_registration, seeded
in V029, and the code deliberately avoids a duplicate permission. Nothing was
changed on that basis.
Verification: 33 E2E passed against a live stack; unreachable stack and a
missing operator password both exit 4 (never skip/pass); backend regression
656 / 0 failures / 0 errors / 3 skipped (baseline 655, +1 guard, negatively
validated by injecting a duplicate V042 -> exactly one test turned red).
Backend
- Service Station: expose the 「临时公告」 chain end-to-end (V042 table/service existed
but had ZERO endpoints -> both admin and miniapp could never reach it): 5 operator
endpoints (list/create/update/publish/archive) + 1 user-facing read endpoint.
- Fix privilege escalation: create/update were guarded only by the class-level
service_location.view, making V015's service_location.manage a never-used dead
permission -> read-only roles could edit stations. Permissions moved to method level.
- Add ServiceLocationOperatorController to OperatorEndpointPermissionCoverageTest
(it was NOT scanned, which is why the escalation stayed invisible).
- Add create/update validation (address NOT NULL + status CHECK only blew up on PG).
- Banner: validation failures threw bare RuntimeException -> 500; now VALIDATION_ERROR
-> 400. Add deleted_at IS NULL to both home queries (soft-deleted banners stayed live).
- Content: operator list/detail split (admin «publish» was a dead end because it reused
the published-only public API); category deactivate/reactivate; article body on edit.
- Service Program: operator member list now includes pending (approval reachable);
active-only public visibility (draft leak); memberCount/pendingCount aggregation;
withdraw/removeMember; reuse row on re-apply (UNIQUE(program_id,user_id) crash).
- MeController: add the missing GET /me/service-history route.
Schema / guards
- schema-h2.sql: add service_location_announcements (+ chk_announcement_status) and
content_categories (+ uk_content_category_code, chk_category_status); move both into
REQUIRED_IN_TEST_SCHEMA. MigrationValidationTest baseline 41 -> 42.
- V11PermissionMatrixContractTest now scans ALL migrations instead of hardcoding V015+V029.
- New: ServiceLocationAnnouncementPhase19IntegrationTest (24),
ServiceLocationControllerPermissionContractTest (5),
ServiceLocationAnnouncementConstraintParityTest (5, prod DDL <-> test schema <->
backend whitelist <-> admin labels).
Frontend
- admin-web: ServiceStationView gains announcement management (statuses, create/edit,
publish/archive with confirm, expiry); write buttons gated by service_location.manage.
- miniapp: U50 renders the 临时公告 block via new serviceStationService; page no longer
talks to utils/api directly; announcement failure degrades to [] instead of erroring.
- ui-tests: U37 four-group + U50 announcement contracts; /home assertion retargeted to
the service layer.
Verification
- Negative verification: 3 injected defects (drop soft-delete filter / drop test-schema
CHECK / downgrade write endpoint to view) -> 4 named tests fail precisely, no
collateral failures; restored, NEG-VAL residue = 0.
- Backend full regression: 602 tests, 0 failures, 0 errors, 3 skipped (baseline 420).
- admin-web: vue-tsc --noEmit EXIT=0, vite build EXIT=0. Miniapp: tsc --noEmit EXIT=0.
- ui-tests 14/14 PASS.
Phase 15 (activity registration / review / checkin) verification closure.
Two CRITICAL defects, both "production always had it, test schema never did":
- D1: generateCheckinToken wrote the token to Redis with value "used", while
scanCheckin rejects any token marked "used" -> every freshly issued check-in
code was dead on arrival, staff scanning failed 100% of the time.
Issuing now writes "issued"; "used" is set only after a successful scan.
- D9: UNIQUE(activity_id, user_id) exists in production, but re-registering
after cancel/reject went through INSERT -> users who cancelled once could
never register for that activity again (duplicate key 500). The H2 test
schema lacked the UNIQUE, so the path had zero coverage. Now the original
row is reused via an atomic conditional UPDATE.
Checkin (D2-D8):
- D2: duplicate check-in was read-then-write; now atomic checkinIfNotCheckedIn
and repeat scans are idempotent (alreadyCheckedIn) without duplicate audit.
- D3: no data scope at all -> added activity staff assignment (V040) and
assertCheckinScope; INV-020 (org relation grants nothing).
- D4: manualCheckin did not validate activity existence/cancellation.
- D5: getCheckinStats NPE on missing activity; remainingCount was semantically
"free seats" -> replaced with notCheckedInCount.
- D6: malformed timestamp caused 500 -> 400. D7: constant-time signature compare.
Registration / review / cancel (D10-D13):
- D10: backend registration list only queried status='approved' -> the review
screen never saw pending rows (review unusable).
- D11: registration_start_at / registration_end_at were never enforced.
- D12: cancel had no audit log (the only write without one).
- D13: reject/cancel were read-then-full-update -> rejectIfPending/cancelIfActive.
Capacity groups (D14, plan item 2): groups were stored and read back but never
participated in capacity -> incrementIfAvailable / decrementIfPositive now make
approved_count real; activities without groups are unaffected.
Frontend contract (F1-F2):
- F1: admin-web had no check-in surface at all -> ActivityCheckinView.vue
(paste/scan token, manual check-in, stats) + /activities/checkin route & nav.
- F2: miniapp had no cancel-registration wrapper although the endpoint existed.
Test fidelity (T1-T3):
- T1: test schema gained UNIQUE(activity_id,user_id) + chk_reg_status +
chk_checkin_status; two new guards parse the real V010 DDL and compare.
- T2: 3 assertTrue(true) placeholders in CheckinServiceTest (covering duplicate
check-in, expiry, no-sensitive-plaintext) replaced with 5 real guards that
parse the real source and real production DDL.
- T3: activity_registrations + activity_staff_assignments moved into
REQUIRED_IN_TEST_SCHEMA; MigrationValidationTest baseline 39 -> 40.
Note: ActivityStaffAssignment must NOT extend BaseEntity - the table is a
composite PK (activity_id, operator_id); BaseEntity would add id/updated_at/
deleted_at to every INSERT and break on PostgreSQL (caught by
EntityMigrationParityTest immediately).
Tests: ActivityPhase15Test (21), CheckinServiceTest (5), 2 new constraint
guards. Negative verification: 3 injected defects -> 8 assertions turned red,
all restored.
Regression: 474 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS
(baseline 450). admin-web vue-tsc 0 error, vite build OK.