From 45a0b62fcbe14147d98151d55360994e40d1711a Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Fri, 13 Feb 2026 16:08:47 -0800 Subject: [PATCH] test: add Round 103 edge-case tests (countInFile bool, grepFile numeric, loadAliases array) - countInFile(file, false): boolean falls to else-return-0 type guard (utils.js:443) - grepFile(file, 0): numeric pattern implicitly coerced via RegExp constructor, contrasting with countInFile which explicitly rejects non-string non-RegExp - loadAliases with array aliases: typeof [] === 'object' bypasses validation at session-aliases.js:58, returning array instead of plain object Total tests: 869 (all passing) --- tests/lib/session-aliases.test.js | 28 +++++++++++++++++ tests/lib/utils.test.js | 52 +++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/tests/lib/session-aliases.test.js b/tests/lib/session-aliases.test.js index 72ee69c9..58df696b 100644 --- a/tests/lib/session-aliases.test.js +++ b/tests/lib/session-aliases.test.js @@ -1327,6 +1327,34 @@ function runTests() { 'Persisted title should be null after round-trip through saveAliases/loadAliases'); })) passed++; else failed++; + // ── Round 103: loadAliases with array aliases in JSON (typeof [] === 'object' bypass) ── + console.log('\nRound 103: loadAliases (array aliases — typeof bypass):'); + if (test('loadAliases accepts array aliases because typeof [] === "object" passes validation', () => { + // session-aliases.js line 58: `typeof data.aliases !== 'object'` is the guard. + // Arrays are typeof 'object' in JavaScript, so {"aliases": [1,2,3]} passes + // validation. The returned data.aliases is an array, not a plain object. + // Downstream code (Object.keys, Object.entries, bracket access) behaves + // differently on arrays vs objects but doesn't crash — it just produces + // unexpected results like numeric string keys "0", "1", "2". + resetAliases(); + const aliasesPath = aliases.getAliasesPath(); + fs.writeFileSync(aliasesPath, JSON.stringify({ + version: '1.0', + aliases: ['item0', 'item1', 'item2'], + metadata: { totalCount: 3, lastUpdated: new Date().toISOString() } + })); + const data = aliases.loadAliases(); + // The array passes the typeof 'object' check and is returned as-is + assert.ok(Array.isArray(data.aliases), + 'data.aliases should be an array (typeof [] === "object" bypasses guard)'); + assert.strictEqual(data.aliases.length, 3, + 'Array should have 3 elements'); + // Object.keys on an array returns ["0", "1", "2"] — numeric index strings + const keys = Object.keys(data.aliases); + assert.deepStrictEqual(keys, ['0', '1', '2'], + 'Object.keys of array returns numeric string indices, not named alias keys'); + })) passed++; else failed++; + // Summary console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); diff --git a/tests/lib/utils.test.js b/tests/lib/utils.test.js index bf726175..f874a78f 100644 --- a/tests/lib/utils.test.js +++ b/tests/lib/utils.test.js @@ -1467,6 +1467,58 @@ function runTests() { ); })) passed++; else failed++; + // ── Round 103: countInFile with boolean false pattern (non-string non-RegExp) ── + console.log('\nRound 103: countInFile (boolean false — explicit type guard returns 0):'); + if (test('countInFile returns 0 for boolean false pattern (else branch at line 443)', () => { + // utils.js lines 438-444: countInFile checks `instanceof RegExp` then `typeof === "string"`. + // Boolean `false` fails both checks and falls to the `else return 0` at line 443. + // This is the correct rejection path for non-string non-RegExp patterns, but was + // previously untested with boolean specifically (only null, undefined, object tested). + const tmpDir = fs.mkdtempSync(path.join(utils.getTempDir(), 'r103-bool-pattern-')); + const testFile = path.join(tmpDir, 'test.txt'); + try { + fs.writeFileSync(testFile, 'false is here\nfalse again\ntrue as well'); + // Even though "false" appears in the content, boolean `false` is rejected by type guard + const count = utils.countInFile(testFile, false); + assert.strictEqual(count, 0, + 'Boolean false should return 0 (typeof false === "boolean", not "string")'); + // Contrast: string "false" should match normally + const stringCount = utils.countInFile(testFile, 'false'); + assert.strictEqual(stringCount, 2, + 'String "false" should match 2 times (proving content exists but type guard blocked boolean)'); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + })) passed++; else failed++; + + // ── Round 103: grepFile with numeric 0 pattern (implicit RegExp coercion) ── + console.log('\nRound 103: grepFile (numeric 0 — implicit toString via RegExp constructor):'); + if (test('grepFile with numeric 0 implicitly coerces to /0/ via RegExp constructor', () => { + // utils.js line 468: grepFile's non-RegExp path does `regex = new RegExp(pattern)`. + // Unlike countInFile (which has explicit type guards), grepFile passes any value + // to the RegExp constructor, which calls toString() on it. So new RegExp(0) + // becomes /0/, and grepFile actually searches for lines containing "0". + // This contrasts with countInFile(file, 0) which returns 0 (type-rejected). + const tmpDir = fs.mkdtempSync(path.join(utils.getTempDir(), 'r103-grep-numeric-')); + const testFile = path.join(tmpDir, 'test.txt'); + try { + fs.writeFileSync(testFile, 'line with 0 zero\nno digit here\n100 bottles'); + const matches = utils.grepFile(testFile, 0); + assert.strictEqual(matches.length, 2, + 'grepFile(file, 0) should find 2 lines containing "0" (RegExp(0) → /0/)'); + assert.strictEqual(matches[0].lineNumber, 1, + 'First match on line 1 ("line with 0 zero")'); + assert.strictEqual(matches[1].lineNumber, 3, + 'Second match on line 3 ("100 bottles")'); + // Contrast: countInFile with numeric 0 returns 0 (type-rejected) + const count = utils.countInFile(testFile, 0); + assert.strictEqual(count, 0, + 'countInFile(file, 0) returns 0 — API inconsistency with grepFile'); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + })) passed++; else failed++; + // Summary console.log('\n=== Test Results ==='); console.log(`Passed: ${passed}`);