Skip to content

Make the unit suite runnable and gate it in CI - #73

Open
jules-paystack wants to merge 2 commits into
masterfrom
fix/money-path-verification-gate
Open

Make the unit suite runnable and gate it in CI#73
jules-paystack wants to merge 2 commits into
masterfrom
fix/money-path-verification-gate

Conversation

@jules-paystack

Copy link
Copy Markdown
Collaborator

Infrastructure only — no production code changes. This unblocks the verification-gate work: until now there was no way to run a test for it.

The problem

The 98 tests in Test/Unit/ had never executed anywhere. composer.json has an empty require block and no require-dev, there's no vendor/, and the only workflow was CodeQL. That's why SetupTest had been failing since March 2026 without anyone knowing — and why the getCurrency() bug fixed in #72 survived years of review: the test that would have caught it stubbed a method that doesn't exist, and the stub only errors when the suite actually runs.

What this does

OK (98 tests, 137 assertions)   exit 0

Verified from a clean checkout using the exact commands now documented in CLAUDE.md.

  • Dependencies live in a CI-only Test/Unit/composer.json + committed lock — 184 packages pinned to exact versions, resolved from the Mage-OS mirror with no Adobe auth. The shipped root composer.json is untouched: its composer.lock already has a stale content-hash (it locks yabacon/paystack-php, absent from require), so adding require-dev there would break composer install and the regenerated lock would ship to Marketplace. 3.0.10 passed Adobe review with an empty require; that surface stays as-is.
  • Fixed the 7 remaining errors properly. The observer tests stubbed getPaystackOrder()/getOrder(), which aren't declared methods on Magento\Framework\Event\Observer — they resolve via __call, and PHPUnit 10 refuses to configure undeclared methods. Real Observer/Event objects instead.
  • phpunit.xml is now the single config shared by CI and local runs, bootstrapping Test/Unit/vendor/autoload.php and excluding Test/Unit/vendor from discovery (magento/framework ships its own *Test.php files that fatal when loaded).
  • CLAUDE.md corrected — it claimed "there are no unit tests" and "the only CI is codeql-analysis.yml", listed a stale build-exclusion list, named the wrong MFTF page object, and repeated an unverified "PHP 8.2+".

What adversarial review caught

Two findings I'd have shipped otherwise:

  1. phpunit.xml's cache was shipping into the Marketplace zip. cacheDirectory=".phpunit.cache" created a new repo-root directory without adding its exclusion — the exact rule CLAUDE.md records after the dev-ee/ incident, broken by the commit citing it. Confirmed by replaying the zip command. Cache relocated under Test/Unit/.
  2. The gate was non-reproducible. The lock was gitignored while every magento/module-* was "*", so 184 packages floated on every run. My own workflow comment refused extra PHP legs because unpinned resolution makes them untested, while leaving 8.5 unpinned over time — same file, opposite conclusions. Lock now committed.

Also: masked Test/Unit/vendor out of dev/'s bind mount (the repo mounts as a Magento module, so a second magento/framework inside it would be scanned by setup:di:compile); reverted an unvalidated change to the shipped artifact; SHA-pinned the actions; --no-plugins --no-scripts; dropped the pull_request base-branch filter so stacked PRs are gated; and documented honestly that two negative tests only prove "does not throw".

