ima-claude 2.20.0 → 2.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +74 -9
  2. package/dist/cli.js +2 -1
  3. package/package.json +1 -1
  4. package/plugins/ima-claude/.claude-plugin/plugin.json +2 -2
  5. package/plugins/ima-claude/agents/explorer.md +29 -15
  6. package/plugins/ima-claude/agents/implementer.md +58 -13
  7. package/plugins/ima-claude/agents/memory.md +19 -19
  8. package/plugins/ima-claude/agents/reviewer.md +84 -34
  9. package/plugins/ima-claude/agents/tester.md +59 -16
  10. package/plugins/ima-claude/agents/wp-developer.md +66 -21
  11. package/plugins/ima-claude/hooks/bootstrap.sh +42 -44
  12. package/plugins/ima-claude/hooks/prompt_coach_digest.md +14 -17
  13. package/plugins/ima-claude/hooks/prompt_coach_system.md +10 -12
  14. package/plugins/ima-claude/personalities/README.md +17 -6
  15. package/plugins/ima-claude/personalities/enable-efficient.md +61 -0
  16. package/plugins/ima-claude/personalities/enable-terse.md +71 -0
  17. package/plugins/ima-claude/skills/agentic-workflows/SKILL.md +35 -71
  18. package/plugins/ima-claude/skills/architect/SKILL.md +54 -168
  19. package/plugins/ima-claude/skills/compound-bridge/SKILL.md +41 -94
  20. package/plugins/ima-claude/skills/design-to-code/SKILL.md +43 -78
  21. package/plugins/ima-claude/skills/discourse/SKILL.md +79 -194
  22. package/plugins/ima-claude/skills/discourse-admin/SKILL.md +41 -103
  23. package/plugins/ima-claude/skills/docs-organize/SKILL.md +63 -203
  24. package/plugins/ima-claude/skills/ember-discourse/SKILL.md +90 -200
  25. package/plugins/ima-claude/skills/espocrm/SKILL.md +14 -23
  26. package/plugins/ima-claude/skills/espocrm-api/SKILL.md +79 -192
  27. package/plugins/ima-claude/skills/functional-programmer/SKILL.md +33 -237
  28. package/plugins/ima-claude/skills/gh-cli/SKILL.md +26 -65
  29. package/plugins/ima-claude/skills/ima-bootstrap/SKILL.md +71 -104
  30. package/plugins/ima-claude/skills/ima-bootstrap/references/ima-brand.md +32 -22
  31. package/plugins/ima-claude/skills/ima-brand/SKILL.md +18 -23
  32. package/plugins/ima-claude/skills/ima-copywriting/SKILL.md +68 -179
  33. package/plugins/ima-claude/skills/ima-doc2pdf/SKILL.md +32 -102
  34. package/plugins/ima-claude/skills/ima-editorial-scorecard/SKILL.md +38 -63
  35. package/plugins/ima-claude/skills/ima-editorial-workflow/SKILL.md +69 -114
  36. package/plugins/ima-claude/skills/ima-email-creator/SKILL.md +16 -22
  37. package/plugins/ima-claude/skills/ima-forms-expert/SKILL.md +21 -37
  38. package/plugins/ima-claude/skills/ima-git/SKILL.md +81 -0
  39. package/plugins/ima-claude/skills/jira-checkpoint/SKILL.md +39 -120
  40. package/plugins/ima-claude/skills/jquery/SKILL.md +107 -233
  41. package/plugins/ima-claude/skills/js-fp/SKILL.md +75 -296
  42. package/plugins/ima-claude/skills/js-fp-api/SKILL.md +52 -162
  43. package/plugins/ima-claude/skills/js-fp-react/SKILL.md +47 -270
  44. package/plugins/ima-claude/skills/js-fp-vue/SKILL.md +55 -209
  45. package/plugins/ima-claude/skills/js-fp-wordpress/SKILL.md +59 -204
  46. package/plugins/ima-claude/skills/livecanvas/SKILL.md +19 -32
  47. package/plugins/ima-claude/skills/mcp-atlassian/SKILL.md +92 -162
  48. package/plugins/ima-claude/skills/mcp-context7/SKILL.md +32 -64
  49. package/plugins/ima-claude/skills/mcp-gitea/SKILL.md +98 -188
  50. package/plugins/ima-claude/skills/mcp-github/SKILL.md +60 -124
  51. package/plugins/ima-claude/skills/mcp-memory/SKILL.md +1 -177
  52. package/plugins/ima-claude/skills/mcp-qdrant/SKILL.md +58 -115
  53. package/plugins/ima-claude/skills/mcp-sequential/SKILL.md +32 -87
  54. package/plugins/ima-claude/skills/mcp-serena/SKILL.md +54 -80
  55. package/plugins/ima-claude/skills/mcp-tavily/SKILL.md +40 -63
  56. package/plugins/ima-claude/skills/mcp-vestige/SKILL.md +75 -116
  57. package/plugins/ima-claude/skills/php-authnet/SKILL.md +32 -65
  58. package/plugins/ima-claude/skills/php-fp/SKILL.md +50 -129
  59. package/plugins/ima-claude/skills/php-fp-wordpress/SKILL.md +25 -73
  60. package/plugins/ima-claude/skills/phpunit-wp/SKILL.md +103 -463
  61. package/plugins/ima-claude/skills/playwright/SKILL.md +69 -220
  62. package/plugins/ima-claude/skills/prompt-starter/SKILL.md +33 -83
  63. package/plugins/ima-claude/skills/prompt-starter/references/code-review.md +38 -0
  64. package/plugins/ima-claude/skills/py-fp/SKILL.md +78 -384
  65. package/plugins/ima-claude/skills/quasar-fp/SKILL.md +54 -255
  66. package/plugins/ima-claude/skills/quickstart/SKILL.md +7 -11
  67. package/plugins/ima-claude/skills/rails/SKILL.md +63 -184
  68. package/plugins/ima-claude/skills/resume-session/SKILL.md +14 -35
  69. package/plugins/ima-claude/skills/rg/SKILL.md +61 -146
  70. package/plugins/ima-claude/skills/ruby-fp/SKILL.md +66 -163
  71. package/plugins/ima-claude/skills/save-session/SKILL.md +10 -39
  72. package/plugins/ima-claude/skills/scorecard/SKILL.md +42 -40
  73. package/plugins/ima-claude/skills/skill-analyzer/SKILL.md +42 -71
  74. package/plugins/ima-claude/skills/skill-creator/SKILL.md +79 -250
  75. package/plugins/ima-claude/skills/task-master/SKILL.md +11 -31
  76. package/plugins/ima-claude/skills/task-planner/SKILL.md +44 -153
  77. package/plugins/ima-claude/skills/task-runner/SKILL.md +61 -143
  78. package/plugins/ima-claude/skills/unit-testing/SKILL.md +59 -134
  79. package/plugins/ima-claude/skills/wp-ddev/SKILL.md +38 -120
  80. package/plugins/ima-claude/skills/wp-local/SKILL.md +26 -108
