2bdee990a2
- dev-port.mjs 新增 resolveAgcAdminWebEndpoint:后台 Web 端口按 Linux 用户端口段 start + 3、非 Linux 优先 3102 解析,ADMIN_WEB_PORT 可显式指定,并把已解析的 AGC Vite 端口列为保留端口 - start-dev-stack.mjs 随 AGC 一起拉起 apps/admin-web(AGC_DEV_ADMIN_WEB=0 关闭):与 AGC Vite 一样由启动器直接持有并纳入信号与退出收束,不走会整体重写 .app/dev-stack.json 的 dev:admin-web - start-dev-stack.mjs 新增启动汇总行,一次给出前端、后端、后台、数据库与 bgfilter-worker 的实际地址,端口漂移后以该行为准 - start-dev-stack.mjs 端口归属探测改用 netstat -ano 取端口到 PID、.NET Process 读可执行文件路径,仅在核对 SpacetimeDB --data-dir 归属时按 PID 取命令行并做 5 分钟 TTL 缓存:本机实测单轮探测由约 43 秒降到 0.37 秒,agc:serve 的 starting backend stack 到 backend ready 由约 80 秒降到 16.7 秒 - 后台 Web 端口解析或启动失败只告警,不阻断也不连带停止 AGC 客户端与配套后端 - tests/dev-port.test.ts、tests/start-dev-stack.test.ts 补充用例覆盖后台 Web 端口解析与严格占用、失败软化、启动汇总、netstat 探测脚本与命令行缓存失效 - docs/【开发运维】与 docs/technical 同步 AGC 开发态启动口径,pitfalls.md 记录 WMI 慢速探测的实测数据、netstat 改法与 PID 取最后一列的易错点
226 lines
7.0 KiB
TypeScript
226 lines
7.0 KiB
TypeScript
import { describe, expect, test, vi } from 'vitest';
|
|
|
|
import {
|
|
createAgcAdminWebEndpoint,
|
|
createAgcDevEndpoint,
|
|
readConfiguredAgcAdminWebPort,
|
|
resolveAgcAdminWebEndpoint,
|
|
resolveAgcDevEndpoint,
|
|
withAgcDevEndpointEnv,
|
|
} from '../scripts/dev-port.mjs';
|
|
|
|
describe('AI 游戏创作 dev 端口', () => {
|
|
test('Linux 使用用户端口段的 start + 5 槽位', async () => {
|
|
const findPort = vi.fn(async ({ preferredPort }) => 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 }) => {
|
|
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 }) => 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 }) => 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 }) => 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 }) => 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 }) => 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/',
|
|
});
|
|
});
|
|
});
|