Files
kdletters 2bdee990a2 AGC 开发态补齐后台 Web 启动与端口汇总,端口归属探测从 WMI 换成 netstat
- 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 取最后一列的易错点
2026-09-14 21:02:08 +08:00

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/',
});
});
});