@@ -5,46 +5,28 @@ description: "PHPUnit testing for WordPress plugins with FP principles - fast un
5
5
 
6
6
  # PHPUnit for WordPress Plugins
7
7
 
8
- Expert guidance for PHPUnit testing in WordPress plugins, emphasizing pure function testing, minimal mocking, and FP principles.
9
-
10
- ## When to Use This Skill
11
-
12
- - Setting up PHPUnit for new WordPress plugins
13
- - Debugging silent/hanging test runs
14
- - Writing testable pure functions
15
- - Mocking WordPress functions correctly
16
- - Testing with FP principles
17
-
18
8
  ## Core Philosophy
19
9
 
20
- **Test pure functions, not WordPress integration.**
10
+ Test pure functions, not WordPress integration.
21
11
 
22
- 1. **Pure business logic** = Fast unit tests (<100ms, no WordPress)
23
- 2. **WordPress wrappers** = Integration tests (slower, full WP environment)
24
- 3. **Mock minimally** = Only mock what's needed for pure function context
12
+ | Layer | Approach | Speed |
13
+ |-------|----------|-------|
14
+ | Pure business logic | Unit tests, no WordPress | <100ms |
15
+ | WordPress wrappers | Integration tests, full WP env | Seconds |
16
+ | Mocks | Only what pure functions actually use | Minimal |
25
17
 
26
- **Foundation**: Reference `../php-fp/SKILL.md` and `../php-fp-wordpress/SKILL.md` for FP patterns.
18
+ References: `../php-fp/SKILL.md`, `../php-fp-wordpress/SKILL.md`
27
19
 
28
20
  ---
29
21
 
30
- ## THE TWO CRITICAL SETUP BUGS (We Keep Forgetting!)
22
+ ## THE TWO CRITICAL SETUP BUGS
31
23
 
32
- ### 🐛 Bug #1: Silent PHPUnit Execution
24
+ ### Bug #1: Silent PHPUnit Execution
33
25
 
34
- **Symptom**: `composer test` produces ZERO output, not even "no tests found"
26
+ **Symptom**: `composer test` produces zero output
35
27
 
36
- **Root Cause**: PHPUnit 9.x doesn't output anything without `--testdox` flag
28
+ **Cause**: PHPUnit 9.x requires `--testdox` for visible output
37
29
 
38
- **❌ BROKEN** (silent execution):
39
- ```json
40
- {
41
- "scripts": {
42
- "test": "phpunit"
43
- }
44
- }
45
- ```
46
-
47
- **✅ FIXED** (visible output):
48
30
  ```json
49
31
  {
50
32
  "scripts": {
@@ -54,46 +36,25 @@ Expert guidance for PHPUnit testing in WordPress plugins, emphasizing pure funct
54
36
  }
55
37
  ```
