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; 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; findPort?: (options: FindPortOptions) => number | Promise; }; type ResolveAgcAdminWebEndpointOptions = ResolveAgcDevEndpointOptions & { reservedPorts?: number[]; }; // 导入实现本身是同一份,仅按运行时契约修正推导过窄的签名(js 无 JSDoc 导致结构上不兼容)。 const resolveAgcDevEndpoint = resolveAgcDevEndpointImpl as unknown as ( options?: ResolveAgcDevEndpointOptions, ) => Promise; // 同上:reservedPorts 在 JS 推断里被收窄为 never[],与实际可传入的保留端口数组不兼容。 const resolveAgcAdminWebEndpoint = resolveAgcAdminWebEndpointImpl as unknown as ( options?: ResolveAgcAdminWebEndpointOptions, ) => Promise; 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/', }); }); });