Files
warmbly/internal/app/feature/gate_ai_test.go
T
Matthew Meszaros de38a10748 feat: AI is a paid feature, so take it off the free plan (#466)
* feat: take AI off the free plan, zeroing the free allowance and gating the writing assistant to paid like the inbox agent already was, because a trial that can only ever be told it has no credits reads as a broken feature rather than a locked one

* feat: state in the down migration which case does not round-trip, a free plan already at zero before 000148 ran, since the up migration recorded no prior value and a zero it did not write cannot be told from one it did
2026-09-12 09:09:00 -07:00

75 lines
2.3 KiB
Go

package feature
import (
"context"
"testing"
"time"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/repository"
)
// Only the one read the AI gates make is implemented; anything else panics,
// which is the point.
type stubSubs struct {
repository.SubscriptionRepository
sub *models.Subscription
}
func (s stubSubs) GetByOrganizationID(context.Context, uuid.UUID) (*models.Subscription, error) {
return s.sub, nil
}
func trialSub() *models.Subscription {
future := time.Now().Add(14 * 24 * time.Hour)
return &models.Subscription{
PlanID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
Status: models.SubscriptionStatusTrialing,
FreeTrialEndsAt: &future,
}
}
// The free plan carries no credit allowance, so the assistant must be locked
// rather than shown. Offering it to someone who can only ever be told they
// have no credits reads as a broken feature, not a paid one.
func TestWritingAssistantIsPaidOnly(t *testing.T) {
g := &featureGateService{subRepo: stubSubs{sub: trialSub()}}
ok, xerr := g.CanUseWritingAssistant(context.Background(), uuid.New())
if xerr != nil {
t.Fatalf("unexpected error: %v", xerr)
}
if ok {
t.Error("a free trial must not be offered the writing assistant")
}
}
func TestWritingAssistantAndInboxAgentAgree(t *testing.T) {
ctx := context.Background()
org := uuid.New()
g := &featureGateService{subRepo: stubSubs{sub: trialSub()}}
wa, _ := g.CanUseWritingAssistant(ctx, org)
ia, _ := g.CanUseInboxAgent(ctx, org)
if wa != ia {
t.Errorf("both AI features should gate the same way on a free trial: assistant=%v agent=%v", wa, ia)
}
}
// No subscription at all is still a refusal, not a panic.
func TestAIGatesRefuseWithNoSubscription(t *testing.T) {
g := &featureGateService{subRepo: stubSubs{sub: nil}}
if ok, _ := g.CanUseWritingAssistant(context.Background(), uuid.New()); ok {
t.Error("no subscription must not unlock the assistant")
}
}
// A self-hosted instance has no payment provider, so every gate stays open.
func TestSelfHostKeepsAIOpen(t *testing.T) {
g := &featureGateService{selfHost: true}
if ok, _ := g.CanUseWritingAssistant(context.Background(), uuid.New()); !ok {
t.Error("self-host must keep the assistant available")
}
}