594063a1e0
- 按 vitest 0.34 的参数元组口径改写 mock 泛型 - 修正 noUncheckedIndexedAccess 下的下标访问 - 补齐与当前类型脱节的测试桩字段 - 按 scripts/*.mjs 的运行时契约补局部类型声明
299 lines
9.2 KiB
TypeScript
299 lines
9.2 KiB
TypeScript
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/',
|
||
});
|
||
});
|
||
});
|