'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const { buildDeviceTree, flattenVisibleRows, createSearchRunner, calculateVirtualWindow, } = require('../device-tree'); function baseFixture() { return { sites: [ { id: 's2', name: 'Zulu' }, { id: 's1', name: 'Alpha', pendingDeletionStart: '2026-08-19T12:00:00Z' }, ], groups: [ { id: 'g2', name: 'Doors', parentId: 's2' }, { id: 'g1', name: 'Lobby', parentId: 's1', pendingDeletionStart: '2026-08-19T12:00:00Z' }, ], devices: [ { id: 'c4', name: 'No Group', siteId: 's1', type: 'camera' }, { id: 'c2', name: 'Inferred', deviceGroupId: 'g2', model: 'H5A' }, { id: 'c1', name: 'Front', siteId: 's1', deviceGroupId: 'g1', address: '10.0.0.1' }, { id: 'c3', name: 'Conflict', siteId: 's1', deviceGroupId: 'g2' }, { id: 'c5', name: 'Unknown Group', siteId: 's1', deviceGroupId: 'missing-g' }, { id: 'c6', name: 'Unknown Site', siteId: 'missing-s' }, { id: 'c7', name: 'Orphan' }, ], }; } test('builds deterministic hierarchy with explicit exceptional buckets and diagnostics', () => { const model = buildDeviceTree(baseFixture()); assert.deepEqual(model.sites.map((site) => site.name), ['Alpha', 'Orphaned cameras', 'Unknown site: missing-s', 'Zulu']); assert.equal(model.sites[0].pendingDeletion, true); assert.deepEqual(model.sites[0].groups.map((group) => group.name), ['Doors', 'Lobby', 'Ungrouped', 'Unknown group: missing-g']); assert.equal(model.sites[0].groups.find((group) => group.id === 'g1').pendingDeletion, true); assert.deepEqual(model.sites[3].groups[0].cameras.map((camera) => camera.name), ['Inferred']); const conflict = model.cameraByKey.get('camera:c3'); assert.equal(conflict.site.id, 's1'); assert.equal(conflict.group.name, 'Doors'); assert.equal(conflict.hierarchyStatus, 'conflict'); assert.equal(model.cameraByKey.get('camera:c2').hierarchyStatus, 'inferred-site'); assert.ok(model.diagnostics.some((entry) => entry.code === 'site-group-conflict' && entry.key === 'camera:c3')); }); test('duplicate and malformed IDs get stable unique keys and diagnostics', () => { const fixture = { sites: [{ id: 's', name: 'A' }, { id: 's', name: 'B' }, { name: 'Bad' }], groups: [{ id: 'g', name: 'G', parentId: 's' }, { id: 'g', name: 'G2', parentId: 's' }], devices: [ { id: 'c', name: 'Same', siteId: 's', deviceGroupId: 'g' }, { id: 'c', name: 'Same', siteId: 's', deviceGroupId: 'g' }, { name: 'Missing', siteId: 's' }, ], }; const first = buildDeviceTree(fixture); const second = buildDeviceTree(fixture); assert.deepEqual([...first.rowByKey.keys()], [...second.rowByKey.keys()]); assert.equal(new Set(first.rowByKey.keys()).size, first.rowByKey.size); assert.ok(first.diagnostics.filter((entry) => entry.code === 'duplicate-id').length >= 3); assert.ok(first.diagnostics.filter((entry) => entry.code === 'malformed-id').length >= 2); }); test('sites start collapsed; expansion produces levels and complete ARIA metadata', () => { const model = buildDeviceTree(baseFixture()); let rows = flattenVisibleRows(model, { expandedKeys: new Set(), selectedKey: 'camera:c1' }); assert.equal(rows.length, 4); assert.ok(rows.every((row) => row.kind === 'site' && row.aria.level === 1 && row.aria.expanded === false)); assert.ok(rows.every((row) => row.aria.setsize === 4)); assert.equal(rows.hiddenSelected, true); rows = flattenVisibleRows(model, { expandedKeys: new Set(['site:s1']) }); assert.deepEqual(rows.slice(0, 5).map((row) => row.kind), ['site', 'group', 'group', 'group', 'group']); assert.ok(rows.slice(1, 5).every((row) => row.aria.level === 2 && row.aria.expanded === false)); rows = flattenVisibleRows(model, { expandedKeys: new Set(['site:s1', 'group:s1:g1']), selectedKey: 'camera:c1', }); const camera = rows.find((row) => row.key === 'camera:c1'); assert.equal(camera.parentKey, 'group:s1:g1'); assert.deepEqual(camera.aria, { level: 3, expanded: undefined, selected: true, posinset: 1, setsize: 1 }); assert.equal(rows.hiddenSelected, false); }); test('search covers camera fields and ancestors without mutating saved expansion', async () => { const model = buildDeviceTree(baseFixture()); const scheduled = []; const runner = createSearchRunner({ scheduler: (work) => scheduled.push(work), chunkSize: 2 }); const saved = new Set(['site:s2']); const promise = runner.search(model, 'alpha lobby 10.0.0.1', { expandedKeys: saved }); while (scheduled.length) scheduled.shift()(); const result = await promise; assert.equal(result.count, 1); assert.deepEqual([...result.cameraKeys], ['camera:c1']); assert.ok(result.expandedKeys.has('site:s1')); assert.ok(result.expandedKeys.has('group:s1:g1')); assert.deepEqual([...saved], ['site:s2']); const ancestorPromise = runner.search(model, 'zulu', { expandedKeys: new Set() }); while (scheduled.length) scheduled.shift()(); const ancestor = await ancestorPromise; assert.deepEqual([...ancestor.cameraKeys], ['camera:c2']); }); test('new search cancels stale chunked generation', async () => { const model = buildDeviceTree({ sites: [{ id: 's', name: 'Site' }], groups: [], devices: Array.from({ length: 700 }, (_, i) => ({ id: `c${i}`, name: `Camera ${i}`, siteId: 's' })), }); const scheduled = []; const runner = createSearchRunner({ scheduler: (work) => scheduled.push(work), chunkSize: 250 }); const stale = runner.search(model, 'camera', {}); scheduled.shift()(); const current = runner.search(model, 'camera 699', {}); while (scheduled.length) scheduled.shift()(); const [staleResult, currentResult] = await Promise.all([stale, current]); assert.equal(staleResult.cancelled, true); assert.equal(currentResult.cancelled, false); assert.equal(currentResult.count, 1); }); test('virtual window remains structurally bounded for 5,000 expanded shuffled cameras', () => { const devices = Array.from({ length: 5000 }, (_, i) => ({ id: `id-${i}`, name: `Camera ${String(i).padStart(4, '0')}`, siteId: 's' })); for (let i = devices.length - 1; i > 0; i -= 1) { const j = (i * 48271) % (i + 1); [devices[i], devices[j]] = [devices[j], devices[i]]; } const model = buildDeviceTree({ sites: [{ id: 's', name: 'Large' }], groups: [], devices }); const rows = flattenVisibleRows(model, { expandedKeys: new Set(['site:s', 'group:s:__ungrouped__']) }); const window = calculateVirtualWindow(rows, { scrollTop: 50000, viewportHeight: 320, rowHeight: 28 }); assert.ok(window.rows.length >= 40 && window.rows.length <= 50, `mounted ${window.rows.length}`); assert.equal(window.totalHeight, rows.length * 28); assert.equal(window.offsetTop, window.startIndex * 28); }); test('1,500-site/group fixture has deterministic stable ordering', () => { const sites = Array.from({ length: 1500 }, (_, i) => ({ id: `s-${i}`, name: `Site ${i % 17}` })); const groups = sites.map((site, i) => ({ id: `g-${i}`, name: `Group ${i % 11}`, parentId: site.id })); const model = buildDeviceTree({ sites: sites.reverse(), groups: groups.reverse(), devices: [] }); const namesAndIds = model.sites.map((site) => `${site.normalizedName}|${site.id}`); assert.deepEqual(namesAndIds, [...namesAndIds].sort((a, b) => a.localeCompare(b))); });