Files
paliad/pkg/litigationplanner/embedded/upc/snapshot_test.go
mAi fb0c849fc4
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
feat(litigationplanner): embedded UPC snapshot + generator (Slice C, m/paliad#124 §19)
Lays the foundation for youpc.org's cross-repo integration: an
in-package UPC subset of paliad's deadline corpus, embedded as JSON,
that any consumer can use to run the litigationplanner engine without
DB access.

Generator (cmd/gen-upc-snapshot):
  - Reads paliad's live DB (DATABASE_URL), applies pending migrations
    to match schema HEAD, SELECTs the UPC subset
    (proceeding_types WHERE jurisdiction='UPC' AND is_active=true,
    deadline_rules WHERE lifecycle_state='published' AND is_active=true
    on those proceedings, referenced trigger_events, DE+UPC holidays,
    UPC courts).
  - Writes pretty-printed JSON to
    pkg/litigationplanner/embedded/upc/{proceeding_types, rules,
    trigger_events, holidays, courts, meta}.json.
  - Idempotent — same DB state → same output (modulo
    meta.generated_at + auto-versioned suffix).
  - Date-stamped versioning (YYYY-MM-DD-N) with same-day suffix bump.
  - Operator runbook in cmd/gen-upc-snapshot/README.md.

Embedded subpackage (pkg/litigationplanner/embedded/upc/):
  - embed.go    — //go:embed *.json + LoadMeta()
  - snapshot.go — SnapshotCatalog (full lp.Catalog impl: LoadProceeding
    / LoadProceedingByID / LoadRuleByID / LoadRuleByCode /
    LoadRulesByTriggerEvent / LoadTriggerEventsByIDs / LookupEvents);
    O(1) map lookups; LookupEvents linear over the < 100-row UPC corpus.
  - holidays.go — SnapshotHolidayCalendar implementing lp.HolidayCalendar
    (IsNonWorkingDay / Adjust* with structured AdjustmentReason).
  - courts.go   — SnapshotCourtRegistry implementing lp.CourtRegistry.
  - Compile-time assertions (_ lp.X = (*Snapshot*)(nil)) catch
    interface drift.

Wire-up for consumers:
  cat, _ := upc.NewCatalog()
  hc, _  := upc.NewHolidayCalendar()
  cr, _  := upc.NewCourtRegistry()
  timeline, _ := lp.Calculate(ctx, "upc.inf.cfi", "2026-05-26",
                              lp.CalcOptions{}, cat, hc, cr)

Tests (snapshot_test.go, all DB-free):
  - meta parses cleanly, non-zero counts
  - LoadProceeding(upc.inf.cfi) returns expected proc + rules
  - LoadProceeding(unknown) returns ErrUnknownProceedingType
  - LookupEvents(Jurisdiction:UPC, all-following) covers corpus
  - LookupEvents(party=defendant, next) scopes anchors correctly
  - engine end-to-end via lp.Calculate against the embedded snapshot
  - holiday calendar (weekends, DE closures, UPC vacation block)
  - court registry (empty courtID fallback, known + unknown court)

Placeholder data shipped (2 proceedings, 2 rules, 5 holidays, 2
courts) so tests run without a live DB. Operator regenerates against
prod via `make snapshot-upc` once migrations 134 (B1) and 135 (B3)
have landed on prod — see cmd/gen-upc-snapshot/README.md for the
runbook. The placeholder's meta.version is suffixed `-placeholder`
to make the regeneration delta obvious.

Makefile target:
  make snapshot-upc — wraps the generator + reruns the snapshot tests

Design (§19 of docs/design-litigation-planner-2026-05-26.md):
  - Embedding format: go:embed JSON (diff-friendly, no compile coupling)
  - Generator entry: cmd/gen-upc-snapshot/main.go (idiomatic Go cmd path)
  - Versioning: meta.json carries semver + generated_at + paliad_commit
  - Regeneration: manual via Make target or `go generate`; no CI cron in v1
  - Out of scope: snapshot signing, DE/EPA/DPMA snapshots, snapshot
    diff tooling

Acceptance:
  - go build clean, go test all green (incl. 6 new tests in
    pkg/litigationplanner/embedded/upc, all DB-free)
  - SnapshotCatalog passes the compile-time lp.Catalog assertion
  - Generator binary builds + runs (Idempotence verified by re-running
    against the same source data)
2026-05-26 15:09:37 +02:00

216 lines
6.4 KiB
Go

package upc
import (
"context"
"testing"
"time"
lp "mgit.msbls.de/m/paliad/pkg/litigationplanner"
)
// TestSnapshotMeta loads + parses meta.json and asserts the version
// + non-zero counts. Until the operator regenerates the snapshot the
// placeholder shipped with Slice C must still parse cleanly.
func TestSnapshotMeta(t *testing.T) {
meta, err := LoadMeta()
if err != nil {
t.Fatalf("LoadMeta: %v", err)
}
if meta.Version == "" {
t.Error("meta.Version is empty")
}
if meta.ProceedingCount <= 0 {
t.Errorf("meta.ProceedingCount = %d, want > 0", meta.ProceedingCount)
}
if meta.RuleCount <= 0 {
t.Errorf("meta.RuleCount = %d, want > 0", meta.RuleCount)
}
}
// TestSnapshotCatalog smoke-tests the embedded catalog's lookups
// against the shipped placeholder. After operator regeneration the
// asserts on per-row content still hold because they pin the wire
// shape (proceedingType.Code, rule resolution by code, lookup-events
// jurisdiction filter).
func TestSnapshotCatalog(t *testing.T) {
cat, err := NewCatalog()
if err != nil {
t.Fatalf("NewCatalog: %v", err)
}
ctx := context.Background()
t.Run("LoadProceeding upc.inf.cfi", func(t *testing.T) {
pt, rules, err := cat.LoadProceeding(ctx, "upc.inf.cfi", lp.ProjectHint{})
if err != nil {
t.Fatalf("LoadProceeding: %v", err)
}
if pt.Code != "upc.inf.cfi" {
t.Errorf("pt.Code = %q, want upc.inf.cfi", pt.Code)
}
if pt.Jurisdiction == nil || *pt.Jurisdiction != "UPC" {
t.Errorf("pt.Jurisdiction = %v, want UPC", pt.Jurisdiction)
}
if len(rules) == 0 {
t.Error("LoadProceeding returned zero rules — snapshot empty?")
}
})
t.Run("LoadProceeding unknown code returns ErrUnknownProceedingType", func(t *testing.T) {
_, _, err := cat.LoadProceeding(ctx, "no.such.code", lp.ProjectHint{})
if err != lp.ErrUnknownProceedingType {
t.Errorf("got %v, want ErrUnknownProceedingType", err)
}
})
t.Run("LookupEvents UPC all-following returns the whole UPC corpus", func(t *testing.T) {
matches, err := cat.LookupEvents(ctx, lp.EventLookupAxes{
Jurisdiction: "UPC",
}, lp.EventLookupDepthAllFollowing)
if err != nil {
t.Fatalf("LookupEvents: %v", err)
}
if len(matches) == 0 {
t.Fatal("expected non-empty UPC corpus")
}
for _, m := range matches {
if m.ProceedingType.Jurisdiction == nil || *m.ProceedingType.Jurisdiction != "UPC" {
t.Errorf("non-UPC row leaked: %v", m.ProceedingType.Code)
}
if m.DepthFromAnchor < 1 {
t.Errorf("depth = %d, want >= 1", m.DepthFromAnchor)
}
}
})
t.Run("LookupEvents party=defendant scopes anchors", func(t *testing.T) {
matches, err := cat.LookupEvents(ctx, lp.EventLookupAxes{
Jurisdiction: "UPC",
Party: "defendant",
}, lp.EventLookupDepthNext)
if err != nil {
t.Fatalf("LookupEvents: %v", err)
}
// Anchor rows (depth=1) must all be defendant.
anyDefendant := false
for _, m := range matches {
if m.DepthFromAnchor != 1 {
continue
}
if m.Rule.PrimaryParty == nil || *m.Rule.PrimaryParty != "defendant" {
t.Errorf("anchor row %s is not defendant: %v", m.Rule.Name, m.Rule.PrimaryParty)
}
anyDefendant = true
}
if !anyDefendant {
t.Log("no defendant rules in the placeholder corpus — operator should regenerate the snapshot")
}
})
}
// TestSnapshotEngineCompute runs the litigationplanner engine against
// the embedded snapshot end-to-end. Ensures the wiring between the
// snapshot Catalog / HolidayCalendar / CourtRegistry + the engine
// produces a non-empty timeline.
func TestSnapshotEngineCompute(t *testing.T) {
cat, err := NewCatalog()
if err != nil {
t.Fatalf("NewCatalog: %v", err)
}
hc, err := NewHolidayCalendar()
if err != nil {
t.Fatalf("NewHolidayCalendar: %v", err)
}
cr, err := NewCourtRegistry()
if err != nil {
t.Fatalf("NewCourtRegistry: %v", err)
}
ctx := context.Background()
timeline, err := lp.Calculate(ctx, "upc.inf.cfi", "2026-01-15", lp.CalcOptions{}, cat, hc, cr)
if err != nil {
t.Fatalf("Calculate: %v", err)
}
if timeline == nil {
t.Fatal("Calculate returned nil timeline")
}
if timeline.ProceedingType != "upc.inf.cfi" {
t.Errorf("timeline.ProceedingType = %q, want upc.inf.cfi", timeline.ProceedingType)
}
if len(timeline.Deadlines) == 0 {
t.Error("timeline has zero deadlines — snapshot empty?")
}
}
// TestSnapshotHolidayCalendar smoke-tests the embedded calendar.
// Pins core semantics: weekends are non-working; holidays at
// matching country/regime are non-working; mismatches don't fire.
func TestSnapshotHolidayCalendar(t *testing.T) {
hc, err := NewHolidayCalendar()
if err != nil {
t.Fatalf("NewHolidayCalendar: %v", err)
}
// 2026-01-03 is a Saturday — weekend, non-working regardless of
// country/regime.
sat := time.Date(2026, 1, 3, 0, 0, 0, 0, time.UTC)
if !hc.IsNonWorkingDay(sat, "DE", "UPC") {
t.Error("Saturday should be non-working")
}
// 2026-01-01 is Neujahr (DE closure) — non-working when country=DE.
newYear := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
if !hc.IsNonWorkingDay(newYear, "DE", "UPC") {
t.Error("Neujahr should be non-working for DE")
}
// 2026-01-05 is a Monday — working (not in holidays, not weekend).
mon := time.Date(2026, 1, 5, 0, 0, 0, 0, time.UTC)
if hc.IsNonWorkingDay(mon, "DE", "UPC") {
t.Error("Monday 2026-01-05 should be working")
}
// AdjustForNonWorkingDays from a Saturday should land on Monday.
adj, _, was := hc.AdjustForNonWorkingDays(sat, "DE", "UPC")
if !was {
t.Error("expected adjustment for Saturday")
}
if adj.Weekday() != time.Monday {
t.Errorf("adjusted weekday = %v, want Monday", adj.Weekday())
}
}
// TestSnapshotCourtRegistry pins (country, regime) resolution.
func TestSnapshotCourtRegistry(t *testing.T) {
cr, err := NewCourtRegistry()
if err != nil {
t.Fatalf("NewCourtRegistry: %v", err)
}
t.Run("empty courtID falls back to defaults", func(t *testing.T) {
c, r, err := cr.CountryRegime("", "DE", "UPC")
if err != nil {
t.Fatalf("CountryRegime: %v", err)
}
if c != "DE" || r != "UPC" {
t.Errorf("got (%q, %q), want (DE, UPC)", c, r)
}
})
t.Run("known UPC court resolves", func(t *testing.T) {
c, r, err := cr.CountryRegime("upc-ld-munich", "DE", "")
if err != nil {
t.Fatalf("CountryRegime: %v", err)
}
if c != "DE" || r != "UPC" {
t.Errorf("got (%q, %q), want (DE, UPC)", c, r)
}
})
t.Run("unknown court returns error", func(t *testing.T) {
_, _, err := cr.CountryRegime("not-a-court", "DE", "UPC")
if err == nil {
t.Error("expected error for unknown court")
}
})
}