Files
harbor-net/test/web/device-inventory-contract.test.js
T
dokril 3cdb0c735e
Build and Deploy Gateway / build-and-push (push) Successful in 11s
Build and Deploy Gateway / deploy (push) Successful in 6s
Reposition Gateway traffic summary
2026-08-08 00:43:28 +03:00

242 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import {
formatByteString,
formatLastSeen,
positiveByteDelta,
sortDevicesByTraffic,
stabilizeDevicesByTraffic,
trafficAxisMid,
trafficScaleRatio,
} from '../../src/web/utils/format.js';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const overview = fs.readFileSync(path.join(root, 'src/web/components/ClientOverviewPage.jsx'), 'utf8');
const panel = fs.readFileSync(path.join(root, 'src/web/components/DevicesPanel.jsx'), 'utf8');
const chart = fs.readFileSync(path.join(root, 'src/web/components/TrafficChart.jsx'), 'utf8');
const api = fs.readFileSync(path.join(root, 'src/web/api.js'), 'utf8');
const server = fs.readFileSync(path.join(root, 'src/server/index.js'), 'utf8');
const styles = fs.readFileSync(path.join(root, 'src/web/styles.css'), 'utf8');
test('Gateway device inventory uses the existing accessible responsive drawer', () => {
assert.match(overview, /isGateway && <button[\s\S]*client-devices-toggle/);
assert.match(overview, /api\.devices\.list\(\)/);
assert.match(overview, /api\.devices\.refresh\(\)/);
assert.match(overview, /DEVICE_AUTO_REFRESH_MS = 15_000/);
assert.match(overview, /setDeviceSnapshot\(\(current\) => !current \|\| next\.revision > current\.revision/);
assert.match(overview, /snapshot=\{deviceSnapshot\}[\s\S]*onSnapshot=\{setDeviceSnapshot\}/);
assert.doesNotMatch(panel, /AUTO_REFRESH_MS|const \[snapshot, setSnapshot\]|setTimeout\(\(\) => load\(true\),/);
assert.match(panel, /api\.devices\.update\(device\.id, patch, snapshot\.revision\)/);
assert.match(panel, /requestError\.code !== 'STATE_CONFLICT'[\s\S]*api\.devices\.list\(\)[\s\S]*Object\.keys\(patch\)\.some\(\(key\) => latestDevice\[key\] !== device\[key\]\)[\s\S]*api\.devices\.update\(device\.id, patch, latest\.revision\)/);
assert.match(panel, /deviceNodes\.current\.get\(id\)\?\.animate/);
assert.match(panel, /movementAnimations\.current\.get\(id\)\?\.cancel\(\)/);
assert.match(panel, /const orderChanged = previousOrder\.current\.length > 0/);
assert.match(panel, /previousScrollTop\.current - currentScrollTop/);
assert.match(panel, /next\.revision > current\.revision/);
assert.doesNotMatch(panel, /revision >= current\.revision/);
assert.match(panel, /prefers-reduced-motion: reduce/);
assert.match(api, /refresh: \(\) => request\('\/api\/devices\/refresh', \{ method: 'POST' \}\)/);
assert.match(api, /setPolicy: \(id, mode, expectedRevision\) => request\(`\/api\/devices\/\$\{id\}\/policy`/);
assert.match(server, /requestUrl\.pathname === '\/api\/devices\/refresh'[\s\S]*deviceInventory\.refresh\(\)/);
assert.match(server, /\/api\\\/devices\\\/\(dev_\[a-f0-9\]\{16\}\)\\\/policy\$[\s\S]*deviceInventory\.setPolicy/);
assert.match(panel, /<Tooltip>Изменить название<\/Tooltip>/);
assert.match(panel, /copyText\(device\.ip\)/);
assert.match(panel, /const hasName = Boolean\(device\.alias \|\| device\.hostname\)/);
assert.match(panel, /client-device-alias-trigger[\s\S]*client-device-name-separator[\s\S]*client-device-ip/);
assert.match(panel, /onClick=\{\(\) => startEditing\(device\)\}/);
assert.match(panel, /\{!hasName && !editing && <span className="client-device-edit-wrap client-tooltip-anchor">/);
assert.match(panel, /client-device-name-heading\$\{hasName \? '' : ' is-address-only'\}\$\{editing \? ' is-editing' : ''\}/);
assert.match(panel, /className="client-device-alias-input"[\s\S]*onBlur=\{\(\) => saveAlias\(device\)\}[\s\S]*event\.key === 'Enter'[\s\S]*event\.currentTarget\.blur\(\)/);
assert.match(panel, /aliasBaseline\.current = \{ id: device\.id, value \}/);
assert.match(panel, /style=\{\{ '--alias-width': `\$\{Math\.max\(1, alias\.length\)\}ch` \}\}/);
assert.doesNotMatch(panel, /aliasWidth|getBoundingClientRect\(\)\.width/);
assert.match(panel, /nextAlias === aliasBaseline\.current\.value\.trim\(\)[\s\S]*setEditingId\(\(current\) => current === device\.id \? '' : current\)/);
assert.doesNotMatch(panel, /client-device-alias"|Сохранить название|Отменить изменение/);
assert.match(panel, /pencilAnimationId === device\.id \? ' is-writing'/);
assert.match(panel, /onAnimationEnd=\{\(\) => setPencilAnimationId/);
assert.match(panel, /COPY_FEEDBACK_MS = 800/);
assert.match(panel, /IP скопирован/);
assert.doesNotMatch(panel, /client-device-name-feedback|client-device-name-primary/);
assert.doesNotMatch(panel, /client-device-title/);
assert.match(panel, /online \? 'В сети' : <TextMorph from="Не в сети" to=\{seen\.relative\} \/>/);
assert.match(panel, /aria-label=\{`\$\{online \? 'В сети' : 'Не в сети'\}\. Последний контакт: \$\{seen\.tooltip\}`\}/);
assert.doesNotMatch(panel, /client-device-status|STATUS_LABELS/);
assert.doesNotMatch(panel, /client-text-morph-goo/);
assert.doesNotMatch(panel, /client-device-addresses|client-device-manufacturer|client-device-meta/);
assert.doesNotMatch(panel, /device\.interface/);
assert.doesNotMatch(panel, /device\.mac/);
assert.doesNotMatch(panel, /client-device-identity|Пояснение идентификации устройства|ⓘ/);
assert.match(panel, /device\.confidence === 'ambiguous'/);
assert.match(panel, /stabilizeDevicesByTraffic\(snapshot\?\.devices, sortDirection, previousIds\)/);
assert.match(panel, /Трафик временно не обновляется/);
assert.match(panel, /Учитывается только трафик, который прошёл через Harbor/);
assert.match(panel, /client-device-traffic-total[\s\S]*<b>Всего<\/b><TrafficValue value=\{totalTraffic\} delta=\{trafficDelta\.total\}/);
assert.match(panel, /client-device-traffic-breakdown[\s\S]*<b>Gateway<\/b><TrafficValue value=\{gatewayTraffic\} delta=\{trafficDelta\.gateway\}/);
assert.match(panel, /const hasProxyTraffic = proxyTotal > 0n/);
assert.match(panel, /client-device-traffic-breakdown[\s\S]*\{hasProxyTraffic && <span className="is-proxy"><b>Прокси<\/b><TrafficValue value=\{proxyTraffic\} delta=\{trafficDelta\.proxy\}/);
assert.match(panel, /TrafficChart[\s\S]*samples=\{device\.trafficHistory \|\| \[\]\}[\s\S]*scale=\{trafficScale\}[\s\S]*capacity=\{snapshot\.trafficHistoryCapacity/);
assert.doesNotMatch(panel, /setTrafficHistory|TRAFFIC_HISTORY_LIMIT/);
assert.match(panel, /aria-pressed=\{trafficScale === 'linear'\}[\s\S]*aria-pressed=\{trafficScale === 'log'\}/);
assert.match(chart, /previousScale\.current !== scale[\s\S]*attributeName="d"[\s\S]*dur="520ms"/);
assert.match(chart, /function smoothTrafficPath[\s\S]*const midX = \(previous\.x \+ point\.x\) \/ 2[\s\S]* C /);
assert.match(chart, /TRAFFIC_CHART_HEADROOM = 10[\s\S]*trafficChartY = \(ratio\) => 100 - ratio \* \(100 - TRAFFIC_CHART_HEADROOM\)/);
assert.match(chart, /gatewayY: trafficChartY\(trafficScaleRatio\(gateway, max, scale\)\)[\s\S]*proxyY: trafficChartY\(trafficScaleRatio\(proxy, max, scale\)\)/);
assert.match(chart, /client-device-traffic-grid[\s\S]*y1=\{TRAFFIC_CHART_HEADROOM\}[\s\S]*y1=\{\(100 \+ TRAFFIC_CHART_HEADROOM\) \/ 2\}/);
assert.match(chart, /client-device-traffic-cursor[\s\S]*y1=\{TRAFFIC_CHART_HEADROOM\} y2="100"/);
assert.doesNotMatch(panel, /key=\{latest\}/);
assert.match(chart, /createPortal\([\s\S]*client-device-traffic-point-tooltip[\s\S]*document\.body/);
assert.match(chart, /onPointerMove=\{trackPointer\}/);
assert.match(chart, /pinned && max > 0n && <span className="client-device-traffic-axis"[\s\S]*is-max[\s\S]*is-mid[\s\S]*is-zero/);
assert.match(chart, /client-device-traffic-grid[\s\S]*path className="is-gateway"[\s\S]*path className="is-proxy"/);
assert.match(chart, /client-device-traffic-cursor[\s\S]*className="is-point is-gateway"[\s\S]*className="is-point is-proxy"/);
assert.match(panel, /routeLabel=\{device\.appliedPolicy === 'direct' \? 'Напрямую' : 'Gateway'\}/);
assert.match(chart, /\{hovered\.proxy > 0n && <span className="is-proxy">Proxy \{formatByteString\(hovered\.proxy\)\}<\/span>\}/);
assert.match(chart, /<time dateTime=\{samples\[0\]\.observedAt\}>[\s\S]*<span>15 с<\/span>/);
assert.match(panel, /positiveByteDelta\(previous\.gateway, gateway\)[\s\S]*positiveByteDelta\(previous\.proxy, proxy\)/);
assert.match(panel, /setTimeout\(\(\) => setTrafficDeltas\(\{\}\), TRAFFIC_DELTA_MS\)/);
assert.doesNotMatch(panel, /client-device-traffic client-tooltip-anchor|<Tooltip>\{trafficLabel\}<\/Tooltip>/);
assert.match(panel, /source\?\.traffic\?\.proxy\?\.error/);
assert.match(panel, /client-device-pin-wrap[\s\S]*client-device-main[\s\S]*client-device-traffic[\s\S]*client-device-policy-wrap/);
assert.doesNotMatch(panel, /client-device-details/);
assert.match(panel, /api\.devices\.setPolicy\(device\.id, mode, snapshot\.revision\)/);
assert.match(panel, /requestError\.code !== 'STATE_CONFLICT'[\s\S]*latestDevice\.desiredPolicy !== device\.desiredPolicy[\s\S]*api\.devices\.setPolicy\(device\.id, mode, latest\.revision\)/);
assert.match(panel, /className=\{`client-device-policy is-\$\{displayPolicy\}/);
assert.match(panel, /displayPolicy === 'direct' \? <svg[\s\S]*M4 12h15M14 7l5 5-5 5[\s\S]*M12 3 19 6v5/);
assert.match(panel, /Полностью обходит sing-box/);
assert.match(panel, /client-drawer client-instructions client-devices/);
assert.doesNotMatch(panel, /точная MAC|частная MAC|<dt>Источник<\/dt>/);
assert.match(panel, /aria-pressed=\{device\.pinned\}/);
assert.doesNotMatch(panel, /Закрепите устройство, чтобы изменить маршрут|Сначала верните маршрут через Gateway/);
assert.match(panel, /maxLength="64"[\s\S]*autoFocus/);
assert.match(styles, /\.client-devices \{\s*width: min\(580px, 100vw\)/);
assert.match(styles, /\.client-device \{[\s\S]*--client-device-chart-height: 34px;[\s\S]*grid-template-columns: 34px minmax\(0, 1fr\) 112px 34px;[\s\S]*grid-template-rows: 34px var\(--client-device-chart-height\);[\s\S]*padding: 10px 8px/);
assert.match(styles, /\.client-device\.is-pinned \{[\s\S]*--client-device-chart-height: 72px/);
assert.match(styles, /\.client-device\.is-pinned \.client-device-traffic-chart \{[\s\S]*client-device-chart-expand 620ms/);
assert.match(styles, /\.client-device-main \{[\s\S]*display: flex;[\s\S]*align-items: center/);
assert.match(styles, /\.client-device-traffic-total,[\s\S]*\.client-device-traffic-breakdown > span \{[\s\S]*grid-template-columns: 46px minmax\(0, 1fr\)/);
assert.match(styles, /\.client-device-traffic-breakdown > span \{[\s\S]*translateY\(-12px\)[\s\S]*\.client-device-traffic:hover \.client-device-traffic-breakdown > span,[\s\S]*opacity: 1[\s\S]*translateY\(0\)/);
assert.match(styles, /\.client-device-traffic-breakdown \{[^}]*background: color-mix\(in oklch, var\(--client-bg\) 84%, transparent\);[^}]*backdrop-filter: blur\(8px\)/);
assert.doesNotMatch(styles, /\.client-device-traffic-breakdown \{[^}]*box-shadow:/);
assert.match(styles, /\.client-device-traffic-chart \{[\s\S]*grid-column: 1 \/ 4;[\s\S]*grid-template-columns: 34px minmax\(0, 1fr\);[\s\S]*column-gap: 8px/);
assert.match(styles, /\.client-device-traffic-plot \{[\s\S]*grid-column: 2;/);
assert.match(styles, /\.client-device-traffic-time \{[\s\S]*grid-column: 2;/);
assert.match(styles, /\.client-device-traffic-grid line \{[\s\S]*vector-effect: non-scaling-stroke/);
assert.match(styles, /\.client-device-traffic-lines path,[\s\S]*stroke-width: 1\.8/);
assert.match(styles, /\.client-device-traffic-cursor \.is-point \{[\s\S]*stroke-width: 4;[\s\S]*stroke-linecap: round;[\s\S]*vector-effect: non-scaling-stroke/);
assert.match(styles, /\.client-device-traffic-axis > \.is-max \{[\s\S]*top: var\(--traffic-chart-top\)/);
assert.match(styles, /\.client-device-traffic-axis > \.is-mid \{[\s\S]*top: var\(--traffic-chart-mid\)/);
assert.match(styles, /\.client-device-traffic-point-tooltip \{[\s\S]*position: fixed;[\s\S]*z-index: 1002/);
assert.match(styles, /@keyframes client-device-traffic-shift[\s\S]*translateX\(calc\(100% \/ var\(--sample-count\)\)\)[\s\S]*translateX\(0\)/);
assert.match(styles, /@keyframes client-device-traffic-line-draw[\s\S]*stroke-dashoffset: 0/);
assert.match(styles, /@keyframes client-device-chart-expand[\s\S]*clip-path: inset\(calc\(100% - 34px\) 0 0\)[\s\S]*scaleY\(1\)/);
assert.match(styles, /\.client-device-policy \{[\s\S]*width: 34px;[\s\S]*border-radius: 50%/);
assert.match(styles, /\.client-device-alias-trigger \{[\s\S]*font: 700 14px\/1\.2/);
assert.match(styles, /\.client-device-alias-input \{[\s\S]*width: var\(--alias-width\)[\s\S]*caret-color: var\(--client-accent\)[\s\S]*client-device-alias-edit-in 360ms/);
assert.match(styles, /@keyframes client-device-alias-edit-in[\s\S]*color: var\(--client-accent\)[\s\S]*filter: blur\(2px\)/);
assert.doesNotMatch(styles, /\.client-device-alias \{/);
assert.match(styles, /\.client-device-ip \{[\s\S]*font: 600 12px\/1\.2/);
assert.match(styles, /\.client-device-last-seen \{[\s\S]*position: absolute;[\s\S]*top: 0;[\s\S]*left: 0;[\s\S]*font-size: 8\.5px/);
assert.match(styles, /\.client-device-traffic strong \{[\s\S]*font-size: 10px/);
assert.match(styles, /\.client-device-name-separator \{[\s\S]*color: var\(--client-muted\)/);
assert.match(styles, /\.client-device-ip\.is-copied \{[\s\S]*client-device-ip-copy 800ms/);
assert.match(styles, /\.client-device-main \{[^}]*height: 34px[\s\S]*align-items: flex-end[\s\S]*padding: 11px 0 0/);
assert.match(styles, /\.client-device-main > h3\.is-address-only \{[\s\S]*flex: 0 1 auto/);
assert.match(styles, /\.client-device-last-seen \{[^}]*height: 10px[\s\S]*align-items: center/);
assert.match(styles, /\.client-device-traffic-value\.has-delta > \.is-total[\s\S]*translateY\(-0\.18em\)/);
assert.match(styles, /\.client-device-last-seen\.is-online \{[\s\S]*color: var\(--client-accent\)/);
assert.match(styles, /\.client-text-morph-value \{[\s\S]*transition: opacity 360ms[\s\S]*filter 480ms/);
assert.match(styles, /\.client-device-last-seen:hover \.client-text-morph-value\.is-relative[\s\S]*opacity: 1/);
assert.match(styles, /\.client-device-pin-wrap\.client-tooltip-anchor:hover > \.client-tooltip[\s\S]*translate\(0, 0\)/);
assert.match(styles, /\.client-device-pin:hover:not\(:disabled\) svg[\s\S]*translateY\(-2px\) rotate\(-12deg\)/);
assert.match(styles, /\.client-device-pin\[aria-pressed="true"\] svg/);
assert.match(styles, /\.client-device-edit-wrap \{[\s\S]*flex: 0 0 23px;[\s\S]*width: 23px/);
assert.doesNotMatch(styles, /\.client-device-edit-wrap \{[^}]*position: absolute/);
assert.match(styles, /\.client-device-edit\.is-writing svg \{[\s\S]*client-device-pencil-write 620ms/);
assert.match(styles, /@keyframes client-device-pencil-write[\s\S]*0%, 100%[\s\S]*translate\(1px, -1px\) rotate\(-5deg\)/);
assert.match(styles, /\.client-device-traffic-plot \{[\s\S]*height: 100%;[\s\S]*min-height: 0;[\s\S]*overflow: hidden/);
assert.match(styles, /\.client-device-traffic-plot svg \{[\s\S]*height: 100%;[\s\S]*overflow: hidden/);
assert.match(styles, /\.client-devices-sort:hover \.client-devices-sort-icon[\s\S]*rotate\(360deg\)/);
assert.match(styles, /\.client-devices-refresh-ring circle[\s\S]*client-devices-refresh-progress 15s/);
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-device-policy svg[\s\S]*\.client-device-alias-trigger[\s\S]*\.client-device-ip[\s\S]*\.client-device-traffic-value > span[\s\S]*\.client-device-traffic-breakdown[\s\S]*\.client-device-traffic-lines[\s\S]*\.client-text-morph-value/);
assert.match(styles, /\.client-drawer \{[\s\S]*z-index: 50;[\s\S]*box-shadow:/);
assert.match(styles, /\.harbor-versions \{[\s\S]*z-index: 40/);
});
test('Gateway Home reuses the canonical device snapshot for applied route and global traffic', () => {
assert.match(overview, /const appliedServerId = state\?\.selection\?\.appliedServerId \|\| ''/);
assert.match(overview, /appliedServer\?\.label \|\| 'VPN-сервер не используется'/);
assert.match(overview, /selectedServerId !== appliedServerId[\s\S]*Переключаем на \{desiredServer\.label\}/);
assert.match(overview, /formatByteString\(globalTraffic\?\.totalBytes \|\| '0'\)/);
assert.match(overview, /samples=\{globalTraffic\?\.history \|\| \[\]\}[\s\S]*capacity=\{deviceSnapshot\?\.trafficHistoryCapacity \|\| 120\}[\s\S]*routeLabel="Gateway"/);
assert.match(overview, /<section className="client-power-section"[\s\S]*client-connection-title[\s\S]*client-gateway-route-summary[\s\S]*client-state-detail[\s\S]*<section className="client-gateway-summary"[\s\S]*client-proxies/);
assert.doesNotMatch(overview, /<TrafficChart[\s\S]{0,240}scale=/);
assert.match(overview, /deviceStatus === 'error' \? deviceError : null/);
assert.match(overview, /if \(!isGateway && \(!connected \|\| !state\?\.singboxStartedAt\)\) return undefined/);
assert.match(overview, /trafficSourceError[\s\S]*Трафик не обновляется · последние данные/);
assert.match(overview, /client-subscription-drawer\$\{subscriptionOpen \? ' is-open' : ''\}/);
assert.match(overview, /const confirmingDeleteRef = useRef\(confirmingDelete\)[\s\S]*if \(confirmingDeleteRef\.current\) return/);
assert.match(overview, /onClick=\{\(\) => setConfirmingDelete\(true\)\}[\s\S]*open=\{confirmingDelete\}[\s\S]*onCancel=\{\(\) => setConfirmingDelete\(false\)\}/);
assert.match(overview, /aria-label=\{isGateway[\s\S]*Остановить Harbor Connect[\s\S]*Запустить Harbor Connect/);
assert.match(overview, /className="client-power-control client-tooltip-anchor"[\s\S]*aria-describedby=\{powerUnavailable \? 'gateway-power-unavailable'[\s\S]*Сначала добавьте подписку и выберите сервер/);
assert.match(overview, /const powerButton = <button[\s\S]*className="client-power"[\s\S]*\{isGateway \? <span[\s\S]*<\/span> : powerButton\}/);
assert.match(overview, /isGateway && <DevicesPanel[\s\S]*snapshot=\{deviceSnapshot\}/);
assert.doesNotMatch(panel, /const \[snapshot, setSnapshot\]|setTimeout\(\(\) => load\(true\),/);
});
test('traffic chart scale compares Gateway and Proxy before choosing the maximum', () => {
assert.match(chart, /function trafficSeriesMax[\s\S]*proxy > gateway \? proxy : gateway[\s\S]*proxy > largest \? proxy : largest/);
});
test('device last-seen copy is compact with precise accessible and relative forms', () => {
const lastSeen = new Date(2026, 7, 7, 13, 15);
const now = new Date(2026, 7, 7, 13, 27);
assert.deepEqual(
formatLastSeen(lastSeen.toISOString(), now),
{
label: 'Сегодня, 13:15',
relative: '12 минут назад',
tooltip: '7 августа 2026 г. в 13:15 (12 минут назад)',
},
);
});
test('device traffic formatting and sorting preserve uint64 precision and canonical ties', () => {
assert.equal(formatByteString('9007199254740993'), '8,0 ПБ');
assert.equal(formatByteString('1536'), '1,5 КБ');
assert.equal(formatByteString('invalid'), '0 Б');
assert.equal(positiveByteDelta('1048576', '3145728'), '2,0 МБ');
assert.equal(positiveByteDelta('3145728', '3145728'), '');
assert.equal(positiveByteDelta('3145728', '1048576'), '');
assert.equal(trafficScaleRatio('50', '100', 'linear'), 0.5);
assert.equal(Math.round(trafficScaleRatio('9', '99', 'log') * 100), 50);
assert.equal(trafficAxisMid('100', 'linear'), 50n);
assert.equal(trafficAxisMid('99', 'log'), 9n);
const devices = [
{ id: 'a', uploadBytes: '9007199254740993', downloadBytes: '0', proxyUploadBytes: '0' },
{ id: 'b', uploadBytes: '9007199254740992', downloadBytes: '2' },
{ id: 'd', uploadBytes: '15', downloadBytes: '5', proxyUploadBytes: '100' },
{ id: 'c', uploadBytes: '10', downloadBytes: '10', proxyDownloadBytes: '100' },
{ id: 'e', pinned: true, uploadBytes: '0', downloadBytes: '0' },
];
assert.deepEqual(sortDevicesByTraffic(devices, 'desc').map(({ id }) => id), ['e', 'b', 'a', 'c', 'd']);
assert.deepEqual(sortDevicesByTraffic(devices, 'asc').map(({ id }) => id), ['e', 'c', 'd', 'a', 'b']);
assert.deepEqual(
stabilizeDevicesByTraffic([
{ id: 'a', uploadBytes: '999' },
{ id: 'b', uploadBytes: '1' },
], 'desc', ['b', 'a']).ids,
['b', 'a'],
);
assert.deepEqual(
stabilizeDevicesByTraffic([
{ id: 'a', uploadBytes: '999' },
{ id: 'b', pinned: true, uploadBytes: '1' },
], 'desc', ['a', 'b']).ids,
['b', 'a'],
);
});