2025-07-31 05:38:20 +09:00
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
2025-08-18 14:09:02 -07:00
import { describe , it , expect , beforeEach , afterEach , vi } from 'vitest' ;
2025-08-25 22:11:27 +02:00
import * as fs from 'node:fs' ;
import * as os from 'node:os' ;
import * as path from 'node:path' ;
2025-07-31 05:38:20 +09:00
import { WorkspaceContext } from './workspaceContext.js' ;
2025-11-20 10:44:02 -08:00
import { debugLogger } from './debugLogger.js' ;
2025-07-31 05:38:20 +09:00
2025-08-14 15:59:37 -07:00
describe ( 'WorkspaceContext with real filesystem' , ( ) = > {
let tempDir : string ;
let cwd : string ;
let otherDir : string ;
2025-07-31 05:38:20 +09:00
beforeEach ( ( ) = > {
2025-08-14 15:59:37 -07:00
// os.tmpdir() can return a path using a symlink (this is standard on macOS)
// Use fs.realpathSync to fully resolve the absolute path.
tempDir = fs . realpathSync (
fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'workspace-context-test-' ) ) ,
) ;
2025-07-31 05:38:20 +09:00
2025-08-14 15:59:37 -07:00
cwd = path . join ( tempDir , 'project' ) ;
otherDir = path . join ( tempDir , 'other-project' ) ;
2025-07-31 05:38:20 +09:00
2025-08-14 15:59:37 -07:00
fs . mkdirSync ( cwd , { recursive : true } ) ;
fs . mkdirSync ( otherDir , { recursive : true } ) ;
} ) ;
afterEach ( ( ) = > {
fs . rmSync ( tempDir , { recursive : true , force : true } ) ;
2025-07-31 05:38:20 +09:00
} ) ;
describe ( 'initialization' , ( ) = > {
it ( 'should initialize with a single directory (cwd)' , ( ) = > {
2025-08-14 15:59:37 -07:00
const workspaceContext = new WorkspaceContext ( cwd ) ;
2025-07-31 05:38:20 +09:00
const directories = workspaceContext . getDirectories ( ) ;
2025-08-14 15:59:37 -07:00
expect ( directories ) . toEqual ( [ cwd ] ) ;
2025-07-31 05:38:20 +09:00
} ) ;
it ( 'should validate and resolve directories to absolute paths' , ( ) = > {
2025-08-14 15:59:37 -07:00
const workspaceContext = new WorkspaceContext ( cwd , [ otherDir ] ) ;
2025-07-31 05:38:20 +09:00
const directories = workspaceContext . getDirectories ( ) ;
2025-08-14 15:59:37 -07:00
expect ( directories ) . toEqual ( [ cwd , otherDir ] ) ;
2025-07-31 05:38:20 +09:00
} ) ;
it ( 'should handle empty initialization' , ( ) = > {
2025-08-14 15:59:37 -07:00
const workspaceContext = new WorkspaceContext ( cwd , [ ] ) ;
2025-07-31 05:38:20 +09:00
const directories = workspaceContext . getDirectories ( ) ;
expect ( directories ) . toHaveLength ( 1 ) ;
2025-08-14 15:59:37 -07:00
expect ( fs . realpathSync ( directories [ 0 ] ) ) . toBe ( cwd ) ;
2025-07-31 05:38:20 +09:00
} ) ;
} ) ;
describe ( 'adding directories' , ( ) = > {
it ( 'should add valid directories' , ( ) = > {
2025-08-14 15:59:37 -07:00
const workspaceContext = new WorkspaceContext ( cwd ) ;
workspaceContext . addDirectory ( otherDir ) ;
2025-07-31 05:38:20 +09:00
const directories = workspaceContext . getDirectories ( ) ;
2025-08-14 15:59:37 -07:00
expect ( directories ) . toEqual ( [ cwd , otherDir ] ) ;
2025-07-31 05:38:20 +09:00
} ) ;
it ( 'should resolve relative paths to absolute' , ( ) = > {
2025-08-14 15:59:37 -07:00
const workspaceContext = new WorkspaceContext ( cwd ) ;
const relativePath = path . relative ( cwd , otherDir ) ;
2025-11-06 15:03:52 -08:00
workspaceContext . addDirectory ( relativePath ) ;
2025-07-31 05:38:20 +09:00
const directories = workspaceContext . getDirectories ( ) ;
2025-08-14 15:59:37 -07:00
expect ( directories ) . toEqual ( [ cwd , otherDir ] ) ;
2025-07-31 05:38:20 +09:00
} ) ;
it ( 'should prevent duplicate directories' , ( ) = > {
2025-08-14 15:59:37 -07:00
const workspaceContext = new WorkspaceContext ( cwd ) ;
workspaceContext . addDirectory ( otherDir ) ;
workspaceContext . addDirectory ( otherDir ) ;
2025-07-31 05:38:20 +09:00
const directories = workspaceContext . getDirectories ( ) ;
2025-08-14 15:59:37 -07:00
expect ( directories ) . toHaveLength ( 2 ) ;
2025-07-31 05:38:20 +09:00
} ) ;
2025-11-05 11:53:03 -05:00
it . skipIf ( os . platform ( ) === 'win32' ) (
'should handle symbolic links correctly' ,
( ) = > {
const realDir = path . join ( tempDir , 'real' ) ;
fs . mkdirSync ( realDir , { recursive : true } ) ;
const symlinkDir = path . join ( tempDir , 'symlink-to-real' ) ;
fs . symlinkSync ( realDir , symlinkDir , 'dir' ) ;
const workspaceContext = new WorkspaceContext ( cwd ) ;
workspaceContext . addDirectory ( symlinkDir ) ;
const directories = workspaceContext . getDirectories ( ) ;
expect ( directories ) . toEqual ( [ cwd , realDir ] ) ;
} ,
) ;
2025-07-31 05:38:20 +09:00
} ) ;
describe ( 'path validation' , ( ) = > {
2025-08-14 15:59:37 -07:00
it ( 'should accept paths within workspace directories' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd , [ otherDir ] ) ;
const validPath1 = path . join ( cwd , 'src' , 'file.ts' ) ;
const validPath2 = path . join ( otherDir , 'lib' , 'module.js' ) ;
fs . mkdirSync ( path . dirname ( validPath1 ) , { recursive : true } ) ;
fs . writeFileSync ( validPath1 , 'content' ) ;
fs . mkdirSync ( path . dirname ( validPath2 ) , { recursive : true } ) ;
fs . writeFileSync ( validPath2 , 'content' ) ;
expect ( workspaceContext . isPathWithinWorkspace ( validPath1 ) ) . toBe ( true ) ;
expect ( workspaceContext . isPathWithinWorkspace ( validPath2 ) ) . toBe ( true ) ;
2025-07-31 05:38:20 +09:00
} ) ;
2025-08-14 15:59:37 -07:00
it ( 'should accept non-existent paths within workspace directories' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd , [ otherDir ] ) ;
const validPath1 = path . join ( cwd , 'src' , 'file.ts' ) ;
const validPath2 = path . join ( otherDir , 'lib' , 'module.js' ) ;
2025-07-31 05:38:20 +09:00
expect ( workspaceContext . isPathWithinWorkspace ( validPath1 ) ) . toBe ( true ) ;
expect ( workspaceContext . isPathWithinWorkspace ( validPath2 ) ) . toBe ( true ) ;
} ) ;
it ( 'should reject paths outside workspace' , ( ) = > {
2025-08-14 15:59:37 -07:00
const workspaceContext = new WorkspaceContext ( cwd , [ otherDir ] ) ;
const invalidPath = path . join ( tempDir , 'outside-workspace' , 'file.txt' ) ;
2025-07-31 05:38:20 +09:00
expect ( workspaceContext . isPathWithinWorkspace ( invalidPath ) ) . toBe ( false ) ;
} ) ;
2025-08-14 15:59:37 -07:00
it ( 'should reject non-existent paths outside workspace' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd , [ otherDir ] ) ;
const invalidPath = path . join ( tempDir , 'outside-workspace' , 'file.txt' ) ;
2025-07-31 05:38:20 +09:00
2025-08-14 15:59:37 -07:00
expect ( workspaceContext . isPathWithinWorkspace ( invalidPath ) ) . toBe ( false ) ;
2025-07-31 05:38:20 +09:00
} ) ;
it ( 'should handle nested directories correctly' , ( ) = > {
2025-08-14 15:59:37 -07:00
const workspaceContext = new WorkspaceContext ( cwd , [ otherDir ] ) ;
const nestedPath = path . join ( cwd , 'deeply' , 'nested' , 'path' , 'file.txt' ) ;
2025-07-31 05:38:20 +09:00
expect ( workspaceContext . isPathWithinWorkspace ( nestedPath ) ) . toBe ( true ) ;
} ) ;
it ( 'should handle edge cases (root, parent references)' , ( ) = > {
2025-08-14 15:59:37 -07:00
const workspaceContext = new WorkspaceContext ( cwd , [ otherDir ] ) ;
const rootPath = path . parse ( tempDir ) . root ;
const parentPath = path . dirname ( cwd ) ;
2025-07-31 05:38:20 +09:00
expect ( workspaceContext . isPathWithinWorkspace ( rootPath ) ) . toBe ( false ) ;
expect ( workspaceContext . isPathWithinWorkspace ( parentPath ) ) . toBe ( false ) ;
} ) ;
it ( 'should handle non-existent paths correctly' , ( ) = > {
2025-08-14 15:59:37 -07:00
const workspaceContext = new WorkspaceContext ( cwd , [ otherDir ] ) ;
const nonExistentPath = path . join ( cwd , 'does-not-exist.txt' ) ;
2025-07-31 05:38:20 +09:00
expect ( workspaceContext . isPathWithinWorkspace ( nonExistentPath ) ) . toBe (
true ,
) ;
} ) ;
2025-11-05 11:53:03 -05:00
describe . skipIf ( os . platform ( ) === 'win32' ) ( 'with symbolic link' , ( ) = > {
2025-08-14 15:59:37 -07:00
describe ( 'in the workspace' , ( ) = > {
let realDir : string ;
let symlinkDir : string ;
beforeEach ( ( ) = > {
realDir = path . join ( cwd , 'real-dir' ) ;
fs . mkdirSync ( realDir , { recursive : true } ) ;
2025-07-31 05:38:20 +09:00
2025-08-14 15:59:37 -07:00
symlinkDir = path . join ( cwd , 'symlink-file' ) ;
fs . symlinkSync ( realDir , symlinkDir , 'dir' ) ;
} ) ;
2025-07-31 05:38:20 +09:00
2025-08-14 15:59:37 -07:00
it ( 'should accept dir paths' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd ) ;
expect ( workspaceContext . isPathWithinWorkspace ( symlinkDir ) ) . toBe ( true ) ;
} ) ;
2025-07-31 05:38:20 +09:00
2025-08-14 15:59:37 -07:00
it ( 'should accept non-existent paths' , ( ) = > {
const filePath = path . join ( symlinkDir , 'does-not-exist.txt' ) ;
2025-07-31 05:38:20 +09:00
2025-08-14 15:59:37 -07:00
const workspaceContext = new WorkspaceContext ( cwd ) ;
expect ( workspaceContext . isPathWithinWorkspace ( filePath ) ) . toBe ( true ) ;
} ) ;
it ( 'should accept non-existent deep paths' , ( ) = > {
const filePath = path . join ( symlinkDir , 'deep' , 'does-not-exist.txt' ) ;
const workspaceContext = new WorkspaceContext ( cwd ) ;
expect ( workspaceContext . isPathWithinWorkspace ( filePath ) ) . toBe ( true ) ;
} ) ;
2025-07-31 05:38:20 +09:00
} ) ;
2025-08-14 15:59:37 -07:00
describe ( 'outside the workspace' , ( ) = > {
let realDir : string ;
let symlinkDir : string ;
beforeEach ( ( ) = > {
realDir = path . join ( tempDir , 'real-dir' ) ;
fs . mkdirSync ( realDir , { recursive : true } ) ;
symlinkDir = path . join ( cwd , 'symlink-file' ) ;
fs . symlinkSync ( realDir , symlinkDir , 'dir' ) ;
} ) ;
it ( 'should reject dir paths' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd ) ;
expect ( workspaceContext . isPathWithinWorkspace ( symlinkDir ) ) . toBe (
false ,
) ;
} ) ;
it ( 'should reject non-existent paths' , ( ) = > {
const filePath = path . join ( symlinkDir , 'does-not-exist.txt' ) ;
const workspaceContext = new WorkspaceContext ( cwd ) ;
expect ( workspaceContext . isPathWithinWorkspace ( filePath ) ) . toBe ( false ) ;
} ) ;
it ( 'should reject non-existent deep paths' , ( ) = > {
const filePath = path . join ( symlinkDir , 'deep' , 'does-not-exist.txt' ) ;
const workspaceContext = new WorkspaceContext ( cwd ) ;
expect ( workspaceContext . isPathWithinWorkspace ( filePath ) ) . toBe ( false ) ;
} ) ;
2025-07-31 05:38:20 +09:00
2025-08-14 15:59:37 -07:00
it ( 'should reject partially non-existent deep paths' , ( ) = > {
const deepDir = path . join ( symlinkDir , 'deep' ) ;
fs . mkdirSync ( deepDir , { recursive : true } ) ;
const filePath = path . join ( deepDir , 'does-not-exist.txt' ) ;
const workspaceContext = new WorkspaceContext ( cwd ) ;
expect ( workspaceContext . isPathWithinWorkspace ( filePath ) ) . toBe ( false ) ;
} ) ;
2025-07-31 05:38:20 +09:00
} ) ;
2025-08-14 15:59:37 -07:00
it ( 'should reject symbolic file links outside the workspace' , ( ) = > {
const realFile = path . join ( tempDir , 'real-file.txt' ) ;
fs . writeFileSync ( realFile , 'content' ) ;
const symlinkFile = path . join ( cwd , 'symlink-to-real-file' ) ;
fs . symlinkSync ( realFile , symlinkFile , 'file' ) ;
const workspaceContext = new WorkspaceContext ( cwd ) ;
expect ( workspaceContext . isPathWithinWorkspace ( symlinkFile ) ) . toBe ( false ) ;
2025-07-31 05:38:20 +09:00
} ) ;
2025-08-14 15:59:37 -07:00
it ( 'should reject non-existent symbolic file links outside the workspace' , ( ) = > {
const realFile = path . join ( tempDir , 'real-file.txt' ) ;
const symlinkFile = path . join ( cwd , 'symlink-to-real-file' ) ;
fs . symlinkSync ( realFile , symlinkFile , 'file' ) ;
2025-07-31 05:38:20 +09:00
2025-08-14 15:59:37 -07:00
const workspaceContext = new WorkspaceContext ( cwd ) ;
2025-07-31 05:38:20 +09:00
2025-08-14 15:59:37 -07:00
expect ( workspaceContext . isPathWithinWorkspace ( symlinkFile ) ) . toBe ( false ) ;
2025-07-31 05:38:20 +09:00
} ) ;
2025-08-14 15:59:37 -07:00
it ( 'should handle circular symlinks gracefully' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd ) ;
const linkA = path . join ( cwd , 'link-a' ) ;
const linkB = path . join ( cwd , 'link-b' ) ;
// Create a circular dependency: linkA -> linkB -> linkA
fs . symlinkSync ( linkB , linkA , 'dir' ) ;
fs . symlinkSync ( linkA , linkB , 'dir' ) ;
// fs.realpathSync should throw ELOOP, and isPathWithinWorkspace should
// handle it gracefully and return false.
expect ( workspaceContext . isPathWithinWorkspace ( linkA ) ) . toBe ( false ) ;
expect ( workspaceContext . isPathWithinWorkspace ( linkB ) ) . toBe ( false ) ;
2026-01-13 23:40:23 -08:00
} , 30000 ) ;
2025-08-14 15:59:37 -07:00
} ) ;
} ) ;
2025-08-18 14:09:02 -07:00
describe ( 'onDirectoriesChanged' , ( ) = > {
it ( 'should call listener when adding a directory' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd ) ;
const listener = vi . fn ( ) ;
workspaceContext . onDirectoriesChanged ( listener ) ;
workspaceContext . addDirectory ( otherDir ) ;
expect ( listener ) . toHaveBeenCalledOnce ( ) ;
} ) ;
it ( 'should not call listener when adding a duplicate directory' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd ) ;
workspaceContext . addDirectory ( otherDir ) ;
const listener = vi . fn ( ) ;
workspaceContext . onDirectoriesChanged ( listener ) ;
workspaceContext . addDirectory ( otherDir ) ;
expect ( listener ) . not . toHaveBeenCalled ( ) ;
} ) ;
it ( 'should call listener when setting different directories' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd ) ;
const listener = vi . fn ( ) ;
workspaceContext . onDirectoriesChanged ( listener ) ;
workspaceContext . setDirectories ( [ otherDir ] ) ;
expect ( listener ) . toHaveBeenCalledOnce ( ) ;
} ) ;
it ( 'should not call listener when setting same directories' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd ) ;
const listener = vi . fn ( ) ;
workspaceContext . onDirectoriesChanged ( listener ) ;
workspaceContext . setDirectories ( [ cwd ] ) ;
expect ( listener ) . not . toHaveBeenCalled ( ) ;
} ) ;
it ( 'should support multiple listeners' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd ) ;
const listener1 = vi . fn ( ) ;
const listener2 = vi . fn ( ) ;
workspaceContext . onDirectoriesChanged ( listener1 ) ;
workspaceContext . onDirectoriesChanged ( listener2 ) ;
workspaceContext . addDirectory ( otherDir ) ;
expect ( listener1 ) . toHaveBeenCalledOnce ( ) ;
expect ( listener2 ) . toHaveBeenCalledOnce ( ) ;
} ) ;
it ( 'should allow unsubscribing a listener' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd ) ;
const listener = vi . fn ( ) ;
const unsubscribe = workspaceContext . onDirectoriesChanged ( listener ) ;
unsubscribe ( ) ;
workspaceContext . addDirectory ( otherDir ) ;
expect ( listener ) . not . toHaveBeenCalled ( ) ;
} ) ;
it ( 'should not fail if a listener throws an error' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd ) ;
const errorListener = ( ) = > {
throw new Error ( 'test error' ) ;
} ;
const listener = vi . fn ( ) ;
workspaceContext . onDirectoriesChanged ( errorListener ) ;
workspaceContext . onDirectoriesChanged ( listener ) ;
expect ( ( ) = > {
workspaceContext . addDirectory ( otherDir ) ;
} ) . not . toThrow ( ) ;
expect ( listener ) . toHaveBeenCalledOnce ( ) ;
} ) ;
} ) ;
2025-08-14 15:59:37 -07:00
describe ( 'getDirectories' , ( ) = > {
it ( 'should return a copy of directories array' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd ) ;
const dirs1 = workspaceContext . getDirectories ( ) ;
const dirs2 = workspaceContext . getDirectories ( ) ;
expect ( dirs1 ) . not . toBe ( dirs2 ) ;
expect ( dirs1 ) . toEqual ( dirs2 ) ;
2025-07-31 05:38:20 +09:00
} ) ;
} ) ;
2026-01-21 09:58:23 -08:00
describe ( 'addDirectories' , ( ) = > {
it ( 'should add multiple directories and emit one event' , ( ) = > {
const dir3 = path . join ( tempDir , 'dir3' ) ;
fs . mkdirSync ( dir3 ) ;
const workspaceContext = new WorkspaceContext ( cwd ) ;
const listener = vi . fn ( ) ;
workspaceContext . onDirectoriesChanged ( listener ) ;
const result = workspaceContext . addDirectories ( [ otherDir , dir3 ] ) ;
expect ( workspaceContext . getDirectories ( ) ) . toContain ( otherDir ) ;
expect ( workspaceContext . getDirectories ( ) ) . toContain ( dir3 ) ;
expect ( listener ) . toHaveBeenCalledOnce ( ) ;
expect ( result . added ) . toHaveLength ( 2 ) ;
expect ( result . failed ) . toHaveLength ( 0 ) ;
} ) ;
it ( 'should handle partial failures' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd ) ;
const listener = vi . fn ( ) ;
workspaceContext . onDirectoriesChanged ( listener ) ;
const loggerSpy = vi
. spyOn ( debugLogger , 'warn' )
. mockImplementation ( ( ) = > { } ) ;
const nonExistent = path . join ( tempDir , 'does-not-exist' ) ;
const result = workspaceContext . addDirectories ( [ otherDir , nonExistent ] ) ;
expect ( workspaceContext . getDirectories ( ) ) . toContain ( otherDir ) ;
expect ( workspaceContext . getDirectories ( ) ) . not . toContain ( nonExistent ) ;
expect ( listener ) . toHaveBeenCalledOnce ( ) ;
expect ( loggerSpy ) . toHaveBeenCalled ( ) ;
expect ( result . added ) . toEqual ( [ otherDir ] ) ;
expect ( result . failed ) . toHaveLength ( 1 ) ;
expect ( result . failed [ 0 ] . path ) . toBe ( nonExistent ) ;
expect ( result . failed [ 0 ] . error ) . toBeDefined ( ) ;
loggerSpy . mockRestore ( ) ;
} ) ;
it ( 'should not emit event if no directories added' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd ) ;
const listener = vi . fn ( ) ;
workspaceContext . onDirectoriesChanged ( listener ) ;
const loggerSpy = vi
. spyOn ( debugLogger , 'warn' )
. mockImplementation ( ( ) = > { } ) ;
const nonExistent = path . join ( tempDir , 'does-not-exist' ) ;
const result = workspaceContext . addDirectories ( [ nonExistent ] ) ;
expect ( listener ) . not . toHaveBeenCalled ( ) ;
expect ( result . added ) . toHaveLength ( 0 ) ;
expect ( result . failed ) . toHaveLength ( 1 ) ;
loggerSpy . mockRestore ( ) ;
} ) ;
} ) ;
describe ( 'addDirectory' , ( ) = > {
it ( 'should throw error if directory fails to add' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd ) ;
const nonExistent = path . join ( tempDir , 'does-not-exist' ) ;
expect ( ( ) = > workspaceContext . addDirectory ( nonExistent ) ) . toThrow ( ) ;
} ) ;
} ) ;
2025-07-31 05:38:20 +09:00
} ) ;
2025-08-22 15:49:35 -05:00
describe ( 'WorkspaceContext with optional directories' , ( ) = > {
let tempDir : string ;
let cwd : string ;
let existingDir1 : string ;
let existingDir2 : string ;
let nonExistentDir : string ;
beforeEach ( ( ) = > {
tempDir = fs . realpathSync (
fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'workspace-context-optional-' ) ) ,
) ;
cwd = path . join ( tempDir , 'project' ) ;
existingDir1 = path . join ( tempDir , 'existing-dir-1' ) ;
existingDir2 = path . join ( tempDir , 'existing-dir-2' ) ;
nonExistentDir = path . join ( tempDir , 'non-existent-dir' ) ;
fs . mkdirSync ( cwd , { recursive : true } ) ;
fs . mkdirSync ( existingDir1 , { recursive : true } ) ;
fs . mkdirSync ( existingDir2 , { recursive : true } ) ;
2025-11-20 10:44:02 -08:00
vi . spyOn ( debugLogger , 'warn' ) . mockImplementation ( ( ) = > { } ) ;
2025-08-22 15:49:35 -05:00
} ) ;
afterEach ( ( ) = > {
fs . rmSync ( tempDir , { recursive : true , force : true } ) ;
vi . restoreAllMocks ( ) ;
} ) ;
it ( 'should skip a missing optional directory and log a warning' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd , [
nonExistentDir ,
existingDir1 ,
] ) ;
const directories = workspaceContext . getDirectories ( ) ;
expect ( directories ) . toEqual ( [ cwd , existingDir1 ] ) ;
2025-11-20 10:44:02 -08:00
expect ( debugLogger . warn ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( debugLogger . warn ) . toHaveBeenCalledWith (
2025-08-22 15:49:35 -05:00
` [WARN] Skipping unreadable directory: ${ nonExistentDir } (Directory does not exist: ${ nonExistentDir } ) ` ,
) ;
} ) ;
it ( 'should include an existing optional directory' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd , [ existingDir1 ] ) ;
const directories = workspaceContext . getDirectories ( ) ;
expect ( directories ) . toEqual ( [ cwd , existingDir1 ] ) ;
2025-11-20 10:44:02 -08:00
expect ( debugLogger . warn ) . not . toHaveBeenCalled ( ) ;
2025-08-22 15:49:35 -05:00
} ) ;
2026-06-26 19:36:00 +00:00
describe ( 'Security Regression: Case-Insensitive Sensitive Path Blocklist' , ( ) = > {
it ( 'should reject sensitive paths like .git, .env, and node_modules case-insensitively, including Windows trailing character and NTFS ADS bypasses' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd ) ;
const sensitivePaths = [
path . join ( cwd , '.git' , 'config' ) ,
path . join ( cwd , '.GIT' , 'config' ) ,
path . join ( cwd , '.Git' , 'config' ) ,
path . join ( cwd , '.env' ) ,
path . join ( cwd , '.Env' ) ,
path . join ( cwd , '.ENV' ) ,
path . join ( cwd , 'node_modules' , 'package' , 'index.js' ) ,
path . join ( cwd , 'NODE_MODULES' , 'package' , 'index.js' ) ,
// Windows trailing character bypasses
path . join ( cwd , '.git ' , 'config' ) ,
path . join ( cwd , '.git.' , 'config' ) ,
path . join ( cwd , '.env ' , 'config' ) ,
path . join ( cwd , '.env.' , 'config' ) ,
path . join ( cwd , 'node_modules ' , 'package' , 'index.js' ) ,
// NTFS Alternate Data Stream bypasses
path . join ( cwd , '.git::$DATA' , 'config' ) ,
path . join ( cwd , '.env::$DATA' ) ,
path . join ( cwd , 'node_modules::$DATA' , 'package' , 'index.js' ) ,
] ;
for ( const p of sensitivePaths ) {
expect ( workspaceContext . isPathWithinWorkspace ( p ) ) . toBe ( false ) ;
}
} ) ;
2026-07-07 20:30:02 +00:00
it ( 'should reject GitHub Actions Workload Identity credentials' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd ) ;
const sensitivePaths = [
path . join ( cwd , 'gha-creds-12345.json' ) ,
path . join ( cwd , 'gha-creds-abcde.json' ) ,
path . join ( cwd , 'GHA-CREDS-abcde.JSON' ) , // Case-insensitivity check
path . join ( cwd , 'subfolder' , 'gha-creds-12345.json' ) , // Nested
] ;
for ( const p of sensitivePaths ) {
expect ( workspaceContext . isPathWithinWorkspace ( p ) ) . toBe ( false ) ;
}
} ) ;
2026-06-26 19:36:00 +00:00
it ( 'should allow standard non-sensitive paths' , ( ) = > {
const workspaceContext = new WorkspaceContext ( cwd ) ;
const safePaths = [
path . join ( cwd , 'src' , 'index.ts' ) ,
path . join ( cwd , '.gitignore' ) ,
path . join ( cwd , '.env.example' ) ,
path . join ( cwd , 'package.json' ) ,
2026-07-07 20:30:02 +00:00
path . join ( cwd , 'tsconfig.json' ) ,
path . join ( cwd , 'gha-creds.json' ) , // Doesn't match the pattern
2026-06-26 19:36:00 +00:00
] ;
for ( const p of safePaths ) {
expect ( workspaceContext . isPathWithinWorkspace ( p ) ) . toBe ( true ) ;
}
} ) ;
} ) ;
2025-08-22 15:49:35 -05:00
} ) ;