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