Files
Maven/internal/router/semantic/contract_test.go
T
claude fb9b719f0b router/semantic: corpus validation infrastructure (slice 13)
Add ValidateCorpus and CorpusStatsFrom for structural integrity checks
on the semantic route corpus. Validates total counts, route/source sums,
fast-path+residual partition, empty SourceID/SplitGroup, invalid routes,
duplicate identity, and conflicting labels on identical text.

CorpusStats provides deterministic dataset hash (SHA-256 of sorted texts,
first 16 bytes). ReproducibilityMeta in CorpusEnvelope records source
fixture hashes, generator version, split algorithm, and dataset hash.

TestCorpusValidation exercises the full validation pipeline.
2026-09-07 02:06:58 +04:00

181 lines
4.6 KiB
Go

package semantic
import (
"testing"
"github.com/kami/maven/internal/router"
)
func TestValidRoute(t *testing.T) {
for _, r := range AllRoutes {
if !ValidRoute(r) {
t.Errorf("ValidRoute(%q) = false, want true", r)
}
}
if ValidRoute("bogus") {
t.Error("ValidRoute(\"bogus\") = true, want false")
}
}
func TestIntentToRouteMapping(t *testing.T) {
cases := []struct {
intent router.Intent
route SemanticRoute
}{
{router.IntentChat, RouteConversation},
{router.IntentQuery, RouteKnowledge},
{router.IntentAct, RouteAction},
{router.IntentReminder, RouteAction},
{router.IntentFact, RouteMemoryWrite},
{router.IntentNote, RouteMemoryWrite},
{router.IntentSystem, RouteSystem},
{router.Intent("unknown"), RouteUncertain},
{router.Intent(""), RouteUncertain},
}
for _, c := range cases {
got := IntentToRoute(c.intent)
if got != c.route {
t.Errorf("IntentToRoute(%q) = %q, want %q", c.intent, got, c.route)
}
}
}
func TestLoadCorpus(t *testing.T) {
exs, err := LoadCorpus()
if err != nil {
t.Fatal(err)
}
if len(exs) == 0 {
t.Fatal("corpus is empty")
}
for _, e := range exs {
if !ValidRoute(e.Route) {
t.Errorf("example %q has invalid route %q", e.SourceID, e.Route)
}
if e.Source == "" {
t.Errorf("example %q has empty source", e.SourceID)
}
if e.SplitGroup == "" {
t.Errorf("example %q has empty split_group", e.SourceID)
}
}
fastPath, residual := SplitCounts(exs)
if fastPath == 0 {
t.Error("no fast_path_resolved examples in corpus")
}
if residual == 0 {
t.Error("no residual examples in corpus")
}
t.Logf("corpus: %d examples (%d fast-path, %d residual)", len(exs), fastPath, residual)
}
func TestCorpusValidation(t *testing.T) {
exs, err := LoadCorpus()
if err != nil {
t.Fatal(err)
}
stats := CorpusStatsFrom(exs)
// Total matches.
if stats.Total != len(exs) {
t.Errorf("stats.Total=%d, want %d", stats.Total, len(exs))
}
// Route counts sum to total.
routeSum := 0
for _, c := range stats.RouteCounts {
routeSum += c
}
if routeSum != stats.Total {
t.Errorf("route counts sum to %d, want %d", routeSum, stats.Total)
}
// Source counts sum to total.
sourceSum := 0
for _, c := range stats.SourceCounts {
sourceSum += c
}
if sourceSum != stats.Total {
t.Errorf("source counts sum to %d, want %d", sourceSum, stats.Total)
}
// Fast-path + residual == total.
if stats.FastPath+stats.Residual != stats.Total {
t.Errorf("fast_path(%d) + residual(%d) = %d, want %d",
stats.FastPath, stats.Residual, stats.FastPath+stats.Residual, stats.Total)
}
// All SourceIDs are valid (non-empty, checked by ValidateCorpus).
// All SplitGroups are non-empty (checked by ValidateCorpus).
// No duplicate corpus identity (checked by ValidateCorpus).
// No identical Text with conflicting labels (checked by ValidateCorpus).
t.Logf("validation passed: %d rows, route_hash=%s", stats.Total, stats.DatasetHash)
// Verify the exact counts match what we expect.
if stats.Total != 136 {
t.Errorf("total = %d, want 136", stats.Total)
}
if stats.SourceCounts["ru_routing_v1"] != 96 {
t.Errorf("ru_routing_v1 count = %d, want 96", stats.SourceCounts["ru_routing_v1"])
}
if stats.SourceCounts["contrastive"] != 40 {
t.Errorf("contrastive count = %d, want 40", stats.SourceCounts["contrastive"])
}
}
func TestCorpusByRoute(t *testing.T) {
exs, err := LoadCorpus()
if err != nil {
t.Fatal(err)
}
byRoute := ByRoute(exs)
for _, route := range AllRoutes {
count := len(byRoute[route])
if count == 0 {
t.Errorf("no examples for route %q", route)
}
t.Logf(" %s: %d examples", route, count)
}
}
func TestSplitByFamily(t *testing.T) {
exs, err := LoadCorpus()
if err != nil {
t.Fatal(err)
}
train, eval := SplitByFamily(exs, 0.8)
total := len(train) + len(eval)
if total != len(exs) {
t.Errorf("split lost examples: train=%d eval=%d total=%d, want %d",
len(train), len(eval), total, len(exs))
}
trainGroups := map[string]bool{}
for _, e := range train {
trainGroups[e.SplitGroup] = true
}
for _, e := range eval {
if trainGroups[e.SplitGroup] {
t.Errorf("split_group %q leaked across train/eval (example %q)",
e.SplitGroup, e.SourceID)
}
}
t.Logf("split: %d train, %d eval", len(train), len(eval))
}
func TestFamilyID(t *testing.T) {
id1 := FamilyID("ru-act-002", "negation")
id2 := FamilyID("ru-act-002", "question")
id3 := FamilyID("ru-act-002", "")
id4 := FamilyID("ru-act-001", "negation")
if id1 == id2 {
t.Error("different transforms produced same FamilyID")
}
if id1 == id3 {
t.Error("transform and base produced same FamilyID")
}
if id1 == id4 {
t.Error("different base IDs produced same FamilyID")
}
}