Files
hanzo.ai/e2e/gates/chat.ts
zeekay 37ff5ac382 gates: the composer's model menu, its suggestions and its agent shelf as they are
The model word under the composer opens @hanzo/build's own menu since 0.2.73
(Search models, Enso's row, model rows carrying data-model and data-paused),
not a panel with @hanzo/ui ModelPicker inside it, and effort is a word of its
own. chat.ts reads that menu; picker, limits, upgrade and named measure it.
ModelPicker's research-preview row has no counterpart in build's menu, which
lists what /v1/models serves, so picker.spec stops asserting it and LLM.md
says where Zen 7 is asked for instead. The usage bars are UsageMeter rows
keyed by class (usage-meter-row), as the settings gates already read them.

An empty chat shows @hanzo/build Suggestions since @hanzo/rooms 0.1.83, and a
starter does its act on the press; starters and local-cloud hold that, with
the kept rows answered so no model is asked. Agents are seated from
+ › Agents, which lists the org's own agents; together.spec gives the org the
core team's rows and holds the same room, briefs and names. The thread on
/models is found by its accessible name: rooms' Ask no longer draws a
placeholder, so TryChat stops passing one.

Claude-Session: https://claude.ai/code/session_01BLLMyBFSvAfCt7ZGqYuiEz
2026-10-22 13:05:18 -07:00

105 lines
5.1 KiB
TypeScript

import type { Page } from '@playwright/test'
import { answer, signIn } from '../session'
import { config, person } from './heard'
import { PLANS } from './api'
/**
* A signed-in reader at /chat's composer, and the model chip beside send
* (@hanzo/build's Tune, whose choice is kept as `hanzo.mind`).
*
* Three models are served: Enso's free route, Enso's router, and one Enso does
* not route to. The Free plan runs the first two.
*/
export const PAID = { id: 'zen-6', name: 'Zen 6', family: 'zen', class: 'ours' }
export const FREE = { id: 'enso-free', name: 'Enso Free', family: 'enso', class: 'free' }
/** On the Free plan's row, though `isFree` does not pass it. */
export const AUTO = { id: 'enso-auto', name: 'Enso Auto', family: 'enso', class: 'ours' }
/** Where the browser keeps the reader's model and effort (@hanzo/build/mind). */
const MIND = 'hanzo.mind'
/** The model the browser keeps as the reader's choice, or null when none is kept. */
export const kept = (page: Page): Promise<string | null> =>
page.evaluate((key) => {
try {
return (JSON.parse(localStorage.getItem(key) ?? 'null') as { model?: string } | null)?.model ?? null
} catch {
return null
}
}, MIND)
/**
* Chat ready for a keystroke: the app's frame in Chat, with the composer's field
* drawn. Drawn means hydrated: the app renders on the client only
* (app/_web.tsx), so nothing of it is in the export to be typed into early. The
* field, not send: an empty composer's round button is voice mode, and send is
* drawn only once there are words.
*/
export async function ready(page: Page, timeout = 30_000) {
await page.locator('[data-slot="frame"][data-app="chat"] [data-slot="composer-field"]').first().waitFor({ timeout })
}
/** A signed-in reader in their personal organization, on `tier`, with `chosen` as their model and `more` served beside the three. */
export async function arrive(page: Page, base: string, tier: string, chosen: string, more: { id: string; name: string; owned_by?: string; class?: string; family?: string }[] = []) {
await signIn(page, { sub: 'bob', name: 'bob', email: '[email protected]', org: 'bob' })
await person(page)
await config(page)
await page.route('https://www.googletagmanager.com/**', (route) =>
route.fulfill({ status: 200, contentType: 'text/javascript', body: '' }),
)
await page.route('https://connect.facebook.net/**', (route) =>
route.fulfill({ status: 200, contentType: 'text/javascript', body: '' }),
)
await answer(page, '/v1/billing/tier', {
user: 'bob',
tier: { name: tier, displayName: tier, dailyCreditsCents: 0, maxAgents: 1, unlimitedAgents: false },
balance: { currency: 'usd', prepaidAvailable: 0, creditsRemaining: 0, dailyRemaining: 0, effectiveAvailable: 0 },
})
await answer(page, '/v1/billing/balance', { available: 0 })
// The plan authority, as the gates answer it: the upgrade quotes from it.
await answer(page, '/v1/billing/plans', PLANS)
await answer(page, '/v1/iam/organizations', {
organizations: [{ owner: 'admin', name: 'bob', displayName: 'bob', isPersonal: true }],
})
await answer(page, '/v1/models', {
object: 'list',
data: [FREE, AUTO, PAID, ...more].map((m) => ({ object: 'model', owned_by: 'hanzo', ...m })),
})
await answer(page, '/v1/agents', { agents: [] })
await answer(page, '/v1/agents/chat/conversations', { conversations: [] })
await answer(page, '/v1/agents/sessions', { sessions: [] })
await answer(page, '/v1/projects', [])
await answer(page, '/v1/team/rooms', { rooms: [] })
await answer(page, '/v1/channels/inbox', { items: [] })
await page.addInitScript(([key, model]) => localStorage.setItem(key, JSON.stringify({ model, effort: 'medium' })), [MIND, chosen] as const)
await page.goto(`${base}/chat`)
await ready(page)
}
/**
* The model's word under the composer: the button named `Model: <name>` inside
* @hanzo/build Tune's `[data-slot="mind"]`. Since @hanzo/build 0.2.73 it opens
* the model menu directly — no panel with a picker one press further in — and
* the effort is a word of its own beside it (`Effort: <pace>`).
*/
export const chip = (page: Page) => page.locator('[data-slot="mind"] button')
/** The effort's word beside the model's, drawn while the model takes an effort. */
export const effort = (page: Page) => page.locator('[data-slot="tune"]').getByRole('button', { name: /^Effort: / })
/** The model menu the chip opens: Search models, Enso's row, then the catalog by family. */
export const panel = (page: Page) => page.locator('[data-slot="model-menu"]')
/** Enso's row, the router, at the head of the menu on any plan but Free. */
export const enso = (page: Page) => panel(page).getByRole('menuitem', { name: 'Enso Picks the model for each message and step' })
/** A model's row in the open menu. */
export const row = (page: Page, id: string) => panel(page).locator(`[data-slot="model-row"][data-model="${id}"]`)
/** Opens the model menu and picks `id`: Enso's own row for the router, else the model's. */
export async function pick(page: Page, id: string) {
await chip(page).click()
await (id === AUTO.id ? enso(page) : row(page, id)).click()
}