mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
On a phone the map was hidden for two columns of buttons, and the layer's detail sat in an inspector beside or under the drawing. Now every width draws the plates: below 768px the drawing is cropped to the plates and scaled to the column, so at 390 all twelve stand inside the screen, each name 17px and each target 44px tall. A plate opens its own card: number, name, one line, Page, Docs and Source. From 768px the card stands in a lane level with the plate, shown while a mouse is on it and pinned by a press; on a phone it stands under the plate and the plates below part to make room. A tap opens, the same tap closes, another tap moves it; nothing is reached by hovering alone. /platform keeps the inspector (`Explorer deep`). The scroll-driven close and part run on the map's ViewTimeline instead of a scroll listener writing currentTime, so no script runs per frame. The map's window clips with overflow: clip, since hidden would make it the timeline's scroll container. Parting is one CSS variable, a card's words are drawn once, and only plates whose lift changes are animated, so a tap redraws what it lights and nothing else. Each layer gains a one-line `line` for its card.
576 lines
36 KiB
TypeScript
576 lines
36 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test'
|
||
import { read, serveExport } from './export'
|
||
import { MODELS, PRICING, block, serve, settled } from './api'
|
||
import { api, listing } from './revision'
|
||
import { FIELD } from './field'
|
||
import snapshot from '../../lib/data/benchmarks-kai.json' with { type: 'json' }
|
||
import { toRuns } from '../../lib/api/research'
|
||
import { BENCH } from '../../lib/coding'
|
||
import { clock, ms, race } from '../../lib/home'
|
||
import type { Coding } from '../../lib/coding'
|
||
import type { Speed } from '../../lib/figures'
|
||
import ensoFigure from '../../lib/data/figures/enso.json' with { type: 'json' }
|
||
import codingFigure from '../../lib/data/figures/coding.json' with { type: 'json' }
|
||
import speedFigure from '../../lib/data/figures/speed.json' with { type: 'json' }
|
||
import { derive as kaiOf, table, TIMED, type Run } from '../../lib/kai'
|
||
import { MOVED, NOINDEX } from '../../lib/publish'
|
||
import { JEV } from '../../lib/tally'
|
||
|
||
/** The coding board's runs, on whichever origin the page asks them of (lib/coding `runs`). */
|
||
const CODING = (u: URL): boolean => u.pathname === '/v1/research/runs' && u.searchParams.get('benchmark') === BENCH
|
||
|
||
/**
|
||
* THE LANDING, `/`: every figure on it is the one the page was served, it fits
|
||
* every screen, it says nothing in the console, it names no rival decision
|
||
* model and links no run record, it is offered to the index, and /home2, the
|
||
* address it was read at before it replaced `/`, forwards to it.
|
||
*
|
||
* The answers are invented (e2e/gates/api.ts, e2e/gates/field.ts and the
|
||
* prices, LLM rates, call timings and coding runs below), numbers no source
|
||
* holds, and the figures the page must print from them are written here BY
|
||
* HAND, so a wrong derivation in lib/home fails rather than agreeing with
|
||
* itself. The Decision Suite's count and /kai's decision timings are lib/kai's
|
||
* (its own gates hold that derivation).
|
||
* The reduced-motion reader is the one measured, so every mark stands finished.
|
||
*/
|
||
|
||
/** Every page the stack's layers name, by hand (lib/stack): Sandboxes and Cloud share /platform, Hanzo ML and the kernel DSL /open-source. */
|
||
const STACK = ['/gateway', '/api', '/enso', '/zen', '/kai', '/agents', '/platform', '/node', '/engine', '/open-source']
|
||
|
||
/** The price list: the fixtures, Enso Pro beside them, and the single models of field.ts's GPQA board at invented rates. */
|
||
const LIST = {
|
||
...PRICING,
|
||
hanzoModels: [
|
||
...PRICING.hanzoModels,
|
||
{ name: 'enso-pro', fullName: 'enso-pro', description: 'Fixture: the pro preset.', features: [], tier: 'pro', owned_by: 'hanzo', family: 'enso', pricing: { input: 6, output: 27, cacheRead: null, cacheWrite: null } },
|
||
],
|
||
thirdPartyModels: [
|
||
...PRICING.thirdPartyModels,
|
||
{ id: 'anthropic/claude-opus-4.8', name: 'Anthropic: Claude Opus 4.8', provider: 'Anthropic', features: [], contextWindow: 1, isFree: false, pricing: { input: 8, output: 29, cacheRead: null, cacheWrite: null } },
|
||
{ id: 'lab/wide-5', name: 'Lab: Wide 5', provider: 'Lab', features: [], contextWindow: 1, isFree: false, pricing: { input: 3, output: 15, cacheRead: null, cacheWrite: null } },
|
||
{ id: 'lab/mid-4', name: 'Lab: Mid 4', provider: 'Lab', features: [], contextWindow: 1, isFree: false, pricing: { input: 1.5, output: 6, cacheRead: null, cacheWrite: null } },
|
||
{ id: 'lab/cheap-3', name: 'Lab: Cheap 3', provider: 'Lab', features: [], contextWindow: 1, isFree: false, pricing: { input: 0.2, output: 0.8, cacheRead: null, cacheWrite: null } },
|
||
// A variant of a board model is another product, and is never read for it.
|
||
{ id: 'anthropic/claude-opus-4.8:batch', name: 'Anthropic: Claude Opus 4.8 (batch)', provider: 'Anthropic', features: [], contextWindow: 1, isFree: false, pricing: { input: 1, output: 1, cacheRead: null, cacheWrite: null } },
|
||
],
|
||
}
|
||
/** The model list with Kai's and the other decision model's rates, so the Decision Suite's cost row holds and its count is stated. */
|
||
const RATES = { kai: 0.02, jev: 0.061 }
|
||
/**
|
||
* Two of lib/home's frontier LLMs at invented rates (the other two are not
|
||
* served, so they are skipped). Kai at $0.02 against $9.10 and $4.40 per
|
||
* million input tokens is 455× and 220× less: "up to 455× less".
|
||
*/
|
||
const LLMS = [
|
||
{ id: 'openai/gpt-6-astra', name: 'GPT-6 Astra', owned_by: 'openai', outputs: ['text'], pricing: { input_per_million: 9.1, output_per_million: 45.5 } },
|
||
{ id: 'anthropic/claude-opus-5', name: 'Claude Opus 5', owned_by: 'anthropic', outputs: ['text'], pricing: { input_per_million: 4.4, output_per_million: 22 } },
|
||
]
|
||
const SERVED = {
|
||
...MODELS,
|
||
data: [
|
||
...MODELS.data.map((m) => (m.id === 'kai' ? { ...m, pricing: { input_per_million: RATES.kai, output_per_million: 0 } } : m)),
|
||
{ id: JEV, owned_by: 'other', outputs: ['decision'], pricing: { input_per_million: RATES.jev, output_per_million: 0 } },
|
||
...LLMS,
|
||
],
|
||
}
|
||
|
||
/**
|
||
* Invented call timings on the timing benchmark, newer than the snapshot's: an
|
||
* LLM's call through a coding agent's client (an older run of it too, which the
|
||
* newest replaces), Kai over the network, and Kai on one CPU. 1,500 ms against
|
||
* 120 ms is 12.5×; the CPU's 6.4 ms is drawn and divides nothing.
|
||
*/
|
||
const timing = (id: string, split: string, system: string, version: string, ended: string, category: string, p50: number, of: number) => ({
|
||
id,
|
||
benchmark: TIMED,
|
||
split,
|
||
system,
|
||
version,
|
||
baseline: false,
|
||
commit: 'e'.repeat(40),
|
||
ended,
|
||
notes: 'Fixture run.',
|
||
measures: [{ category, metric: 'p50ms', value: p50, n: null, of }],
|
||
})
|
||
const CALLS = [
|
||
timing('gate-llm-old', 'turns', 'claude-code', 'anthropic/claude-opus-5', '2026-10-20T00:00:00Z', 'turn/latency_ms', 9_999, 10),
|
||
timing('gate-llm', 'turns', 'claude-code', 'anthropic/claude-opus-5', '2026-10-28T00:00:00Z', 'turn/latency_ms', 1_500, 321),
|
||
timing('gate-kai-network', 'network', 'kai', 'kai-gate', '2026-10-28T00:00:00Z', 'served/network/n1', 120, 77),
|
||
timing('gate-kai-cpu', 'loopback', 'kai', 'kai-gate', '2026-10-28T00:00:00Z', 'served/loopback/cpu/same/wall', 6.4, 40),
|
||
]
|
||
|
||
/** The research runs: the Kai snapshot's decision runs, the invented call timings and the invented enso field. */
|
||
const RESEARCH = [...(snapshot.data as unknown as Run[]).filter((r) => !r.benchmark.startsWith('enso.')), ...CALLS, ...FIELD]
|
||
|
||
/** Invented coding.repair runs: Claude Code as the baseline, Hanzo Dev's paired speed and two of its paths. */
|
||
const coded = (id: string, system: string, baseline: boolean, solved: number, median: number, calls: number, extra: object[] = []) => ({
|
||
id,
|
||
benchmark: 'coding.repair',
|
||
split: 'held',
|
||
system,
|
||
version: system,
|
||
baseline,
|
||
study: 'gate',
|
||
commit: 'f'.repeat(40),
|
||
ended: '2026-10-10T00:00:00Z',
|
||
notes: 'Fixture run.',
|
||
canonical: true,
|
||
visibility: 'public',
|
||
completion: 'complete',
|
||
measures: [
|
||
{ category: 'all', metric: 'solved', value: solved, n: 40, of: 40 },
|
||
{ category: 'all', metric: 'median_s', value: median },
|
||
{ category: 'all', metric: 'model_calls', value: calls },
|
||
...extra,
|
||
],
|
||
})
|
||
const repairs = (lo: number) => [
|
||
coded('coding.repair-held-gate-claude-code', 'claude-code', true, 40, 7.77, 4.4),
|
||
coded('coding.repair-held-gate-hanzo-dev', 'hanzo-dev', false, 39, 1.11, 1.3, [
|
||
{ category: 'vs-claude-code', metric: 'speedup', value: 4.444, lo, hi: 6.222, n: 39 },
|
||
{ category: 'vs-claude-code/search', metric: 'speedup', value: 9.876, lo: 8.1, hi: 11.2, n: 21 },
|
||
{ category: 'vs-claude-code/model', metric: 'speedup', value: 0.777, lo: 0.6, hi: 0.9, n: 18 },
|
||
]),
|
||
]
|
||
const REPAIRS = repairs(3.111)
|
||
|
||
/** The Decision Suite and its timings, as lib/kai and lib/home read the snapshot's runs. */
|
||
const KEPT = toRuns({ data: RESEARCH })!
|
||
const T = table(kaiOf(KEPT)!.suite, RATES)
|
||
const C = clock(T, KEPT)
|
||
|
||
/** Answer every read the page makes, as a reader who wants no motion. */
|
||
async function open(page: Page, url: string, width: number, coding: unknown[] = REPAIRS, path = '/', models: object = SERVED) {
|
||
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||
await page.setViewportSize({ width, height: width < 600 ? 844 : 900 })
|
||
await serve(page, { '/v1/models': models, '/v1/pricing': LIST })
|
||
await page.route(api, (r) => r.fulfill({ json: listing(r, RESEARCH) }))
|
||
await page.route(CODING, (r) => r.fulfill({ json: { data: coding } }))
|
||
await page.goto(`${url}${path}`, { waitUntil: 'load' })
|
||
await expect(page.locator('[data-api="loading"]')).toHaveCount(0, { timeout: 30_000 })
|
||
await settled(page)
|
||
}
|
||
|
||
test.describe('the landing', () => {
|
||
let site: Awaited<ReturnType<typeof serveExport>>
|
||
test.beforeAll(async () => {
|
||
site = await serveExport()
|
||
})
|
||
test.afterAll(async () => site.close())
|
||
|
||
test('is offered to the index at https://hanzo.ai, and /home2 forwards to it', () => {
|
||
const html = read('index.html')
|
||
expect(html).not.toContain(NOINDEX)
|
||
expect(html).toContain('<link rel="canonical" href="https://hanzo.ai"/>')
|
||
expect(read('sitemap.xml')).toContain('<loc>https://hanzo.ai</loc>')
|
||
expect(read('sitemap.xml')).not.toContain('/home2')
|
||
expect(MOVED['/home2']).toBe('/')
|
||
})
|
||
|
||
test('the export names no rival decision model, and carries every figure the build read, so nothing is left reading', () => {
|
||
const html = read('index.html')
|
||
expect(html).not.toMatch(/jev|typesafe/i)
|
||
// A reader on a slow network reads the page from the HTML: no section waits on a read to say what it measured.
|
||
expect(html).not.toContain('data-api="loading"')
|
||
expect(html).not.toContain('Reading ')
|
||
expect(html).toContain('data-slot="plans-grid"')
|
||
const held: [string, unknown][] = [
|
||
['data-chart="gpqa"', (ensoFigure.enso as { field: unknown } | null)?.field],
|
||
['data-proof="dev-speed"', race(codingFigure.coding as Coding | null)],
|
||
['data-proof="kai-speed"', (speedFigure.speed as unknown as Speed | null)?.call],
|
||
['data-proof="kai-savings"', (speedFigure.speed as unknown as Speed | null)?.spend],
|
||
]
|
||
for (const [mark, figure] of held) expect(html.includes(mark), `${mark} drawn exactly while the build holds its figure`).toBe(figure != null)
|
||
})
|
||
|
||
for (const width of [390, 1440, 1920]) {
|
||
test(`at ${width}: every figure is the served one, nothing leaves the screen, and the console is quiet`, async ({ page }) => {
|
||
const errors: string[] = []
|
||
page.on('pageerror', (e) => errors.push(`pageerror: ${e.message}`))
|
||
page.on('console', (m) => {
|
||
if (m.type() !== 'error') return
|
||
const at = m.location().url
|
||
if (at && !at.startsWith(site.url)) return
|
||
if (at && new URL(at).pathname.startsWith('/v1/')) return
|
||
const away = /(?:Access to \w+ at '|GET |POST )(https?:\/\/[^\s']+)/.exec(m.text())?.[1]
|
||
if (away && !away.startsWith(site.url)) return
|
||
errors.push(m.text())
|
||
})
|
||
await open(page, site.url, width)
|
||
|
||
// Research.Log is hidden from the landing because its benchmarks are shown in the dedicated sections above.
|
||
await expect(page.locator('section#research')).toHaveCount(0)
|
||
|
||
// The plans: the cards /pricing sells from, each at its served price. Max is one card whose pill picks
|
||
// the tier, and its price, its line and its way in follow the pick.
|
||
const plans = page.locator('#plans')
|
||
await expect(plans.getByRole('heading', { level: 2, name: 'Pricing' })).toBeVisible()
|
||
await expect(plans.getByRole('region', { name: 'Free', exact: true })).toContainText('Free')
|
||
await expect(plans.getByRole('region', { name: 'Pro', exact: true })).toContainText('$23')
|
||
// Every card's way in is Try Hanzo, with no commitment under it. No upgrade prompts on landing.
|
||
await expect(plans.getByRole('link', { name: 'Upgrade Plan →' })).toHaveCount(0)
|
||
await expect(plans.getByRole('button', { name: 'Upgrade to Business →' })).toHaveCount(0)
|
||
for (const name of ['Free', 'Pro', 'Max']) {
|
||
const c = plans.getByRole('region', { name, exact: true })
|
||
await expect(c.getByRole('link', { name: 'Try Hanzo', exact: true })).toHaveCount(1)
|
||
await expect(c).toContainText('No commitment · Cancel anytime')
|
||
}
|
||
const max = plans.getByRole('region', { name: 'Max', exact: true })
|
||
const tier = max.getByRole('group', { name: 'Max usage' })
|
||
await expect(tier.getByRole('button')).toHaveText(['5x', '20x'])
|
||
await expect(tier.getByRole('button', { name: 'Max 5x' })).toHaveAttribute('aria-pressed', 'true')
|
||
await expect(max).toContainText('From $111')
|
||
await expect(max).toContainText('5x more usage than Pro')
|
||
const next = async () => new URL(new URL((await max.getByRole('link', { name: 'Try Hanzo' }).getAttribute('href'))!, site.url).searchParams.get('next')!, site.url).searchParams.get('plan')
|
||
expect(await next()).toBe('max-5x')
|
||
await tier.getByRole('button', { name: 'Max 20x' }).click()
|
||
await expect(tier.getByRole('button', { name: 'Max 20x' })).toHaveAttribute('aria-pressed', 'true')
|
||
await expect(max).toContainText('$222')
|
||
await expect(max).toContainText('20x more usage than Pro')
|
||
await expect(max).not.toContainText('$111')
|
||
expect(await next()).toBe('max-20x')
|
||
// The page ends on who trusts us, then the footer: no close, no guide (owner, 2026-10-30). The questions are /learn's.
|
||
await expect(page.locator('section#close')).toHaveCount(0)
|
||
await expect(page.locator('[data-slot="guide"]')).toHaveCount(0)
|
||
await expect(page.getByRole('heading', { name: 'FAQ' })).toHaveCount(0)
|
||
// The pill sits on the card, inside its edge, at every width.
|
||
const [card, pill] = await Promise.all([max.boundingBox(), tier.boundingBox()])
|
||
expect(pill!.x + pill!.width, 'the pill inside the card').toBeLessThanOrEqual(card!.x + card!.width)
|
||
expect(pill!.x, 'the pill inside the card').toBeGreaterThanOrEqual(card!.x)
|
||
|
||
// Faster and Cheaper are the measured comparisons; no link opens a research run record.
|
||
await expect(page.locator('main')).not.toContainText(/per dollar/)
|
||
await expect(page.locator('[data-proof="dev-speed"]')).toContainText('4.44×')
|
||
// Kai against an LLM call, network against network, and on one CPU as a figure of its own.
|
||
const speed = page.locator('[data-proof="kai-speed"]')
|
||
await expect(speed.locator('[data-figure="kai-speed"]')).toHaveText('12.5×')
|
||
await expect(speed).toContainText('faster than an LLM call, over the network')
|
||
await expect(speed).toContainText('a Claude Opus 5 call as Claude Code saw it, over 321 calls, and a Kai decision on api.hanzo.ai, over 77.')
|
||
await expect(speed).toContainText('Kai answers in 6.4 ms.')
|
||
for (const [label, figure] of [['Claude Opus 5 call', '1,500 ms'], ['Kai on api.hanzo.ai', '120 ms'], ['Kai served on one CPU, no network', '6.4 ms']]) {
|
||
await expect(speed.getByText(label, { exact: true })).toBeVisible()
|
||
await expect(speed.getByText(figure, { exact: true })).toBeVisible()
|
||
}
|
||
await expect(speed, 'the older LLM run is replaced by the newest').not.toContainText('9,999')
|
||
await expect(speed, 'no multiple is made of the CPU figure').not.toContainText('234')
|
||
// Kai against frontier LLMs per input token: the served ones, dearest first, and Kai's rate with no output billed.
|
||
const price = page.locator('[data-proof="kai-savings"]')
|
||
await expect(price.locator('[data-figure="kai-savings"]')).toHaveText('up to 455× less')
|
||
await expect(price).toContainText('than frontier LLMs, per input token')
|
||
await expect(price).toContainText('Kai bills input only; an LLM also bills every token it writes.')
|
||
for (const figure of ['$9.10 in · $45.50 out', '$4.40 in · $22 out', '$0.02 in · $0 out']) await expect(price.getByText(figure, { exact: true })).toBeVisible()
|
||
expect(await price.innerText()).toMatch(/GPT-6 Astra[\s\S]*Claude Opus 5[\s\S]*Kai/)
|
||
await expect(price).not.toContainText('Fable')
|
||
await expect(page.locator('#cheaper')).toContainText('Cheaper')
|
||
await expect(page.locator('main a[href*="/v1/research/runs"]')).toHaveCount(0)
|
||
|
||
// Nothing leaves the screen: no sideways scroll, and no line of text past the right edge outside a scrolling code block.
|
||
await page.mouse.wheel(0, 30_000)
|
||
await page.waitForTimeout(400)
|
||
const out = await page.evaluate(() => {
|
||
const w = document.documentElement.clientWidth
|
||
const past = [...document.querySelectorAll('main *')]
|
||
.filter((e) => [...e.childNodes].some((n) => n.nodeType === 3 && n.textContent!.trim()))
|
||
.filter((e) => !e.closest('pre') && !e.closest('[data-models]'))
|
||
.filter((e) => {
|
||
const r = e.getBoundingClientRect()
|
||
return r.width > 0 && r.right > w + 1
|
||
})
|
||
.map((e) => `${e.tagName} "${e.textContent!.trim().slice(0, 40)}"`)
|
||
return { wide: document.documentElement.scrollWidth > w + 1, past }
|
||
})
|
||
expect(out.wide, `sideways scroll at ${width}`).toBe(false)
|
||
expect(out.past, `text past the edge at ${width}`).toEqual([])
|
||
|
||
// The rival decision model is never named on the page, in its text or its labels.
|
||
expect(await page.locator('main').innerText()).not.toMatch(/\bjev\b|typesafe/i)
|
||
expect(await page.locator('main [aria-label]').evaluateAll((es) => es.map((e) => e.getAttribute('aria-label')).join(' '))).not.toMatch(/\bjev\b|typesafe/i)
|
||
expect(errors).toEqual([])
|
||
})
|
||
}
|
||
|
||
test('a hero device moves by its title bar on a wide screen, its controls stay controls, and the phone stays put on a phone', async ({ page }) => {
|
||
await open(page, site.url, 1440)
|
||
// The terminal: its bar is the handle, and its tabs inside the bar stay buttons (chrome `useMove`, as every window's).
|
||
const term = page.locator('[data-slot="terminal"]')
|
||
const bar = term.locator('[data-handle]')
|
||
const before = (await term.boundingBox())!
|
||
const grip = (await bar.boundingBox())!
|
||
await page.mouse.move(grip.x + grip.width - 12, grip.y + grip.height / 2)
|
||
await page.mouse.down()
|
||
await page.mouse.move(grip.x + grip.width - 182, grip.y + grip.height / 2 + 30, { steps: 8 })
|
||
await page.mouse.up()
|
||
const moved = (await term.boundingBox())!
|
||
expect(before.x - moved.x, 'moved left by the drag').toBeGreaterThan(150)
|
||
expect(moved.y - before.y, 'moved down by the drag').toBeGreaterThan(20)
|
||
// The arrow keys nudge it while the bar has focus.
|
||
await bar.focus()
|
||
await page.keyboard.press('ArrowRight')
|
||
expect((await term.boundingBox())!.x - moved.x).toBeCloseTo(12, 0)
|
||
// A window's title bar holds no button; what a device holds stays a control of its own.
|
||
expect(await page.locator('[data-win] [data-handle] button').count()).toBe(0)
|
||
await expect(term.getByRole('button', { name: /^Copy: / }).first()).toBeVisible()
|
||
// On a phone the scene is the phone alone, and it stays where the page put it.
|
||
await page.setViewportSize({ width: 390, height: 844 })
|
||
await page.reload({ waitUntil: 'load' })
|
||
await expect(page.locator('[data-slot="terminal"]')).toBeHidden()
|
||
const phone = page.locator('[data-slot="build-preview"] [data-slot="phone"]')
|
||
const at = (await phone.boundingBox())!
|
||
await page.mouse.move(at.x + at.width / 2, at.y + 12)
|
||
await page.mouse.down()
|
||
await page.mouse.move(at.x + at.width / 2 - 120, at.y + 12, { steps: 6 })
|
||
await page.mouse.up()
|
||
expect((await phone.boundingBox())!.x).toBeCloseTo(at.x, 0)
|
||
})
|
||
|
||
for (const width of [390, 1440]) {
|
||
test(`at ${width}: /dev races Hanzo Dev beside Claude Code on the served runs`, async ({ page }) => {
|
||
await open(page, site.url, width, REPAIRS, '/dev')
|
||
// The race: the served runs, not the build's snapshot.
|
||
await expect(page.locator('#race-title')).toHaveText('4.44× faster on the same coding tasks.')
|
||
await expect(page.locator('[data-slot="lane-Claude Code"] [data-clock]')).toHaveText('7.77 s')
|
||
await expect(page.locator('[data-slot="lane-Hanzo Dev"] [data-clock]')).toHaveText('1.11 s')
|
||
// Claude Code solved more (40 against 39), so no solved count is drawn on either side (lib/coding `ahead`).
|
||
await expect(page.locator('section#race')).not.toContainText('40 of 40')
|
||
await expect(page.locator('section#race')).not.toContainText('39 of 40')
|
||
await expect(page.locator('[data-slot="lane-Hanzo Dev"]')).toContainText('replay of run coding.repair-held-gate-hanzo-dev')
|
||
await expect(page.locator('[data-figure="speed"]')).toHaveText('4.44× faster over the 39 tasks both solved, 95% interval 3.11–6.22×.')
|
||
// The paths that are resolved gains, and not the slower one.
|
||
await expect(page.locator('[data-path]')).toHaveCount(1)
|
||
await expect(page.locator('[data-path="Settled by search, no model call"]')).toContainText('21 tasks')
|
||
await expect(page.locator('[data-path="Settled by search, no model call"]')).toContainText('9.88× · 95% 8.10–11.20×')
|
||
await expect(page.locator('section#race')).not.toContainText('A model wrote the edit')
|
||
await expect(page.locator('section#race')).not.toContainText('0.78×')
|
||
|
||
})
|
||
|
||
test(`at ${width}: /kai times one decision on the served runs, and no hosted call beside it`, async ({ page }) => {
|
||
await open(page, site.url, width, REPAIRS, '/kai')
|
||
// Decisions in a flash: the cost row's GPU timing and the served run, cached, runtime and HTTP, and the suite's rated tasks Kai wins; nothing hosted, nothing of Zen.
|
||
expect(C, 'the snapshot holds the cost row').not.toBeNull()
|
||
await expect(page.locator('#flash-title')).toHaveText('Decisions in a flash.')
|
||
for (const v of [C!.cached, C!.same, C!.fresh].filter((v): v is number => v != null)) await expect(page.locator('[data-slot="clock"]')).toContainText(`${ms(v)} ms`)
|
||
await expect(page.locator('[data-slot="clock"]')).not.toContainText('CPU')
|
||
await expect(page.locator('section#flash'), 'no hosted call, no cached-decision or in-process talk').not.toContainText(/hosted|cached decision|in process|Zen 6/)
|
||
await expect(page.locator('[data-slot="suite"] [data-task]').first()).toContainText('Jev')
|
||
await expect(page.locator('section#speed'), "Kai's page carries Kai's section, not Zen's").toHaveCount(0)
|
||
|
||
})
|
||
}
|
||
|
||
test("in the dark, every maker's mark on the GPQA chart is drawn in the page's ink, and Enso's H in the ground on an ink disc", async ({ page }) => {
|
||
await page.emulateMedia({ colorScheme: 'dark' })
|
||
await open(page, site.url, 1440, REPAIRS, '/enso')
|
||
const plot = page.locator('[data-chart="gpqa"] svg[data-plot]').first()
|
||
await expect(plot.locator('[data-dot="opus-4.8"] foreignObject svg')).toHaveCount(1)
|
||
await expect(plot.locator('[data-dot="enso-ultra"] foreignObject svg')).toHaveCount(1)
|
||
const marks = await plot.locator('[data-dot]').evaluateAll((gs) => {
|
||
const ink = getComputedStyle(gs.find((g) => g.getAttribute('data-kind') === 'ours')!.querySelector(':scope > text')!).fill
|
||
return gs.flatMap((g) => {
|
||
const svg = g.querySelector('foreignObject svg')
|
||
const disc = g.querySelectorAll('circle')
|
||
return svg ? [{ id: g.getAttribute('data-dot'), ours: g.getAttribute('data-kind') === 'ours', mark: getComputedStyle(svg).color, disc: getComputedStyle(disc[disc.length - 1]).fill, ink, ground: getComputedStyle(document.body).backgroundColor }] : []
|
||
})
|
||
})
|
||
expect(marks.filter((m) => !m.ours).length, "a lab's mark is drawn").toBeGreaterThan(0)
|
||
for (const m of marks) {
|
||
expect(m.mark, `${m.id} stands out of its disc`).not.toBe(m.disc)
|
||
if (m.ours) expect(m.disc, `${m.id}'s disc is the page's ink`).toBe(m.ink)
|
||
else {
|
||
expect(m.mark, `${m.id} in the page's ink`).toBe(m.ink)
|
||
expect(m.mark, m.id!).not.toBe(m.ground)
|
||
}
|
||
}
|
||
})
|
||
|
||
test('the landing shows the product overview, all models and the stack, with no closing key pitch', async ({ page }) => {
|
||
await page.setViewportSize({ width: 1440, height: 900 })
|
||
await serve(page)
|
||
await page.goto(`${site.url}/`, { waitUntil: 'load' })
|
||
await page.locator('html[data-hydrated]').waitFor({ state: 'attached' })
|
||
await expect(page.locator('#products')).toHaveCount(1)
|
||
await expect(page.locator('section#open-source')).toHaveCount(1)
|
||
for (const slug of ['enso', 'kai', 'zen', 'koma', 'iro', 'mori', 'oto', 'koe', 'uta']) {
|
||
await expect(page.locator(`[data-models] [data-model="${slug}"]`)).toHaveAttribute('href', `/${slug}`)
|
||
}
|
||
for (const slug of ['kata']) {
|
||
await expect(page.locator(`[data-models] [data-model="${slug}"]`)).toHaveCount(0)
|
||
}
|
||
// The stack: every layer's page is in the section, whichever layer the inspector shows.
|
||
for (const href of STACK) expect(await page.locator(`section#platform a[href="${href}"]`).count(), href).toBeGreaterThan(0)
|
||
await expect(page.locator('section#platform a[href="/platform#stack"]')).toHaveCount(1)
|
||
await expect(page.locator('section#platform a[href="/mcp"]')).toHaveCount(0)
|
||
await expect(page.locator('section#platform a[href="/search"]')).toHaveCount(0)
|
||
// Each app's name opens the app; Explore opens its page.
|
||
for (const [key, app, about] of [
|
||
['chat', '/chat', '/chat'],
|
||
['dev', '/dev', '/dev'],
|
||
['bot', 'https://hanzo.bot', '/bot'],
|
||
['studio', '/studio', '/studio'],
|
||
['team', 'https://hanzo.team', '/team'],
|
||
]) {
|
||
await expect(page.locator(`[data-product="${key}"] a[data-app][href="${app}"]`)).toHaveCount(1)
|
||
await expect(page.locator(`[data-product="${key}"] a[data-page][href="${about}"]`)).toHaveCount(1)
|
||
}
|
||
await expect(page.locator('#get-key')).toHaveCount(0)
|
||
// How an agent signs itself up is on the page, above pricing, with its way in.
|
||
const agents = page.locator('section#compatible')
|
||
await expect(agents).toContainText('No human required')
|
||
await expect(agents.locator('a[href="/agents#headless"]')).toHaveCount(1)
|
||
await expect(agents.locator('a[href="https://docs.hanzo.ai/docs/agents/headless"]')).toHaveCount(1)
|
||
// Each step opens the script's own lines for it, to copy.
|
||
await agents.locator('[data-step="2"]').click()
|
||
await expect(agents.locator('[data-slot="headless-code"] pre')).toContainText('/v1/account/keys')
|
||
expect(await page.evaluate(() => {
|
||
const ids = [...document.querySelectorAll('main [id]')].map((e) => e.id)
|
||
return ids.indexOf('compatible') < ids.indexOf('plans')
|
||
})).toBe(true)
|
||
})
|
||
|
||
test('a model list that serves none of the frontier LLMs draws no Kai price, and the call still names its model', async ({ page }) => {
|
||
await open(page, site.url, 1440, REPAIRS, '/', { ...SERVED, data: SERVED.data.filter((m) => !LLMS.includes(m as (typeof LLMS)[number])) })
|
||
await expect(page.locator('[data-proof="enso-savings"]')).toHaveCount(1)
|
||
await expect(page.locator('[data-proof="kai-savings"]')).toHaveCount(0)
|
||
// Unlisted, the LLM is named by its id less its lab.
|
||
await expect(page.locator('[data-proof="kai-speed"]')).toContainText('claude-opus-5 call')
|
||
})
|
||
|
||
test('a coding answer that no longer holds a resolved gain takes the race down', async ({ page }) => {
|
||
await open(page, site.url, 1440, repairs(0.9), '/dev')
|
||
await expect(page.locator('#race')).toHaveCount(0, { timeout: 15_000 })
|
||
})
|
||
|
||
test('with api.hanzo.ai down, the build’s figures stand, nothing is left reading and no section stands empty', async ({ page }) => {
|
||
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||
await page.setViewportSize({ width: 1440, height: 900 })
|
||
await block(page)
|
||
await page.route(api, (r) => r.abort('connectionrefused'))
|
||
await page.route(CODING, (r) => r.abort('connectionrefused'))
|
||
await page.goto(`${site.url}/`, { waitUntil: 'load' })
|
||
await expect(page.locator('[data-api="loading"]')).toHaveCount(0, { timeout: 30_000 })
|
||
await expect(page.locator('section#research')).toHaveCount(0)
|
||
await expect(page.locator('[data-slot="clock"]')).toHaveCount(0)
|
||
// The plans and the proof stand as the build drew them: the API out of reach does not make them false.
|
||
await page.waitForTimeout(3000)
|
||
await expect(page.locator('[data-api="unreachable"]')).toHaveCount(0)
|
||
await expect(page.locator('#plans [data-slot="plans-grid"]')).toHaveCount(1)
|
||
if (race(codingFigure.coding as Coding | null)) await expect(page.locator('[data-proof="dev-speed"]')).toHaveCount(1)
|
||
for (const [proof, held] of [['kai-speed', (speedFigure.speed as unknown as Speed | null)?.call], ['kai-savings', (speedFigure.speed as unknown as Speed | null)?.spend]] as const) await expect(page.locator(`[data-proof="${proof}"]`)).toHaveCount(held ? 1 : 0)
|
||
// The sign-up, the product preview and the stack need no read.
|
||
await expect(page.locator('[data-slot="start"]')).toContainText('curl -fsSL https://hanzo.sh | sh')
|
||
await expect(page.locator('[data-product]')).toHaveCount(5)
|
||
for (const href of STACK) expect(await page.locator(`section#platform a[href="${href}"]`).count(), href).toBeGreaterThan(0)
|
||
})
|
||
|
||
test('the stack: twelve plates, each opening its card in place by pointer and keyboard', async ({ page }) => {
|
||
await open(page, site.url, 1440)
|
||
const stack = page.locator('section#platform [data-slot="stack"]')
|
||
const map = stack.locator('[data-slot="stack-map"]')
|
||
const plate = (id: string) => map.locator(`[data-plate="${id}"]`)
|
||
const card = () => stack.locator('[data-card]:visible')
|
||
await expect(map.locator('[data-plate][role="button"]')).toHaveCount(12)
|
||
// Enso opens first; its repository holds examples, and says so.
|
||
await expect(plate('enso')).toHaveAttribute('aria-expanded', 'true')
|
||
await expect(card()).toHaveCount(1)
|
||
await expect(card().getByRole('heading', { name: 'Enso' })).toBeVisible()
|
||
await expect(card().getByRole('link', { name: /Examples/ })).toHaveAttribute('href', 'https://github.com/hanzoai/enso')
|
||
// Pointing at a plate shows its card beside it, level with it; leaving returns to the pinned one.
|
||
await plate('kai').hover()
|
||
await expect(card().getByRole('heading', { name: 'Kai' })).toBeVisible()
|
||
const [p, c] = await Promise.all([plate('kai').boundingBox(), card().boundingBox()])
|
||
expect(c!.x).toBeGreaterThan(p!.x + p!.width * 0.8)
|
||
expect(Math.abs(c!.y - p!.y)).toBeLessThan(40)
|
||
await page.mouse.move(4, 4)
|
||
await expect(card().getByRole('heading', { name: 'Enso' })).toBeVisible()
|
||
// A press pins a plate and the same press lets it go.
|
||
await plate('agents').click()
|
||
await page.mouse.move(4, 4)
|
||
await expect(card().getByRole('heading', { name: 'Agents' })).toBeVisible()
|
||
await expect(card().getByRole('link', { name: /Page/ })).toHaveAttribute('href', '/agents')
|
||
await plate('agents').click()
|
||
await page.mouse.move(4, 4)
|
||
await expect(card()).toHaveCount(0)
|
||
// The keyboard moves between plates and opens each; Escape closes.
|
||
await plate('agents').focus()
|
||
await page.keyboard.press('ArrowDown')
|
||
await expect(plate('sandboxes')).toBeFocused()
|
||
await expect(card().getByRole('heading', { name: 'Sandboxes' })).toBeVisible()
|
||
await page.keyboard.press('Escape')
|
||
await expect(card()).toHaveCount(0)
|
||
// Reduced motion: nothing to pause, and the plates stand apart where the export drew them.
|
||
await expect(stack.getByRole('button', { name: 'Pause animation' })).toHaveCount(0)
|
||
expect(await plate('gateway').evaluate((e) => getComputedStyle(e.parentElement!).transform)).toBe('none')
|
||
expect(await plate('gateway').evaluate((e) => e.parentElement!.getAnimations().length)).toBe(0)
|
||
})
|
||
|
||
test('on a phone and a tablet every plate is drawn, and a tap opens its card in place', async ({ browser }) => {
|
||
for (const width of [390, 430, 768]) {
|
||
const ctx = await browser.newContext({ viewport: { width, height: 844 }, hasTouch: true, isMobile: true, reducedMotion: 'reduce' })
|
||
const page = await ctx.newPage()
|
||
await open(page, site.url, width)
|
||
const map = page.locator('section#platform [data-slot="stack-map"]')
|
||
const plate = (id: string) => map.locator(`[data-plate="${id}"]`)
|
||
const card = () => page.locator('section#platform [data-card]:visible')
|
||
await map.scrollIntoViewIfNeeded()
|
||
// All twelve, inside the screen, each name at least 12px tall.
|
||
const drawn = await map.evaluate((m) => ({
|
||
vw: document.documentElement.clientWidth,
|
||
wide: document.documentElement.scrollWidth,
|
||
boxes: [...m.querySelectorAll('[data-plate]')].map((h) => h.getBoundingClientRect()).map((r) => [r.left, r.right, r.height]),
|
||
names: [...m.querySelectorAll('svg text[font-weight="600"]')].map((t) => t.getBoundingClientRect().height),
|
||
}))
|
||
expect(drawn.boxes, `${width}`).toHaveLength(12)
|
||
expect(drawn.wide, `${width}`).toBeLessThanOrEqual(drawn.vw)
|
||
for (const [l, r] of drawn.boxes) expect(l >= 0 && r <= drawn.vw, `${width}: ${l}–${r}`).toBe(true)
|
||
expect(Math.min(...drawn.names), `${width}`).toBeGreaterThanOrEqual(12)
|
||
// A tap opens Kai's card; it stays until a tap on the same plate.
|
||
await plate('kai').tap()
|
||
await expect(plate('kai')).toHaveAttribute('aria-expanded', 'true')
|
||
await expect(card().getByRole('heading', { name: 'Kai' })).toBeVisible()
|
||
for (const name of [/Page/, /Docs/, /Examples/]) await expect(card().getByRole('link', { name })).toBeVisible()
|
||
const [k, c] = await Promise.all([plate('kai').boundingBox(), card().boundingBox()])
|
||
expect(c!.x >= 0 && c!.x + c!.width <= width, `${width}: card inside`).toBe(true)
|
||
if (width < 768) expect(c!.y, `${width}: card under the plate`).toBeGreaterThan(k!.y)
|
||
else expect(Math.abs(c!.y - k!.y), `${width}: card level with the plate`).toBeLessThan(40)
|
||
await plate('kai').tap()
|
||
await expect(card()).toHaveCount(0)
|
||
await plate('engine').tap()
|
||
await expect(card().getByRole('heading', { name: 'Engine' })).toBeVisible()
|
||
await ctx.close()
|
||
}
|
||
})
|
||
|
||
test('the stack closes while below the screen and parts as it comes up', async ({ page }) => {
|
||
await page.setViewportSize({ width: 1440, height: 900 })
|
||
await serve(page, { '/v1/models': SERVED, '/v1/pricing': LIST })
|
||
await page.goto(`${site.url}/`, { waitUntil: 'load' })
|
||
await page.locator('html[data-hydrated]').waitFor({ state: 'attached' })
|
||
const map = page.locator('[data-slot="stack-map"]')
|
||
const gateway = map.locator('[data-plate="gateway"]')
|
||
const shift = () => gateway.evaluate((e) => new DOMMatrix(getComputedStyle(e.parentElement!).transform).m42)
|
||
// Its middle just under the screen: the stack is closed, the top plate drawn down onto the rest.
|
||
await map.evaluate((e) => window.scrollBy(0, e.getBoundingClientRect().top + e.getBoundingClientRect().height / 2 - innerHeight - 20))
|
||
await expect.poll(shift).toBeGreaterThan(100)
|
||
// Its middle above the screen's: the plates stand apart.
|
||
await map.evaluate((e) => window.scrollBy(0, e.getBoundingClientRect().top + e.getBoundingClientRect().height / 2 - innerHeight * 0.4))
|
||
await expect.poll(async () => Math.abs(await shift())).toBeLessThan(4)
|
||
})
|
||
|
||
test('animates only once seen, and plays the recorded medians', async ({ page }) => {
|
||
await page.setViewportSize({ width: 1440, height: 900 })
|
||
await serve(page, { '/v1/models': SERVED, '/v1/pricing': LIST })
|
||
await page.route(api, (r) => r.fulfill({ json: listing(r, RESEARCH) }))
|
||
await page.route(CODING, (r) => r.fulfill({ json: { data: REPAIRS } }))
|
||
await page.goto(`${site.url}/dev`, { waitUntil: 'load' })
|
||
await settled(page)
|
||
await expect(page.locator('#race-title')).toHaveText('4.44× faster on the same coding tasks.', { timeout: 15_000 })
|
||
// Off screen, the race waits at zero.
|
||
await expect(page.locator('[data-slot="lane-Hanzo Dev"] [data-clock]')).toHaveText('0.00 s')
|
||
await page.locator('[data-slot="race"]').scrollIntoViewIfNeeded()
|
||
// On screen, each clock runs to its median and stops there.
|
||
await expect(page.locator('[data-slot="lane-Hanzo Dev"] [data-clock]')).toHaveText('1.11 s', { timeout: 8_000 })
|
||
await expect(page.locator('[data-slot="lane-Claude Code"] [data-clock]')).toHaveText('7.77 s', { timeout: 15_000 })
|
||
await expect(page.locator('[data-slot="lane-Claude Code"]')).toContainText('done')
|
||
})
|
||
})
|