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)
216 lines
6.4 KiB
Go
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")
|
|
}
|
|
})
|
|
}
|