Files
hanzo.ai/e2e/gates/edge.spec.ts
487b108e12 The edge serves each conversation under /chat with Chat's page, and the app's own data files as they are
lib/edge `APP` answers every one segment under /chat but `shared` with /chat,
as every path under /dev with /dev. hanzo-ai-data, above dev-on-hanzo-ai (300)
and chat-on-hanzo-ai (200), serves the export's .txt and .md files under both
as they are, so Next's segment data is never answered with the page. answer()
follows the routes' priorities, and a gate reads every such file the export
wrote. scripts/edge.mjs renders both into universe (4a33f335a).







Co-authored-by: Hanzo Dev <[email protected]>
2026-10-16 15:05:23 -07:00

136 lines
6.5 KiB
TypeScript

import { test, expect } from '@playwright/test'
import { execFileSync } from 'node:child_process'
import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { MODULES } from '../../lib/go-modules'
import { APP, SITE, answer, moved, shelves } from '../../lib/edge'
import { redirects, forward } from '../../lib/moved'
import { HOSTED, SHELVES } from '../../lib/publish'
import { OUT, pages } from './export'
/**
* The edge's rules (lib/edge, served by hanzoai/universe) against the export
* they front: a retired address reaches a page in one hop, a page answers at
* one address, and nothing the site serves is redirected away.
*/
/** The retired addresses, read against the model pages this export wrote (lib/moved). */
const REDIRECTS = redirects()
const shipped = () => new Set(pages().map(({ route }) => route))
const url = (to: string) => (to.startsWith('/') ? SITE + to : to)
const where = (u: string) => {
const a = answer(u)
return 'location' in a ? `301 ${a.location}` : `serves ${a.path}`
}
test('a retired address ships no page, unless it is a Go import path', () => {
const live = shipped()
const modules = new Set<string>(MODULES.map((m) => `/${m}`))
const wrong = Object.keys(REDIRECTS).filter((from) => live.has(from) !== modules.has(from))
expect(wrong, 'a page and a redirect at one address, or a Go import path with no page for go-get').toEqual([])
})
test('every retired address reaches a page in one hop, query kept', () => {
const live = shipped()
const broken: string[] = []
for (const [from, to] of Object.entries(REDIRECTS)) {
if (to.startsWith('/') && (!live.has(to) || REDIRECTS[to])) broken.push(`${from} -> ${to}: not a page`)
const want = url(to)
for (const [asked, then] of [
[SITE + from, want],
[`${SITE}${from}/`, want],
[`${SITE}${from}.html`, want],
[`https://www.hanzo.ai${from}`, want],
[`${SITE}${from}?utm_source=x`, `${want}?utm_source=x`],
]) {
if (where(asked) !== `301 ${then}`) broken.push(`${asked}: ${where(asked)}, want 301 ${then}`)
}
}
expect(broken).toEqual([])
})
test('a shelf keeps exactly the pages published under it and sends the rest on', () => {
const live = [...shipped()]
for (const { from, to, keep } of SHELVES) {
expect(live.filter((r) => r.startsWith(from)).sort(), `published under ${from}`).toEqual([...keep].sort())
for (const k of keep) expect(where(SITE + k), `${k} is served`).toBe(`serves ${k}`)
expect(where(`${SITE}${from}a/b/`)).toBe(`301 ${forward(`${from}a/b`)}`)
expect(where(`${SITE}${from}a/b?x=1`)).toBe(`301 ${to}a/b?x=1`)
}
})
test('every page answers at one address', () => {
const broken: string[] = []
for (const route of shipped()) {
if (HOSTED[route]) continue
if (REDIRECTS[route]) {
if (where(`${SITE}${route}?go-get=1`) !== `serves ${route}`) broken.push(`${route}?go-get=1: ${where(`${SITE}${route}?go-get=1`)}`)
continue
}
const self = SITE + route
const checks: [string, string][] = [
[self, `serves ${route}`],
[`${self}?utm_source=x&gclid=y`, `serves ${route}`],
[`https://www.hanzo.ai${route}`, `301 ${self}`],
]
if (route !== '/') {
checks.push([`${self}/`, `301 ${self}`], [`${self}.html`, `301 ${self}`], [`${self}/index.html`, `301 ${self}`])
}
for (const [asked, want] of checks) if (where(asked) !== want) broken.push(`${asked}: ${where(asked)}, want ${want}`)
}
expect(where(`${SITE}/index.html`)).toBe(`301 ${SITE}/`)
expect(broken).toEqual([])
})
// /chat and /dev answer every path under them with their page; the files the
// export wrote under them are not paths of the app, and are served as they are.
test('an app root\'s own files are served as they are, never as its page', () => {
const files = APP.flatMap(({ replacement: root }) =>
readdirSync(join(OUT, root.slice(1)), { recursive: true, withFileTypes: true })
.filter((f) => f.isFile() && /\.(md|txt)$/.test(f.name))
.map((f) => `${join(f.parentPath, f.name).slice(OUT.length)}`),
)
expect(files.filter((f) => f.includes('__next.')).length, 'the export wrote segment data under /chat and /dev').toBeGreaterThan(4)
const wrong = files.filter((f) => where(SITE + f) !== `serves ${f}`).map((f) => `${f}: ${where(SITE + f)}`)
expect(wrong).toEqual([])
// And a path of the app is still its page.
expect(where(`${SITE}/chat/1790205245190697166-000014`)).toBe('serves /chat')
expect(where(`${SITE}/dev/run/sess_${'a'.repeat(32)}`)).toBe('serves /dev')
})
test('the not-found page answers 404 by its own name too', () => {
for (const name of ['/404', '/404.html', '/_not-found', '/_not-found.html']) {
const a = answer(SITE + name)
expect('path' in a && !shipped().has(a.path), `${name} must reach a path the export does not hold`).toBe(true)
}
})
test('the rules are names the cluster accepts', () => {
const names = [...moved(), ...shelves()].map((r) => r.name)
expect(names.filter((n) => !/^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$/.test(n))).toEqual([])
expect(names.filter((n, i) => names.indexOf(n) !== i)).toEqual([])
})
// The edge's file, rendered: it reads the export's model pages (lib/moved), so it is a gate.
test('scripts/edge.mjs renders the headers and the routes that send them', () => {
const dir = mkdtempSync(join(tmpdir(), 'frame-'))
const file = join(dir, 'static-sites.yaml')
const block = '# BEGIN hanzo.ai edge\n# END hanzo.ai edge\n'
writeFileSync(file, `middlewares:\n${block}routes:\n${block}`)
try {
execFileSync(process.execPath, ['scripts/edge.mjs', file])
const out = readFileSync(file, 'utf8')
expect(out).toContain(`- name: hanzo-ai-frame\n spec:\n headers:\n customResponseHeaders:\n X-Frame-Options: 'DENY'\n Content-Security-Policy: 'frame-ancestors ''none'''`)
expect(out).toContain("PathRegexp(`^/(login|signup|forget|join)(/|$)`)")
const route = (name: string) => out.slice(out.indexOf(`- name: ${name}\n`)).split('\n- name: ')[0]
expect(route('hanzo-ai-signin')).toContain('- name: hanzo-ai-frame\n - name: hanzo-ai-edge')
expect(route('hanzo-ai-auth')).toContain('- name: hanzo-ai-noreferrer\n - name: hanzo-ai-frame\n - name: hanzo-ai-edge')
// The roots' files outrank the roots' own routes (dev-on-hanzo-ai at 300).
expect(route('hanzo-ai-data')).toContain('priority: 310')
expect(route('hanzo-ai-data')).not.toContain('hanzo-ai-edge')
} finally {
rmSync(dir, { recursive: true, force: true })
}
})