textmachine/backend/internal/pipeline/refusal_test.go

58 lines
2.2 KiB
Go

package pipeline
import (
"errors"
"fmt"
"testing"
"textmachine/backend/internal/store"
)
// refusal_test.go pins RefuseStoreOpen, the one place a store-open failure becomes a shell class. Two
// callers share it (openRunner and `tmctl migrate`), and its arms decide what an automated caller DOES:
// wait, migrate, or treat the project as broken.
func TestRefuseStoreOpenClassifiesWhatACallerActsOn(t *testing.T) {
locked := fmt.Errorf("opening the project: %w", store.ErrLocked)
mismatch := fmt.Errorf("opening the project: %w", &store.SchemaMismatchError{Path: "/b.db", Found: 14, Expected: 15})
for _, tc := range []struct {
name string
err error
want RefusalClass
}{
{"another process holds it", locked, RefusalProjectLocked},
{"the schema is not this binary's", mismatch, RefusalSchemaMismatch},
} {
t.Run(tc.name, func(t *testing.T) {
var refusal *Refusal
if !errors.As(RefuseStoreOpen(tc.err), &refusal) {
t.Fatalf("%v was not classified as a refusal", tc.err)
}
if refusal.Class != tc.want {
t.Fatalf("class %q, want %q", refusal.Class, tc.want)
}
// Both arms must survive a %w-wrap, because that is how they arrive: neither caller opens the
// store at the top of its own stack.
if !errors.Is(refusal, store.ErrLocked) && !errors.As(refusal, new(*store.SchemaMismatchError)) {
t.Fatal("the classification dropped the cause it was made from")
}
})
}
// The safety rule, and the reason the default arm is a passthrough: the refusal band means "turned
// down before doing ANY work", and the platform's intake acts on it — it waits instead of failing,
// and one number in it deletes the user's upload. A disk error during an open is not that; it must
// stay exit 1 (infra failure) rather than borrow a class that promises nothing happened.
disk := errors.New("store: open write pool: input/output error")
got := RefuseStoreOpen(disk)
if !errors.Is(got, disk) {
t.Fatalf("an unrecognised open failure must come back with its cause intact, got %v", got)
}
if errors.As(got, new(*Refusal)) {
t.Fatal("an unrecognised open failure was swept into the refusal band")
}
if RefuseStoreOpen(nil) != nil {
t.Fatal("a nil failure must stay nil: the classifier never invents one")
}
}