210 lines
8.1 KiB
Markdown
210 lines
8.1 KiB
Markdown
# Testing Patterns
|
|
|
|
**Analysis Date:** 2026-03-27
|
|
|
|
## Test Framework
|
|
|
|
**Status:** Not detected
|
|
- No test files (*.test.js, *.spec.js) found in codebase
|
|
- No test runner configured (no jest.config.js, vitest.config.js, or mocha configuration)
|
|
- No test dependencies listed in package.json files
|
|
|
|
**Code Context:**
|
|
- The codebase consists of 5 Node.js hook scripts (579 total lines across `.claude/hooks/`)
|
|
- Each hook is a standalone CLI tool that reads JSON from stdin and outputs JSON to stdout
|
|
- Hooks are event-driven (SessionStart, PreToolUse, PostToolUse, AfterTool lifecycle events)
|
|
- No application code beyond these hooks exists in the repository
|
|
|
|
## Script Type & Testing Approach
|
|
|
|
**Current Architecture:**
|
|
Each hook file (`gsd-*.js` in `/home/ys/family-repo/AgenticCode/.claude/hooks/`) follows the same structural pattern:
|
|
- Shebang: `#!/usr/bin/env node`
|
|
- Node.js built-in modules only (fs, path, os, child_process)
|
|
- JSON stdin → processing → JSON stdout
|
|
- Silent failure on errors (timeout guards, try-catch with exit(0))
|
|
|
|
**Hook Files:**
|
|
- `gsd-prompt-guard.js` (96 lines) - Detects prompt injection patterns in written files
|
|
- `gsd-statusline.js` (119 lines) - Renders context usage and task status
|
|
- `gsd-context-monitor.js` (156 lines) - Warns when context window is low
|
|
- `gsd-workflow-guard.js` (94 lines) - Advises on workflow compliance
|
|
- `gsd-check-update.js` (114 lines) - Checks for GSD updates in background
|
|
|
|
## Manual Testing Indicators
|
|
|
|
**Integration Points (evidence of real-world testing):**
|
|
- Comments referencing specific issues: `See #775`, `See #1162`, `See #870`, `See #884`
|
|
- Platform-specific workarounds: `windowsHide: true` for child_process to prevent console flash on Windows
|
|
- Timeout guards: `const stdinTimeout = setTimeout(() => process.exit(0), 3000);` prevents hanging on pipe issues
|
|
- Git Bash compatibility: explicit handling of stdin timeout on Windows Git Bash
|
|
|
|
**Behavioral Validation Patterns:**
|
|
- Config file validation: reads `.planning/config.json`, catches parse errors gracefully
|
|
- File existence checks before operations: `if (fs.existsSync(filePath))`
|
|
- Stale data detection: `if ((now - metrics.timestamp) > STALE_SECONDS)`
|
|
- Severity escalation tracking: debounce counter resets on warning level change
|
|
|
|
## Input Validation
|
|
|
|
**JSON Parsing with Error Handling:**
|
|
All hooks follow this pattern (example from `gsd-prompt-guard.js`):
|
|
```javascript
|
|
let input = '';
|
|
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
|
|
process.stdin.setEncoding('utf8');
|
|
process.stdin.on('data', chunk => input += chunk);
|
|
process.stdin.on('end', () => {
|
|
clearTimeout(stdinTimeout);
|
|
try {
|
|
const data = JSON.parse(input);
|
|
// Process data
|
|
} catch {
|
|
// Silent fail — never block tool execution
|
|
process.exit(0);
|
|
}
|
|
});
|
|
```
|
|
|
|
**Defensive Checks:**
|
|
- Field existence: `const toolName = data.tool_name;` then `if (toolName !== 'Write' && toolName !== 'Edit')`
|
|
- Optional chaining: `data.tool_input?.file_path || ''`
|
|
- Null checks: `if (!sessionId) { process.exit(0); }`
|
|
- Default values: `data.cwd || process.cwd()`, `data.model?.display_name || 'Claude'`
|
|
|
|
## File I/O Testing
|
|
|
|
**Patterns for Reliability:**
|
|
- Synchronous I/O ensures order: `fs.readFileSync()` → process → `fs.writeFileSync()`
|
|
- Directory existence checked before writing: `if (!fs.existsSync(cacheDir)) { fs.mkdirSync(cacheDir, { recursive: true }); }`
|
|
- Try-catch wraps all file operations that could fail:
|
|
```javascript
|
|
try {
|
|
const bridgeData = JSON.stringify({ ... });
|
|
fs.writeFileSync(bridgePath, bridgeData);
|
|
} catch (e) {
|
|
// Silent fail -- bridge is best-effort, don't break statusline
|
|
}
|
|
```
|
|
|
|
## State Machine / Behavior Testing
|
|
|
|
**Context Monitor Debounce Logic** (`gsd-context-monitor.js`):
|
|
- Tracks warning state in file: `/tmp/claude-ctx-{session_id}-warned.json`
|
|
- Debounce counter incremented: `warnData.callsSinceWarn = (warnData.callsSinceWarn || 0) + 1`
|
|
- Severity escalation bypasses debounce: `if (severityEscalated) { // emit immediately }`
|
|
- State reset on warn: `warnData.callsSinceWarn = 0`
|
|
|
|
This pattern validates behavior without formal tests:
|
|
```javascript
|
|
let warnData = { callsSinceWarn: 0, lastLevel: null };
|
|
const isCritical = remaining <= CRITICAL_THRESHOLD;
|
|
const currentLevel = isCritical ? 'critical' : 'warning';
|
|
const severityEscalated = currentLevel === 'critical' && warnData.lastLevel === 'warning';
|
|
if (!firstWarn && warnData.callsSinceWarn < DEBOUNCE_CALLS && !severityEscalated) {
|
|
process.exit(0); // Suppress warning
|
|
}
|
|
```
|
|
|
|
## Regex Pattern Testing
|
|
|
|
**Prompt Injection Detection** (`gsd-prompt-guard.js`):
|
|
Patterns tested against content without formal unit tests:
|
|
```javascript
|
|
const INJECTION_PATTERNS = [
|
|
/ignore\s+(all\s+)?previous\s+instructions/i,
|
|
/override\s+(system|previous)\s+(prompt|instructions)/i,
|
|
/you\s+are\s+now\s+(?:a|an|the)\s+/i,
|
|
/(?:print|output|reveal|show|display|repeat)\s+(?:your\s+)?(?:system\s+)?(?:prompt|instructions)/i,
|
|
/\[SYSTEM\]/i,
|
|
/<<\s*SYS\s*>>/i,
|
|
];
|
|
for (const pattern of INJECTION_PATTERNS) {
|
|
if (pattern.test(content)) {
|
|
findings.push(pattern.source);
|
|
}
|
|
}
|
|
```
|
|
|
|
Unicode detection without regex: `if (/[\u200B-\u200F\u2028-\u202F\uFEFF\u00AD]/.test(content))`
|
|
|
|
## Environment & Configuration Testing
|
|
|
|
**Configuration Loading Safety** (all hooks):
|
|
- Try-catch with silent fail:
|
|
```javascript
|
|
const configPath = path.join(cwd, '.planning', 'config.json');
|
|
if (fs.existsSync(configPath)) {
|
|
try {
|
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
if (config.hooks?.context_warnings === false) {
|
|
process.exit(0); // Feature disabled
|
|
}
|
|
} catch (e) {
|
|
// Ignore config parse errors
|
|
}
|
|
}
|
|
```
|
|
- Optional chaining for nested config: `config.hooks?.workflow_guard`, `config.hooks?.context_warnings`
|
|
|
|
**Environment Variable Access:**
|
|
```javascript
|
|
const envDir = process.env.CLAUDE_CONFIG_DIR;
|
|
if (envDir && fs.existsSync(path.join(envDir, 'get-shit-done', 'VERSION'))) {
|
|
return envDir; // Custom config dir detected
|
|
}
|
|
```
|
|
|
|
## Performance & Resource Management
|
|
|
|
**Timeout Guards (prevent resource leaks):**
|
|
- All hooks implement stdin timeout: `const stdinTimeout = setTimeout(() => process.exit(0), 3000);`
|
|
- Longer timeout for high-volume operations: `const stdinTimeout = setTimeout(() => process.exit(0), 10000);` in context-monitor
|
|
- Always cleared before processing: `clearTimeout(stdinTimeout);`
|
|
|
|
**Background Process Management** (`gsd-check-update.js`):
|
|
- Child process spawned with `stdio: 'ignore'`: doesn't inherit parent's stdio
|
|
- Process detached on Windows: `detached: true` (required for proper cleanup)
|
|
- Parent calls `child.unref()`: parent doesn't wait for child to exit
|
|
|
|
```javascript
|
|
const child = spawn(process.execPath, ['-e', `...inline script...`], {
|
|
stdio: 'ignore',
|
|
windowsHide: true,
|
|
detached: true
|
|
});
|
|
child.unref();
|
|
```
|
|
|
|
## Test Coverage Gaps
|
|
|
|
**Areas Without Formal Testing:**
|
|
1. **Regex Pattern Accuracy** - Injection patterns untested against false positives/negatives
|
|
2. **Debounce Counter Edge Cases** - Corruption recovery, counter reset logic
|
|
3. **Platform-Specific Behavior** - Windows vs Linux path handling, process detachment
|
|
4. **Concurrent Access** - Multiple hooks writing to same state files simultaneously
|
|
5. **Large Input Handling** - No tests for multi-megabyte JSON on stdin
|
|
6. **Stale File Cleanup** - No validation that temp files are properly removed
|
|
7. **Spawn Child Behavior** - Background update check success/failure not validated
|
|
|
|
## Recommendation for Testing
|
|
|
|
**Given the architecture (CLI hooks, not library code):**
|
|
- Integration testing more valuable than unit tests
|
|
- Manual testing via real Claude Code sessions is current primary validation
|
|
- Would recommend:
|
|
1. Snapshot tests for JSON output structure
|
|
2. Mock file system for configuration loading paths
|
|
3. Integration tests simulating tool use hook flow
|
|
4. Platform-specific testing (Windows, macOS, Linux) for path handling
|
|
|
|
**Lack of tests is acceptable for:**
|
|
- Simple stdin/stdout data transformation scripts
|
|
- Hooks deployed once per installation (not on every tool call)
|
|
- Silent-fail-safe design (errors don't break workflows)
|
|
- Real-world testing via 50+ GitHub issues and fixes
|
|
|
|
---
|
|
|
|
*Testing analysis: 2026-03-27*
|