56
38
 
57
- ---
58
-
59
- ### 🐛 Bug #2: Autoload Files Kill Tests
39
+ ### Bug #2: Autoload Files Kill Tests
60
40
 
61
- **Symptom**: Tests hang or exit silently with no error message
41
+ **Symptom**: Tests hang or exit silently
62
42
 
63
- **Root Cause**: Composer autoload runs BEFORE test bootstrap defines `ABSPATH`
43
+ **Cause**: Composer autoload runs BEFORE bootstrap defines `ABSPATH`
64
44
 
65
- **The Fatal Flow**:
66
45
  ```
67
- 1. bootstrap.php line 17: require vendor/autoload.php
68
- 2. Composer sees: "autoload": { "files": ["includes/helpers.php"] }
69
- 3. Composer loads helpers.php IMMEDIATELY
70
- 4. helpers.php line 14: if (!defined('ABSPATH')) { exit; }
71
- 5. ABSPATH not defined yet (happens bootstrap.php line 22)
72
- 6. Script exits silently
73
- 7. PHPUnit never starts
46
+ 1. bootstrap.php requires vendor/autoload.php
47
+ 2. Composer loads autoload "files" immediately
48
+ 3. helpers.php: if (!defined('ABSPATH')) { exit; }
49
+ 4. ABSPATH not yet defined silent exit → PHPUnit never starts
74
50
  ```
75
51
 
76
- **❌ BROKEN** (causes silent exit):
77
- ```json
78
- {
79
- "autoload": {
80
- "files": [
81
- "includes/helpers/url-validation.php",
82
- "includes/helpers/share-urls.php"
83
- ]
84
- }
85
- }
86
- ```
52
+ Fix: Remove `autoload.files`, load helpers manually in bootstrap AFTER defining `ABSPATH`.
87
53
 
88
- **✅ FIXED** (no autoload files):
89
54
  ```json
90
55
  {
91
- // NO autoload section with files array!
92
- // Bootstrap loads helpers manually AFTER defining ABSPATH
93
56
  "autoload-dev": {
94
- "psr-4": {
95
- "MyPlugin\\Tests\\": "tests/"
96
- }
57
+ "psr-4": { "MyPlugin\\Tests\\": "tests/" }
97
58
  }
98
59
  }
99
60
  ```
@@ -102,90 +63,43 @@ Expert guidance for PHPUnit testing in WordPress plugins, emphasizing pure funct
102
63
 
103
64
  ## Environment Setup
104
65
 
105
- ### Running Tests in Local WP Environment
106
-
107
- **Important**: Composer and PHPUnit need Local WP's PHP environment. Git commands work in normal terminal.
108
-
109
- **Use the project's configured Local WP environment**:
110
-
111
- The `wp-local` skill provides the environment loader pattern. For composer/phpunit commands, create a similar wrapper or use the pattern directly:
66
+ Composer/PHPUnit require Local WP's PHP environment. Git commands work in normal terminal.
112
67
 
113
68
  ```bash
114
- # Pattern from wp-local skill:
115
- # 1. Sources ~/kitty/load-localwp-env.sh with site name from $WP_LOCAL_SITE or .wp-local
116
- # 2. Runs command in that environment
69
+ # Run tests in Local WP environment
70
+ bash -c "source ~/kitty/load-localwp-env.sh $(cat .wp-local || echo $WP_LOCAL_SITE) && composer test"
117
71
 
118
- # Example - Run tests:
119
- bash -c "source ~/kitty/load-localwp-env.sh \$(cat .wp-local || echo \$WP_LOCAL_SITE) && composer test"
120
-
121
- # Or install dependencies:
122
- bash -c "source ~/kitty/load-localwp-env.sh \$(cat .wp-local || echo \$WP_LOCAL_SITE) && composer install"
72
+ # Useful aliases (~/.zshrc)
73
+ alias wptest='bash -c "source ~/kitty/load-localwp-env.sh $(cat .wp-local || echo $WP_LOCAL_SITE) && composer test"'
74
+ alias wpcomposer='bash -c "source ~/kitty/load-localwp-env.sh $(cat .wp-local || echo $WP_LOCAL_SITE) && composer $@"'
123
75
  ```
124
76
 
125
- **Pro Tip**: Create a shell alias in your `~/.bashrc` or `~/.zshrc`:
126
- ```bash
127
- alias wptest='bash -c "source ~/kitty/load-localwp-env.sh \$(cat .wp-local || echo \$WP_LOCAL_SITE) && composer test"'
128
- alias wpcomposer='bash -c "source ~/kitty/load-localwp-env.sh \$(cat .wp-local || echo \$WP_LOCAL_SITE) && composer $@"'
129
- ```
130
-
131
- Then simply:
132
- ```bash
133
- wptest # Run tests
134
- wpcomposer install # Install dependencies
135
- ```
136
-
137
- ### Environment Configuration
77
+ **Site resolution priority**: `$WP_LOCAL_SITE` env var `.wp-local` file error
138
78
 
