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

193 lines
7.4 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 { paying } from '@hanzo/app/lib/tier'
import { chooseFree, choseFree, DEFAULT_DESTINATION, picker, safe } from '@hanzo/app/lib/destination'
/**
* EVERY SIGN-IN MEETS THE PLANS, AND A PAID ACCOUNT PASSES STRAIGHT THROUGH.
*
* The callback sends each sign-in to the picker by `picker`, except a shared
* chat, which opens. The picker asks `paying` first: a paid plan in the
* organization the reader works in, or before one is picked in any of theirs,
* sends them on. A read that fails is no plan, so the plans are offered.
*/
function bearer(claims: Record<string, unknown>): string {
return `header.${Buffer.from(JSON.stringify(claims)).toString('base64url')}.signature`
}
const tierOf = (name: string, plan = '') => ({
user: 'gate',
plan,
tier: { name, displayName: name, dailyCreditsCents: 0, maxAgents: 1, unlimitedAgents: false },
balance: { currency: 'usd', prepaidAvailable: 0, creditsRemaining: 0, dailyRemaining: 0, effectiveAvailable: 0 },
})
/**
* A browser signed in with `orgs`, working in `current`, answering each org's
* tier from `tiers`: a tier name (a paid one names the plan it is on), `trial`
* (a tier with no plan), `hang` (no answer), or a status for a failure.
*/
function world(orgs: string[], tiers: Record<string, string | number>, current?: string, signedIn = true) {
const global = globalThis as { window?: unknown; fetch: typeof fetch }
const hadWindow = 'window' in global
const wasWindow = global.window
const wasFetch = global.fetch
const store = new Map<string, string>()
if (signedIn) store.set('hanzo_iam_access_token', bearer({ owner: 'hanzo', orgs: orgs.map((org) => ({ org, role: 'owner' })) }))
if (current) store.set('hanzo_iam_current_org', current)
global.window = {
location: { hostname: 'hanzo.ai', origin: 'https://hanzo.ai' },
localStorage: { getItem: (k: string) => store.get(k) ?? null },
}
const asked: string[] = []
global.fetch = (async (input: string | URL, init?: RequestInit) => {
const url = new URL(String(input))
const org = new Headers(init?.headers).get('X-Org-Id') ?? '-'
asked.push(`${url.pathname} ${org}`)
const t = tiers[org]
if (t === undefined) throw new TypeError('network')
if (typeof t === 'number') return new Response('{}', { status: t })
if (t === 'hang')
return new Promise<Response>((_, reject) => init?.signal?.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError'))))
if (t === 'trial') return new Response(JSON.stringify(tierOf('starter', '')), { status: 200 })
if (t === 'comp') return new Response(JSON.stringify(tierOf('pro', '')), { status: 200 })
if (t === 'enterprise') return new Response(JSON.stringify(tierOf('enterprise', '')), { status: 200 })
return new Response(JSON.stringify(tierOf(t, t === 'free' ? '' : 'max-20x')), { status: 200 })
}) as typeof fetch
return {
asked,
done: () => {
global.fetch = wasFetch
if (hadWindow) global.window = wasWindow
else delete global.window
},
}
}
test('a free account in the organization it works in is not paying, and only that organization is asked', async () => {
const w = world(['acme', 'side'], { acme: 'free', side: 'pro' }, 'acme')
try {
expect(await paying()).toBe(false)
expect(w.asked).toEqual(['/v1/billing/tier acme'])
} finally {
w.done()
}
})
test('a paid plan in the organization it works in is paying', async () => {
const w = world(['acme'], { acme: 'pro' })
try {
expect(await paying()).toBe(true)
expect(w.asked).toEqual(['/v1/billing/tier acme'])
} finally {
w.done()
}
})
test('before an organization is picked, a paid plan in any of them is paying', async () => {
const w = world(['home', 'webby', 'guard'], { home: 'free', webby: 'pro', guard: 500 })
try {
expect(await paying()).toBe(true)
expect(w.asked.sort()).toEqual(['/v1/billing/tier guard', '/v1/billing/tier home', '/v1/billing/tier webby'])
} finally {
w.done()
}
})
test('the platform org’s enterprise tier is paying', async () => {
const w = world(['admin'], { admin: 'enterprise' })
try {
expect(await paying()).toBe(true)
} finally {
w.done()
}
})
test('a plan recorded outside checkout, served the pro tier with no plan named, is paying', async () => {
const w = world(['acme'], { acme: 'comp' })
try {
expect(await paying()).toBe(true)
} finally {
w.done()
}
})
test('a tier with no plan behind it, as a trial nobody paid for, is not paying', async () => {
const w = world(['acme'], { acme: 'trial' })
try {
expect(await paying()).toBe(false)
} finally {
w.done()
}
})
test('billing that does not answer is no plan within five seconds', async () => {
const w = world(['acme'], { acme: 'hang' })
try {
const t0 = Date.now()
expect(await paying()).toBe(false)
expect(Date.now() - t0).toBeLessThan(7000)
} finally {
w.done()
}
})
test('a tier that cannot be read is no plan, so the plans are offered', async () => {
const w = world(['acme', 'other'], { acme: 502 })
try {
expect(await paying()).toBe(false)
} finally {
w.done()
}
})
test('nobody signed in is not paying and nothing is asked', async () => {
const w = world([], {}, undefined, false)
try {
expect(await paying()).toBe(false)
expect(w.asked).toEqual([])
} finally {
w.done()
}
})
test('every sign-in goes to the picker on its way, and a shared chat opens', () => {
expect(picker('/chat')).toBe('/onboarding?next=%2Fchat')
expect(picker('/')).toBe('/onboarding?next=%2F')
expect(picker('/pay/confirm/card?plan=dev')).toBe(`/onboarding?next=${encodeURIComponent('/pay/confirm/card?plan=dev')}`)
expect(picker('/chat/shared')).toBe('/chat/shared')
expect(picker('/chat/shared?s=abc')).toBe('/chat/shared?s=abc')
expect(picker('/chat/shared/')).toBe('/chat/shared/')
expect(picker('/onboarding?next=%2Fdev')).toBe('/onboarding?next=%2Fdev')
})
test('an address the browser would read as another site is not where a sign-in goes', () => {
// The parser drops a tab or a newline and reads a backslash as a slash, so each
// of these is `//evil.example` to a browser.
for (const bad of ['/\t/evil.example', '/\n/evil.example', '/\t\\evil.example', '/\r\n/evil.example', '/%09/evil.example'.replace('%09', '\t')])
expect(safe(bad), JSON.stringify(bad)).toBe(DEFAULT_DESTINATION)
expect(picker(safe('/\t/evil.example'))).toBe(`/onboarding?next=${encodeURIComponent(DEFAULT_DESTINATION)}`)
// What a person wrote into the address survives.
expect(safe('/chat?q=build a shift board')).toBe('/chat?q=build a shift board')
expect(safe('/chat/shared?s=abc#t')).toBe('/chat/shared?s=abc#t')
})
test('Free chosen before signing in skips the picker for that one sign-in only', () => {
const global = globalThis as { window?: unknown }
const had = 'window' in global
const was = global.window
const kept = new Map<string, string>()
global.window = {
sessionStorage: { getItem: (k: string) => kept.get(k) ?? null, setItem: (k: string, v: string) => void kept.set(k, v), removeItem: (k: string) => void kept.delete(k) },
}
try {
expect(choseFree(), 'nothing chosen').toBe(false)
chooseFree()
expect(choseFree(), 'Free was chosen').toBe(true)
expect(choseFree(), 'read once: the next sign-in asks again').toBe(false)
} finally {
if (had) global.window = was
else delete global.window
}
})