Two things worth your attention, not fixed here

  • CodeQL scans JavaScript only. The entire PHP money path has never been statically analysed, and that workflow still pins retired actions/checkout@v2 and github/codeql-action/*@v1 (CodeQL v1 is EOL, so the scan may be silently degraded).
  • paystack_payment_verify_after is registered only in etc/frontend/events.xml, while the inline verify endpoint runs in webapi_rest. If the REST leg's observer never fires, guarantees placed behind that event would hold only for the registered area — and the unit suite can't detect it, because the tests mock the dispatcher on one side and the observer on the other. Needs an empirical check in dev/ before the verification gate is built on it.

Suggested merge note

The check is intentionally non-required for now. Flip it to required once it's been green on a few PRs — a red required check on protected master blocks everything.

🤖 Generated with Claude Code

jules-paystack and others added 2 commits August 17, 2026 14:35
The 98 tests under Test/Unit had never executed anywhere: composer.json has an
empty require block and no require-dev, there is no vendor/, and the only
workflow was codeql-analysis.yml. That is why SetupTest had been failing since
March 2026 unnoticed.

magento/framework alone is not enough -- the tests mock Magento\Sales\... and
Magento\Store\... classes. Six packages plus phpunit are needed, and they resolve
cleanly from the Mage-OS mirror with no Adobe auth.

The dependencies deliberately do NOT go in the shipped root composer.json:
composer.lock is committed and already locks yabacon/paystack-php, a package not
in require, so its content-hash is stale and adding require-dev would make
composer install refuse until the lock is regenerated -- and the regenerated lock
would ship in the Marketplace zip. "minimum-stability": "dev" with no
prefer-stable would also resolve dev branches. 3.0.10 passed Adobe review with an
empty require block, so that surface stays untouched. Instead the deps live in a
CI-only Test/Unit/composer.json.

- Fixed the 7 remaining errors: the observer tests stubbed getPaystackOrder() and
  getOrder(), which are not declared methods on Magento\Framework\Event\Observer
  or ...\Event -- they resolve via __call, and PHPUnit 10 refuses to configure
  undeclared methods. Construct real Observer/Event objects with data instead.
- Strengthened testNullOrderDoesNotCrash and testNullPaymentDoesNotCrash to
  assert the observable consequence (no email sent / no email suppression applied)
  rather than only "did not throw".
- phpunit.xml repointed at Test/Unit/vendor/autoload.php and now excludes
  Test/Unit/vendor from discovery, so CI and a local run share one config.
  magento/framework ships its own *Test.php files which fatal when loaded.
- Workflow pinned to PHP 8.5 only, and the comment records why: 8.2 cannot
  install at all (magento/framework 103.0.9 needs ~8.3||~8.4||~8.5, which also
  contradicts the "PHP 8.2+" claim in README/CLAUDE.md), and 8.3/8.4 are untested
  because the lock is not committed so each runner resolves its own set. An
  untested matrix leg is a red check waiting to happen.
- permissions: contents: read, and pull_request rather than
  pull_request_target, so fork code never gets a writable token or secrets.
- build-adobe-zip.sh excludes phpunit.xml, composer.lock and Test/Unit/*.
  Test/Mftf/ still ships (verified: 10 entries present, 0 test-infra entries).

Verified with the exact CI invocation: OK (98 tests, 137 assertions), exit 0.

Gate 2 (adversarial diff review) has NOT run for this item -- both critics died
on an API session limit. No production code is touched here, but the gate is
outstanding and recorded as such in the plan file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adversarial diff review of 165e6a1 (security + architecture critics, 24 findings).
Actioned every high-severity item.

Packaging leak (high, both critics, confirmed by replaying the exact zip
invocation): phpunit.xml's cacheDirectory=".phpunit.cache" created a new repo-root
directory whose -x line was not added in the same commit, so .phpunit.cache/ and
its test-results shipped into the Marketplace package -- the precise rule CLAUDE.md
records after the dev-ee/ incident, broken by the commit that cited it. Fixed by
relocating the cache to Test/Unit/.phpunit.cache, which the existing Test/Unit/*
exclusion already covers; no fourth -x needed.

Non-reproducible gate (high, both critics): Test/Unit/composer.lock was gitignored
while every magento/module-* was constrained "*", so the check meant to guard the
money path re-resolved 184 floating packages every run. The reasons for not
committing the *root* lock do not transfer -- Test/Unit/* is excluded from the zip,
so this lock ships nothing and constrains no merchant. The commit was also
internally inconsistent: it refused 8.3/8.4 legs *because* unpinned resolution
makes them untested, then left 8.5 unpinned over time. Lock now committed, all 184
packages pinned, every constraint exact, cache rekeyed on the lock.

dev/ coupling (high): the whole repo is bind-mounted to app/code/Pstk/Paystack, so
`cd Test/Unit && composer install` plants a second full magento/framework inside a
Magento module path that setup:di:compile scans. Masked with an anonymous volume.
dev-repro/ needs the same line but is untracked, so that is noted in CLAUDE.md.

Shipped-artifact change reverted: -x "composer.lock" dropped the root lock from the
package. It is inert, but 3.0.10 shipped it and passed Adobe review, and changing
the artifact's composition in a test-infra commit without validating against
Adobe's package checks is not a trade worth making. Removing the stale root lock
outright is filed separately. Also dropped the redundant
-x "Test/Unit/composer.json" (Test/Unit/* already matches).

Test honesty: the two "strengthened" negative tests were cosmetic. Remove the
guard they nominally cover and production throws before the never() can be
evaluated, so the test fails on the Error, not the assertion. Kept the never() as a
smoke check but documented what it does and does not prove, and pointed at the
case that actually distinguishes. Also unified the mixed idiom (the outer Observer
was still a mock while the inner Event was real) and flagged that
testNonPendingOrderIsNotUpdated encodes the status-string gate the verification
work will change, so that edit reads as planned rather than as bending a green test.

Supply chain: --no-plugins/--no-scripts (verified the suite passes without them),
config.allow-plugins removed entirely, actions pinned to commit SHAs, composer
pinned to 2.9, persist-credentials: false, concurrency + timeout-minutes, and the
cache now stores Composer's content-addressed download cache rather than vendor/,
which was restored-and-executed trusted code under an immutable key.

CI scope: pull_request no longer filters on base branch -- the money-path work
lands as a stack of PRs onto feature branches, which the filter skipped entirely.

Docs: CLAUDE.md said "there are no unit tests" and "the only CI is
codeql-analysis.yml", listed a stale exclusion list and the wrong MFTF page-object
name, and repeated an unverified "PHP 8.2+" claim -- magento/framework 103.0.9
requires ~8.3||~8.4||~8.5. All corrected, with the install prerequisite documented.
Also recorded that CodeQL scans JavaScript only, so the PHP money path currently
gets no static analysis.

Verified from a clean state via the documented commands: OK (98 tests, 137
assertions), exit 0. Zip composition re-checked: 0 phpunit-cache, 0 Test/Unit,
0 phpunit.xml, 0 auth.json entries; composer.lock present; 10 Test/Mftf entries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant