mirror of
https://gitee.com/pnoker/iot-dc3.git
synced 2026-11-02 19:21:51 +08:00
- 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
114 lines
4.3 KiB
TypeScript
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);
|
|
});
|
|
});
|