Add deprioritized device group
This commit is contained in:
@@ -79,6 +79,7 @@ test('malformed persisted devices and remote observations cannot enter canonical
|
||||
id: deviceId(mac),
|
||||
alias: '',
|
||||
pinned: false,
|
||||
deprioritized: false,
|
||||
hostname: null,
|
||||
manufacturer: null,
|
||||
source: 'neighbor',
|
||||
@@ -183,6 +184,7 @@ test('device inventory discovers, merges, persists metadata and expires anonymou
|
||||
assert.equal(snapshot.devices[0].status, 'recent');
|
||||
|
||||
snapshot = service.update(snapshot.devices[0].id, { alias: 'Телевизор', pinned: true }, snapshot.revision);
|
||||
const namedId = snapshot.devices[0].id;
|
||||
const restarted = createDeviceInventoryService({ store, observe: async () => observation, vendor, now: () => current });
|
||||
assert.deepEqual(restarted.snapshot().devices[0], snapshot.devices[0]);
|
||||
assert.throws(
|
||||
@@ -190,15 +192,57 @@ test('device inventory discovers, merges, persists metadata and expires anonymou
|
||||
(error) => error.code === 'STATE_CONFLICT',
|
||||
);
|
||||
|
||||
observation = { observedAt: current.toISOString(), observations: [], error: 'source unavailable' };
|
||||
current = new Date('2026-07-01T10:06:00.000Z');
|
||||
observation = {
|
||||
observedAt: current.toISOString(),
|
||||
observations: parseNeighborSnapshot([
|
||||
{ dst: '192.168.50.18', dev: 'br0', lladdr: '00:11:22:33:44:55', state: ['REACHABLE'] },
|
||||
], current.toISOString()),
|
||||
error: null,
|
||||
};
|
||||
snapshot = await restarted.refresh();
|
||||
assert.equal(snapshot.devices.length, 1);
|
||||
assert.equal(snapshot.devices[0].id, namedId);
|
||||
assert.equal(snapshot.devices[0].ip, '192.168.50.18');
|
||||
assert.equal(snapshot.devices[0].alias, 'Телевизор');
|
||||
assert.equal(snapshot.devices[0].pinned, true);
|
||||
|
||||
observation = {
|
||||
observedAt: current.toISOString(),
|
||||
observations: parseNeighborSnapshot([
|
||||
{ dst: '192.168.50.18', dev: 'br0', lladdr: '00:11:22:33:44:66', state: ['REACHABLE'] },
|
||||
], current.toISOString()),
|
||||
error: null,
|
||||
};
|
||||
snapshot = await restarted.refresh();
|
||||
const replacement = snapshot.devices.find(({ mac: deviceMac }) => deviceMac === '00:11:22:33:44:66');
|
||||
assert.notEqual(replacement.id, namedId);
|
||||
assert.equal(replacement.alias, '');
|
||||
assert.equal(replacement.pinned, false);
|
||||
|
||||
snapshot = restarted.update(namedId, { deprioritized: true }, snapshot.revision);
|
||||
const deprioritized = snapshot.devices.find(({ id }) => id === namedId);
|
||||
assert.equal(deprioritized.pinned, false);
|
||||
assert.equal(deprioritized.deprioritized, true);
|
||||
assert.throws(
|
||||
() => restarted.update(namedId, { pinned: true, deprioritized: true }, snapshot.revision),
|
||||
(error) => error.code === 'REQUEST_INVALID',
|
||||
);
|
||||
const persisted = createDeviceInventoryService({ store, observe: async () => observation, vendor, now: () => current });
|
||||
assert.equal(persisted.snapshot().devices.find(({ id }) => id === namedId).deprioritized, true);
|
||||
|
||||
observation = { observedAt: current.toISOString(), observations: [], error: 'source unavailable' };
|
||||
snapshot = await persisted.refresh();
|
||||
assert.equal(snapshot.devices.length, 2);
|
||||
assert.equal(snapshot.source.error, 'source unavailable');
|
||||
|
||||
snapshot = restarted.update(snapshot.devices[0].id, { alias: '', pinned: false }, snapshot.revision);
|
||||
snapshot = persisted.update(namedId, { alias: '' }, snapshot.revision);
|
||||
current = new Date('2026-08-02T10:00:00.000Z');
|
||||
observation = { observedAt: current.toISOString(), observations: [], error: null };
|
||||
snapshot = await restarted.refresh();
|
||||
snapshot = await persisted.refresh();
|
||||
assert.deepEqual(snapshot.devices.map(({ id }) => id), [namedId]);
|
||||
snapshot = persisted.update(namedId, { deprioritized: false }, snapshot.revision);
|
||||
snapshot = await persisted.refresh();
|
||||
assert.equal(snapshot.devices.length, 0);
|
||||
|
||||
const failed = readNeighborSnapshot(() => ({ status: 1, stderr: 'not available' }), () => current);
|
||||
|
||||
@@ -75,7 +75,7 @@ test('device route forwards list and refresh query paths as raw JSON responses',
|
||||
});
|
||||
|
||||
test('device route forwards metadata patch and policy arguments without coercion', async () => {
|
||||
const patch = { expectedRevision: 7, alias: 'Desk', pinned: false, extra: 0 };
|
||||
const patch = { expectedRevision: 7, alias: 'Desk', pinned: false, deprioritized: true, extra: 0 };
|
||||
const metadata = createHarness({ body: patch });
|
||||
const metadataResponse = response();
|
||||
assert.equal(await metadata.route.handle({
|
||||
@@ -85,7 +85,7 @@ test('device route forwards metadata patch and policy arguments without coercion
|
||||
assert.deepEqual(metadata.calls, [[
|
||||
'update',
|
||||
deviceId,
|
||||
{ alias: 'Desk', pinned: false, extra: 0 },
|
||||
{ alias: 'Desk', pinned: false, deprioritized: true, extra: 0 },
|
||||
7,
|
||||
]]);
|
||||
assert.deepEqual(metadataResponse.payload, { revision: 3 });
|
||||
|
||||
@@ -116,12 +116,22 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
||||
assert.doesNotMatch(panel, /точная MAC|частная MAC|<dt>Источник<\/dt>/);
|
||||
assert.match(panel, /aria-pressed=\{device\.pinned\}/);
|
||||
assert.match(panel, /const \[pinCollapses, setPinCollapses\][\s\S]*animationDone[\s\S]*result: saved \? 'saved' : 'failed'/);
|
||||
assert.match(panel, /const expanded = device\.pinned && !collapsing[\s\S]*const showDetails = device\.pinned \|\| collapsing[\s\S]*pinned=\{showDetails\}[\s\S]*onCollapseEnd=/);
|
||||
assert.match(panel, /const compact = deprioritized && !collapsing[\s\S]*const expanded = device\.pinned && !collapsing[\s\S]*const showDetails = !compact && \(device\.pinned \|\| collapsing\)[\s\S]*pinned=\{showDetails\}[\s\S]*onCollapseEnd=/);
|
||||
assert.match(panel, /Закреплённые[\s\S]*Убраны вниз[\s\S]*Остальные/);
|
||||
assert.match(panel, /client-device-group-heading[\s\S]*role=\{groupStart \? 'heading' : undefined\}[\s\S]*aria-level=\{groupStart \? 3 : undefined\}/);
|
||||
assert.match(panel, /toggleDeprioritized[\s\S]*deprioritized: !deprioritized[\s\S]*deprioritized: true/);
|
||||
assert.match(panel, /client-device-deprioritize[\s\S]*aria-pressed=\{deprioritized\}[\s\S]*Вернуть \$\{title\} в основной список[\s\S]*Убрать \$\{title\} вниз/);
|
||||
assert.match(panel, /\{!compact && <TrafficChart/);
|
||||
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-deprioritized \{[\s\S]*grid-template-rows: 34px;[\s\S]*padding-block: 6px/);
|
||||
assert.match(styles, /\.client-device\.is-deprioritized \.client-device-main \{[\s\S]*grid-column: 1 \/ 4/);
|
||||
assert.match(styles, /\.client-device-deprioritize-wrap \{[\s\S]*grid-column: 4;[\s\S]*grid-row: 2/);
|
||||
assert.match(styles, /\.client-device\.is-deprioritized \.client-device-deprioritize-wrap \{[\s\S]*grid-row: 1/);
|
||||
assert.match(styles, /\.client-device-group-heading \{[\s\S]*text-transform: uppercase/);
|
||||
assert.match(styles, /\.client-device\.is-pinned \.client-device-traffic-plot \{[\s\S]*client-device-traffic-plot-expand 520ms/);
|
||||
assert.match(styles, /\.client-device\.is-collapsing \.client-device-traffic-plot \{[\s\S]*client-device-traffic-plot-collapse 520ms/);
|
||||
assert.match(styles, /\.client-device\.is-collapsing \.client-device-traffic-axis \{[\s\S]*top: 0;[\s\S]*height: 58px/);
|
||||
@@ -156,7 +166,7 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
||||
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-main > h4\.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\)/);
|
||||
@@ -249,9 +259,10 @@ test('device traffic formatting and sorting preserve uint64 precision and canoni
|
||||
{ id: 'd', uploadBytes: '15', downloadBytes: '5', proxyUploadBytes: '100' },
|
||||
{ id: 'c', uploadBytes: '10', downloadBytes: '10', proxyDownloadBytes: '100' },
|
||||
{ id: 'e', pinned: true, uploadBytes: '0', downloadBytes: '0' },
|
||||
{ id: 'f', deprioritized: true, uploadBytes: '999999', 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(sortDevicesByTraffic(devices, 'desc').map(({ id }) => id), ['e', 'b', 'a', 'c', 'd', 'f']);
|
||||
assert.deepEqual(sortDevicesByTraffic(devices, 'asc').map(({ id }) => id), ['e', 'c', 'd', 'a', 'b', 'f']);
|
||||
assert.deepEqual(
|
||||
stabilizeDevicesByTraffic([
|
||||
{ id: 'a', uploadBytes: '999' },
|
||||
@@ -266,4 +277,12 @@ test('device traffic formatting and sorting preserve uint64 precision and canoni
|
||||
], 'desc', ['a', 'b']).ids,
|
||||
['b', 'a'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
stabilizeDevicesByTraffic([
|
||||
{ id: 'a', deprioritized: true, uploadBytes: '999' },
|
||||
{ id: 'b', uploadBytes: '1' },
|
||||
{ id: 'c', pinned: true, uploadBytes: '0' },
|
||||
], 'desc', ['a', 'b', 'c']).ids,
|
||||
['c', 'b', 'a'],
|
||||
);
|
||||
});
|
||||
|
||||
@@ -50,6 +50,7 @@ test('all unknown inventory results pass one identity-preserving runtime parser'
|
||||
lastSeenAt: null,
|
||||
status: 'online',
|
||||
pinned: true,
|
||||
deprioritized: false,
|
||||
downloadBytes: '12',
|
||||
uploadBytes: '30',
|
||||
proxyDownloadBytes: '0',
|
||||
@@ -99,6 +100,8 @@ test('all unknown inventory results pass one identity-preserving runtime parser'
|
||||
{ ...valid, devices: [{ ...valid.devices[0], uploadBytes: '-1' }] },
|
||||
{ ...valid, devices: [{ ...valid.devices[0], proxyUploadBytes: 1 }] },
|
||||
{ ...valid, devices: [{ ...valid.devices[0], status: 'connected' }] },
|
||||
{ ...valid, devices: [{ ...valid.devices[0], deprioritized: 'yes' }] },
|
||||
{ ...valid, devices: [{ ...valid.devices[0], pinned: true, deprioritized: true }] },
|
||||
{ ...valid, devices: [{ ...valid.devices[0], desiredPolicy: 'automatic' }] },
|
||||
{ ...valid, devices: [{ ...valid.devices[0], policyStatus: 'queued' }] },
|
||||
{ ...valid, devices: [{ ...valid.devices[0], confidence: 'unknown' }] },
|
||||
|
||||
@@ -36,26 +36,26 @@ const expectedImports = [
|
||||
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
||||
const acceptedLedger = {
|
||||
counts: {
|
||||
cascadeEdges: 804,
|
||||
cascadeEdges: 821,
|
||||
customProperties: 31,
|
||||
declarations: 2914,
|
||||
declarations: 2941,
|
||||
important: 0,
|
||||
keyframes: 51,
|
||||
media: 10,
|
||||
rules: 878,
|
||||
variableReferences: 336,
|
||||
rules: 888,
|
||||
variableReferences: 338,
|
||||
},
|
||||
hashes: {
|
||||
cascadeEdges: '18e5bdda435293145dbb52319f1e842cc8b7cc3e8beb60ec381c26d0410cfe44',
|
||||
cascadeEdges: '1ffc2a2f986970988643129ff3e32f19c31bb8b1e018a3a474a976e23adc9776',
|
||||
customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545',
|
||||
declarations: '01152af9f22aa7a7654356f81b8a6110488f9d70754b2517d1f6bafeff4b14fd',
|
||||
declarations: '77314876411a21c88472dc277a59ca24192ca7d38de1ce8f367e7bd1007c5323',
|
||||
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
||||
duplicateSelectors: '1565bf06e07fd7cbf601d24846bb3e1059d06720ace1544f2ac7f6ecf36cab47',
|
||||
keyframes: '0bed7ec3cd3a86ee091cf9b07c081ab2364ac5a17e55e95bc9c4a6430419b019',
|
||||
ruleDeclarationSequences: '7ffd551563a8d801bcd5e5f9886b9551940dd9561240f0f8a771a85d44fc19db',
|
||||
selectors: '09962ebb3bc769f74ae785a44f8e032c0a83985ce14ea652135732b5af69042a',
|
||||
variableReferences: '5988cb6c9b658d380c5b51c04621c0579e1cc5e8abcd764d78a9d42b802252c9',
|
||||
witnesses: '68d7c48f20f7a8ae2c0a6ddabe743dfb84fd02fba303e3cc9d3b902efd0f8758',
|
||||
ruleDeclarationSequences: 'ce3831db5966264c17fa3af3e10fc6a41398cc619d061fa155ac43b47f0947ea',
|
||||
selectors: '828cf6e95b03c890e3a969be20eb05be1f70872d6076e8f9a6a38cf6b415d012',
|
||||
variableReferences: '9b6214da6e8e02b01deb6d5a98fe4c695a1fdaff4f65c936b805d7d5841db04d',
|
||||
witnesses: 'c34721ec2cb99104eae77841ae88ddf77cde278d15c21675a020f3cfdd13a28a',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -110,7 +110,7 @@ test('tokens, shared primitives, and feature styles have one explicit owner', ()
|
||||
|
||||
test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => {
|
||||
const witnesses = readStyleWitnesses(root);
|
||||
assert.equal(witnesses.length, 709);
|
||||
assert.equal(witnesses.length, 716);
|
||||
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
|
||||
const ledger = createStyleLedger(readStyleSource(root), { witnesses });
|
||||
assert.deepEqual(ledger.counts, acceptedLedger.counts);
|
||||
@@ -293,8 +293,8 @@ test('main owns one public stylesheet and the regrouped production CSS is determ
|
||||
assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1);
|
||||
|
||||
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
|
||||
assert.deepEqual(assets, ['index-BJnEzGgg.css']);
|
||||
assert.deepEqual(assets, ['index-DQr1ElW4.css']);
|
||||
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
|
||||
assert.equal(built.byteLength, 109140);
|
||||
assert.equal(sha256(built), '9e1f7811080177f366b693a77e92c615aff1a74537b34d597100c9707a8aea95');
|
||||
assert.equal(built.byteLength, 110647);
|
||||
assert.equal(sha256(built), '91bf07a74af3312f9b048fdebd642e811fafc3bb2ec49b55592a35f085c25f41');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user