| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- 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
- });
- });
- });
|