项目文件夹

文件
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

138 行
4.2 KiB
TypeScript

import type { Server } from 'node:http';
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, expect, it, vi } from 'vitest';
type StartedServer = {
url: string;
server: Server;
shutdown?: () => Promise<void> | void;
};
type ServerModule = {
startServer: (options: {
port: number;
returnServer: boolean;
}) => Promise<StartedServer>;
};
let started: StartedServer | null = null;
let dataDir: string | null = null;
let serverModule: ServerModule | null = null;
const originalDataDir = process.env.OD_DATA_DIR;
afterEach(async () => {
await stopServer();
if (dataDir) await rm(dataDir, { recursive: true, force: true });
dataDir = null;
if (originalDataDir === undefined) delete process.env.OD_DATA_DIR;
else process.env.OD_DATA_DIR = originalDataDir;
serverModule = null;
vi.resetModules();
}, 30_000);
it('[P0] starts on an existing data dir with legacy app config and persisted projects', async () => {
dataDir = await mkdtemp(join(tmpdir(), 'od-server-persistence-smoke-'));
await mkdir(dataDir, { recursive: true });
await writeFile(join(dataDir, 'app-config.json'), JSON.stringify({
agentId: 'claude',
agentModels: { claude: { model: 'claude-legacy-config-smoke' } },
onboardingCompleted: true,
legacyUnknownPreference: { migratedFrom: '0.9.x' },
}), 'utf8');
started = await startIsolatedServer(dataDir);
const firstConfig = await fetchJson<{ config: Record<string, unknown> }>(
`${started.url}/api/app-config`,
);
expect(firstConfig.config).toMatchObject({
agentId: 'claude',
agentModels: { claude: { model: 'claude-legacy-config-smoke' } },
});
const projectId = `persisted-project-${Date.now()}`;
const projectResponse = await fetch(`${started.url}/api/projects`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
id: projectId,
name: 'Persisted startup smoke',
metadata: { kind: 'prototype' },
skipDiscoveryBrief: true,
}),
});
expect(projectResponse.status).toBe(200);
await stopServer();
started = await startIsolatedServer(dataDir);
const health = await fetchJson<{ ok: boolean }>(`${started.url}/api/health`);
expect(health).toMatchObject({ ok: true });
const projects = await fetchJson<{ projects: Array<{ id: string; name: string }> }>(
`${started.url}/api/projects`,
);
expect(projects.projects).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: projectId, name: 'Persisted startup smoke' }),
]),
);
const secondConfig = await fetchJson<{ config: Record<string, unknown> }>(
`${started.url}/api/app-config`,
);
expect(secondConfig.config).toMatchObject({
agentId: 'claude',
agentModels: { claude: { model: 'claude-legacy-config-smoke' } },
});
}, 60_000);
async function startIsolatedServer(root: string): Promise<StartedServer> {
process.env.OD_DATA_DIR = root;
if (!serverModule) {
vi.resetModules();
serverModule = await loadServerModule();
}
const module = serverModule;
return await module.startServer({ port: 0, returnServer: true });
}
async function loadServerModule(): Promise<ServerModule> {
return await import('../src/server.js') as unknown as ServerModule;
}
async function stopServer(): Promise<void> {
const current = started;
started = null;
if (!current) return;
await withTimeout(Promise.resolve(current.shutdown?.()), 8_000);
if (current.server) {
current.server.closeAllConnections?.();
current.server.closeIdleConnections?.();
await withTimeout(
new Promise<void>((resolve) => current.server.close(() => resolve())),
8_000,
);
}
}
async function fetchJson<T>(url: string): Promise<T> {
const response = await fetch(url);
expect(response.status).toBe(200);
return await response.json() as T;
}
async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T | undefined> {
let timer: NodeJS.Timeout | undefined;
try {
return await Promise.race([
promise,
new Promise<undefined>((resolve) => {
timer = setTimeout(() => resolve(undefined), ms);
}),
]);
} finally {
if (timer) clearTimeout(timer);
}
}