clone

a hermes agent plugin for openspec: tools for spec workflows and a dashboard tab for browsing changes and specs

main
hermes-openspec / tests / lane-override-behavior.mjs
lane-override-behavior.mjs 10.6K
1#!/usr/bin/env node2/**3 * Lane override behavior tests — toggle semantics, phase pruning, source keying.4 *5 * These test pure helpers that must be added to plugin.js __test export:6 * - computeCollapsedSet(autoCollapsed, manualOverrides, statusOrder)7 * - toggleOverride(prev, status, autoCollapsed) → next overrides8 * - computeLanePhase(grouped, statusOrder) → phase signature string9 * - pruneStaleOverrides(prevOverrides, prevPhase, lanePhase) → pruned overrides10 * - workViewIsSourceKeyed → true (static check)11 *12 * Run: node tests/lane-override-behavior.mjs13 * Exit 0 = pass, exit 1 = failure.14 */1516import { readFileSync } from 'node:fs'17import { execSync } from 'node:child_process'18import { dirname, resolve } from 'node:path'19import { fileURLToPath } from 'node:url'2021const __dirname = dirname(fileURLToPath(import.meta.url))22const pluginPath = resolve(__dirname, '../desktop/plugin.js')23const source = readFileSync(pluginPath, 'utf8')2425let passed = 026let failed = 027const failures = []2829function ok(name) { passed++; console.log(' PASS ' + name) }30function fail(name, d) {31 failed++32 const msg = ' FAIL ' + name + (d ? ': ' + d : '')33 console.error(msg)34 failures.push({ name, detail: d })35}36function check(c, n, d) { c ? ok(n) : fail(n, d) }3738console.log('=== Lane override behavior tests ===\n')3940// ═══════════════════════════════════════════════════════════════════════════════41// 0. Static source checks — WorkView is keyed by selectedSource.id42// ═══════════════════════════════════════════════════════════════════════════════43console.log('[0] WorkView source keying (static)')4445// The WorkView render call must have a key prop bound to selectedSource.id46// Look for the pattern: jsx(WorkView, {..., key: selectedSource.id}) or47// key={selectedSource.id} near the WorkView usage48{49 const workViewKeyPattern = /key:\s*selectedSource\.id/50 check(workViewKeyPattern.test(source), 'WorkView render has key={selectedSource.id}',51 !workViewKeyPattern.test(source) ? 'Missing key prop on WorkView jsx() call' : undefined)52}5354// ═══════════════════════════════════════════════════════════════════════════════55// Load dynamic helpers56// ═══════════════════════════════════════════════════════════════════════════════57console.log('\n[Dynamic] Load __test exports')5859const helperPath = resolve(__dirname, '_dynamic_import_helper.mjs')6061let r62try {63 const raw = execSync('node ' + helperPath, { encoding: 'utf8', timeout: 30000 }).trim()64 r = JSON.parse(raw)65} catch (e) {66 fail('Dynamic import', (e.message || '').slice(0, 500))67 r = {}68}6970if (!r.__testAvailable) {71 fail('__test available', '__test not available — cannot run behavioral tests')72 console.log('\n=== Results: ' + passed + ' passed, ' + failed + ' failed ===')73 process.exit(failed > 0 ? 1 : 0)74}7576// ═══════════════════════════════════════════════════════════════════════════════77// 1. computeCollapsedSet — merge auto with manual overrides78// ═══════════════════════════════════════════════════════════════════════════════79console.log('\n[1] computeCollapsedSet')8081const cs = r.collapsedSet || {}82check(cs.autoOnlyEmpty !== undefined, 'computeCollapsedSet produces results')8384// Empty overrides → auto rules85check(cs.autoOnlyEmpty === true, 'Empty lane collapsed when board has work, no override',86 JSON.stringify(cs.autoOnlyEmpty))87check(cs.autoOnlyPopulated === true, 'Populated lane expanded when board has work, no override',88 JSON.stringify(cs.autoOnlyPopulated))8990// Manual override wins over auto91check(cs.overrideEmpty === true, 'Override overrides auto-collapse for empty lane (user expanded)',92 JSON.stringify(cs.overrideEmpty))93check(cs.overridePopulated === true, 'Override overrides auto-expand for populated lane (user collapsed)',94 JSON.stringify(cs.overridePopulated))9596// ═══════════════════════════════════════════════════════════════════════════════97// 2. toggleOverride — minimal deviation storage98// ═══════════════════════════════════════════════════════════════════════════════99console.log('\n[2] toggleOverride (minimal deviation)')100101const to = r.toggle || {}102check(typeof to.firstToggleEmpty === 'boolean', 'toggleOverride returns boolean for empty lane')103check(to.firstToggleEmpty === false, 'First toggle on auto-collapsed empty lane: stores false (expanded)',104 JSON.stringify(to.firstToggleEmpty))105check(to.secondToggleEmpty === undefined || to.secondToggleEmpty === null,106 'Second toggle on previously-toggled empty lane: deletes override (back to auto)',107 JSON.stringify(to.secondToggleEmpty))108109check(to.firstTogglePopulated === true, 'First toggle on auto-expanded populated lane: stores true (collapsed)',110 JSON.stringify(to.firstTogglePopulated))111check(to.secondTogglePopulated === undefined || to.secondTogglePopulated === null,112 'Second toggle on previously-toggled populated lane: deletes override (back to auto)',113 JSON.stringify(to.secondTogglePopulated))114115// ═══════════════════════════════════════════════════════════════════════════════116// 3. computeLanePhase — phase signature string117// ═══════════════════════════════════════════════════════════════════════════════118console.log('\n[3] computeLanePhase')119120const lp = r.lanePhase || {}121check(typeof lp.empty === 'string', 'computeLanePhase returns string')122check(lp.empty === 'todo:empty|in-progress:full|done:empty',123 'Phase signature for empty todo, populated in-progress, empty done',124 JSON.stringify(lp.empty))125check(lp.full === 'todo:full|in-progress:empty|done:full',126 'Phase signature for populated todo, empty in-progress, populated done',127 JSON.stringify(lp.full))128check(lp.nullGrouped === null || lp.nullGrouped === undefined,129 'computeLanePhase returns null for null grouped input',130 JSON.stringify(lp.nullGrouped))131132// ═══════════════════════════════════════════════════════════════════════════════133// 4. pruneStaleOverrides — phase-change pruning134// ═══════════════════════════════════════════════════════════════════════════════135console.log('\n[4] pruneStaleOverrides')136137const po = r.prune || {}138139// Phase changed on a lane with override → override pruned140check(po.changedLanePruned === true, 'Phase change on override lane prunes that override',141 JSON.stringify(po.changedLanePruned))142143// Phase unchanged on a lane with override → override preserved144check(po.unchangedLaneKept === true, 'Phase unchanged on override lane preserves override',145 JSON.stringify(po.unchangedLaneKept))146147// Phase changed on a lane WITHOUT override → nothing changes148check(po.changedNoOverride === true, 'Phase change on non-overridden lane is harmless',149 JSON.stringify(po.changedNoOverride))150151// Null previous phase (first render) → no pruning152check(po.nullPrevPhase === true, 'Null previous phase (first render) does not prune',153 JSON.stringify(po.nullPrevPhase))154155// Multiple overrides, mixed phase changes → only affected ones pruned156check(po.mixedPruning === true, 'Mixed phase changes: only changed-lane overrides pruned, others kept',157 JSON.stringify(po.mixedPruning))158159// ═══════════════════════════════════════════════════════════════════════════════160// 5. Toggle semantic integration — second toggle deletes override161// ═══════════════════════════════════════════════════════════════════════════════162console.log('\n[5] Toggle integration (toggle → toggle → auto restored)')163164const ti = r.toggleIntegration || {}165check(ti.autoCollapsedDeleted === true,166 'Toggle auto-collapsed empty lane twice: override deleted, auto collapsed again',167 JSON.stringify(ti.autoCollapsedDeleted))168check(ti.autoExpandedDeleted === true,169 'Toggle auto-expanded populated lane twice: override deleted, auto expanded again',170 JSON.stringify(ti.autoExpandedDeleted))171check(ti.partialToggleKept === true,172 'Toggle only one lane: other lane overrides preserved',173 JSON.stringify(ti.partialToggleKept))174175// ═══════════════════════════════════════════════════════════════════════════════176// Summary177// ═══════════════════════════════════════════════════════════════════════════════178console.log('\n=== Results: ' + passed + ' passed, ' + failed + ' failed ===')179if (failed > 0) {180 console.log('\nFailures:')181 failures.forEach(function(f) { console.log(' - ' + f.name + (f.detail ? ': ' + f.detail : '')) })182}183process.exit(failed > 0 ? 1 : 0)