Files
hanzo.ai/e2e/unit/api.spec.ts
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

136 lines
5.2 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 { api, iam } from '@hanzo/app/lib/api'
import { configure, where } from '@hanzo/app/where'
/**
* WHERE THE PLATFORM IS, ASKED AT THE MOMENT OF ASKING.
*
* `e2e/gates/studio-host.spec.ts` proves the studio's turn lands on
* api.hanzo.ai by serving the export as the apex and reading the wire. That is
* the important half and it can only measure ONE host: the gate's page is
* hanzo.ai, so the branch a developer runs — and the branch that made eight
* modules write their own copy of this derivation before it had one home — is
* never exercised there.
*
* So this spec measures the derivation itself, and every case is one of
* `lib/hanzo/api.ts`'s own claims:
*
* the apex an absolute https://api.hanzo.ai, because the gateway
* already admits that origin
* a dev host the page's own origin, because api.hanzo.ai admits an
* origin by allowlist and by a portless https proof, and
* localhost satisfies neither — `next.config`'s `/v1` rewrite
* is what answers there, and only a same-origin caller
* reaches a rewrite
* the prerender no `window` at all, which is the export being written
* an override whatever it names, with no trailing slash, so
* `${api()}/v1/…` cannot come out doubled
*
* A FUNCTION IS THE SUBJECT, NOT A CONSTANT. Every module that got this wrong
* got it wrong the same way: a module-scope `const` evaluated while the static
* export prerenders, where there is no `window`, freezing one answer into the
* bundle for every host it is later served from. Each case below calls `api()`
* after the world it describes exists, which a `const` cannot survive — and
* that is the regression this file exists to fail on.
*/
/** The world a page is open in, for the length of one call. */
function at(hostname: string, origin: string): () => void {
const global = globalThis as { window?: unknown }
const had = 'window' in global
const was = global.window
global.window = { location: { hostname, origin } }
return () => {
if (had) global.window = was
else delete global.window
}
}
/** The override the host states with `configure({ api })`: set, or genuinely absent. */
function override(value: string | undefined): () => void {
const was = where().api
configure({ api: value })
return () => configure({ api: was })
}
test.describe('the API host is derived where the page is open', () => {
test('the apex, its subdomains and hanzo.team address api.hanzo.ai, never themselves', async () => {
const unset = override(undefined)
try {
for (const host of ['hanzo.ai', 'cloud.hanzo.ai', 'dev.hanzo.ai', 'hanzo.team', 'hanzo.bot', 'hanzo-bot.hanzo.app']) {
const restore = at(host, `https://${host}`)
try {
expect(api(), `${host} asks the gateway`).toBe('https://api.hanzo.ai')
// The defect this replaced, stated as an absence: a bare `/v1/…` path
// on the published site is a read of a static host with no /v1 on it.
expect(api()).not.toBe(`https://${host}`)
} finally {
restore()
}
}
} finally {
unset()
}
})
test('a dev host stays on its own origin, so the /v1 rewrite is what answers', async () => {
const unset = override(undefined)
try {
for (const [host, origin] of [
['localhost', 'http://localhost:8084'],
['127.0.0.1', 'http://127.0.0.1:3000'],
// A preview host is not hanzo.ai either: the gateway pins
// `access-control-allow-origin` to the apex exactly, so an absolute
// address from anywhere else fails its preflight.
['hanzo-ai.pages.dev', 'https://hanzo-ai.pages.dev'],
]) {
const restore = at(host, origin)
try {
expect(api(), `${host} keeps its own origin`).toBe(origin)
} finally {
restore()
}
}
} finally {
unset()
}
})
test('the prerender has no window, and answers the published address', async () => {
const unset = override(undefined)
try {
expect('window' in (globalThis as { window?: unknown }), 'the runner is not a browser').toBe(false)
expect(api()).toBe('https://api.hanzo.ai')
} finally {
unset()
}
})
test('an override wins everywhere, and brings no trailing slash with it', async () => {
const set = override('https://api.example.test/')
try {
expect(api(), 'the slash is the caller’s to add').toBe('https://api.example.test')
const restore = at('hanzo.ai', 'https://hanzo.ai')
try {
expect(api(), 'an override outranks the host').toBe('https://api.example.test')
} finally {
restore()
}
} finally {
set()
}
})
test('identity is a second address, and it is not the gateway', async () => {
const was = where().iam
configure({ iam: undefined })
try {
expect(iam()).toBe('https://hanzo.id')
configure({ iam: 'https://lux.id/' })
expect(iam(), 'one setting, one spelling, no trailing slash').toBe('https://lux.id')
} finally {
configure({ iam: was })
}
})
})