139
- **Priority order** (same as wp-local):
140
- 1. `$WP_LOCAL_SITE` environment variable (set by Kitty terminal)
141
- 2. `.wp-local` file in project root (site UUID like `19efkkzWB`)
142
- 3. Error if neither configured
79
+ ### Quick Diagnosis
143
80
 
144
- **For git operations** (no environment needed):
145
81
  ```bash
146
- # Regular terminal works fine - git doesn't need PHP environment
147
- git status
148
- git commit -m "fix: phpunit setup"
149
- git push
150
- ```
151
-
152
- ### Quick Test Diagnosis
153
-
154
- ```bash
155
- # 1. Check if bootstrap prints (should see "Bootstrap Loaded")
156
- php tests/bootstrap.php
157
-
158
- # 2. Check if PHPUnit is installed
159
- ls -la vendor/bin/phpunit
160
-
161
- # 3. Check composer.json scripts
82
+ php tests/bootstrap.php # Should print "Bootstrap Loaded"
83
+ ls -la vendor/bin/phpunit # Verify phpunit installed
162
84
  cat composer.json | grep -A 3 scripts
163
-
164
- # 4. Check for autoload files bug
165
85
  cat composer.json | grep -A 5 autoload
166
86
  ```
167
87
 
168
88
  ---
169
89
 
170
- ## Copy-Paste Working Template
90
+ ## Working Templates
171
91
 
172
92
  ### composer.json
93
+
173
94
  ```json
174
95
  {
175
96
  "name": "ima-network/my-plugin",
176
- "description": "Plugin description",
177
97
  "type": "wordpress-plugin",
178
98
  "license": "GPL-2.0-or-later",
179
- "require": {
180
- "php": ">=7.4"
181
- },
182
- "require-dev": {
183
- "phpunit/phpunit": "^9.5"
184
- },
99
+ "require": { "php": ">=7.4" },
100
+ "require-dev": { "phpunit/phpunit": "^9.5" },
185
101
  "autoload-dev": {
186
- "psr-4": {
187
- "MyPlugin\\Tests\\": "tests/"
188
- }
102
+ "psr-4": { "MyPlugin\\Tests\\": "tests/" }
189
103
  },
190
104
  "scripts": {
191
105
  "test": "phpunit --colors=always --testdox",
@@ -195,6 +109,7 @@ cat composer.json | grep -A 5 autoload
195
109
  ```
196
110
 
197
111
  ### phpunit.xml
112
+
198
113
  ```xml
199
114
  <?xml version="1.0" encoding="UTF-8"?>
200
115
  <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
@@ -225,236 +140,126 @@ cat composer.json | grep -A 5 autoload
225
140
  ```
226
141
 
227
142
  ### tests/bootstrap.php
143
+
228
144
  ```php
229
145
  <?php
230
- /**
231
- * PHPUnit Bootstrap for My Plugin
232
- *
233
- * Loads pure functions for unit testing without WordPress dependencies.
234
- */
235
146
  declare(strict_types=1);
236
147
 
237
- // 1. Load Composer autoloader FIRST (but it won't autoload files - we removed that!)
148
+ // 1. Autoloader first (no autoload.files removed!)
238
149
  require_once dirname(__DIR__) . '/vendor/autoload.php';
239
150
 
240
- // 2. Define ABSPATH to prevent plugin file exits
151
+ // 2. Define ABSPATH before loading any plugin files
241
152
  if (!defined('ABSPATH')) {
242
153
  define('ABSPATH', '/tmp/wordpress/');
243
154
  }
244
155
 
245
- // 3. Define plugin constants
156
+ // 3. Plugin constants
246
157
  if (!defined('MY_PLUGIN_PATH')) {
247
158
  define('MY_PLUGIN_PATH', dirname(__DIR__) . '/');
248
159
  }
249
160
 
250
- // 4. NOW manually load helper files (AFTER ABSPATH defined!)
161
+ // 4. Load helpers manually (AFTER ABSPATH)
251
162
  require_once MY_PLUGIN_PATH . 'includes/helpers/url-validation.php';
252
163
  require_once MY_PLUGIN_PATH . 'includes/helpers/share-urls.php';
253
164
 
254
- // 5. Mock WordPress functions minimally
165
+ // 5. Minimal WP function mocks
255
166
  if (!function_exists('home_url')) {
256
167
  function home_url(string $path = ''): string {
257
168
  return 'https://example.com' . $path;
258
169
  }
259
170
  }
260
-
261
171
  if (!function_exists('sanitize_text_field')) {
262
172
  function sanitize_text_field(string $str): string {
263
- $filtered = strip_tags($str);
264
- $filtered = str_replace(["\r", "\n"], '', $filtered);
265
- return trim($filtered);
173
+ return trim(str_replace(["\r", "\n"], '', strip_tags($str)));
266
174
  }
267
175
  }
268
-
269
176
  if (!function_exists('esc_html')) {
270
177
  function esc_html(string $text): string {
271
178
  return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
272
179
  }
273
180
  }
274
181
 
275
- // 6. Confirmation message (proves bootstrap loaded)
276
- echo "✅ My Plugin Test Bootstrap Loaded\n";
182
+ echo "My Plugin Test Bootstrap Loaded\n";
277
183
  ```
