From 7e4e30a1790ced2b745a73e9c83e41d6e224bac6 Mon Sep 17 00:00:00 2001 From: Agando Date: Tue, 27 Oct 2026 16:07:35 +0100 Subject: fix(newos4): the loop guard reads the config it documents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MAINT-004. The guard read warnAt/blockAt/hardStopAt while one config path wrote `threshold`, and `enabled` was never consulted anywhere — writing {"enabled": false} into webx11_ai_config changed nothing at all. One resolver, _loopPolicy(), now normalises both spellings, guarantees warnAt <= blockAt < hardStopAt, and both the block and the warning are gated on enabled. Live on v4.6.110 (_loopPolicy called in the page): defaults {warn 3, block 6, hardstop 9}; {threshold: 4} -> {warn 2, block 4, hardstop 7}; {enabled: false, threshold: 3} -> enabled false. test-loop-policy.js: 17 asserts, RED on HEAD ("_loopPolicy is not a function"), suite 16/16. --- modules/test/test-loop-policy.js | 74 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 modules/test/test-loop-policy.js (limited to 'modules/test/test-loop-policy.js') diff --git a/modules/test/test-loop-policy.js b/modules/test/test-loop-policy.js new file mode 100644 index 0000000..340e308 --- /dev/null +++ b/modules/test/test-loop-policy.js @@ -0,0 +1,74 @@ +// modules/test/test-loop-policy.js — run: node +// MAINT-004: the loop guard must read the config it documents — `threshold` as an alias, and +// `enabled: false` must actually switch it off (it was never read at all). +const fsMod = require('fs'), path = require('path'); +const ROOT = path.join(__dirname, '..', 'core'); +const store = {}; +global.localStorage = { getItem: k => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: k => { delete store[k]; } }; +global.indexedDB = { open() { throw new Error('no idb'); }, deleteDatabase() { throw new Error('no idb'); } }; +global.window = global; + +const AI = new Function(fsMod.readFileSync(path.join(ROOT, 'module-aiassist.js'), 'utf8') + '\nreturn WebX11AIAssistantModule;')(); +const ai = Object.create(AI.prototype); +ai.terminal = { print: () => {}, log: () => {} }; + +let fails = 0, passes = 0; +function ok(name, cond, detail) { + if (cond) { passes++; return; } + fails++; + console.log('FAIL ' + name + (detail ? ' — ' + detail : '')); +} + +// A. defaults +{ + const p = ai._loopPolicy(undefined); + ok('A1 enabled by default', p.enabled === true); + ok('A2 default warn/block/hardstop', p.warnAt === 3 && p.blockAt === 6 && p.hardStopAt === 9, JSON.stringify(p)); + ok('A3 default window', p.windowMs === 30000); +} + +// B. `threshold` is honoured (the old code ignored it entirely) +{ + const p = ai._loopPolicy({ threshold: 4 }); + ok('B1 threshold sets blockAt', p.blockAt === 4, JSON.stringify(p)); + ok('B2 warn comes before block', p.warnAt < p.blockAt, JSON.stringify(p)); + ok('B3 hard stop comes after block', p.hardStopAt > p.blockAt, JSON.stringify(p)); +} + +// C. explicit values win over the alias +{ + const p = ai._loopPolicy({ threshold: 4, blockAt: 8, warnAt: 5, hardStopAt: 12 }); + ok('C1 blockAt wins over threshold', p.blockAt === 8, JSON.stringify(p)); + ok('C2 warnAt respected', p.warnAt === 5, JSON.stringify(p)); + ok('C3 hardStopAt respected', p.hardStopAt === 12, JSON.stringify(p)); +} + +// D. `enabled: false` is a real kill switch +{ + const p = ai._loopPolicy({ enabled: false, threshold: 3 }); + ok('D1 disable is reported', p.enabled === false, JSON.stringify(p)); + ok('D2 disable keeps the numbers for display', p.blockAt === 3, JSON.stringify(p)); +} + +// E. nonsense input cannot produce a backwards policy +{ + const p = ai._loopPolicy({ warnAt: 9, blockAt: 6, hardStopAt: 2 }); + ok('E1 warn never exceeds block', p.warnAt <= p.blockAt, JSON.stringify(p)); + ok('E2 hard stop never equals block', p.hardStopAt > p.blockAt, JSON.stringify(p)); +} + +// F. the guard actually consults the policy in the loop source +{ + const src = fsMod.readFileSync(path.join(ROOT, 'module-aiassist.js'), 'utf8'); + ok('F1 the loop uses the resolver', /var _lp = this\._loopPolicy\(loopDetect\)/.test(src)); + ok('F2 blocking is gated on enabled', /_lp\.enabled && \(entry\.blocked \|\| entry\.count >= _blockAt\)/.test(src)); + ok('F3 warning is gated on enabled', /_lp\.enabled && entry\.count === _warnAt/.test(src)); + ok('F4 no direct reads of loopDetect.warnAt/blockAt remain', !/loopDetect\.(warnAt|blockAt|hardStopAt)/.test(src)); +} + +if (fails === 0) { + console.log('test-loop-policy: ' + passes + ' passed, 0 failed'); +} else { + console.log('test-loop-policy: ' + passes + ' passed, ' + fails + ' FAILED'); + process.exit(1); +} -- cgit v1.2.3