- Surface device identity state, carrier details and upstream link health. - Add fleet-level identity monitoring and restart/baseband context actions. - Apply consistent notification scoping across fleet views.
301 lines
9.8 KiB
TypeScript
301 lines
9.8 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import { createFleetApiDataSource } from './fleet-api-data-source.js';
|
|
|
|
describe('Fleet API data source', () => {
|
|
it('loads the aggregate fleet overview and its resource summaries', async () => {
|
|
const fetcher = vi.fn(
|
|
async () =>
|
|
new Response(
|
|
JSON.stringify({
|
|
items: [
|
|
{
|
|
id: 'alpha',
|
|
name: 'Alpha',
|
|
origin: 'https://alpha.example/admin',
|
|
tags: ['lab'],
|
|
revision: 4,
|
|
capabilityStatus: 'unknown',
|
|
freshness: 'unknown',
|
|
credentialConfigured: false,
|
|
resources: {
|
|
cpuPercent: 23.4,
|
|
memoryPercent: 67.8,
|
|
maxTemperatureCelsius: 52.6,
|
|
phoneNumbers: ['13800000000'],
|
|
hardwareOnline: true,
|
|
controlOnline: true,
|
|
simPresent: true,
|
|
carrier: 'China Mobile',
|
|
cellularRegistration: 'registered_home',
|
|
accessTechnology: 'LTE',
|
|
cellularOnline: true,
|
|
signalPercent: 72,
|
|
uptimeSeconds: 93784.9,
|
|
version: '1.9.4',
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
{ status: 200, headers: { 'content-type': 'application/json' } },
|
|
),
|
|
);
|
|
|
|
const snapshot = await createFleetApiDataSource(fetcher as typeof fetch).load();
|
|
|
|
expect(snapshot.instances).toEqual([
|
|
{
|
|
id: 'alpha',
|
|
name: 'Alpha',
|
|
url: 'https://alpha.example/admin',
|
|
tags: ['lab'],
|
|
revision: 4,
|
|
},
|
|
]);
|
|
expect(snapshot.statuses.get('alpha')?.summary?.resources).toEqual({
|
|
cpuPercent: 23.4,
|
|
memoryPercent: 67.8,
|
|
maxTemperatureCelsius: 52.6,
|
|
phoneNumbers: ['13800000000'],
|
|
hardwareOnline: true,
|
|
controlOnline: true,
|
|
simPresent: true,
|
|
carrier: 'China Mobile',
|
|
cellularRegistration: 'registered_home',
|
|
accessTechnology: 'LTE',
|
|
cellularOnline: true,
|
|
signalPercent: 72,
|
|
uptimeSeconds: 93784,
|
|
});
|
|
expect(snapshot.statuses.get('alpha')?.summary?.version).toBe('1.9.4');
|
|
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
expect(fetcher).toHaveBeenCalledWith(
|
|
'/api/v1/fleet/overview',
|
|
expect.objectContaining({ credentials: 'same-origin' }),
|
|
);
|
|
});
|
|
|
|
it('emits skeleton and complete snapshots from the aggregate overview', async () => {
|
|
const fetcher = vi.fn(
|
|
async () =>
|
|
new Response(
|
|
JSON.stringify({
|
|
items: [
|
|
{
|
|
id: 'alpha',
|
|
name: 'Alpha',
|
|
origin: 'https://alpha.example/admin',
|
|
tags: [],
|
|
revision: 1,
|
|
capabilityStatus: 'unknown',
|
|
freshness: 'unknown',
|
|
credentialConfigured: false,
|
|
resources: { cpuPercent: 11, memoryPercent: 22 },
|
|
},
|
|
],
|
|
}),
|
|
{ status: 200, headers: { 'content-type': 'application/json' } },
|
|
),
|
|
);
|
|
const partials: unknown[] = [];
|
|
|
|
await createFleetApiDataSource(fetcher as typeof fetch).load(undefined, (snapshot) => {
|
|
partials.push({
|
|
resources: snapshot.statuses.get('alpha')?.summary?.resources,
|
|
freshness: snapshot.statuses.get('alpha')?.summary?.freshness,
|
|
});
|
|
});
|
|
|
|
expect(partials.length).toBe(2);
|
|
expect(partials[0]).toEqual({ resources: undefined, freshness: 'unknown' });
|
|
expect(partials.at(-1)).toEqual({
|
|
resources: { cpuPercent: 11, memoryPercent: 22 },
|
|
freshness: 'fresh',
|
|
});
|
|
});
|
|
|
|
it('rejects legacy or malformed envelopes instead of rendering a false empty fleet', async () => {
|
|
const fetcher = vi.fn(async () => new Response(JSON.stringify({ data: [] }), { status: 200 }));
|
|
await expect(createFleetApiDataSource(fetcher as typeof fetch).load()).rejects.toThrow(
|
|
'Fleet response is invalid.',
|
|
);
|
|
});
|
|
});
|
|
|
|
function overviewWith(connection: unknown) {
|
|
return vi.fn(
|
|
async () =>
|
|
new Response(
|
|
JSON.stringify({
|
|
items: [
|
|
{
|
|
id: 'alpha',
|
|
name: 'Alpha',
|
|
origin: 'https://alpha.example/admin',
|
|
tags: [],
|
|
revision: 1,
|
|
capabilityStatus: 'unknown',
|
|
freshness: 'unknown',
|
|
credentialConfigured: false,
|
|
connection,
|
|
resources: {},
|
|
},
|
|
],
|
|
}),
|
|
{ status: 200, headers: { 'content-type': 'application/json' } },
|
|
),
|
|
);
|
|
}
|
|
|
|
describe('Fleet resource bounds', () => {
|
|
const overviewWithResources = (resources: Record<string, unknown>) =>
|
|
vi.fn(
|
|
async () =>
|
|
new Response(
|
|
JSON.stringify({
|
|
items: [
|
|
{
|
|
id: 'alpha',
|
|
name: 'Alpha',
|
|
origin: 'https://alpha.example/admin',
|
|
tags: [],
|
|
revision: 1,
|
|
capabilityStatus: 'unknown',
|
|
freshness: 'unknown',
|
|
credentialConfigured: false,
|
|
resources,
|
|
},
|
|
],
|
|
}),
|
|
{ status: 200, headers: { 'content-type': 'application/json' } },
|
|
),
|
|
) as unknown as typeof fetch;
|
|
|
|
it('drops a negative or non-numeric uptime instead of rendering a bogus clock', async () => {
|
|
const negative = await createFleetApiDataSource(
|
|
overviewWithResources({ uptimeSeconds: -1 }),
|
|
).load();
|
|
const text = await createFleetApiDataSource(
|
|
overviewWithResources({ uptimeSeconds: '93784' }),
|
|
).load();
|
|
expect(negative.statuses.get('alpha')?.summary?.resources?.uptimeSeconds).toBeUndefined();
|
|
expect(text.statuses.get('alpha')?.summary?.resources?.uptimeSeconds).toBeUndefined();
|
|
});
|
|
|
|
it('keeps a zero uptime, which means the device just booted', async () => {
|
|
const snapshot = await createFleetApiDataSource(
|
|
overviewWithResources({ uptimeSeconds: 0 }),
|
|
).load();
|
|
expect(snapshot.statuses.get('alpha')?.summary?.resources?.uptimeSeconds).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('Fleet heartbeat connection state', () => {
|
|
it('carries a live heartbeat through to the row status', async () => {
|
|
const snapshot = await createFleetApiDataSource(
|
|
overviewWith({
|
|
probed: true,
|
|
reachable: true,
|
|
authenticated: true,
|
|
checkedAt: '2026-09-05T00:00:00.000Z',
|
|
}) as typeof fetch,
|
|
).load();
|
|
expect(snapshot.statuses.get('alpha')).toMatchObject({
|
|
probed: true,
|
|
reachable: true,
|
|
authenticated: true,
|
|
checkedAt: '2026-09-05T00:00:00.000Z',
|
|
});
|
|
});
|
|
|
|
it('marks a device the heartbeat lost as offline but still checked', async () => {
|
|
const snapshot = await createFleetApiDataSource(
|
|
overviewWith({
|
|
probed: true,
|
|
reachable: false,
|
|
authenticated: false,
|
|
checkedAt: '2026-09-05T00:00:00.000Z',
|
|
}) as typeof fetch,
|
|
).load();
|
|
expect(snapshot.statuses.get('alpha')).toMatchObject({
|
|
probed: true,
|
|
reachable: false,
|
|
authenticated: false,
|
|
checkedAt: '2026-09-05T00:00:00.000Z',
|
|
});
|
|
});
|
|
|
|
it('reports a device that has never been probed as unknown', async () => {
|
|
const snapshot = await createFleetApiDataSource(
|
|
overviewWith({
|
|
probed: false,
|
|
reachable: false,
|
|
authenticated: false,
|
|
checkedAt: null,
|
|
}) as typeof fetch,
|
|
).load();
|
|
expect(snapshot.statuses.get('alpha')).toMatchObject({
|
|
probed: false,
|
|
checkedAt: null,
|
|
});
|
|
});
|
|
|
|
it('keeps the optimistic shape for a server without the heartbeat journal', async () => {
|
|
const snapshot = await createFleetApiDataSource(overviewWith(undefined) as typeof fetch).load();
|
|
expect(snapshot.statuses.get('alpha')).toMatchObject({
|
|
reachable: true,
|
|
authenticated: true,
|
|
});
|
|
expect(snapshot.statuses.get('alpha')?.probed).toBeUndefined();
|
|
expect(snapshot.statuses.get('alpha')?.checkedAt).toBeUndefined();
|
|
});
|
|
|
|
it('applies the heartbeat state to the skeleton snapshot too', async () => {
|
|
const partials: unknown[] = [];
|
|
await createFleetApiDataSource(
|
|
overviewWith({
|
|
probed: true,
|
|
reachable: false,
|
|
authenticated: false,
|
|
checkedAt: null,
|
|
}) as typeof fetch,
|
|
).load(undefined, (snapshot) => {
|
|
partials.push(snapshot.statuses.get('alpha'));
|
|
});
|
|
expect(partials[0]).toMatchObject({ probed: true, reachable: false });
|
|
expect(partials.at(-1)).toMatchObject({ probed: true, reachable: false });
|
|
});
|
|
|
|
it('carries the identity guard verdict onto the row, in both passes', async () => {
|
|
const fetcher = vi.fn(async () => {
|
|
const base = overviewWith({ probed: true, reachable: true, authenticated: true });
|
|
const response = await base();
|
|
const body = await response.json();
|
|
body.items[0].identity = { tracked: true, status: 'pending', reasons: ['imei_claimed'] };
|
|
return new Response(JSON.stringify(body), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
});
|
|
const partials: unknown[] = [];
|
|
const snapshot = await createFleetApiDataSource(fetcher as unknown as typeof fetch).load(
|
|
undefined,
|
|
(value) => partials.push(value.statuses.get('alpha')),
|
|
);
|
|
|
|
expect(partials[0]).toMatchObject({ identity: { status: 'pending' } });
|
|
expect(snapshot.statuses.get('alpha')?.identity).toEqual({
|
|
tracked: true,
|
|
status: 'pending',
|
|
reasons: ['imei_claimed'],
|
|
});
|
|
});
|
|
|
|
it('leaves the row unbadged for a server without the identity guard', async () => {
|
|
const snapshot = await createFleetApiDataSource(
|
|
overviewWith(undefined) as unknown as typeof fetch,
|
|
).load();
|
|
expect(snapshot.statuses.get('alpha')?.identity).toBeUndefined();
|
|
});
|
|
});
|