278
184
 
279
185
  ---
280
186
 
281
- ## What to Test (FP Principles)
187
+ ## What to Test
282
188
 
283
- ### DO TEST: Pure Business Logic
189
+ ### DO: Pure Business Logic
284
190
 
285
191
  ```php
286
192
  <?php
287
- // includes/helpers/validation.php
288
- declare(strict_types=1);
289
-
290
- /**
291
- * PURE function - Zero WordPress dependencies
292
- * Perfect for unit testing
293
- */
193
+ // Pure function — zero WordPress dependencies, perfect for unit testing
294
194
  function my_plugin_validate_email_pure(string $email): array {
295
- if (empty($email)) {
296
- return ['valid' => false, 'error' => 'Email required'];
297
- }
298
-
299
- if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
300
- return ['valid' => false, 'error' => 'Invalid email format'];
301
- }
195
+ if (empty($email)) return ['valid' => false, 'error' => 'Email required'];
196
+ if (!filter_var($email, FILTER_VALIDATE_EMAIL)) return ['valid' => false, 'error' => 'Invalid email format'];
302
197
 
303
- // Check for disposable domains
304
198
  $disposable = ['tempmail.com', 'throwaway.email'];
305
199
  $domain = substr(strrchr($email, '@'), 1);
306
- if (in_array($domain, $disposable)) {
307
- return ['valid' => false, 'error' => 'Disposable email not allowed'];
308
- }
200
+ if (in_array($domain, $disposable)) return ['valid' => false, 'error' => 'Disposable email not allowed'];
309
201
 
310
202
  return ['valid' => true];
311
203
  }
312
204
  ```
313
205
 
314
- **Test**:
315
206
  ```php
316
207
  <?php
317
- // tests/Unit/ValidationTest.php
318
- use PHPUnit\Framework\TestCase;
319
-
320
208
  class ValidationTest extends TestCase {
321
209
  public function test_empty_email_returns_error() {
322
210
  $result = my_plugin_validate_email_pure('');
323
-
324
211
  $this->assertFalse($result['valid']);
325
212
  $this->assertEquals('Email required', $result['error']);
326
213
  }
327
214
 
328
- public function test_invalid_format_returns_error() {
329
- $result = my_plugin_validate_email_pure('not-an-email');
330
-
331
- $this->assertFalse($result['valid']);
332
- $this->assertEquals('Invalid email format', $result['error']);
333
- }
334
-
335
- public function test_disposable_email_rejected() {
336
- $result = my_plugin_validate_email_pure('user@tempmail.com');
337
-
338
- $this->assertFalse($result['valid']);
339
- $this->assertEquals('Disposable email not allowed', $result['error']);
340
- }
341
-
342
215
  public function test_valid_email_passes() {
343
216
  $result = my_plugin_validate_email_pure('user@example.com');
344
-
345
217
  $this->assertTrue($result['valid']);
346
- $this->assertArrayNotHasKey('error', $result);
347
218
  }
348
219
 
349
- // FP Principle: Test determinism
350
220
  public function test_function_is_deterministic() {
351
- $email = 'test@example.com';
352
- $result1 = my_plugin_validate_email_pure($email);
353
- $result2 = my_plugin_validate_email_pure($email);
354
-
355
- $this->assertEquals($result1, $result2);
221
+ $this->assertEquals(
222
+ my_plugin_validate_email_pure('test@example.com'),
223
+ my_plugin_validate_email_pure('test@example.com')
224
+ );
356
225
  }
357
226
  }
358
227
  ```
359
228
 
360
- ---
361
-
362
- ### ❌ DON'T TEST: WordPress Integration Wrappers
229
+ ### DON'T: WordPress Integration Wrappers
363
230
 
364
231
  ```php
365
232
  <?php
366
- // includes/ajax-handlers.php - WordPress wrapper
233
+ // Skip unit testing this — it's 100% WordPress integration
367
234
  function my_plugin_ajax_validate_email() {
368
- // DON'T unit test this - it's all WordPress integration!
369
235
  check_ajax_referer('validate_email_nonce', 'nonce');
370
-
371
- if (!current_user_can('read')) {
372
- wp_send_json_error('Unauthorized', 403);
373
- }
374
-
375
- $email = sanitize_email($_POST['email']);
376
-
377
- // Call pure function (which IS tested)
378
- $result = my_plugin_validate_email_pure($email);
379
-
380
- wp_send_json_success($result);
236
+ if (!current_user_can('read')) wp_send_json_error('Unauthorized', 403);
237
+ wp_send_json_success(my_plugin_validate_email_pure(sanitize_email($_POST['email'])));
381
238
  }
382
- add_action('wp_ajax_my_plugin_validate_email', 'my_plugin_ajax_validate_email');
383
239
  ```
