From ecea14b051ddda23297cbb76f81dad5530abc30d Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 1 Jun 2026 17:59:36 -0500 Subject: [PATCH] test(build): extend behavioral drift checks to all four pure functions --- tests/build.test.js | 121 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/tests/build.test.js b/tests/build.test.js index 370ab99..c27d55c 100644 --- a/tests/build.test.js +++ b/tests/build.test.js @@ -71,3 +71,124 @@ test('bundle summary behavior matches src/pure.js (catches Math.floor-style regr // And specifically: perDay must be the floored integer 70, not the float 70.57. assert.equal(bundleResult.perDay, 70, 'perDay should be floored to 70'); }); + +test('bundle parseTarget behavior matches src/pure.js', () => { + const src = readFileSync(join(root, 'src/pure.js'), 'utf8'); + const bundle = readFileSync(join(root, 'torn-attribute-tracker.user.js'), 'utf8'); + + function extractBody(source, fnName) { + const start = source.indexOf('function ' + fnName + '('); + if (start === -1) throw new Error('could not find ' + fnName + ' in source'); + let i = source.indexOf('{', start); + if (i === -1) throw new Error('no opening brace for ' + fnName); + let depth = 1; + i++; + while (i < source.length && depth > 0) { + const c = source[i]; + if (c === '{') depth++; + else if (c === '}') depth--; + i++; + } + return source.slice(start, i); + } + + // Both src and bundle reference module-scope `SUFFIXES`, so inject it for both. + const srcFn = eval('(function() { const SUFFIXES = { k: 1e3, m: 1e6, b: 1e9, t: 1e12 }; ' + extractBody(src, 'parseTarget') + ' ; return parseTarget; })()'); + const bundleFn = eval('(function() { const SUFFIXES = { k: 1e3, m: 1e6, b: 1e9, t: 1e12 }; ' + extractBody(bundle, 'parseTarget') + ' ; return parseTarget; })()'); + + for (const input of [25, 25000000, '25M', '1.5B', '25,000,000', 'abc', null, undefined, 0, -1]) { + assert.equal(bundleFn(input), srcFn(input), 'parseTarget drift on input: ' + JSON.stringify(input)); + } +}); + +test('bundle computeEstimate behavior matches src/pure.js', () => { + const src = readFileSync(join(root, 'src/pure.js'), 'utf8'); + const bundle = readFileSync(join(root, 'torn-attribute-tracker.user.js'), 'utf8'); + + function extractBody(source, fnName) { + const start = source.indexOf('function ' + fnName + '('); + if (start === -1) throw new Error('could not find ' + fnName + ' in source'); + let i = source.indexOf('{', start); + if (i === -1) throw new Error('no opening brace for ' + fnName); + let depth = 1; + i++; + while (i < source.length && depth > 0) { + const c = source[i]; + if (c === '{') depth++; + else if (c === '}') depth--; + i++; + } + return source.slice(start, i); + } + + // The bundle inlines the 86_400_000 constant; src/pure.js uses MS_PER_DAY. + // Both versions call `Date.now()` inside the function for the `eta` Date, so + // we strip `eta` from the comparison (it would differ by sub-millisecond + // jitter between the two evals) and only compare the numeric fields. + const srcFn = eval('(function() { const MS_PER_DAY = 86_400_000; ' + extractBody(src, 'computeEstimate') + ' ; return computeEstimate; })()'); + const bundleFn = eval('(function() { ' + extractBody(bundle, 'computeEstimate') + ' ; return computeEstimate; })()'); + + function stripEta(r) { + return { remaining: r.remaining, trainsToGo: r.trainsToGo, days: r.days, etaDays: r.days }; + } + + for (const args of [ + [14_328_501, 25_000_000, 247, 4520], + [25_000_000, 25_000_000, 247, 4520], + [30_000_000, 25_000_000, 247, 4520], + [100, 200, 0, 50], + [100, 200, 50, 0], + ]) { + const [c, t, pt, pd] = args; + const srcResult = srcFn(c, t, pt, pd); + const bundleResult = bundleFn(c, t, pt, pd); + // Both eta fields should be null together, or both should be Date objects. + assert.equal(bundleResult.eta === null, srcResult.eta === null, 'eta nullity drift on args: ' + JSON.stringify(args)); + assert.deepEqual(stripEta(bundleResult), stripEta(srcResult), 'computeEstimate drift on args: ' + JSON.stringify(args)); + } +}); + +test('bundle pruneHistory behavior matches src/pure.js (catches strict-boundary regressions)', () => { + const src = readFileSync(join(root, 'src/pure.js'), 'utf8'); + const bundle = readFileSync(join(root, 'torn-attribute-tracker.user.js'), 'utf8'); + + function extractBody(source, fnName) { + const start = source.indexOf('function ' + fnName + '('); + if (start === -1) throw new Error('could not find ' + fnName + ' in source'); + let i = source.indexOf('{', start); + if (i === -1) throw new Error('no opening brace for ' + fnName); + let depth = 1; + i++; + while (i < source.length && depth > 0) { + const c = source[i]; + if (c === '{') depth++; + else if (c === '}') depth--; + i++; + } + return source.slice(start, i); + } + + // The bundle inlines 30 * 86_400_000; src uses THIRTY_DAYS_MS = 30 * MS_PER_DAY. + // Both must be visible to the extracted function body. + const srcFn = eval('(function() { const MS_PER_DAY = 86_400_000; const THIRTY_DAYS_MS = 30 * MS_PER_DAY; ' + extractBody(src, 'pruneHistory') + ' ; return pruneHistory; })()'); + const bundleFn = eval('(function() { const THIRTY_DAYS_MS = 30 * 86_400_000; ' + extractBody(bundle, 'pruneHistory') + ' ; return pruneHistory; })()'); + + const NOW = 1_700_000_000_000; + const DAY = 86_400_000; + + // Critical case: entry at exactly 30 days. Source drops it (strict >), bundle should too. + const boundaryEntries = [ + { ts: NOW, delta: 1 }, + { ts: NOW - 1 * DAY, delta: 1 }, + { ts: NOW - 29 * DAY, delta: 1 }, + { ts: NOW - 30 * DAY, delta: 1 }, // exactly 30 days — should be DROPPED + { ts: NOW - 31 * DAY, delta: 1 }, + ]; + + const srcResult = srcFn(boundaryEntries, NOW); + const bundleResult = bundleFn(boundaryEntries, NOW); + + assert.deepEqual(bundleResult, srcResult, 'pruneHistory drift on 30-day boundary'); + assert.equal(srcResult.length, 3, 'source should keep 3 entries (drop the 30-day one)'); + assert.equal(bundleResult.length, 3, 'bundle should keep 3 entries (drop the 30-day one)'); +});