feat: design system overhaul — sidebar, AI chats, settings, brainstorm, color cleanup
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
- Sidebar: dynamic brand-accent colors, brainstorm section restyled - AI chat general: popup panel with expand/collapse, hides when contextual AI open - AI chat contextual: tabs reordered (Actions first), X close button, height fix - Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.) - Global color cleanup: emerald/orange hardcoded → brand-accent dynamic - Brainstorm page: orange → brand-accent throughout - PageEntry animation component added to key pages - Floating AI button: bg-brand-accent instead of hardcoded black - i18n: all 15 locales updated with new AI/billing keys - Billing: freemium quota tracking, BYOK, stripe subscription scaffolding - Admin: integrated into new design - AGENTS.md + CLAUDE.md project rules added
This commit is contained in:
140
memento-note/tests/unit/brainstorm-billing.test.ts
Normal file
140
memento-note/tests/unit/brainstorm-billing.test.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import {
|
||||
billingOwnerFromSession,
|
||||
getBillingOwner,
|
||||
} from '@/lib/brainstorm-collab'
|
||||
import {
|
||||
checkSessionEntitlementOrThrow,
|
||||
incrementUsageAsync,
|
||||
QuotaExceededError,
|
||||
} from '@/lib/entitlements'
|
||||
|
||||
const { prismaMock } = vi.hoisted(() => ({
|
||||
prismaMock: {
|
||||
subscription: { findUnique: vi.fn() },
|
||||
brainstormSession: { findUnique: vi.fn() },
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/prisma', () => ({
|
||||
prisma: prismaMock,
|
||||
default: prismaMock,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/redis', () => ({
|
||||
redis: {
|
||||
get: vi.fn(),
|
||||
mget: vi.fn(),
|
||||
eval: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/byok', () => ({
|
||||
hasAnyActiveByok: vi.fn().mockResolvedValue(false),
|
||||
}))
|
||||
|
||||
import { redis } from '@/lib/redis'
|
||||
import { hasAnyActiveByok } from '@/lib/byok'
|
||||
|
||||
function mockActiveSubscription(tier: string) {
|
||||
prismaMock.subscription.findUnique.mockResolvedValue({
|
||||
userId: 'host-1',
|
||||
tier: tier as any,
|
||||
status: 'ACTIVE' as any,
|
||||
currentPeriodStart: new Date(),
|
||||
currentPeriodEnd: new Date(),
|
||||
} as any)
|
||||
}
|
||||
|
||||
describe('brainstorm host-pays billing (Story 3.4)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('billingOwnerFromSession', () => {
|
||||
it('returns host id for host actor', () => {
|
||||
expect(billingOwnerFromSession('host-1', 'host-1')).toEqual({
|
||||
billingOwnerId: 'host-1',
|
||||
isGuestActor: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('returns host id for guest actor', () => {
|
||||
expect(billingOwnerFromSession('host-1', 'guest-9')).toEqual({
|
||||
billingOwnerId: 'host-1',
|
||||
isGuestActor: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getBillingOwner', () => {
|
||||
it('loads session and resolves billing owner', async () => {
|
||||
prismaMock.brainstormSession.findUnique.mockResolvedValue({ userId: 'host-1' })
|
||||
|
||||
const result = await getBillingOwner('session-abc', 'guest-9')
|
||||
expect(result).toEqual({ billingOwnerId: 'host-1', isGuestActor: true })
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkSessionEntitlementOrThrow', () => {
|
||||
it('throws QuotaExceededError with guest metadata when host quota exhausted', async () => {
|
||||
mockActiveSubscription('BASIC')
|
||||
vi.mocked(redis.eval).mockResolvedValue(-1)
|
||||
|
||||
await expect(
|
||||
checkSessionEntitlementOrThrow('host-1', 'guest-9', true, 'brainstorm_expand'),
|
||||
).rejects.toMatchObject({
|
||||
billingOwnerId: 'host-1',
|
||||
triggeredByUserId: 'guest-9',
|
||||
isGuestActor: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('increments host redis key when guest action bills host', async () => {
|
||||
mockActiveSubscription('PRO')
|
||||
vi.mocked(redis.eval).mockResolvedValue(1)
|
||||
|
||||
await checkSessionEntitlementOrThrow('host-1', 'guest-9', true, 'brainstorm_expand')
|
||||
|
||||
expect(redis.eval).toHaveBeenCalled()
|
||||
const keyArg = String(vi.mocked(redis.eval).mock.calls[0]?.[2])
|
||||
expect(keyArg).toContain('host-1')
|
||||
expect(keyArg).not.toContain('guest-9')
|
||||
|
||||
const allKeyArgs = vi.mocked(redis.eval).mock.calls.map(call => String(call[2]))
|
||||
const guestKeyUsed = allKeyArgs.some(k => k.includes('guest-9'))
|
||||
expect(guestKeyUsed).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('host BYOK bypass (Story 3.5)', () => {
|
||||
it('reserveUsage still runs when host has BYOK (bypass is per-provider in route layer)', async () => {
|
||||
vi.mocked(hasAnyActiveByok).mockResolvedValue(true)
|
||||
mockActiveSubscription('BASIC')
|
||||
vi.mocked(redis.eval).mockResolvedValue(-1)
|
||||
|
||||
await expect(
|
||||
checkSessionEntitlementOrThrow('host-1', 'guest-9', true, 'brainstorm_expand'),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('QuotaExceededError.toJSON', () => {
|
||||
it('includes isGuestActor in toJSON for 402 responses', () => {
|
||||
const err = new QuotaExceededError('PRO', 'brainstorm_expand', 10, 10, false, {
|
||||
billingOwnerId: 'host-1',
|
||||
triggeredByUserId: 'guest-9',
|
||||
isGuestActor: true,
|
||||
})
|
||||
expect(err.toJSON()).toEqual({
|
||||
error: 'QUOTA_EXCEEDED',
|
||||
feature: 'brainstorm_expand',
|
||||
upgradeTier: 'PRO',
|
||||
byokConfigured: false,
|
||||
isGuestActor: true,
|
||||
})
|
||||
expect(err.billingOwnerId).toBe('host-1')
|
||||
expect(err.triggeredByUserId).toBe('guest-9')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user