384
240
 
385
- **Why not test**: This is 100% WordPress integration. Testing it requires:
386
- - Full WordPress environment
387
- - Mocking `$_POST`, nonces, capabilities, AJAX functions
388
- - Complex setup that's brittle and slow
389
-
390
- **Better approach**:
391
- - Unit test the pure function (`my_plugin_validate_email_pure`) ✅
392
- - Integration test the AJAX handler with real WordPress (separate test suite)
241
+ Unit test the pure function. Integration test the AJAX handler with a real WP environment.
393
242
 
394
243
  ---
395
244
 
396
- ## Mocking Rules (Minimal Philosophy)
397
-
398
- ### Mock Only What's Needed for Pure Context
399
-
400
- **❌ DON'T over-mock**:
401
- ```php
402
- <?php
403
- // Excessive mocking for a pure function test
404
- if (!function_exists('wp_remote_post')) { /* ... */ }
405
- if (!function_exists('wp_remote_get')) { /* ... */ }
406
- if (!function_exists('get_option')) { /* ... */ }
407
- if (!function_exists('update_option')) { /* ... */ }
408
- if (!function_exists('delete_option')) { /* ... */ }
409
- // ... 50 more mocks you don't need
410
- ```
411
-
412
- **✅ DO mock minimally**:
413
- ```php
414
- <?php
415
- // Only mock what pure functions actually use
416
- if (!function_exists('sanitize_text_field')) {
417
- function sanitize_text_field(string $str): string {
418
- return trim(strip_tags($str));
419
- }
420
- }
421
-
422
- if (!function_exists('home_url')) {
423
- function home_url(string $path = ''): string {
424
- return 'https://example.com' . $path;
425
- }
426
- }
427
- ```
245
+ ## Mocking Rules
428
246
 
429
- ### Common WordPress Mocks for Pure Functions
247
+ Mock only what pure functions actually call. Common mocks for pure functions:
430
248
 
431
249
  ```php
432
250
  <?php
433
- // WordPress utility functions (pure-ish)
434
251
  if (!function_exists('wp_parse_args')) {
435
252
  function wp_parse_args($args, $defaults = []): array {
436
- if (is_string($args)) {
437
- parse_str($args, $parsed_args);
438
- $args = $parsed_args;
439
- }
253
+ if (is_string($args)) { parse_str($args, $args); }
440
254
  return array_merge($defaults, (array) $args);
441
255
  }
442
256
  }
443
-
444
- // Sanitization functions
445
257
  if (!function_exists('sanitize_email')) {
446
- function sanitize_email(string $email): string {
447
- return strtolower(trim($email));
448
- }
258
+ function sanitize_email(string $email): string { return strtolower(trim($email)); }
449
259
  }
450
-
451
260
  if (!function_exists('esc_url_raw')) {
452
- function esc_url_raw(string $url): string {
453
- return filter_var($url, FILTER_SANITIZE_URL) ?: '';
454
- }
261
+ function esc_url_raw(string $url): string { return filter_var($url, FILTER_SANITIZE_URL) ?: ''; }
455
262
  }
456
-
457
- // URL parsing (WordPress wrapper for parse_url)
458
263
  if (!function_exists('wp_parse_url')) {
459
264
  function wp_parse_url(string $url, int $component = -1) {
460
265
  return $component === -1 ? parse_url($url) : parse_url($url, $component);
@@ -464,241 +269,82 @@ if (!function_exists('wp_parse_url')) {
464
269
 
465
270
  ---
466
271
 
467
- ## Test Organization Patterns
468
-
469
- ### Pattern 1: Pure Function Testing (Fastest)
470
-
471
- ```
472
- tests/
473
- └── Unit/
474
- ├── ValidationTest.php # Pure validation functions
475
- ├── CalculationTest.php # Pure calculation logic
476
- └── FormatterTest.php # Pure formatting functions
477
- ```
478
-
479
- **Characteristics**:
480
- - No WordPress dependencies
481
- - Fast (<100ms total)
482
- - No database, no HTTP, no filesystem
483
- - Run on every commit
484
-
485
- ### Pattern 2: Integration Testing (Slower)
272
+ ## Test Organization
486
273
 
487
274
  ```
488
275
  tests/
