Files
zeekay 6c65e3e941 @hanzo/app 0.2.0: the app SDK, renamed from @hanzo/rooms
Every import, the catalog pin and the docs name @hanzo/app and
github.com/hanzoai/app. lib/rooms.ts is lib/app.ts and
components/rooms.tsx is components/app.tsx.
2026-10-22 19:07:20 -07:00

213 lines
9.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect } from '@playwright/test'
import { apex, app, away, CHAT, dev, GET, guest, place, room, ROOMS, settings, SIGNIN, talk, team, WEB, WORKSPACE } from '@hanzo/app/lib/host'
import { landing, safe } from '@hanzo/app/lib/destination'
import { roomHref } from '@hanzo/app/rooms'
import { SECTIONS } from '@hanzo/build/route'
import { MOVED } from '../../lib/publish'
/**
* ONE EXPORT, TWO PRODUCTS, SPLIT BY HOST.
*
* hanzo.ai is the app — `/` once signed in, /dev, and /chat; hanzo.team is the workspace and keeps
* every room. `lib/host.ts` says which host is which and where a room asked for
* on the apex belongs, and a sign-in with nowhere to return lands on the host's
* own front room. `e2e/gates/host.spec.ts` drives the same answers in a browser
* under the real hostnames; this holds the mapping itself.
*/
test.describe('the app has one address', () => {
const ID = 'sess_0123456789abcdef0123456789abcdef'
test('Dev is `/dev`, and the path under it is what it shows; bare, it is New', () => {
expect(app('')).toBe('/dev')
expect(app(ID)).toBe(`/dev/run/${ID}`)
expect(app('hanzo/circle')).toBe('/dev/projects/hanzo/circle')
expect(app('my-site')).toBe('/dev/projects/my-site')
})
test('every kind of app address survives the path, both ways, and carries no query', () => {
// New, a run, a project, a screen, a place the builder files under Settings, a Customize tab, the plans, a site.
for (const at of ['', ID, 'hanzo/circle', '-/projects', '-/settings/machines', '-/customize', '-/customize/connectors', '-/plans', 'my-site']) {
const url = new URL(app(at), 'https://hanzo.ai')
expect(url.search, at || 'New').toBe('')
expect(place(url.pathname), at || 'New').toEqual({ mode: 'dev', at })
}
expect(place('/')).toBeNull()
expect(place('/developers')).toBeNull()
})
test('Settings is one tree at /settings, and no builder section is written under /dev', () => {
expect(app('-/settings')).toBe('/settings')
expect(app('-/settings/usage')).toBe('/settings/organization/usage')
expect(app('-/settings/billing')).toBe('/settings/organization/billing')
expect(app('-/settings/members')).toBe('/settings/organization/members')
for (const s of SECTIONS) expect(app(`-/settings/${s}`), s).not.toMatch(/^\/dev\/settings/)
for (const p of ['/settings', '/settings/organization/usage']) expect(place(p), p).toBeNull()
})
test('the edge answers every address once written under /dev/settings at the page Settings names for it', () => {
const rows = Object.entries(MOVED).filter(([from]) => from === '/dev/settings' || from.startsWith('/dev/settings/'))
for (const s of SECTIONS.filter((s) => app(`-/settings/${s}`).startsWith('/settings'))) expect(MOVED[`/dev/settings/${s}`], s).toBeDefined()
for (const [from, to] of rows) expect(to, from).toBe(settings(from === '/dev/settings' ? null : from.split('/').pop()))
})
test('Chat is `/chat`, a new conversation, and `/chat/<id>` one of the reader’s', () => {
expect(CHAT).toBe('/chat')
expect(talk()).toBe('/chat')
expect(talk(null)).toBe('/chat')
const id = '1790205245190697166-000014'
expect(talk(id)).toBe(`/chat/${id}`)
expect(place(talk(id))).toEqual({ mode: 'chat', thread: id })
expect(place('/chat')).toEqual({ mode: 'chat', thread: null })
// An id is carried as one segment, never as more of the address.
expect(talk('a/../../dev')).toBe('/chat/a%2F..%2F..%2Fdev')
expect(place(talk('a/../../dev'))).toEqual({ mode: 'chat', thread: 'a/../../dev' })
// A chat shared by link is a page of its own, not a conversation.
expect(place('/chat/shared')).toBeNull()
})
test('it is /dev, which every Dev link opens', () => {
expect(dev()).toBe('/dev')
expect(dev()).toBe(dev())
expect(dev(null)).toBe(dev())
expect(dev('')).toBe(dev())
})
test('a run, a project or a site is carried as its path when it is exactly one', () => {
expect(dev(ID)).toBe(`${dev()}/run/${ID}`)
expect(dev('hanzo/circle')).toBe(`${dev()}/projects/hanzo/circle`)
expect(dev('my-site')).toBe(`${dev()}/projects/my-site`)
expect(dev('a'.repeat(40))).toBe(`${dev()}/projects/${'a'.repeat(40)}`)
})
test('anything else opens the builder, and never another address', () => {
for (const ref of [
'a'.repeat(41),
'My-Site',
'-lead',
'trail-',
'sess_zz',
`${ID}0`,
'../x',
'//evil.com',
'https://evil.com',
'%2F%2Fevil.com',
'x?y=1',
'x#y',
'@evil.com',
'javascript:alert(1)',
])
expect(dev(ref), ref).toBe(dev())
})
})
test.describe('which host is which', () => {
test('the apex is hanzo.ai and www.hanzo.ai, and nothing that merely contains the name', () => {
expect(apex('hanzo.ai')).toBe(true)
expect(apex('www.hanzo.ai')).toBe(true)
for (const other of [
'hanzo.team',
'www.hanzo.team',
'localhost',
'127.0.0.1',
'cloud.hanzo.ai',
'api.hanzo.ai',
'hanzo-ai.hanzo.app',
'hanzo.ai.example.com',
'nothanzo.ai',
'',
])
expect(apex(other), other).toBe(false)
})
test('the workspace host is hanzo.team and www.hanzo.team', () => {
expect(team('hanzo.team')).toBe(true)
expect(team('www.hanzo.team')).toBe(true)
for (const other of ['hanzo.ai', 'www.hanzo.ai', 'localhost', '127.0.0.1', 'hanzo.team.example.com', ''])
expect(team(other), other).toBe(false)
})
})
test.describe('where a sign-in can finish', () => {
test('hanzo.app, every <slug>.hanzo.app alias and hanzo.chat are guests, and hand sign-in to hanzo.ai', () => {
expect(SIGNIN).toBe('https://hanzo.ai')
for (const host of ['hanzo.app', 'cloud-hanzo-ai.hanzo.app', 'hanzo-bot.hanzo.app', 'hanzo-ai.hanzo.app', 'hanzo.chat'])
expect(guest(host), host).toBe(true)
for (const own of ['hanzo.ai', 'cloud.hanzo.ai', 'hanzo.bot', 'hanzo.team', 'localhost', 'hanzo.app.example.com', 'nothanzo.app', 'chat.hanzo.chat', 'hanzo.chat.example.com', ''])
expect(guest(own), own).toBe(false)
})
test('on a guest every address is the same address on hanzo.ai', () => {
expect(away('hanzo-bot.hanzo.app', '/chat?q=hi')).toBe('https://hanzo.ai/chat?q=hi')
expect(away('cloud-hanzo-ai.hanzo.app', '/board')).toBe('https://hanzo.ai/board')
expect(away('hanzo.app', '/chat')).toBe('https://hanzo.ai/chat')
})
test('hanzo.app is where the app is got, and the app opens at hanzo.ai/chat', () => {
expect(GET).toBe('https://hanzo.app')
expect(WEB).toBe('https://hanzo.ai/chat')
})
})
test.describe('where a room belongs', () => {
test('the workspace keeps nine rooms, and the apex keeps the app', () => {
expect([...ROOMS].sort()).toEqual(
['/board', '/bots', '/cal', '/contacts', '/drive', '/home', '/inbox', '/meet', '/work'].sort(),
)
for (const kept of ['/chat', '/dev', '/settings', '/settings/organization/members']) expect(room(kept), kept).toBe(false)
})
test('an address names a room by its path, whatever it carries', () => {
expect(room('/board')).toBe(true)
expect(room('/board/')).toBe(true)
expect(room('/board?view=list')).toBe(true)
expect(room('/home#top')).toBe(true)
// A path that begins like a room is a different page.
expect(room('/boards')).toBe(false)
expect(room('/')).toBe(false)
expect(room('/pricing')).toBe(false)
expect(room('https://hanzo.team/board')).toBe(false)
})
test('on the apex a room is the same path and query on hanzo.team', () => {
expect(WORKSPACE).toBe('https://hanzo.team')
expect(away('hanzo.ai', '/board')).toBe('https://hanzo.team/board')
expect(away('www.hanzo.ai', '/inbox?from=mail')).toBe('https://hanzo.team/inbox?from=mail')
expect(away('hanzo.ai', roomHref('team:acme/r1'))).toBe('https://hanzo.team/?room=team%3Aacme%2Fr1')
expect(away('hanzo.ai', '/home')).toBe('https://hanzo.team/')
expect(away('hanzo.ai', '/board?project=x')).toBe('https://hanzo.team/board?project=x')
expect(away('hanzo.ai', '/homework')).toBe(null)
expect(away('hanzo.ai', '/chat')).toBeNull()
expect(away('hanzo.ai', '/dev?run=1')).toBeNull()
expect(away('hanzo.ai', '/pricing')).toBeNull()
})
test('every other host keeps the room where it is', () => {
for (const host of ['hanzo.team', 'localhost', '127.0.0.1', 'cloud.hanzo.ai'])
expect(away(host, '/board'), host).toBeNull()
})
test('a room key opens on Home, carried the way Home reads it', () => {
const href = roomHref('team:acme/r1')
expect(href).toBe('/home?room=team%3Aacme%2Fr1')
expect(new URL(href, 'https://hanzo.team').searchParams.get('room')).toBe('team:acme/r1')
})
})
test.describe('where a sign-in lands with nowhere to return', () => {
test('the apex lands on the app at `/`, every other host on Home', () => {
expect(landing('hanzo.ai')).toBe('/')
expect(landing('www.hanzo.ai')).toBe('/')
expect(landing('hanzo.team')).toBe('/home')
expect(landing('localhost')).toBe('/home')
})
test('a refused destination falls to the landing it is given, and a good one is kept', () => {
expect(safe(null, '/home')).toBe('/home')
expect(safe('//evil.example', '/home')).toBe('/home')
expect(safe('/login', '/home')).toBe('/home')
expect(safe('/pricing', '/home')).toBe('/pricing')
expect(safe('/board?x=1', '/chat')).toBe('/board?x=1')
})
})