Skip to content

fix(admin): open each screen to its own role, proved by the M4 exit-gate browser specs - #167

Merged
TheAggressive merged 2 commits into
masterfrom
test/m4-exit-gate
Oct 11, 2026
Merged

TheAggressive merged 2 commits into
masterfrom
test/m4-exit-gate

Conversation

@TheAggressive

Copy link
Copy Markdown
Owner

Closes #155. Completes #65 (M4).

Exit-gate evidence

Three browser specs run in CI against the test-only simulated Mailchimp, so no real email is sent.

  • m4-journey.spec.ts: a brand-new operator goes from first run to a reconciled send entirely in the UI:

    • follows the onboarding checklist and publishes a template in the block editor;
    • creates a Mailchimp campaign from the checklist, then prepares, submits, and approves it;
    • creates the Mailchimp draft and sends a test;
    • schedules through the audience-naming confirmation and unschedules;
    • sends now through the same confirmation and reconciles to sent.

    Exactly one provider request of each kind is made, and the history records every step.

  • role-matrix.spec.ts: with separation of duties on, five roles each see only their own data, menus, buttons, and published actions:

    • author;
    • manager;
    • approver: approves and hands off, but can't schedule or send what they approved;
    • deliverer: tests, schedules, and sends;
    • outsider (no CampaignBridge capabilities): refused the screen, and gets a 403 from the REST collection.
  • campaign-states.spec.ts:

    • axe finds no serious WCAG 2.1 AA violation in the empty, loading, offline-with-retry, review, stale (version conflict), and permission-denied states;
    • a keyboard-only operator creates and prepares a campaign;
    • nothing animates under reduced motion;
    • a pseudo-locale shows every campaign-screen string going through translation, including a real translation delivered through WordPress script translations.

A second test-only mu-plugin (campaignbridge-e2e-operators.php) creates throwaway users with exact CampaignBridge capabilities and serves the pseudo-locale. CI now installs both test plugins. Neither ships in the release package.

Defects the matrix found (fixed, with regression tests)

  • Template authors were refused the editor.
    • WordPress authorizes a post type shown under another plugin's menu against Posts → Add New (edit_posts). It authorizes screens it can't match against the top-level menu, which required campaignbridge_manage.
    • The menu now uses the first CampaignBridge capability the user holds, and the new-template screen has its own entry.
    • Admin_Screens_Test builds the real admin menu and checks user_can_access_admin_page() for a template-only user; without either fix the test fails.
  • Screens offered actions the workflow refuses. Edit, snapshot, submit, approve, and duplicate were offered to users without access to the campaign's template. Campaign_Actions now follows template access. This is covered by a unit test and a REST wiring test.
  • Duplicate menu entry. Users without template access saw the menu's own title repeated as a submenu entry.
  • Controllers were built on every admin request. The settings controller checked the Mailchimp key from unrelated pages. Controllers are now built only when their own screen loads. A test shows that registering the menu makes no HTTP request, and opening Settings does.

Other changes

  • Simulator connection check: the simulator answers /ping only for its own fake key. A development site's real key gets Mailchimp's real answer, never a fake "verified". The fake key is built at runtime so it can't trigger secret scanning.
  • Docs:
    • ROADMAP records how the M4 exit gate is met;
    • README, api.md (template-gated actions), runbook.md (when the connection check runs), and testing-strategy.md are updated.

Verification

  • pnpm qa:fast and pnpm build pass.
  • Unit (1127), integration (434), security (117), accessibility (25), and performance (7) suites: all pass.
  • The full E2E suite passes on Studio (33).
  • Stability runs on Studio: the journey passed 5/5 after fixing an editor-insertion race; the matrix, states, and onboarding specs each passed twice.

🤖 Generated with Claude Code

…ate browser specs

Three browser specs demonstrate the M4 exit gate against a test-only
simulated Mailchimp:

- m4-journey: a new operator goes from the onboarding checklist through a
  template published in the block editor, a Mailchimp campaign, review,
  approval, provider draft, test, guarded schedule and send confirmations,
  and reconciliation to sent, with one provider request of each kind.
- role-matrix: author, manager, approver, deliverer, and a user without
  CampaignBridge capabilities each see only their data, menus, buttons, and
  published actions under separation of duties.
- campaign-states: axe-clean empty, loading, offline, review, stale, and
  denied states; keyboard-only creation; reduced motion; and a pseudo-locale
  proving every string is translatable and script translations arrive.

A second test-only mu-plugin creates throwaway operators with exact
capabilities and serves the pseudo-locale; CI installs both test plugins.

The matrix found defects, fixed here with regression tests:

- A template author who was not a WordPress editor was refused the template
  editor. WordPress authorizes a post type shown under another plugin's menu
  against Posts -> Add New, and screens it cannot match against the
  top-level menu, which required the management capability. The menu now
  uses the first CampaignBridge capability the user holds, and the
  new-template screen has its own entry.
- Screens offered edit, snapshot, submit, approve, and duplicate to users
  without access to the campaign's template, which the workflow then
  refused. Actions now follow template access.
- Users without template access saw the menu title repeated as a submenu.
- Every screen controller was built on each admin request, so the settings
  controller checked the Mailchimp key from unrelated pages. Controllers are
  now built only for their own screen.

The simulator now answers the connection check only for its own key, so a
real key on a development site is never marked verified by a fake answer.
ROADMAP records how the M4 exit gate is met.

Closes #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added area:ci Continuous-integration policy or tooling area:frontend Frontend source or Node tooling area:php PHP or Composer code area:tests Automated tests or test configuration risk:high Security or release sensitive; never auto-merged type:fix Corrects a defect needs-attention Automation stopped; maintainer action required labels Oct 9, 2026
…tings page

CI's disposable site uses plain permalinks, so the outsider's request to
/wp-json/ returned the home page with 200 instead of reaching the REST API;
it now uses ?rest_route=, which works with either. Saving the delivery
policy left the admin page on Settings, which does not load wp.apiFetch, so
the following API calls (and cleanup after a retry) failed; the helper now
returns to the Campaigns screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@TheAggressive
TheAggressive merged commit dce839f into master Oct 11, 2026
21 checks passed
@TheAggressive
TheAggressive deleted the test/m4-exit-gate branch October 11, 2026 10:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:ci Continuous-integration policy or tooling area:frontend Frontend source or Node tooling area:php PHP or Composer code area:tests Automated tests or test configuration needs-attention Automation stopped; maintainer action required risk:high Security or release sensitive; never auto-merged type:fix Corrects a defect

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test(M4): exit-gate E2E journey, role matrix, and accessible states

1 participant