Files
deepseek-harness/packages/host/frontend-static/tests/frontend-static.spec.ts
T
imccyu 50bfb00985 feat(web): single-build preview page and its acceptance e2e
One Vite build emits dist/index.html and dist/preview.html sharing every
chunk; the only difference is one prepended bootstrap entry whose module
connects the worker host, so the page from the stock entry onward is the
served startup chain verbatim. The dist moves to a relative base so the
preview mounts under any static directory, and the served form anchors
deep SPA-fallback paths with a rendered <base href="/">. The preview-boot
e2e serves the real built pages, packs the VFS image when absent, and
holds the boot line's lowering contract, the interactive hero, and a
clean page-error channel in headless Chromium.
2026-08-21 20:35:33 +08:00

177 lines
7.3 KiB
TypeScript

/**
* REAL-composition coverage: a test-only cordis.yml booted through the
* vendored Loader mounts the webserver and frontend-static rows, and every
* assertion observes the served HTTP surface — asset serving, explicit index
* entry points with index taps, 404 misses, traversal rejection, 405 on non-
* GET/HEAD, and seat release on fiber disposal (HMR safety).
*/
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import HttpServer from '@deepseek-ai/dsh-host-webserver'
import * as FrontendStatic from '../src/index.ts'
let root: string | undefined
let context: Context | undefined
afterEach(async () => {
await context?.fiber.dispose()
context = undefined
if (root !== undefined) await rm(root, { recursive: true, force: true })
root = undefined
})
/** Write a dist fixture and a two-row cordis.yml, then boot it through the real Loader. */
async function loadComposition(): Promise<Context> {
root = await mkdtemp(join(tmpdir(), 'dsh-frontend-static-'))
const dist = join(root, 'dist')
await mkdir(dist)
const distIndex = join(dist, 'index.html')
await writeFile(distIndex, '<head></head><body>shell</body>')
await writeFile(join(dist, 'app.js'), 'export {}')
await writeFile(join(dist, 'blob.bin'), 'BLOB')
await writeFile(join(dist, 'manifest.webmanifest'), '{}')
await mkdir(join(dist, 'empty'))
const configPath = join(root, 'cordis.yml')
await writeFile(configPath, [
"- name: '@deepseek-ai/dsh-host-webserver'",
' config:',
" host: '127.0.0.1'",
' port: 0',
'- id: frontend',
" name: '@deepseek-ai/dsh-host-frontend-static'",
' config:',
` distIndex: '${distIndex}'`,
'',
].join('\n'))
context = new Context()
context.baseUrl = pathToFileURL(root).href + '/'
await context.plugin(Loader)
context.loader.builtins.include = Include
const modules = new Map<string, unknown>([
['@deepseek-ai/dsh-host-webserver', HttpServer],
['@deepseek-ai/dsh-host-frontend-static', FrontendStatic],
])
context.loader.internal = {
version: 'v2',
async import(specifier: string) {
if (!modules.has(specifier)) throw new Error(`unexpected Loader import: ${specifier}`)
return modules.get(specifier)
},
} as unknown as NonNullable<typeof context.loader.internal>
await context.loader.create({
name: 'cordis:include',
config: { path: pathToFileURL(configPath).href },
})
await context.loader.await()
return context
}
/** GET (by default) one path against the running server; returns status, content-type, and a body prefix. */
async function request(port: number, path: string, init?: RequestInit): Promise<{ status: number; type: string | null; body: string }> {
const response = await fetch(`http://127.0.0.1:${String(port)}${path}`, init)
return {
status: response.status,
type: response.headers.get('content-type'),
// Window wide enough to keep index body markers visible behind the
// served prelude (base anchor + injection rows + boot-readiness tail).
body: (await response.text()).slice(0, 200),
}
}
describe('real Loader composition', () => {
it('serves explicit index entries and files while preserving HTTP error semantics', { timeout: 60_000 }, async () => {
const loaded = await loadComposition()
const unloaded = [...loaded.loader.entries()]
.filter(entry => entry.fiber === undefined && !entry.disabled)
.map(entry => entry.options.name)
expect(unloaded).toEqual([])
const server = loaded.webServer
const port = server.port
// Real assets with their MIME types; a live rebuild is served on the next read.
expect(await request(port, '/app.js')).toMatchObject({ status: 200, type: 'text/javascript; charset=utf-8', body: 'export {}' })
expect(await request(port, '/manifest.webmanifest')).toMatchObject({
status: 200,
type: 'application/manifest+json',
body: '{}',
})
expect(await request(port, '/app.js', { method: 'HEAD' })).toEqual({
status: 200,
type: 'text/javascript; charset=utf-8',
body: '',
})
await writeFile(join(root!, 'dist', 'app.js'), 'export const rebuilt = true')
expect(await request(port, '/app.js')).toMatchObject({ status: 200, body: 'export const rebuilt = true' })
// Unknown extension ships as octet-stream.
expect(await request(port, '/blob.bin')).toMatchObject({ status: 200, type: 'application/octet-stream', body: 'BLOB' })
// Only the root and index path render index.html through registered taps.
const untap = server.tapIndex(html => html.replace('<head>', '<head><script>window.__T__=1</script>'))
for (const path of ['/', '/index.html', '/?fixture']) {
const got = await request(port, path)
expect(got.status).toBe(200)
expect(got.type).toBe('text/html; charset=utf-8')
expect(got.body).toContain('__T__')
expect(got.body).toContain('shell')
}
expect(await request(port, '/', { method: 'HEAD' })).toEqual({
status: 200,
type: 'text/html; charset=utf-8',
body: '',
})
untap()
expect((await request(port, '/')).body).not.toContain('__T__')
// A missing configured index follows the same empty-404 contract for both
// of its public entry paths and for both supported methods.
await rm(join(root!, 'dist', 'index.html'))
for (const path of ['/', '/index.html']) {
const get = await request(port, path)
const head = await request(port, path, { method: 'HEAD' })
expect(get).toEqual({ status: 404, type: null, body: '' })
expect(head).toEqual(get)
}
// Ordinary unknown paths and static-resource misses are empty 404s for
// both GET and HEAD; neither class can be mistaken for the HTML shell.
const ordinaryMisses = ['/no/such/route', '/api/no/such/route', '/empty', '/app.js/child']
const assetMisses = [
'/missing.js',
'/missing.css',
'/missing.mjs',
'/missing.js.map',
'/missing.webmanifest',
'/missing.manifest',
]
for (const path of [...ordinaryMisses, ...assetMisses]) {
const get = await request(port, path)
const head = await request(port, path, { method: 'HEAD' })
expect(get).toEqual({ status: 404, type: null, body: '' })
expect(head).toEqual(get)
}
// Traversal outside the dist root is 403, non-GET/HEAD is 405, and a
// malformed filesystem target still reaches the webserver's 400 guard.
expect((await request(port, '/..%2f..%2fetc%2fpasswd')).status).toBe(403)
expect((await request(port, '/app.js', { method: 'POST' })).status).toBe(405)
expect((await request(port, '/bad%00path')).status).toBe(400)
// HMR safety: disposing the frontend row releases the fallback seat (the
// unclaimed webserver answers 404) and the seat is claimable again.
const frontendEntry = [...loaded.loader.entries()].find(e => e.options.id === 'frontend')
expect(frontendEntry).toBeDefined()
await frontendEntry!.fiber?.dispose()
expect((await request(port, '/no/such/route')).status).toBe(404)
expect(() => server.registerFallback(() => {})).not.toThrow()
})
})