'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()); const camera = (id) => [...model.cameraByKey.values()].find((entry) => entry.canonicalId === id); 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 = camera('c3'); assert.equal(conflict.site.id, 's1'); assert.equal(conflict.group.name, 'Doors'); assert.equal(conflict.hierarchyStatus, 'conflict'); assert.equal(camera('c2').hierarchyStatus, 'inferred-site'); assert.ok(model.diagnostics.some((entry) => entry.code === 'site-group-conflict' && entry.key === conflict.key)); }); 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()); const site = model.sites.find((entry) => entry.canonicalId === 's1' && !entry.synthetic); const group = site.groups.find((entry) => entry.canonicalId === 'g1' && !entry.synthetic); const selected = [...model.cameraByKey.values()].find((entry) => entry.canonicalId === 'c1'); let rows = flattenVisibleRows(model, { expandedKeys: new Set(), selectedKey: selected.key }); 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.key]) }); 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.key, group.key]), selectedKey: selected.key, }); const camera = rows.find((row) => row.key === selected.key); assert.equal(camera.parentKey, group.key); 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 site = (id) => model.sites.find((entry) => entry.canonicalId === id && !entry.synthetic); const camera = (id) => [...model.cameraByKey.values()].find((entry) => entry.canonicalId === id); const scheduled = []; const runner = createSearchRunner({ scheduler: (work) => scheduled.push(work), chunkSize: 2 }); const saved = new Set([site('s2').key]); 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').key]); assert.ok(result.expandedKeys.has(site('s1').key)); assert.ok(result.expandedKeys.has(camera('c1').group.key)); assert.deepEqual([...saved], [site('s2').key]); const ancestorPromise = runner.search(model, 'zulu', { expandedKeys: new Set() }); while (scheduled.length) scheduled.shift()(); const ancestor = await ancestorPromise; assert.deepEqual([...ancestor.cameraKeys], [camera('c2').key]); }); 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([model.sites[0].key, model.sites[0].groups[0].key]), }); 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))); }); test('tenant and synthetic namespaces stay distinct for reserved and ambiguous IDs', () => { const model = buildDeviceTree({ sites: [ { id: '__orphaned__', name: 'Tenant Reserved Site' }, { id: '__unknown_site__:missing', name: 'Tenant Unknown-shaped Site' }, { id: 'a', name: 'A' }, { id: 'a:b', name: 'A colon B' }, { id: 'percent%:site', name: 'Percent Site' }, ], groups: [ { id: '__ungrouped__', name: 'Tenant Reserved Group', parentId: '__orphaned__' }, { id: 'b:c', name: 'Tuple One', parentId: 'a' }, { id: 'c', name: 'Tuple Two', parentId: 'a:b' }, { id: 'g:%', name: 'Escaped Group', parentId: 'percent%:site' }, ], devices: [ { id: 'tenant-group', siteId: '__orphaned__', deviceGroupId: '__ungrouped__' }, { id: 'synthetic-group', siteId: '__orphaned__' }, { id: 'orphan' }, { id: 'unknown-site', siteId: 'missing' }, { id: 'tuple-one', siteId: 'a', deviceGroupId: 'b:c' }, { id: 'tuple-two', siteId: 'a:b', deviceGroupId: 'c' }, { id: 'escaped', siteId: 'percent%:site', deviceGroupId: 'g:%' }, ], }); const tenantReserved = model.sites.find((site) => !site.synthetic && site.id === '__orphaned__'); const orphanBucket = model.sites.find((site) => site.synthetic && site.hierarchyStatus === 'orphan'); const unknownBucket = model.sites.find((site) => site.synthetic && site.hierarchyStatus === 'unknown-site'); assert.ok(tenantReserved && orphanBucket && unknownBucket); assert.notEqual(tenantReserved.key, orphanBucket.key); const camera = (id) => [...model.cameraByKey.values()].find((entry) => entry.canonicalId === id); assert.notEqual(camera('tenant-group').group.key, camera('synthetic-group').group.key); assert.notEqual(camera('tuple-one').group.key, camera('tuple-two').group.key); assert.equal(model.rowByKey.size, model.sites.length + model.sites.reduce((count, site) => count + site.groups.length, 0) + model.cameraCount); assert.ok([...model.rowByKey.keys()].every((key) => !/\s/.test(key))); }); test('malformed Unicode metadata builds deterministically and remains searchable and visible', async () => { const fixture = { sites: [{ id: 'site-\ud800', name: 'Site \ud83d\udcf7 \udc00' }], groups: [{ id: 'group-\udc00', name: 'Group \ud83d\udcf7 \ud800', parentId: 'site-\ud800' }], devices: [{ id: 'camera-valid', name: 'Lobby \ud800 camera', siteId: 'site-\ud800', deviceGroupId: 'group-\udc00', address: 'Floor \udc00', }], }; const first = buildDeviceTree(fixture); const second = buildDeviceTree(fixture); assert.equal(first.cameraCount, 1); assert.deepEqual([...first.rowByKey.keys()], [...second.rowByKey.keys()]); assert.doesNotMatch([...first.rowByKey.keys()].join(''), /[\ud800-\udfff]/u); const camera = [...first.cameraByKey.values()][0]; assert.equal(camera.name, 'Lobby \ufffd camera'); assert.equal(camera.site.name, 'Site \ud83d\udcf7 \ufffd'); assert.equal(camera.group.name, 'Group \ud83d\udcf7 \ufffd'); assert.equal(camera.source.address, 'Floor \ufffd'); assert.doesNotMatch(JSON.stringify(camera.source), /[\ud800-\udfff]/u); assert.match(camera.searchCorpus, /floor \ufffd/); const result = await createSearchRunner().search(first, 'lobby \ufffd'); assert.deepEqual([...result.cameraKeys], [camera.key]); }); test('virtual window clamps an obsolete scroll offset after rows shrink', () => { const rows = [{ key: 'only-row' }]; const window = calculateVirtualWindow(rows, { scrollTop: 10000, viewportHeight: 320, rowHeight: 28, overscan: 0, }); assert.deepEqual(window.rows, rows); assert.equal(window.startIndex, 0); assert.equal(window.endIndex, 1); });