489
- └── Integration/
490
- ├── AjaxHandlerTest.php # Full WordPress + AJAX
491
- ├── ShortcodeTest.php # Full WordPress + rendering
492
- └── DatabaseTest.php # Full WordPress + DB
276
+ ├── Unit/ # Pure functions — no WP, <100ms, run every commit
277
+ ├── ValidationTest.php
278
+ ├── CalculationTest.php
279
+ └── FormatterTest.php
280
+ └── Integration/ # Full WP env — run before releases
281
+ ├── AjaxHandlerTest.php
282
+ └── DatabaseTest.php
493
283
  ```
494
284
 
495
- **Characteristics**:
496
- - Requires full WordPress installation
497
- - Uses WP_UnitTestCase from wordpress-develop
498
- - Slower (seconds to minutes)
499
- - Run before releases
500
-
501
285
  ---
502
286
 
503
- ## Testing Patterns by Function Type
287
+ ## Patterns by Function Type
504
288
 
505
- ### Pure Calculations
506
289
  ```php
507
- <?php
508
- function calculate_discount_pure(float $price, float $rate): float {
509
- return round($price * (1 - $rate), 2);
510
- }
511
-
512
- // Test: Assert exact values
290
+ // Calculations — assert exact values
513
291
  $this->assertEquals(90.0, calculate_discount_pure(100.0, 0.10));
514
- $this->assertEquals(95.0, calculate_discount_pure(100.0, 0.05));
515
- ```
516
292
 
517
- ### Pure Validation
518
- ```php
519
- <?php
520
- function validate_age_pure(int $age): array {
521
- if ($age < 0) return ['valid' => false, 'error' => 'Negative age'];
522
- if ($age < 18) return ['valid' => false, 'error' => 'Must be 18+'];
523
- return ['valid' => true];
524
- }
525
-
526
- // Test: Assert result structure
293
+ // Validation — assert result structure
527
294
  $result = validate_age_pure(15);
528
295
  $this->assertFalse($result['valid']);
529
296
  $this->assertArrayHasKey('error', $result);
530
- ```
531
297
 
532
- ### Pure Transformations
533
- ```php
534
- <?php
535
- function format_phone_pure(string $phone): string {
536
- $digits = preg_replace('/\D/', '', $phone);
537
- if (strlen($digits) === 10) {
538
- return sprintf('(%s) %s-%s',
539
- substr($digits, 0, 3),
540
- substr($digits, 3, 3),
541
- substr($digits, 6, 4)
542
- );
543
- }
544
- return $phone;
545
- }
546
-
547
- // Test: Assert transformations
298
+ // Transformations — assert output shape
548
299
  $this->assertEquals('(555) 123-4567', format_phone_pure('5551234567'));
549
300
  $this->assertEquals('(555) 123-4567', format_phone_pure('555-123-4567'));
550
- $this->assertEquals('invalid', format_phone_pure('invalid'));
551
- ```
552
-
553
- ---
554
301
 
555
- ## Performance Testing (FP Principle)
556
-
557
- Pure functions enable easy performance testing:
558
-
559
- ```php
560
- <?php
561
- public function test_performance_fast_execution() {
562
- $iterations = 10000;
563
- $start = microtime(true);
564
-
565
- for ($i = 0; $i < $iterations; $i++) {
566
- my_plugin_validate_email_pure('user@example.com');
567
- }
568
-
569
- $elapsed = microtime(true) - $start;
570
-
571
- // Should complete 10k validations in < 100ms
572
- $this->assertLessThan(0.1, $elapsed);
573
- }
302
+ // Performance pure functions enable this for free
303
+ $start = microtime(true);
304
+ for ($i = 0; $i < 10000; $i++) { my_plugin_validate_email_pure('user@example.com'); }
305
+ $this->assertLessThan(0.1, microtime(true) - $start);
574
306
  ```
575
307
 
576
308
  ---
577
309
 
578
- ## Common Anti-Patterns
579
-
580
- ### ❌ Testing Private Methods
310
+ ## Anti-Patterns
581
311
 
582
- ```php
583
- <?php
584
- // DON'T use reflection to test private methods
585
- class MyTest extends TestCase {
586
- public function test_private_method() {
587
- $reflection = new ReflectionClass(MyClass::class);
588
- $method = $reflection->getMethod('privateMethod');
589
- $method->setAccessible(true);
590
- // ...
591
- }
592
- }
593
-
594
- // DO extract pure function and test that
595
- function my_plugin_process_data_pure(array $data): array {
596
- // Extracted logic, now testable
597
- return $data;
598
- }
599
- ```
600
-
601
- ### ❌ Testing Implementation Details
602
-
603
- ```php
604
- <?php
605
- // DON'T test internal variable values
606
- public function test_internal_state() {
607
- $obj = new MyClass();
608
- $this->assertEquals(5, $obj->internalCounter); // Brittle!
609
- }
610
-
611
- // DO test public behavior
612
- public function test_public_behavior() {
613
- $result = my_plugin_process_items(['a', 'b', 'c']);
614
- $this->assertCount(3, $result);
615
- }
616
- ```
617
-
618
- ### ❌ Over-Mocking
619
-
620
- ```php
621
- <?php
622
- // DON'T mock everything
623
- $mock = $this->createMock(Database::class);
624
- $mock->method('query')->willReturn([]);
625
- $mock->method('insert')->willReturn(1);
626
- $mock->method('update')->willReturn(true);
627
- // ... 20 more mocks
628
-
629
- // DO test pure functions that don't need mocks
630
- function process_results_pure(array $results): array {
631
- return array_map('strtoupper', $results);
632
- }
633
- ```
312
+ | Avoid | Do Instead |
313
+ |-------|-----------|
314
+ | Reflection to test private methods | Extract as testable pure function |
315
+ | Test internal variable values | Test public behavior/output |
316
+ | Mock 20+ WordPress functions | Redesign so pure function needs <5 mocks |
634
317
 
635
318
  ---
636
319
 
637
320
  ## Quality Gates
638
321
 
639
- Before merging:
640
- - [ ] `composer test` produces visible output (not silent)
641
- - [ ] Bootstrap prints "Bootstrap Loaded" message
642
- - [ ] Tests run in < 100ms for pure functions
643
- - [ ] No autoload files section in composer.json
644
- - [ ] Pure business logic separated from WordPress wrappers
645
- - [ ] Mock count is minimal (< 10 functions)
646
- - [ ] Test assertions are deterministic (no flaky tests)
322
+ - [ ] `composer test` produces visible output
323
+ - [ ] Bootstrap prints confirmation message
324
+ - [ ] Pure function tests run in <100ms
325
+ - [ ] No `autoload.files` in composer.json
326
+ - [ ] Pure business logic separated from WP wrappers
327
+ - [ ] Mock count <10 functions
328
+ - [ ] No flaky assertions
647
329
 
648
330
  ---
649
331
 
650
- ## Working Examples
332
+ ## Reference Plugins
651
333
 
652
- **Reference plugins with working tests**:
653
334
  ```bash
