mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
The root layout handed every page the plan catalog (lib/rooms.ts configure with lib/data/plans.json), and /pricing, /calculator, /customers and the benchmark pages bundled the 202 KB price list. lib/api is now the one reader: plans() GET /v1/billing/plans, pricing() /v1/pricing, summary(), served() /v1/models, templates(), blueprints() /v1/blueprint/sbom, one promise a path a page load, each drawn as loading, ready or "api.hanzo.ai is unreachable" (components/api) and never an older number. plans.json, pricing.json, models.json, templates.json, blueprints.json and sync-pricing, sync-models, sync-templates, sync-blueprints are gone. What a static export must know before anyone asks, which model pages exist and their titles, is read by the build itself (lib/api/build, Next's fetch cache keyed by the build's stamp: one answer per build) and renders no rate. lib/moved reads the retired model pages against the export, and the 153 pages the old snapshot kept for models the gateway dropped are in retired.json. Kai's price beside Jev's, Enso's tier rates, a model page's rate and the GPU table read the price list in the browser; the site's plan math no longer imports @hanzo/rooms/lib/plans, whose bundled @hanzo/plans catalog rode the home, /pricing and the picker. Home first load 452.7 -> 437.4 KB gzip (budget 440); pages over perf.budget.json 32 -> 9, the rest over for settings code, connectors and the commerce catalog. The gates serve invented answers (e2e/gates/api) and block them; e2e/gates/read.spec.ts holds that no script carries a plan, price or model table.
99 lines
4.1 KiB
TypeScript
99 lines
4.1 KiB
TypeScript
import { expect, test } from '@playwright/test'
|
||
import { readFileSync, readdirSync } from 'node:fs'
|
||
import { join } from 'node:path'
|
||
import { OUT, read, requireExport, serveExport } from './export'
|
||
import { PLANS, SUMMARY, block, money, serve } from './api'
|
||
|
||
/**
|
||
* NO PAGE CARRIES A PLAN, A PRICE OR A MODEL TABLE IT DID NOT READ.
|
||
*
|
||
* The root layout once handed every page the plan catalog, and /pricing,
|
||
* /calculator and the benchmark pages carried the whole price list in their
|
||
* scripts — copies a build took, which went months stale. Now a page reads
|
||
* them from api.hanzo.ai when it opens (lib/api) and the export holds none:
|
||
* these gates read the bytes that ship for any of the three documents, then
|
||
* drive /calculator and the plan picker over served and blocked backends.
|
||
*/
|
||
|
||
/** Every script the export ships. */
|
||
function scripts(): string[] {
|
||
requireExport()
|
||
const out: string[] = []
|
||
const walk = (dir: string) => {
|
||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||
const path = join(dir, e.name)
|
||
if (e.isDirectory()) walk(path)
|
||
else if (e.name.endsWith('.js')) out.push(path)
|
||
}
|
||
}
|
||
walk(join(OUT, '_next', 'static', 'chunks'))
|
||
return out
|
||
}
|
||
|
||
/** What a bundled copy of each document looks like: its rows, as an inlined JSON literal carries them. */
|
||
const COPIES: [string, RegExp][] = [
|
||
['the plan catalog', /\\?"slug\\?":\\?"dev\\?",\\?"name\\?":\\?"Pro\\?"/],
|
||
['the price list', /\\?"(hanzoModels|thirdPartyModels)\\?":\[\{/],
|
||
['the model table', /\\?"absent\\?":\[\\?"/],
|
||
['a stamped snapshot', /\\?"source\\?":\\?"https:\/\/api\.hanzo\.ai\/v1\/(billing\/plans|pricing|models)\\?"/],
|
||
]
|
||
|
||
test('no script in the export carries the plan catalog, the price list or the model table', () => {
|
||
const found: string[] = []
|
||
for (const file of scripts()) {
|
||
const text = readFileSync(file, 'utf8')
|
||
for (const [what, copy] of COPIES) if (copy.test(text)) found.push(`${what} in ${file.slice(OUT.length)}`)
|
||
}
|
||
expect(found).toEqual([])
|
||
})
|
||
|
||
test('/pricing and /calculator ship the loading line, and no price or count', () => {
|
||
const pricing = read('pricing.html')
|
||
expect(pricing).toContain('data-api="loading"')
|
||
expect(pricing, 'a plan price in the HTML').not.toContain('USD/month')
|
||
const calculator = read('calculator.html')
|
||
expect(calculator, 'a model count in the HTML').not.toMatch(/\d+\+ models/)
|
||
expect(calculator).toContain('every model on the gateway')
|
||
})
|
||
|
||
test.describe('read when the page opens', () => {
|
||
let base: string
|
||
let stop: () => Promise<void>
|
||
test.beforeAll(async () => {
|
||
const served = await serveExport()
|
||
base = served.url
|
||
stop = served.close
|
||
})
|
||
test.afterAll(async () => stop())
|
||
|
||
test('/calculator states the count the price list answers', async ({ page }) => {
|
||
const asked = await serve(page)
|
||
await page.goto(`${base}/calculator`, { waitUntil: 'load' })
|
||
const phrase = `${Math.floor(SUMMARY.totalModels / 100) * 100}+ models`
|
||
await expect(page.getByText(phrase).first()).toBeVisible({ timeout: 20_000 })
|
||
expect(asked).toContain('/v1/pricing/summary')
|
||
})
|
||
|
||
test('/calculator with api.hanzo.ai down states no count at all', async ({ page }) => {
|
||
await block(page)
|
||
await page.goto(`${base}/calculator`, { waitUntil: 'load' })
|
||
await expect(page.getByText('every model on the gateway').first()).toBeVisible()
|
||
await page.waitForTimeout(1500)
|
||
await expect(page.getByText(/\d+\+ models/)).toHaveCount(0)
|
||
})
|
||
|
||
test('the plan cards on the landing are the authority’s, and say so when it is down', async ({ page }) => {
|
||
await serve(page)
|
||
await page.goto(`${base}/pricing`, { waitUntil: 'load' })
|
||
const pro = PLANS.find((p) => p.slug === 'dev')!
|
||
await expect(page.getByRole('region', { name: pro.name, exact: true })).toContainText(money(pro.price), { timeout: 20_000 })
|
||
|
||
const down = await page.context().newPage()
|
||
await block(down)
|
||
await down.goto(`${base}/pricing`, { waitUntil: 'load' })
|
||
await expect(down.locator('[data-api="unreachable"]').first()).toBeVisible({ timeout: 20_000 })
|
||
await expect(down.getByRole('region', { name: pro.name, exact: true })).toHaveCount(0)
|
||
await down.close()
|
||
})
|
||
})
|