mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
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.
66 lines
4.0 KiB
TypeScript
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([])
|
|
})
|
|
})
|