Files
Maven/cmd/semantic-router-experiment/slice21/guard_test.go
T
claude fa98e4722e router/semantic: slice 21 deterministic execution-frame guard engine, fixtures, runner and emit step
The guard answers one question — may this utterance become an executable
action — as a three-way policy gate (permissive / blocked / ambiguous) and
never decides what the utterance is. Rules are the encoding of the measured
slice-20 dev-pool discriminators: 126 capability-question rows are 42/42/42
addressed / bare-ability / bare-future; 127 bare можешь+пожалуйста rows are
100% action; can-you-please is 100% action. Reuses the shipped prohibition
parser, morph finiteness and lexicon fillers; reason vocabulary is closed.

Slice 21 (task/725, brief after the accepted slice 20).
2026-09-07 23:43:47 +04:00

140 lines
3.3 KiB
Go

package main
import (
"encoding/json"
"os"
"strings"
"testing"
"github.com/kami/maven/internal/morph"
)
func TestEvaluateFixtures(t *testing.T) {
for _, fx := range Fixtures {
got := Evaluate(fx.Utterance)
if got.Eligibility != fx.Want {
t.Errorf("%s: want %s got %s (reasons %v)", fx.Utterance, fx.Want, got.Eligibility, got.Reasons)
}
}
}
// TestMeasuredDiscriminators pins the corpus-verified numbers on the frozen
// pool. These are the exact measurements the rules were built on, so a change
// that moves them is a rule regression visible in the slice.
func loadPool(t *testing.T) []Row {
t.Helper()
b, err := os.ReadFile("/tmp/mvn-s21/pool.json")
if os.IsNotExist(err) {
t.Skip("pool.json missing; run slice21_emit.py first")
}
if err != nil {
t.Fatal(err)
}
var rows []Row
if err := json.Unmarshal(b, &rows); err != nil {
t.Fatal(err)
}
return rows
}
func TestDevCapabilityProhibition(t *testing.T) {
if !morph.Available() {
t.Skip("morph dict unavailable")
}
rows := loadPool(t)
var n, pass, blockedN, ambig int
for _, r := range rows {
if !hasTok(r.Tags, "capability_question") {
continue
}
n++
switch Evaluate(variants(r.NText)[vOrig]).Eligibility {
case Permissive:
pass++
case Blocked:
blockedN++
case Ambiguous:
ambig++
}
}
if n != 126 {
t.Fatalf("capability-question rows = %d, want 126", n)
}
if pass != 0 {
t.Fatalf("capability-question dangerous pass = %d, want 0", pass)
}
if blockedN != 126 && ambig != 0 {
t.Errorf("blocked=%d ambig=%d, expect all 126 blocked", blockedN, ambig)
}
}
func TestDevBareCanPoliteIsAction(t *testing.T) {
if !morph.Available() {
t.Skip("morph dict unavailable")
}
rows := loadPool(t)
total, action, perm := 0, 0, 0
for _, r := range rows {
toks := tokens(r.NText)
if !hasTok(toks, "можешь") || hasAny(toks, ruAddress) ||
hasAny(toks, append([]string{}, ruAbilityForms.single...)) {
continue
}
total++
if r.Route == "action" {
action++
}
if Evaluate(r.NText).Eligibility == Permissive {
perm++
}
}
if total != 127 || action != 127 {
t.Fatalf("bare-можешь+polite: n=%d action=%d, want 127/127", total, action)
}
if perm != 127 {
t.Fatalf("bare-можешь+polite permissive=%d, want 127", perm)
}
}
func TestDevEnglishCanPoliteIsAction(t *testing.T) {
if !morph.Available() {
t.Skip("morph dict unavailable")
}
rows := loadPool(t)
total, action, perm := 0, 0, 0
for _, r := range rows {
toks := tokens(r.NText)
if !hasAny(toks, enCanForms) {
continue
}
total++
if r.Route == "action" {
action++
}
if Evaluate(r.NText).Eligibility == Permissive {
perm++
}
}
if total != 96 || action != 96 {
t.Fatalf("can-rows: n=%d action=%d, want 96/96", total, action)
}
if perm != 96 {
t.Fatalf("can-rows permissive=%d, want 96", perm)
}
}
func TestNoPermissiveRowEndsInQuestion(t *testing.T) {
// asymmetric posture: the guard must never approve a trailing-? frame --
// the corpus has 0 action rows ending in "?", and approving any would
// bet on punctuation the slice has ruled uncertain.
rows := loadPool(t)
for _, r := range rows {
if !strings.HasSuffix(r.NText, "?") {
continue
}
if got := Evaluate(r.NText).Eligibility; got == Permissive {
t.Errorf("canonical '?': %q approved (%s)", r.Text, got)
}
}
}