package ipc import ( "fmt" "go/ast" "go/parser" "go/token" "io/fs" "strings" "testing" "github.com/kami/maven/internal/store" ) // mapErr turns a store sentinel into its wire twin so a module can errors.Is // without importing internal/store. The design is right and the failure mode is // quiet: add a sentinel to store, forget the switch, and the client gets an // untyped error that no caller can branch on. These two tests are the alarm. // mapErrPairs — every store sentinel that has a wire twin, and the twin. var mapErrPairs = []struct { name string // the store identifier, for the coverage test below from error wants error }{ {"ErrNoFact", store.ErrNoFact, ErrNoFact}, {"ErrConfidence", store.ErrConfidence, ErrConfidence}, {"ErrVoidsMissing", store.ErrVoidsMissing, ErrVoidsMissing}, {"ErrNudgeNotFound", store.ErrNudgeNotFound, ErrNudgeNotFound}, {"ErrNudgeOutcome", store.ErrNudgeOutcome, ErrNudgeOutcome}, {"ErrReminderNotFound", store.ErrReminderNotFound, ErrReminderNotFound}, {"ErrReminderState", store.ErrReminderState, ErrReminderState}, {"ErrToolNotFound", store.ErrToolNotFound, ErrToolNotFound}, {"ErrNoSuchTrace", store.ErrNoSuchTrace, ErrNoSuchTrace}, {"ErrTaskNoDoneWhen", store.ErrTaskNoDoneWhen, ErrTaskNoDoneWhen}, {"ErrTaskDuplicate", store.ErrTaskDuplicate, ErrTaskDuplicate}, {"ErrTaskResolved", store.ErrTaskResolved, ErrTaskResolved}, } // unmappedStoreErrors — store sentinels that deliberately have no wire twin, // each with the reason it stays store-side. A new sentinel is in neither list // and fails TestMapErrCoversEveryStoreSentinel, which is the point: whether a // module can branch on an error is a decision, not a default. var unmappedStoreErrors = map[string]string{ "ErrKeyLen": "unlock path — the key never crosses CoreAPI", "ErrDecrypt": "unlock path — the key never crosses CoreAPI", "ErrToolCmd": "write-side validation of an allowlist mutation; the caller is the owner at a step-up, not a module branching on the verdict", "ErrProposedRoutineNotFound": "no module branches on a routine id that vanished; accept and dismiss are owner clicks", "ErrProposedRoutineExists": "the propose path already reports 'nothing new' through its bool return", "ErrRoutineStatus": "an unknown status is a caller bug, not a state a module recovers from", "ErrTaskNotFound": "the task surfaces re-list rather than branch", "ErrTaskEmpty": "input validation — the surface refuses empty text before it gets here", "ErrTaskStatus": "an illegal status move is a caller bug; the surface offers only legal ones", // The shopping and packing lists follow the task verdicts exactly, and for // the same reasons. No module branches on any of the three today. "ErrListItemNotFound": "the surface re-lists rather than branch on an item id that vanished", "ErrListItemEmpty": "input validation — an empty item is refused before it reaches the store", "ErrListItemStatus": "an unknown status is a caller bug, not a state a module recovers from", } // The mapping itself, through a wrap, because every real caller wraps. func TestMapErrMapsEveryPair(t *testing.T) { for _, p := range mapErrPairs { got := mapErr(fmt.Errorf("storeapi: %w", p.from)) if got != p.wants { t.Errorf("mapErr(store.%s) = %v, want %v", p.name, got, p.wants) } } } // Anything mapErr does not recognise passes through untouched. A module that // cannot branch on an error must still see the original text. func TestMapErrPassesUnknownThrough(t *testing.T) { if mapErr(nil) != nil { t.Error("mapErr(nil) must stay nil") } own := fmt.Errorf("socket closed") if got := mapErr(own); got != own { t.Errorf("mapErr(%v) = %v, want the same error back", own, got) } } // The parity half: every exported sentinel in internal/store is either mapped // or listed with a reason. Read off the source, so a sentinel added in a file // this package never touches still trips it. func TestMapErrCoversEveryStoreSentinel(t *testing.T) { mapped := map[string]bool{} for _, p := range mapErrPairs { mapped[p.name] = true } for _, name := range storeSentinelNames(t) { if mapped[name] || unmappedStoreErrors[name] != "" { continue } t.Errorf("store.%s is a new sentinel with no verdict: add it to mapErr and mapErrPairs, "+ "or to unmappedStoreErrors with the reason a module cannot branch on it", name) } } // storeSentinelNames reads internal/store for exported package-level error // values: `var ErrX = errors.New(...)`, inside a block or on its own. func storeSentinelNames(t *testing.T) []string { t.Helper() fset := token.NewFileSet() pkgs, err := parser.ParseDir(fset, "../store", func(fi fs.FileInfo) bool { return !strings.HasSuffix(fi.Name(), "_test.go") }, 0) if err != nil { t.Fatalf("parse internal/store: %v", err) } var out []string for _, pkg := range pkgs { for _, f := range pkg.Files { for _, d := range f.Decls { gd, ok := d.(*ast.GenDecl) if !ok || gd.Tok != token.VAR { continue } for _, spec := range gd.Specs { vs, ok := spec.(*ast.ValueSpec) if !ok { continue } for i, n := range vs.Names { if !strings.HasPrefix(n.Name, "Err") || !n.IsExported() { continue } if i < len(vs.Values) && isErrorsNew(vs.Values[i]) { out = append(out, n.Name) } } } } } } if len(out) < len(mapErrPairs) { t.Fatalf("found %d sentinels in internal/store, fewer than the %d already mapped — the scan is broken, not the store", len(out), len(mapErrPairs)) } return out } func isErrorsNew(e ast.Expr) bool { call, ok := e.(*ast.CallExpr) if !ok { return false } sel, ok := call.Fun.(*ast.SelectorExpr) if !ok || sel.Sel.Name != "New" { return false } id, ok := sel.X.(*ast.Ident) return ok && id.Name == "errors" }