Migration 052 fixes six concept↔leaf mismaps in the v3 seed and adds
three proactive entry leaves under spaetere-schriftsaetze.
1. cms-eingang.gericht.hinweisbeschluss — drop the response-to-
preliminary-opinion | DE_INF row. DE_INF (LG) has no
Hinweisbeschluss; the concept lives only in DE_NULL via PatG §83.
2. cms-eingang.gegenseite.upc-inf.klageschrift — drop the notice-of-
defence-intention | UPC_INF row. UPC has no such rule in the corpus;
R.23 reaction is captured by statement-of-defence directly.
3. UPC R.221 cost-appeal sequence (m's Q5): three leaves now surface
BOTH application-for-leave-to-appeal | UPC_COST_APPEAL (sort 100,
R.221.1, 15 days) AND notice-of-appeal | UPC_APP (sort 200,
conditional on leave granted, R.220.1). Replaces the wrong notice-of-
appeal | UPC_COST_APPEAL row that was silently dropping pills.
4. ich-moechte-einreichen.berufung.upc-coa-orders — replace the buggy
application-for-leave-to-appeal | UPC_APP_ORDERS (no rule for that
combo) with request-for-discretionary-review | UPC_APP_ORDERS
(R.220.3).
5. cms-eingang.gericht.anordnung — narrow request-for-discretionary-
review NULL → UPC_APP_ORDERS. R.220.3 review applies specifically
to the Anordnungen / 15-day track.
6a. reply-to-cross-appeal coverage: add UPC_APP rows under upc-{inf,
rev}.berufungsschrift so the reply leaf is reachable when the
opponent files an Anschlussberufung.
6b. New leaves under ich-moechte-einreichen.spaetere-schriftsaetze for
proactive entry: r116-eingaben (EPA R.116 final submissions),
anschlussberufung-upc (R.237), reply-to-cross-appeal-upc (R.238).
NO `RAISE EXCEPTION` coverage gate (m's Q7) — last night's outage was
caused by exactly that pattern in migration 049. Replaced with a Go-
side test in event_category_coverage_test.go that asserts every
category='submission' concept is reachable from at least one leaf
(except the prosecution-only exempt list: filing, request-for-
examination, approval-and-translation). Skipped without
TEST_DATABASE_URL; CI gates on it.
bescheid-mit-frist mapping deferred per m's Q4. Will land separately.
Migration verified via supabase MCP dry-run + ROLLBACK on the live
youpc DB; end-state matches design §3.2-§3.4.
101 lines
3.3 KiB
Go
101 lines
3.3 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
_ "github.com/lib/pq"
|
|
|
|
"mgit.msbls.de/m/paliad/internal/db"
|
|
)
|
|
|
|
// TestEventCategoryCoverage is the v4 (t-paliad-136 Phase C) replacement
|
|
// for migration 049's `RAISE EXCEPTION` coverage gate. m's Q7 (2026-05-05)
|
|
// moved gating from migration time (which can block server boot — last
|
|
// night's outage was caused by exactly that) to CI time.
|
|
//
|
|
// Invariant: every concept with category='submission' and is_active=true
|
|
// MUST be reachable from at least one row in paliad.event_category_concepts,
|
|
// EXCEPT a small exempt list of pure-administrative concepts that live on
|
|
// Pathway A (browse-by-proceeding) only — they don't fit the "what just
|
|
// happened" mental model that drives Pathway B.
|
|
//
|
|
// Why this matters: an unreachable submission concept is invisible from
|
|
// the decision tree. The user can't pick it via "CMS-Eingang → …" or any
|
|
// other narrative entry — they'd have to know the German legal name and
|
|
// type it into B2 search. That's an acceptable fallback for prosecution-
|
|
// only artefacts (filing, exam request, translation), and not for
|
|
// litigation submissions.
|
|
//
|
|
// Skipped when TEST_DATABASE_URL is unset, mirroring the other live-DB
|
|
// tests in this package. CI must export TEST_DATABASE_URL pointing at a
|
|
// fresh paliad-schema DB; the migration runner brings it up to head.
|
|
func TestEventCategoryCoverage(t *testing.T) {
|
|
url := os.Getenv("TEST_DATABASE_URL")
|
|
if url == "" {
|
|
t.Skip("TEST_DATABASE_URL not set — skipping live DB test")
|
|
}
|
|
if err := db.ApplyMigrations(url); err != nil {
|
|
t.Fatalf("apply migrations: %v", err)
|
|
}
|
|
pool, err := sqlx.Connect("postgres", url)
|
|
if err != nil {
|
|
t.Fatalf("connect: %v", err)
|
|
}
|
|
defer pool.Close()
|
|
|
|
// Pathway-A-only exempt list. After Phase C (migration 052), reply-
|
|
// to-cross-appeal is reachable via the upc-{inf,rev}.berufungsschrift
|
|
// leaves and the new ich-moechte-einreichen.spaetere-schriftsaetze.
|
|
// reply-to-cross-appeal-upc leaf, so it's no longer exempt here.
|
|
exempt := map[string]bool{
|
|
"filing": true, // filed during prosecution
|
|
"request-for-examination": true, // EPC R.70(1), prosecution
|
|
"approval-and-translation": true, // EPC R.71(3), prosecution
|
|
}
|
|
|
|
ctx := context.Background()
|
|
rows, err := pool.QueryxContext(ctx, `
|
|
SELECT dc.slug
|
|
FROM paliad.deadline_concepts dc
|
|
WHERE dc.is_active
|
|
AND dc.category = 'submission'
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM paliad.event_category_concepts ecc
|
|
WHERE ecc.concept_id = dc.id
|
|
)
|
|
ORDER BY dc.slug
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("coverage query: %v", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var unreachable []string
|
|
for rows.Next() {
|
|
var slug string
|
|
if err := rows.Scan(&slug); err != nil {
|
|
t.Fatalf("scan: %v", err)
|
|
}
|
|
if exempt[slug] {
|
|
continue
|
|
}
|
|
unreachable = append(unreachable, slug)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
t.Fatalf("rows.Err: %v", err)
|
|
}
|
|
|
|
if len(unreachable) > 0 {
|
|
sort.Strings(unreachable)
|
|
t.Errorf("v4 coverage gate: %d submission concept(s) unreachable from any event_category leaf:\n %v\n"+
|
|
"Either map them via paliad.event_category_concepts, or add to the exempt list with a comment "+
|
|
"explaining why they live on Pathway A only.",
|
|
len(unreachable), unreachable)
|
|
}
|
|
}
|