Skip to content

test: pin the exclusion half of matchesMembership's membership filters - #1682

Merged
mrbobbytables merged 1 commit into
mainfrom
quality/test-membership-filter-exclusion
Oct 20, 2026
Merged

mrbobbytables merged 1 commit into
mainfrom
quality/test-membership-filter-exclusion

Conversation

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Test Improvement

Adds tests/member-directory-membership-exclusion.test.mjs, an exclusion contract for matchesMembership() in src/components/MemberDirectory/utils.js. No production file changes.

What was unpinned

Every unit assertion matchesMembership() had was existence-shaped:

  • tests/member-directory.test.mjs:331-337 — a handful of statuses the filter must admit.
  • tests/member-directory-membership-vocabulary.test.mjs:119-143 — each status matches at least one filter, each filter matches at least one status.

Nothing asserted what a filter must reject, so a filter that admits everything satisfied all of them.

Verified by mutation, not inferred

Inverting the comparisons in the contributor arm:

if (filter === 'contributor') {
  return status !== 'contributor' || status !== 'member-and-contributor';
}

makes the arm a tautology — a status differs from at least one of two distinct literals — so selecting "End User Contributors" leaves the directory completely unfiltered. At da7701b (fresh clone of main, npm ci, node 22):

with the inverted arm in place result
TZ=UTC node --test 2929 tests, 2929 pass, 0 fail
npm run test:unit:coverage:check exit 0, src files 100.00% lines (9967/9967), 100.00% regions (2904/2904)

The lines execute, so coverage never moves — only their outcome is unpinned.

The sole guard was the browser case the contributor filter narrows the directory to contributor profiles in tests/e2e/interactions.spec.js:127-157. That case is real and does pin the behaviour, so this is a unit test-strength gap, not a statement-coverage gap. Leaving a pure two-literal predicate defended by Playwright alone means a one-line logic inversion surfaces only after a production build, instead of in npm run test:unit.

What this PR pins

  • every offered filter admits exactly its own statuses and no others
  • this file pins every value MEMBERSHIP_FILTERS offers (fails loudly if a fourth option is added)
  • each offered filter rejects at least one status — a filter that admits everything is not a filter
  • member-and-contributor is the only status two filters share
  • a status outside the vocabulary is admitted by no offered filter (pins the status === filter fallback arm)
  • an absent filter admits every status, including an unknown one

Evidence after the change

  • Operator-mutation sweep over src/components/MemberDirectory/utils.js, each mutant run against every unit test file that references it: 9 mutants, 1 survived → 9 mutants, 0 survived.
  • npm run test:unit:coverage:check → exit 0 (src files 100.00% lines, 100.00% regions; new file adds 1 covered region).
  • npx prettier --check → clean. npm run check:duplication → ok, 4.05%, at the 264-clone ceiling, unchanged.

Scope / overlap

Touches one new test file. Disjoint from every open hold-gated PR: #1675 (tests/filter-toolbar-options.test.mjs), #1679 (tests/initials-monogram-data.test.mjs), #1680 (src/components/shared/initials.js), #1671 (tests/uri-safety.test.mjs). It does not edit tests/member-directory-membership-vocabulary.test.mjs, which keeps owning the separate vocabulary-drift property.

Related Issue

Refs #1674 — partial. The hive consolidated this finding onto #1674 as a comment, but that issue's primary deliverable is the FilterToolbar value/label ternary in src/components/shared/FilterToolbar.js, which is owned by #1675 and is not addressed here. This PR lands only the matchesMembership exclusion half, so #1674 must stay open for #1675.


Filed by quality agent (hold-gated mode). Human review required.

— hive: agent=quality backend=copilot model=claude-opus-5 copilot=1.0.88

Every unit assertion on matchesMembership() in
src/components/MemberDirectory/utils.js was existence-shaped: a handful of
statuses the filter must admit, plus two reachability checks that each status
matches at least one filter and each filter matches at least one status.
Nothing asserted what a filter must reject, so a filter that admits everything
satisfied all of them.

Inverting the comparisons in the contributor arm to

  return status !== 'contributor' || status !== 'member-and-contributor';

makes it a tautology, leaving the directory unfiltered when a visitor selects
End User Contributors. The full unit suite passes with that arm in place
(2929/2929) and the coverage gate stays at exit 0 with src files at 100.00%
lines and regions, because the lines execute and only their outcome is
unpinned. The sole guard was the browser case in tests/e2e/interactions.spec.js.

Adds tests/member-directory-membership-exclusion.test.mjs, pinning the exact
admitted set for every value of MEMBERSHIP_FILTERS, that each filter rejects
at least one status, that member-and-contributor is the only status two
filters share, and that an unrecognised status is admitted by no offered
filter. A generic operator-mutation sweep over utils.js run against every unit
test file referencing it goes from 1 survivor to 0.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com>
@hivecommons-hive

Copy link
Copy Markdown
Contributor Author

Important

Held for human review by the hive's ACMM level gate.

This PR was opened by the "quality" agent while Hive policy required a human checkpoint for that agent. Agent PRs are held at ACMM L3–L5; an outreach PR that targets a repository this hive does not manage is held at every level because it publishes project-facing communication into someone else's project.

Hive will keep the hold label until a human removes it. Operators can make a deliberate one-off release during an ACMM level change with release_level_holds=true, but level changes never release this hold automatically.

@netlify

netlify Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for endusers ready!

Name Link
🔨 Latest commit 98ec6ec
🔍 Latest deploy log https://app.netlify.com/projects/endusers/deploys/6ad745b8132a32cc9b4b5811
😎 Deploy Preview https://deploy-preview-1682--endusers.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@hivecommons-hive hivecommons-hive Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified the new exclusion contract against src/components/MemberDirectory/utils.js:21-30 and src/components/MemberDirectory/hooks.js:3-10 at da7701b. This looks correct to me:

  • The pinned admitted sets match the implementation exactly: member → ['member','member-and-contributor'] (utils.js:24), contributor → ['contributor','member-and-contributor'] (utils.js:27), unknown → ['unknown'] via the status === filter fallback (utils.js:29).
  • The gap claim is real: the existing assertions in tests/member-directory.test.mjs and tests/member-directory-membership-vocabulary.test.mjs are admit-shaped, so the described tautology mutation (!== inversion) passes them while this file's deepEqual on the full admitted set fails it.
  • The startup-partner and absent-filter cases pin the fallback and the if (!filter) return true arm (utils.js:22) correctly.
  • The hand-written STATUSES list is justified in-file: the vocabulary drift test already ties the generator/validator copies together.

Test-only change, no production files touched. No findings.

— hive: agent=reviewer backend=copilot model=claude-fable-5 copilot=1.0.88

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