Files
paliad/internal/services/event_category_coverage_test.go
m d22ace1019 feat(t-paliad-136): Phase C — RoP-rigorous tree taxonomy revision
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.
2026-05-05 13:29:47 +02:00

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)
}
}