|
| 1 | +--- |
| 2 | +name: plugin-contract-tests |
| 3 | +description: Sets up, regenerates or debugs the shared MyAdmin plugin contract harness for this package — tests/ContractTest.php, the contract inspectors, and `composer myadmin:scaffold-tests`. Use when the user says 'add tests to this plugin', 'set up the harness', 'scaffold tests', 'why is ContractTest failing', or when deciding whether a contract failure is the plugin's fault or the harness's. Do NOT use for this package's own non-plugin classes — its other testing skills cover those. |
| 4 | +--- |
| 5 | +# Plugin contract tests |
| 6 | + |
| 7 | +The class under contract here is `Detain\MyAdminKayako\Plugin`. |
| 8 | + |
| 9 | +## Critical |
| 10 | + |
| 11 | +- **Never hand-write or hand-edit `tests/ContractTest.php`.** It is generated. Run |
| 12 | + `composer myadmin:scaffold-tests` from inside this repo; regenerate with `--force`. A hand |
| 13 | + edit is invisible to the next regeneration and to the next person. |
| 14 | +- **Never write a reflection-only test for the plugin class.** Asserting that a handler exists, |
| 15 | + is static and takes one parameter passes whether or not the handler works. Execute it. The |
| 16 | + harness has already done the hard part — priming the constants that used to make that |
| 17 | + impossible. |
| 18 | +- **Never delete an existing test to make room.** The harness is strictly additive: `ContractTest` |
| 19 | + runs *alongside* whatever this package already had. Duplicate coverage is the cheaper mistake. |
| 20 | + Removing anything is a question for the owner first — that is a standing rule on this fleet. |
| 21 | +- **Run the whole suite, never just `--filter ContractTest`.** The contract class primes constants |
| 22 | + and calls `register_module()`, neither of which can be undone, so it can change how this |
| 23 | + package's *other* tests behave. A filtered run cannot show that. |
| 24 | +- **`composer myadmin:scaffold-tests` does not exist in MyAdmin core.** Core sets |
| 25 | + `config.allow-plugins: false`, so Composer never activates the installer there and no |
| 26 | + `myadmin:*` command is registered. Run it from this repo. |
| 27 | +- This package declares no `$module`, which is correct for a `type=plugin` package and is asserted bidirectionally by A-7 — adding one without adding the module is a failure. |
| 28 | + |
| 29 | +## Instructions |
| 30 | + |
| 31 | +### Step 1 — regenerate, do not edit |
| 32 | + |
| 33 | +```bash |
| 34 | +composer myadmin:scaffold-tests # plan only; writes nothing |
| 35 | +composer myadmin:scaffold-tests --write # create what is missing |
| 36 | +composer myadmin:scaffold-tests --force --write # also re-emit tests/ContractTest.php |
| 37 | +``` |
| 38 | + |
| 39 | +`CREATE` means a file is missing. `KEEP` means one exists and will not be touched. `DRIFT` |
| 40 | +means an existing `phpunit.xml.dist` is missing a setting the harness depends on. |
| 41 | + |
| 42 | +If Composer deadlocks, this package still vendors installer `v2.0.2`, which predates Composer |
| 43 | +2's `PluginInterface` and fatals while activating — break it once with `composer update |
| 44 | +--no-plugins`. |
| 45 | + |
| 46 | +### Step 2 — fix a reported DRIFT by hand |
| 47 | + |
| 48 | +The three settings are load-bearing, not stylistic: |
| 49 | + |
| 50 | +- `failOnWarning="true"` — several findings surface first as a PHP warning; without it PHPUnit |
| 51 | + prints the finding and exits 0. |
| 52 | +- `failOnRisky="true"` — a test asserting nothing because its subject would not load is risky, |
| 53 | + not passing. |
| 54 | +- `beStrictAboutOutputDuringTests="true"` — assertion B-15 (a plugin must not echo while its |
| 55 | + handlers run) is unenforceable without it. |
| 56 | + |
| 57 | +### Step 3 — classify a failure before changing anything |
| 58 | + |
| 59 | +This decides *which repository you touch*, so do it first: |
| 60 | + |
| 61 | +| symptom | verdict | action | |
| 62 | +|---|---|---| |
| 63 | +| the plugin genuinely does the wrong thing — uses a variable before assigning it, constructs a class with the wrong arity, registers a requirement path that does not exist | **P-bug** | fix in this repo, on its own branch, with its own review. Do not bundle it into a test-scaffolding commit | |
| 64 | +| the harness accuses the plugin of something it did not do | **H-bug** | fix in `detain/myadmin-plugin-installer`, never here, and add the counter-test proving the inspector can still fail | |
| 65 | +| the blocker is the environment — a `require` of a path that only exists inside a MyAdmin checkout | neither | the inspector should *skip*, naming the blocker. If it fails instead, that is an H-bug | |
| 66 | + |
| 67 | +Three H-bugs have shipped, and all three were the harness falsely accusing a plugin: a shadowed |
| 68 | +observer read as dead code (v2.1.1), a failed `require` read as the handler's own logic (v2.1.2), |
| 69 | +and a Windows path treated as relative so every package looked like it shipped no templates |
| 70 | +(v2.2.1). **Suspect the harness first** when a verdict changes depending on how the suite was |
| 71 | +launched, or when a finding fires on every package at once. |
| 72 | + |
| 73 | +### Step 4 — if the generated file is wrong, change the generator |
| 74 | + |
| 75 | +`src/Testing/Scaffold/ContractTestGenerator.php` in the installer is the single source of truth |
| 76 | +for all 66 generated copies. Fix it there, tag, then regenerate here. |
| 77 | + |
| 78 | +## Three ordering rules the generated file encodes |
| 79 | + |
| 80 | +They look like style. They are not. |
| 81 | + |
| 82 | +1. **`primeConstants()` runs before the plugin class is mentioned at all.** A static property |
| 83 | + initializer can reference a bare constant — `$settings` holding |
| 84 | + `REPEAT_BILLING_METHOD => PRORATE_BILLING` is the common shape — and initializers run on class |
| 85 | + *load*, so even reading `::$type` fatals on an unprimed class. |
| 86 | +2. **The hook table is read through `TierA5HooksAreIdempotent::hookTable()`,** never a direct |
| 87 | + `getHooks()` call. A direct call is a second, independent answer to a question A-5 owns, and |
| 88 | + the two disagree for any plugin whose body touches a bare constant. |
| 89 | +3. **The table is evaluated exactly once.** Calling `getHooks()` twice asserts idempotence by |
| 90 | + accident and doubles whatever side effect the body has. |
| 91 | + |
| 92 | +Plus `@runTestsInSeparateProcesses` + `@preserveGlobalState disabled`, always. |
| 93 | + |
| 94 | +### Namespaced stubs |
| 95 | + |
| 96 | +If this package ships a `tests/stubs.php` declaring helpers **inside the plugin's own |
| 97 | +namespace**, PHP binds the plugin's unqualified calls to those rather than to the harness's |
| 98 | +observers. Eight packages in the fleet do this. The harness detects the shadow and skips instead |
| 99 | +of accusing, but the assertion is then vacuous. Prefer forwarding such a stub into the harness |
| 100 | +over making it a no-op, so the observation still lands. |
| 101 | + |
| 102 | +## Verify |
| 103 | + |
| 104 | +```bash |
| 105 | +vendor/bin/phpunit |
| 106 | +``` |
| 107 | + |
| 108 | +Whole suite, green, before committing. |
| 109 | + |
| 110 | +## Reference |
| 111 | + |
| 112 | +- `docs/testing-harness.md` in `detain/myadmin-plugin-installer` — §1.5 scaffolding, §3 traps, |
| 113 | + §7 the P-bug/H-bug split, §11 the generated file. |
| 114 | +- `.claude/rules/plugin-tests.md` in MyAdmin core. |
0 commit comments