2026-01-23 15:08:07 +08:00
/**
* Tests for scripts/lib/utils.js
*
* Run with: node tests/lib/utils.test.js
*/
const assert = require ( 'assert' );
const path = require ( 'path' );
const fs = require ( 'fs' );
2026-03-24 23:08:27 -04:00
const { spawnSync } = require ( 'child_process' );
2026-01-23 15:08:07 +08:00
// Import the module
const utils = require ( '../../scripts/lib/utils' );
// Test helper
function test ( name , fn ) {
try {
fn ();
console . log ( ` ✓ ${ name } ` );
return true ;
} catch ( err ) {
console . log ( ` ✗ ${ name } ` );
console . log ( ` Error: ${ err . message } ` );
return false ;
}
}
// Test suite
function runTests () {
console . log ( '\n=== Testing utils.js ===\n' );
let passed = 0 ;
let failed = 0 ;
// Platform detection tests
console . log ( 'Platform Detection:' );
if ( test ( 'isWindows/isMacOS/isLinux are booleans' , () => {
assert . strictEqual ( typeof utils . isWindows , 'boolean' );
assert . strictEqual ( typeof utils . isMacOS , 'boolean' );
assert . strictEqual ( typeof utils . isLinux , 'boolean' );
})) passed ++ ; else failed ++ ;
if ( test ( 'exactly one platform should be true' , () => {
const platforms = [ utils . isWindows , utils . isMacOS , utils . isLinux ];
const trueCount = platforms . filter ( p => p ). length ;
// Note: Could be 0 on other platforms like FreeBSD
assert . ok ( trueCount <= 1 , 'More than one platform is true' );
})) passed ++ ; else failed ++ ;
// Directory functions tests
console . log ( '\nDirectory Functions:' );
if ( test ( 'getHomeDir returns valid path' , () => {
const home = utils . getHomeDir ();
assert . strictEqual ( typeof home , 'string' );
assert . ok ( home . length > 0 , 'Home dir should not be empty' );
assert . ok ( fs . existsSync ( home ), 'Home dir should exist' );
})) passed ++ ; else failed ++ ;
if ( test ( 'getClaudeDir returns path under home' , () => {
const claudeDir = utils . getClaudeDir ();
const homeDir = utils . getHomeDir ();
assert . ok ( claudeDir . startsWith ( homeDir ), 'Claude dir should be under home' );
assert . ok ( claudeDir . includes ( '.claude' ), 'Should contain .claude' );
})) passed ++ ; else failed ++ ;
if ( test ( 'getSessionsDir returns path under Claude dir' , () => {
const sessionsDir = utils . getSessionsDir ();
const claudeDir = utils . getClaudeDir ();
assert . ok ( sessionsDir . startsWith ( claudeDir ), 'Sessions should be under Claude dir' );
2026-03-24 23:08:27 -04:00
assert . ok ( sessionsDir . endsWith ( path . join ( '.claude' , 'session-data' )) || sessionsDir . endsWith ( '/.claude/session-data' ), 'Should use canonical session-data directory' );
})) passed ++ ; else failed ++ ;
if ( test ( 'getSessionSearchDirs includes canonical and legacy paths' , () => {
const searchDirs = utils . getSessionSearchDirs ();
2026-03-25 03:36:36 -04:00
assert . strictEqual ( searchDirs [ 0 ], utils . getSessionsDir (), 'Canonical session dir should be searched first' );
assert . strictEqual ( searchDirs [ 1 ], utils . getLegacySessionsDir (), 'Legacy session dir should be searched second' );
2026-01-23 15:08:07 +08:00
})) passed ++ ; else failed ++ ;
if ( test ( 'getTempDir returns valid temp directory' , () => {
const tempDir = utils . getTempDir ();
assert . strictEqual ( typeof tempDir , 'string' );
assert . ok ( tempDir . length > 0 , 'Temp dir should not be empty' );
})) passed ++ ; else failed ++ ;
if ( test ( 'ensureDir creates directory' , () => {
const testDir = path . join ( utils . getTempDir (), `utils-test- ${ Date . now () } ` );
try {
utils . ensureDir ( testDir );
assert . ok ( fs . existsSync ( testDir ), 'Directory should be created' );
} finally {
fs . rmSync ( testDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
// Date/Time functions tests
console . log ( '\nDate/Time Functions:' );
if ( test ( 'getDateString returns YYYY-MM-DD format' , () => {
const date = utils . getDateString ();
assert . ok ( /^\d{4}-\d{2}-\d{2}$/ . test ( date ), `Expected YYYY-MM-DD, got ${ date } ` );
})) passed ++ ; else failed ++ ;
if ( test ( 'getTimeString returns HH:MM format' , () => {
const time = utils . getTimeString ();
assert . ok ( /^\d{2}:\d{2}$/ . test ( time ), `Expected HH:MM, got ${ time } ` );
})) passed ++ ; else failed ++ ;
if ( test ( 'getDateTimeString returns full datetime format' , () => {
const dt = utils . getDateTimeString ();
assert . ok ( /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/ . test ( dt ), `Expected YYYY-MM-DD HH:MM:SS, got ${ dt } ` );
})) passed ++ ; else failed ++ ;
2026-01-29 01:01:19 -06:00
// Project name tests
console . log ( '\nProject Name Functions:' );
if ( test ( 'getGitRepoName returns string or null' , () => {
const repoName = utils . getGitRepoName ();
assert . ok ( repoName === null || typeof repoName === 'string' );
})) passed ++ ; else failed ++ ;
if ( test ( 'getProjectName returns non-empty string' , () => {
const name = utils . getProjectName ();
assert . ok ( name && name . length > 0 );
})) passed ++ ; else failed ++ ;
2026-03-24 23:08:27 -04:00
// sanitizeSessionId tests
console . log ( '\nsanitizeSessionId:' );
if ( test ( 'sanitizeSessionId strips leading dots' , () => {
assert . strictEqual ( utils . sanitizeSessionId ( '.claude' ), 'claude' );
})) passed ++ ; else failed ++ ;
if ( test ( 'sanitizeSessionId replaces dots and spaces' , () => {
assert . strictEqual ( utils . sanitizeSessionId ( 'my.project' ), 'my-project' );
assert . strictEqual ( utils . sanitizeSessionId ( 'my project' ), 'my-project' );
})) passed ++ ; else failed ++ ;
if ( test ( 'sanitizeSessionId replaces special chars and collapses runs' , () => {
assert . strictEqual ( utils . sanitizeSessionId ( 'project@v2' ), 'project-v2' );
assert . strictEqual ( utils . sanitizeSessionId ( 'a...b' ), 'a-b' );
})) passed ++ ; else failed ++ ;
if ( test ( 'sanitizeSessionId preserves valid chars' , () => {
assert . strictEqual ( utils . sanitizeSessionId ( 'my-project_123' ), 'my-project_123' );
})) passed ++ ; else failed ++ ;
2026-03-25 04:00:50 -04:00
if ( test ( 'sanitizeSessionId appends hash suffix for all Windows reserved device names' , () => {
2026-03-25 03:44:03 -04:00
for ( const reservedName of [ 'CON' , 'prn' , 'Aux' , 'nul' , 'COM1' , 'lpt9' ]) {
const sanitized = utils . sanitizeSessionId ( reservedName );
assert . ok ( sanitized , `Expected sanitized output for ${ reservedName } ` );
assert . notStrictEqual ( sanitized . toUpperCase (), reservedName . toUpperCase ());
assert . ok ( /-[a-f0-9]{6}$/i . test ( sanitized ), `Expected deterministic hash suffix for ${ reservedName } , got ${ sanitized } ` );
}
})) passed ++ ; else failed ++ ;
2026-03-24 23:08:27 -04:00
if ( test ( 'sanitizeSessionId returns null for empty or punctuation-only values' , () => {
assert . strictEqual ( utils . sanitizeSessionId ( '' ), null );
assert . strictEqual ( utils . sanitizeSessionId ( null ), null );
assert . strictEqual ( utils . sanitizeSessionId ( undefined ), null );
assert . strictEqual ( utils . sanitizeSessionId ( '...' ), null );
assert . strictEqual ( utils . sanitizeSessionId ( '…' ), null );
})) passed ++ ; else failed ++ ;
if ( test ( 'sanitizeSessionId returns stable hashes for non-ASCII values' , () => {
const chinese = utils . sanitizeSessionId ( '我的项目' );
const cyrillic = utils . sanitizeSessionId ( 'проект' );
const emoji = utils . sanitizeSessionId ( '🚀🎉' );
assert . ok ( /^[a-f0-9]{8}$/ . test ( chinese ), `Expected 8-char hash, got: ${ chinese } ` );
assert . ok ( /^[a-f0-9]{8}$/ . test ( cyrillic ), `Expected 8-char hash, got: ${ cyrillic } ` );
assert . ok ( /^[a-f0-9]{8}$/ . test ( emoji ), `Expected 8-char hash, got: ${ emoji } ` );
assert . notStrictEqual ( chinese , cyrillic );
assert . notStrictEqual ( chinese , emoji );
assert . strictEqual ( utils . sanitizeSessionId ( '日本語プロジェクト' ), utils . sanitizeSessionId ( '日本語プロジェクト' ));
})) passed ++ ; else failed ++ ;
if ( test ( 'sanitizeSessionId disambiguates mixed-script names from pure ASCII' , () => {
const mixed = utils . sanitizeSessionId ( '我的app' );
const mixedTwo = utils . sanitizeSessionId ( '他的app' );
const pure = utils . sanitizeSessionId ( 'app' );
assert . strictEqual ( pure , 'app' );
assert . ok ( mixed . startsWith ( 'app-' ), `Expected mixed-script prefix, got: ${ mixed } ` );
assert . notStrictEqual ( mixed , pure );
assert . notStrictEqual ( mixed , mixedTwo );
})) passed ++ ; else failed ++ ;
if ( test ( 'sanitizeSessionId is idempotent' , () => {
for ( const input of [ '.claude' , 'my.project' , 'project@v2' , 'a...b' , 'my-project_123' ]) {
const once = utils . sanitizeSessionId ( input );
const twice = utils . sanitizeSessionId ( once );
assert . strictEqual ( once , twice , `Expected idempotent result for ${ input } ` );
}
})) passed ++ ; else failed ++ ;
2026-03-25 04:00:50 -04:00
if ( test ( 'sanitizeSessionId preserves readable prefixes for Windows reserved device names' , () => {
2026-03-25 03:36:36 -04:00
const con = utils . sanitizeSessionId ( 'CON' );
const aux = utils . sanitizeSessionId ( 'aux' );
assert . ok ( con . startsWith ( 'CON-' ), `Expected CON to get a suffix, got: ${ con } ` );
assert . ok ( aux . startsWith ( 'aux-' ), `Expected aux to get a suffix, got: ${ aux } ` );
assert . notStrictEqual ( utils . sanitizeSessionId ( 'COM1' ), 'COM1' );
})) passed ++ ; else failed ++ ;
2026-01-25 18:21:27 -08:00
// Session ID tests
console . log ( '\nSession ID Functions:' );
2026-03-24 23:08:27 -04:00
if ( test ( 'getSessionIdShort falls back to sanitized project name' , () => {
2026-01-29 01:01:19 -06:00
const original = process . env . CLAUDE_SESSION_ID ;
2026-01-25 18:21:27 -08:00
delete process . env . CLAUDE_SESSION_ID ;
try {
const shortId = utils . getSessionIdShort ();
2026-03-24 23:08:27 -04:00
assert . strictEqual ( shortId , utils . sanitizeSessionId ( utils . getProjectName ()));
2026-01-25 18:21:27 -08:00
} finally {
2026-03-24 23:08:27 -04:00
if ( original !== undefined ) process . env . CLAUDE_SESSION_ID = original ;
else delete process . env . CLAUDE_SESSION_ID ;
2026-01-25 18:21:27 -08:00
}
})) passed ++ ; else failed ++ ;
if ( test ( 'getSessionIdShort returns last 8 characters' , () => {
2026-01-29 01:01:19 -06:00
const original = process . env . CLAUDE_SESSION_ID ;
2026-01-25 18:21:27 -08:00
process . env . CLAUDE_SESSION_ID = 'test-session-abc12345' ;
try {
2026-01-29 01:01:19 -06:00
assert . strictEqual ( utils . getSessionIdShort (), 'abc12345' );
2026-01-25 18:21:27 -08:00
} finally {
2026-01-29 01:01:19 -06:00
if ( original ) process . env . CLAUDE_SESSION_ID = original ;
else delete process . env . CLAUDE_SESSION_ID ;
2026-01-25 18:21:27 -08:00
}
})) passed ++ ; else failed ++ ;
if ( test ( 'getSessionIdShort handles short session IDs' , () => {
2026-01-29 01:01:19 -06:00
const original = process . env . CLAUDE_SESSION_ID ;
2026-01-25 18:21:27 -08:00
process . env . CLAUDE_SESSION_ID = 'short' ;
try {
2026-01-29 01:01:19 -06:00
assert . strictEqual ( utils . getSessionIdShort (), 'short' );
2026-01-25 18:21:27 -08:00
} finally {
2026-01-29 01:01:19 -06:00
if ( original ) process . env . CLAUDE_SESSION_ID = original ;
else delete process . env . CLAUDE_SESSION_ID ;
2026-01-25 18:21:27 -08:00
}
})) passed ++ ; else failed ++ ;
2026-03-24 23:08:27 -04:00
if ( test ( 'getSessionIdShort sanitizes explicit fallback parameter' , () => {
if ( process . platform === 'win32' ) {
console . log ( ' (skipped — root CWD differs on Windows)' );
return true ;
}
const utilsPath = path . join ( __dirname , '..' , '..' , 'scripts' , 'lib' , 'utils.js' );
const script = `
const utils = require(' ${ utilsPath . replace ( /'/g , "\\'" ) } ');
process.stdout.write(utils.getSessionIdShort('my.fallback'));
` ;
const result = spawnSync ( 'node' , [ '-e' , script ], {
encoding : 'utf8' ,
cwd : '/' ,
env : { ... process . env , CLAUDE_SESSION_ID : '' },
timeout : 10000
});
assert . strictEqual ( result . status , 0 , `Expected exit 0, got ${ result . status } . stderr: ${ result . stderr } ` );
assert . strictEqual ( result . stdout , 'my-fallback' );
})) passed ++ ; else failed ++ ;
2026-01-23 15:08:07 +08:00
// File operations tests
console . log ( '\nFile Operations:' );
if ( test ( 'readFile returns null for non-existent file' , () => {
const content = utils . readFile ( '/non/existent/file/path.txt' );
assert . strictEqual ( content , null );
})) passed ++ ; else failed ++ ;
if ( test ( 'writeFile and readFile work together' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test- ${ Date . now () } .txt` );
const testContent = 'Hello, World!' ;
try {
utils . writeFile ( testFile , testContent );
const read = utils . readFile ( testFile );
assert . strictEqual ( read , testContent );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
if ( test ( 'appendFile adds content to file' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'Line 1\n' );
utils . appendFile ( testFile , 'Line 2\n' );
const content = utils . readFile ( testFile );
assert . strictEqual ( content , 'Line 1\nLine 2\n' );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
if ( test ( 'replaceInFile replaces text' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'Hello, World!' );
utils . replaceInFile ( testFile , /World/ , 'Universe' );
const content = utils . readFile ( testFile );
assert . strictEqual ( content , 'Hello, Universe!' );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
if ( test ( 'countInFile counts occurrences' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'foo bar foo baz foo' );
const count = utils . countInFile ( testFile , /foo/g );
assert . strictEqual ( count , 3 );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
if ( test ( 'grepFile finds matching lines' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'line 1 foo\nline 2 bar\nline 3 foo' );
const matches = utils . grepFile ( testFile , /foo/ );
assert . strictEqual ( matches . length , 2 );
assert . strictEqual ( matches [ 0 ]. lineNumber , 1 );
assert . strictEqual ( matches [ 1 ]. lineNumber , 3 );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
// findFiles tests
console . log ( '\nfindFiles:' );
if ( test ( 'findFiles returns empty for non-existent directory' , () => {
const results = utils . findFiles ( '/non/existent/dir' , '*.txt' );
assert . strictEqual ( results . length , 0 );
})) passed ++ ; else failed ++ ;
if ( test ( 'findFiles finds matching files' , () => {
const testDir = path . join ( utils . getTempDir (), `utils-test- ${ Date . now () } ` );
try {
fs . mkdirSync ( testDir );
fs . writeFileSync ( path . join ( testDir , 'test1.txt' ), 'content' );
fs . writeFileSync ( path . join ( testDir , 'test2.txt' ), 'content' );
fs . writeFileSync ( path . join ( testDir , 'test.md' ), 'content' );
const txtFiles = utils . findFiles ( testDir , '*.txt' );
assert . strictEqual ( txtFiles . length , 2 );
const mdFiles = utils . findFiles ( testDir , '*.md' );
assert . strictEqual ( mdFiles . length , 1 );
} finally {
fs . rmSync ( testDir , { recursive : true });
}
})) passed ++ ; else failed ++ ;
2026-02-12 15:24:28 -08:00
// Edge case tests for defensive code
console . log ( '\nEdge Cases:' );
if ( test ( 'findFiles returns empty for null/undefined dir' , () => {
assert . deepStrictEqual ( utils . findFiles ( null , '*.txt' ), []);
assert . deepStrictEqual ( utils . findFiles ( undefined , '*.txt' ), []);
assert . deepStrictEqual ( utils . findFiles ( '' , '*.txt' ), []);
})) passed ++ ; else failed ++ ;
if ( test ( 'findFiles returns empty for null/undefined pattern' , () => {
assert . deepStrictEqual ( utils . findFiles ( '/tmp' , null ), []);
assert . deepStrictEqual ( utils . findFiles ( '/tmp' , undefined ), []);
assert . deepStrictEqual ( utils . findFiles ( '/tmp' , '' ), []);
})) passed ++ ; else failed ++ ;
if ( test ( 'findFiles supports maxAge filter' , () => {
const testDir = path . join ( utils . getTempDir (), `utils-test-maxage- ${ Date . now () } ` );
try {
fs . mkdirSync ( testDir );
fs . writeFileSync ( path . join ( testDir , 'recent.txt' ), 'content' );
const results = utils . findFiles ( testDir , '*.txt' , { maxAge : 1 });
assert . strictEqual ( results . length , 1 );
assert . ok ( results [ 0 ]. path . endsWith ( 'recent.txt' ));
} finally {
fs . rmSync ( testDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
if ( test ( 'findFiles supports recursive option' , () => {
const testDir = path . join ( utils . getTempDir (), `utils-test-recursive- ${ Date . now () } ` );
const subDir = path . join ( testDir , 'sub' );
try {
fs . mkdirSync ( subDir , { recursive : true });
fs . writeFileSync ( path . join ( testDir , 'top.txt' ), 'content' );
fs . writeFileSync ( path . join ( subDir , 'nested.txt' ), 'content' );
// Without recursive: only top level
const shallow = utils . findFiles ( testDir , '*.txt' , { recursive : false });
assert . strictEqual ( shallow . length , 1 );
// With recursive: finds nested too
const deep = utils . findFiles ( testDir , '*.txt' , { recursive : true });
assert . strictEqual ( deep . length , 2 );
} finally {
fs . rmSync ( testDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
if ( test ( 'countInFile handles invalid regex pattern' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'test content' );
const count = utils . countInFile ( testFile , '(unclosed' );
assert . strictEqual ( count , 0 );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
if ( test ( 'countInFile handles non-string non-regex pattern' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'test content' );
const count = utils . countInFile ( testFile , 42 );
assert . strictEqual ( count , 0 );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
if ( test ( 'countInFile enforces global flag on RegExp' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'foo bar foo baz foo' );
// RegExp without global flag — countInFile should still count all
const count = utils . countInFile ( testFile , /foo/ );
assert . strictEqual ( count , 3 );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
if ( test ( 'grepFile handles invalid regex pattern' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'test content' );
const matches = utils . grepFile ( testFile , '[invalid' );
assert . deepStrictEqual ( matches , []);
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
if ( test ( 'replaceInFile returns false for non-existent file' , () => {
const result = utils . replaceInFile ( '/non/existent/file.txt' , 'foo' , 'bar' );
assert . strictEqual ( result , false );
})) passed ++ ; else failed ++ ;
if ( test ( 'countInFile returns 0 for non-existent file' , () => {
const count = utils . countInFile ( '/non/existent/file.txt' , /foo/g );
assert . strictEqual ( count , 0 );
})) passed ++ ; else failed ++ ;
if ( test ( 'grepFile returns empty for non-existent file' , () => {
const matches = utils . grepFile ( '/non/existent/file.txt' , /foo/ );
assert . deepStrictEqual ( matches , []);
})) passed ++ ; else failed ++ ;
if ( test ( 'commandExists rejects unsafe command names' , () => {
assert . strictEqual ( utils . commandExists ( 'cmd; rm -rf' ), false );
assert . strictEqual ( utils . commandExists ( '$(whoami)' ), false );
assert . strictEqual ( utils . commandExists ( 'cmd && echo hi' ), false );
})) passed ++ ; else failed ++ ;
if ( test ( 'ensureDir is idempotent' , () => {
const testDir = path . join ( utils . getTempDir (), `utils-test-idem- ${ Date . now () } ` );
try {
const result1 = utils . ensureDir ( testDir );
const result2 = utils . ensureDir ( testDir );
assert . strictEqual ( result1 , testDir );
assert . strictEqual ( result2 , testDir );
assert . ok ( fs . existsSync ( testDir ));
} finally {
fs . rmSync ( testDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-01-23 15:08:07 +08:00
// System functions tests
console . log ( '\nSystem Functions:' );
if ( test ( 'commandExists finds node' , () => {
const exists = utils . commandExists ( 'node' );
assert . strictEqual ( exists , true );
})) passed ++ ; else failed ++ ;
if ( test ( 'commandExists returns false for fake command' , () => {
const exists = utils . commandExists ( 'nonexistent_command_12345' );
assert . strictEqual ( exists , false );
})) passed ++ ; else failed ++ ;
if ( test ( 'runCommand executes simple command' , () => {
const result = utils . runCommand ( 'node --version' );
assert . strictEqual ( result . success , true );
assert . ok ( result . output . startsWith ( 'v' ), 'Should start with v' );
})) passed ++ ; else failed ++ ;
if ( test ( 'runCommand handles failed command' , () => {
const result = utils . runCommand ( 'node --invalid-flag-12345' );
assert . strictEqual ( result . success , false );
})) passed ++ ; else failed ++ ;
2026-02-12 16:19:04 -08:00
// output() and log() tests
console . log ( '\noutput() and log():' );
if ( test ( 'output() writes string to stdout' , () => {
// Capture stdout by temporarily replacing console.log
let captured = null ;
const origLog = console . log ;
console . log = ( v ) => { captured = v ; };
try {
utils . output ( 'hello' );
assert . strictEqual ( captured , 'hello' );
} finally {
console . log = origLog ;
}
})) passed ++ ; else failed ++ ;
if ( test ( 'output() JSON-stringifies objects' , () => {
let captured = null ;
const origLog = console . log ;
console . log = ( v ) => { captured = v ; };
try {
utils . output ({ key : 'value' , num : 42 });
assert . strictEqual ( captured , '{"key":"value","num":42}' );
} finally {
console . log = origLog ;
}
})) passed ++ ; else failed ++ ;
if ( test ( 'output() JSON-stringifies null (typeof null === "object")' , () => {
let captured = null ;
const origLog = console . log ;
console . log = ( v ) => { captured = v ; };
try {
utils . output ( null );
// typeof null === 'object' in JS, so it goes through JSON.stringify
assert . strictEqual ( captured , 'null' );
} finally {
console . log = origLog ;
}
})) passed ++ ; else failed ++ ;
if ( test ( 'output() handles arrays as objects' , () => {
let captured = null ;
const origLog = console . log ;
console . log = ( v ) => { captured = v ; };
try {
utils . output ([ 1 , 2 , 3 ]);
assert . strictEqual ( captured , '[1,2,3]' );
} finally {
console . log = origLog ;
}
})) passed ++ ; else failed ++ ;
if ( test ( 'log() writes to stderr' , () => {
let captured = null ;
const origError = console . error ;
console . error = ( v ) => { captured = v ; };
try {
utils . log ( 'test message' );
assert . strictEqual ( captured , 'test message' );
} finally {
console . error = origError ;
}
})) passed ++ ; else failed ++ ;
// isGitRepo() tests
console . log ( '\nisGitRepo():' );
if ( test ( 'isGitRepo returns true in a git repo' , () => {
// We're running from within the ECC repo, so this should be true
assert . strictEqual ( utils . isGitRepo (), true );
})) passed ++ ; else failed ++ ;
// getGitModifiedFiles() tests
console . log ( '\ngetGitModifiedFiles():' );
if ( test ( 'getGitModifiedFiles returns an array' , () => {
const files = utils . getGitModifiedFiles ();
assert . ok ( Array . isArray ( files ));
})) passed ++ ; else failed ++ ;
if ( test ( 'getGitModifiedFiles filters by regex patterns' , () => {
const files = utils . getGitModifiedFiles ([ '\\.NONEXISTENT_EXTENSION$' ]);
assert . ok ( Array . isArray ( files ));
assert . strictEqual ( files . length , 0 );
})) passed ++ ; else failed ++ ;
if ( test ( 'getGitModifiedFiles skips invalid patterns' , () => {
// Mix of valid and invalid patterns — should not throw
const files = utils . getGitModifiedFiles ([ '(unclosed' , '\\.js$' , '[invalid' ]);
assert . ok ( Array . isArray ( files ));
})) passed ++ ; else failed ++ ;
if ( test ( 'getGitModifiedFiles skips non-string patterns' , () => {
const files = utils . getGitModifiedFiles ([ null , undefined , 42 , '' , '\\.js$' ]);
assert . ok ( Array . isArray ( files ));
})) passed ++ ; else failed ++ ;
// getLearnedSkillsDir() test
console . log ( '\ngetLearnedSkillsDir():' );
if ( test ( 'getLearnedSkillsDir returns path under Claude dir' , () => {
const dir = utils . getLearnedSkillsDir ();
assert . ok ( dir . includes ( '.claude' ));
assert . ok ( dir . includes ( 'skills' ));
assert . ok ( dir . includes ( 'learned' ));
})) passed ++ ; else failed ++ ;
2026-02-12 16:31:07 -08:00
// replaceInFile behavior tests
console . log ( '\nreplaceInFile (behavior):' );
if ( test ( 'replaces first match when regex has no g flag' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'foo bar foo baz foo' );
utils . replaceInFile ( testFile , /foo/ , 'qux' );
const content = utils . readFile ( testFile );
// Without g flag, only first 'foo' should be replaced
assert . strictEqual ( content , 'qux bar foo baz foo' );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
if ( test ( 'replaces all matches when regex has g flag' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'foo bar foo baz foo' );
utils . replaceInFile ( testFile , /foo/g , 'qux' );
const content = utils . readFile ( testFile );
assert . strictEqual ( content , 'qux bar qux baz qux' );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
if ( test ( 'replaces with string search (first occurrence)' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'hello world hello' );
utils . replaceInFile ( testFile , 'hello' , 'goodbye' );
const content = utils . readFile ( testFile );
// String.replace with string search only replaces first
assert . strictEqual ( content , 'goodbye world hello' );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
2026-02-12 17:32:04 -08:00
if ( test ( 'replaces all occurrences with string when options.all is true' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'hello world hello again hello' );
utils . replaceInFile ( testFile , 'hello' , 'goodbye' , { all : true });
const content = utils . readFile ( testFile );
assert . strictEqual ( content , 'goodbye world goodbye again goodbye' );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
if ( test ( 'options.all is ignored for regex patterns' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'foo bar foo' );
// all option should be ignored for regex; only g flag matters
utils . replaceInFile ( testFile , /foo/ , 'qux' , { all : true });
const content = utils . readFile ( testFile );
assert . strictEqual ( content , 'qux bar foo' , 'Regex without g should still replace first only' );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
2026-02-12 16:31:07 -08:00
if ( test ( 'replaces with capture groups' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , '**Last Updated:** 10:30' );
utils . replaceInFile ( testFile , /\*\*Last Updated:\*\*.*/ , '**Last Updated:** 14:45' );
const content = utils . readFile ( testFile );
assert . strictEqual ( content , '**Last Updated:** 14:45' );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
// writeFile edge cases
console . log ( '\nwriteFile (edge cases):' );
if ( test ( 'writeFile overwrites existing content' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'original' );
utils . writeFile ( testFile , 'replaced' );
const content = utils . readFile ( testFile );
assert . strictEqual ( content , 'replaced' );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
if ( test ( 'writeFile handles unicode content' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test- ${ Date . now () } .txt` );
try {
const unicode = '日本語テスト 🚀 émojis' ;
utils . writeFile ( testFile , unicode );
const content = utils . readFile ( testFile );
assert . strictEqual ( content , unicode );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
2026-02-12 16:19:04 -08:00
// findFiles with regex special characters in pattern
console . log ( '\nfindFiles (regex chars):' );
if ( test ( 'findFiles handles regex special chars in pattern' , () => {
const testDir = path . join ( utils . getTempDir (), `utils-test-regex- ${ Date . now () } ` );
try {
fs . mkdirSync ( testDir );
// Create files with regex-special characters in names
fs . writeFileSync ( path . join ( testDir , 'file(1).txt' ), 'content' );
fs . writeFileSync ( path . join ( testDir , 'file+2.txt' ), 'content' );
fs . writeFileSync ( path . join ( testDir , 'file[3].txt' ), 'content' );
// These patterns should match literally, not as regex metacharacters
const parens = utils . findFiles ( testDir , 'file(1).txt' );
assert . strictEqual ( parens . length , 1 , 'Should match file(1).txt literally' );
const plus = utils . findFiles ( testDir , 'file+2.txt' );
assert . strictEqual ( plus . length , 1 , 'Should match file+2.txt literally' );
const brackets = utils . findFiles ( testDir , 'file[3].txt' );
assert . strictEqual ( brackets . length , 1 , 'Should match file[3].txt literally' );
} finally {
fs . rmSync ( testDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
if ( test ( 'findFiles wildcard still works with special chars' , () => {
const testDir = path . join ( utils . getTempDir (), `utils-test-glob- ${ Date . now () } ` );
try {
fs . mkdirSync ( testDir );
fs . writeFileSync ( path . join ( testDir , 'app(v2).js' ), 'content' );
fs . writeFileSync ( path . join ( testDir , 'app(v3).ts' ), 'content' );
const jsFiles = utils . findFiles ( testDir , '*.js' );
assert . strictEqual ( jsFiles . length , 1 );
assert . ok ( jsFiles [ 0 ]. path . endsWith ( 'app(v2).js' ));
} finally {
fs . rmSync ( testDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-12 16:53:06 -08:00
// readStdinJson tests (via subprocess — safe hardcoded inputs)
2026-02-12 17:14:03 -08:00
// Use execFileSync with input option instead of shell echo|pipe for Windows compat
2026-02-12 16:53:06 -08:00
console . log ( '\nreadStdinJson():' );
2026-02-12 17:14:03 -08:00
const stdinScript = 'const u=require("./scripts/lib/utils");u.readStdinJson({timeoutMs:2000}).then(d=>{process.stdout.write(JSON.stringify(d))})' ;
const stdinOpts = { encoding : 'utf8' , cwd : path . join ( __dirname , '..' , '..' ), timeout : 5000 };
2026-02-12 16:53:06 -08:00
if ( test ( 'readStdinJson parses valid JSON from stdin' , () => {
2026-02-12 17:14:03 -08:00
const { execFileSync } = require ( 'child_process' );
const result = execFileSync ( 'node' , [ '-e' , stdinScript ], { ... stdinOpts , input : '{"tool_input":{"command":"ls"}}' });
2026-02-12 16:53:06 -08:00
const parsed = JSON . parse ( result );
assert . deepStrictEqual ( parsed , { tool_input : { command : 'ls' } });
})) passed ++ ; else failed ++ ;
if ( test ( 'readStdinJson returns {} for invalid JSON' , () => {
2026-02-12 17:14:03 -08:00
const { execFileSync } = require ( 'child_process' );
const result = execFileSync ( 'node' , [ '-e' , stdinScript ], { ... stdinOpts , input : 'not json' });
2026-02-12 16:53:06 -08:00
assert . deepStrictEqual ( JSON . parse ( result ), {});
})) passed ++ ; else failed ++ ;
if ( test ( 'readStdinJson returns {} for empty stdin' , () => {
2026-02-12 17:14:03 -08:00
const { execFileSync } = require ( 'child_process' );
const result = execFileSync ( 'node' , [ '-e' , stdinScript ], { ... stdinOpts , input : '' });
2026-02-12 16:53:06 -08:00
assert . deepStrictEqual ( JSON . parse ( result ), {});
})) passed ++ ; else failed ++ ;
if ( test ( 'readStdinJson handles nested objects' , () => {
2026-02-12 17:14:03 -08:00
const { execFileSync } = require ( 'child_process' );
const result = execFileSync ( 'node' , [ '-e' , stdinScript ], { ... stdinOpts , input : '{"a":{"b":1},"c":[1,2]}' });
2026-02-12 16:53:06 -08:00
const parsed = JSON . parse ( result );
assert . deepStrictEqual ( parsed , { a : { b : 1 }, c : [ 1 , 2 ] });
})) passed ++ ; else failed ++ ;
2026-02-13 01:18:07 -08:00
// grepFile with global regex (regression: g flag causes alternating matches)
console . log ( '\ngrepFile (global regex fix):' );
if ( test ( 'grepFile with /g flag finds ALL matching lines (not alternating)' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test-grep-g- ${ Date . now () } .txt` );
try {
// 4 consecutive lines matching the same pattern
utils . writeFile ( testFile , 'match-line\nmatch-line\nmatch-line\nmatch-line' );
// Bug: without fix, /match/g would only find lines 1 and 3 (alternating)
const matches = utils . grepFile ( testFile , /match/g );
assert . strictEqual ( matches . length , 4 , `Should find all 4 lines, found ${ matches . length } ` );
assert . strictEqual ( matches [ 0 ]. lineNumber , 1 );
assert . strictEqual ( matches [ 1 ]. lineNumber , 2 );
assert . strictEqual ( matches [ 2 ]. lineNumber , 3 );
assert . strictEqual ( matches [ 3 ]. lineNumber , 4 );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
if ( test ( 'grepFile preserves regex flags other than g (e.g. case-insensitive)' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test-grep-flags- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'FOO\nfoo\nFoO\nbar' );
const matches = utils . grepFile ( testFile , /foo/gi );
assert . strictEqual ( matches . length , 3 , `Should find 3 case-insensitive matches, found ${ matches . length } ` );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
2026-02-13 01:36:42 -08:00
// commandExists edge cases
console . log ( '\ncommandExists Edge Cases:' );
if ( test ( 'commandExists rejects empty string' , () => {
assert . strictEqual ( utils . commandExists ( '' ), false , 'Empty string should not be a valid command' );
})) passed ++ ; else failed ++ ;
if ( test ( 'commandExists rejects command with spaces' , () => {
assert . strictEqual ( utils . commandExists ( 'my command' ), false , 'Commands with spaces should be rejected' );
})) passed ++ ; else failed ++ ;
if ( test ( 'commandExists rejects command with path separators' , () => {
assert . strictEqual ( utils . commandExists ( '/usr/bin/node' ), false , 'Commands with / should be rejected' );
assert . strictEqual ( utils . commandExists ( '..\\cmd' ), false , 'Commands with \\ should be rejected' );
})) passed ++ ; else failed ++ ;
if ( test ( 'commandExists rejects shell metacharacters' , () => {
assert . strictEqual ( utils . commandExists ( 'cmd;ls' ), false , 'Semicolons should be rejected' );
assert . strictEqual ( utils . commandExists ( '$(whoami)' ), false , 'Subshell syntax should be rejected' );
assert . strictEqual ( utils . commandExists ( 'cmd|cat' ), false , 'Pipes should be rejected' );
})) passed ++ ; else failed ++ ;
if ( test ( 'commandExists allows dots and underscores' , () => {
// These are valid chars per the regex check — the command might not exist
// but it shouldn't be rejected by the validator
const dotResult = utils . commandExists ( 'definitely.not.a.real.tool.12345' );
assert . strictEqual ( typeof dotResult , 'boolean' , 'Should return boolean, not throw' );
})) passed ++ ; else failed ++ ;
// findFiles edge cases
console . log ( '\nfindFiles Edge Cases:' );
if ( test ( 'findFiles with ? wildcard matches single character' , () => {
const testDir = path . join ( utils . getTempDir (), `ff-qmark- ${ Date . now () } ` );
utils . ensureDir ( testDir );
try {
fs . writeFileSync ( path . join ( testDir , 'a1.txt' ), '' );
fs . writeFileSync ( path . join ( testDir , 'b2.txt' ), '' );
fs . writeFileSync ( path . join ( testDir , 'abc.txt' ), '' );
const results = utils . findFiles ( testDir , '??.txt' );
const names = results . map ( r => path . basename ( r . path )). sort ();
assert . deepStrictEqual ( names , [ 'a1.txt' , 'b2.txt' ], 'Should match exactly 2-char basenames' );
} finally {
fs . rmSync ( testDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
if ( test ( 'findFiles sorts by mtime (newest first)' , () => {
const testDir = path . join ( utils . getTempDir (), `ff-sort- ${ Date . now () } ` );
utils . ensureDir ( testDir );
try {
const f1 = path . join ( testDir , 'old.txt' );
const f2 = path . join ( testDir , 'new.txt' );
fs . writeFileSync ( f1 , 'old' );
// Set older mtime on first file
const past = new Date ( Date . now () - 60000 );
fs . utimesSync ( f1 , past , past );
fs . writeFileSync ( f2 , 'new' );
const results = utils . findFiles ( testDir , '*.txt' );
assert . strictEqual ( results . length , 2 );
assert . ok (
path . basename ( results [ 0 ]. path ) === 'new.txt' ,
`Newest file should be first, got ${ path . basename ( results [ 0 ]. path ) } `
);
} finally {
fs . rmSync ( testDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
if ( test ( 'findFiles with maxAge filters old files' , () => {
const testDir = path . join ( utils . getTempDir (), `ff-age- ${ Date . now () } ` );
utils . ensureDir ( testDir );
try {
const recent = path . join ( testDir , 'recent.txt' );
const old = path . join ( testDir , 'old.txt' );
fs . writeFileSync ( recent , 'new' );
fs . writeFileSync ( old , 'old' );
// Set mtime to 30 days ago
const past = new Date ( Date . now () - 30 * 24 * 60 * 60 * 1000 );
fs . utimesSync ( old , past , past );
const results = utils . findFiles ( testDir , '*.txt' , { maxAge : 7 });
assert . strictEqual ( results . length , 1 , 'Should only return recent file' );
assert . ok ( results [ 0 ]. path . includes ( 'recent.txt' ), 'Should return the recent file' );
} finally {
fs . rmSync ( testDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
// ensureDir edge cases
console . log ( '\nensureDir Edge Cases:' );
if ( test ( 'ensureDir is safe for concurrent calls (EEXIST race)' , () => {
const testDir = path . join ( utils . getTempDir (), `ensure-race- ${ Date . now () } ` , 'nested' );
try {
// Call concurrently — both should succeed without throwing
const results = [ utils . ensureDir ( testDir ), utils . ensureDir ( testDir )];
assert . strictEqual ( results [ 0 ], testDir );
assert . strictEqual ( results [ 1 ], testDir );
assert . ok ( fs . existsSync ( testDir ));
} finally {
fs . rmSync ( path . dirname ( testDir ), { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
if ( test ( 'ensureDir returns the directory path' , () => {
const testDir = path . join ( utils . getTempDir (), `ensure-ret- ${ Date . now () } ` );
try {
const result = utils . ensureDir ( testDir );
assert . strictEqual ( result , testDir , 'Should return the directory path' );
} finally {
fs . rmSync ( testDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
// runCommand edge cases
console . log ( '\nrunCommand Edge Cases:' );
if ( test ( 'runCommand returns trimmed output' , () => {
2026-02-18 15:10:32 +00:00
// Windows echo includes quotes in output, use node to ensure consistent behavior
const result = utils . runCommand ( 'node -e "process.stdout.write(\' hello \')"' );
2026-02-13 01:36:42 -08:00
assert . strictEqual ( result . success , true );
assert . strictEqual ( result . output , 'hello' , 'Should trim leading/trailing whitespace' );
})) passed ++ ; else failed ++ ;
if ( test ( 'runCommand captures stderr on failure' , () => {
const result = utils . runCommand ( 'node -e "process.exit(1)"' );
assert . strictEqual ( result . success , false );
assert . ok ( typeof result . output === 'string' , 'Output should be a string on failure' );
})) passed ++ ; else failed ++ ;
// getGitModifiedFiles edge cases
console . log ( '\ngetGitModifiedFiles Edge Cases:' );
if ( test ( 'getGitModifiedFiles returns array with empty patterns' , () => {
const files = utils . getGitModifiedFiles ([]);
assert . ok ( Array . isArray ( files ), 'Should return array' );
})) passed ++ ; else failed ++ ;
// replaceInFile edge cases
console . log ( '\nreplaceInFile Edge Cases:' );
if ( test ( 'replaceInFile with regex capture groups works correctly' , () => {
const testFile = path . join ( utils . getTempDir (), `replace-capture- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'version: 1.0.0' );
const result = utils . replaceInFile ( testFile , /version: (\d+)\.(\d+)\.(\d+)/ , 'version: $1.$2.99' );
assert . strictEqual ( result , true );
assert . strictEqual ( utils . readFile ( testFile ), 'version: 1.0.99' );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
// readStdinJson (function API, not actual stdin — more thorough edge cases)
console . log ( '\nreadStdinJson Edge Cases:' );
if ( test ( 'readStdinJson type check: returns a Promise' , () => {
// readStdinJson returns a Promise regardless of stdin state
const result = utils . readStdinJson ({ timeoutMs : 100 });
assert . ok ( result instanceof Promise , 'Should return a Promise' );
// Don't await — just verify it's a Promise type
})) passed ++ ; else failed ++ ;
2026-02-13 03:11:51 -08:00
// ── Round 28: readStdinJson maxSize truncation and edge cases ──
console . log ( '\nreadStdinJson maxSize truncation:' );
if ( test ( 'readStdinJson maxSize stops accumulating after threshold (chunk-level guard)' , () => {
2026-02-18 15:10:32 +00:00
if ( process . platform === 'win32' ) {
console . log ( ' (skipped — stdin chunking behavior differs on Windows)' );
return true ;
}
2026-02-13 03:11:51 -08:00
const { execFileSync } = require ( 'child_process' );
// maxSize is a chunk-level guard: once data.length >= maxSize, no MORE chunks are added.
// A single small chunk that arrives when data.length < maxSize is added in full.
// To test multi-chunk behavior, we send >64KB (Node default highWaterMark=16KB)
// which should arrive in multiple chunks. With maxSize=100, only the first chunk(s)
// totaling under 100 bytes should be captured; subsequent chunks are dropped.
const script = 'const u=require("./scripts/lib/utils");u.readStdinJson({timeoutMs:2000,maxSize:100}).then(d=>{process.stdout.write(JSON.stringify(d))})' ;
// Generate 100KB of data (arrives in multiple chunks)
const bigInput = '{"k":"' + 'X' . repeat ( 100000 ) + '"}' ;
const result = execFileSync ( 'node' , [ '-e' , script ], { ... stdinOpts , input : bigInput });
// Truncated mid-string → invalid JSON → resolves to {}
assert . deepStrictEqual ( JSON . parse ( result ), {});
})) passed ++ ; else failed ++ ;
if ( test ( 'readStdinJson with maxSize large enough preserves valid JSON' , () => {
const { execFileSync } = require ( 'child_process' );
const script = 'const u=require("./scripts/lib/utils");u.readStdinJson({timeoutMs:2000,maxSize:1024}).then(d=>{process.stdout.write(JSON.stringify(d))})' ;
const input = JSON . stringify ({ key : 'value' });
const result = execFileSync ( 'node' , [ '-e' , script ], { ... stdinOpts , input });
assert . deepStrictEqual ( JSON . parse ( result ), { key : 'value' });
})) passed ++ ; else failed ++ ;
if ( test ( 'readStdinJson resolves {} for whitespace-only stdin' , () => {
const { execFileSync } = require ( 'child_process' );
const result = execFileSync ( 'node' , [ '-e' , stdinScript ], { ... stdinOpts , input : ' \n \t ' });
// data.trim() is empty → resolves {}
assert . deepStrictEqual ( JSON . parse ( result ), {});
})) passed ++ ; else failed ++ ;
if ( test ( 'readStdinJson handles JSON with trailing whitespace/newlines' , () => {
const { execFileSync } = require ( 'child_process' );
const result = execFileSync ( 'node' , [ '-e' , stdinScript ], { ... stdinOpts , input : '{"a":1} \n\n' });
assert . deepStrictEqual ( JSON . parse ( result ), { a : 1 });
})) passed ++ ; else failed ++ ;
if ( test ( 'readStdinJson handles JSON with BOM prefix (returns {})' , () => {
const { execFileSync } = require ( 'child_process' );
// BOM (\uFEFF) before JSON makes it invalid for JSON.parse
const result = execFileSync ( 'node' , [ '-e' , stdinScript ], { ... stdinOpts , input : '\uFEFF{"a":1}' });
// BOM prefix makes JSON.parse fail → resolve {}
assert . deepStrictEqual ( JSON . parse ( result ), {});
})) passed ++ ; else failed ++ ;
2026-02-13 03:37:46 -08:00
// ── Round 31: ensureDir error propagation ──
console . log ( '\nensureDir Error Propagation (Round 31):' );
if ( test ( 'ensureDir wraps non-EEXIST errors with descriptive message' , () => {
// Attempting to create a dir under a file should fail with ENOTDIR, not EEXIST
const testFile = path . join ( utils . getTempDir (), `ensure-err- ${ Date . now () } .txt` );
try {
fs . writeFileSync ( testFile , 'blocking file' );
const badPath = path . join ( testFile , 'subdir' );
assert . throws (
() => utils . ensureDir ( badPath ),
( err ) => err . message . includes ( 'Failed to create directory' ),
'Should throw with descriptive "Failed to create directory" message'
);
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
if ( test ( 'ensureDir error includes the directory path' , () => {
const testFile = path . join ( utils . getTempDir (), `ensure-err2- ${ Date . now () } .txt` );
try {
fs . writeFileSync ( testFile , 'blocker' );
const badPath = path . join ( testFile , 'nested' , 'dir' );
try {
utils . ensureDir ( badPath );
assert . fail ( 'Should have thrown' );
} catch ( err ) {
assert . ok ( err . message . includes ( badPath ), 'Error should include the target path' );
}
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
// ── Round 31: runCommand stderr preference on failure ──
console . log ( '\nrunCommand failure output (Round 31):' );
if ( test ( 'runCommand returns stderr content on failure when stderr exists' , () => {
const result = utils . runCommand ( 'node -e "process.stderr.write(\'custom error\'); process.exit(1)"' );
assert . strictEqual ( result . success , false );
assert . ok ( result . output . includes ( 'custom error' ), 'Should include stderr output' );
})) passed ++ ; else failed ++ ;
2026-03-08 08:47:31 +10:00
if ( test ( 'runCommand returns error output on failed command' , () => {
// Use an allowed prefix with a nonexistent subcommand to reach execSync
const result = utils . runCommand ( 'git nonexistent-subcmd-xyz-12345' );
2026-02-13 03:37:46 -08:00
assert . strictEqual ( result . success , false );
assert . ok ( result . output . length > 0 , 'Should have some error output' );
})) passed ++ ; else failed ++ ;
2026-03-08 08:47:31 +10:00
// ── runCommand security: allowlist and metacharacter blocking ──
console . log ( '\nrunCommand Security (allowlist + metacharacters):' );
if ( test ( 'runCommand blocks disallowed command prefix' , () => {
const result = utils . runCommand ( 'rm -rf /' );
assert . strictEqual ( result . success , false );
assert . ok ( result . output . includes ( 'unrecognized command prefix' ), 'Should mention blocked prefix' );
})) passed ++ ; else failed ++ ;
if ( test ( 'runCommand blocks curl command' , () => {
const result = utils . runCommand ( 'curl http://example.com' );
assert . strictEqual ( result . success , false );
assert . ok ( result . output . includes ( 'unrecognized command prefix' ));
})) passed ++ ; else failed ++ ;
if ( test ( 'runCommand blocks bash command' , () => {
const result = utils . runCommand ( 'bash -c "echo hello"' );
assert . strictEqual ( result . success , false );
assert . ok ( result . output . includes ( 'unrecognized command prefix' ));
})) passed ++ ; else failed ++ ;
if ( test ( 'runCommand blocks semicolon command chaining' , () => {
const result = utils . runCommand ( 'git status; echo pwned' );
assert . strictEqual ( result . success , false );
assert . ok ( result . output . includes ( 'metacharacters not allowed' ), 'Should block semicolon chaining' );
})) passed ++ ; else failed ++ ;
if ( test ( 'runCommand blocks pipe command chaining' , () => {
const result = utils . runCommand ( 'git log | cat' );
assert . strictEqual ( result . success , false );
assert . ok ( result . output . includes ( 'metacharacters not allowed' ), 'Should block pipe chaining' );
})) passed ++ ; else failed ++ ;
if ( test ( 'runCommand blocks ampersand command chaining' , () => {
const result = utils . runCommand ( 'git status && echo pwned' );
assert . strictEqual ( result . success , false );
assert . ok ( result . output . includes ( 'metacharacters not allowed' ), 'Should block ampersand chaining' );
})) passed ++ ; else failed ++ ;
if ( test ( 'runCommand blocks dollar sign command substitution' , () => {
const result = utils . runCommand ( 'git log $(whoami)' );
assert . strictEqual ( result . success , false );
assert . ok ( result . output . includes ( 'metacharacters not allowed' ), 'Should block $ substitution' );
})) passed ++ ; else failed ++ ;
if ( test ( 'runCommand blocks backtick command substitution' , () => {
const result = utils . runCommand ( 'git log `whoami`' );
assert . strictEqual ( result . success , false );
assert . ok ( result . output . includes ( 'metacharacters not allowed' ), 'Should block backtick substitution' );
})) passed ++ ; else failed ++ ;
if ( test ( 'runCommand allows metacharacters inside double quotes' , () => {
// Semicolon inside quotes should not trigger metacharacter blocking
const result = utils . runCommand ( 'node -e "console.log(1);process.exit(0)"' );
assert . strictEqual ( result . success , true );
})) passed ++ ; else failed ++ ;
if ( test ( 'runCommand allows metacharacters inside single quotes' , () => {
const result = utils . runCommand ( "node -e 'process.exit(0);'" );
assert . strictEqual ( result . success , true );
})) passed ++ ; else failed ++ ;
if ( test ( 'runCommand blocks unquoted metacharacters alongside quoted ones' , () => {
// Semicolon inside quotes is safe, but && outside is not
const result = utils . runCommand ( 'git log "safe;part" && echo pwned' );
assert . strictEqual ( result . success , false );
assert . ok ( result . output . includes ( 'metacharacters not allowed' ));
})) passed ++ ; else failed ++ ;
if ( test ( 'runCommand blocks prefix without trailing space' , () => {
// "gitconfig" starts with "git" but not "git " — must be blocked
const result = utils . runCommand ( 'gitconfig --list' );
assert . strictEqual ( result . success , false );
assert . ok ( result . output . includes ( 'unrecognized command prefix' ));
})) passed ++ ; else failed ++ ;
if ( test ( 'runCommand allows npx prefix' , () => {
const result = utils . runCommand ( 'npx --version' );
assert . strictEqual ( result . success , true );
})) passed ++ ; else failed ++ ;
if ( test ( 'runCommand blocks newline command injection' , () => {
const result = utils . runCommand ( 'git status\necho pwned' );
assert . strictEqual ( result . success , false );
assert . ok ( result . output . includes ( 'metacharacters not allowed' ), 'Should block newline injection' );
})) passed ++ ; else failed ++ ;
if ( test ( 'runCommand blocks $() inside double quotes (shell still evaluates)' , () => {
// $() inside double quotes is still evaluated by the shell, so block $ everywhere
const result = utils . runCommand ( 'node -e "$(whoami)"' );
assert . strictEqual ( result . success , false );
assert . ok ( result . output . includes ( 'metacharacters not allowed' ), 'Should block $ inside quotes' );
})) passed ++ ; else failed ++ ;
if ( test ( 'runCommand blocks backtick inside double quotes (shell still evaluates)' , () => {
const result = utils . runCommand ( 'node -e "`whoami`"' );
assert . strictEqual ( result . success , false );
assert . ok ( result . output . includes ( 'metacharacters not allowed' ), 'Should block backtick inside quotes' );
})) passed ++ ; else failed ++ ;
if ( test ( 'runCommand error message does not leak command string' , () => {
const secret = 'rm secret_password_123' ;
const result = utils . runCommand ( secret );
assert . strictEqual ( result . success , false );
assert . ok ( ! result . output . includes ( 'secret_password_123' ), 'Should not leak command contents' );
})) passed ++ ; else failed ++ ;
2026-02-13 03:37:46 -08:00
// ── Round 31: getGitModifiedFiles with empty patterns ──
console . log ( '\ngetGitModifiedFiles empty patterns (Round 31):' );
if ( test ( 'getGitModifiedFiles with empty array returns all modified files' , () => {
// With an empty patterns array, every file should match (no filter applied)
const withEmpty = utils . getGitModifiedFiles ([]);
const withNone = utils . getGitModifiedFiles ();
// Both should return the same list (no filtering)
assert . deepStrictEqual ( withEmpty , withNone ,
'Empty patterns array should behave same as no patterns' );
})) passed ++ ; else failed ++ ;
2026-02-13 03:50:44 -08:00
// ── Round 33: readStdinJson error event handling ──
console . log ( '\nreadStdinJson error event (Round 33):' );
if ( test ( 'readStdinJson resolves {} when stdin emits error (via broken pipe)' , () => {
// Spawn a subprocess that reads from stdin, but close the pipe immediately
// to trigger an error or early-end condition
const { execFileSync } = require ( 'child_process' );
const script = 'const u=require("./scripts/lib/utils");u.readStdinJson({timeoutMs:2000}).then(d=>{process.stdout.write(JSON.stringify(d))})' ;
// Pipe stdin from /dev/null — this sends EOF immediately (no data)
const result = execFileSync ( 'node' , [ '-e' , script ], {
encoding : 'utf8' ,
input : '' , // empty stdin triggers 'end' with empty data
timeout : 5000 ,
cwd : path . join ( __dirname , '..' , '..' ),
});
const parsed = JSON . parse ( result );
assert . deepStrictEqual ( parsed , {}, 'Should resolve to {} for empty stdin (end event path)' );
})) passed ++ ; else failed ++ ;
if ( test ( 'readStdinJson error handler is guarded by settled flag' , () => {
// If 'end' fires first setting settled=true, then a late 'error' should be ignored
// We test this by verifying the code structure works: send valid JSON, the end event
// fires, settled=true, any late error is safely ignored
const { execFileSync } = require ( 'child_process' );
const script = 'const u=require("./scripts/lib/utils");u.readStdinJson({timeoutMs:2000}).then(d=>{process.stdout.write(JSON.stringify(d))})' ;
const result = execFileSync ( 'node' , [ '-e' , script ], {
encoding : 'utf8' ,
input : '{"test":"settled-guard"}' ,
timeout : 5000 ,
cwd : path . join ( __dirname , '..' , '..' ),
});
const parsed = JSON . parse ( result );
assert . strictEqual ( parsed . test , 'settled-guard' , 'Should parse normally when end fires first' );
})) passed ++ ; else failed ++ ;
2026-02-13 06:44:52 -08:00
// replaceInFile returns false when write fails (e.g., read-only file)
if ( test ( 'replaceInFile returns false on write failure (read-only file)' , () => {
if ( process . platform === 'win32' || process . getuid ? .() === 0 ) {
console . log ( ' (skipped — chmod ineffective on Windows/root)' );
return ;
}
const testDir = path . join ( utils . getTempDir (), `utils-test-readonly- ${ Date . now () } ` );
fs . mkdirSync ( testDir , { recursive : true });
const filePath = path . join ( testDir , 'readonly.txt' );
try {
fs . writeFileSync ( filePath , 'hello world' , 'utf8' );
fs . chmodSync ( filePath , 0o444 );
const result = utils . replaceInFile ( filePath , 'hello' , 'goodbye' );
assert . strictEqual ( result , false , 'Should return false when file is read-only' );
// Verify content unchanged
const content = fs . readFileSync ( filePath , 'utf8' );
assert . strictEqual ( content , 'hello world' , 'Original content should be preserved' );
} finally {
fs . chmodSync ( filePath , 0o644 );
fs . rmSync ( testDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 07:35:20 -08:00
// ── Round 69: getGitModifiedFiles with ALL invalid patterns ──
console . log ( '\ngetGitModifiedFiles all-invalid patterns (Round 69):' );
if ( test ( 'getGitModifiedFiles with all-invalid patterns skips filtering (returns all files)' , () => {
// When every pattern is invalid regex, compiled.length === 0 at line 386,
// so the filtering is skipped entirely and all modified files are returned.
// This differs from the mixed-valid test where at least one pattern compiles.
const allInvalid = utils . getGitModifiedFiles ([ '(unclosed' , '[bad' , '**invalid' ]);
const unfiltered = utils . getGitModifiedFiles ();
// Both should return the same list — all-invalid patterns = no filtering
assert . deepStrictEqual ( allInvalid , unfiltered ,
'All-invalid patterns should return same result as no patterns (no filtering)' );
})) passed ++ ; else failed ++ ;
2026-02-13 08:01:15 -08:00
// ── Round 71: findFiles recursive scan skips unreadable subdirectory ──
console . log ( '\nRound 71: findFiles (unreadable subdirectory in recursive scan):' );
if ( test ( 'findFiles recursive scan skips unreadable subdirectory silently' , () => {
if ( process . platform === 'win32' || process . getuid ? .() === 0 ) {
console . log ( ' (skipped — chmod ineffective on Windows/root)' );
return ;
}
const tmpDir = path . join ( utils . getTempDir (), `ecc-findfiles-r71- ${ Date . now () } ` );
const readableSubdir = path . join ( tmpDir , 'readable' );
const unreadableSubdir = path . join ( tmpDir , 'unreadable' );
fs . mkdirSync ( readableSubdir , { recursive : true });
fs . mkdirSync ( unreadableSubdir , { recursive : true });
// Create files in both subdirectories
fs . writeFileSync ( path . join ( readableSubdir , 'found.txt' ), 'data' );
fs . writeFileSync ( path . join ( unreadableSubdir , 'hidden.txt' ), 'data' );
// Make the subdirectory unreadable — readdirSync will throw EACCES
fs . chmodSync ( unreadableSubdir , 0o000 );
try {
const results = utils . findFiles ( tmpDir , '*.txt' , { recursive : true });
// Should find the readable file but silently skip the unreadable dir
assert . ok ( results . length >= 1 , 'Should find at least the readable file' );
const paths = results . map ( r => r . path );
assert . ok ( paths . some ( p => p . includes ( 'found.txt' )), 'Should find readable/found.txt' );
assert . ok ( ! paths . some ( p => p . includes ( 'hidden.txt' )), 'Should not find unreadable/hidden.txt' );
} finally {
fs . chmodSync ( unreadableSubdir , 0o755 );
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 10:26:58 -08:00
// ── Round 79: countInFile with valid string pattern ──
console . log ( '\nRound 79: countInFile (valid string pattern):' );
if ( test ( 'countInFile counts occurrences using a plain string pattern' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test-count-str- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'apple banana apple cherry apple' );
// Pass a plain string (not RegExp) — exercises typeof pattern === 'string'
// branch at utils.js:441-442 which creates new RegExp(pattern, 'g')
const count = utils . countInFile ( testFile , 'apple' );
assert . strictEqual ( count , 3 , 'String pattern should count all occurrences' );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
// ── Round 79: grepFile with valid string pattern ──
console . log ( '\nRound 79: grepFile (valid string pattern):' );
if ( test ( 'grepFile finds matching lines using a plain string pattern' , () => {
const testFile = path . join ( utils . getTempDir (), `utils-test-grep-str- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'line1 alpha\nline2 beta\nline3 alpha\nline4 gamma' );
// Pass a plain string (not RegExp) — exercises the else branch
// at utils.js:468-469 which creates new RegExp(pattern)
const matches = utils . grepFile ( testFile , 'alpha' );
assert . strictEqual ( matches . length , 2 , 'String pattern should find 2 matching lines' );
assert . strictEqual ( matches [ 0 ]. lineNumber , 1 , 'First match at line 1' );
assert . strictEqual ( matches [ 1 ]. lineNumber , 3 , 'Second match at line 3' );
assert . ok ( matches [ 0 ]. content . includes ( 'alpha' ), 'Content should include pattern' );
} finally {
fs . unlinkSync ( testFile );
}
})) passed ++ ; else failed ++ ;
2026-02-13 11:35:22 -08:00
// ── Round 84: findFiles inner statSync catch (TOCTOU — broken symlink) ──
console . log ( '\nRound 84: findFiles (inner statSync catch — broken symlink):' );
if ( test ( 'findFiles skips broken symlinks that match the pattern' , () => {
// findFiles at utils.js:170-173: readdirSync returns entries including broken
// symlinks (entry.isFile() returns false for broken symlinks, but the test also
// verifies the overall robustness). On some systems, broken symlinks can be
// returned by readdirSync and pass through isFile() depending on the driver.
// More importantly: if statSync throws inside the inner loop, catch continues.
//
// To reliably trigger the statSync catch: create a real file, list it, then
// simulate the race. Since we can't truly race, we use a broken symlink which
// will at minimum verify the function doesn't crash on unusual dir entries.
const tmpDir = path . join ( utils . getTempDir (), `ecc-r84-findfiles-toctou- ${ Date . now () } ` );
fs . mkdirSync ( tmpDir , { recursive : true });
// Create a real file and a broken symlink, both matching *.txt
const realFile = path . join ( tmpDir , 'real.txt' );
fs . writeFileSync ( realFile , 'content' );
const brokenLink = path . join ( tmpDir , 'broken.txt' );
fs . symlinkSync ( '/nonexistent/path/does/not/exist' , brokenLink );
try {
const results = utils . findFiles ( tmpDir , '*.txt' );
// The real file should be found; the broken symlink should be skipped
const paths = results . map ( r => r . path );
assert . ok ( paths . some ( p => p . includes ( 'real.txt' )), 'Should find the real file' );
assert . ok ( ! paths . some ( p => p . includes ( 'broken.txt' )),
'Should not include broken symlink in results' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 12:11:26 -08:00
// ── Round 85: getSessionIdShort fallback parameter ──
console . log ( '\ngetSessionIdShort fallback (Round 85):' );
if ( test ( 'getSessionIdShort uses fallback when getProjectName returns null (CWD at root)' , () => {
if ( process . platform === 'win32' ) {
console . log ( ' (skipped — root CWD differs on Windows)' );
return ;
}
// Spawn a subprocess at CWD=/ with CLAUDE_SESSION_ID empty.
// At /, git rev-parse --show-toplevel fails → getGitRepoName() = null.
// path.basename('/') = '' → '' || null = null → getProjectName() = null.
// So getSessionIdShort('my-custom-fallback') = null || 'my-custom-fallback'.
const utilsPath = path . join ( __dirname , '..' , '..' , 'scripts' , 'lib' , 'utils.js' );
const script = `
const utils = require(' ${ utilsPath . replace ( /'/g , "\\'" ) } ');
process.stdout.write(utils.getSessionIdShort('my-custom-fallback'));
` ;
const { spawnSync } = require ( 'child_process' );
const result = spawnSync ( 'node' , [ '-e' , script ], {
encoding : 'utf8' ,
cwd : '/' ,
env : { ... process . env , CLAUDE_SESSION_ID : '' },
timeout : 10000
});
assert . strictEqual ( result . status , 0 , `Should exit 0, got status ${ result . status } . stderr: ${ result . stderr } ` );
assert . strictEqual ( result . stdout , 'my-custom-fallback' ,
`At CWD=/ with no session ID, should use the fallback parameter. Got: " ${ result . stdout } "` );
})) passed ++ ; else failed ++ ;
2026-02-13 12:49:53 -08:00
// ── Round 88: replaceInFile with empty replacement (deletion) ──
console . log ( '\nRound 88: replaceInFile with empty replacement string (deletion):' );
if ( test ( 'replaceInFile with empty string replacement deletes matched text' , () => {
const tmpDir = path . join ( utils . getTempDir (), `ecc-r88-replace-empty- ${ Date . now () } ` );
fs . mkdirSync ( tmpDir , { recursive : true });
const tmpFile = path . join ( tmpDir , 'delete-test.txt' );
try {
fs . writeFileSync ( tmpFile , 'hello REMOVE_ME world' );
const result = utils . replaceInFile ( tmpFile , 'REMOVE_ME ' , '' );
assert . strictEqual ( result , true , 'Should return true on successful replacement' );
const content = fs . readFileSync ( tmpFile , 'utf8' );
assert . strictEqual ( content , 'hello world' ,
'Empty replacement should delete the matched text' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
// ── Round 88: countInFile with valid file but zero matches ──
console . log ( '\nRound 88: countInFile with existing file but non-matching pattern:' );
if ( test ( 'countInFile returns 0 for valid file with no pattern matches' , () => {
const tmpDir = path . join ( utils . getTempDir (), `ecc-r88-count-zero- ${ Date . now () } ` );
fs . mkdirSync ( tmpDir , { recursive : true });
const tmpFile = path . join ( tmpDir , 'no-match.txt' );
try {
fs . writeFileSync ( tmpFile , 'apple banana cherry' );
const count = utils . countInFile ( tmpFile , 'ZZZZNOTHERE' );
assert . strictEqual ( count , 0 ,
'Should return 0 when regex matches nothing in existing file' );
const countRegex = utils . countInFile ( tmpFile , /ZZZZNOTHERE/g );
assert . strictEqual ( countRegex , 0 ,
'Should return 0 for RegExp with no matches in existing file' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 14:05:24 -08:00
// ── Round 92: countInFile with object pattern type ──
console . log ( '\nRound 92: countInFile (non-string non-RegExp pattern):' );
if ( test ( 'countInFile returns 0 for object pattern (neither string nor RegExp)' , () => {
// utils.js line 443-444: The else branch returns 0 when pattern is
// not instanceof RegExp and typeof !== 'string'. An object like {invalid: true}
// triggers this early return without throwing.
const testFile = path . join ( utils . getTempDir (), `utils-test-obj-pattern- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'some test content to match against' );
const count = utils . countInFile ( testFile , { invalid : 'object' });
assert . strictEqual ( count , 0 , 'Object pattern should return 0' );
} finally {
try { fs . unlinkSync ( testFile ); } catch { /* best-effort */ }
}
})) passed ++ ; else failed ++ ;
2026-02-13 14:21:03 -08:00
// ── Round 93: countInFile with /pattern/i (g flag appended) ──
console . log ( '\nRound 93: countInFile (case-insensitive RegExp, g flag auto-appended):' );
if ( test ( 'countInFile with /pattern/i appends g flag and counts case-insensitively' , () => {
// utils.js line 440: pattern.flags = 'i', 'i'.includes('g') → false,
// so new RegExp(source, 'i' + 'g') → /pattern/ig
const testFile = path . join ( utils . getTempDir (), `utils-test-ci-flag- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'Foo foo FOO fOo bar baz' );
const count = utils . countInFile ( testFile , /foo/i );
assert . strictEqual ( count , 4 ,
'Case-insensitive regex with auto-appended g should match all 4 occurrences' );
} finally {
try { fs . unlinkSync ( testFile ); } catch { /* best-effort */ }
}
})) passed ++ ; else failed ++ ;
// ── Round 93: countInFile with /pattern/gi (g flag already present) ──
console . log ( '\nRound 93: countInFile (case-insensitive RegExp, g flag preserved):' );
if ( test ( 'countInFile with /pattern/gi preserves existing flags and counts correctly' , () => {
// utils.js line 440: pattern.flags = 'gi', 'gi'.includes('g') → true,
// so new RegExp(source, 'gi') — flags preserved unchanged
const testFile = path . join ( utils . getTempDir (), `utils-test-gi-flag- ${ Date . now () } .txt` );
try {
utils . writeFile ( testFile , 'Foo foo FOO fOo bar baz' );
const count = utils . countInFile ( testFile , /foo/gi );
assert . strictEqual ( count , 4 ,
'Case-insensitive regex with pre-existing g should match all 4 occurrences' );
} finally {
try { fs . unlinkSync ( testFile ); } catch { /* best-effort */ }
}
})) passed ++ ; else failed ++ ;
2026-02-13 14:50:49 -08:00
// ── Round 95: countInFile with regex alternation (no g flag) ──
console . log ( '\nRound 95: countInFile (regex alternation without g flag):' );
if ( test ( 'countInFile with /apple|banana/ (alternation, no g) counts all matches' , () => {
const tmpDir = path . join ( utils . getTempDir (), `ecc-r95-alternation- ${ Date . now () } ` );
fs . mkdirSync ( tmpDir , { recursive : true });
const testFile = path . join ( tmpDir , 'alternation.txt' );
try {
utils . writeFile ( testFile , 'apple banana apple cherry banana apple' );
// /apple|banana/ has alternation but no g flag — countInFile should auto-append g
const count = utils . countInFile ( testFile , /apple|banana/ );
assert . strictEqual ( count , 5 ,
'Should find 3 apples + 2 bananas = 5 total (g flag auto-appended to alternation regex)' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 15:28:06 -08:00
// ── Round 97: getSessionIdShort with whitespace-only CLAUDE_SESSION_ID ──
console . log ( '\nRound 97: getSessionIdShort (whitespace-only session ID):' );
2026-03-24 23:08:27 -04:00
if ( test ( 'getSessionIdShort sanitizes whitespace-only CLAUDE_SESSION_ID to fallback' , () => {
if ( process . platform === 'win32' ) {
console . log ( ' (skipped — root CWD differs on Windows)' );
return true ;
2026-02-13 15:28:06 -08:00
}
2026-03-24 23:08:27 -04:00
const utilsPath = path . join ( __dirname , '..' , '..' , 'scripts' , 'lib' , 'utils.js' );
const script = `
const utils = require(' ${ utilsPath . replace ( /'/g , "\\'" ) } ');
process.stdout.write(utils.getSessionIdShort('fallback'));
` ;
const result = spawnSync ( 'node' , [ '-e' , script ], {
encoding : 'utf8' ,
cwd : '/' ,
env : { ... process . env , CLAUDE_SESSION_ID : ' ' },
timeout : 10000
});
assert . strictEqual ( result . status , 0 , `Expected exit 0, got ${ result . status } . stderr: ${ result . stderr } ` );
assert . strictEqual ( result . stdout , 'fallback' );
2026-02-13 15:28:06 -08:00
})) passed ++ ; else failed ++ ;
// ── Round 97: countInFile with same RegExp object called twice (lastIndex reuse) ──
console . log ( '\nRound 97: countInFile (RegExp lastIndex reuse validation):' );
if ( test ( 'countInFile returns consistent count when same RegExp object is reused' , () => {
// utils.js lines 438-440: Always creates a new RegExp to prevent lastIndex
// state bugs. Without this defense, a global regex's lastIndex would persist
// between calls, causing alternating match/miss behavior.
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r97-lastindex-' ));
const testFile = path . join ( tmpDir , 'test.txt' );
try {
fs . writeFileSync ( testFile , 'foo bar foo baz foo\nfoo again foo' );
const sharedRegex = /foo/g ;
// First call
const count1 = utils . countInFile ( testFile , sharedRegex );
// Second call with SAME regex object — would fail without defensive new RegExp
const count2 = utils . countInFile ( testFile , sharedRegex );
assert . strictEqual ( count1 , 5 , 'First call should find 5 matches' );
assert . strictEqual ( count2 , 5 ,
'Second call with same RegExp should also find 5 (lastIndex reset by defensive code)' );
assert . strictEqual ( count1 , count2 ,
'Both calls must return identical counts (proves lastIndex is not shared)' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 15:35:18 -08:00
// ── Round 98: findFiles with maxAge: -1 (negative boundary — excludes everything) ──
console . log ( '\nRound 98: findFiles (maxAge: -1 — negative boundary excludes all):' );
if ( test ( 'findFiles with maxAge: -1 excludes all files (ageInDays always >= 0)' , () => {
// utils.js line 176-178: `if (maxAge !== null) { ageInDays = ...; if (ageInDays <= maxAge) }`
// With maxAge: -1, the condition requires ageInDays <= -1. Since ageInDays =
// (Date.now() - mtimeMs) / 86400000 is always >= 0 for real files, nothing passes.
// This negative boundary deterministically excludes everything.
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r98-maxage-neg-' ));
try {
fs . writeFileSync ( path . join ( tmpDir , 'fresh.txt' ), 'created just now' );
const results = utils . findFiles ( tmpDir , '*.txt' , { maxAge : - 1 });
assert . strictEqual ( results . length , 0 ,
'maxAge: -1 should exclude all files (ageInDays is always >= 0)' );
// Contrast: maxAge: null (default) should include the file
const noMaxAge = utils . findFiles ( tmpDir , '*.txt' );
assert . strictEqual ( noMaxAge . length , 1 ,
'No maxAge (null default) should include the file (proving it exists)' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 15:41:15 -08:00
// ── Round 99: replaceInFile returns true even when pattern not found ──
console . log ( '\nRound 99: replaceInFile (no-match still returns true):' );
if ( test ( 'replaceInFile returns true and rewrites file even when search does not match' , () => {
// utils.js lines 405-417: replaceInFile reads content, calls content.replace(search, replace),
// and writes back the result. When the search pattern doesn't match anything,
// String.replace() returns the original string unchanged, but the function still
// writes it back to disk (changing mtime) and returns true. This means callers
// cannot distinguish "replacement made" from "no match found."
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r99-no-match-' ));
const testFile = path . join ( tmpDir , 'test.txt' );
try {
fs . writeFileSync ( testFile , 'hello world' );
const result = utils . replaceInFile ( testFile , 'NONEXISTENT_PATTERN' , 'replacement' );
assert . strictEqual ( result , true ,
'replaceInFile returns true even when pattern is not found (no match guard)' );
const content = fs . readFileSync ( testFile , 'utf8' );
assert . strictEqual ( content , 'hello world' ,
'Content should be unchanged since pattern did not match' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
// ── Round 99: grepFile with CR-only line endings (\r without \n) ──
console . log ( '\nRound 99: grepFile (CR-only line endings — classic Mac format):' );
if ( test ( 'grepFile treats CR-only file as a single line (splits on \\n only)' , () => {
// utils.js line 474: `content.split('\\n')` splits only on \\n (LF).
// A file using \\r (CR) line endings (classic Mac format) has no \\n characters,
// so split('\\n') returns the entire content as a single element array.
// This means grepFile reports everything on "line 1" regardless of \\r positions.
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r99-cr-only-' ));
const testFile = path . join ( tmpDir , 'cr-only.txt' );
try {
// Write file with CR-only line endings (no LF)
fs . writeFileSync ( testFile , 'alpha\rbeta\rgamma' );
const matches = utils . grepFile ( testFile , 'beta' );
assert . strictEqual ( matches . length , 1 ,
'Should find exactly 1 match (entire file is one "line")' );
assert . strictEqual ( matches [ 0 ]. lineNumber , 1 ,
'Match should be reported on line 1 (no \\n splitting occurred)' );
assert . ok ( matches [ 0 ]. content . includes ( '\r' ),
'Content should contain \\r characters (unsplit)' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 15:49:06 -08:00
// ── Round 100: findFiles with both maxAge AND recursive (interaction test) ──
console . log ( '\nRound 100: findFiles (maxAge + recursive combined — untested interaction):' );
if ( test ( 'findFiles with maxAge AND recursive filters age across subdirectories' , () => {
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r100-maxage-recur-' ));
const subDir = path . join ( tmpDir , 'nested' );
try {
fs . mkdirSync ( subDir );
// Create files: one in root, one in subdirectory
const rootFile = path . join ( tmpDir , 'root.txt' );
const nestedFile = path . join ( subDir , 'nested.txt' );
fs . writeFileSync ( rootFile , 'root file' );
fs . writeFileSync ( nestedFile , 'nested file' );
// maxAge: 1 with recursive: true — both files are fresh (ageInDays ≈ 0)
const results = utils . findFiles ( tmpDir , '*.txt' , { maxAge : 1 , recursive : true });
assert . strictEqual ( results . length , 2 ,
'Both root and nested files should match (fresh, maxAge: 1, recursive: true)' );
// maxAge: -1 with recursive: true — no files should match (age always >= 0)
const noResults = utils . findFiles ( tmpDir , '*.txt' , { maxAge : - 1 , recursive : true });
assert . strictEqual ( noResults . length , 0 ,
'maxAge: -1 should exclude all files even in subdirectories' );
// maxAge: 1 with recursive: false — only root file
const rootOnly = utils . findFiles ( tmpDir , '*.txt' , { maxAge : 1 , recursive : false });
assert . strictEqual ( rootOnly . length , 1 ,
'recursive: false should only find root-level file' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 15:54:02 -08:00
// ── Round 101: output() with circular reference object throws (no try/catch around JSON.stringify) ──
console . log ( '\nRound 101: output() (circular reference — JSON.stringify crash):' );
if ( test ( 'output() throws TypeError on circular reference object (JSON.stringify has no try/catch)' , () => {
const circular = { a : 1 };
circular . self = circular ; // Creates circular reference
assert . throws (
() => utils . output ( circular ),
{ name : 'TypeError' },
'JSON.stringify of circular object should throw TypeError (no try/catch in output())'
);
})) passed ++ ; else failed ++ ;
2026-02-13 16:08:47 -08:00
// ── 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 ++ ;
2026-02-13 16:59:56 -08:00
// ── 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 ++ ;
2026-02-13 17:11:32 -08:00
// ── Round 107: grepFile with ^$ pattern — empty line matching after split ──
console . log ( '\nRound 107: grepFile (empty line matching — ^$ on split lines, trailing \\n creates extra empty element):' );
if ( test ( 'grepFile matches empty lines with ^$ pattern including trailing newline phantom line' , () => {
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r107-grep-empty-' ));
const testFile = path . join ( tmpDir , 'test.txt' );
try {
// 'line1\n\nline3\n\n'.split('\n') → ['line1','','line3','',''] (5 elements, 3 empty)
fs . writeFileSync ( testFile , 'line1\n\nline3\n\n' );
const results = utils . grepFile ( testFile , /^$/ );
assert . strictEqual ( results . length , 3 ,
'Should match 3 empty lines: line 2, line 4, and trailing phantom line 5' );
assert . strictEqual ( results [ 0 ]. lineNumber , 2 , 'First empty line at position 2' );
assert . strictEqual ( results [ 1 ]. lineNumber , 4 , 'Second empty line at position 4' );
assert . strictEqual ( results [ 2 ]. lineNumber , 5 , 'Third empty line is the trailing phantom from split' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
// ── Round 107: replaceInFile where replacement re-introduces search pattern (single-pass) ──
console . log ( '\nRound 107: replaceInFile (replacement contains search pattern — String.replace is single-pass):' );
if ( test ( 'replaceInFile does not re-scan replacement text (single-pass, no infinite loop)' , () => {
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r107-replace-reintr-' ));
const testFile = path . join ( tmpDir , 'test.txt' );
try {
fs . writeFileSync ( testFile , 'foo bar baz' );
// Replace "foo" with "foo extra foo" — should only replace the first occurrence
const result = utils . replaceInFile ( testFile , 'foo' , 'foo extra foo' );
assert . strictEqual ( result , true , 'replaceInFile should return true' );
const content = utils . readFile ( testFile );
assert . strictEqual ( content , 'foo extra foo bar baz' ,
'Only the original "foo" is replaced — replacement text is not re-scanned' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 17:07:13 -08:00
// ── Round 106: countInFile with named capture groups — match(g) ignores group details ──
console . log ( '\nRound 106: countInFile (named capture groups — String.match(g) returns full matches only):' );
if ( test ( 'countInFile with named capture groups counts matches not groups' , () => {
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r106-count-named-' ));
const testFile = path . join ( tmpDir , 'test.txt' );
try {
fs . writeFileSync ( testFile , 'foo bar baz\nfoo qux\nbar foo end' );
// Named capture group — should still count 3 matches for "foo"
const count = utils . countInFile ( testFile , /(?<word>foo)/ );
assert . strictEqual ( count , 3 ,
'Named capture group should not inflate count — match(g) returns full matches only' );
// Compare with plain pattern
const plainCount = utils . countInFile ( testFile , /foo/ );
assert . strictEqual ( plainCount , 3 , 'Plain regex should also find 3 matches' );
assert . strictEqual ( count , plainCount ,
'Named group pattern and plain pattern should return identical counts' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
// ── Round 106: grepFile with multiline (m) flag — preserved, unlike g which is stripped ──
console . log ( '\nRound 106: grepFile (multiline m flag — preserved in regex, unlike g which is stripped):' );
if ( test ( 'grepFile preserves multiline (m) flag and anchors work on split lines' , () => {
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r106-grep-multiline-' ));
const testFile = path . join ( tmpDir , 'test.txt' );
try {
fs . writeFileSync ( testFile , 'hello\nworld hello\nhello world' );
// With m flag + anchors: ^hello$ should match only exact "hello" line
const mResults = utils . grepFile ( testFile , /^hello$/m );
assert . strictEqual ( mResults . length , 1 ,
'With m flag, ^hello$ should match only line 1 (exact "hello")' );
assert . strictEqual ( mResults [ 0 ]. lineNumber , 1 );
// Without m flag: same behavior since grepFile splits lines individually
const noMResults = utils . grepFile ( testFile , /^hello$/ );
assert . strictEqual ( noMResults . length , 1 ,
'Without m flag, same result — grepFile splits lines so anchors are per-line already' );
assert . strictEqual ( mResults . length , noMResults . length ,
'm flag is preserved but irrelevant — line splitting makes anchors per-line already' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 17:24:36 -08:00
// ── Round 109: appendFile creating new file in non-existent directory (ensureDir + appendFileSync) ──
console . log ( '\nRound 109: appendFile (new file creation — ensureDir creates parent, appendFileSync creates file):' );
if ( test ( 'appendFile creates parent directory and new file when neither exist' , () => {
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r109-append-new-' ));
const nestedPath = path . join ( tmpDir , 'deep' , 'nested' , 'dir' , 'newfile.txt' );
try {
// Parent directory 'deep/nested/dir' does not exist yet
assert . ok ( ! fs . existsSync ( path . join ( tmpDir , 'deep' )),
'Parent "deep" should not exist before appendFile' );
utils . appendFile ( nestedPath , 'first line\n' );
assert . ok ( fs . existsSync ( nestedPath ),
'File should be created by appendFile' );
assert . strictEqual ( utils . readFile ( nestedPath ), 'first line\n' ,
'Content should match what was appended' );
// Append again to verify it adds to existing file
utils . appendFile ( nestedPath , 'second line\n' );
assert . strictEqual ( utils . readFile ( nestedPath ), 'first line\nsecond line\n' ,
'Second append should add to existing file' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 17:18:06 -08:00
// ── Round 108: grepFile with Unicode/emoji content — UTF-16 string matching on split lines ──
console . log ( '\nRound 108: grepFile (Unicode/emoji — regex matching on UTF-16 split lines):' );
if ( test ( 'grepFile finds Unicode emoji patterns across lines' , () => {
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r108-grep-unicode-' ));
const testFile = path . join ( tmpDir , 'test.txt' );
try {
fs . writeFileSync ( testFile , '🎉 celebration\nnormal line\n🎉 party\n日本語テスト' );
const emojiResults = utils . grepFile ( testFile , /🎉/ );
assert . strictEqual ( emojiResults . length , 2 ,
'Should find emoji on 2 lines (lines 1 and 3)' );
assert . strictEqual ( emojiResults [ 0 ]. lineNumber , 1 );
assert . strictEqual ( emojiResults [ 1 ]. lineNumber , 3 );
const cjkResults = utils . grepFile ( testFile , /日本語/ );
assert . strictEqual ( cjkResults . length , 1 ,
'Should find CJK characters on line 4' );
assert . strictEqual ( cjkResults [ 0 ]. lineNumber , 4 );
assert . ok ( cjkResults [ 0 ]. content . includes ( '日本語テスト' ),
'Matched line should contain full CJK text' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 17:30:38 -08:00
// ── Round 110: findFiles root directory unreadable — silent empty return (not throw) ──
console . log ( '\nRound 110: findFiles (root directory unreadable — EACCES on readdirSync caught silently):' );
if ( test ( 'findFiles returns empty array when root directory exists but is unreadable' , () => {
2026-02-18 15:10:32 +00:00
if ( process . platform === 'win32' || process . getuid ? .() === 0 ) {
console . log ( ' (skipped — chmod ineffective on Windows/root)' );
return true ;
}
2026-02-13 17:30:38 -08:00
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r110-unreadable-root-' ));
const unreadableDir = path . join ( tmpDir , 'no-read' );
fs . mkdirSync ( unreadableDir );
fs . writeFileSync ( path . join ( unreadableDir , 'secret.txt' ), 'hidden' );
try {
fs . chmodSync ( unreadableDir , 0o000 );
// Verify dir exists but is unreadable
assert . ok ( fs . existsSync ( unreadableDir ), 'Directory should exist' );
// findFiles should NOT throw — catch block at line 188 handles EACCES
const results = utils . findFiles ( unreadableDir , '*' );
assert . ok ( Array . isArray ( results ), 'Should return an array' );
assert . strictEqual ( results . length , 0 ,
'Should return empty array when root dir is unreadable (not throw)' );
// Also test with recursive flag
const recursiveResults = utils . findFiles ( unreadableDir , '*' , { recursive : true });
assert . strictEqual ( recursiveResults . length , 0 ,
'Recursive search on unreadable root should also return empty array' );
} finally {
// Restore permissions before cleanup
2026-02-18 07:04:29 +00:00
try { fs . chmodSync ( unreadableDir , 0o755 ); } catch ( _e ) { /* ignore permission errors */ }
2026-02-13 17:30:38 -08:00
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 17:54:48 -08:00
// ── Round 113: replaceInFile with zero-width regex — inserts between every character ──
console . log ( '\nRound 113: replaceInFile (zero-width regex /(?:)/g — matches every position):' );
if ( test ( 'replaceInFile with zero-width regex /(?:)/g inserts replacement at every position' , () => {
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r113-zero-width-' ));
const testFile = path . join ( tmpDir , 'test.txt' );
try {
fs . writeFileSync ( testFile , 'abc' );
// /(?:)/g matches at every position boundary: before 'a', between 'a'-'b', etc.
// "abc".replace(/(?:)/g, 'X') → "XaXbXcX" (7 chars from 3)
const result = utils . replaceInFile ( testFile , /(?:)/g , 'X' );
assert . strictEqual ( result , true , 'Should succeed (no error)' );
const content = utils . readFile ( testFile );
assert . strictEqual ( content , 'XaXbXcX' ,
'Zero-width regex inserts at every position boundary' );
// Also test with /^/gm (start of each line)
fs . writeFileSync ( testFile , 'line1\nline2\nline3' );
utils . replaceInFile ( testFile , /^/gm , '> ' );
const prefixed = utils . readFile ( testFile );
assert . strictEqual ( prefixed , '> line1\n> line2\n> line3' ,
'/^/gm inserts at start of each line' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 18:01:25 -08:00
// ── Round 114: replaceInFile options.all is silently ignored for RegExp search ──
console . log ( '\nRound 114: replaceInFile (options.all silently ignored for RegExp search):' );
if ( test ( 'replaceInFile ignores options.all when search is a RegExp — falls through to .replace()' , () => {
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r114-all-regex-' ));
const testFile = path . join ( tmpDir , 'test.txt' );
try {
// File with repeated pattern: "foo bar foo baz foo"
fs . writeFileSync ( testFile , 'foo bar foo baz foo' );
// With options.all=true and a non-global RegExp:
// Line 411: (options.all && typeof search === 'string') → false (RegExp !== string)
// Falls through to content.replace(regex, replace) — only replaces FIRST match
const result = utils . replaceInFile ( testFile , /foo/ , 'QUX' , { all : true });
assert . strictEqual ( result , true , 'Should succeed' );
const content = utils . readFile ( testFile );
assert . strictEqual ( content , 'QUX bar foo baz foo' ,
'Non-global RegExp with options.all=true should still only replace FIRST match' );
// Contrast: global RegExp replaces all regardless of options.all
fs . writeFileSync ( testFile , 'foo bar foo baz foo' );
utils . replaceInFile ( testFile , /foo/g , 'QUX' , { all : true });
const globalContent = utils . readFile ( testFile );
assert . strictEqual ( globalContent , 'QUX bar QUX baz QUX' ,
'Global RegExp replaces all matches (options.all irrelevant for RegExp)' );
// String with options.all=true — uses replaceAll, replaces ALL occurrences
fs . writeFileSync ( testFile , 'foo bar foo baz foo' );
utils . replaceInFile ( testFile , 'foo' , 'QUX' , { all : true });
const allContent = utils . readFile ( testFile );
assert . strictEqual ( allContent , 'QUX bar QUX baz QUX' ,
'String with options.all=true uses replaceAll — replaces ALL occurrences' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
// ── Round 114: output with object containing BigInt — JSON.stringify throws ──
console . log ( '\nRound 114: output (object containing BigInt — JSON.stringify throws):' );
if ( test ( 'output throws TypeError when object contains BigInt values (JSON.stringify cannot serialize)' , () => {
// Capture original console.log to prevent actual output during test
const originalLog = console . log ;
try {
// Plain BigInt — typeof is 'bigint', not 'object', so goes to else branch
// console.log can handle BigInt directly (prints "42n")
let captured = null ;
console . log = ( val ) => { captured = val ; };
utils . output ( BigInt ( 42 ));
// Node.js console.log prints BigInt as-is
assert . strictEqual ( captured , BigInt ( 42 ), 'Plain BigInt goes to else branch, logged directly' );
// Object containing BigInt — typeof is 'object', so JSON.stringify is called
// JSON.stringify(BigInt) throws: "Do not know how to serialize a BigInt"
console . log = originalLog ; // restore before throw test
assert . throws (
() => utils . output ({ value : BigInt ( 42 ) }),
( err ) => err instanceof TypeError && /BigInt/ . test ( err . message ),
'Object with BigInt should throw TypeError from JSON.stringify'
);
// Array containing BigInt — also typeof 'object'
assert . throws (
() => utils . output ([ BigInt ( 1 ), BigInt ( 2 )]),
( err ) => err instanceof TypeError && /BigInt/ . test ( err . message ),
'Array with BigInt should also throw TypeError from JSON.stringify'
);
} finally {
console . log = originalLog ;
}
})) passed ++ ; else failed ++ ;
2026-02-13 18:05:28 -08:00
// ── Round 115: countInFile with empty string pattern — matches at every position boundary ──
console . log ( '\nRound 115: countInFile (empty string pattern — matches at every zero-width position):' );
if ( test ( 'countInFile with empty string pattern returns content.length + 1 (matches between every char)' , () => {
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r115-empty-pattern-' ));
const testFile = path . join ( tmpDir , 'test.txt' );
try {
// "hello" is 5 chars → 6 zero-width positions: |h|e|l|l|o|
fs . writeFileSync ( testFile , 'hello' );
const count = utils . countInFile ( testFile , '' );
assert . strictEqual ( count , 6 ,
'Empty string pattern creates /(?:)/g which matches at 6 position boundaries in "hello"' );
// Empty file → "" has 1 zero-width position (the empty string itself)
fs . writeFileSync ( testFile , '' );
const emptyCount = utils . countInFile ( testFile , '' );
assert . strictEqual ( emptyCount , 1 ,
'Empty file still has 1 zero-width position boundary' );
// Single char → 2 positions: |a|
fs . writeFileSync ( testFile , 'a' );
const singleCount = utils . countInFile ( testFile , '' );
assert . strictEqual ( singleCount , 2 ,
'Single character file has 2 position boundaries' );
// Newlines count as characters too
fs . writeFileSync ( testFile , 'a\nb' );
const newlineCount = utils . countInFile ( testFile , '' );
assert . strictEqual ( newlineCount , 4 ,
'"a\\nb" is 3 chars → 4 position boundaries' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 18:16:10 -08:00
// ── Round 117: grepFile with CRLF content — split('\n') leaves \r, anchored patterns fail ──
console . log ( '\nRound 117: grepFile (CRLF content — trailing \\r breaks anchored regex patterns):' );
if ( test ( 'grepFile with CRLF content: unanchored patterns work but anchored $ fails due to trailing \\r' , () => {
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r117-grep-crlf-' ));
const testFile = path . join ( tmpDir , 'test.txt' );
try {
// Write CRLF content
fs . writeFileSync ( testFile , 'hello\r\nworld\r\nfoo bar\r\n' );
// Unanchored pattern works — 'hello' matches in 'hello\r'
const unanchored = utils . grepFile ( testFile , 'hello' );
assert . strictEqual ( unanchored . length , 1 , 'Unanchored pattern should find 1 match' );
assert . strictEqual ( unanchored [ 0 ]. lineNumber , 1 , 'Should be on line 1' );
assert . ok ( unanchored [ 0 ]. content . endsWith ( '\r' ),
'Line content should have trailing \\r from split("\\n") on CRLF' );
// Anchored pattern /^hello$/ does NOT match 'hello\r' because $ is before \r
const anchored = utils . grepFile ( testFile , /^hello$/ );
assert . strictEqual ( anchored . length , 0 ,
'Anchored /^hello$/ should NOT match "hello\\r" — $ fails before \\r' );
// But /^hello\r?$/ or /^hello/ work
const withOptCr = utils . grepFile ( testFile , /^hello\r?$/ );
assert . strictEqual ( withOptCr . length , 1 ,
'/^hello\\r?$/ matches "hello\\r" because \\r? consumes the trailing CR' );
// Contrast: LF-only content works with anchored patterns
fs . writeFileSync ( testFile , 'hello\nworld\nfoo bar\n' );
const lfAnchored = utils . grepFile ( testFile , /^hello$/ );
assert . strictEqual ( lfAnchored . length , 1 ,
'LF-only content: anchored /^hello$/ matches normally' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 18:11:58 -08:00
// ── Round 116: replaceInFile with null/undefined replacement — JS coerces to string ──
console . log ( '\nRound 116: replaceInFile (null/undefined replacement — JS coerces to string "null"/"undefined"):' );
if ( test ( 'replaceInFile with null replacement coerces to string "null" via String.replace ToString' , () => {
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r116-null-replace-' ));
const testFile = path . join ( tmpDir , 'test.txt' );
try {
// null replacement → String.replace coerces null to "null"
fs . writeFileSync ( testFile , 'hello world' );
const result = utils . replaceInFile ( testFile , 'world' , null );
assert . strictEqual ( result , true , 'Should succeed' );
const content = utils . readFile ( testFile );
assert . strictEqual ( content , 'hello null' ,
'null replacement is coerced to string "null" by String.replace' );
// undefined replacement → coerced to "undefined"
fs . writeFileSync ( testFile , 'hello world' );
utils . replaceInFile ( testFile , 'world' , undefined );
const undefinedContent = utils . readFile ( testFile );
assert . strictEqual ( undefinedContent , 'hello undefined' ,
'undefined replacement is coerced to string "undefined" by String.replace' );
// Contrast: empty string replacement works as expected
fs . writeFileSync ( testFile , 'hello world' );
utils . replaceInFile ( testFile , 'world' , '' );
const emptyContent = utils . readFile ( testFile );
assert . strictEqual ( emptyContent , 'hello ' ,
'Empty string replacement correctly removes matched text' );
// options.all with null replacement
fs . writeFileSync ( testFile , 'foo bar foo baz foo' );
utils . replaceInFile ( testFile , 'foo' , null , { all : true });
const allContent = utils . readFile ( testFile );
assert . strictEqual ( allContent , 'null bar null baz null' ,
'replaceAll also coerces null to "null" for every occurrence' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
// ── Round 116: ensureDir with null path — throws wrapped TypeError ──
console . log ( '\nRound 116: ensureDir (null path — fs.existsSync(null) throws TypeError):' );
if ( test ( 'ensureDir with null path throws wrapped Error from TypeError (ERR_INVALID_ARG_TYPE)' , () => {
// fs.existsSync(null) throws TypeError in modern Node.js
// Caught by ensureDir catch block, err.code !== 'EEXIST' → re-thrown as wrapped Error
assert . throws (
() => utils . ensureDir ( null ),
( err ) => {
// Should be a wrapped Error (not raw TypeError)
assert . ok ( err instanceof Error , 'Should throw an Error' );
assert . ok ( err . message . includes ( 'Failed to create directory' ),
'Error message should include "Failed to create directory"' );
return true ;
},
'ensureDir(null) should throw wrapped Error'
);
// undefined path — same behavior
assert . throws (
() => utils . ensureDir ( undefined ),
( err ) => err instanceof Error && err . message . includes ( 'Failed to create directory' ),
'ensureDir(undefined) should also throw wrapped Error'
);
})) passed ++ ; else failed ++ ;
2026-02-13 18:19:21 -08:00
// ── Round 118: writeFile with non-string content — TypeError propagates (no try/catch) ──
console . log ( '\nRound 118: writeFile (non-string content — TypeError propagates uncaught):' );
if ( test ( 'writeFile with null/number content throws TypeError because fs.writeFileSync rejects non-string data' , () => {
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r118-writefile-type-' ));
const testFile = path . join ( tmpDir , 'test.txt' );
try {
// null content → TypeError from fs.writeFileSync (data must be string/Buffer/etc.)
assert . throws (
() => utils . writeFile ( testFile , null ),
( err ) => err instanceof TypeError ,
'writeFile(path, null) should throw TypeError (no try/catch in writeFile)'
);
// undefined content → TypeError
assert . throws (
() => utils . writeFile ( testFile , undefined ),
( err ) => err instanceof TypeError ,
'writeFile(path, undefined) should throw TypeError'
);
// number content → TypeError (numbers not valid for fs.writeFileSync)
assert . throws (
() => utils . writeFile ( testFile , 42 ),
( err ) => err instanceof TypeError ,
'writeFile(path, 42) should throw TypeError (number not a valid data type)'
);
// Contrast: string content works fine
utils . writeFile ( testFile , 'valid string content' );
assert . strictEqual ( utils . readFile ( testFile ), 'valid string content' ,
'String content should write and read back correctly' );
// Empty string is valid
utils . writeFile ( testFile , '' );
assert . strictEqual ( utils . readFile ( testFile ), '' ,
'Empty string should write correctly' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 18:21:39 -08:00
// ── Round 119: appendFile with non-string content — TypeError propagates (no try/catch) ──
console . log ( '\nRound 119: appendFile (non-string content — TypeError propagates like writeFile):' );
if ( test ( 'appendFile with null/number content throws TypeError (no try/catch wrapper)' , () => {
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r119-appendfile-type-' ));
const testFile = path . join ( tmpDir , 'test.txt' );
try {
// Create file with initial content
fs . writeFileSync ( testFile , 'initial' );
// null content → TypeError from fs.appendFileSync
assert . throws (
() => utils . appendFile ( testFile , null ),
( err ) => err instanceof TypeError ,
'appendFile(path, null) should throw TypeError'
);
// undefined content → TypeError
assert . throws (
() => utils . appendFile ( testFile , undefined ),
( err ) => err instanceof TypeError ,
'appendFile(path, undefined) should throw TypeError'
);
// number content → TypeError
assert . throws (
() => utils . appendFile ( testFile , 42 ),
( err ) => err instanceof TypeError ,
'appendFile(path, 42) should throw TypeError'
);
// Verify original content is unchanged after failed appends
assert . strictEqual ( utils . readFile ( testFile ), 'initial' ,
'File content should be unchanged after failed appends' );
// Contrast: string append works
utils . appendFile ( testFile , ' appended' );
assert . strictEqual ( utils . readFile ( testFile ), 'initial appended' ,
'String append should work correctly' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 18:23:55 -08:00
// ── Round 120: replaceInFile with empty string search — prepend vs insert-between-every-char ──
console . log ( '\nRound 120: replaceInFile (empty string search — replace vs replaceAll dramatic difference):' );
if ( test ( 'replaceInFile with empty search: replace prepends at pos 0; replaceAll inserts between every char' , () => {
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r120-empty-search-' ));
const testFile = path . join ( tmpDir , 'test.txt' );
try {
// Without options.all: .replace('', 'X') prepends at position 0
fs . writeFileSync ( testFile , 'hello' );
utils . replaceInFile ( testFile , '' , 'X' );
const prepended = utils . readFile ( testFile );
assert . strictEqual ( prepended , 'Xhello' ,
'replace("", "X") should prepend X at position 0 only' );
// With options.all: .replaceAll('', 'X') inserts between every character
fs . writeFileSync ( testFile , 'hello' );
utils . replaceInFile ( testFile , '' , 'X' , { all : true });
const insertedAll = utils . readFile ( testFile );
assert . strictEqual ( insertedAll , 'XhXeXlXlXoX' ,
'replaceAll("", "X") inserts X at every position boundary' );
// Empty file + empty search
fs . writeFileSync ( testFile , '' );
utils . replaceInFile ( testFile , '' , 'X' );
const emptyReplace = utils . readFile ( testFile );
assert . strictEqual ( emptyReplace , 'X' ,
'Empty content + empty search: single insertion at position 0' );
// Empty file + empty search + all
fs . writeFileSync ( testFile , '' );
utils . replaceInFile ( testFile , '' , 'X' , { all : true });
const emptyAll = utils . readFile ( testFile );
assert . strictEqual ( emptyAll , 'X' ,
'Empty content + replaceAll("", "X"): single position boundary → "X"' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 18:25:56 -08:00
// ── Round 121: findFiles with ? glob pattern — single character wildcard ──
console . log ( '\nRound 121: findFiles (? glob pattern — converted to . regex for single char match):' );
if ( test ( 'findFiles with ? glob matches single character only — test?.txt matches test1 but not test12' , () => {
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r121-glob-question-' ));
try {
// Create test files
fs . writeFileSync ( path . join ( tmpDir , 'test1.txt' ), 'a' );
fs . writeFileSync ( path . join ( tmpDir , 'testA.txt' ), 'b' );
fs . writeFileSync ( path . join ( tmpDir , 'test12.txt' ), 'c' );
fs . writeFileSync ( path . join ( tmpDir , 'test.txt' ), 'd' );
// ? matches exactly one character
const results = utils . findFiles ( tmpDir , 'test?.txt' );
const names = results . map ( r => path . basename ( r . path )). sort ();
assert . ok ( names . includes ( 'test1.txt' ), 'Should match test1.txt (? = single digit)' );
assert . ok ( names . includes ( 'testA.txt' ), 'Should match testA.txt (? = single letter)' );
assert . ok ( ! names . includes ( 'test12.txt' ), 'Should NOT match test12.txt (12 is two chars)' );
assert . ok ( ! names . includes ( 'test.txt' ), 'Should NOT match test.txt (no char for ?)' );
// Multiple ? marks
fs . writeFileSync ( path . join ( tmpDir , 'ab.txt' ), 'e' );
fs . writeFileSync ( path . join ( tmpDir , 'abc.txt' ), 'f' );
const multiResults = utils . findFiles ( tmpDir , '??.txt' );
const multiNames = multiResults . map ( r => path . basename ( r . path ));
assert . ok ( multiNames . includes ( 'ab.txt' ), '?? should match 2-char filename' );
assert . ok ( ! multiNames . includes ( 'abc.txt' ), '?? should NOT match 3-char filename' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 18:30:42 -08:00
// ── Round 122: findFiles dot extension escaping — *.txt must not match filetxt ──
console . log ( '\nRound 122: findFiles (dot escaping — *.txt matches file.txt but not filetxt):' );
if ( test ( 'findFiles escapes dots in glob pattern so *.txt only matches literal .txt extension' , () => {
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r122-dot-escape-' ));
try {
fs . writeFileSync ( path . join ( tmpDir , 'file.txt' ), 'a' );
fs . writeFileSync ( path . join ( tmpDir , 'filetxt' ), 'b' );
fs . writeFileSync ( path . join ( tmpDir , 'file.txtx' ), 'c' );
fs . writeFileSync ( path . join ( tmpDir , 'notes.txt' ), 'd' );
const results = utils . findFiles ( tmpDir , '*.txt' );
const names = results . map ( r => path . basename ( r . path )). sort ();
assert . ok ( names . includes ( 'file.txt' ), 'Should match file.txt' );
assert . ok ( names . includes ( 'notes.txt' ), 'Should match notes.txt' );
assert . ok ( ! names . includes ( 'filetxt' ),
'Should NOT match filetxt (dot is escaped to literal, not wildcard)' );
assert . ok ( ! names . includes ( 'file.txtx' ),
'Should NOT match file.txtx ($ anchor requires exact end)' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 18:36:09 -08:00
// ── Round 123: countInFile with overlapping patterns — match(g) is non-overlapping ──
console . log ( '\nRound 123: countInFile (overlapping patterns — String.match(/g/) is non-overlapping):' );
if ( test ( 'countInFile counts non-overlapping matches only — "aaa" with /aa/g returns 1 not 2' , () => {
// utils.js line 449: `content.match(regex)` with 'g' flag returns an array of
// non-overlapping matches. After matching "aa" starting at index 0, the engine
// advances to index 2, where only one "a" remains — no second match.
// This is standard JS regex behavior but can surprise users expecting overlap.
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r123-overlap-' ));
const testFile = path . join ( tmpDir , 'test.txt' );
try {
// "aaa" — a human might count 2 occurrences of "aa" (at 0,1) but match(g) finds 1
fs . writeFileSync ( testFile , 'aaa' );
const count1 = utils . countInFile ( testFile , 'aa' );
assert . strictEqual ( count1 , 1 ,
'"aaa".match(/aa/g) returns ["aa"] — only 1 non-overlapping match' );
// "aaaa" — 2 non-overlapping matches (at 0,2), not 3 overlapping (at 0,1,2)
fs . writeFileSync ( testFile , 'aaaa' );
const count2 = utils . countInFile ( testFile , 'aa' );
assert . strictEqual ( count2 , 2 ,
'"aaaa".match(/aa/g) returns ["aa","aa"] — 2 non-overlapping, not 3 overlapping' );
// "abab" with /aba/g — only 1 match (at 0), not 2 (overlapping at 0,2)
fs . writeFileSync ( testFile , 'ababab' );
const count3 = utils . countInFile ( testFile , 'aba' );
assert . strictEqual ( count3 , 1 ,
'"ababab".match(/aba/g) returns 1 — after match at 0, next try starts at 3' );
// RegExp object behaves the same
fs . writeFileSync ( testFile , 'aaa' );
const count4 = utils . countInFile ( testFile , /aa/ );
assert . strictEqual ( count4 , 1 ,
'RegExp /aa/ also gives 1 non-overlapping match on "aaa" (g flag auto-added)' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
// ── Round 123: replaceInFile with $& and $$ substitution tokens in replacement string ──
console . log ( '\nRound 123: replaceInFile ($& and $$ substitution tokens in replacement):' );
if ( test ( 'replaceInFile replacement string interprets $& as matched text and $$ as literal $' , () => {
// JS String.replace() interprets special patterns in the replacement string:
// $& → inserts the entire matched substring
// $$ → inserts a literal "$" character
// $' → inserts the portion after the matched substring
// $` → inserts the portion before the matched substring
// This is different from capture groups ($1, $2) already tested in Round 91.
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r123-dollar-' ));
const testFile = path . join ( tmpDir , 'test.txt' );
try {
// $& — inserts the matched text itself
fs . writeFileSync ( testFile , 'hello world' );
utils . replaceInFile ( testFile , 'world' , '[$&]' );
assert . strictEqual ( utils . readFile ( testFile ), 'hello [world]' ,
'$& in replacement inserts the matched text "world" → "[world]"' );
// $$ — inserts a literal $ sign
fs . writeFileSync ( testFile , 'price is 100' );
utils . replaceInFile ( testFile , '100' , '$$100' );
assert . strictEqual ( utils . readFile ( testFile ), 'price is $100' ,
'$$ becomes literal $ → "100" replaced with "$100"' );
// $& with options.all — applies to each match
fs . writeFileSync ( testFile , 'foo bar foo' );
utils . replaceInFile ( testFile , 'foo' , '($&)' , { all : true });
assert . strictEqual ( utils . readFile ( testFile ), '(foo) bar (foo)' ,
'$& in replaceAll inserts each respective matched text' );
// Combined $$ and $& in same replacement (3 $ + &)
fs . writeFileSync ( testFile , 'item costs 50' );
utils . replaceInFile ( testFile , '50' , '$$$&' );
// In replacement string: $$ → "$" then $& → "50" so result is "$50"
assert . strictEqual ( utils . readFile ( testFile ), 'item costs $50' ,
'$$$& (3 dollars + ampersand) means literal $ followed by matched text → "$50"' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 18:40:07 -08:00
// ── Round 124: findFiles matches dotfiles (unlike shell glob where * excludes hidden files) ──
console . log ( '\nRound 124: findFiles (* glob matches dotfiles — unlike shell globbing):' );
if ( test ( 'findFiles with * pattern matches dotfiles because .* regex includes hidden files' , () => {
// In shell: `ls *` excludes .hidden files. In findFiles, `*` → `.*` regex which
// matches ANY filename including those starting with `.`. This is a behavioral
// difference from shell globbing that could surprise users.
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r124-dotfiles-' ));
try {
// Create normal and hidden files
fs . writeFileSync ( path . join ( tmpDir , 'normal.txt' ), 'visible' );
fs . writeFileSync ( path . join ( tmpDir , '.hidden' ), 'hidden' );
fs . writeFileSync ( path . join ( tmpDir , '.gitignore' ), 'ignore' );
fs . writeFileSync ( path . join ( tmpDir , 'README.md' ), 'readme' );
// * matches ALL files including dotfiles
const allResults = utils . findFiles ( tmpDir , '*' );
const names = allResults . map ( r => path . basename ( r . path )). sort ();
assert . ok ( names . includes ( '.hidden' ),
'* should match .hidden (unlike shell glob)' );
assert . ok ( names . includes ( '.gitignore' ),
'* should match .gitignore' );
assert . ok ( names . includes ( 'normal.txt' ),
'* should match normal.txt' );
assert . strictEqual ( names . length , 4 ,
'Should find all 4 files including 2 dotfiles' );
// *.txt does NOT match dotfiles (because they don't end with .txt)
const txtResults = utils . findFiles ( tmpDir , '*.txt' );
assert . strictEqual ( txtResults . length , 1 ,
'*.txt should only match normal.txt, not dotfiles' );
// .* pattern specifically matches only dotfiles
const dotResults = utils . findFiles ( tmpDir , '.*' );
const dotNames = dotResults . map ( r => path . basename ( r . path )). sort ();
assert . ok ( dotNames . includes ( '.hidden' ), '.* matches .hidden' );
assert . ok ( dotNames . includes ( '.gitignore' ), '.* matches .gitignore' );
assert . ok ( ! dotNames . includes ( 'normal.txt' ),
'.* should NOT match normal.txt (needs leading dot)' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
2026-02-13 18:44:07 -08:00
// ── Round 125: readFile with binary content — returns garbled UTF-8, not null ──
console . log ( '\nRound 125: readFile (binary/non-UTF8 content — garbled, not null):' );
if ( test ( 'readFile with binary content returns garbled string (not null) because UTF-8 decode does not throw' , () => {
// utils.js line 285: fs.readFileSync(filePath, 'utf8') — binary data gets UTF-8 decoded.
// Invalid byte sequences become U+FFFD replacement characters. The function does
// NOT return null for binary files (only returns null on ENOENT/permission errors).
// This means grepFile/countInFile would operate on corrupted content silently.
const tmpDir = fs . mkdtempSync ( path . join ( utils . getTempDir (), 'r125-binary-' ));
const testFile = path . join ( tmpDir , 'binary.dat' );
try {
// Write raw binary data (invalid UTF-8 sequences)
const binaryData = Buffer . from ([ 0x00 , 0x80 , 0xFF , 0xFE , 0x48 , 0x65 , 0x6C , 0x6C , 0x6F ]);
fs . writeFileSync ( testFile , binaryData );
const content = utils . readFile ( testFile );
assert . ok ( content !== null ,
'readFile should NOT return null for binary files' );
assert . ok ( typeof content === 'string' ,
'readFile always returns a string (or null for missing files)' );
// The string contains "Hello" (bytes 0x48-0x6F) somewhere in the garbled output
assert . ok ( content . includes ( 'Hello' ),
'ASCII subset of binary data should survive UTF-8 decode' );
// Content length may differ from byte length due to multi-byte replacement chars
assert . ok ( content . length > 0 , 'Non-empty content from binary file' );
// grepFile on binary file — still works but on garbled content
const matches = utils . grepFile ( testFile , 'Hello' );
assert . strictEqual ( matches . length , 1 ,
'grepFile finds "Hello" even in binary file (ASCII bytes survive)' );
// Non-existent file — returns null (contrast with binary)
const missing = utils . readFile ( path . join ( tmpDir , 'no-such-file.txt' ));
assert . strictEqual ( missing , null ,
'Missing file returns null (not garbled content)' );
} finally {
fs . rmSync ( tmpDir , { recursive : true , force : true });
}
})) passed ++ ; else failed ++ ;
// ── Round 125: output() with undefined, NaN, Infinity — non-object primitives logged directly ──
console . log ( '\nRound 125: output() (undefined/NaN/Infinity — typeof checks and JSON.stringify):' );
if ( test ( 'output() handles undefined, NaN, Infinity as non-objects — logs directly' , () => {
// utils.js line 273: `if (typeof data === 'object')` — undefined/NaN/Infinity are NOT objects.
// typeof undefined → "undefined", typeof NaN → "number", typeof Infinity → "number"
// All three bypass JSON.stringify and go to console.log(data) directly.
const origLog = console . log ;
const logged = [];
console . log = (... args ) => logged . push ( args );
try {
// undefined — typeof "undefined", logged directly
utils . output ( undefined );
assert . strictEqual ( logged [ 0 ][ 0 ], undefined ,
'output(undefined) logs undefined (not "undefined" string)' );
// NaN — typeof "number", logged directly
utils . output ( NaN );
assert . ok ( Number . isNaN ( logged [ 1 ][ 0 ]),
'output(NaN) logs NaN directly (typeof "number", not "object")' );
// Infinity — typeof "number", logged directly
utils . output ( Infinity );
assert . strictEqual ( logged [ 2 ][ 0 ], Infinity ,
'output(Infinity) logs Infinity directly' );
// Object containing NaN — JSON.stringify converts NaN to null
utils . output ({ value : NaN , count : Infinity });
const parsed = JSON . parse ( logged [ 3 ][ 0 ]);
assert . strictEqual ( parsed . value , null ,
'JSON.stringify converts NaN to null inside objects' );
assert . strictEqual ( parsed . count , null ,
'JSON.stringify converts Infinity to null inside objects' );
} finally {
console . log = origLog ;
}
})) passed ++ ; else failed ++ ;
2026-03-20 01:38:11 -07:00
// ─── stripAnsi ───
console . log ( '\nstripAnsi:' );
if ( test ( 'strips SGR color codes (\\x1b[...m)' , () => {
assert . strictEqual ( utils . stripAnsi ( '\x1b[31mRed text\x1b[0m' ), 'Red text' );
assert . strictEqual ( utils . stripAnsi ( '\x1b[1;36mBold cyan\x1b[0m' ), 'Bold cyan' );
})) passed ++ ; else failed ++ ;
if ( test ( 'strips cursor movement sequences (\\x1b[H, \\x1b[2J, \\x1b[3J)' , () => {
// These are the exact sequences reported in issue #642
assert . strictEqual ( utils . stripAnsi ( '\x1b[H\x1b[2J\x1b[3JHello' ), 'Hello' );
assert . strictEqual ( utils . stripAnsi ( 'before\x1b[Hafter' ), 'beforeafter' );
})) passed ++ ; else failed ++ ;
if ( test ( 'strips cursor position sequences (\\x1b[row;colH)' , () => {
assert . strictEqual ( utils . stripAnsi ( '\x1b[5;10Hplaced' ), 'placed' );
})) passed ++ ; else failed ++ ;
if ( test ( 'strips erase line sequences (\\x1b[K, \\x1b[2K)' , () => {
assert . strictEqual ( utils . stripAnsi ( 'line\x1b[Kend' ), 'lineend' );
assert . strictEqual ( utils . stripAnsi ( 'line\x1b[2Kend' ), 'lineend' );
})) passed ++ ; else failed ++ ;
if ( test ( 'strips OSC sequences (window title, hyperlinks)' , () => {
// OSC terminated by BEL (\x07)
assert . strictEqual ( utils . stripAnsi ( '\x1b]0;My Title\x07content' ), 'content' );
// OSC terminated by ST (\x1b\\)
assert . strictEqual ( utils . stripAnsi ( '\x1b]8;;https://example.com\x1b\\link\x1b]8;;\x1b\\' ), 'link' );
})) passed ++ ; else failed ++ ;
if ( test ( 'strips charset selection (\\x1b(B)' , () => {
assert . strictEqual ( utils . stripAnsi ( '\x1b(Bnormal' ), 'normal' );
})) passed ++ ; else failed ++ ;
if ( test ( 'strips bare ESC + letter (\\x1bM reverse index)' , () => {
assert . strictEqual ( utils . stripAnsi ( 'line\x1bMup' ), 'lineup' );
})) passed ++ ; else failed ++ ;
if ( test ( 'handles mixed ANSI sequences in one string' , () => {
const input = '\x1b[H\x1b[2J\x1b[1;36mSession\x1b[0m summary\x1b[K' ;
assert . strictEqual ( utils . stripAnsi ( input ), 'Session summary' );
})) passed ++ ; else failed ++ ;
if ( test ( 'returns empty string for non-string input' , () => {
assert . strictEqual ( utils . stripAnsi ( null ), '' );
assert . strictEqual ( utils . stripAnsi ( undefined ), '' );
assert . strictEqual ( utils . stripAnsi ( 42 ), '' );
})) passed ++ ; else failed ++ ;
if ( test ( 'preserves string with no ANSI codes' , () => {
assert . strictEqual ( utils . stripAnsi ( 'plain text' ), 'plain text' );
assert . strictEqual ( utils . stripAnsi ( '' ), '' );
})) passed ++ ; else failed ++ ;
if ( test ( 'handles CSI with question mark parameter (DEC private modes)' , () => {
// e.g. \x1b[?25h (show cursor), \x1b[?25l (hide cursor)
assert . strictEqual ( utils . stripAnsi ( '\x1b[?25hvisible\x1b[?25l' ), 'visible' );
})) passed ++ ; else failed ++ ;
2026-01-23 15:08:07 +08:00
// Summary
console . log ( '\n=== Test Results ===' );
console . log ( `Passed: ${ passed } ` );
console . log ( `Failed: ${ failed } ` );
console . log ( `Total: ${ passed + failed } \n` );
process . exit ( failed > 0 ? 1 : 0 );
}
runTests ();