This commit is contained in:
2026-04-12 01:06:31 +07:00
commit 10d660cbcb
1066 changed files with 228596 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
#!/usr/bin/env node
/**
* test-broad-pattern-detector.cjs - Unit tests for broad pattern detection
*
* Tests the detection of overly broad glob patterns that would fill context.
*/
const {
isBroadPattern,
hasSpecificDirectory,
isHighLevelPath,
detectBroadPatternIssue,
suggestSpecificPatterns
} = require('../broad-pattern-detector.cjs');
// === isBroadPattern tests ===
const broadPatternTests = [
// Should be detected as broad - TypeScript/JavaScript
{ pattern: '**/*', expected: true, desc: 'all files everywhere' },
{ pattern: '**', expected: true, desc: 'double star alone' },
{ pattern: '*', expected: true, desc: 'single star alone' },
{ pattern: '**/.*', expected: true, desc: 'all dotfiles' },
// Should NOT be detected as broad (specific)
{ pattern: 'package.json', expected: false, desc: 'specific file' },
{ pattern: 'src/index.ts', expected: false, desc: 'specific file path' },
{ pattern: null, expected: false, desc: 'null pattern' },
{ pattern: '', expected: false, desc: 'empty pattern' },
];
// === isHighLevelPath tests ===
const highLevelPathTests = [
// High level (risky)
{ path: null, expected: true, desc: 'null path (uses CWD)' },
{ path: undefined, expected: true, desc: 'undefined path' },
{ path: '.', expected: true, desc: 'current directory' },
{ path: './', expected: true, desc: 'current directory with slash' },
{ path: '', expected: true, desc: 'empty path' },
{ path: '/home/user/worktrees/myproject', expected: true, desc: 'worktree root' },
{ path: 'myproject', expected: true, desc: 'single directory' },
// Specific (OK)
{ path: 'src/components', expected: false, desc: 'nested in src' },
{ path: 'lib/utils', expected: false, desc: 'nested in lib' },
{ path: 'packages/web/src', expected: false, desc: 'monorepo src' },
{ path: '/home/user/project/src', expected: false, desc: 'absolute with src' },
];
// === detectBroadPatternIssue integration tests ===
const integrationTests = [
// Should BLOCK
{
input: { pattern: '**/*.ts' },
expected: true,
desc: 'broad pattern, no path'
},
{
input: { pattern: '**/*.{ts,tsx}', path: '/home/user/worktrees/myproject' },
expected: true,
desc: 'broad pattern at worktree'
},
{
input: { pattern: '**/*', path: '.' },
expected: true,
desc: 'all files at current dir'
},
{
input: { pattern: '**/index.ts', path: 'myproject' },
expected: true,
desc: 'all index.ts at shallow path'
},
// Should ALLOW
{
input: { pattern: 'src/**/*.ts' },
expected: false,
desc: 'scoped to src'
},
{
input: { pattern: '**/*.ts', path: 'src/components' },
expected: false,
desc: 'broad pattern but specific path'
},
{
input: { pattern: 'package.json' },
expected: false,
desc: 'specific file'
},
{
input: { pattern: 'lib/**/*.js', path: '/home/user/project' },
expected: false,
desc: 'scoped pattern'
},
{
input: {},
expected: false,
desc: 'no pattern'
},
{
input: null,
expected: false,
desc: 'null input'
},
];
// Run tests
console.log('Testing broad-pattern-detector module...\n');
let passed = 0;
let failed = 0;
// Test isBroadPattern
console.log('\x1b[1m--- isBroadPattern ---\x1b[0m');
for (const test of broadPatternTests) {
const result = isBroadPattern(test.pattern);
const success = result === test.expected;
if (success) {
console.log(`\x1b[32m✓\x1b[0m ${test.desc}: "${test.pattern}" -> ${result ? 'BROAD' : 'OK'}`);
passed++;
} else {
console.log(`\x1b[31m✗\x1b[0m ${test.desc}: expected ${test.expected ? 'BROAD' : 'OK'}, got ${result ? 'BROAD' : 'OK'}`);
failed++;
}
}
// Test isHighLevelPath
console.log('\n\x1b[1m--- isHighLevelPath ---\x1b[0m');
for (const test of highLevelPathTests) {
const result = isHighLevelPath(test.path);
const success = result === test.expected;
if (success) {
console.log(`\x1b[32m✓\x1b[0m ${test.desc}: "${test.path}" -> ${result ? 'HIGH_LEVEL' : 'SPECIFIC'}`);
passed++;
} else {
console.log(`\x1b[31m✗\x1b[0m ${test.desc}: expected ${test.expected ? 'HIGH_LEVEL' : 'SPECIFIC'}, got ${result ? 'HIGH_LEVEL' : 'SPECIFIC'}`);
failed++;
}
}
// Test integration
console.log('\n\x1b[1m--- detectBroadPatternIssue (integration) ---\x1b[0m');
for (const test of integrationTests) {
const result = detectBroadPatternIssue(test.input);
const success = result.blocked === test.expected;
if (success) {
console.log(`\x1b[32m✓\x1b[0m ${test.desc} -> ${result.blocked ? 'BLOCKED' : 'ALLOWED'}`);
passed++;
} else {
console.log(`\x1b[31m✗\x1b[0m ${test.desc}: expected ${test.expected ? 'BLOCKED' : 'ALLOWED'}, got ${result.blocked ? 'BLOCKED' : 'ALLOWED'}`);
failed++;
}
}
// Test suggestions
console.log('\n\x1b[1m--- suggestSpecificPatterns ---\x1b[0m');
const suggestions = suggestSpecificPatterns('**/*.ts');
if (suggestions.length > 0 && suggestions.some(s => s.includes('src/'))) {
console.log(`\x1b[32m✓\x1b[0m suggestions for **/*.ts include src-scoped patterns`);
passed++;
} else {
console.log(`\x1b[31m✗\x1b[0m suggestions should include src-scoped patterns`);
failed++;
}
console.log(`\n\x1b[1mResults:\x1b[0m ${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);

View File

@@ -0,0 +1,137 @@
#!/usr/bin/env node
/**
* test-build-command-allowlist.cjs - Tests for build command allowlist patterns
*
* Tests that build commands from various languages/tools are properly recognized
* and allowed (bypassing path blocking).
*/
// Replicate the patterns from scout-block.cjs
const BUILD_COMMAND_PATTERN = /^(npm|pnpm|yarn|bun)\s+([^\s]+\s+)*(run\s+)?(build|test|lint|dev|start|install|ci|add|remove|update|publish|pack|init|create|exec)/;
const TOOL_COMMAND_PATTERN = /^(\.\/)?(npx|pnpx|bunx|tsc|esbuild|vite|webpack|rollup|turbo|nx|jest|vitest|mocha|eslint|prettier|go|cargo|make|mvn|mvnw|gradle|gradlew|dotnet|docker|podman|kubectl|helm|terraform|ansible|bazel|cmake|sbt|flutter|swift|ant|ninja|meson)/;
function isBuildCommand(command) {
if (!command || typeof command !== 'string') return false;
const trimmed = command.trim();
return BUILD_COMMAND_PATTERN.test(trimmed) || TOOL_COMMAND_PATTERN.test(trimmed);
}
const tests = [
// JS/Node package managers - should be allowed
{ cmd: 'npm run build', expected: true, desc: 'npm run build' },
{ cmd: 'npm build', expected: true, desc: 'npm build' },
{ cmd: 'pnpm build', expected: true, desc: 'pnpm build' },
{ cmd: 'yarn build', expected: true, desc: 'yarn build' },
{ cmd: 'bun build', expected: true, desc: 'bun build' },
{ cmd: 'npm install', expected: true, desc: 'npm install' },
{ cmd: 'pnpm --filter web run build', expected: true, desc: 'pnpm with filter' },
{ cmd: 'yarn workspace app build', expected: true, desc: 'yarn workspace build' },
// JS tools - should be allowed
{ cmd: 'npx tsc', expected: true, desc: 'npx tsc' },
{ cmd: 'tsc --build', expected: true, desc: 'tsc --build' },
{ cmd: 'esbuild src/index.ts', expected: true, desc: 'esbuild' },
{ cmd: 'vite build', expected: true, desc: 'vite build' },
{ cmd: 'webpack', expected: true, desc: 'webpack' },
{ cmd: 'turbo run build', expected: true, desc: 'turbo run build' },
{ cmd: 'nx build app', expected: true, desc: 'nx build' },
// Go - should be allowed (THE BUG FIX)
{ cmd: 'go build ./...', expected: true, desc: 'go build ./...' },
{ cmd: 'go build -o app main.go', expected: true, desc: 'go build with flags' },
{ cmd: 'go test ./...', expected: true, desc: 'go test' },
{ cmd: 'go run main.go', expected: true, desc: 'go run' },
{ cmd: 'go mod tidy', expected: true, desc: 'go mod tidy' },
{ cmd: 'go install', expected: true, desc: 'go install' },
// Rust/Cargo - should be allowed
{ cmd: 'cargo build', expected: true, desc: 'cargo build' },
{ cmd: 'cargo build --release', expected: true, desc: 'cargo build --release' },
{ cmd: 'cargo test', expected: true, desc: 'cargo test' },
{ cmd: 'cargo run', expected: true, desc: 'cargo run' },
// Make - should be allowed
{ cmd: 'make', expected: true, desc: 'make' },
{ cmd: 'make build', expected: true, desc: 'make build' },
{ cmd: 'make clean', expected: true, desc: 'make clean' },
{ cmd: 'make -j4', expected: true, desc: 'make -j4' },
// Java/Maven/Gradle - should be allowed
{ cmd: 'mvn clean install', expected: true, desc: 'mvn clean install' },
{ cmd: 'mvn package', expected: true, desc: 'mvn package' },
{ cmd: 'gradle build', expected: true, desc: 'gradle build' },
{ cmd: 'gradle test', expected: true, desc: 'gradle test' },
// Maven/Gradle wrappers - should be allowed (NEW)
{ cmd: './gradlew build', expected: true, desc: './gradlew build' },
{ cmd: './gradlew clean test', expected: true, desc: './gradlew clean test' },
{ cmd: 'gradlew build', expected: true, desc: 'gradlew build (no ./)' },
{ cmd: './mvnw clean install', expected: true, desc: './mvnw clean install' },
{ cmd: './mvnw package', expected: true, desc: './mvnw package' },
{ cmd: 'mvnw clean install', expected: true, desc: 'mvnw clean install (no ./)' },
// .NET - should be allowed
{ cmd: 'dotnet build', expected: true, desc: 'dotnet build' },
{ cmd: 'dotnet run', expected: true, desc: 'dotnet run' },
{ cmd: 'dotnet test', expected: true, desc: 'dotnet test' },
// Docker/Container tools - should be allowed
{ cmd: 'docker build .', expected: true, desc: 'docker build' },
{ cmd: 'docker build -t myapp .', expected: true, desc: 'docker build with tag' },
{ cmd: 'docker compose up', expected: true, desc: 'docker compose' },
{ cmd: 'podman build .', expected: true, desc: 'podman build' },
// Kubernetes/Infrastructure - should be allowed
{ cmd: 'kubectl apply -f deploy/', expected: true, desc: 'kubectl apply' },
{ cmd: 'kubectl get pods', expected: true, desc: 'kubectl get' },
{ cmd: 'helm install myapp ./chart', expected: true, desc: 'helm install' },
{ cmd: 'terraform apply', expected: true, desc: 'terraform apply' },
{ cmd: 'terraform plan', expected: true, desc: 'terraform plan' },
{ cmd: 'ansible-playbook site.yml', expected: true, desc: 'ansible playbook' },
// Additional build systems - should be allowed (NEW)
{ cmd: 'bazel build //...', expected: true, desc: 'bazel build' },
{ cmd: 'bazel test //...', expected: true, desc: 'bazel test' },
{ cmd: 'cmake --build .', expected: true, desc: 'cmake build' },
{ cmd: 'cmake -B build', expected: true, desc: 'cmake configure' },
{ cmd: 'sbt compile', expected: true, desc: 'sbt compile' },
{ cmd: 'sbt test', expected: true, desc: 'sbt test' },
{ cmd: 'flutter build apk', expected: true, desc: 'flutter build apk' },
{ cmd: 'flutter run', expected: true, desc: 'flutter run' },
{ cmd: 'swift build', expected: true, desc: 'swift build' },
{ cmd: 'swift test', expected: true, desc: 'swift test' },
{ cmd: 'ant build', expected: true, desc: 'ant build' },
{ cmd: 'ant clean', expected: true, desc: 'ant clean' },
{ cmd: 'ninja', expected: true, desc: 'ninja' },
{ cmd: 'ninja -C build', expected: true, desc: 'ninja -C build' },
{ cmd: 'meson compile', expected: true, desc: 'meson compile' },
{ cmd: 'meson setup build', expected: true, desc: 'meson setup' },
// Directory access - should be BLOCKED (not recognized as build commands)
{ cmd: 'cd build', expected: false, desc: 'cd build (blocked)' },
{ cmd: 'ls build', expected: false, desc: 'ls build (blocked)' },
{ cmd: 'cat build/output.js', expected: false, desc: 'cat build file (blocked)' },
{ cmd: 'cd node_modules', expected: false, desc: 'cd node_modules (blocked)' },
{ cmd: 'rm -rf dist', expected: false, desc: 'rm -rf dist (blocked)' },
];
console.log('Testing build command allowlist...\n');
let passed = 0;
let failed = 0;
for (const test of tests) {
const result = isBuildCommand(test.cmd);
const success = result === test.expected;
if (success) {
console.log(`\x1b[32m✓\x1b[0m ${test.desc}: ${result}`);
passed++;
} else {
console.log(`\x1b[31m✗\x1b[0m ${test.desc}: expected ${test.expected}, got ${result}`);
failed++;
}
}
console.log(`\nResults: ${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);

View File

@@ -0,0 +1,114 @@
#!/usr/bin/env node
/**
* test-error-formatter.cjs - Unit tests for error-formatter module
*/
const {
formatBlockedError,
formatSimpleError,
formatMachineError,
formatWarning,
formatConfigPath,
supportsColor,
colorize,
COLORS
} = require('../error-formatter.cjs');
let passed = 0;
let failed = 0;
function test(name, condition) {
if (condition) {
console.log(`\x1b[32m✓\x1b[0m ${name}`);
passed++;
} else {
console.log(`\x1b[31m✗\x1b[0m ${name}`);
failed++;
}
}
console.log('Testing error-formatter module...\n');
// formatConfigPath tests
console.log('--- formatConfigPath Tests ---');
test('formatConfigPath with claudeDir', formatConfigPath('/home/user/.claude').includes('.ckignore'));
test('formatConfigPath prefers explicit configPath', formatConfigPath('/home/user/.claude', '/tmp/project/.ckignore') === '/tmp/project/.ckignore');
test('formatConfigPath without claudeDir', formatConfigPath(null) === '.claude/.ckignore');
test('formatConfigPath empty string', formatConfigPath('') === '.claude/.ckignore');
// formatBlockedError tests
console.log('\n--- formatBlockedError Tests ---');
const blockError = formatBlockedError({
path: 'packages/web/node_modules/react',
pattern: 'node_modules',
tool: 'Bash',
claudeDir: '/home/user/project/.claude',
configPath: '/home/user/project/.ckignore'
});
test('formatBlockedError contains BLOCKED', blockError.includes('BLOCKED'));
test('formatBlockedError contains path', blockError.includes('packages/web/node_modules/react'));
test('formatBlockedError contains pattern', blockError.includes('node_modules'));
test('formatBlockedError contains tool', blockError.includes('Bash'));
test('formatBlockedError contains fix hint', blockError.includes('!node_modules'));
test('formatBlockedError prefers explicit config path', blockError.includes('/home/user/project/.ckignore'));
// Test long path truncation
const longPath = 'a/'.repeat(50) + 'node_modules/package/index.js';
const longPathError = formatBlockedError({
path: longPath,
pattern: 'node_modules',
tool: 'Read',
claudeDir: '.claude'
});
test('formatBlockedError truncates long path', longPathError.includes('...'));
// formatSimpleError tests
console.log('\n--- formatSimpleError Tests ---');
const simpleError = formatSimpleError('node_modules', 'packages/web/node_modules');
test('formatSimpleError contains ERROR', simpleError.includes('ERROR'));
test('formatSimpleError contains pattern', simpleError.includes('node_modules'));
test('formatSimpleError contains path', simpleError.includes('packages/web/node_modules'));
// formatMachineError tests
console.log('\n--- formatMachineError Tests ---');
const machineError = formatMachineError({
path: 'dist/bundle.js',
pattern: 'dist',
tool: 'Read',
claudeDir: '.claude',
configPath: '/tmp/project/.ckignore'
});
const parsed = JSON.parse(machineError);
test('formatMachineError is valid JSON', typeof parsed === 'object');
test('formatMachineError has error field', parsed.error === 'BLOCKED');
test('formatMachineError has path field', parsed.path === 'dist/bundle.js');
test('formatMachineError has pattern field', parsed.pattern === 'dist');
test('formatMachineError has tool field', parsed.tool === 'Read');
test('formatMachineError has config field', parsed.config === '/tmp/project/.ckignore');
test('formatMachineError has fix field', parsed.fix.includes('!dist'));
// formatWarning tests
console.log('\n--- formatWarning Tests ---');
const warning = formatWarning('Test warning message');
test('formatWarning contains WARN', warning.includes('WARN'));
test('formatWarning contains message', warning.includes('Test warning message'));
// colorize tests (with forced NO_COLOR)
console.log('\n--- colorize Tests ---');
const originalNoColor = process.env.NO_COLOR;
process.env.NO_COLOR = '1';
test('colorize respects NO_COLOR', colorize('test', 'red') === 'test');
delete process.env.NO_COLOR;
// Test COLORS constant exists
test('COLORS constant has expected keys',
'red' in COLORS && 'yellow' in COLORS && 'blue' in COLORS && 'reset' in COLORS
);
// Restore original NO_COLOR
if (originalNoColor !== undefined) {
process.env.NO_COLOR = originalNoColor;
}
console.log(`\nResults: ${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env node
/**
* test-full-flow-edge-cases.cjs - Edge case validation for full hook flow
*/
const BUILD_COMMAND_PATTERN = /^(npm|pnpm|yarn|bun)\s+([^\s]+\s+)*(run\s+)?(build|test|lint|dev|start|install|ci|add|remove|update|publish|pack|init|create|exec)/;
const TOOL_COMMAND_PATTERN = /^(npx|pnpx|bunx|tsc|esbuild|vite|webpack|rollup|turbo|nx|jest|vitest|mocha|eslint|prettier|go|cargo|make|mvn|gradle|dotnet)/;
function isBuildCommand(command) {
if (!command || typeof command !== 'string') return false;
const trimmed = command.trim();
return BUILD_COMMAND_PATTERN.test(trimmed) || TOOL_COMMAND_PATTERN.test(trimmed);
}
console.log('=== FULL FLOW EDGE CASE VALIDATION ===\n');
const tests = [
// Should be ALLOWED (bypass path extraction)
{ cmd: 'go build ./...', expect: true, desc: 'go build basic' },
{ cmd: 'cargo build', expect: true, desc: 'cargo build basic' },
{ cmd: 'make build', expect: true, desc: 'make build' },
{ cmd: 'make -j4', expect: true, desc: 'make with flags' },
{ cmd: 'mvn clean install', expect: true, desc: 'maven' },
{ cmd: 'gradle build', expect: true, desc: 'gradle' },
{ cmd: 'dotnet build', expect: true, desc: 'dotnet' },
{ cmd: 'npm run build', expect: true, desc: 'npm run build' },
{ cmd: 'go test ./...', expect: true, desc: 'go test' },
// Should be BLOCKED (goes through path extraction)
{ cmd: 'docker build .', expect: false, desc: 'docker build (not in allowlist)' },
{ cmd: 'cd proj && go build', expect: false, desc: 'chained with cd first' },
{ cmd: 'GOOS=linux go build', expect: false, desc: 'env var prefix' },
{ cmd: 'sudo go build', expect: false, desc: 'sudo prefix' },
{ cmd: 'time go build', expect: false, desc: 'time prefix' },
{ cmd: 'ls build', expect: false, desc: 'ls build dir' },
{ cmd: 'cd build', expect: false, desc: 'cd build dir' },
];
let passed = 0;
let failed = 0;
for (const t of tests) {
const result = isBuildCommand(t.cmd);
const success = result === t.expect;
if (success) {
console.log(`\x1b[32m✓\x1b[0m ${t.desc}: "${t.cmd}" → ${result}`);
passed++;
} else {
console.log(`\x1b[31m✗\x1b[0m ${t.desc}: "${t.cmd}" → ${result} (expected ${t.expect})`);
failed++;
}
}
console.log(`\nResults: ${passed} passed, ${failed} failed`);
// Additional edge case analysis
console.log('\n=== EDGE CASES REQUIRING ATTENTION ===\n');
const edgeCases = [
{ cmd: 'docker build .', issue: 'docker not in TOOL_COMMAND_PATTERN - should it be?' },
{ cmd: 'cd proj && go build', issue: 'Chained commands: first segment checked, not individual commands' },
{ cmd: 'GOOS=linux go build', issue: 'Env var prefix breaks regex start anchor' },
{ cmd: 'php artisan build', issue: 'php/artisan not in patterns' },
{ cmd: 'bundle exec build', issue: 'ruby bundler not in patterns' },
];
console.log('Known edge cases that may cause UX issues:\n');
for (const ec of edgeCases) {
const allowed = isBuildCommand(ec.cmd);
console.log(` ${allowed ? '✓' : '⚠'} "${ec.cmd}"`);
console.log(` Issue: ${ec.issue}\n`);
}
process.exit(failed > 0 ? 1 : 0);

View File

@@ -0,0 +1,225 @@
#!/usr/bin/env node
/**
* test-monorepo-scenarios.cjs - Integration tests for monorepo patterns
*
* THIS IS THE CRITICAL TEST FILE FOR THE BUG FIX!
* Tests that subfolder blocked directories (node_modules, dist, etc.)
* are properly blocked in monorepo structures.
*/
const { execSync } = require('child_process');
const path = require('path');
const hookPath = path.join(__dirname, '..', '..', 'scout-block.cjs');
const scenarios = [
// === THE BUG CASES - These MUST be BLOCKED ===
{
input: { tool_name: 'Bash', tool_input: { command: 'ls packages/web/node_modules' } },
expected: 'BLOCKED',
desc: '[BUG FIX] ls subfolder node_modules'
},
{
input: { tool_name: 'Bash', tool_input: { command: 'cd apps/api/node_modules' } },
expected: 'BLOCKED',
desc: '[BUG FIX] cd subfolder node_modules'
},
{
input: { tool_name: 'Bash', tool_input: { command: 'cat packages/shared/node_modules/lodash/index.js' } },
expected: 'BLOCKED',
desc: '[BUG FIX] cat file in subfolder node_modules'
},
{
input: { tool_name: 'Read', tool_input: { file_path: 'packages/web/node_modules/react/package.json' } },
expected: 'BLOCKED',
desc: '[BUG FIX] Read subfolder node_modules'
},
{
input: { tool_name: 'Grep', tool_input: { pattern: 'export', path: 'packages/web/node_modules' } },
expected: 'BLOCKED',
desc: '[BUG FIX] Grep in subfolder node_modules'
},
{
input: { tool_name: 'Glob', tool_input: { pattern: 'packages/web/node_modules/**/*.js' } },
expected: 'BLOCKED',
desc: '[BUG FIX] Glob subfolder node_modules'
},
// === Deep nesting (also bug cases) ===
{
input: { tool_name: 'Read', tool_input: { file_path: 'a/b/c/d/node_modules/pkg/index.js' } },
expected: 'BLOCKED',
desc: '[BUG FIX] Deep nested node_modules'
},
{
input: { tool_name: 'Bash', tool_input: { command: 'ls packages/web/dist' } },
expected: 'BLOCKED',
desc: '[BUG FIX] ls subfolder dist'
},
{
input: { tool_name: 'Bash', tool_input: { command: 'cat apps/api/build/server.js' } },
expected: 'BLOCKED',
desc: '[BUG FIX] cat subfolder build'
},
// === Root level blocking (should still work) ===
{
input: { tool_name: 'Bash', tool_input: { command: 'ls node_modules' } },
expected: 'BLOCKED',
desc: 'ls root node_modules'
},
{
input: { tool_name: 'Read', tool_input: { file_path: 'node_modules/lodash/index.js' } },
expected: 'BLOCKED',
desc: 'Read root node_modules'
},
{
input: { tool_name: 'Bash', tool_input: { command: 'cat .git/config' } },
expected: 'BLOCKED',
desc: 'cat .git file'
},
// === Build commands - MUST be ALLOWED ===
{
input: { tool_name: 'Bash', tool_input: { command: 'npm run build' } },
expected: 'ALLOWED',
desc: 'npm run build'
},
{
input: { tool_name: 'Bash', tool_input: { command: 'pnpm build' } },
expected: 'ALLOWED',
desc: 'pnpm build'
},
{
input: { tool_name: 'Bash', tool_input: { command: 'yarn build' } },
expected: 'ALLOWED',
desc: 'yarn build'
},
{
input: { tool_name: 'Bash', tool_input: { command: 'npm test' } },
expected: 'ALLOWED',
desc: 'npm test'
},
{
input: { tool_name: 'Bash', tool_input: { command: 'npm install' } },
expected: 'ALLOWED',
desc: 'npm install'
},
{
input: { tool_name: 'Bash', tool_input: { command: 'pnpm --filter web run build' } },
expected: 'ALLOWED',
desc: 'pnpm filter build'
},
{
input: { tool_name: 'Bash', tool_input: { command: 'npx tsc' } },
expected: 'ALLOWED',
desc: 'npx tsc'
},
{
input: { tool_name: 'Bash', tool_input: { command: 'jest --coverage' } },
expected: 'ALLOWED',
desc: 'jest with flags'
},
// === Safe operations - MUST be ALLOWED ===
{
input: { tool_name: 'Read', tool_input: { file_path: 'packages/web/src/App.tsx' } },
expected: 'ALLOWED',
desc: 'Read safe path'
},
{
input: { tool_name: 'Bash', tool_input: { command: 'ls packages/web/src' } },
expected: 'ALLOWED',
desc: 'ls safe path'
},
{
input: { tool_name: 'Grep', tool_input: { pattern: 'import', path: 'src' } },
expected: 'ALLOWED',
desc: 'Grep in src'
},
{
input: { tool_name: 'Glob', tool_input: { pattern: '**/*.ts' } },
expected: 'ALLOWED',
desc: 'Glob all .ts files'
},
{
input: { tool_name: 'Bash', tool_input: { command: 'find packages -name "*.json" | head' } },
expected: 'ALLOWED',
desc: 'find without blocked dirs'
},
// === Edge cases - names containing blocked words but NOT the dirs ===
{
input: { tool_name: 'Read', tool_input: { file_path: 'my-node_modules-project/file.js' } },
expected: 'ALLOWED',
desc: 'node_modules in project name'
},
{
input: { tool_name: 'Bash', tool_input: { command: 'ls build-tools' } },
expected: 'ALLOWED',
desc: 'build- prefix directory'
},
];
console.log('Testing monorepo scenarios (scout-block integration)...\n');
console.log('Hook path:', hookPath, '\n');
let passed = 0;
let failed = 0;
for (const scenario of scenarios) {
try {
execSync(`node "${hookPath}"`, {
input: JSON.stringify(scenario.input),
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe']
});
// Exit 0 = ALLOWED
const actual = 'ALLOWED';
const success = actual === scenario.expected;
if (success) {
console.log(`\x1b[32m✓\x1b[0m ${scenario.desc}: ${actual}`);
passed++;
} else {
console.log(`\x1b[31m✗\x1b[0m ${scenario.desc}: expected ${scenario.expected}, got ${actual}`);
failed++;
}
} catch (error) {
// Exit 2 = BLOCKED
const actual = error.status === 2 ? 'BLOCKED' : `ERROR(${error.status})`;
const success = actual === scenario.expected;
if (success) {
console.log(`\x1b[32m✓\x1b[0m ${scenario.desc}: ${actual}`);
passed++;
} else {
console.log(`\x1b[31m✗\x1b[0m ${scenario.desc}: expected ${scenario.expected}, got ${actual}`);
if (error.stderr) {
console.log(` stderr: ${error.stderr.toString().trim().split('\n')[0]}`);
}
failed++;
}
}
}
console.log(`\nResults: ${passed} passed, ${failed} failed`);
// Highlight if any bug fix cases failed
const bugFixFailed = scenarios.filter(s => s.desc.includes('[BUG FIX]')).some(s => {
try {
execSync(`node "${hookPath}"`, {
input: JSON.stringify(s.input),
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe']
});
return s.expected === 'BLOCKED'; // Should have been blocked but wasn't
} catch (error) {
return error.status !== 2 && s.expected === 'BLOCKED';
}
});
if (bugFixFailed) {
console.log('\n\x1b[31mWARNING: Some bug fix test cases failed!\x1b[0m');
console.log('The subfolder blocking bug has NOT been fixed properly.');
}
process.exit(failed > 0 ? 1 : 0);

View File

@@ -0,0 +1,138 @@
#!/usr/bin/env node
/**
* test-path-extractor.cjs - Unit tests for path-extractor module
*/
const { extractFromToolInput, extractFromCommand, looksLikePath } = require('../path-extractor.cjs');
const toolInputTests = [
{
input: { file_path: 'packages/web/src/index.js' },
expected: ['packages/web/src/index.js'],
desc: 'file_path extraction'
},
{
input: { path: 'node_modules' },
expected: ['node_modules'],
desc: 'path extraction'
},
{
input: { pattern: '**/node_modules/**' },
expected: ['**/node_modules/**'],
desc: 'pattern extraction'
},
{
input: { command: 'ls packages/web/node_modules' },
hasPath: 'packages/web/node_modules',
desc: 'command path extraction'
},
{
input: { file_path: '/home/user/project/node_modules/pkg/index.js' },
expected: ['/home/user/project/node_modules/pkg/index.js'],
desc: 'absolute path extraction'
},
{
input: { file_path: 'packages/web/node_modules/react/package.json', path: 'src' },
hasPath: 'packages/web/node_modules',
desc: 'multiple params extraction'
}
];
const commandTests = [
{ cmd: 'ls packages/web/node_modules', hasPath: 'packages/web/node_modules', desc: 'ls with subfolder' },
{ cmd: 'cat "path with spaces/file.js"', hasPath: 'path with spaces/file.js', desc: 'quoted path' },
{ cmd: "cat 'single/quoted/path.js'", hasPath: 'single/quoted/path.js', desc: 'single quoted path' },
{ cmd: 'cd apps/api/node_modules && ls', hasPath: 'apps/api/node_modules', desc: 'cd with chained command' },
{ cmd: 'rm -rf node_modules', hasPath: 'node_modules', desc: 'rm with flags' },
{ cmd: 'cp -r dist/ backup/', hasPath: 'dist', desc: 'cp with flags' },
// Note: Build commands may extract 'build' as a blocked dir name, but this is handled
// at the dispatcher level (build commands bypass path checking entirely).
// The path extractor correctly identifies blocked dir names like 'build'.
{ cmd: 'npm run build', hasPath: 'build', desc: 'npm run build (extracts build)' },
{ cmd: 'pnpm build', hasPath: 'build', desc: 'pnpm build (extracts build)' },
{ cmd: 'cd build', hasPath: 'build', desc: 'cd build (extracts build)' },
{ cmd: 'yarn test', hasPath: null, desc: 'yarn test (no blocked paths)' },
{ cmd: 'npm install', hasPath: null, desc: 'npm install (no blocked paths)' },
];
const looksLikePathTests = [
{ str: 'packages/web/src', expected: true, desc: 'relative path with slashes' },
{ str: '/home/user/project', expected: true, desc: 'absolute path' },
{ str: './src/index.js', expected: true, desc: 'dot-relative path' },
{ str: '../parent/file.js', expected: true, desc: 'parent-relative path' },
{ str: 'file.txt', expected: true, desc: 'file with extension' },
{ str: 'node_modules', expected: true, desc: 'blocked dir name' },
{ str: 'ls', expected: false, desc: 'command word' },
{ str: 'npm', expected: false, desc: 'package manager' },
{ str: '-rf', expected: false, desc: 'flag' },
{ str: '123', expected: false, desc: 'number' },
];
console.log('Testing path-extractor module...\n');
let passed = 0;
let failed = 0;
// Tool input tests
console.log('--- Tool Input Tests ---');
for (const test of toolInputTests) {
const result = extractFromToolInput(test.input);
let success;
if (test.expected) {
success = test.expected.every(e => result.includes(e));
} else if (test.hasPath) {
success = result.some(p => p.includes(test.hasPath));
}
if (success) {
console.log(`\x1b[32m✓\x1b[0m ${test.desc}`);
passed++;
} else {
console.log(`\x1b[31m✗\x1b[0m ${test.desc}: got ${JSON.stringify(result)}`);
failed++;
}
}
// Command tests
console.log('\n--- Command Tests ---');
for (const test of commandTests) {
const result = extractFromCommand(test.cmd);
let success;
if (test.hasPath === null) {
// Build commands should extract few/no blocked-related paths
success = result.length === 0 || !result.some(p =>
p.includes('node_modules') || p.includes('dist') || p.includes('build')
);
} else {
success = result.some(p => p.includes(test.hasPath));
}
if (success) {
console.log(`\x1b[32m✓\x1b[0m ${test.desc}: ${JSON.stringify(result)}`);
passed++;
} else {
console.log(`\x1b[31m✗\x1b[0m ${test.desc}: expected path containing '${test.hasPath}', got ${JSON.stringify(result)}`);
failed++;
}
}
// looksLikePath tests
console.log('\n--- looksLikePath Tests ---');
for (const test of looksLikePathTests) {
const result = looksLikePath(test.str);
const success = result === test.expected;
if (success) {
console.log(`\x1b[32m✓\x1b[0m ${test.desc}: '${test.str}' -> ${result}`);
passed++;
} else {
console.log(`\x1b[31m✗\x1b[0m ${test.desc}: expected ${test.expected}, got ${result}`);
failed++;
}
}
console.log(`\nResults: ${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env node
/**
* test-pattern-matcher.cjs - Unit tests for pattern-matcher module
*/
const path = require('path');
const { loadPatterns, createMatcher, matchPath, DEFAULT_PATTERNS } = require('../pattern-matcher.cjs');
const tests = [
// === Basic blocking at root ===
{ path: 'node_modules/lodash', expected: true, desc: 'root node_modules with content' },
{ path: 'node_modules', expected: true, desc: 'root node_modules bare' },
{ path: '.git/objects', expected: true, desc: 'root .git' },
{ path: 'dist/bundle.js', expected: true, desc: 'root dist' },
{ path: 'build/output', expected: true, desc: 'root build' },
{ path: '__pycache__/file.pyc', expected: true, desc: 'root __pycache__' },
// === Subfolder blocking (THE BUG FIX!) ===
{ path: 'packages/web/node_modules/react', expected: true, desc: 'subfolder node_modules (monorepo)' },
{ path: 'apps/api/node_modules', expected: true, desc: 'subfolder node_modules bare' },
{ path: 'packages/.git/HEAD', expected: true, desc: 'subfolder .git' },
{ path: 'packages/web/dist/index.js', expected: true, desc: 'subfolder dist' },
{ path: 'apps/backend/build/server.js', expected: true, desc: 'subfolder build' },
{ path: 'packages/shared/__pycache__/module.pyc', expected: true, desc: 'subfolder __pycache__' },
// === Deep nesting ===
{ path: 'a/b/c/d/node_modules/e', expected: true, desc: 'deep nested node_modules' },
{ path: 'projects/monorepo/packages/web/node_modules/react/index.js', expected: true, desc: 'very deep nested' },
// === Allowed paths ===
{ path: 'src/index.js', expected: false, desc: 'src directory' },
{ path: 'packages/web/src/App.tsx', expected: false, desc: 'nested src' },
{ path: 'lib/utils.js', expected: false, desc: 'lib directory' },
{ path: 'README.md', expected: false, desc: 'root file' },
{ path: 'apps/api/server.ts', expected: false, desc: 'nested app file' },
// === Edge cases (should NOT be blocked) ===
{ path: 'my-node_modules-project/file.js', expected: false, desc: 'node_modules in project name' },
{ path: 'build-tools/script.sh', expected: false, desc: 'build- prefix in name' },
{ path: 'src/dist-utils.js', expected: false, desc: 'dist- prefix in name' },
{ path: 'nodemodulesbackup/file.js', expected: false, desc: 'node_modules without separator' },
{ path: 'distro/file.js', expected: false, desc: 'dist prefix without separator' },
];
console.log('Testing pattern-matcher module...\n');
const matcher = createMatcher(DEFAULT_PATTERNS);
let passed = 0;
let failed = 0;
for (const test of tests) {
const result = matchPath(matcher, test.path);
const success = result.blocked === test.expected;
if (success) {
console.log(`\x1b[32m✓\x1b[0m ${test.desc}: ${test.path} -> ${result.blocked ? 'BLOCKED' : 'ALLOWED'}`);
passed++;
} else {
console.log(`\x1b[31m✗\x1b[0m ${test.desc}: expected ${test.expected ? 'BLOCKED' : 'ALLOWED'}, got ${result.blocked ? 'BLOCKED' : 'ALLOWED'}`);
failed++;
}
}
console.log(`\nResults: ${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);