Files
iot-dc3/dc3-cli/test/analytics-alert.test.ts
pnoker 1b6903cdfb feat(cli): analytics group and alert deep-analysis surface
- dc3 analytics run <op> --args posts to the nine /api/v3/data/analytics
  operations (S19 agent face); analytics list prints the catalog
- dc3 alert gains activity/storm-sources/flapping/correlation/
  peer-deviation/aging/mtta/change-impact (GET), latency, silent-sources,
  coverage-gap and bulk-confirm, with snake_case param mapping and
  generic k=v passthrough
- seven new suites cover path shaping, param mapping, passthrough,
  bulk body verbatim and op validation
2026-09-01 22:03:15 +08:00

114 lines
4.3 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
const fetchCalls: Array<{ url: string; init: RequestInit }> = [];
vi.mock('../src/core/config-manager.js', () => ({
configManager: {
getActiveProfile: vi.fn(async () => ({ gateway: 'http://gw.test/', tenant: 't', username: 'u', credential_store: 'env' })),
load: vi.fn(async () => ({ current_profile: 'default' })),
getSettings: vi.fn(async () => ({ renewal_threshold_hours: 12, output_format: 'json', color: false, retry_count: 1 })),
},
}));
vi.mock('../src/core/token-manager.js', () => ({
tokenManager: {
getState: vi.fn(async () => null),
needsRenewal: vi.fn(async () => false),
},
}));
import { Command } from 'commander';
import { registerAlertCommand } from '../src/commands/alert.js';
import { registerAnalyticsCommand } from '../src/commands/analytics.js';
function buildProgram(): Command {
const program = new Command();
program.exitOverride();
registerAlertCommand(program);
registerAnalyticsCommand(program);
return program;
}
async function run(args: string[]): Promise<string> {
const program = buildProgram();
let out = '';
vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
out += String(chunk);
return true;
});
vi.spyOn(process, 'exit').mockImplementation(((code?: number) => undefined) as never);
try {
await program.parseAsync(args, { from: 'user' });
} finally {
(process.stdout.write as ReturnType<typeof vi.spyOn>).mockRestore();
}
return out;
}
describe('alert deep-analysis commands', () => {
beforeEach(() => {
fetchCalls.length = 0;
vi.stubGlobal('fetch', vi.fn(async (url: string, init?: RequestInit) => {
fetchCalls.push({ url, init: init ?? {} });
return new Response(JSON.stringify({ ok: true, data: [] }), { status: 200 });
}));
});
it('aging hits the alert aging endpoint with days passthrough', async () => {
await run(['alert', 'aging', '--days', '7']);
expect(fetchCalls[0].url).toBe('http://gw.test/api/v3/data/dashboard/alert/aging?days=7');
});
it('silent-sources maps baseline/limit to snake_case server params', async () => {
await run(['alert', 'silent-sources', '--baseline-days', '3', '--limit', '5']);
expect(fetchCalls[0].url).toContain('/dashboard/silent/sources');
expect(fetchCalls[0].url).toContain('baseline_days=3');
expect(fetchCalls[0].url).toContain('limit=5');
});
it('arbitrary kv pairs pass through for endpoints with extra params', async () => {
await run(['alert', 'storm-sources', '--query', 'source=device']);
const url = fetchCalls[0].url;
expect(url).toContain('/alert/storm_sources');
expect(url).toContain('source=device');
});
it('bulk-confirm posts the parsed items body verbatim', async () => {
await run(['alert', 'bulk-confirm', '--args', '{"items":[{"source":"device","id":1}]}']);
expect(fetchCalls[0].init.method).toBe('POST');
const body = JSON.parse(String(fetchCalls[0].init.body));
expect(body.items[0]).toEqual({ source: 'device', id: 1 });
});
it('unknown analytics op exits with a business error without calling fetch', async () => {
fetchCalls.length = 0;
await run(['analytics', 'run', 'nope', '--args', '{}']).catch(() => undefined);
expect(fetchCalls.length).toBe(0);
});
});
describe('analytics command group', () => {
beforeEach(() => {
fetchCalls.length = 0;
vi.stubGlobal('fetch', vi.fn(async (url: string, init?: RequestInit) => {
fetchCalls.push({ url, init: init ?? {} });
return new Response(JSON.stringify({ ok: true, data: { conclusion: 'x' } }), { status: 200 });
}));
});
it('run posts the JSON body to the mapped operation path', async () => {
await run(['analytics', 'run', 'query_history', '--args', '{"pointId":1,"days":2}']);
expect(fetchCalls[0].url).toBe('http://gw.test/api/v3/data/analytics/query_history');
expect(JSON.parse(String(fetchCalls[0].init.body))).toEqual({ pointId: 1, days: 2 });
});
it('list prints the nine operations without network access', async () => {
fetchCalls.length = 0;
const out = await run(['analytics', 'list']);
const parsed = JSON.parse(out);
expect(parsed.operations).toHaveLength(9);
expect(parsed.operations).toContain('data_quality_report');
expect(fetchCalls.length).toBe(0);
});
});