Files
zeekay 46a5638bf4 Answer IAM in every gate from one helper, at the hosts the site asks
The gates answered hanzo.id only, so once serverUrl moved to api.hanzo.ai
discovery and the address lookup went unanswered and 22 of them failed.
e2e/gates/issuer.ts now answers discovery, the token, userinfo, the lookup
and the methods on api.hanzo.ai, records the authorize page on hanzo.id, and
answers this origin's credential routes. login, silent, signin, follow, apex,
guest, cards, forward, host, orgs, own, affiliate, signin-header and signIn
use it in place of their own copies.

contract() is the rule: hanzo.id is the authorize page, and is asked nothing
but the account row and appearance @hanzo/rooms still addresses there.

Claude-Session: https://claude.ai/code/session_01HvACxex9ZZTzJCCHgirsGy
2026-10-06 22:13:27 -07:00

107 lines
4.4 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test'
import { serveExport } from './export'
import { issuer } from './issuer'
/**
* A BROWSER BELONGS TO ONE PERSON AT A TIME.
*
* Conversations, contacts and settings this site keeps in localStorage are
* written per browser, so whoever signs in next at the same machine would read
* the last person's. `own()` (lib/auth/session.ts) records whose they are and
* empties them when somebody else — or nobody — arrives. Measured here on the
* shipped bytes: the previous person's group chat is seeded, and the page a new
* person (or a signed-out one) opens neither lists it nor keeps it.
*/
const THREAD = 'Alice and the offsite crew'
function seed(page: Page, sub: string | null) {
return page.addInitScript(
({ sub, thread }) => {
if (sessionStorage.getItem('gate.seeded')) return
sessionStorage.setItem('gate.seeded', '1')
// What the last person left behind.
localStorage.setItem('hanzo:who', 'alice')
localStorage.setItem(
'hanzo_custom_threads_v1',
JSON.stringify([{ id: 'ct-alice', title: thread, type: 'group', participants: [], updatedAt: Date.now() }]),
)
localStorage.setItem('hanzo_iam_current_org', 'alices-org')
localStorage.setItem('hanzo.agent.persona', 'alice-persona')
localStorage.setItem('hanzo:sidebar:open', '1')
if (!sub) return
// Who arrives now: a session the SDK reads as live.
const part = (v: unknown) =>
btoa(JSON.stringify(v)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
const exp = Math.floor(Date.now() / 1000) + 3600
const token = `${part({ alg: 'none', typ: 'JWT' })}.${part({ sub, name: sub, email: `${sub}@hanzo.ai`, exp })}.`
localStorage.setItem('hanzo_iam_access_token', token)
localStorage.setItem('hanzo_iam_id_token', token)
localStorage.setItem('hanzo_iam_expires_at', String(exp * 1000))
},
{ sub, thread: THREAD },
)
}
async function arrive(page: Page, base: string, sub: string | null) {
// Every gateway call answers empty: the measurement is what the browser kept.
await page.route('**/v1/**', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: '{}' }))
await issuer(page)
await seed(page, sub)
await page.goto(`${base}/chat`, { waitUntil: 'domcontentloaded' })
}
const kept = (page: Page) =>
page.evaluate(() => ({
who: localStorage.getItem('hanzo:who'),
threads: localStorage.getItem('hanzo_custom_threads_v1'),
org: localStorage.getItem('hanzo_iam_current_org'),
persona: localStorage.getItem('hanzo.agent.persona'),
sidebar: localStorage.getItem('hanzo:sidebar:open'),
token: localStorage.getItem('hanzo_iam_access_token'),
}))
test.describe('the browser is one person\'s', () => {
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('a different person signs in and finds none of the last one\'s', async ({ page }) => {
await arrive(page, base, 'bob')
await expect.poll(async () => (await kept(page)).who).toBe('bob')
const k = await kept(page)
expect(k.threads, 'the last person\'s group chat').toBeNull()
expect(k.org, 'the last person\'s selected org').toBeNull()
expect(k.persona, 'the last person\'s agent persona').toBeNull()
expect(k.sidebar, 'chrome that carries nobody\'s data').toBe('1')
expect(k.token, 'the arriving session').toBeTruthy()
await expect(page.getByText(THREAD)).toHaveCount(0)
})
test('a signed-out browser keeps nothing of the last person', async ({ page }) => {
await arrive(page, base, null)
// A signed-out reader of /chat is sent to sign in; the browser is read there,
// not while the page is between the two.
await page.waitForURL((u) => /^\/(login|signup)/.test(u.pathname))
await expect.poll(async () => (await kept(page)).who).toBeNull()
const k = await kept(page)
expect(k.threads).toBeNull()
expect(k.org).toBeNull()
await expect(page.getByText(THREAD)).toHaveCount(0)
})
test('the same person returning keeps their own', async ({ page }) => {
await arrive(page, base, 'alice')
await page.waitForTimeout(1500)
const k = await kept(page)
expect(k.who).toBe('alice')
expect(k.threads, 'alice\'s own group chat').toContain(THREAD)
expect(k.org).toBe('alices-org')
})
})