245 lines
8.6 KiB
TypeScript
245 lines
8.6 KiB
TypeScript
import {mkdtempSync, rmSync, writeFileSync} from 'node:fs';
|
||
import {tmpdir} from 'node:os';
|
||
import {join} from 'node:path';
|
||
|
||
import {afterEach, describe, expect, test, vi} from 'vitest';
|
||
|
||
import {
|
||
DevRunner,
|
||
createDevServerSpawnOptions,
|
||
createWatchConfigs,
|
||
parseArgs,
|
||
shouldAcceptWatchEvent,
|
||
} from './dev.mjs';
|
||
|
||
const originalFetch = globalThis.fetch;
|
||
|
||
afterEach(() => {
|
||
globalThis.fetch = originalFetch;
|
||
vi.restoreAllMocks();
|
||
});
|
||
|
||
describe('dev scheduler argument routing', () => {
|
||
test('完整 dev 栈覆盖前端代理到本次解析出的 api-server 地址', () => {
|
||
const {command, explicitOptions, options} = parseArgs([], {
|
||
GENARRATIVE_API_PORT: '8090',
|
||
GENARRATIVE_RUNTIME_SERVER_TARGET: 'http://127.0.0.1:3100',
|
||
RUST_SERVER_TARGET: 'http://127.0.0.1:3100',
|
||
GENARRATIVE_API_TARGET: 'http://127.0.0.1:3100',
|
||
});
|
||
|
||
const runner = new DevRunner(options, {}, explicitOptions);
|
||
runner.command = command;
|
||
expect(runner.resolveFrontendApiTarget()).toBe('http://127.0.0.1:8090');
|
||
});
|
||
|
||
test('单独 dev:web 未显式指定 api 参数时沿用已有 Rust target', () => {
|
||
const testEnv = {
|
||
RUST_SERVER_TARGET: 'http://127.0.0.1:3100',
|
||
GENARRATIVE_API_PORT: '8082',
|
||
};
|
||
const {command, explicitOptions, options} = parseArgs(['web'], testEnv);
|
||
|
||
const runner = new DevRunner(options, testEnv, explicitOptions);
|
||
runner.command = command;
|
||
expect(runner.resolveFrontendApiTarget()).toBe('http://127.0.0.1:3100');
|
||
});
|
||
|
||
test('单独 dev:web 显式指定 api-port 时覆盖代理目标', () => {
|
||
const testEnv = {
|
||
RUST_SERVER_TARGET: 'http://127.0.0.1:3100',
|
||
GENARRATIVE_API_PORT: '8082',
|
||
};
|
||
const {command, explicitOptions, options} = parseArgs(
|
||
['web', '--api-port', '9090'],
|
||
testEnv,
|
||
);
|
||
|
||
const runner = new DevRunner(options, testEnv, explicitOptions);
|
||
runner.command = command;
|
||
expect(runner.resolveFrontendApiTarget()).toBe('http://127.0.0.1:9090');
|
||
});
|
||
|
||
test('单独 dev:admin-web 优先沿用 ADMIN_API_TARGET', () => {
|
||
const testEnv = {
|
||
ADMIN_API_TARGET: 'http://127.0.0.1:3100',
|
||
RUST_SERVER_TARGET: 'http://127.0.0.1:8082',
|
||
};
|
||
const {command, explicitOptions, options} = parseArgs(['admin-web'], testEnv);
|
||
|
||
const runner = new DevRunner(options, testEnv, explicitOptions);
|
||
runner.command = command;
|
||
expect(runner.resolveFrontendApiTarget({admin: true})).toBe(
|
||
'http://127.0.0.1:3100',
|
||
);
|
||
});
|
||
});
|
||
|
||
describe('dev scheduler spacetime reuse guard', () => {
|
||
test('记录 URL 可 ping 但没有 spacetime.pid 时不复用宿主', async () => {
|
||
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-spacetime-reuse-'));
|
||
try {
|
||
writeFileSync(join(tempDir, 'dev-spacetime-url'), 'http://127.0.0.1:3199\n', 'utf8');
|
||
globalThis.fetch = vi.fn(async () => ({status: 200})) as unknown as typeof fetch;
|
||
|
||
const {command, explicitOptions, options} = parseArgs(
|
||
['--spacetime-data-dir', tempDir],
|
||
{},
|
||
);
|
||
const runner = new DevRunner(options, {}, explicitOptions);
|
||
|
||
await runner.tryReuseExistingSpacetime(command);
|
||
|
||
expect(runner.state.spacetimeReused).toBeUndefined();
|
||
expect(runner.state.spacetimeServer).toBe('http://127.0.0.1:3101');
|
||
} finally {
|
||
rmSync(tempDir, {recursive: true, force: true});
|
||
}
|
||
});
|
||
|
||
test('记录 URL 可 ping 且 spacetime.pid 存活时复用宿主', async () => {
|
||
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-spacetime-reuse-'));
|
||
try {
|
||
writeFileSync(join(tempDir, 'dev-spacetime-url'), 'http://127.0.0.1:3199\n', 'utf8');
|
||
writeFileSync(join(tempDir, 'spacetime.pid'), `${process.pid}\n`, 'utf8');
|
||
globalThis.fetch = vi.fn(async () => ({status: 200})) as unknown as typeof fetch;
|
||
|
||
const {command, explicitOptions, options} = parseArgs(
|
||
['--spacetime-data-dir', tempDir],
|
||
{},
|
||
);
|
||
const runner = new DevRunner(options, {}, explicitOptions);
|
||
|
||
await runner.tryReuseExistingSpacetime(command);
|
||
|
||
expect(runner.state.spacetimeReused).toBe(true);
|
||
expect(runner.state.spacetimeServer).toBe('http://127.0.0.1:3199');
|
||
expect(runner.options.spacetimePort).toBe(3199);
|
||
} finally {
|
||
rmSync(tempDir, {recursive: true, force: true});
|
||
}
|
||
});
|
||
|
||
test('没有 URL 记录但 spacetime.pid 存活时复用默认宿主', async () => {
|
||
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-spacetime-reuse-'));
|
||
try {
|
||
writeFileSync(join(tempDir, 'spacetime.pid'), `${process.pid}\n`, 'utf8');
|
||
globalThis.fetch = vi.fn(async () => ({status: 200})) as unknown as typeof fetch;
|
||
|
||
const {command, explicitOptions, options} = parseArgs(
|
||
['--spacetime-data-dir', tempDir, '--spacetime-port', '3198'],
|
||
{},
|
||
);
|
||
const runner = new DevRunner(options, {}, explicitOptions);
|
||
|
||
await runner.tryReuseExistingSpacetime(command);
|
||
|
||
expect(runner.state.spacetimeReused).toBe(true);
|
||
expect(runner.state.spacetimeServer).toBe('http://127.0.0.1:3198');
|
||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||
'http://127.0.0.1:3198/v1/ping',
|
||
expect.any(Object),
|
||
);
|
||
} finally {
|
||
rmSync(tempDir, {recursive: true, force: true});
|
||
}
|
||
});
|
||
|
||
test('spacetime.pid 存活但候选地址不可访问时不继续启动第二个宿主', async () => {
|
||
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-spacetime-reuse-'));
|
||
try {
|
||
writeFileSync(join(tempDir, 'spacetime.pid'), `${process.pid}\n`, 'utf8');
|
||
globalThis.fetch = vi.fn(async () => ({status: 503})) as unknown as typeof fetch;
|
||
|
||
const {command, explicitOptions, options} = parseArgs(
|
||
['--spacetime-data-dir', tempDir, '--spacetime-port', '3198'],
|
||
{},
|
||
);
|
||
const runner = new DevRunner(options, {}, explicitOptions);
|
||
|
||
await expect(runner.tryReuseExistingSpacetime(command)).rejects.toThrow(
|
||
'检测到 spacetime.pid',
|
||
);
|
||
expect(runner.state.spacetimeReused).toBeUndefined();
|
||
} finally {
|
||
rmSync(tempDir, {recursive: true, force: true});
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('dev scheduler interactive input', () => {
|
||
test('前端 dev server 不继承 stdin,避免吞掉 rs 重启命令', () => {
|
||
const options = createDevServerSpawnOptions({cwd: repoRootForTest(), env: {A: 'B'}});
|
||
|
||
expect(options.stdio).toEqual(['ignore', 'pipe', 'pipe']);
|
||
expect(options.env).toEqual({A: 'B'});
|
||
});
|
||
});
|
||
|
||
function repoRootForTest() {
|
||
return process.cwd();
|
||
}
|
||
|
||
describe('dev scheduler watch routing', () => {
|
||
test('watch 模式不重启 web/admin-web,交给 Vite 自身 watch', () => {
|
||
const configs = createWatchConfigs();
|
||
|
||
expect(configs.web).toEqual([]);
|
||
expect(configs['admin-web']).toEqual([]);
|
||
});
|
||
|
||
test('watch 过滤依赖缓存和构建产物,避免自触发循环', () => {
|
||
const config = {
|
||
path: join(process.cwd(), 'apps/admin-web'),
|
||
filter: () => true,
|
||
};
|
||
|
||
expect(
|
||
shouldAcceptWatchEvent(config, join(process.cwd(), 'apps/admin-web/src/App.tsx')),
|
||
).toBe(true);
|
||
expect(
|
||
shouldAcceptWatchEvent(
|
||
config,
|
||
join(process.cwd(), 'apps/admin-web/node_modules/.vite/deps/_metadata.json'),
|
||
),
|
||
).toBe(false);
|
||
expect(
|
||
shouldAcceptWatchEvent(config, join(process.cwd(), 'apps/admin-web/dist/assets/app.js')),
|
||
).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('dev scheduler spacetime refresh', () => {
|
||
test('手动刷新 spacetime 只重新发布模块,不重启 standalone 进程', async () => {
|
||
const {explicitOptions, options} = parseArgs([], {});
|
||
const runner = new DevRunner(options, {}, explicitOptions);
|
||
const restart = vi.fn();
|
||
|
||
runner.services.set('spacetime', {restart});
|
||
runner.waitForSpacetime = vi.fn(async () => {});
|
||
runner.publishSpacetimeModule = vi.fn(async () => {});
|
||
|
||
await runner.restartService('spacetime');
|
||
|
||
expect(restart).not.toHaveBeenCalled();
|
||
expect(runner.waitForSpacetime).toHaveBeenCalledTimes(1);
|
||
expect(runner.publishSpacetimeModule).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
test('skip-publish 时 spacetime 刷新不会重启或发布', async () => {
|
||
const {explicitOptions, options} = parseArgs(['--skip-publish'], {});
|
||
const runner = new DevRunner(options, {}, explicitOptions);
|
||
const restart = vi.fn();
|
||
|
||
runner.services.set('spacetime', {restart});
|
||
runner.waitForSpacetime = vi.fn(async () => {});
|
||
runner.publishSpacetimeModule = vi.fn(async () => {});
|
||
|
||
await runner.restartService('spacetime');
|
||
|
||
expect(restart).not.toHaveBeenCalled();
|
||
expect(runner.waitForSpacetime).not.toHaveBeenCalled();
|
||
expect(runner.publishSpacetimeModule).not.toHaveBeenCalled();
|
||
});
|
||
});
|