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