paper-test
Use when testing code through mental execution - trace code line-by-line with concrete input values to find bugs, logic errors, missing code, edge cases, and contract violations before deployment
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install camoa-claude-skills-paper-test
Repository
Skill path: code-paper-test/skills/paper-test
Use when testing code through mental execution - trace code line-by-line with concrete input values to find bugs, logic errors, missing code, edge cases, and contract violations before deployment
Open repositoryBest for
Primary workflow: Run DevOps.
Technical facets: Full Stack, DevOps, Testing.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: camoa.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install paper-test into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/camoa/claude-skills before adding paper-test to shared team environments
- Use paper-test for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: paper-test
description: Use when testing code through mental execution - trace code line-by-line with concrete input values to find bugs, logic errors, missing code, edge cases, and contract violations before deployment
version: 0.1.0
---
# Paper Test
Systematically test code by mentally executing it line-by-line with concrete values.
## When to Use
- "Paper test this code" / "Trace this code" / "Test without running"
- "Find bugs in this code" / "Check for edge cases"
- "Validate this implementation" / "Review this logic"
- Before deploying changes
- Debugging without a debugger
- Reviewing unfamiliar or AI-generated code
- Validating complex logic (loops, conditionals, recursion)
## Method
Follow code logic with concrete test cases to find:
1. **Potential issues** - Bugs, wrong assumptions, edge cases
2. **Missing code** - What's needed but not written to achieve intent
NOT just reading - actually run the code in your head with real values.
## Critical: How AI Should Verify
**NEVER assume or guess** - Use your tools to verify every claim:
### Verifying External Dependencies
When code calls external methods/services:
1. **Use Read tool** to check actual source files:
```
Read: src/Service/UserService.php
→ Find loadByEmail() method
→ Note: Returns User|null, throws InvalidArgumentException if empty
```
2. **Use Grep tool** to find method definitions:
```
Grep: "public function loadByEmail" in src/
→ Verify method exists and signature
```
3. **Check interfaces** for injected services:
```
Read: vendor/.../LoggerInterface.php
→ Verify info() method signature
```
**DO NOT** write "Assume method exists" - Actually verify or mark as UNVERIFIED RISK.
### Verifying Code Contracts
When code has relationships (extends, implements, uses):
1. **Read parent/base classes**:
```
Read: src/Plugin/ActionBase.php
→ Check for abstract methods
→ Verify parent constructor signature
```
2. **Read interfaces**:
```
Read: src/Handler/HandlerInterface.php
→ List all required methods
→ Note exact signatures
```
3. **Check service definitions**:
```
Read: config/services.yml
Read: modulename.services.yml
→ Verify service ID exists
→ Check tags if using service collectors
```
### When You Cannot Verify
If source is unavailable (external package, closed-source):
```
DEPENDENCY CHECK: $externalApi->fetchData()
VERIFICATION: Unable to read source
RISK: Cannot verify method exists or return type
RECOMMENDATION: Add runtime checks for null/exceptions
```
Mark as risk - don't assume it works.
---
## Quick Reference
| Test Type | When | Output |
|-----------|------|--------|
| Happy Path | First test | Verify correct flow |
| Edge Cases | After happy path | Find boundary issues |
| Error Cases | Last | Verify error handling |
| Contract Verification | Always | Check dependencies |
---
## Paper Testing Workflow
When user provides code to test:
### Step 1: Define Test Scenarios
Pick concrete input values. Start with happy path, then edge cases.
```
SCENARIO: [Description of what we're testing]
INPUT:
$variable1 = [concrete value]
$variable2 = [concrete value]
[initial state]
```
### Step 2: Trace Line by Line
Follow each line. Write the variable state after execution.
```
Line [N]: [code statement]
→ [variable] = [new value]
→ [state change description]
```
### Step 3: Follow Every Branch
At each conditional, note which branch is taken and why.
```
Line [N]: if ([condition])
→ [variable1]=[value], [variable2]=[value]
→ [evaluation] = [true/false]
→ TAKES: [if branch / else branch] (lines X-Y)
```
### Step 4: Track Loop Iterations
For loops, trace EACH iteration with index and values.
```
Line [N]: foreach ([collection] as [item])
Iteration 1: $key=[value], $item=[value]
Line [N+1]: [statement]
→ [state change]
Iteration 2: $key=[value], $item=[value]
Line [N+1]: [statement]
→ [state change]
Loop ends. Final state: [describe]
```
### Step 5: Verify External Dependencies
For EVERY external call (methods, services, APIs), verify:
```
DEPENDENCY CHECK: [service/method name]
Location: [file path or interface]
Method signature: [actual signature]
Returns: [actual return type and values]
Throws: [exceptions, when]
Side effects: [what it modifies]
VERIFICATION:
- [ ] Method exists
- [ ] Parameters correct (type, order)
- [ ] Return type handled correctly
- [ ] Edge cases considered
```
**DO NOT ASSUME** - Read the actual source code or documentation.
### Step 6: Verify Code Contracts
For classes with relationships (extends, implements, uses, injects):
```
CONTRACT VERIFICATION: [Class name]
Extends: [Parent class]
- [ ] All abstract methods implemented
- [ ] Parent constructor called (if required)
- [ ] Parent methods called when needed
Implements: [Interface]
- [ ] All interface methods present
- [ ] Signatures match exactly
- [ ] Return types correct
Injected Services:
- [ ] Service exists in container
- [ ] Interface methods verified
- [ ] Return types handled
Tagged Service (if applicable):
- [ ] Tag name matches collector
- [ ] Implements required interface
- [ ] Priority appropriate
```
See reference guide for complete contract patterns.
### Step 7: Note Output and Flaws
```
OUTPUT:
Return value: [what's returned]
Side effects: [database changes, API calls, etc.]
State changes: [session, cache, variables]
FLAWS FOUND:
- Line [N]: [description of issue]
FIX: [how to resolve]
- Line [N]: [description of issue]
FIX: [how to resolve]
```
---
## What to Look For
### Logic Errors
- Wrong comparison (`=` vs `==` vs `===`)
- Inverted condition (`if ($x)` should be `if (!$x)`)
- Off-by-one in loops
- Missing break in switch
### Null/Undefined Access
- Accessing property on null object
- Array key that might not exist
- Uninitialized variable
### Edge Cases
- Empty array/string
- Zero, negative numbers
- Null values
- Very large inputs
### State Issues
- Variable modified but not used
- Variable used before assignment
- Stale data from previous iteration
### Flow Issues
- Unreachable code
- Missing return statement
- Early return skips cleanup
### AI Code Issues
- Invented methods that don't exist
- Wrong return types assumed
- Wrong parameter order
- Mixed API versions
- Assumed service existence
- Wrong namespace imports
---
## Testing Strategy for Modules
For modules with multiple components (ECA plugins, form systems, etc.), use coverage-driven hybrid approach.
### Two Levels
**Flow-based**: Real user workflows end-to-end
- Tests integration and data handoffs
- Catches format mismatches, token issues
**Component**: Each component with edge cases
- Tests individual logic thoroughly
- Catches implementation bugs, null handling
### Coverage Method
```
Step 1: Map all components
- List every event, condition, action, service
Step 2: Design flows covering all components
- Each component in at least one flow
- 3-5 flows typically cover a module
Step 3: Add component edge cases
- For each component: scenarios NOT in flows
- Error cases, empty inputs, boundaries
- 2-4 edge cases per component
```
---
## Output Format
Use this template for all paper tests:
```
PAPER TEST: [File/Function name]
SCENARIO: [Description]
INPUT:
[variable] = [value]
[variable] = [value]
TRACE:
Line [N]: [code]
→ [variable] = [new value]
Line [N]: [conditional]
→ [evaluation] = [result]
→ TAKES: [branch]
Line [N]: [loop start]
Iteration [N]: [values]
Line [N]: [code]
→ [state]
OUTPUT:
Return: [value]
Side effects: [list]
State changes: [list]
DEPENDENCY CHECKS:
[method/service]: VERIFIED / ISSUE FOUND
Issue: [description]
CONTRACT CHECKS:
[pattern]: VERIFIED / VIOLATION
Issue: [description]
FLAWS FOUND:
- [Line N]: [issue]
FIX: [solution]
- [Line N]: [issue]
FIX: [solution]
EDGE CASES TO TEST:
1. [scenario]
2. [scenario]
```
---
## References
All detailed guides are in `references/` directory:
- `references/core-method.md` - Complete paper testing method
- `references/dependency-verification.md` - How to verify external calls
- `references/contract-patterns.md` - All code contract types
- `references/ai-code-auditing.md` - Testing AI-generated code
- `references/hybrid-testing.md` - Module-level testing strategy
- `references/common-flaws.md` - Catalog of frequent bugs
- `references/advanced-techniques.md` - Progressive injects, red team testing, attack surface analysis, AAR format
---
## Example
```php
function getDiscount($total, $coupon) {
if ($coupon == 'SAVE10') {
$discount = $total * 0.10;
}
if ($coupon == 'SAVE20') {
$discount = $total * 0.20;
}
return $discount;
}
```
Paper test:
```
SCENARIO: No coupon provided
INPUT: $total = 100, $coupon = null
TRACE:
Line 2: if ($coupon == 'SAVE10')
→ null == 'SAVE10' = false
→ SKIP
Line 5: if ($coupon == 'SAVE20')
→ null == 'SAVE20' = false
→ SKIP
Line 8: return $discount
→ $discount is UNDEFINED
OUTPUT:
Return: PHP Warning - undefined variable
FLAWS FOUND:
- Line 8: Returns undefined variable when no coupon matches
FIX: Initialize $discount = 0 at start of function
```
---
## Progressive Disclosure
The SKILL.md provides the core workflow. For detailed guidance:
- Complete methodology → `references/core-method.md`
- Dependency verification patterns → `references/dependency-verification.md`
- Contract verification (extends, implements, DI, plugins, etc.) → `references/contract-patterns.md`
- AI code specific checks → `references/ai-code-auditing.md`
- Module testing strategy → `references/hybrid-testing.md`
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/core-method.md
```markdown
# Core Paper Testing Method
Complete methodology for testing code through mental execution.
## Contents
- [Purpose](#purpose)
- [Method Overview](#method-overview)
- [Step-by-Step Process](#step-by-step-process)
- [Scenario Selection](#scenario-selection)
- [Line-by-Line Tracing](#line-by-line-tracing)
- [Branch Handling](#branch-handling)
- [Loop Iteration](#loop-iteration)
- [Output Documentation](#output-documentation)
---
## Purpose
Follow code logic to find:
1. **Potential issues** - Bugs, wrong assumptions, edge cases
2. **Missing code** - What's needed but not written to achieve the intent
## Method Overview
- **Mentally execute** the code with concrete test cases (happy path, edge cases, errors)
- **Trace variable state** at each step
- **Verify external dependencies** - don't assume, confirm methods exist and behave as expected
Not just reading - actually running the code in your head with real values.
---
## When to Use
- Before deploying changes
- Debugging without a debugger
- Reviewing unfamiliar code
- Validating complex logic (loops, conditionals, recursion)
- Finding edge cases
- Auditing AI-generated code
---
## Step-by-Step Process
### 1. Define Test Scenario
Pick concrete input values. Start with the happy path, then edge cases.
```
SCENARIO: User submits form with email "[email protected]"
INPUT:
$email = "[email protected]"
$user = null
```
### 2. Trace Line by Line
Follow each line. Write the variable state after execution.
```
Line 10: $email = trim($email)
→ $email = "[email protected]"
Line 11: if (empty($email))
→ false, skip to line 15
Line 15: $user = loadUserByEmail($email)
→ assume returns User object with id=42
Line 16: if (!$user)
→ false (user exists), skip to line 20
Line 20: return $user->id
→ returns 42
```
### 3. Follow Every Branch
At each conditional, note which branch is taken and why.
```
Line 25: if ($count > 0 && $enabled)
→ $count=3, $enabled=true
→ true && true = true
→ TAKES: if branch (lines 26-30)
```
### 4. Track Loop Iterations
For loops, trace each iteration with index and values.
```
Line 30: foreach ($items as $key => $item)
Iteration 1: $key=0, $item="apple"
Line 31: $result[] = strtoupper($item)
→ $result = ["APPLE"]
Iteration 2: $key=1, $item="banana"
Line 31: $result[] = strtoupper($item)
→ $result = ["APPLE", "BANANA"]
Loop ends. $result = ["APPLE", "BANANA"]
```
### 5. Note the Output
What is returned? What state changed? What side effects occurred?
```
OUTPUT:
Return value: 42
Side effects: None
Token/Session changes: None
```
---
## Scenario Selection
### Happy Path (Test 1)
The expected, normal case.
```
SCENARIO: Valid user login
INPUT:
$username = "john_doe"
$password = "correct_password"
User exists in database with matching credentials
```
### Edge Cases (Tests 2-N)
Boundary conditions and special cases.
```
SCENARIO: Empty input
INPUT:
$username = ""
$password = ""
SCENARIO: Very long input
INPUT:
$username = [string of 1000 characters]
$password = [string of 1000 characters]
SCENARIO: Special characters
INPUT:
$username = "[email protected]"
$password = "p@$$w0rd!"
```
### Error Cases (Final tests)
Things that should fail gracefully.
```
SCENARIO: User not found
INPUT:
$username = "nonexistent"
$password = "anything"
User does NOT exist in database
SCENARIO: Wrong password
INPUT:
$username = "john_doe"
$password = "wrong_password"
User exists but password doesn't match
```
---
## Line-by-Line Tracing
### Simple Assignments
```
Line 5: $total = $price * $quantity
→ $price = 10, $quantity = 3
→ $total = 30
```
### Method Calls
```
Line 10: $result = $this->calculate($value)
→ $value = 100
→ [Need to verify what calculate() does]
→ Assume returns: $result = 120
```
**CRITICAL**: Don't guess what methods do - verify them (see dependency-verification.md).
### Property Access
```
Line 15: $name = $user->getName()
→ $user = User{id: 1, name: "John"}
→ $name = "John"
```
### Conditionals
```
Line 20: if ($x > 5 && $y < 10)
→ $x = 7, $y = 3
→ 7 > 5 = true, 3 < 10 = true
→ true && true = true
→ TAKES if branch (lines 21-25)
```
---
## Branch Handling
### If/Else
```
Line 10: if ($status === 'active')
→ $status = 'inactive'
→ false
→ TAKES else branch (line 15)
Line 15: $message = "User is not active"
→ $message = "User is not active"
```
### Switch
```
Line 20: switch ($type)
→ $type = 'premium'
Line 21: case 'free':
→ no match, skip
Line 23: case 'premium':
→ MATCH, execute lines 24-25
Line 24: $price = 99.99
→ $price = 99.99
Line 25: break
→ EXIT switch
```
### Ternary
```
Line 30: $discount = $isPremium ? 0.20 : 0.10
→ $isPremium = true
→ takes first value
→ $discount = 0.20
```
---
## Loop Iteration
### Foreach
```
Line 40: foreach ($users as $user)
→ $users = [User{id:1}, User{id:2}]
Iteration 1:
$user = User{id: 1, name: "Alice"}
Line 41: $names[] = $user->name
→ $names = ["Alice"]
Iteration 2:
$user = User{id: 2, name: "Bob"}
Line 41: $names[] = $user->name
→ $names = ["Alice", "Bob"]
Loop ends.
Final state: $names = ["Alice", "Bob"]
```
### For
```
Line 50: for ($i = 0; $i < 3; $i++)
Iteration 1:
$i = 0
Line 51: $sum += $i
→ $sum = 0 (was 0)
Increment: $i = 1
Iteration 2:
$i = 1
Line 51: $sum += $i
→ $sum = 1 (was 0)
Increment: $i = 2
Iteration 3:
$i = 2
Line 51: $sum += $i
→ $sum = 3 (was 1)
Increment: $i = 3
Check condition: $i < 3 → 3 < 3 = false
Loop ends. $sum = 3
```
### While
```
Line 60: while ($count < 10)
Check: $count = 0, 0 < 10 = true, ENTER loop
Iteration 1:
Line 61: $count += 2
→ $count = 2
Check: 2 < 10 = true, CONTINUE
Iteration 2:
Line 61: $count += 2
→ $count = 4
Check: 4 < 10 = true, CONTINUE
...
Iteration 5:
Line 61: $count += 2
→ $count = 10
Check: 10 < 10 = false, EXIT
Final: $count = 10
```
---
## Output Documentation
### Return Values
```
OUTPUT:
Return value: [value and type]
Return path: [which return statement, line number]
```
### Side Effects
```
OUTPUT:
Side effects:
- Database: INSERT into users (id: 123)
- API call: POST to /api/notify
- File system: Created /tmp/cache_abc.json
- Email: Sent to [email protected]
```
### State Changes
```
OUTPUT:
State changes:
- Session: Set 'user_id' = 42
- Cache: Stored 'user_42_profile'
- Global: $GLOBALS['last_login'] = timestamp
- Object: $this->isAuthenticated = true
```
---
## Complete Example
```php
function calculatePrice($items, $couponCode = null) {
$total = 0;
foreach ($items as $item) {
$total += $item['price'] * $item['quantity'];
}
if ($couponCode === 'SAVE20') {
$total *= 0.8;
}
return $total;
}
```
**Paper Test:**
```
SCENARIO: Two items with 20% coupon
INPUT:
$items = [
['price' => 10, 'quantity' => 2],
['price' => 5, 'quantity' => 3]
]
$couponCode = 'SAVE20'
TRACE:
Line 2: $total = 0
→ $total = 0
Line 3: foreach ($items as $item)
→ 2 items to process
Iteration 1:
$item = ['price' => 10, 'quantity' => 2]
Line 4: $total += $item['price'] * $item['quantity']
→ $total += 10 * 2
→ $total = 20
Iteration 2:
$item = ['price' => 5, 'quantity' => 3]
Line 4: $total += $item['price'] * $item['quantity']
→ $total += 5 * 3
→ $total = 35 (was 20)
Loop ends.
Line 7: if ($couponCode === 'SAVE20')
→ 'SAVE20' === 'SAVE20' = true
→ TAKES if branch (line 8)
Line 8: $total *= 0.8
→ $total = 35 * 0.8
→ $total = 28
Line 11: return $total
→ returns 28
OUTPUT:
Return value: 28
Side effects: None
State changes: None
FLAWS FOUND:
None - logic is correct
```
**Edge Case Test:**
```
SCENARIO: Empty items array
INPUT:
$items = []
$couponCode = null
TRACE:
Line 2: $total = 0
→ $total = 0
Line 3: foreach ($items as $item)
→ 0 items, loop never executes
Line 7: if ($couponCode === 'SAVE20')
→ null === 'SAVE20' = false
→ SKIP if branch
Line 11: return $total
→ returns 0
OUTPUT:
Return value: 0
Side effects: None
FLAWS FOUND:
None - handles empty array correctly
```
```
### references/dependency-verification.md
```markdown
# External Dependency Verification
How to verify external calls during paper testing - don't assume, know.
## Contents
- [The Problem with Assumptions](#the-problem-with-assumptions)
- [What to Do](#what-to-do)
- [Common Dependency Questions](#common-dependency-questions)
- [Verification Methods](#verification-methods)
- [Dependency Types](#dependency-types)
- [Example - Dependency Bug](#example---dependency-bug)
---
## The Problem with Assumptions
When you hit a function call, API request, database query, or library method - **stop and understand what it actually does**.
```php
Line 15: $user = $userService->loadByEmail($email);
→ assume returns User object
```
This assumption could be wrong. The method might:
- Return `null` when not found
- Throw an exception when not found
- Return an empty array
- Return a different object type
**Paper testing rule:** For EVERY external call, verify actual behavior - don't guess.
---
## What to Do
### Option 1: Read the Source
Look at the actual implementation. Know the real behavior.
```php
// Checked UserService::loadByEmail()
// - Returns User object if found
// - Returns NULL if not found (does not throw)
// - Throws InvalidArgumentException if email is empty
Line 15: $user = $userService->loadByEmail($email);
→ KNOWN: returns User|null, throws on empty email
→ $email = "[email protected]" (not empty)
→ assume found: $user = User{id: 42}
```
### Option 2: Read the Documentation
If source isn't available, check official docs.
```
Line 15: $user = $userService->loadByEmail($email);
→ DOCS: "Returns User object or null if not found"
→ VERIFIED: Safe to assume User|null
```
### Option 3: Mark as Unknown Risk
If you can't verify, flag it as a potential flaw source.
```php
Line 15: $user = $userService->loadByEmail($email);
→ UNKNOWN: Need to verify return type when not found
→ RISK: Line 16 assumes $user is object, might be null
```
---
## Common Dependency Questions
Ask these for EVERY external call:
| Question | Why It Matters |
|----------|----------------|
| What does it return on success? | Know the exact type and structure |
| What does it return on "not found"? | null? empty array? false? |
| Does it throw exceptions? When? | Uncaught exceptions break flow |
| Does it have side effects? | Modifies database? Sends email? |
| Is it synchronous or async? | Might not have result immediately |
| What are the edge cases? | Empty input? Special characters? |
---
## Verification Methods
### Method 1: Read Implementation
```
DEPENDENCY CHECK: UserService::loadByEmail()
SOURCE REVIEW:
File: src/Service/UserService.php:42
Implementation:
public function loadByEmail(string $email): ?User {
if (empty($email)) {
throw new InvalidArgumentException('Email cannot be empty');
}
$user = $this->repository->findOneBy(['email' => $email]);
return $user; // returns User or null
}
VERIFIED BEHAVIOR:
- Returns: User object if found
- Returns: null if not found
- Throws: InvalidArgumentException if empty string
- Side effects: None (read-only query)
```
### Method 2: Read Interface/Contract
```
DEPENDENCY CHECK: LoggerInterface::info()
INTERFACE REVIEW:
File: vendor/psr/log/src/LoggerInterface.php:25
Signature:
public function info(string $message, array $context = []): void
Documentation:
"Logs informational message. Does not throw exceptions."
VERIFIED BEHAVIOR:
- Parameter 1: string $message (required)
- Parameter 2: array $context (optional, defaults to [])
- Returns: void (no return value)
- Throws: None (guaranteed not to throw)
```
### Method 3: Read Documentation
```
DEPENDENCY CHECK: EntityTypeManagerInterface::getStorage()
OFFICIAL DOCS:
URL: api.drupal.org/EntityTypeManagerInterface::getStorage
Signature:
public function getStorage(string $entity_type): EntityStorageInterface
Documentation:
"Gets the storage handler for an entity type."
"Throws InvalidPluginDefinitionException if storage handler cannot be loaded."
"Throws PluginNotFoundException if entity type does not exist."
VERIFIED BEHAVIOR:
- Returns: EntityStorageInterface object (always)
- Throws: InvalidPluginDefinitionException
- Throws: PluginNotFoundException
- Never returns null
```
### Method 4: Test/Inspect
If source and docs unavailable:
```
DEPENDENCY CHECK: $externalApi->fetchData()
INSPECTION METHOD:
- Wrote test calling fetchData() with various inputs
- Observed: Returns array on success
- Observed: Returns empty array on not found
- Observed: Throws HttpException on network error
VERIFIED BEHAVIOR:
- Returns: array (always, never null)
- Empty array: when no results
- Throws: HttpException on failure
```
---
## Dependency Types
### Service Methods
```php
Line 10: $entity = $this->entityTypeManager->getStorage('node')->load($id);
```
**What to verify:**
- `getStorage()` - returns what? throws what?
- `load()` - returns what when ID doesn't exist?
**Verification:**
```
DEPENDENCY: EntityTypeManagerInterface::getStorage()
Returns: EntityStorageInterface
Throws: InvalidPluginDefinitionException, PluginNotFoundException
DEPENDENCY: EntityStorageInterface::load()
Returns: EntityInterface|null
Returns null when: ID doesn't exist
Throws: None
```
### Database Queries
```php
Line 15: $user = $connection->query('SELECT * FROM users WHERE id = :id', [':id' => $id])->fetchObject();
```
**What to verify:**
- Does `query()` return a result object even if no rows?
- Does `fetchObject()` return null or false when no rows?
**Verification:**
```
DEPENDENCY: Connection::query()
Returns: StatementInterface (always, even if 0 rows)
Throws: DatabaseExceptionWrapper on SQL error
DEPENDENCY: StatementInterface::fetchObject()
Returns: stdClass object if row exists
Returns: FALSE if no rows (not null!)
Note: Check with === false, not !
```
### API Calls
```php
Line 20: $response = $httpClient->get('https://api.example.com/users/' . $id);
```
**What to verify:**
- Does it throw on 404?
- What object does it return?
- What properties/methods on response object?
**Verification:**
```
DEPENDENCY: HttpClient::get()
Returns: Response object on 2xx status
Throws: ClientException on 4xx status (including 404)
Throws: ServerException on 5xx status
Throws: NetworkException on connection failure
DEPENDENCY: Response object
Methods:
- getStatusCode(): int
- getBody(): StreamInterface
- getBody()->getContents(): string (JSON or text)
```
### Framework/Library Functions
```php
Line 25: $decoded = json_decode($json, true);
```
**What to verify:**
- What does it return on malformed JSON?
- Does second parameter affect return type?
**Verification:**
```
DEPENDENCY: json_decode()
Signature: json_decode(string $json, bool $assoc = false, ...)
Returns when $assoc = true:
- array if valid JSON object/array
- null if malformed JSON
- null if input is "null"
Returns when $assoc = false:
- stdClass object if valid JSON object
- array if valid JSON array
- null if malformed JSON
Note: Cannot distinguish malformed from valid "null" without json_last_error()
```
### Plugin/Service Methods
```php
Line 30: $result = $this->myPlugin->process($data);
```
**What to verify:**
- Read the plugin base class or interface
- What does `process()` return?
- What parameters does it expect?
**Verification:**
```
DEPENDENCY: MyPluginInterface::process()
Interface location: src/Plugin/MyPluginInterface.php
Signature: public function process(array $data): ProcessResult
Returns: ProcessResult object (always)
Throws: ProcessException if $data invalid
Side effects: May write to cache
ProcessResult methods:
- isSuccess(): bool
- getData(): array
- getErrors(): array
```
---
## Dependency Types
### 1. Internal Methods (Same Class)
```php
Line 5: $result = $this->calculateTotal($items);
```
**Verify:**
- Read `calculateTotal()` method in same class
- Check return type, exceptions, side effects
### 2. Injected Services
```php
Line 10: $user = $this->userService->loadByEmail($email);
```
**Verify:**
- Find service interface in constructor
- Read interface for method signature
- Read implementation if interface unclear
### 3. Static/Global Functions
```php
Line 15: $config = \Drupal::config('my_module.settings');
```
**Verify:**
- Read framework documentation
- Check what object type is returned
- Check what methods available on returned object
### 4. Database/ORM
```php
Line 20: $entity = Node::load($id);
```
**Verify:**
- Check ORM documentation
- Returns object or null?
- Throws exceptions?
### 5. External APIs
```php
Line 25: $data = $client->fetchData($endpoint);
```
**Verify:**
- API documentation
- Response structure
- Error handling (exceptions, status codes, null)
### 6. Library/Package Methods
```php
Line 30: $hash = password_hash($password, PASSWORD_BCRYPT);
```
**Verify:**
- PHP/library documentation
- Return type
- Failure modes (returns false? throws?)
---
## Example - Dependency Bug
```php
function findActiveUsers($emails) {
$users = [];
foreach ($emails as $email) {
$user = $this->repository->findByEmail($email);
if ($user->isActive()) { // BUG: assumes $user is object
$users[] = $user;
}
}
return $users;
}
```
Paper test with dependency verification:
```
SCENARIO: One email doesn't exist in database
INPUT: $emails = ["[email protected]", "[email protected]"]
DEPENDENCY CHECK: repository->findByEmail()
File: src/Repository/UserRepository.php:50
Signature: public function findByEmail(string $email): ?User
Returns: User if found, NULL if not found
Throws: None
TRACE:
Iteration 1: $email = "[email protected]"
Line 4: $user = repository->findByEmail("[email protected]")
→ VERIFIED: returns User{id: 1, active: true}
Line 5: if ($user->isActive())
→ true, adds to $users
Iteration 2: $email = "[email protected]"
Line 4: $user = repository->findByEmail("[email protected]")
→ VERIFIED: returns NULL (not found)
Line 5: if ($user->isActive())
→ FATAL ERROR: Calling isActive() on null
OUTPUT:
Fatal error on line 5, iteration 2
FLAW FOUND:
Line 5: No null check before calling method on $user
FIX: Add null check:
if ($user && $user->isActive()) {
```
**Without checking what `findByEmail` returns, this bug is invisible.**
---
## Verification Template
Use this for every external call:
```
DEPENDENCY CHECK: [ServiceName::methodName()]
LOCATION:
File: [path to file]
Line: [line number]
Signature: [full method signature]
BEHAVIOR:
Returns on success: [type and value]
Returns on failure: [null | false | empty array | etc.]
Throws: [exception types and when]
Side effects: [database writes | API calls | cache | etc.]
EDGE CASES:
- Empty input: [behavior]
- Null input: [behavior]
- Invalid input: [behavior]
VERIFICATION METHOD:
[ ] Read source code
[ ] Read interface/docs
[ ] Tested manually
[ ] Unable to verify (mark as RISK)
USAGE IN CODE:
Line [N]: [how it's used]
Handles null: [YES/NO]
Handles exceptions: [YES/NO]
Assumes: [what assumptions made]
```
---
## AI-Generated Code Risks
AI models often invent methods that don't exist. Always verify:
**Common AI inventions:**
- Methods with plausible names that don't exist
- Wrong parameter order (common in similar APIs)
- Wrong return types
- Mixing different framework versions
**Example:**
```php
// AI might generate:
$user = $userService->getByEmail($email); // Method doesn't exist!
// Actual method:
$user = $userService->loadByEmail($email); // Correct method name
```
**Protection:** Verify EVERY external method exists and has correct signature.
---
## Quick Checklist
For every external call in the code:
- [ ] What does it return on success?
- [ ] What does it return on failure/not found?
- [ ] Does it throw exceptions? Which ones?
- [ ] What are the side effects?
- [ ] Are parameter types correct?
- [ ] Is return type handled correctly in my code?
- [ ] Are edge cases (null, empty, invalid) handled?
**If you can't answer these, the code is untested.**
```
### references/contract-patterns.md
```markdown
# Code Contract Patterns
Comprehensive verification templates for all code relationship patterns.
## Contents
- [Pattern 1: Inheritance](#pattern-1-inheritance)
- [Pattern 2: Plugin System](#pattern-2-plugin-system)
- [Pattern 3: Service/Dependency Injection](#pattern-3-servicedependency-injection)
- [Pattern 4: Interface Implementation](#pattern-4-interface-implementation)
- [Pattern 5: Traits](#pattern-5-traits)
- [Pattern 6: Event/Hook Systems](#pattern-6-eventhook-systems)
- [Pattern 7: Middleware/Decorators](#pattern-7-middlewaredecorators)
- [Pattern 8: Service Collectors / Tagged Services](#pattern-8-service-collectors--tagged-services)
---
## Pattern 1: Inheritance
**When code extends a parent class:**
```php
class MyController extends ControllerBase {
public function build() { }
}
```
**What to verify:**
| Check | How to Verify | Risk if Wrong |
|-------|---------------|---------------|
| Parent exists | Read parent class file | Fatal error |
| Abstract methods | Read parent for abstract methods | Fatal error |
| Constructor | Does parent __construct need calling? | Uninitialized state |
| Protected properties | What properties inherited? | Wrong usage |
| Method overrides | Parent method signature | Type errors |
**Inheritance verification template:**
```
CLASS: [MyClass]
EXTENDS: [ParentClass]
PARENT CLASS REVIEW:
Location: [path to parent class file]
Abstract methods (MUST implement):
- [ ] method1(Type $param): ReturnType
My implementation: line [N]
- [ ] method2(): void
My implementation: line [N]
Constructor:
- [ ] Parent constructor exists
- [ ] Requires parameters: [list]
- [ ] My constructor calls parent::__construct()
Protected/Public methods inherited:
- method3() - what it does
- method4() - what it does
Protected properties inherited:
- $this->property1 (type) - what it's for
VERIFICATION:
- [ ] All abstract methods implemented
- [ ] Parent constructor called (if required)
- [ ] Inherited properties used correctly
- [ ] Return types match parent expectations
```
---
## Pattern 2: Plugin System
**When code is a plugin:**
```php
#[Action(
id: "my_action",
label: new TranslatableMarkup("My Action"),
)]
class MyAction extends ConfigurableActionBase { }
```
**What to verify:**
| Check | How to Verify | Risk if Wrong |
|-------|---------------|---------------|
| Annotation/Attribute | Read plugin manager for required fields | Plugin not discovered |
| Base class | Read base class for required methods | Runtime errors |
| Configuration | What config keys expected? Schema? | Form failures |
| Dependencies | What services available? How injected? | Null service errors |
| Plugin context | What context passed to plugin? | Missing data |
**Plugin verification template:**
```
PLUGIN: [PluginClass]
TYPE: [Action | Condition | Event | Field | Widget | etc.]
BASE CLASS: [BaseClassName]
ANNOTATION/ATTRIBUTE CHECK:
Required fields from plugin manager:
- [ ] id: [value]
- [ ] label: [value]
- [ ] [other required]: [value]
BASE CLASS REVIEW:
Location: [file path]
Required methods:
- [ ] execute() / evaluate() / process()
- [ ] defaultConfiguration()
- [ ] buildConfigurationForm()
Services provided by parent:
- $this->service1 (how injected, when available)
- $this->service2 (how injected, when available)
Configuration access:
- $this->configuration['key'] (what keys expected)
- $this->getConfiguration() (returns what)
PLUGIN MANAGER CHECK:
- How does manager create plugins?
- What context passed to plugin?
- What lifecycle hooks exist?
```
---
## Pattern 3: Service/Dependency Injection
**When code depends on injected services:**
```php
class MyService {
public function __construct(
private readonly LoggerInterface $logger,
private readonly EntityTypeManagerInterface $entityTypeManager,
) {}
}
```
**What to verify:**
| Check | How to Verify | Risk if Wrong |
|-------|---------------|---------------|
| Service exists | Check services.yml or container | Container error |
| Interface correct | Read interface for method signatures | Method not found |
| Method signatures | Read interface for parameters, returns | Type errors |
| Return types | What does method actually return? | Wrong usage |
| Side effects | Does method modify state? | Unexpected behavior |
**DI verification template:**
```
SERVICE: [MyService]
DEPENDENCIES:
1. LoggerInterface ($this->logger)
Service ID: [logger.channel.my_module or similar]
Interface location: [path]
Methods used:
- [ ] info() - signature: (string $message, array $context = [])
- [ ] error() - signature: (string $message, array $context = [])
2. EntityTypeManagerInterface ($this->entityTypeManager)
Service ID: [entity_type.manager]
Interface location: [path]
Methods used:
- [ ] getStorage() - returns: EntityStorageInterface
- [ ] getDefinition() - returns: EntityTypeInterface|null
INJECTION VERIFICATION:
- [ ] All dependencies declared in services.yml (or auto-wired)
- [ ] Interface type hints match actual service
- [ ] All methods called exist in interface
- [ ] Return types handled correctly
```
---
## Pattern 4: Interface Implementation
**When code implements an interface:**
```php
class MyHandler implements HandlerInterface {
public function handle(Request $request): Response { }
}
```
**What to verify:**
| Check | How to Verify | Risk if Wrong |
|-------|---------------|---------------|
| All methods | Read interface for complete method list | Interface violation |
| Signatures | Exact parameter types and order | Type errors |
| Return types | Interface-declared return type | Type errors |
| Semantics | Interface documentation for behavior | Wrong behavior |
**Interface verification template:**
```
CLASS: [MyClass]
IMPLEMENTS: [Interface1], [Interface2]
INTERFACE: Interface1
Location: [path]
Required methods:
- [ ] method1(Type $param): ReturnType
My implementation: line [N]
Signature matches: [YES/NO]
- [ ] method2(): void
My implementation: line [N]
Signature matches: [YES/NO]
Semantic requirements (from docs):
- method1 must: [expected behavior]
- method2 must: [expected behavior]
```
---
## Pattern 5: Traits
**When code uses traits:**
```php
class MyClass {
use LoggerTrait;
use EntityTrait;
}
```
**What to verify:**
| Check | How to Verify | Risk if Wrong |
|-------|---------------|---------------|
| Trait requirements | Does trait expect properties/methods? | Undefined errors |
| Conflicts | Multiple traits with same method? | Ambiguity errors |
| Initialization | Does trait need setup? | Null properties |
| Abstract methods | Trait may declare abstract methods | Fatal errors |
**Trait verification template:**
```
CLASS: [MyClass]
USES TRAITS: [Trait1], [Trait2]
TRAIT: Trait1
Location: [path]
Properties provided:
- $this->property1 (type)
Methods provided:
- method1() - does what
Requirements from host class:
- [ ] Expects $this->requiredProperty
- [ ] Expects method requiredMethod()
Abstract methods requiring implementation:
- [ ] abstractMethod()
CONFLICT CHECK:
- Trait1::method() vs Trait2::method()? [CONFLICT/NO CONFLICT]
- Resolution: [how resolved]
```
---
## Pattern 6: Event/Hook Systems
**When code handles events or hooks:**
```php
// Drupal hook
function mymodule_entity_presave(EntityInterface $entity) { }
// Symfony event subscriber
public function onKernelRequest(RequestEvent $event) { }
```
**What to verify:**
| Check | How to Verify | Risk if Wrong |
|-------|---------------|---------------|
| Signature | Read dispatcher for expected parameters | Missing data |
| Return value | What does dispatcher expect returned? | Broken chain |
| Order | When does this fire relative to others? | Race conditions |
| Event object | What methods/data on event object? | Wrong access |
**Event verification template:**
```
HOOK/EVENT: [hook_name or EventClass]
DISPATCHER CHECK:
Where dispatched: [file:line]
Expected signature:
- Parameter 1: [Type] $name - what it contains
- Parameter 2: [Type] $name - what it contains
Return expectation:
- Return type: [void | mixed | specific]
- Return affects: [nothing | stops propagation | modifies data]
EVENT OBJECT (if applicable):
Methods available:
- [ ] getEntity() - returns EntityInterface
- [ ] stopPropagation() - prevents further handlers
- [ ] getData() - returns [type]
MY IMPLEMENTATION:
- [ ] Signature matches expected
- [ ] Return type correct
- [ ] Event object methods exist
```
---
## Pattern 7: Middleware/Decorators
**When code wraps or decorates:**
```php
class MyMiddleware implements MiddlewareInterface {
public function process(Request $request, Handler $handler): Response {
// before
$response = $handler->handle($request);
// after
return $response;
}
}
```
**What to verify:**
| Check | How to Verify | Risk if Wrong |
|-------|---------------|---------------|
| Chain call | Must call next handler? | Broken chain |
| Request mutation | Can/should modify request? | Unexpected state |
| Response handling | Must return response? Modify it? | Lost response |
| Order | Where in stack? Before/after what? | Wrong timing |
**Middleware verification template:**
```
MIDDLEWARE: [MiddlewareClass]
IMPLEMENTS: [MiddlewareInterface]
INTERFACE CHECK:
Location: [path]
Required signature: process(Request $request, Handler $handler): Response
CHAIN VERIFICATION:
- [ ] Calls $handler->handle($request)
- [ ] Returns Response object
- [ ] Proper exception handling
STACK POSITION:
- Where registered: [middleware config file]
- Priority/order: [value]
- Runs before: [other middleware]
- Runs after: [other middleware]
REQUEST/RESPONSE HANDLING:
- Modifies request: [yes/no - how]
- Modifies response: [yes/no - how]
- Can short-circuit: [yes/no]
```
---
## Pattern 8: Service Collectors / Tagged Services
**When services are dynamically collected via tags:**
This pattern is used when a "manager" service aggregates multiple "worker" services that register themselves via tags. The manager doesn't know ahead of time which services will be collected.
```yaml
# services.yml - Service registers itself with a tag
services:
my_module.my_breadcrumb_builder:
class: Drupal\my_module\MyBreadcrumbBuilder
tags:
- { name: breadcrumb_builder, priority: 100 }
```
```php
// The collected service must implement the expected interface
class MyBreadcrumbBuilder implements BreadcrumbBuilderInterface {
public function applies(RouteMatchInterface $route_match): bool { }
public function build(RouteMatchInterface $route_match): Breadcrumb { }
}
```
**Common collector patterns:**
| Pattern | Tag | Collector | Interface Required |
|---------|-----|-----------|-------------------|
| Breadcrumbs | `breadcrumb_builder` | BreadcrumbManager | BreadcrumbBuilderInterface |
| Access checks | `access_check` | AccessManager | AccessInterface |
| Route subscribers | `event_subscriber` | EventDispatcher | EventSubscriberInterface |
| Param converters | `paramconverter` | ParamConverterManager | ParamConverterInterface |
| Normalizers | `normalizer` | Serializer | NormalizerInterface |
| Context providers | `context_provider` | ContextRepository | ContextProviderInterface |
| Validation constraints | `validation.constraint_validator` | ConstraintValidatorFactory | ConstraintValidatorInterface |
**What to verify:**
| Check | How to Verify | Risk if Wrong |
|-------|---------------|---------------|
| Tag name | Read collector for expected tag name | Service not discovered |
| Tag attributes | priority, id, method - what's required? | Wrong order, missing calls |
| Interface | What interface does collector expect? | Method not found errors |
| Method signatures | Read interface for exact signatures | Type errors |
| Return values | What does collector do with returns? | Logic failures |
| Execution order | Does priority matter for your use case? | Race conditions |
**Service collector verification template:**
```
TAGGED SERVICE: [MyService]
TAG: [tag_name]
COLLECTOR: [CollectorService]
TAG CONFIGURATION:
Location: services.yml or services annotation
Tag name: [exact tag name]
Tag attributes:
- priority: [value] (higher = earlier?)
- id: [if required]
- method: [if collector calls specific method]
COLLECTOR CHECK:
Location: [collector class file]
How collector discovers services:
- Compiler pass? ServiceCollectorInterface? Manual?
How collector invokes services:
- Calls method: [method name]
- Passes parameters: [what params]
- Expects return: [what return type]
Execution order:
- Priority direction: [higher first / lower first]
- Can stop chain: [yes/no]
INTERFACE REQUIRED:
Interface: [InterfaceName]
Location: [path]
Required methods:
- [ ] method1(params): ReturnType
- [ ] method2(params): ReturnType
MY IMPLEMENTATION CHECK:
- [ ] Tag name exactly matches collector expectation
- [ ] All required tag attributes present
- [ ] Implements correct interface
- [ ] All interface methods implemented
- [ ] Method signatures match exactly
- [ ] Return types correct for collector logic
- [ ] Priority appropriate for use case
```
**Framework-specific examples:**
**Drupal - Breadcrumb Builder:**
```yaml
services:
my_module.breadcrumb:
class: Drupal\my_module\Breadcrumb\MyBreadcrumbBuilder
tags:
- { name: breadcrumb_builder, priority: 1000 }
```
```php
class MyBreadcrumbBuilder implements BreadcrumbBuilderInterface {
// MUST implement applies() - collector calls this first
public function applies(RouteMatchInterface $route_match): bool {
return $route_match->getRouteName() === 'my_module.my_route';
}
// MUST implement build() - only called if applies() returns TRUE
public function build(RouteMatchInterface $route_match): Breadcrumb {
$breadcrumb = new Breadcrumb();
// ... build breadcrumb
return $breadcrumb; // MUST return Breadcrumb object
}
}
```
**Symfony - Event Subscriber:**
```yaml
services:
my_bundle.event_subscriber:
class: MyBundle\EventSubscriber\MySubscriber
tags:
- { name: kernel.event_subscriber }
```
```php
class MySubscriber implements EventSubscriberInterface {
// MUST implement getSubscribedEvents() - tells dispatcher what to call
public static function getSubscribedEvents(): array {
return [
KernelEvents::REQUEST => ['onKernelRequest', 10],
KernelEvents::RESPONSE => 'onKernelResponse',
];
}
public function onKernelRequest(RequestEvent $event): void {
// Handler implementation
}
}
```
---
## Quick Contract Detection
Use this to quickly identify which pattern applies:
| Code Pattern | Contract Type |
|--------------|---------------|
| `extends ParentClass` | Pattern 1: Inheritance |
| `#[Plugin(...)]` or `@Plugin(...)` | Pattern 2: Plugin System |
| `__construct(ServiceInterface $service)` | Pattern 3: Dependency Injection |
| `implements InterfaceX` | Pattern 4: Interface Implementation |
| `use TraitName;` | Pattern 5: Traits |
| `function hook_*()` or `EventSubscriberInterface` | Pattern 6: Event/Hook Systems |
| `MiddlewareInterface` or `process()` method | Pattern 7: Middleware |
| `tags: [{ name: ... }]` in services.yml | Pattern 8: Service Collectors |
---
## Common Contract Violations
**Missing abstract method implementation:**
```php
// Parent has abstract method
abstract class Base {
abstract public function process(): void;
}
// Child MUST implement it
class Child extends Base {
// VIOLATION: Missing process() method
}
```
**Wrong interface signature:**
```php
// Interface defines exact signature
interface Handler {
public function handle(Request $request): Response;
}
// VIOLATION: Wrong parameter type
class MyHandler implements Handler {
public function handle(array $request): Response { }
// ^^^^^ should be Request
}
```
**Plugin missing required annotation fields:**
```php
// VIOLATION: Missing required 'id' field
#[Action(
label: new TranslatableMarkup("My Action")
)]
class MyAction extends ActionBase { }
```
**Tagged service not implementing required interface:**
```yaml
services:
my.breadcrumb:
class: My\Class
tags:
- { name: breadcrumb_builder }
```
```php
// VIOLATION: Must implement BreadcrumbBuilderInterface
class My\Class {
// Missing applies() and build() methods
}
```
```
### references/ai-code-auditing.md
```markdown
# Auditing AI-Generated Code
AI (LLMs) generates code based on patterns, not actual knowledge of your codebase. This creates specific risks that paper testing helps catch.
## Contents
- [Common AI Code Problems](#common-ai-code-problems)
- [How to Catch AI Assumptions](#how-to-catch-ai-assumptions)
- [Paper Test Notation for AI Code](#paper-test-notation-for-ai-code)
- [Red Flags in AI Code](#red-flags-in-ai-code)
- [Verification Checklist](#verification-checklist-for-ai-code)
- [Example - Catching AI Hallucination](#example---catching-ai-hallucination)
---
## Common AI Code Problems
| Problem | Example |
|---------|---------|
| **Invented methods** | Calls `$entity->getFieldValue()` but method is actually `$entity->get()` |
| **Wrong return types** | Assumes method returns array, actually returns object |
| **Wrong parameters** | Passes `(id, type)` but signature is `(type, id)` |
| **Mixed APIs** | Uses Drupal 7 pattern in Drupal 10 code |
| **Assumed existence** | Calls service that doesn't exist in the container |
| **Wrong namespace** | Imports class from wrong module |
---
## How to Catch AI Assumptions
During paper test, for every external call AI wrote:
1. **Verify the method exists** - Check the actual class
2. **Verify the signature** - Parameters in right order? Right types?
3. **Verify return type** - What does it actually return?
4. **Verify the import** - Is the namespace correct?
**NEVER trust AI code without verification** - AI generates plausible code, not necessarily correct code.
---
## Paper Test Notation for AI Code
Mark AI assumptions explicitly during tracing:
```
Line 15: $mapping = $this->mappingStorage->loadByProperties(['status' => 1]);
AI ASSUMPTION CHECK:
- Method exists? → CHECK: Yes, from ConfigEntityStorage
- Parameters? → CHECK: Takes array of conditions
- Returns? → PROBLEM: AI assumes returns single entity
ACTUAL: Returns array of entities
FLAW: AI assumed loadByProperties returns one entity, returns array.
FIX: Use reset($mapping) or loop through results.
```
This makes assumptions visible and catchable.
---
## Red Flags in AI Code
Watch for these patterns that often indicate AI assumptions:
### 1. Chained Method Calls
```php
// Red flag: Did AI verify each method exists?
$value = $entity->getMapping()->getSalesforceId()->getValue();
```
**What to check:**
- Does `getMapping()` exist?
- Does it return an object with `getSalesforceId()`?
- Does that return an object with `getValue()`?
- What if any step returns null?
### 2. Specific Method Names
```php
// Red flag: Might be invented
$result = $service->fetchAllActiveRecords();
// Red flag: Very specific helper
$user = $userRepository->findByEmailAndStatus($email, 'active');
```
**What to check:**
- Does this exact method name exist?
- Or did AI invent a plausible-sounding name?
### 3. Assumed Data Structures
```php
// Red flag: Assumed nested array structure
$name = $response['data']['user']['name'];
// Red flag: Assumed object property
$id = $result->data->id;
```
**What to check:**
- Is this the actual structure returned?
- What if keys don't exist?
- What if nesting is different?
### 4. Framework-Specific Helpers
```php
// Red flag: Right version? Right parameters?
$url = Url::fromRoute('entity.node.canonical', ['node' => $nid]);
// Red flag: D7 pattern in D10 code
$node = node_load($nid); // Old API!
```
**What to check:**
- Is this the current API version?
- Are parameters in correct order?
- Does route name exist?
### 5. Assumed Constants/Enums
```php
// Red flag: Does this constant exist?
if ($status === Node::STATUS_PUBLISHED) { }
// Red flag: Right enum value?
$entity->setStatus(EntityStatus::ACTIVE);
```
**What to check:**
- Does constant exist on that class?
- Is the value correct?
### 6. Service Assumptions
```php
// Red flag: Does this service exist in container?
$this->customApiClient->send($data);
// Red flag: Right service method?
$this->logger->logError($message); // Might be error(), not logError()
```
**What to check:**
- Is service registered?
- Is method name correct?
- Is interface imported?
---
## Verification Checklist for AI Code
For each external dependency in AI-generated code:
- [ ] Class/service exists in codebase
- [ ] Method exists on that class
- [ ] Method signature matches (parameter order, types)
- [ ] Return type matches AI's usage
- [ ] Namespace/import is correct
- [ ] API version matches project (D10 vs D9, React 18 vs 17, etc.)
- [ ] Null/error cases handled
- [ ] Data structure assumptions verified
---
## Example - Catching AI Hallucination
### AI Generated Code:
```php
public function execute($entity = NULL): void {
$fields = $this->salesforceClient->describeObject('Contact')->getFields();
foreach ($fields as $field) {
$value = $entity->getFieldValue($field->name);
$this->salesforceClient->updateField('Contact', $entity->id(), $field->name, $value);
}
}
```
### Paper Test with Verification:
```
PAPER TEST: AI-generated Salesforce field sync
SCENARIO: Update Contact fields
INPUT: $entity = Node{id: 123, type: 'contact'}
---
LINE 2: $fields = $this->salesforceClient->describeObject('Contact')->getFields();
AI ASSUMPTION CHECK:
Method: describeObject()
Verify method exists:
→ Checked RestClientInterface
→ PROBLEM: Method is objectDescribe(), not describeObject()
→ AI HALLUCINATION: Invented plausible method name
Verify return:
→ objectDescribe() returns RestResponseDescribe object
→ Has getFields() method? → YES, verified exists
→ getFields() returns: array of field descriptor objects
FLAW FOUND #1:
Line 2: AI invented method name "describeObject"
ACTUAL: Method is "objectDescribe"
FIX: $this->salesforceClient->objectDescribe('Contact')->getFields()
---
LINE 4: $value = $entity->getFieldValue($field->name);
AI ASSUMPTION CHECK:
Method: getFieldValue()
Verify method exists:
→ Checked EntityInterface
→ PROBLEM: Method does not exist
→ ACTUAL: Method is get($field_name)->value or $entity->get($field_name)->getString()
→ AI HALLUCINATION: Invented convenient method
Verify field name handling:
→ $field->name might be Salesforce field (e.g., "FirstName")
→ Need to map to Drupal field (e.g., "field_first_name")
→ AI missed mapping step entirely
FLAW FOUND #2:
Line 4: Method getFieldValue() doesn't exist
FIX: Use $entity->get($drupal_field_name)->value
FLAW FOUND #3:
Line 4: No field mapping from Salesforce to Drupal
FIX: Add mapping lookup before get()
---
LINE 5: $this->salesforceClient->updateField('Contact', $entity->id(), $field->name, $value);
AI ASSUMPTION CHECK:
Method: updateField()
Verify method exists:
→ Checked RestClientInterface
→ PROBLEM: No updateField() method exists
→ ACTUAL: Must use objectUpdate() with full object data
→ AI HALLUCINATION: Invented granular update method
Verify parameters:
→ Even if method existed, parameter order seems wrong
→ Usually: (object_type, object_id, data) not individual fields
FLAW FOUND #4:
Line 5: Method updateField() doesn't exist
ACTUAL API: objectUpdate(string $name, array $data)
FIX: Collect all field values, then call objectUpdate() once
---
FLAWS SUMMARY:
1. describeObject() → objectDescribe() (hallucinated method name)
2. getFieldValue() doesn't exist (hallucinated convenience method)
3. Missing Salesforce-to-Drupal field mapping
4. updateField() doesn't exist (hallucinated granular update)
5. Should batch update, not update field-by-field
CORRECTED CODE:
```php
public function execute($entity = NULL): void {
// Get Salesforce field definitions
$sf_fields = $this->salesforceClient->objectDescribe('Contact')->getFields();
// Build update data
$data = [];
foreach ($sf_fields as $sf_field) {
// Map Salesforce field to Drupal field
$drupal_field = $this->fieldMapping->toDrupalField($sf_field->name);
if ($entity->hasField($drupal_field)) {
$field = $entity->get($drupal_field);
if (!$field->isEmpty()) {
$data[$sf_field->name] = $field->value;
}
}
}
// Single update call with all fields
$salesforce_id = $this->getSalesforceId($entity);
$this->salesforceClient->objectUpdate('Contact', $salesforce_id, $data);
}
```
---
## Common AI Hallucination Patterns
### Pattern 1: Convenience Methods
AI invents methods that "should" exist but don't:
```php
// AI generates:
$user->getFullName(); // Doesn't exist
// Actual:
$user->get('field_first_name')->value . ' ' . $user->get('field_last_name')->value;
// AI generates:
$entity->setFieldValue('field_name', $value); // Doesn't exist
// Actual:
$entity->set('field_name', $value);
```
### Pattern 2: Wrong API Version
AI mixes patterns from different versions:
```php
// AI generates (Drupal 7 in D10):
$node = node_load($nid);
// D10 actual:
$node = Node::load($nid);
// AI generates (old Symfony):
$request->get('param');
// Current:
$request->query->get('param');
```
### Pattern 3: Plausible Names
AI generates method names that sound right:
```php
// AI generates:
$storage->findByStatus('published'); // Sounds right, doesn't exist
// Actual:
$storage->loadByProperties(['status' => 'published']);
// AI generates:
$repository->getAllActive(); // Sounds right, doesn't exist
// Actual:
$repository->findBy(['active' => true]);
```
### Pattern 4: Wrong Return Assumptions
AI assumes convenient return types:
```php
// AI assumes:
$user = $repository->find($id); // Assumes single User
// Actually returns:
$users = $repository->find($id); // Returns array!
// AI assumes:
$result = $service->process($data); // Assumes direct value
// Actually:
$result = $service->process($data); // Returns Result object with ->getData()
```
---
## AI Code Testing Strategy
### Phase 1: Syntax Check
Run linter/static analysis:
- Catches undefined methods immediately
- Catches wrong parameter counts
- Catches type mismatches
### Phase 2: Paper Test with Verification
For each external call:
1. Find actual class/service
2. Verify method exists
3. Verify signature
4. Verify return type
5. Verify error handling
### Phase 3: Run Tests
After paper test fixes:
- Unit tests catch remaining issues
- Integration tests catch API mismatches
- But paper test catches 80% before running
---
## Quick Verification Template
```
LINE [N]: [AI-generated code]
AI VERIFICATION:
Class/Service: [name]
→ Exists? [YES/NO]
→ Imported from: [correct namespace?]
Method: [name]
→ Exists? [YES/NO]
→ Signature: [actual signature]
→ AI used: [what AI assumed]
→ Match? [YES/NO]
Return type:
→ Actually returns: [type]
→ AI assumes: [type]
→ Match? [YES/NO]
Edge cases:
→ Handles null? [YES/NO]
→ Handles empty? [YES/NO]
→ Handles errors? [YES/NO]
RESULT:
[ ] VERIFIED - code is correct
[ ] FLAW FOUND - [description]
FIX: [correction]
```
---
## Summary
**AI code is plausible, not verified.**
Every external call in AI-generated code is an assumption that needs verification:
- Method names (might be invented)
- Parameter order (might be wrong)
- Return types (might be assumed)
- API versions (might be mixed)
- Error handling (might be missing)
**Paper testing with verification catches these before deployment.**
```
### references/hybrid-testing.md
```markdown
# Hybrid Testing Strategy
For modules with multiple components (events, conditions, actions, services), use a **coverage-driven hybrid** approach that combines flow-based and component testing.
## Contents
- [Two Levels of Testing](#two-levels-of-testing)
- [Why Both?](#why-both)
- [Coverage-Driven Method](#coverage-driven-method)
- [Coverage Matrix Example](#coverage-matrix-example)
- [Minimum Coverage Requirements](#minimum-coverage-requirements)
- [Flow Testing Template](#flow-testing-template)
- [Component Edge Case Template](#component-edge-case-template)
- [Using Agents for Parallel Testing](#using-agents-for-parallel-testing)
---
## Two Levels of Testing
| Level | What | Purpose | Catches |
|-------|------|---------|---------|
| **Flow-based** | Real user workflows end-to-end | Test integration, data handoffs | Data format mismatches, token issues, missing handoffs |
| **Component** | Each component with edge cases | Test individual logic thoroughly | Implementation bugs, null handling, error cases |
---
## Why Both?
### Flow-based Testing Alone Misses:
- Edge cases within components (empty results, null inputs)
- Error handling paths that don't occur in happy-path flows
- Component-specific logic bugs
### Component-only Testing Misses:
- Data format incompatibilities between components
- Token/state handoff issues
- Real-world usage patterns
**Both together = comprehensive coverage with minimal redundancy**
---
## Coverage-Driven Method
```
Step 1: Map all components
- List every event, condition, action, service
- Example: 7 events, 2 conditions, 7 actions = 16 components
Step 2: Design flows that COVER all components
- Each component must appear in at least one flow
- Flows represent real user workflows
- 3-5 flows typically cover a module
Step 3: Add component edge cases
- For each component, identify scenarios NOT covered by flows
- Error cases, empty inputs, boundary conditions
- 2-4 edge cases per component
```
This ensures:
- Every component tested in realistic integration (flows)
- Every component tested with edge cases (components)
- No redundant testing
---
## Coverage Matrix Example
For an ECA integration module:
```
FLOWS:
┌─────────────────────────────────────────────────────────────────┐
│ Flow 1: "Push on entity save" │
│ Event: entity_presave → Condition: has_mapping → │
│ Action: trigger_push → (uses: push result tokens) │
├─────────────────────────────────────────────────────────────────┤
│ Flow 2: "Query and process" │
│ Event: custom_trigger → Action: execute_soql → │
│ Action: get_field_value (loop through results) │
├─────────────────────────────────────────────────────────────────┤
│ Flow 3: "Handle pull event" │
│ Event: pull_success → Condition: check_object_type → │
│ Action: get_mapped_object → Action: update_entity │
└─────────────────────────────────────────────────────────────────┘
COMPONENT EDGE CASES:
┌─────────────────────────────────────────────────────────────────┐
│ execute_soql: │
│ - Empty result set (0 records) │
│ - API error / connection failure │
│ - Malformed SOQL query │
├─────────────────────────────────────────────────────────────────┤
│ trigger_push: │
│ - Entity has no mapping │
│ - Multiple mappings for same entity │
│ - Push fails due to validation │
├─────────────────────────────────────────────────────────────────┤
│ has_mapping condition: │
│ - Entity type not supported │
│ - Null entity passed │
└─────────────────────────────────────────────────────────────────┘
```
Result:
- All 16 components appear in at least one flow
- Each component has 2-4 edge case tests
- Total scenarios: 3 flows + ~30 edge cases = 33 tests
---
## Minimum Coverage Requirements
| Component Type | In Flows | Edge Cases | Total Scenarios |
|----------------|----------|------------|-----------------|
| Events | 1+ flow each | 1-2 (error events) | 2-3 per event |
| Conditions | 1+ flow each | 2-3 (true, false, edge) | 3-4 per condition |
| Actions | 1+ flow each | 2-4 (error, empty, edge) | 3-5 per action |
---
## Flow Testing Template
```
FLOW: [Name - what user is trying to accomplish]
TRIGGER: [What starts this flow]
EXPECTED OUTCOME: [What should happen when complete]
COMPONENTS INVOLVED:
- Event: [event_id]
- Condition: [condition_id] (optional)
- Action: [action_id]
- Action: [action_id]
SCENARIO: [Concrete example]
INPUT:
[Initial state - entity values, configuration, etc.]
TRACE:
1. EVENT FIRES: [event_id]
Data provided: [what data/tokens are available]
Configuration: [event configuration]
Token data stored:
- [token_name]: [token_value]
- [token_name]: [token_value]
2. CONDITION EVALUATES: [condition_id]
Input received:
- From tokens: [what tokens it reads]
- Configuration: [condition configuration]
Processing:
Line X: [condition logic traced]
Line Y: [...]
Result: [true/false and why]
Action: [CONTINUE to action / STOP execution]
3. ACTION EXECUTES: [action_id]
Input received:
- From tokens: [which tokens from event/previous actions]
- Configuration: [action configuration values]
Processing:
Line X: [action logic traced]
Line Y: [external call - verified behavior]
Line Z: [...]
Output:
Token data stored:
- [token_name]: [value]
Side effects: [database changes, API calls, etc.]
4. NEXT ACTION: [action_id]
Input received:
- From tokens: [uses tokens from step 3]
- Configuration: [...]
Processing:
Line X: [...]
Output:
Return value: [...]
Side effects: [...]
INTEGRATION CHECKS:
- [ ] Event provides data action expects?
- [ ] Token names consistent between components?
- [ ] Data types compatible (string vs array vs object)?
- [ ] Error from one component handled by next?
- [ ] Missing null checks between handoffs?
FLAWS FOUND:
- [Integration issue description]
Component A outputs: [format]
Component B expects: [format]
FIX: [how to resolve]
- [Token mismatch]
Event stores token: "entity_id"
Action reads token: "entity.id"
FIX: Standardize token names
```
---
## Component Edge Case Template
```
COMPONENT: [component_id]
TYPE: [Event | Condition | Action]
BASE CLASS: [ConfigurableActionBase, etc.]
COVERED IN FLOWS:
- Flow 1: [scenario name] - happy path
SCENARIOS NOT COVERED BY FLOWS:
1. [edge case description]
2. [edge case description]
3. [error case description]
---
SCENARIO 1: [Description - e.g., "Empty result set"]
INPUT:
Configuration:
- soql_query: "SELECT Id FROM Contact WHERE Email = '[email protected]'"
- result_token: "query_result"
Token data available:
- entity: Node{id: 123}
TRACE:
Line 5: $query = $this->getConfigWithTokens('soql_query')
→ $query = "SELECT Id FROM Contact WHERE Email = '[email protected]'"
Line 6: $result = $this->salesforceClient->query($query)
DEPENDENCY CHECK: salesforceClient->query()
Returns: SelectQueryResult object
When no results: result->size() = 0, result->records() = []
→ $result = SelectQueryResult{size: 0, records: []}
Line 7: $records = $result->records()
→ $records = [] (empty array)
Line 8: $this->tokenService->addTokenData('query_result', $records)
→ Stores empty array in token
OUTPUT:
Token data stored:
- query_result: [] (empty array)
Side effects: None
FLAW CHECK:
- [ ] Handles empty array correctly?
- [ ] Should set a "no_results" flag?
- [ ] Next action expects array or specific format?
FLAW FOUND: None - correctly handles empty results
---
SCENARIO 2: [Description - e.g., "API connection failure"]
INPUT:
Configuration: [same as scenario 1]
TRACE:
Line 6: $result = $this->salesforceClient->query($query)
DEPENDENCY CHECK:
Throws: RestException on connection failure
→ THROWS RestException("Connection timeout")
Line 7: NOT REACHED (exception thrown)
OUTPUT:
Exception thrown: RestException
No token data stored
Flow stops
FLAW FOUND:
- Line 6: No try/catch for connection failures
- Flow crashes instead of graceful failure
- FIX: Wrap in try/catch, log error, set error token
---
SCENARIO 3: [Description - e.g., "Malformed SOQL query"]
INPUT:
Configuration:
- soql_query: "SELECTT Id FROM Contact" # Typo: SELECTT
TRACE:
Line 5: $query = $this->getConfigWithTokens('soql_query')
→ $query = "SELECTT Id FROM Contact"
Line 6: $result = $this->salesforceClient->query($query)
DEPENDENCY CHECK:
Throws: RestException on SOQL syntax error
→ THROWS RestException("Invalid SOQL syntax")
OUTPUT:
Exception thrown: RestException
No token data stored
FLAW FOUND:
- Same as scenario 2 - needs error handling
- FIX: try/catch + error token
```
---
## Using Agents for Parallel Testing
For large modules, spawn agents to test in parallel:
### Option 1: Agent per Component Type
```
- Agent 1: Test all events (7 events × 2 scenarios each = 14 tests)
- Agent 2: Test all conditions (2 conditions × 3 scenarios each = 6 tests)
- Agent 3: Test all actions (7 actions × 3 scenarios each = 21 tests)
```
**When to use:** Small-to-medium modules, clear component separation
### Option 2: Agent per Component
```
- Agent 1: execute_soql action (4 scenarios)
- Agent 2: trigger_push action (4 scenarios)
- Agent 3: get_field_value action (3 scenarios)
- Agent 4: entity_presave event (2 scenarios)
- Agent 5: has_mapping condition (3 scenarios)
- ...
```
**When to use:** Large modules with complex components
### Option 3: Agent per Flow + Edge Case Agents
```
Integration Testing:
- Agent 1: Flow 1 - Push on entity save
- Agent 2: Flow 2 - Query and process
- Agent 3: Flow 3 - Handle pull event
Edge Case Testing (parallel):
- Agent 4: execute_soql edge cases
- Agent 5: trigger_push edge cases
- Agent 6: All condition edge cases
- Agent 7: All event edge cases
```
**When to use:** When flows are complex and need focused attention
---
## Coverage Checklist
Before finishing paper testing:
### Flow Coverage
- [ ] Every component appears in at least one flow
- [ ] Every flow represents a real user workflow
- [ ] Integration between components tested (token handoffs, data formats)
### Component Coverage
- [ ] Each component has edge case scenarios
- [ ] Error cases covered (API failures, null inputs, empty results)
- [ ] Boundary conditions tested (first/last item, min/max values)
### Quality Checks
- [ ] No redundant scenarios (each test adds unique coverage)
- [ ] Realistic test data (not contrived examples)
- [ ] External dependencies verified (not assumed)
---
## Example: Complete Coverage Plan
**Module:** Salesforce ECA Integration (16 components)
### Flow Tests (3 flows):
1. "Push entity on save" (4 components)
2. "Query and process results" (3 components)
3. "Handle pull event" (5 components)
**Coverage:** 12/16 components in flows
### Missing from Flows:
- authenticate action
- disconnect action
- get_api_version action
- connection_status event
### Edge Case Tests (32 scenarios):
- 7 events × 2 edge cases = 14 scenarios
- 2 conditions × 3 edge cases = 6 scenarios
- 7 actions × 3 edge cases = 21 scenarios
- Missing components: 4 × 2 = 8 scenarios (added to flows or edge cases)
**Total:** 3 flows + 40 edge cases = 43 total test scenarios
**Estimated time:**
- Flows: 30 min each × 3 = 1.5 hours
- Edge cases: 5 min each × 40 = 3.3 hours
- Total: ~5 hours for complete module testing
**vs. Component-only:**
- 16 components × 5 scenarios each = 80 tests
- Time: ~7 hours with significant redundancy
**Hybrid saves time while improving coverage.**
```
### references/common-flaws.md
```markdown
# Common Code Flaws Catalog
Comprehensive list of bugs and issues paper testing frequently catches.
## Contents
- [Logic Errors](#logic-errors)
- [Null/Undefined Access](#nullundefined-access)
- [Edge Cases](#edge-cases)
- [State Issues](#state-issues)
- [Flow Issues](#flow-issues)
- [Contract Violations](#contract-violations)
- [Dependency Errors](#dependency-errors)
- [AI-Generated Code Specific](#ai-generated-code-specific)
---
## Logic Errors
### 1. Wrong Comparison Operator
```php
// FLAW: Assignment instead of comparison
if ($status = 'active') { // Sets $status to 'active', always true
// ...
}
// FIX: Use == or ===
if ($status === 'active') {
```
```php
// FLAW: Loose vs strict comparison
if ($count == '0') { // true for 0, '0', '', false, null
// ...
}
// FIX: Use strict comparison
if ($count === 0) {
```
### 2. Inverted Condition
```php
// FLAW: Logic backwards
if ($user) {
return NULL; // Returns null when user EXISTS
}
return $user; // Returns user when user is NULL
// FIX: Invert the condition
if (!$user) {
return NULL;
}
return $user;
```
### 3. Off-by-One in Loops
```php
// FLAW: Starts at 1, skips first item
for ($i = 1; $i < count($items); $i++) {
process($items[$i]);
}
// FIX: Start at 0
for ($i = 0; $i < count($items); $i++) {
```
```php
// FLAW: Uses <=, processes one too many
for ($i = 0; $i <= count($items); $i++) { // Array out of bounds!
process($items[$i]);
}
// FIX: Use <
for ($i = 0; $i < count($items); $i++) {
```
### 4. Missing Break in Switch
```php
// FLAW: Falls through to next case
switch ($type) {
case 'admin':
$permissions = ['all'];
// Missing break! Falls through to 'user' case
case 'user':
$permissions = ['read']; // Overwrites 'all'!
break;
}
// FIX: Add break
case 'admin':
$permissions = ['all'];
break;
```
### 5. Wrong Operator Precedence
```php
// FLAW: && binds tighter than ||
if ($status == 'active' || $status == 'pending' && $verified) {
// Means: active OR (pending AND verified)
// Not: (active OR pending) AND verified
}
// FIX: Use parentheses
if (($status == 'active' || $status == 'pending') && $verified) {
```
---
## Null/Undefined Access
### 1. Accessing Property on Null
```php
// FLAW: No null check
$user = loadUser($id);
$name = $user->getName(); // Fatal if $user is null
// FIX: Check for null
$user = loadUser($id);
if ($user) {
$name = $user->getName();
}
```
### 2. Array Key That Might Not Exist
```php
// FLAW: Assumes key exists
$value = $config['api_key']; // Notice/Warning if not set
// FIX: Check existence or use default
$value = $config['api_key'] ?? 'default';
// OR
$value = isset($config['api_key']) ? $config['api_key'] : 'default';
```
### 3. Uninitialized Variable
```php
// FLAW: Variable never set in some paths
if ($condition) {
$result = calculateResult();
}
return $result; // UNDEFINED if $condition is false
// FIX: Initialize before conditional
$result = null;
if ($condition) {
$result = calculateResult();
}
return $result;
```
### 4. Null Propagation in Chains
```php
// FLAW: Any step could be null
$id = $entity->getMapping()->getSalesforceId()->getValue();
// If getMapping() returns null:
// → Fatal: Call to member function getSalesforceId() on null
// FIX: Check each step or use null-safe operator
$mapping = $entity->getMapping();
if ($mapping) {
$sfId = $mapping->getSalesforceId();
if ($sfId) {
$id = $sfId->getValue();
}
}
// OR (PHP 8+)
$id = $entity->getMapping()?->getSalesforceId()?->getValue();
```
---
## Edge Cases
### 1. Empty Array
```php
// FLAW: Assumes array has items
$first = $items[0]; // Fatal if array is empty
// FIX: Check if array has items
if (!empty($items)) {
$first = $items[0];
}
// OR
$first = $items[0] ?? null;
```
### 2. Empty String
```php
// FLAW: Empty string is falsy but not null
if ($email) { // '' is falsy, skips validation
validate($email);
}
// FIX: Explicit empty check
if ($email !== '' && $email !== null) {
validate($email);
}
// OR check what empty means for your use case
if (strlen($email) > 0) {
```
### 3. Zero and Negative Numbers
```php
// FLAW: Treats 0 as invalid
if ($quantity) { // 0 is falsy, but might be valid
process($quantity);
}
// FIX: Explicit comparison
if ($quantity > 0) {
process($quantity);
}
// OR if 0 is valid:
if ($quantity !== null) {
process($quantity);
}
```
### 4. Very Large Inputs
```php
// FLAW: No limit on loop iterations
foreach ($items as $item) { // What if $items has 1 million entries?
expensiveOperation($item);
}
// FIX: Add limit or pagination
$max = 1000;
$count = 0;
foreach ($items as $item) {
if (++$count > $max) break;
expensiveOperation($item);
}
```
---
## State Issues
### 1. Variable Modified But Not Used
```php
// FLAW: Calculate but don't use
$total = 0;
foreach ($items as $item) {
$total += $item->price;
}
return $items; // BUG: Should return $total, not $items
// FIX: Return calculated value
return $total;
```
### 2. Variable Used Before Assignment
```php
// FLAW: Read before write
if ($status === 'active') {
$count++; // UNDEFINED: Never initialized
}
// FIX: Initialize first
$count = 0;
if ($status === 'active') {
$count++;
}
```
### 3. Stale Data From Previous Iteration
```php
// FLAW: Variable persists across iterations
foreach ($items as $item) {
if ($item->hasDiscount()) {
$discount = $item->getDiscount();
}
$total += $item->price - $discount; // Uses old $discount if current item has none!
}
// FIX: Reset or always assign
foreach ($items as $item) {
$discount = 0; // Reset each iteration
if ($item->hasDiscount()) {
$discount = $item->getDiscount();
}
$total += $item->price - $discount;
}
```
---
## Flow Issues
### 1. Unreachable Code
```php
// FLAW: Code after return never runs
function process($data) {
if (empty($data)) {
return false;
}
return true;
logProcessing($data); // NEVER EXECUTED
}
// FIX: Move logging before returns
```
### 2. Missing Return Statement
```php
// FLAW: Not all paths return value
function getStatus($user) {
if ($user->isActive()) {
return 'active';
}
// MISSING: No return for inactive users
}
// FIX: Ensure all paths return
function getStatus($user) {
if ($user->isActive()) {
return 'active';
}
return 'inactive';
}
```
### 3. Early Return Skips Cleanup
```php
// FLAW: Lock never released if error
function process($id) {
acquireLock($id);
if ($error) {
return false; // BUG: Lock still held!
}
$result = doWork($id);
releaseLock($id);
return $result;
}
// FIX: Release in all paths
function process($id) {
acquireLock($id);
if ($error) {
releaseLock($id);
return false;
}
$result = doWork($id);
releaseLock($id);
return $result;
}
// OR use try/finally
```
---
## Contract Violations
### 1. Missing Abstract Method Implementation
```php
// Parent class:
abstract class ActionBase {
abstract public function execute(): void;
}
// FLAW: Doesn't implement required method
class MyAction extends ActionBase {
// Missing execute() method - Fatal error!
}
// FIX: Implement all abstract methods
class MyAction extends ActionBase {
public function execute(): void {
// Implementation
}
}
```
### 2. Wrong Interface Signature
```php
// Interface:
interface HandlerInterface {
public function handle(Request $request): Response;
}
// FLAW: Wrong parameter type
class MyHandler implements HandlerInterface {
public function handle(array $request): Response {
// ^^^^^ Should be Request object
}
}
// FIX: Match interface signature exactly
public function handle(Request $request): Response {
```
### 3. Missing Parent Constructor Call
```php
// Parent:
class ControllerBase {
protected $logger;
public function __construct(LoggerInterface $logger) {
$this->logger = $logger;
}
}
// FLAW: Doesn't call parent constructor
class MyController extends ControllerBase {
public function __construct(LoggerInterface $logger, OtherService $other) {
$this->other = $other;
// BUG: $this->logger never set!
}
}
// FIX: Call parent::__construct()
public function __construct(LoggerInterface $logger, OtherService $other) {
parent::__construct($logger);
$this->other = $other;
}
```
### 4. Plugin Missing Required Annotation Fields
```php
// FLAW: Missing required 'id' field
#[Action(
label: new TranslatableMarkup("My Action")
)]
class MyAction extends ActionBase { }
// Plugin won't be discovered!
// FIX: Add all required fields
#[Action(
id: "my_action",
label: new TranslatableMarkup("My Action")
)]
```
---
## Dependency Errors
### 1. Method Doesn't Exist
```php
// FLAW: AI invented method name
$storage->loadMultipleByProperties(['status' => 1]);
// Method doesn't exist!
// FIX: Check actual interface
$storage->loadByProperties(['status' => 1]);
```
### 2. Wrong Return Type Assumption
```php
// FLAW: Assumes returns single entity
$mapping = $storage->loadByProperties(['id' => $id]);
$name = $mapping->label();
// FATAL: loadByProperties returns ARRAY, not entity
// FIX: Handle array return
$mappings = $storage->loadByProperties(['id' => $id]);
$mapping = reset($mappings);
if ($mapping) {
$name = $mapping->label();
}
```
### 3. Service Doesn't Exist
```php
// FLAW: Service not registered
public function __construct(
private CustomApiClient $apiClient // Not in container!
) {}
// FIX: Verify service exists in services.yml
// OR use correct service name
```
### 4. Missing Exception Handling
```php
// FLAW: External call can throw
$result = $httpClient->get($url);
$data = json_decode($result->getBody());
// If request fails: Uncaught exception crashes
// FIX: Wrap in try/catch
try {
$result = $httpClient->get($url);
$data = json_decode($result->getBody());
} catch (RequestException $e) {
// Handle error
}
```
---
## AI-Generated Code Specific
### 1. Hallucinated Methods
```php
// AI invents plausible method names:
$entity->getFieldValue('field_name'); // Doesn't exist
$user->getFullName(); // Doesn't exist
$service->fetchAllActive(); // Doesn't exist
// Actual methods:
$entity->get('field_name')->value;
$user->get('field_first_name')->value . ' ' . $user->get('field_last_name')->value;
$service->loadByProperties(['active' => TRUE]);
```
### 2. Mixed API Versions
```php
// AI uses Drupal 7 in Drupal 10 code:
$node = node_load($nid); // Old API!
$account = user_load($uid); // Old API!
// Correct D10:
$node = Node::load($nid);
$account = User::load($uid);
```
### 3. Wrong Parameter Order
```php
// AI guesses parameter order:
$url = Url::fromRoute($params, 'route.name'); // WRONG order
// Actual signature:
$url = Url::fromRoute('route.name', $params);
```
---
## Quick Flaw Detection Checklist
When tracing code, watch for:
**Variables:**
- [ ] Initialized before use?
- [ ] Used after assignment?
- [ ] Correct type?
**Conditionals:**
- [ ] Right comparison operator (=, ==, ===)?
- [ ] Logic correct (not inverted)?
- [ ] All branches return/handle?
**Loops:**
- [ ] Start/end indices correct?
- [ ] Variables reset each iteration?
- [ ] Handles empty collection?
**Function Calls:**
- [ ] Method exists?
- [ ] Parameters correct order/type?
- [ ] Return type handled correctly?
- [ ] Null/error cases handled?
**Objects:**
- [ ] Null check before property access?
- [ ] Object exists before method call?
**Arrays:**
- [ ] Key exists before access?
- [ ] Handles empty array?
**Contracts:**
- [ ] All abstract methods implemented?
- [ ] Interface signatures match?
- [ ] Parent constructors called?
---
## Flaw Documentation Template
```
FLAW FOUND:
Line [N]: [Description of issue]
Current behavior:
[What happens now]
Expected behavior:
[What should happen]
Root cause:
[Why the bug exists]
Impact:
[What breaks - fatal error? Wrong data? Security issue?]
FIX:
[Specific code change needed]
Test case to verify:
INPUT: [values that trigger bug]
EXPECTED: [correct output after fix]
```
```
### references/advanced-techniques.md
```markdown
# Advanced Paper Testing Techniques
Decision-focused techniques adapted from security tabletop exercises for complex code testing.
## Contents
- [When to Use Advanced Techniques](#when-to-use-advanced-techniques)
- [Progressive Inject Testing](#progressive-inject-testing)
- [Red Team Edge Case Discovery](#red-team-edge-case-discovery)
- [Attack Surface Analysis](#attack-surface-analysis)
- [Scenario-Based Workflow Testing](#scenario-based-workflow-testing)
- [After-Action Report Format](#after-action-report-format)
---
## When to Use Advanced Techniques
| Technique | Use When | Skip When | Benefit |
|-----------|----------|-----------|---------|
| **Progressive Injects** | Complex workflows, multiple dependencies, cascading failures | Simple functions, isolated logic | Finds interaction bugs, tests resilience |
| **Red Team Testing** | Security-critical code, public APIs, user input handling | Internal utilities, trusted inputs | Discovers edge cases developers miss |
| **Attack Surface** | Multiple entry points, prioritizing test effort | Single entry point | Focuses testing on highest risk |
| **Scenario Workflows** | End-to-end features, multi-step processes | Single operations | Tests realistic usage patterns |
| **AAR Format** | Complex findings, multiple issues, team review | Simple bugs | Structured documentation, root cause analysis |
**Decision Rule**: Use standard paper testing for simple code. Use advanced techniques when:
- Code has 3+ external dependencies
- Multiple components interact
- Security or reliability critical
- Complex business logic
- Real-world usage requires multi-step flows
---
## Progressive Inject Testing
### Concept
Test scenarios progressively increase in complexity through "inject cards" - each inject adds complications that force code to adapt.
Adapted from security tabletop exercises where facilitators inject new developments to test team adaptability.
### When to Use
- **Multi-step workflows**: Authentication, checkout, data processing pipelines
- **Dependency chains**: Service A calls Service B calls Service C
- **Resource constraints**: Memory, time, connections
- **Failure scenarios**: What happens when step 3 fails after steps 1-2 succeeded?
### Template
```
BASE SCENARIO: [Happy path]
INPUT: [Ideal conditions]
INJECT 1: [Edge case data]
What changes: [Describe complication]
Expected behavior: [How code should adapt]
INJECT 2: [Resource constraint]
What changes: [Describe limitation]
Expected behavior: [Graceful degradation]
INJECT 3: [Concurrent access]
What changes: [Race condition, timing]
Expected behavior: [Atomicity, locks]
INJECT 4: [Infrastructure failure]
What changes: [Service down, timeout]
Expected behavior: [Fallback, retry, error]
INJECT 5: [Requirement change]
What changes: [New validation rule]
Expected behavior: [Code flexibility]
```
### Example: User Authentication Flow
```
BASE SCENARIO: Valid user login
INPUT:
$username = "john_doe"
$password = "correct_password"
Database: Available, user exists
Session store: Available, empty
TRACE:
Line 10: $user = $userRepository->findByUsername($username)
→ Returns User{id: 42, password_hash: "$2y$..."}
Line 12: if (password_verify($password, $user->password_hash))
→ TRUE, password matches
Line 15: $sessionId = $sessionService->create($user->id)
→ Returns "sess_abc123"
Line 17: return new LoginResponse(success: true, sessionId: "sess_abc123")
OUTPUT: Successful login
FLAWS: None in base scenario
---
INJECT 1: Database is slow (3-second query time)
What changes: Database query takes 3 seconds
Expected: Timeout handling, user feedback
TRACE:
Line 10: $user = $userRepository->findByUsername($username)
→ Takes 3 seconds to return
DEPENDENCY CHECK: userRepository->findByUsername()
- Has timeout configured? [Need to verify]
- Read UserRepository.php
VERIFICATION:
- [ ] Query timeout set
- [ ] User shown loading state
- [ ] Timeout doesn't cause crash
FLAW FOUND:
No query timeout configured - could hang indefinitely
FIX: Add 5-second database query timeout
---
INJECT 2: Session store is full
What changes: $sessionService->create() fails
Expected: Error handling, user notification
TRACE:
Line 15: $sessionId = $sessionService->create($user->id)
→ THROWS SessionStorageException("Storage full")
Line 17: NOT REACHED (exception thrown)
FLAW FOUND:
No try/catch around session creation
User sees generic error page instead of helpful message
FIX: Wrap in try/catch, return LoginResponse(success: false, error: "Service unavailable")
---
INJECT 3: Concurrent login from different device
What changes: User logs in twice simultaneously
Expected: Invalidate old session or allow multiple sessions?
SCENARIO:
Request A: Login from laptop (started)
Request B: Login from phone (started 100ms later)
TRACE Request A:
Line 15: $sessionId = $sessionService->create($user->id)
→ Creates "sess_laptop_xyz"
TRACE Request B (concurrent):
Line 15: $sessionId = $sessionService->create($user->id)
→ Creates "sess_phone_abc"
RESULT: Both sessions active simultaneously
SECURITY CHECK:
- Is concurrent session allowed by policy? [Need business rule]
- Should old session be invalidated?
- Session hijacking risk if not tracked?
FLAW FOUND (POTENTIAL):
No policy enforcement on concurrent sessions
RECOMMENDATION: Define business rule, implement session limit or invalidation
---
INJECT 4: Password verification is timing-attackable
What changes: password_verify() takes different times for wrong vs. right password
Expected: Constant-time comparison
SECURITY ANALYSIS:
Line 12: if (password_verify($password, $user->password_hash))
password_verify() is DESIGNED to be constant-time (bcrypt property)
VERIFIED: Not vulnerable to timing attack
FLAW: None
---
INJECT 5: Requirement change: Now needs 2FA
What changes: After password verification, must check 2FA token
Expected: Code can be extended without major refactor
ANALYSIS:
Current code: password verify → create session → return
To add 2FA:
- Need to check if user has 2FA enabled
- If enabled, verify token before creating session
- Return different response if 2FA required
FLEXIBILITY CHECK:
- Can insert 2FA check between lines 12 and 15? YES
- Clean extension point or major refactor? CLEAN
FLAW: None - code structure allows clean extension
```
### Progressive Testing Findings Summary
```
PROGRESSIVE TEST RESULTS: User Authentication
Inject 1 (Slow Database):
❌ No query timeout - could hang indefinitely
FIX: Add 5-second timeout
Inject 2 (Session Storage Full):
❌ No exception handling - user sees generic error
FIX: Try/catch with user-friendly message
Inject 3 (Concurrent Login):
⚠️ No concurrent session policy
FIX: Define business rule, implement enforcement
Inject 4 (Timing Attack):
✅ password_verify() already constant-time
Inject 5 (2FA Requirement):
✅ Code structure allows clean extension
OVERALL: Found 2 bugs, 1 policy gap that wouldn't be found in basic testing.
```
---
## Red Team Edge Case Discovery
### Concept
Adversarial mindset: Think like an attacker trying to break code. Ask "What if I intentionally do the WRONG thing?"
Adapted from security red team exercises where attackers identify vulnerabilities.
### When to Use
- **Public APIs**: External input, untrusted sources
- **Security boundaries**: Authentication, authorization, payment
- **Resource management**: Memory, connections, files
- **State machines**: Can I force invalid state transitions?
### Red Team Questions
**Input Validation:**
- "What if I send null instead of object?"
- "What if I send empty string instead of required field?"
- "What if I send 1 billion characters?"
- "What if I send special characters: `<>\"'&;`?"
- "What if I send negative number for positive field?"
- "What if I send SQL injection: `'; DROP TABLE users;--`?"
- "What if I send script injection: `<script>alert('xss')</script>`?"
**Resource Exhaustion:**
- "What if I call this 10,000 times per second?"
- "What if I upload 10GB file?"
- "What if I create infinite loop by making entity reference itself?"
- "What if I exhaust database connections?"
- "What if I fill all available memory?"
**Race Conditions:**
- "What if I call this twice simultaneously?"
- "What if I modify data while it's being read?"
- "What if I delete object while another process uses it?"
**State Manipulation:**
- "What if I skip step 2 and go directly to step 3?"
- "What if I call finalize() before initialize()?"
- "What if I access object before it's fully constructed?"
- "What if I trigger the same state transition twice?"
**Privilege Escalation:**
- "What if I modify my user ID in the request?"
- "What if I access admin endpoint as regular user?"
- "What if I modify permission check to always return true?"
### Example: Red Team Testing Payment Processing
```
RED TEAM ANALYSIS: Payment Processing
CODE UNDER TEST:
```php
function processPayment($userId, $amount, $paymentMethod) {
$user = loadUser($userId);
$charge = $paymentGateway->charge($amount, $paymentMethod);
if ($charge->success) {
$order = createOrder($userId, $amount);
sendConfirmation($user->email);
return $order;
}
return null;
}
```
---
RED TEAM QUESTION 1: "What if I send negative amount?"
TRACE:
Line 2: $user = loadUser($userId)
→ Returns User{id: 42}
Line 3: $charge = $paymentGateway->charge(-100.00, $paymentMethod)
→ What does gateway do with negative amount?
DEPENDENCY CHECK: paymentGateway->charge()
Need to verify: Does it reject negative amounts or process as refund?
ASSUMPTION RISK:
If gateway processes -$100 as refund, attacker gets money!
FLAW FOUND:
No input validation on $amount
FIX: Add validation before line 3:
if ($amount <= 0) { throw new InvalidArgumentException(); }
---
RED TEAM QUESTION 2: "What if I send someone else's user ID?"
SCENARIO:
Attacker is user_id=999
Sends request with $userId=42 (different user)
TRACE:
Line 2: $user = loadUser(42)
→ Returns victim's user object
Line 3: $charge = $paymentGateway->charge($amount, $paymentMethod)
→ Whose payment method? Function parameter (attacker's)
Line 6: $order = createOrder(42, $amount)
→ Order assigned to user_id=42 (victim)
RESULT: Attacker pays with their card, victim gets the order!
FLAW FOUND:
No authorization check - userId from untrusted input
FIX: Verify $userId matches authenticated user session
---
RED TEAM QUESTION 3: "What if payment succeeds but email fails?"
TRACE:
Line 6: $order = createOrder($userId, $amount)
→ Order created, database committed
Line 7: sendConfirmation($user->email)
→ THROWS EmailServiceException
Line 8: NOT REACHED (exception thrown)
RESULT:
User charged, order created, but no confirmation sent
User doesn't know purchase succeeded
FLAW FOUND:
Email failure causes exception, user thinks payment failed
Could result in duplicate charges if user retries
FIX:
- Wrap email in try/catch, log failure but return success
- OR use async email queue with retry logic
- OR return order even if email fails, show success to user
---
RED TEAM QUESTION 4: "What if I call this twice simultaneously?"
SCENARIO:
Request A: processPayment(userId=42, amount=100)
Request B: processPayment(userId=42, amount=100) [100ms later]
BOTH TRACE:
Line 3: $charge = $paymentGateway->charge($amount, $paymentMethod)
Request A: Charges card $100, creates order
Request B: Charges card $100 AGAIN, creates DUPLICATE order
RESULT: User charged twice!
FLAW FOUND:
No idempotency key - duplicate requests process separately
FIX: Add idempotency check using request ID/nonce
---
RED TEAM SUMMARY:
ATTACK SURFACE: processPayment()
Entry points: 3 parameters (userId, amount, paymentMethod)
External dependencies: 4 (loadUser, paymentGateway, createOrder, sendConfirmation)
VULNERABILITIES FOUND:
1. Negative amount (input validation)
2. Arbitrary user ID (authorization)
3. Email failure causes user confusion (error handling)
4. Duplicate charges (idempotency)
SEVERITY:
CRITICAL: #1, #2 (financial impact, security)
MEDIUM: #4 (reliability)
LOW: #3 (user experience)
FIXES REQUIRED: 4
```
---
## Attack Surface Analysis
### Concept
Map all entry points to code, rank by risk exposure, prioritize testing effort.
Adapted from security attack surface analysis for threat modeling.
### When to Use
- **Multiple entry points**: APIs, forms, file uploads, message queues
- **Limited testing time**: Need to prioritize highest-risk areas
- **Security-critical systems**: Must identify exposed attack surface
- **Complex codebases**: Unclear where to focus testing effort
### Process
```
1. IDENTIFY ENTRY POINTS
- All inputs: user input, API calls, file reads, database queries, external services
- List every way data enters the system
2. RANK BY EXPOSURE
- External > Internal
- User-controlled > System-controlled
- Direct input > Derived data
3. PRIORITIZE TESTING
- HIGH: Public APIs, user forms, file uploads
- MEDIUM: Database queries, config files
- LOW: Internal service calls, hardcoded values
4. DOCUMENT ATTACK SURFACE
Entry point → Input type → Validation → Risk level
5. FOCUS PAPER TESTING
Test HIGH/MEDIUM entry points first with red team mindset
```
### Example
```
ATTACK SURFACE ANALYSIS: E-commerce Checkout
ENTRY POINTS IDENTIFIED:
1. POST /api/checkout
Input: {userId, cartItems[], shippingAddress, paymentMethod}
Validation: Schema validation only
Risk: HIGH (external, user-controlled, financial)
2. Database query: loadCart($userId)
Input: $userId from session
Validation: Integer type check
Risk: MEDIUM (internal but could be tampered session)
3. External API: paymentGateway->charge()
Input: amount, paymentMethod
Validation: Gateway's validation
Risk: MEDIUM (external dependency, financial)
4. File read: config/shipping_rates.yml
Input: File path (hardcoded)
Validation: None (trusted source)
Risk: LOW (internal, system-controlled)
RISK RANKING:
HIGH: /api/checkout endpoint
MEDIUM: loadCart query, paymentGateway call
LOW: config file read
TESTING PRIORITY:
1. Paper test /api/checkout with red team questions (highest risk)
2. Paper test loadCart with session tampering scenarios
3. Paper test paymentGateway error handling
4. Skip detailed testing of config file (low risk)
FOCUSED TESTING EFFORT:
Spend 60% of time on /api/checkout
Spend 30% on database/gateway interactions
Spend 10% on everything else
```
---
## Scenario-Based Workflow Testing
### Concept
Test complete user workflows end-to-end, not isolated functions. Catch integration bugs that unit tests miss.
### When to Use vs. Standard Testing
| Use Scenario Testing | Use Standard Testing |
|---------------------|---------------------|
| Multi-step workflows | Single operations |
| Component integration | Isolated logic |
| Realistic user behavior | Edge cases within function |
| Data handoff between services | Pure calculations |
### Template
```
SCENARIO: [User story - what user is trying to accomplish]
WORKFLOW STEPS:
1. [User action]
2. [System response]
3. [User action]
4. [System response]
...
TRACE COMPLETE FLOW:
Step 1: [Component A]
Line X: [code]
Output: [data passed to next component]
Step 2: [Component B receives data from A]
Line Y: [code]
Check: Does data format match expectations?
Output: [data passed to next component]
Step 3: [Component C receives data from B]
...
INTEGRATION CHECKS:
- [ ] Data format compatible between components?
- [ ] State consistent across steps?
- [ ] Error in step N handled by step N+1?
- [ ] Transaction boundaries correct?
FLAWS: [Issues found in integration, not individual components]
```
### Example
```
SCENARIO: User purchases item with discount coupon
WORKFLOW:
1. User adds item to cart
2. User enters coupon code
3. System validates coupon
4. System recalculates total
5. User proceeds to payment
6. Payment processes
7. Inventory updates
8. Order confirmation sent
---
STEP 1: Add item to cart (CartService)
Line 42: $cart = $this->getOrCreateCart($sessionId)
→ Returns Cart{id: 1, items: []}
Line 43: $cart->addItem($productId, $quantity)
→ Adds Product{id: 100, price: 50.00, qty: 2}
OUTPUT TO NEXT STEP:
Cart state: {items: [{productId: 100, price: 50.00, qty: 2}]}
---
STEP 2: Enter coupon code (CouponController)
INPUT FROM USER: "SAVE20"
Line 55: $coupon = $couponRepository->findByCode("SAVE20")
→ Returns Coupon{code: "SAVE20", discount: 20%, validUntil: "2025-12-31"}
OUTPUT TO NEXT STEP:
Coupon object passed to validation
---
STEP 3: Validate coupon (CouponValidator)
INPUT: Coupon{code: "SAVE20"}, Cart{items: [...]}
Line 67: if ($coupon->isExpired())
→ validUntil: 2025-12-31, today: 2025-12-26
→ FALSE, not expired
Line 69: if ($coupon->minimumPurchase > $cart->getSubtotal())
→ minimumPurchase: 25, subtotal: 100
→ FALSE, meets minimum
Line 71: $coupon->markAsUsed($userId)
→ Updates database: usage_count++
OUTPUT TO NEXT STEP:
Valid coupon, ready for discount calculation
---
STEP 4: Recalculate total (PriceCalculator)
INPUT: Cart{subtotal: 100}, Coupon{discount: 20%}
Line 80: $subtotal = $cart->getSubtotal()
→ $subtotal = 100.00
Line 81: $discount = $subtotal * ($coupon->discount / 100)
→ $discount = 100 * 0.20 = 20.00
Line 82: $total = $subtotal - $discount
→ $total = 80.00
OUTPUT TO NEXT STEP:
Final total: 80.00
INTEGRATION CHECK:
- [ ] Is $cart->getSubtotal() cached or recalculated?
- [ ] What if items added AFTER coupon applied?
POTENTIAL FLAW:
If user adds items after applying coupon, is discount recalculated?
Need to verify discount persists correctly.
---
STEP 5: Process payment (PaymentService)
INPUT: Total: 80.00, PaymentMethod: {...}
Line 95: $charge = $gateway->charge(80.00, $paymentMethod)
→ Returns Charge{success: true, chargeId: "ch_abc"}
OUTPUT TO NEXT STEP:
Successful charge, ready to create order
---
STEP 6: Create order (OrderService)
INPUT: Cart{items: [...]}, Charge{chargeId: "ch_abc"}
Line 110: $order = new Order()
Line 111: $order->setItems($cart->getItems())
Line 112: $order->setTotal(80.00)
Line 113: $order->setChargeId("ch_abc")
Line 114: $order->save()
OUTPUT TO NEXT STEP:
Order{id: 5001, status: "pending"}
---
STEP 7: Update inventory (InventoryService)
INPUT: Order{items: [{productId: 100, qty: 2}]}
Line 125: foreach ($order->getItems() as $item)
Line 126: $product = $productRepository->find($item->productId)
→ Product{id: 100, stock: 50}
Line 127: $product->decrementStock($item->quantity)
→ stock: 50 - 2 = 48
Line 128: $product->save()
OUTPUT TO NEXT STEP:
Inventory updated, ready to send confirmation
---
STEP 8: Send confirmation (EmailService)
INPUT: Order{id: 5001}, User{email: "[email protected]"}
Line 140: $email = new OrderConfirmationEmail($order, $user)
Line 141: $this->mailer->send($email)
→ Email sent successfully
OUTPUT: Workflow complete
---
INTEGRATION FLAWS FOUND:
1. Coupon discount persistence (Step 4):
If user adds items after applying coupon, discount might not recalculate
FIX: Recalculate discount on every cart change
2. Transaction boundaries:
Payment succeeds (Step 5) but inventory update could fail (Step 7)
User charged but order not fulfilled
FIX: Wrap Steps 5-7 in database transaction
3. Email failure (Step 8):
If email fails, user doesn't know order succeeded
Could lead to duplicate orders if user retries
FIX: Return success even if email fails, retry email async
SCENARIO TEST RESULT:
Found 3 integration bugs that wouldn't appear in unit tests of individual components.
```
---
## After-Action Report Format
### Concept
Structured documentation of paper test findings with root cause analysis and improvement plan.
Adapted from security exercise after-action reports (AAR).
### When to Use
- **Complex findings**: Multiple issues found
- **Team review**: Sharing results with team
- **Root cause analysis**: Need to understand why bugs exist
- **Improvement tracking**: Assign owners and due dates
### Template
```
PAPER TEST AFTER-ACTION REPORT
Date: [YYYY-MM-DD]
Tester: [Name]
Code: [File/Function/Feature tested]
---
OBJECTIVES:
What capabilities were being validated:
- [ ] Objective 1
- [ ] Objective 2
- [ ] Objective 3
---
TEST SCENARIOS EXECUTED:
1. [Scenario name] - [Result: PASS/FAIL]
2. [Scenario name] - [Result: PASS/FAIL]
3. [Scenario name] - [Result: PASS/FAIL]
---
STRENGTHS IDENTIFIED:
✅ [What worked well]
✅ [Effective patterns found]
✅ [Good practices observed]
---
GAPS IDENTIFIED:
❌ [Gap 1]: [Description]
Impact: [What breaks, severity]
Location: [File:Line]
❌ [Gap 2]: [Description]
Impact: [What breaks, severity]
Location: [File:Line]
---
ROOT CAUSE ANALYSIS:
Gap 1:
Why it exists: [Developer didn't know requirement | Oversight | Time pressure]
Pattern: [Is this a repeated mistake?]
Gap 2:
Why it exists: [...]
Pattern: [...]
---
IMPROVEMENT PLAN:
| Issue | Fix | Owner | Due Date | Status |
|-------|-----|-------|----------|--------|
| Gap 1 | [Specific code change] | Dev A | 2025-12-30 | Open |
| Gap 2 | [Specific code change] | Dev B | 2025-12-31 | Open |
---
LESSONS LEARNED:
- [Pattern to avoid in future]
- [Best practice to adopt]
- [Testing approach to use]
---
FOLLOW-UP ACTIONS:
- [ ] Update coding standards document
- [ ] Add validation rule to checklist
- [ ] Schedule follow-up review on [date]
```
### Example AAR
```
PAPER TEST AFTER-ACTION REPORT
Date: 2025-12-26
Tester: Senior Developer
Code: src/Payment/PaymentProcessor.php::processPayment()
---
OBJECTIVES:
Validate payment processing security and reliability:
✅ Input validation prevents malicious inputs
✅ Error handling prevents user confusion
✅ Concurrency handled correctly
❌ Authorization checks prevent unauthorized payments
---
TEST SCENARIOS EXECUTED:
1. Happy path (valid payment) - PASS
2. Negative amount attack - FAIL
3. Unauthorized user ID - FAIL
4. Email service failure - FAIL
5. Concurrent duplicate requests - FAIL
---
STRENGTHS IDENTIFIED:
✅ Payment gateway integration is clean and testable
✅ Order creation logic is atomic with proper transactions
✅ Code structure allows clean extension for new payment methods
---
GAPS IDENTIFIED:
❌ Gap 1: No input validation on payment amount
Impact: Attacker could send negative amount, potentially receive money (CRITICAL)
Location: PaymentProcessor.php:15
❌ Gap 2: No authorization check on user ID
Impact: Attacker could purchase for different user (CRITICAL)
Location: PaymentProcessor.php:12
❌ Gap 3: Email failure causes exception that confuses user
Impact: User thinks payment failed, might retry and get charged twice (MEDIUM)
Location: PaymentProcessor.php:25
❌ Gap 4: No idempotency protection
Impact: Duplicate requests process separately, double charge (HIGH)
Location: PaymentProcessor.php:15-28 (entire function)
---
ROOT CAUSE ANALYSIS:
Gap 1 (Input validation):
Why: Developer assumed frontend validates input
Pattern: Repeated mistake - trusting client-side validation
Systemic issue: Missing security requirement in specs
Gap 2 (Authorization):
Why: Developer didn't consider attack scenario
Pattern: First occurrence - payment processing is new feature
Systemic issue: No security review process
Gap 3 (Email error handling):
Why: Time pressure - shipped without complete error handling
Pattern: Repeated - other features also have poor error handling
Systemic issue: Need better error handling patterns
Gap 4 (Idempotency):
Why: Developer unaware of idempotency requirement
Pattern: First occurrence
Systemic issue: Payment processing best practices not documented
---
IMPROVEMENT PLAN:
| Issue | Fix | Owner | Due Date | Status |
|-------|-----|-------|----------|--------|
| Gap 1 | Add amount validation: if ($amount <= 0) throw InvalidArgumentException | Alice | 2025-12-27 | Open |
| Gap 2 | Add auth check: if ($userId !== $authenticatedUserId) throw UnauthorizedException | Alice | 2025-12-27 | Open |
| Gap 3 | Wrap email in try/catch, log error but return success | Bob | 2025-12-28 | Open |
| Gap 4 | Add idempotency key check using Redis cache | Charlie | 2025-12-30 | Open |
| Pattern | Document payment processing security checklist | Alice | 2026-01-05 | Open |
| Pattern | Implement security review for all payment features | Team Lead | 2026-01-10 | Open |
---
LESSONS LEARNED:
- NEVER trust client-side validation - always validate server-side
- Payment processing requires security review before deployment
- Error handling must not confuse users into duplicate actions
- Idempotency is critical for financial operations
- Paper testing with red team mindset found 4 critical issues before production
---
FOLLOW-UP ACTIONS:
- [x] Create GitHub issues for all gaps
- [ ] Update payment processing documentation with security requirements
- [ ] Add payment security checklist to code review template
- [ ] Schedule security training session on financial transaction best practices
- [ ] Re-test after fixes implemented (scheduled: 2025-12-31)
```
---
## Summary
These advanced techniques complement standard paper testing for complex scenarios:
**Progressive Injects**: Add complications incrementally to test resilience
**Red Team**: Adversarial mindset finds edge cases developers miss
**Attack Surface**: Prioritize testing effort on highest-risk entry points
**Scenario Workflows**: Test end-to-end integration, not isolated components
**AAR Format**: Structure findings for team review and improvement tracking
Use when code complexity, security criticality, or multi-component integration demands more rigorous analysis than standard paper testing provides.
```