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") } }