From 308b67ed66dbe12fdbf3bcb5157fb137a4baeed5 Mon Sep 17 00:00:00 2001 From: Kpa-clawbot Date: Thu, 30 Apr 2026 23:28:30 -0700 Subject: [PATCH] chore: remove tautological/vacuous frontend tests (#938) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Remove tautological/vacuous frontend tests identified in the test audit (2026-04-30). ## Deleted Files - **`test-anim-perf.js`** — Tests local simulation functions, not actual `live.js` animation code. Behavioral equivalents exist in `test-live-anims.js`. - **`test-channel-add-ux.js`** — Pure HTML/CSS substring grep tests (`src.includes(...)`) with zero behavioral value. Any refactor breaks them without indicating a real bug. ## Edited Files - **`test-aging.js`** — Removed `mockGetStatusInfo` (5 tests) and `getStatusTooltip` (6 tests) blocks. These re-implement SUT logic inside the test file and assert on the local copy, not the real code. The GOLD-rated `getNodeStatus` boundary tests (18 assertions) and the BUG CHECK section are preserved. ## Why These tests are unfailable by construction — they pass regardless of whether the code under test is correct. They inflate test counts without providing regression protection, and they break on harmless refactors (false negatives). ## Validation - `npm run test:unit` passes (all 3 files: 62 + 18 + 601 assertions). - 9 pre-existing failures in `test-frontend-helpers.js` (hop-resolver affinity tests, unrelated to this change). - No harness references (`test-all.sh`, `package.json`) needed updating — deleted files were not listed there. Co-authored-by: you --- test-aging.js | 111 ------------------------------------- test-anim-perf.js | 123 ----------------------------------------- test-channel-add-ux.js | 64 --------------------- 3 files changed, 298 deletions(-) delete mode 100644 test-anim-perf.js delete mode 100644 test-channel-add-ux.js diff --git a/test-aging.js b/test-aging.js index eb8801bf..80e8476c 100644 --- a/test-aging.js +++ b/test-aging.js @@ -59,118 +59,7 @@ test('null lastSeenMs → stale', () => assert.strictEqual(getNodeStatus('repeat test('undefined lastSeenMs → stale', () => assert.strictEqual(getNodeStatus('repeater', undefined), 'stale')); test('0 lastSeenMs → stale', () => assert.strictEqual(getNodeStatus('repeater', 0), 'stale')); -// === getStatusInfo tests (inline since nodes.js has too many DOM deps) === -console.log('\n=== getStatusInfo (logic validation) ==='); -// Simulate getStatusInfo logic -function mockGetStatusInfo(n) { - const ROLE_COLORS = ctx.window.ROLE_COLORS; - const role = (n.role || '').toLowerCase(); - const roleColor = ROLE_COLORS[n.role] || '#6b7280'; - const lastHeardTime = n._lastHeard || n.last_heard || n.last_seen; - const lastHeardMs = lastHeardTime ? new Date(lastHeardTime).getTime() : 0; - const status = getNodeStatus(role, lastHeardMs); - const statusLabel = status === 'active' ? '🟢 Active' : '⚪ Stale'; - const isInfra = role === 'repeater' || role === 'room'; - - let explanation = ''; - if (status === 'active') { - explanation = 'Last heard recently'; - } else { - const reason = isInfra - ? 'repeaters typically advertise every 12-24h' - : 'companions only advertise when user initiates, this may be normal'; - explanation = 'Not heard — ' + reason; - } - return { status, statusLabel, roleColor, explanation, role }; -} - -test('active repeater → 🟢 Active, red color', () => { - const info = mockGetStatusInfo({ role: 'repeater', last_seen: new Date(now - 1*h).toISOString() }); - assert.strictEqual(info.status, 'active'); - assert.strictEqual(info.statusLabel, '🟢 Active'); - assert.strictEqual(info.roleColor, '#dc2626'); -}); - -test('stale companion → ⚪ Stale, explanation mentions "this may be normal"', () => { - const info = mockGetStatusInfo({ role: 'companion', last_seen: new Date(now - 25*h).toISOString() }); - assert.strictEqual(info.status, 'stale'); - assert.strictEqual(info.statusLabel, '⚪ Stale'); - assert(info.explanation.includes('this may be normal'), 'should mention "this may be normal"'); -}); - -test('missing last_seen → stale', () => { - const info = mockGetStatusInfo({ role: 'repeater' }); - assert.strictEqual(info.status, 'stale'); -}); - -test('missing role → defaults to empty string, uses node threshold', () => { - const info = mockGetStatusInfo({ last_seen: new Date(now - 25*h).toISOString() }); - assert.strictEqual(info.status, 'stale'); - assert.strictEqual(info.roleColor, '#6b7280'); -}); - -test('prefers last_heard over last_seen', () => { - // last_seen is stale, but last_heard is recent - const info = mockGetStatusInfo({ - role: 'companion', - last_seen: new Date(now - 48*h).toISOString(), - last_heard: new Date(now - 1*h).toISOString() - }); - assert.strictEqual(info.status, 'active'); -}); - -// === getStatusTooltip tests === -console.log('\n=== getStatusTooltip ==='); - -// Load from nodes.js by extracting the function -// Since nodes.js is complex, I'll re-implement the tooltip function for testing -function getStatusTooltip(role, status) { - const isInfra = role === 'repeater' || role === 'room'; - const threshold = isInfra ? '72h' : '24h'; - if (status === 'active') { - return 'Active — heard within the last ' + threshold + '.' + (isInfra ? ' Repeaters typically advertise every 12-24h.' : ''); - } - if (role === 'companion') { - return 'Stale — not heard for over ' + threshold + '. Companions only advertise when the user initiates — this may be normal.'; - } - if (role === 'sensor') { - return 'Stale — not heard for over ' + threshold + '. This sensor may be offline.'; - } - return 'Stale — not heard for over ' + threshold + '. This ' + role + ' may be offline or out of range.'; -} - -test('active repeater mentions "72h" and "advertise every 12-24h"', () => { - const tip = getStatusTooltip('repeater', 'active'); - assert(tip.includes('72h'), 'should mention 72h'); - assert(tip.includes('advertise every 12-24h'), 'should mention advertise frequency'); -}); - -test('active companion mentions "24h"', () => { - const tip = getStatusTooltip('companion', 'active'); - assert(tip.includes('24h'), 'should mention 24h'); -}); - -test('stale companion mentions "24h" and "user initiates"', () => { - const tip = getStatusTooltip('companion', 'stale'); - assert(tip.includes('24h'), 'should mention 24h'); - assert(tip.includes('user initiates'), 'should mention user initiates'); -}); - -test('stale repeater mentions "offline or out of range"', () => { - const tip = getStatusTooltip('repeater', 'stale'); - assert(tip.includes('offline or out of range'), 'should mention offline or out of range'); -}); - -test('stale sensor mentions "sensor may be offline"', () => { - const tip = getStatusTooltip('sensor', 'stale'); - assert(tip.includes('sensor may be offline')); -}); - -test('stale room uses 72h threshold', () => { - const tip = getStatusTooltip('room', 'stale'); - assert(tip.includes('72h')); -}); // === Bug check: renderRows uses last_seen instead of last_heard || last_seen === console.log('\n=== BUG CHECK ==='); diff --git a/test-anim-perf.js b/test-anim-perf.js deleted file mode 100644 index edc3e6d4..00000000 --- a/test-anim-perf.js +++ /dev/null @@ -1,123 +0,0 @@ -/** - * test-anim-perf.js — Performance benchmark for animation timer management - * - * Demonstrates that the rAF + concurrency-cap approach keeps active animation - * count bounded, whereas the old setInterval approach accumulated without limit. - * - * Run: node test-anim-perf.js - */ - -'use strict'; - -let passed = 0, failed = 0; -function assert(cond, msg) { - if (cond) { console.log(` ✅ ${msg}`); passed++; } - else { console.log(` ❌ ${msg}`); failed++; } -} - -// --------------------------------------------------------------------------- -// Simulate OLD behaviour: setInterval-based, no concurrency cap -// --------------------------------------------------------------------------- -function simulateOldModel(packetsPerSec, hopsPerPacket, durationSec) { - // Each hop spawns 3 intervals (pulse 26ms, line 33ms, fade 52ms). - // Pulse lasts ~2s, line ~0.66s, fade ~0.8s+0.4s ≈ 1.2s - // At any moment, timers from the last ~2s of packets are still alive. - const intervalLifetimes = [2.0, 0.66, 1.2]; // seconds each interval lives - let maxConcurrent = 0; - // Walk through time in 0.1s steps - const dt = 0.1; - const spawns = []; // {time, lifetime} - for (let t = 0; t < durationSec; t += dt) { - // Spawn timers for packets arriving in this window - const pktsInWindow = packetsPerSec * dt; - for (let p = 0; p < pktsInWindow; p++) { - for (let h = 0; h < hopsPerPacket; h++) { - for (const lt of intervalLifetimes) { - spawns.push({ time: t, lifetime: lt }); - } - } - } - // Count alive timers - const alive = spawns.filter(s => t < s.time + s.lifetime).length; - if (alive > maxConcurrent) maxConcurrent = alive; - } - return maxConcurrent; -} - -// --------------------------------------------------------------------------- -// Simulate NEW behaviour: rAF + MAX_CONCURRENT_ANIMS cap -// --------------------------------------------------------------------------- -function simulateNewModel(packetsPerSec, hopsPerPacket, durationSec) { - const MAX_CONCURRENT_ANIMS = 20; - let activeAnims = 0; - let maxConcurrent = 0; - const anims = []; // {endTime} - const dt = 0.1; - for (let t = 0; t < durationSec; t += dt) { - // Expire finished animations - while (anims.length && anims[0].endTime <= t) { - anims.shift(); - activeAnims--; - } - // Try to start new animations - const pktsInWindow = packetsPerSec * dt; - for (let p = 0; p < pktsInWindow; p++) { - if (activeAnims >= MAX_CONCURRENT_ANIMS) break; // cap reached — drop - activeAnims++; - // rAF animation lifetime: longest is pulse ~2s - anims.push({ endTime: t + 2.0 }); - } - // Sort by endTime so expiry works - anims.sort((a, b) => a.endTime - b.endTime); - if (activeAnims > maxConcurrent) maxConcurrent = activeAnims; - } - return maxConcurrent; -} - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -console.log('\n=== Animation timer accumulation: old vs new ==='); - -// Scenario: 5 pkts/sec, 3 hops each, 30 seconds -const oldPeak30s = simulateOldModel(5, 3, 30); -const newPeak30s = simulateNewModel(5, 3, 30); -console.log(` Old model (30s @ 5pkt/s×3hops): peak ${oldPeak30s} concurrent timers`); -console.log(` New model (30s @ 5pkt/s×3hops): peak ${newPeak30s} concurrent animations`); -assert(oldPeak30s > 100, `old model accumulates >100 timers (got ${oldPeak30s})`); -assert(newPeak30s <= 20, `new model stays ≤20 (got ${newPeak30s})`); - -// Scenario: 5 minutes sustained -const oldPeak5m = simulateOldModel(5, 3, 300); -const newPeak5m = simulateNewModel(5, 3, 300); -console.log(` Old model (5min @ 5pkt/s×3hops): peak ${oldPeak5m} concurrent timers`); -console.log(` New model (5min @ 5pkt/s×3hops): peak ${newPeak5m} concurrent animations`); -assert(oldPeak5m > 100, `old model at 5min still unbounded (got ${oldPeak5m})`); -assert(newPeak5m <= 20, `new model at 5min still ≤20 (got ${newPeak5m})`); - -// Scenario: burst — 20 pkts/sec for 10s -const oldBurst = simulateOldModel(20, 3, 10); -const newBurst = simulateNewModel(20, 3, 10); -console.log(` Old model (burst 20pkt/s×3hops, 10s): peak ${oldBurst} concurrent timers`); -console.log(` New model (burst 20pkt/s×3hops, 10s): peak ${newBurst} concurrent animations`); -assert(oldBurst > 200, `old model under burst >200 timers (got ${oldBurst})`); -assert(newBurst <= 20, `new model under burst stays ≤20 (got ${newBurst})`); - -console.log('\n=== drawAnimatedLine frame-drop catch-up ==='); - -// Read the source and verify catch-up logic exists -const fs = require('fs'); -const src = fs.readFileSync(__dirname + '/public/live.js', 'utf8'); - -// Extract the animateLine function body -const lineMatch = src.match(/function animateLine\(now\)\s*\{[\s\S]*?requestAnimationFrame\(animateLine\)/); -assert(lineMatch && /Math\.min\(Math\.floor\(elapsed\s*\/\s*33\)/.test(lineMatch[0]), - 'drawAnimatedLine catches up on frame drops (multi-tick per frame)'); - -const fadeMatch = src.match(/function animateFade\(now\)\s*\{[\s\S]*?requestAnimationFrame\(animateFade\)/); -assert(fadeMatch && /Math\.min\(Math\.floor\(fadeElapsed\s*\/\s*52\)/.test(fadeMatch[0]), - 'animateFade catches up on frame drops (multi-tick per frame)'); - -console.log(`\n${passed} passed, ${failed} failed\n`); -process.exit(failed ? 1 : 0); diff --git a/test-channel-add-ux.js b/test-channel-add-ux.js deleted file mode 100644 index 1de59009..00000000 --- a/test-channel-add-ux.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Tests for #759 — Add channel UX: button, hint, status feedback. - * Validates the HTML structure rendered by channels.js init. - */ -'use strict'; - -const fs = require('fs'); - -let passed = 0; -let failed = 0; - -function assert(cond, msg) { - if (cond) { passed++; console.log(' ✓ ' + msg); } - else { failed++; console.error(' ✗ ' + msg); } -} - -function assertIncludes(html, substr, msg) { - assert(html.includes(substr), msg); -} - -// Read the channels.js source to extract the HTML template -const src = fs.readFileSync(__dirname + '/public/channels.js', 'utf8'); - -// Extract the sidebar HTML from the template literal -const htmlMatch = src.match(/app\.innerHTML\s*=\s*`([\s\S]*?)`;/); -const html = htmlMatch ? htmlMatch[1] : ''; - -console.log('Test: Add channel UX (#759)'); - -// 1. Button renders in the form -assertIncludes(html, 'class="ch-add-btn"', 'Add button has ch-add-btn class'); -assertIncludes(html, 'type="submit"', 'Button is type=submit'); -assertIncludes(html, '>+', 'Button shows + text'); - -// 2. Form has proper structure -assertIncludes(html, 'class="ch-add-form"', 'Form has ch-add-form class'); -assertIncludes(html, 'class="ch-add-row"', 'Row wrapper present'); -assert(!html.includes('class="ch-add-label"'), 'Label removed (redundant with hint)'); - -// 3. Hint text present -assertIncludes(html, 'class="ch-add-hint"', 'Hint div present'); -assertIncludes(html, 'e.g. #LongFast or 32-char hex key', 'Hint text correct'); - -// 4. Status div present -assertIncludes(html, 'id="chAddStatus"', 'Status div has correct id'); -assertIncludes(html, 'class="ch-add-status"', 'Status div has correct class'); -assertIncludes(html, 'style="display:none"', 'Status div hidden by default'); - -// 5. showAddStatus function exists in source -assert(src.includes('function showAddStatus('), 'showAddStatus function defined'); -assert(src.includes("'success'"), 'Success status type referenced'); -assert(src.includes("'error'"), 'Error status type referenced'); - -// 6. CSS classes exist -const css = fs.readFileSync(__dirname + '/public/style.css', 'utf8'); -assert(css.includes('.ch-add-form'), 'CSS: .ch-add-form defined'); -assert(css.includes('.ch-add-btn'), 'CSS: .ch-add-btn defined'); -assert(css.includes('.ch-add-hint'), 'CSS: .ch-add-hint defined'); -assert(css.includes('.ch-add-status'), 'CSS: .ch-add-status defined'); -assert(css.includes('.ch-add-row'), 'CSS: .ch-add-row defined'); -// .ch-add-label CSS kept for backward compat but label removed from HTML - -console.log('\n' + passed + ' passed, ' + failed + ' failed'); -process.exit(failed > 0 ? 1 : 0);