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
580 lines
24 KiB
TypeScript
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');
|
|
});
|
|
});
|