import type http from 'node:http'; import express from 'express'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { isStaticSpaFallbackRequest, resolveDataDir, startServer, type StartServerResult, } from '../src/server.js'; import { isLocalSameOrigin } from '../src/origin-validation.js'; import { registerDesignSystemRoutes } from '../src/routes/design-systems.js'; import { registerStaticResourceRoutes } from '../src/routes/static-resource.js'; let server: http.Server; let baseUrl: string; let shutdown: (() => Promise | void) | undefined; let routeInventory: StartServerResult['routeInventory']; beforeAll(async () => { const started = (await startServer({ port: 0, returnServer: true })) as StartServerResult; baseUrl = started.url; server = started.server; shutdown = started.shutdown; routeInventory = started.routeInventory; }); afterAll(async () => { await Promise.resolve(shutdown?.()); await new Promise((resolve) => server.close(() => resolve())); }); describe('server route inventory', () => { it('records bootstrap and migrated domain routes in registration order', () => { const routeKeys = routeInventory.map((route) => `${route.method} ${route.path}`); const fallbackIndex = routeKeys.indexOf('GET /*splat'); const automationRouteKeys = [ 'GET /api/automation-source-packets', 'POST /api/automation-ingestions', 'GET /api/automation-source-packets/:id', 'GET /api/automation-proposals', 'POST /api/automation-proposals', 'GET /api/automation-proposals/:id', 'POST /api/automation-proposals/:id/apply', 'POST /api/automation-proposals/:id/reject', ]; const velaRouteKeys = [ 'GET /api/amr/models', 'GET /api/integrations/vela/status', 'ALL /api/integrations/vela/api-proxy/*splat', 'POST /api/integrations/vela/login', 'POST /api/integrations/vela/login/cancel', 'POST /api/integrations/vela/analytics-entry', 'POST /api/integrations/vela/logout', ]; const genuiRouteKeys = [ 'GET /api/runs/:runId/genui', 'GET /api/projects/:projectId/genui', 'POST /api/runs/:runId/genui/:surfaceId/respond', 'POST /api/projects/:projectId/genui/:surfaceId/revoke', 'POST /api/projects/:projectId/genui/prefill', 'GET /api/runs/:runId/genui/:surfaceId', 'GET /api/runs/:runId/devloop-iterations', 'POST /api/runs/:runId/replay', ]; const runRouteKeys = [ 'POST /api/runs', 'GET /api/runs', 'GET /api/runs/:id/result-package', 'GET /api/runs/:id', 'GET /api/runs/:id/events', 'GET /api/runs/:id/agui', 'POST /api/runs/:id/cancel', 'POST /api/chat', ]; const pluginEventRouteKeys = [ 'GET /api/plugins/events/snapshot', 'GET /api/plugins/events/stats', 'POST /api/plugins/events/purge', 'GET /api/plugins/events', ]; const pluginLifecycleRouteKeys = [ 'GET /api/plugins', 'GET /api/plugins/:id', 'POST /api/plugins/upload-zip', 'POST /api/plugins/upload-folder', 'POST /api/plugins/install', 'POST /api/plugins/:id/uninstall', 'POST /api/plugins/:id/upgrade', 'POST /api/plugins/:id/apply', 'POST /api/plugins/:id/duplicate-project', 'POST /api/plugins/:id/share-project', 'POST /api/plugins/:id/doctor', 'POST /api/plugins/:id/trust', 'GET /api/plugins/stats', 'GET /api/applied-plugins/:snapshotId', 'GET /api/applied-plugins/:snapshotId/canon', 'GET /api/applied-plugins', 'GET /api/projects/:projectId/applied-plugins', 'POST /api/applied-plugins/export', 'POST /api/applied-plugins/prune', ]; const pluginAssetRouteKeys = [ 'GET /api/plugins/:id/preview', 'GET /api/plugins/:id/example/:name', 'GET /api/plugins/:id/asset/*splat', 'GET /api/asset-cache', ]; const marketplaceRouteKeys = [ 'GET /api/marketplaces', 'POST /api/marketplaces', 'GET /api/marketplaces/:id', 'DELETE /api/marketplaces/:id', 'POST /api/marketplaces/:id/refresh', 'POST /api/marketplaces/:id/trust', 'GET /api/marketplaces/:id/plugins', ]; const projectPluginRouteKeys = [ 'POST /api/projects/:id/plugins/install-folder', 'POST /api/projects/:id/plugins/publish-github', 'GET /api/projects/:id/plugin-candidates', 'POST /api/projects/:id/plugin-candidates/:candidateId/dismiss', 'POST /api/projects/:id/plugin-candidates/:candidateId/draft', 'POST /api/projects/:id/plugin-candidates/:candidateId/share-tasks', 'POST /api/projects/:id/plugins/contribute-open-design', 'POST /api/projects/:id/plugins/share-tasks', 'POST /api/plugins/share-tasks/:id/wait', ]; const liveArtifactRouteKeys = [ 'GET /api/live-artifacts', 'OPTIONS /api/live-artifacts/:artifactId/preview', 'GET /api/live-artifacts/:artifactId/preview', 'GET /api/live-artifacts/:artifactId', 'GET /api/live-artifacts/:artifactId/refreshes', 'POST /api/tools/live-artifacts/create', 'GET /api/tools/live-artifacts/list', 'POST /api/tools/live-artifacts/update', 'POST /api/tools/live-artifacts/refresh', 'PATCH /api/live-artifacts/:artifactId', 'DELETE /api/live-artifacts/:artifactId', 'OPTIONS /api/live-artifacts/:artifactId/refresh', 'POST /api/live-artifacts/:artifactId/refresh', ]; const deployRouteKeys = [ 'GET /api/deploy/config', 'PUT /api/deploy/config', 'GET /api/deploy/cloudflare-pages/zones', 'GET /api/projects/:id/deployments', 'POST /api/projects/:id/deploy', 'POST /api/projects/:id/deploy/preflight', ]; const deploymentCheckRouteKeys = [ 'POST /api/projects/:id/deployments/:deploymentId/check-link', ]; const projectFileStringRouteKeys = [ 'GET /api/projects/:id/files', 'GET /api/projects/:id/search', 'GET /api/projects/:id/files/:name/preview', 'POST /api/projects/:id/files', 'DELETE /api/projects/:id/files/:name', ]; const projectArchiveRouteKeys = [ 'GET /api/projects/:id/archive', 'POST /api/projects/:id/archive/batch', ]; const projectTemplateAndArtifactRouteKeys = [ 'GET /api/projects/:id/tabs', 'PUT /api/projects/:id/tabs', 'GET /api/templates', 'GET /api/templates/:id', 'POST /api/templates', 'DELETE /api/templates/:id', 'POST /api/upload', 'POST /api/artifacts/save', 'POST /api/artifacts/lint', 'POST /api/projects/:id/finalize/:provider', 'POST /api/projects/:id/upload', ]; const designSystemRouteKeys = [ 'GET /api/design-systems', 'DELETE /api/design-systems/:id', 'POST /api/design-systems', 'POST /api/design-systems/generation-jobs', 'GET /api/design-systems/generation-jobs/:jobId', 'POST /api/design-systems/:id/revision-jobs', 'POST /api/design-systems/:id/token-contract/rebuild-jobs', 'GET /api/design-systems/:id/revisions', 'PATCH /api/design-systems/:id/revisions/:revisionId', 'GET /api/design-systems/:id', 'GET /api/design-systems/:id/preview', 'GET /api/design-systems/:id/showcase', 'POST /api/design-systems/:id/workspace', 'GET /api/design-systems/:id/files', 'GET /api/design-systems/:id/file', 'PATCH /api/design-systems/:id', 'DELETE /api/design-systems/:id', 'GET /api/craft', 'GET /api/craft/:id', ]; const staticCatalogRouteKeys = [ 'GET /api/skills/:id/example', 'GET /api/skills/:id/assets/*splat', 'GET /api/atoms', 'GET /api/atoms/:id', ]; const mediaConfigRouteKeys = [ 'GET /api/media/models', 'GET /api/media/providers/aihubmix/models', 'GET /api/media/config', 'PUT /api/media/config', 'GET /api/media/providers/elevenlabs/voices', 'GET /api/app-config', 'PUT /api/app-config', 'POST /api/dir-exists', 'GET /api/recent-dirs', 'GET /api/orbit/status', 'POST /api/orbit/run', 'POST /api/system/open-external', 'POST /api/dialog/open-folder', 'POST /api/projects/:id/media/generate', 'POST /api/tools/media/generate', 'POST /api/research/search', 'POST /api/media/tasks/:id/wait', 'GET /api/projects/:id/media/tasks', ]; expect(routeKeys).toEqual(expect.arrayContaining([ 'GET /api/health', 'GET /api/ready', 'GET /api/version', 'GET /api/daemon/status', 'GET /api/skills', 'GET /api/codex-pets', 'GET /api/prompt-templates', 'GET /*splat', ])); expect(routeKeys.filter((key) => automationRouteKeys.includes(key))).toEqual(automationRouteKeys); expect(routeKeys.filter((key) => velaRouteKeys.includes(key))).toEqual(velaRouteKeys); expect(routeKeys.filter((key) => genuiRouteKeys.includes(key))).toEqual(genuiRouteKeys); expect(routeKeys.filter((key) => runRouteKeys.includes(key))).toEqual(runRouteKeys); expect(routeKeys.filter((key) => pluginEventRouteKeys.includes(key))).toEqual(pluginEventRouteKeys); expect(routeKeys.filter((key) => pluginLifecycleRouteKeys.includes(key))).toEqual(pluginLifecycleRouteKeys); expect(routeKeys.filter((key) => pluginAssetRouteKeys.includes(key))).toEqual(pluginAssetRouteKeys); expect(routeKeys.filter((key) => marketplaceRouteKeys.includes(key))).toEqual(marketplaceRouteKeys); expect(routeKeys.filter((key) => projectPluginRouteKeys.includes(key))).toEqual(projectPluginRouteKeys); expect(routeKeys.filter((key) => liveArtifactRouteKeys.includes(key))).toEqual(liveArtifactRouteKeys); expect(routeKeys.filter((key) => deployRouteKeys.includes(key))).toEqual(deployRouteKeys); expect(routeKeys.filter((key) => deploymentCheckRouteKeys.includes(key))).toEqual(deploymentCheckRouteKeys); expect(routeKeys.filter((key) => projectFileStringRouteKeys.includes(key))).toEqual(projectFileStringRouteKeys); expect(routeKeys.filter((key) => projectArchiveRouteKeys.includes(key))).toEqual(projectArchiveRouteKeys); expect(routeKeys.filter((key) => projectTemplateAndArtifactRouteKeys.includes(key))).toEqual(projectTemplateAndArtifactRouteKeys); expect(routeKeys.filter((key) => designSystemRouteKeys.includes(key))).toEqual(designSystemRouteKeys); expect(routeKeys.filter((key) => staticCatalogRouteKeys.includes(key))).toEqual(staticCatalogRouteKeys); expect(routeKeys.filter((key) => mediaConfigRouteKeys.includes(key))).toEqual(mediaConfigRouteKeys); expect(fallbackIndex).toBeGreaterThan(-1); expect(routeKeys.indexOf('GET /api/health')).toBeLessThan(fallbackIndex); expect(routeKeys.indexOf('POST /api/daemon/db/verify')).toBeLessThan(routeKeys.indexOf('GET /api/plugins/events/snapshot')); expect(routeKeys.indexOf('GET /api/plugins')).toBeLessThan(routeKeys.indexOf('GET /api/atoms')); expect(routeKeys.indexOf('GET /api/plugins/:id/preview')).toBeLessThan(fallbackIndex); expect(routeKeys.indexOf('GET /api/automation-source-packets')).toBeLessThan( routeKeys.indexOf('GET /api/skills'), ); expect(routeKeys.indexOf('USE /artifacts')).toBeLessThan(routeKeys.indexOf('USE /frames')); expect(routeKeys.filter((key) => key === 'USE /artifacts')).toHaveLength(1); expect(routeKeys.filter((key) => key === 'USE /frames')).toHaveLength(1); expect(routeKeys.filter((key) => key === 'GET /api/plugins')).toHaveLength(1); expect(routeKeys.filter((key) => key === 'GET /api/atoms')).toHaveLength(1); expect(routeKeys.filter((key) => key === 'GET /api/design-systems/:id')).toHaveLength(1); expect(routeKeys.filter((key) => key === 'GET /api/design-systems/:id/preview')).toHaveLength(1); expect(routeKeys.filter((key) => key === 'POST /api/projects/:id/upload')).toHaveLength(1); expect(routeKeys.filter((key) => key === 'POST /api/runs')).toHaveLength(1); expect(routeKeys.filter((key) => key === 'POST /api/chat')).toHaveLength(1); expect(routeKeys.filter((key) => key === 'POST /api/media/tasks/:id/wait')).toHaveLength(1); expect(routeKeys.filter((key) => key === 'GET /api/marketplaces')).toHaveLength(1); expect(routeKeys.filter((key) => key === 'POST /api/projects/:id/plugins/share-tasks')).toHaveLength(1); for (const [index, key] of routeKeys.entries()) { if (key.includes(' /api/')) { expect(index, `${key} should register before SPA fallback`).toBeLessThan(fallbackIndex); } } expect(routeKeys.filter((key) => key === 'GET /api/skills')).toHaveLength(1); }); }); describe('bootstrap route regressions', () => { it('keeps health, ready, version, and daemon status available', async () => { const [health, ready, version, status] = await Promise.all([ fetch(`${baseUrl}/api/health`), fetch(`${baseUrl}/api/ready`), fetch(`${baseUrl}/api/version`), fetch(`${baseUrl}/api/daemon/status`), ]); expect(health.status).toBe(200); expect(ready.status).toBe(200); expect(version.status).toBe(200); expect(status.status).toBe(200); }); it('keeps lightweight extracted-route responses stable without fixtures', async () => { const [ automationList, automationMissing, velaProxyUnknownPath, genuiRunList, genuiRunSurfaceMissing, devloopIterations, replayMissingSnapshot, ] = await Promise.all([ fetch(`${baseUrl}/api/automation-source-packets`), fetch(`${baseUrl}/api/automation-source-packets/missing-packet`), fetch(`${baseUrl}/api/integrations/vela/api-proxy/not-api-v1`), fetch(`${baseUrl}/api/runs/missing-run/genui`), fetch(`${baseUrl}/api/runs/missing-run/genui/missing-surface`), fetch(`${baseUrl}/api/runs/missing-run/devloop-iterations`), fetch(`${baseUrl}/api/runs/missing-run/replay`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({}), }), ]); expect(automationList.status).toBe(200); expect(await automationList.json()).toEqual({ packets: [] }); expect(automationMissing.status).toBe(404); expect(await automationMissing.json()).toEqual({ error: 'automation source packet not found' }); expect(velaProxyUnknownPath.status).toBe(404); expect(await velaProxyUnknownPath.json()).toEqual({ error: 'unknown_amr_api_proxy_path' }); expect(genuiRunList.status).toBe(200); expect(await genuiRunList.json()).toEqual({ runId: 'missing-run', surfaces: [] }); expect(genuiRunSurfaceMissing.status).toBe(404); expect(await genuiRunSurfaceMissing.json()).toEqual({ error: 'surface not found' }); expect(devloopIterations.status).toBe(200); expect(await devloopIterations.json()).toEqual({ runId: 'missing-run', iterations: [] }); expect(replayMissingSnapshot.status).toBe(400); expect(await replayMissingSnapshot.json()).toEqual({ error: 'snapshotId is required (runs are in-memory; pass the snapshotId returned by /api/plugins/:id/apply)', }); }); it('keeps extracted design-system and template example responses stable', async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'od-route-smoke-')); const designSystemId = 'route-smoke-system'; const templateId = 'route-smoke-template'; const designSystemBody = '# Route Smoke System\n\nA seeded design system.'; const templateDir = path.join(tempRoot, 'design-templates', templateId); const assetPath = path.join(templateDir, 'assets', 'demo.css'); fs.mkdirSync(path.dirname(assetPath), { recursive: true }); fs.writeFileSync( path.join(templateDir, 'SKILL.md'), [ '---', `name: ${templateId}`, 'description: Route smoke template.', '---', '', 'Render the route smoke template.', ].join('\n'), ); fs.writeFileSync( path.join(templateDir, 'example.html'), '
Template Example
', ); fs.writeFileSync(assetPath, 'main { color: rgb(1 2 3); }'); const app = express(); app.use(express.json({ limit: '4mb' })); let smokeServer: http.Server | undefined; const paths = { ARTIFACTS_DIR: path.join(tempRoot, 'artifacts'), BRANDS_DIR: path.join(tempRoot, 'brands'), BUNDLED_PETS_DIR: path.join(tempRoot, 'pets'), CRAFT_DIR: path.join(tempRoot, 'craft'), DESIGN_SYSTEMS_DIR: path.join(tempRoot, 'design-systems'), DESIGN_TEMPLATES_DIR: path.join(tempRoot, 'design-templates'), LIBRARY_DIR: path.join(tempRoot, 'library'), OD_BIN: path.join(tempRoot, 'od'), PROJECT_ROOT: tempRoot, PROJECTS_DIR: path.join(tempRoot, 'projects'), PROMPT_TEMPLATES_DIR: path.join(tempRoot, 'prompt-templates'), RUNTIME_DATA_DIR: path.join(tempRoot, 'data'), RUNTIME_DATA_DIR_CANONICAL: path.join(tempRoot, 'data'), SKILLS_DIR: path.join(tempRoot, 'skills'), USER_DESIGN_SYSTEMS_DIR: path.join(tempRoot, 'user-design-systems'), USER_DESIGN_TEMPLATES_DIR: path.join(tempRoot, 'user-design-templates'), USER_SKILLS_DIR: path.join(tempRoot, 'user-skills'), }; const httpDeps = { createSseResponse: () => undefined, isLocalSameOrigin, requireLocalDaemonRequest: (_req: unknown, _res: unknown, next: () => void) => next(), resolvedPortRef: { get current() { const address = smokeServer?.address(); return typeof address === 'object' && address ? address.port : 0; }, }, sendApiError: (res: express.Response, status: number, code: string, message: string) => res.status(status).json({ error: message, code }), sendLiveArtifactRouteError: () => undefined, sendMulterError: () => undefined, }; const designSystemSummary = { id: designSystemId, title: 'Route Smoke System', category: 'test', summary: 'A seeded design system.', swatches: ['#123456'], surface: 'web' as const, body: designSystemBody, source: 'built-in' as const, status: 'published' as const, isEditable: false, }; const templateEntry = { id: templateId, name: templateId, description: 'Route smoke template.', triggers: [], mode: 'template' as const, surface: 'web' as const, source: 'built-in' as const, craftRequires: [], platform: null, scenario: '', category: null, previewType: 'default', designSystemRequired: false, defaultFor: [], upstream: null, featured: null, fidelity: null, speakerNotes: null, animations: null, examplePrompt: '', aggregatesExamples: false, critiquePolicy: null, body: 'Render the route smoke template.', dir: templateDir, }; registerDesignSystemRoutes(app, { db: {} as never, paths, projectFiles: {} as never, projectStore: {} as never, designSystems: { buildUserDesignSystemArchive: async () => null, createUserDesignSystem: async () => designSystemSummary as never, deleteUserDesignSystem: async () => false, ensureUserDesignSystemWorkspaceProject: async () => null, listAllDesignSystems: async () => [designSystemSummary], listUserDesignSystemFiles: async () => null, listUserDesignSystemRevisions: async () => null, prepareDesignTokenContractRebuild: async () => ({ decision: { available: false } }) as never, readAvailableDesignSystem: async (id: string) => id === designSystemId ? designSystemBody : null, readAvailableDesignSystemPackageInfo: async () => null, readAvailableDesignSystemStaticFile: async () => null, readDesignSystemWorkspaceTextFile: async () => null, readUserDesignSystemFile: async () => null, renderDesignSystemPreview: (id: string, body: string) => `${id} preview
${body}
`, renderDesignSystemShowcase: (id: string, body: string) => `${id} showcase
${body}
`, updateUserDesignSystem: async () => null, updateUserDesignSystemRevisionStatus: async () => null, }, generationJobs: { get: () => null, rebuildTokenContract: () => ({}) as never, revise: () => ({}) as never, start: () => ({}) as never, }, }); registerStaticResourceRoutes(app, { http: httpDeps, paths, resources: { listAllDesignSystems: async () => [designSystemSummary], listAllSkills: async () => [], listAllDesignTemplates: async () => [templateEntry], listAllSkillLikeEntries: async () => [templateEntry], mimeFor: (target: string) => target.endsWith('.css') ? 'text/css' : 'application/octet-stream', }, }); try { const smokeBaseUrl = await new Promise((resolve) => { smokeServer = app.listen(0, '127.0.0.1', () => { const address = smokeServer?.address() as { port: number }; resolve(`http://127.0.0.1:${address.port}`); }); }); const [detail, preview, showcase, example] = await Promise.all([ fetch(`${smokeBaseUrl}/api/design-systems/${designSystemId}`), fetch(`${smokeBaseUrl}/api/design-systems/${designSystemId}/preview`), fetch(`${smokeBaseUrl}/api/design-systems/${designSystemId}/showcase`), fetch(`${smokeBaseUrl}/api/skills/${templateId}/example`), ]); expect(detail.status).toBe(200); expect(detail.headers.get('content-type')).toContain('application/json'); await expect(detail.json()).resolves.toMatchObject({ designSystem: { id: designSystemId, body: designSystemBody, }, }); expect(preview.status).toBe(200); expect(preview.headers.get('content-type')).toContain('text/html'); await expect(preview.text()).resolves.toContain(`${designSystemId} preview`); expect(showcase.status).toBe(200); expect(showcase.headers.get('content-type')).toContain('text/html'); await expect(showcase.text()).resolves.toContain(`${designSystemId} showcase`); expect(example.status).toBe(200); expect(example.headers.get('content-type')).toContain('text/html'); const exampleHtml = await example.text(); expect(exampleHtml).toContain(`/api/skills/${templateId}/assets/demo.css`); const asset = await fetch(`${smokeBaseUrl}/api/skills/${templateId}/assets/demo.css`, { headers: { Origin: 'null' }, }); expect(asset.status).toBe(200); expect(asset.headers.get('content-type')).toContain('text/css'); expect(asset.headers.get('access-control-allow-origin')).toBe('*'); await expect(asset.text()).resolves.toBe('main { color: rgb(1 2 3); }'); } finally { await new Promise((resolve) => smokeServer?.close(() => resolve())); fs.rmSync(tempRoot, { recursive: true, force: true }); } }); }); describe('static SPA fallback classification', () => { const makeReq = (path: string, method = 'GET', accept = 'text/html') => ({ method, path, get: (name: string) => (name.toLowerCase() === 'accept' ? accept : ''), }); it('does not classify API, artifacts, frames, or _next requests as SPA fallbacks', () => { expect(isStaticSpaFallbackRequest(makeReq('/api/projects') as never)).toBe(false); expect(isStaticSpaFallbackRequest(makeReq('/artifacts/x') as never)).toBe(false); expect(isStaticSpaFallbackRequest(makeReq('/frames/x') as never)).toBe(false); expect(isStaticSpaFallbackRequest(makeReq('/_next/static/app.js') as never)).toBe(false); }); it('requires a GET or HEAD request with an HTML-compatible Accept header', () => { expect(isStaticSpaFallbackRequest(makeReq('/workspace') as never)).toBe(true); expect(isStaticSpaFallbackRequest(makeReq('/workspace', 'HEAD') as never)).toBe(true); expect(isStaticSpaFallbackRequest(makeReq('/workspace', 'POST') as never)).toBe(false); expect(isStaticSpaFallbackRequest(makeReq('/workspace', 'GET', 'application/json') as never)).toBe(false); }); }); describe('daemon data dir resolver', () => { it('requires explicit OD_DATA_DIR in sandbox mode and resolves project-relative dirs', () => { expect(() => resolveDataDir('', '/tmp/open-design-test', { requireExplicit: true })).toThrow( /OD_DATA_DIR is required/, ); expect(resolveDataDir('relative-data', '/tmp/open-design-test')).toBe('/tmp/open-design-test/relative-data'); }); });