Files
Genarrative/apps/ai-game-creator-shell/tests/dev-port.test.ts
T
k88936 594063a1e0 测试:修正 AGC 测试文件的类型错误
- 按 vitest 0.34 的参数元组口径改写 mock 泛型
- 修正 noUncheckedIndexedAccess 下的下标访问
- 补齐与当前类型脱节的测试桩字段
- 按 scripts/*.mjs 的运行时契约补局部类型声明
2026-10-03 17:41:30 +08:00

299 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, test, vi } from 'vitest';
import {
createAgcAdminWebEndpoint,
createAgcDevEndpoint,
readConfiguredAgcAdminWebPort,
resolveAgcAdminWebEndpoint as resolveAgcAdminWebEndpointImpl,
resolveAgcDevEndpoint as resolveAgcDevEndpointImpl,
withAgcDevEndpointEnv,
} from '../scripts/dev-port.mjs';
// scripts/dev-port.mjs 没有 JSDoc,TS 按 JS 默认值把可选参数推成了过窄类型
// (findPort 的 portRange 推成 null、reservedPorts 推成 never[]),与测试注入的 mock 结构不兼容。
// 下面按运行时真实契约重新声明局部签名,只做类型层校正,调用方式与行为完全不变。
type EndpointPortRange = { start: number; end: number; label: string };
type AgcDevEndpointLike = {
host: string;
port: number;
url: string;
markerUrl: string;
portRange: EndpointPortRange | null;
};
type AgcAdminWebEndpointLike = {
host: string;
port: number;
origin: string;
basePath: string;
url: string;
portRange: EndpointPortRange | null;
};
type FindPortOptions = {
host?: string;
preferredPort: number;
reservedPorts?: Set<number>;
maxAttempts?: number | null;
portRange?: EndpointPortRange | null;
strict?: boolean;
};
type ReservePortRangeResult =
| { range?: EndpointPortRange | null }
| null
| undefined;
type ResolveAgcDevEndpointOptions = {
env?: NodeJS.ProcessEnv;
platform?: NodeJS.Platform;
strictConfigured?: boolean;
reservePortRange?: (options?: {
env?: NodeJS.ProcessEnv;
}) => Promise<ReservePortRangeResult>;
findPort?: (options: FindPortOptions) => number | Promise<number>;
};
type ResolveAgcAdminWebEndpointOptions = ResolveAgcDevEndpointOptions & {
reservedPorts?: number[];
};
// 导入实现本身是同一份,仅按运行时契约修正推导过窄的签名(js 无 JSDoc 导致结构上不兼容)。
const resolveAgcDevEndpoint = resolveAgcDevEndpointImpl as unknown as (
options?: ResolveAgcDevEndpointOptions,
) => Promise<AgcDevEndpointLike>;
// 同上:reservedPorts 在 JS 推断里被收窄为 never[],与实际可传入的保留端口数组不兼容。
const resolveAgcAdminWebEndpoint =
resolveAgcAdminWebEndpointImpl as unknown as (
options?: ResolveAgcAdminWebEndpointOptions,
) => Promise<AgcAdminWebEndpointLike>;
describe('AI 游戏创作 dev 端口', () => {
test('Linux 使用用户端口段的 start + 5 槽位', async () => {
const findPort = vi.fn(
async ({ preferredPort }: FindPortOptions) => preferredPort,
);
const endpoint = await resolveAgcDevEndpoint({
env: { USER: 'alice', LOGNAME: 'alice' },
platform: 'linux',
reservePortRange: async () => ({
username: 'alice',
range: { start: 10000, end: 10099, label: '10000-10099' },
}),
findPort,
});
expect(endpoint).toMatchObject({
port: 10005,
url: 'http://127.0.0.1:10005/',
markerUrl: 'http://127.0.0.1:10005/__agc_dev_server.json',
});
expect(findPort).toHaveBeenCalledWith(
expect.objectContaining({
preferredPort: 10005,
portRange: { start: 10000, end: 10099, label: '10000-10099' },
strict: false,
}),
);
});
test('Linux 槽位被占时接受本用户端口段内的漂移结果', async () => {
const endpoint = await resolveAgcDevEndpoint({
env: { USER: 'alice', LOGNAME: 'alice' },
platform: 'linux',
reservePortRange: async () => ({
username: 'alice',
range: { start: 10000, end: 10099, label: '10000-10099' },
}),
findPort: async ({ preferredPort, portRange }: FindPortOptions) => {
expect(preferredPort).toBe(10005);
expect(portRange?.label).toBe('10000-10099');
return 10006;
},
});
expect(endpoint.port).toBe(10006);
});
test('父启动器选定端口后子启动器严格使用同一端口', async () => {
const findPort = vi.fn(
async ({ preferredPort }: FindPortOptions) => preferredPort,
);
await resolveAgcDevEndpoint({
env: {
USER: 'alice',
LOGNAME: 'alice',
GENARRATIVE_AGC_VITE_PORT: '10007',
},
platform: 'linux',
strictConfigured: true,
reservePortRange: async () => ({
username: 'alice',
range: { start: 10000, end: 10099, label: '10000-10099' },
}),
findPort,
});
expect(findPort).toHaveBeenCalledWith(
expect.objectContaining({ preferredPort: 10007, strict: true }),
);
});
test('旧五端口段无法安全扩容时 AGC 明确失败而不越界', async () => {
await expect(
resolveAgcDevEndpoint({
env: { USER: 'alice', LOGNAME: 'alice' },
platform: 'linux',
reservePortRange: async () => ({
username: 'alice',
range: { start: 10000, end: 10004, label: '10000-10004' },
}),
findPort: async () => 10005,
}),
).rejects.toThrow('缺少 AGC Vite 槽位');
});
test('Linux 显式 AGC 端口不能占用前五个服务槽位', async () => {
await expect(
resolveAgcDevEndpoint({
env: {
USER: 'alice',
LOGNAME: 'alice',
GENARRATIVE_AGC_VITE_PORT: '10004',
},
platform: 'linux',
reservePortRange: async () => ({
username: 'alice',
range: { start: 10000, end: 10099, label: '10000-10099' },
}),
findPort: async () => 10004,
}),
).rejects.toThrow('前五个服务槽位');
});
test('非 Linux 保留 3080 兼容优先端口并允许统一漂移', async () => {
const findPort = vi.fn(async () => 3081);
const endpoint = await resolveAgcDevEndpoint({
env: {},
platform: 'win32',
findPort,
});
expect(endpoint.port).toBe(3081);
expect(findPort).toHaveBeenCalledWith(
expect.objectContaining({ preferredPort: 3080, portRange: null }),
);
});
test('最终端口通过单一环境变量传给 Tauri 和 Vite 子进程', () => {
expect(
withAgcDevEndpointEnv(createAgcDevEndpoint(10008), { KEEP_ME: 'yes' }),
).toMatchObject({
KEEP_ME: 'yes',
GENARRATIVE_AGC_VITE_PORT: '10008',
});
});
});
describe('AGC 开发态后台 Web 端口', () => {
test('Linux 使用用户端口段的 start + 3 槽位', async () => {
const findPort = vi.fn(
async ({ preferredPort }: FindPortOptions) => preferredPort,
);
const endpoint = await resolveAgcAdminWebEndpoint({
env: { USER: 'alice', LOGNAME: 'alice' },
platform: 'linux',
reservePortRange: async () => ({
username: 'alice',
range: { start: 10000, end: 10099, label: '10000-10099' },
}),
findPort,
});
expect(endpoint).toMatchObject({
port: 10003,
origin: 'http://127.0.0.1:10003',
basePath: '/admin/',
url: 'http://127.0.0.1:10003/admin/',
});
expect(findPort).toHaveBeenCalledWith(
expect.objectContaining({
preferredPort: 10003,
portRange: { start: 10000, end: 10099, label: '10000-10099' },
strict: false,
}),
);
});
test('AGC Vite 端口作为保留端口传入,避免被后台 Web 抢先占用', async () => {
const findPort = vi.fn(
async ({ preferredPort }: FindPortOptions) => preferredPort,
);
await resolveAgcAdminWebEndpoint({
env: {},
platform: 'win32',
reservedPorts: [10005, 0, null as unknown as number],
findPort,
});
const [call] = findPort.mock.calls;
expect([...call![0].reservedPorts!]).toEqual([10005]);
});
test('非 Linux 保留 3102 兼容优先端口并允许统一漂移', async () => {
const findPort = vi.fn(
async ({ preferredPort }: FindPortOptions) => preferredPort + 1,
);
const endpoint = await resolveAgcAdminWebEndpoint({
env: {},
platform: 'win32',
findPort,
});
expect(endpoint.port).toBe(3103);
expect(endpoint.url).toBe('http://127.0.0.1:3103/admin/');
expect(findPort).toHaveBeenCalledWith(
expect.objectContaining({ preferredPort: 3102, portRange: null }),
);
});
test('显式 ADMIN_WEB_PORT 与脚本统一命名并支持严格占用', async () => {
const findPort = vi.fn(
async ({ preferredPort }: FindPortOptions) => preferredPort,
);
await resolveAgcAdminWebEndpoint({
env: { ADMIN_WEB_PORT: '3109' },
platform: 'win32',
strictConfigured: true,
findPort,
});
expect(findPort).toHaveBeenCalledWith(
expect.objectContaining({ preferredPort: 3109, strict: true }),
);
expect(readConfiguredAgcAdminWebPort({ ADMIN_WEB_PORT: '3109' })).toBe(
3109,
);
expect(readConfiguredAgcAdminWebPort({})).toBeNull();
});
test('非法 ADMIN_WEB_PORT 直接失败而不是静默回退', () => {
expect(() =>
readConfiguredAgcAdminWebPort({ ADMIN_WEB_PORT: '80' }),
).toThrow('ADMIN_WEB_PORT 必须是 1024-65535 的有效端口');
expect(() =>
readConfiguredAgcAdminWebPort({ ADMIN_WEB_PORT: 'not-a-port' }),
).toThrow('ADMIN_WEB_PORT 必须是 1024-65535 的有效端口');
});
test('后台 Web 地址固定带 /admin/ base', () => {
expect(createAgcAdminWebEndpoint(3102, null)).toMatchObject({
host: '127.0.0.1',
port: 3102,
url: 'http://127.0.0.1:3102/admin/',
});
});
});