From b27c21732ff38e5888fbd4b19f9360af6c08b9ac Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Fri, 13 Feb 2026 16:59:56 -0800 Subject: [PATCH] test: add 3 edge-case tests for regex boundary, sticky flag, and type bypass (Round 105) - parseSessionMetadata: blank line within Completed section truncates items due to regex lookahead (?=###|\n\n|$) stopping at \n\n boundary - grepFile: sticky (y) flag not stripped like g flag, causing stateful .test() behavior that misses matching lines - getExecCommand: object args bypass SAFE_ARGS_REGEX (typeof !== 'string') but coerce to "[object Object]" in command string --- tests/lib/package-manager.test.js | 20 ++++++++++++++++++++ tests/lib/session-manager.test.js | 22 ++++++++++++++++++++++ tests/lib/utils.test.js | 29 +++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/tests/lib/package-manager.test.js b/tests/lib/package-manager.test.js index 409cbb6a..307b126e 100644 --- a/tests/lib/package-manager.test.js +++ b/tests/lib/package-manager.test.js @@ -1469,6 +1469,26 @@ function runTests() { ); })) passed++; else failed++; + // ── Round 105: getExecCommand with object args (bypasses SAFE_ARGS_REGEX, coerced to [object Object]) ── + console.log('\nRound 105: getExecCommand (object args — typeof bypass coerces to [object Object]):'); + + if (test('getExecCommand with args={} bypasses SAFE_ARGS validation and coerces to "[object Object]"', () => { + // package-manager.js line 334: `if (args && typeof args === 'string' && !SAFE_ARGS_REGEX.test(args))` + // When args is an object: typeof {} === 'object' (not 'string'), so the + // SAFE_ARGS_REGEX check is entirely SKIPPED. + // Line 339: `args ? ' ' + args : ''` — object is truthy, so it reaches + // string concatenation which calls {}.toString() → "[object Object]" + // Final command: "npx prettier [object Object]" — brackets bypass validation. + const cmd = pm.getExecCommand('prettier', {}); + assert.ok(cmd.includes('[object Object]'), + 'Object args should be coerced to "[object Object]" via implicit toString()'); + // Verify the SAFE_ARGS regex WOULD reject this string if it were a string arg + assert.throws( + () => pm.getExecCommand('prettier', '[object Object]'), + /unsafe characters/, + 'Same string as explicit string arg is correctly rejected by SAFE_ARGS_REGEX'); + })) passed++; else failed++; + // Summary console.log('\n=== Test Results ==='); console.log(`Passed: ${passed}`); diff --git a/tests/lib/session-manager.test.js b/tests/lib/session-manager.test.js index 57127378..e3543e82 100644 --- a/tests/lib/session-manager.test.js +++ b/tests/lib/session-manager.test.js @@ -1717,6 +1717,28 @@ file.ts 'hasContext should be true (context section has actual content)'); })) passed++; else failed++; + // ── Round 105: parseSessionMetadata blank-line boundary truncates section items ── + console.log('\nRound 105: parseSessionMetadata (blank line inside section — regex stops at \\n\\n):'); + + if (test('parseSessionMetadata drops completed items after a blank line within the section', () => { + // session-manager.js line 119: regex `(?=###|\n\n|$)` uses lazy [\s\S]*? with + // a lookahead that stops at the first \n\n. If completed items are separated + // by a blank line, items below the blank line are silently lost. + const content = '# Session\n\n### Completed\n- [x] Task A\n\n- [x] Task B\n\n### In Progress\n- [ ] Task C\n'; + const meta = sessionManager.parseSessionMetadata(content); + // The regex captures "- [x] Task A\n" then hits \n\n and stops. + // "- [x] Task B" is between the two sections but outside both regex captures. + assert.strictEqual(meta.completed.length, 1, + 'Only Task A captured — blank line terminates the section regex before Task B'); + assert.strictEqual(meta.completed[0], 'Task A', + 'First completed item should be Task A'); + // Task B is lost — it appears after the blank line, outside the captured range + assert.strictEqual(meta.inProgress.length, 1, + 'In Progress should still capture Task C'); + assert.strictEqual(meta.inProgress[0], 'Task C', + 'In-progress item should be Task C'); + })) 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 f874a78f..2e3ddbbc 100644 --- a/tests/lib/utils.test.js +++ b/tests/lib/utils.test.js @@ -1519,6 +1519,35 @@ function runTests() { } })) passed++; else failed++; + // ── Round 105: grepFile with sticky (y) flag — not stripped, causes stateful .test() ── + console.log('\nRound 105: grepFile (sticky y flag — not stripped like g, stateful .test() bug):'); + + if (test('grepFile with /pattern/y sticky flag misses lines due to lastIndex state', () => { + const tmpDir = fs.mkdtempSync(path.join(utils.getTempDir(), 'r105-grep-sticky-')); + const testFile = path.join(tmpDir, 'test.txt'); + try { + fs.writeFileSync(testFile, 'hello world\nhello again\nhello third'); + // grepFile line 466: `pattern.flags.replace('g', '')` strips g but not y. + // With /hello/y (sticky), .test() advances lastIndex after each successful + // match. On the next line, .test() starts at lastIndex (not 0), so it fails + // unless the match happens at that exact position. + const stickyResults = utils.grepFile(testFile, /hello/y); + // Without the bug, all 3 lines should match. With sticky flag preserved, + // line 1 matches (lastIndex advances to 5), line 2 fails (no 'hello' at + // position 5 of "hello again"), line 3 also likely fails. + // The g-flag version (properly stripped) should find all 3: + const globalResults = utils.grepFile(testFile, /hello/g); + assert.strictEqual(globalResults.length, 3, + 'g-flag regex should find all 3 lines (g is stripped, stateless)'); + // Sticky flag causes fewer matches — demonstrating the bug + assert.ok(stickyResults.length < 3, + `Sticky y flag causes stateful .test() — found ${stickyResults.length}/3 lines ` + + '(y flag not stripped like g, so lastIndex advances between lines)'); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + })) passed++; else failed++; + // Summary console.log('\n=== Test Results ==='); console.log(`Passed: ${passed}`);