5a9bc10bb6
将本地发布身份改为按SpacetimeDB数据目录持久化并兼容迁移旧记录 为开发栈状态补充专用数据目录并拒绝复用旧共享后端 统一处理子进程启动错误、启动期信号和进程组清理 补充身份、后端复用与子进程生命周期回归测试及文档
113 lines
3.4 KiB
TypeScript
113 lines
3.4 KiB
TypeScript
import { EventEmitter } from 'node:events';
|
|
import { resolve } from 'node:path';
|
|
|
|
import { describe, expect, test, vi } from 'vitest';
|
|
|
|
import {
|
|
ensureBackend,
|
|
resolveBackendTargetsFromState,
|
|
spawnChild,
|
|
waitForChildTermination,
|
|
} from '../scripts/start-dev-stack.mjs';
|
|
|
|
const expectedDatabase = 'genarrative-game-creator-dev';
|
|
const expectedDataDir = resolve('server-rs/.spacetimedb/ai-game-creator/data');
|
|
|
|
function backendState(spacetimeDataDir?: string) {
|
|
return {
|
|
schemaVersion: spacetimeDataDir ? 2 : 1,
|
|
database: expectedDatabase,
|
|
...(spacetimeDataDir ? { spacetimeDataDir } : {}),
|
|
services: {
|
|
'api-server': {
|
|
status: 'running',
|
|
url: 'http://127.0.0.1:8082',
|
|
},
|
|
spacetime: {
|
|
status: 'running',
|
|
url: 'http://127.0.0.1:3101',
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('AI 游戏创作配套后端复用门禁', () => {
|
|
test('旧状态缺少专用 data dir 时拒绝复用同名健康后端', () => {
|
|
const targets = resolveBackendTargetsFromState(backendState(), {
|
|
requireAgcBackend: true,
|
|
expectedDatabase,
|
|
expectedSpacetimeDataDir: expectedDataDir,
|
|
});
|
|
|
|
expect(targets.hasMatchingDatabase).toBe(true);
|
|
expect(targets.hasMatchingDataDir).toBe(false);
|
|
expect(targets.hasMatchingBackend).toBe(false);
|
|
expect(targets.apiUrl).toBe('');
|
|
expect(targets.spacetimeUrl).toBe('');
|
|
});
|
|
|
|
test('只有数据库名和专用 data dir 都匹配时才允许复用', () => {
|
|
const wrongDir = resolveBackendTargetsFromState(
|
|
backendState(resolve('server-rs/.spacetimedb/local/data')),
|
|
{
|
|
requireAgcBackend: true,
|
|
expectedDatabase,
|
|
expectedSpacetimeDataDir: expectedDataDir,
|
|
},
|
|
);
|
|
const matching = resolveBackendTargetsFromState(
|
|
backendState(expectedDataDir),
|
|
{
|
|
requireAgcBackend: true,
|
|
expectedDatabase,
|
|
expectedSpacetimeDataDir: expectedDataDir,
|
|
},
|
|
);
|
|
|
|
expect(wrongDir.hasMatchingBackend).toBe(false);
|
|
expect(matching.hasMatchingBackend).toBe(true);
|
|
expect(matching.apiUrl).toBe('http://127.0.0.1:8082');
|
|
expect(matching.spacetimeUrl).toBe('http://127.0.0.1:3101');
|
|
});
|
|
});
|
|
|
|
describe('AI 游戏创作启动子进程生命周期', () => {
|
|
const posixTest = process.platform === 'win32' ? test.skip : test;
|
|
|
|
posixTest('npm 不可解析时进入受控 error 结果而不是未处理事件', async () => {
|
|
const child = spawnChild('genarrative-command-that-does-not-exist', [], {
|
|
cwd: process.cwd(),
|
|
});
|
|
|
|
const failure = await waitForChildTermination(child);
|
|
|
|
expect(failure.type).toBe('error');
|
|
expect(failure.error).toMatchObject({ code: 'ENOENT' });
|
|
});
|
|
|
|
test('后端句柄在 ready 等待前交给外层且异常时立即清理', async () => {
|
|
const child = Object.assign(new EventEmitter(), {
|
|
exitCode: null,
|
|
signalCode: null,
|
|
kill: vi.fn(),
|
|
});
|
|
const onBackendChild = vi.fn();
|
|
const waitUntilReady = vi.fn(async (receivedChild) => {
|
|
expect(receivedChild).toBe(child);
|
|
expect(onBackendChild).toHaveBeenCalledWith(child);
|
|
throw new Error('等待配套后端和数据库启动超时');
|
|
});
|
|
|
|
await expect(
|
|
ensureBackend({
|
|
checkBackendReady: async () => false,
|
|
spawnBackend: () => child,
|
|
onBackendChild,
|
|
waitUntilReady,
|
|
}),
|
|
).rejects.toThrow('等待配套后端和数据库启动超时');
|
|
|
|
expect(child.kill).toHaveBeenCalledWith('SIGTERM');
|
|
});
|
|
});
|