Files
hanzo.ai/e2e/unit/documents.spec.ts
zeekay 76b47a4b81 The plan cards list the catalog's lines as served
The cards no longer rewrite a plan's lines in the browser: Free's Hanzo
Dev, MCP and CLI lines were dropped and appended to Pro's, and every Max
was given "Hanzo Bot included". @hanzo/plans 1.8.22 says it itself, in
its own order, with each grant a line claims held to the entitlement
behind it, so what a card lists is what GET /v1/billing/plans serves.

The pricing share card draws the same: Free's third line is web search,
Pro's first is Hanzo Dev. Redrawn, so every card takes v=4.
2026-10-20 08:16:34 -07:00

66 lines
4.0 KiB
TypeScript

import { test, expect } from '@playwright/test'
import { plan, toPlans, toPricing, toServed, toSummary } from '@/lib/api/read'
import { kaiPrice } from '@/lib/tally'
import { MODELS, PLANS, PRICING, SUMMARY } from '../gates/api'
/**
* lib/api is the one reader of what the backend prices: these hold what it
* accepts, what it refuses, and the one conversion it makes (cents to dollars).
* A body that is not the document asked for is a failure, never an empty page.
*/
test.describe('lib/api', () => {
test('a plan row is drawn in dollars, with its features always a list', () => {
const team = plan({ slug: 'team', name: 'Team', description: '', category: 'business', price: 2300, priceAnnual: 1900, annualTotal: 22800 })
expect([team.id, team.priceMonthly, team.priceAnnual, team.annualTotal, team.features]).toEqual(['team', 23, 19, 228, []])
expect(plan({ slug: 'x', name: 'X', description: '', category: 'personal', price: null }).priceMonthly).toBeNull()
})
// What a card lists is the catalog's: the lines as served, in their order, on
// every plan. Which plan names Hanzo Dev or Hanzo Bot is @hanzo/plans', where
// each claim is held to the entitlement behind it, so a card neither adds nor
// drops one.
test("a card lists the catalog's lines as served, adding and dropping none", () => {
const row = (slug: string, features?: string[]) => plan({ slug, name: slug, description: '', category: 'personal', price: 0, features })
const pro = ['Everything in Free, plus:', 'Build apps with Hanzo Dev', 'Connect any MCP server in Hanzo Dev', 'The Hanzo CLI for macOS, Windows and Linux']
expect(row('dev', pro).features).toEqual(pro)
expect(row('free', ['Chat', 'Build apps with Hanzo Dev']).features, 'the catalog decides what Free lists').toEqual(['Chat', 'Build apps with Hanzo Dev'])
expect(row('max-20x', ['Everything in Pro, plus:', 'Hanzo Bot: 3 bots of your own']).features).toEqual(['Everything in Pro, plus:', 'Hanzo Bot: 3 bots of your own'])
expect(row('max-5x').features, 'a row with no lines draws none').toEqual([])
})
test('the plan catalog is a bare array or one wrapped in plans, and nothing else', () => {
expect(toPlans(PLANS)!.map((p) => p.id)).toEqual(PLANS.map((r) => r.slug))
expect(toPlans({ plans: PLANS })).toHaveLength(PLANS.length)
expect(toPlans({ data: PLANS }), 'a wrapper it does not know is not a catalog').toBeNull()
expect(toPlans([{ name: 'no slug' }])).toBeNull()
expect(toPlans(null)).toBeNull()
})
test('the price list needs its rate card and its models', () => {
expect(toPricing(PRICING)!.tariff.rates).toHaveLength(PRICING.tariff.rates.length)
expect(toPricing({ ...PRICING, tariff: { ...PRICING.tariff, rates: [] } }), 'a card with no rates').toBeNull()
expect(toPricing({ ...PRICING, hanzoModels: undefined })).toBeNull()
expect(toPricing({ ...PRICING, tools: undefined })!.tools, 'no tool list is an empty one').toEqual([])
})
test('the routing table and the summary are read as served, or refused', () => {
expect(toServed(MODELS)).toHaveLength(MODELS.data.length)
expect(toServed({ data: [] }), 'an empty table is not an answer').toBeNull()
expect(toSummary(SUMMARY)!.totalModels).toBe(SUMMARY.totalModels)
expect(toSummary(SUMMARY)!.providers, 'a map of labs is counted by its labs').toBe(Object.keys(SUMMARY.providers).length)
expect(toSummary({ totalModels: 7, providers: 3 })!.providers, "/v1/pricing's own summary states a number").toBe(3)
expect(toSummary({ totalModels: '777' })).toBeNull()
expect(toSummary({ totalModels: 7 }), 'no providers is not the summary').toBeNull()
})
test("Kai's price beside Jev's is what the price list states, and nothing when it states nothing", () => {
const doc = toPricing(PRICING)!
const [less, free] = kaiPrice(doc)
expect(less.figure).toBe('75% less')
expect(less.note).toContain('$0.013 against $0.052')
expect(free.figure).toBe('$0')
expect(kaiPrice({ ...doc, hanzoModels: doc.hanzoModels.filter((m) => m.name !== 'kai') })).toEqual([])
})
})