654
- # ima-forms: Gold standard
655
- cd wp-content/plugins/ima-forms
656
- composer test
657
-
658
- # ima-shortcodes: Recently fixed
659
- cd wp-content/plugins/ima-shortcodes
660
- composer test
335
+ cd wp-content/plugins/ima-forms && composer test # Gold standard
336
+ cd wp-content/plugins/ima-shortcodes && composer test # Recently fixed
661
337
  ```
662
338
 
663
339
  ---
664
340
 
665
341
  ## Troubleshooting
666
342
 
667
- ### Issue: Silent test execution
668
-
669
- **Symptom**: `composer test` produces no output
670
-
671
- **Fix**: Add `--testdox` flag to composer.json scripts
672
-
673
- ### Issue: Tests hang/exit silently
674
-
675
- **Symptom**: Process hangs or exits without error
676
-
677
- **Diagnosis**:
678
- ```bash
679
- # Check if bootstrap runs
680
- php tests/bootstrap.php
681
-
682
- # Check for autoload files bug
683
- cat composer.json | grep -A 5 autoload
684
- ```
685
-
686
- **Fix**: Remove autoload files section, load manually in bootstrap
687
-
688
- ### Issue: "Class not found" errors
689
-
690
- **Symptom**: PHPUnit can't find test classes
691
-
692
- **Fix**: Check `autoload-dev` PSR-4 mapping in composer.json:
693
- ```json
694
- "autoload-dev": {
695
- "psr-4": {
696
- "MyPlugin\\Tests\\": "tests/"
697
- }
698
- }
699
- ```
700
-
701
- Then run: `composer dump-autoload`
343
+ | Symptom | Cause | Fix |
344
+ |---------|-------|-----|
345
+ | No output from `composer test` | Missing `--testdox` | Add `--testdox` to scripts |
346
+ | Hang/silent exit | `autoload.files` loads before ABSPATH | Remove files array, load manually in bootstrap |
347
+ | "Class not found" | Bad PSR-4 mapping | Check `autoload-dev` in composer.json, run `composer dump-autoload` |
702
348
 
703
349
  ---
704
350
 
@@ -706,11 +352,5 @@ Then run: `composer dump-autoload`
706
352
 
707
353
  - `../php-fp/SKILL.md` - Core FP principles
708
354
  - `../php-fp-wordpress/SKILL.md` - WordPress security + FP
709
- - `../wp-local/SKILL.md` - Local WP environment handling
710
- - PHPUnit docs: https://phpunit.de/documentation.html
711
- - WordPress test suite: https://make.wordpress.org/core/handbook/testing/automated-testing/phpunit/
712
-
713
- ---
714
-
715
- **Last Updated**: 2026-01-29
716
- **Discovery**: After debugging the same bugs across multiple plugins (ima-forms, ima-shortcodes, ima-access-control)
355
+ - `../wp-local/SKILL.md` - Local WP environment
356
+ - https://phpunit.de/documentation.html