Files
nexu-io--open-design/apps/daemon/tests/server-bootstrap-regression.test.ts
wehub-resource-sync 070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

580 lines
24 KiB
TypeScript

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> | 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<void>((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'),
'<!doctype html><link rel="stylesheet" href="./assets/demo.css"><main>Template Example</main>',
);
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) =>
`<!doctype html><title>${id} preview</title><main>${body}</main>`,
renderDesignSystemShowcase: (id: string, body: string) =>
`<!doctype html><title>${id} showcase</title><main>${body}</main>`,
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<string>((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<void>((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');
});
});