Refactor local dev stack scheduler

This commit is contained in:
kdletters
2026-05-15 09:37:08 +08:00
parent 0152f9bd67
commit 7a3b137565
20 changed files with 2393 additions and 2088 deletions

244
scripts/dev.test.ts Normal file
View File

@@ -0,0 +1,244 @@
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();
});
});