Files
nexu-io__open-design/apps/daemon/tests/server-persistence-smoke.test.ts
Amy 8ff19c6b41 [codex] Add main-risk smoke and E2E coverage (#4322)
* test(e2e): cover daemon reload artifact recovery

* test(e2e): expand critical flow coverage

* test(daemon): add startup and recovery smoke coverage

* test(e2e): add visual and desktop chrome smoke coverage

* ci: gate packaged onboarding smoke to nightly

* fix(web): repair artifact replay type narrowing

Generated-By: looper 0.9.7 (runner=fixer, agent=codex)

* fix(ci): repair preflight smoke coverage checks

Generated-By: looper 0.9.7 (runner=fixer, agent=codex)

* fix(web): tighten artifact recovery polling

Generated-By: looper 0.9.7 (runner=fixer, agent=codex)

* fix(web): keep error recovery retries alive

Generated-By: looper 0.9.7 (runner=fixer, agent=codex)

* fix(e2e): relax brittle UI P0 assertions

Generated-By: looper 0.9.7 (runner=fixer, agent=codex)

* fix(web): recover pointer artifact outputs

Generated-By: looper 0.9.7 (runner=fixer, agent=codex)

* fix(e2e): stabilize manual edit reload smoke

Generated-By: looper 0.9.7 (runner=fixer, agent=codex)

* fix(e2e): avoid noisy win onboarding cleanup

Generated-By: looper 0.9.7 (runner=fixer, agent=codex)

* fix(web): stop failed artifact recovery polling

Generated-By: looper 0.9.7 (runner=fixer, agent=codex)

* fix(daemon): isolate startup smoke data root

Generated-By: looper 0.9.7 (runner=fixer, agent=codex)

* fix(web): recover standalone html artifacts

Generated-By: looper 0.9.7 (runner=fixer, agent=codex)

* fix(web): guard pointer artifact recovery by run time

Generated-By: looper 0.9.7 (runner=fixer, agent=codex)

* fix(ci): narrow strict visual gate to critical smoke

Generated-By: looper 0.9.7 (runner=fixer, agent=codex)

* fix(ci): limit PR visual capture to critical cases

* fix(ci): keep PR visual capture complete

* fix(web): keep reload artifact recovery retryable

* fix(ci): clarify PR visual gate layering

* fix(e2e): stabilize visual home P2 capture

* fix(e2e): align visual gallery detail clicks

* fix(e2e): preserve packaged install during onboarding cleanup

* fix(ci): shard PR visual capture

* fix(ci): remove duplicate nix validation from ci

* fix(ci): keep visual capture screenshots flat

* fix(ci): point nix hash autofix at ci-nix

* fix(ci): stabilize PR P0 recovery gate

* fix(web): ignore stale artifact pointers during recovery

* fix(web): reset BYOK model after clearing key

* ci: run critical UI P0 subset on PRs

* ci: combine PR UI P0 critical shards

* ci: combine PR visual capture shards

* ci: preserve visual capture shard manifests

* fix(web): clear ProjectView delayed timers on unmount

* test(e2e): harden onboarding connect gate coverage

* test(e2e): align AMR auth recovery and visual catalog timeout
2026-06-17 16:01:53 +08:00

138 lines
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);
}
}