import { ParseApi } from './parse-api'; /** * 字段归一单测(2026-10-20 新增兼容层) * * 🔴 背景:实测 `agent-node-lj-test` 容器上报的字段名/结构与平台契约不符: * · `channels` 上报数组 `["wecom"]`(契约要求对象)→ 导致 matrixReady=false * · `host` 上报 `cpuLoad1`(契约要求 `cpuPercent`)→ CPU 不显示 * · `heartbeatAt` 缺失(容器用 `host.probedAt`) * * 本测试覆盖 `mapAgent()` 内的三个归一函数(经 fetchAgents 走通)。 */ describe('parse-api 字段归一(兼容非标准容器)', () => { let api: ParseApi; let calls: { url: string; init: RequestInit }[] = []; function stubFetch(body: unknown): void { calls = []; spyOn(window, 'fetch').and.callFake((input: RequestInfo | URL, init?: RequestInit) => { calls.push({ url: String(input), init: init ?? {} }); return Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve(body), } as unknown as Response); }); } beforeEach(() => { api = new ParseApi(() => 'r:t', () => 'all'); }); describe('channels 数组 → 对象', () => { it('["wecom"] → {wecom:true}(matrix 未点亮)', async () => { stubFetch({ code: 200, agents: [{ agentId: 'a', channels: '["wecom"]' }] }); const [a] = await api.fetchAgents(); expect(a.channels?.wecom).toBe(true); expect(a.channels?.matrix).toBeFalsy(); }); it('["matrix","wecom"] → 两者都点亮', async () => { stubFetch({ code: 200, agents: [{ agentId: 'a', channels: '["matrix","wecom"]' }] }); const [a] = await api.fetchAgents(); expect(a.channels?.matrix).toBe(true); expect(a.channels?.wecom).toBe(true); }); it('🔴 数组含 matrix → matrixReady=true(修复"未接入"误判)', async () => { stubFetch({ code: 200, agents: [{ agentId: 'a', channels: '["matrix"]' }] }); const [a] = await api.fetchAgents(); expect(a.matrixReady).toBe(true); }); it('对象形态(契约标准)原样通过', async () => { stubFetch({ code: 200, agents: [ { agentId: 'a', channels: '{"matrix":true,"homeserver":"https://matrix.fmode.cn","matrixConfig":{"userId":"@x:fmode.cn"}}', }, ], }); const [a] = await api.fetchAgents(); expect(a.channels?.matrix).toBe(true); expect(a.channels?.matrixConfig?.userId).toBe('@x:fmode.cn'); expect(a.matrixReady).toBe(true); }); it('空数组 → undefined(不是"已接入")', async () => { stubFetch({ code: 200, agents: [{ agentId: 'a', channels: '[]' }] }); const [a] = await api.fetchAgents(); expect(a.channels).toBeUndefined(); expect(a.matrixReady).toBe(false); }); }); describe('host 归一:cpuLoad1 → cpuPercent', () => { it('🔴 cpuLoad1=8, cpuCores=8 → cpuPercent=100', async () => { stubFetch({ code: 200, agents: [{ agentId: 'a', host: '{"cpuLoad1":8,"cpuCores":8,"memPercent":50,"diskPercent":30}' }], }); const [a] = await api.fetchAgents(); expect(a.host?.cpuPercent).toBe(100); }); it('🔴 核数未知时不算 cpuPercent(不编假数字)', async () => { // 实测契约口径是 cpuLoad1 / 核数;核数缺失时宁可"未上报",也不假定核数 stubFetch({ code: 200, agents: [{ agentId: 'a', host: '{"cpuLoad1":8,"memPercent":50,"diskPercent":30}' }], }); const [a] = await api.fetchAgents(); expect(a.host?.cpuPercent).toBeUndefined(); expect(a.host?.memPercent).toBe(50); // 其余指标仍保留 }); it('cpuLoad1=9.2, cpuCores=56 → cpuPercent≈16.4', async () => { stubFetch({ code: 200, agents: [ { agentId: 'a', host: '{"cpuLoad1":9.2,"cpuCores":56,"memPercent":55.6,"diskPercent":33.3}' }, ], }); const [a] = await api.fetchAgents(); expect(a.host?.cpuPercent).toBeCloseTo(16.4, 1); }); it('契约标准的 cpuPercent 优先(不被覆盖)', async () => { stubFetch({ code: 200, agents: [{ agentId: 'a', host: '{"cpuPercent":42,"cpuLoad1":99,"memPercent":10}' }], }); const [a] = await api.fetchAgents(); expect(a.host?.cpuPercent).toBe(42); }); it('越界负载被夹到 0~100', async () => { stubFetch({ code: 200, agents: [{ agentId: 'a', host: '{"cpuLoad1":999,"cpuCores":2}' }] }); const [a] = await api.fetchAgents(); expect(a.host?.cpuPercent).toBe(100); }); it('三项全无 → undefined(UI 显示"未上报"而非 0%)', async () => { stubFetch({ code: 200, agents: [{ agentId: 'a', host: '{"hostname":"x"}' }] }); const [a] = await api.fetchAgents(); expect(a.host).toBeUndefined(); }); }); describe('heartbeatAt 归一', () => { it('契约字段 heartbeatAt 优先', async () => { stubFetch({ code: 200, agents: [{ agentId: 'a', heartbeatAt: '2026-10-20T10:00:00Z', host: '{"probedAt":"2026-01-01T00:00:00Z"}' }], }); const [a] = await api.fetchAgents(); expect(a.heartbeatAt).toBe('2026-10-20T10:00:00Z'); }); it('缺失时回落 host.probedAt(容器实际用的键)', async () => { stubFetch({ code: 200, agents: [{ agentId: 'a', host: '{"probedAt":"2026-10-20T10:34:07+08:00"}' }], }); const [a] = await api.fetchAgents(); expect(a.heartbeatAt).toBe('2026-10-20T10:34:07+08:00'); }); it('再缺失时回落 updatedAtClient.iso', async () => { stubFetch({ code: 200, agents: [{ agentId: 'a', updatedAtClient: { iso: '2026-10-20T02:33:55.213Z' } }], }); const [a] = await api.fetchAgents(); expect(a.heartbeatAt).toBe('2026-10-20T02:33:55.213Z'); }); }); describe('balance 归一:容器复刻脚本键名', () => { it('🔴 容器发 {"cny":91.46} → remainCny 有值(修复余额不显示)', async () => { stubFetch({ code: 200, agents: [{ agentId: 'a', balance: '{"source":"session","cny":91.46}' }] }); const [a] = await api.fetchAgents(); expect(a.balance?.remainCny).toBe(91.46); }); it('契约标准 remainCny 优先于 cny', async () => { stubFetch({ code: 200, agents: [{ agentId: 'a', balance: '{"remainCny":10,"cny":99}' }] }); const [a] = await api.fetchAgents(); expect(a.balance?.remainCny).toBe(10); }); it('旧键 remainRmb 仍兼容', async () => { stubFetch({ code: 200, agents: [{ agentId: 'a', balance: '{"remainRmb":7.5}' }] }); const [a] = await api.fetchAgents(); expect(a.balance?.remainCny).toBe(7.5); }); it('source=none 无金额 → remainCny 为 undefined(UI 显示 —)', async () => { stubFetch({ code: 200, agents: [{ agentId: 'a', balance: '{"source":"none"}' }] }); const [a] = await api.fetchAgents(); expect(a.balance?.remainCny).toBeUndefined(); }); }); describe('usage 映射(此前完全缺失 → Token 用量永远空白)', () => { it('🔴 容器上报 inputTokens/outputTokens → 映射为 prompt/completion', async () => { stubFetch({ code: 200, agents: [ { agentId: 'a', usage: '{"totalTokens":2238055,"inputTokens":1856406,"outputTokens":381649,"apiCalls":487}', }, ], }); const [a] = await api.fetchAgents(); expect(a.usage?.totalTokens).toBe(2238055); expect(a.usage?.promptTokens).toBe(1856406); expect(a.usage?.completionTokens).toBe(381649); }); it('契约旧字段 promptTokens 优先', async () => { stubFetch({ code: 200, agents: [{ agentId: 'a', usage: '{"promptTokens":10,"inputTokens":99}' }], }); const [a] = await api.fetchAgents(); expect(a.usage?.promptTokens).toBe(10); }); it('usage 全空 → undefined(不是 0)', async () => { stubFetch({ code: 200, agents: [{ agentId: 'a', usage: '{}' }] }); const [a] = await api.fetchAgents(); expect(a.usage).toBeUndefined(); }); }); describe('deliverable 派生字段(此前 filename/size 从未赋值 → 渲染「· 」)', () => { it('🔴 从 artifacts[0] 推导 filename / fileType', async () => { stubFetch({ code: 200, deliverables: [ { objectId: 'd1', title: '报告', artifacts: [{ name: 'report.pdf', url: 'https://x/1', type: 'pdf' }], }, ], }); const [d] = await api.fetchDeliverables(); expect(d.filename).toBe('report.pdf'); expect(d.fileType).toBe('PDF'); }); it('无 artifacts → filename undefined(UI 显示「暂无附件」)', async () => { stubFetch({ code: 200, deliverables: [{ objectId: 'd1', title: '空成果', artifacts: [] }] }); const [d] = await api.fetchDeliverables(); expect(d.filename).toBeUndefined(); }); }); describe('综合:非标准容器的完整映射', () => { it('数组 channels + cpuLoad1 + probedAt → 全部归一正确', async () => { stubFetch({ code: 200, agents: [ { agentId: 'agent-node-lj-test', botName: 'lj-test', status: 'running', currentTask: 'periodic status reporter', channels: '["matrix","wecom"]', host: '{"hostname":"e14260fdce46","cpuLoad1":9.2,"cpuCores":56,"memPercent":55.6,"diskPercent":33.3,"probedAt":"2026-10-20T10:34:07+08:00"}', balance: '{"source":"none"}', }, ], }); const [a] = await api.fetchAgents(); expect(a.id).toBe('agent-node-lj-test'); expect(a.matrixReady).toBe(true); // ← 修复前是 false expect(a.host?.cpuPercent).toBeCloseTo(16.4, 1); // ← 修复前是 undefined expect(a.host?.memPercent).toBe(55.6); expect(a.heartbeatAt).toBe('2026-10-20T10:34:07+08:00'); // ← 修复前是 undefined }); }); });