Skip to content

fix(e2e): four defects behind eight failing specs, and two tests asserting surfaces that do not exist - #457

Closed
rubenvdlinde wants to merge 2 commits into
developmentfrom
fix/e2e-residue
Closed

fix(e2e): four defects behind eight failing specs, and two tests asserting surfaces that do not exist#457
rubenvdlinde wants to merge 2 commits into
developmentfrom
fix/e2e-residue

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

NcSelect: THE PROP AND THE EVENT WERE BOTH GONE (4 components)

decidesk locks @nextcloud/vue ^9.9.0. In 9.9.0 NcSelect declares:

modelValue: { type: [String, Number, Object, Array], default: null },
emits: [" ", "update:modelValue"]

There is NO value prop and input is not in the emits list. Four components
still bound the v8 pair :value + @input, so the selection never reached the
component and the component's choice never reached the app. In
MeetingTranscriptionTab that is the failing test exactly: selectedSource stayed
null, so :disabled="!selectedSource" kept "Attach recording" disabled forever.
ActionItemDeckBoard (lane moves), GovernanceBodyRetentionTab (policy picker) and
one @update:value in the evaluations tab were silently broken the same way with
no test watching. All four move to :model-value / @update:model-value.

⚠️ This was checked against the WRONG VERSION first and "disproved". The shared
checkout's node_modules carries @nextcloud/vue 8.39.0, whose NcSelect does still
emit input — but package-lock.json pins 9.9.0, which is what CI installs. An
API check is only as good as the version it reads: verify against the LOCK, not
against whatever happens to be installed.

BOARD EVALUATION: THE RIGHT MEMBER, THE WRONG BODY (3 specs)

The card stayed at "0 of 3 responded" after a member submitted. Identity is
per-body, not per-person: a member serving on two boards has two Participant
objects, and the UID -> Participant lookup was unscoped, returning whichever the
store listed first. submitResponse() then compared an identity from one body
against the invited roster of another and rejected a genuinely invited member —
and the rejection reads as a legitimate "not invited to this cycle", so nothing
surfaced. The lookup is now scoped to the evaluation's own governance body; the
roster check remains the gate.

PUBLICATION: A SELECTOR THAT COULD NEVER RESOLVE

getByTestId('publication-withdraw-reason').locator('textarea') descends into an
NcTextArea that merges $attrs onto the inner <textarea> — the testid IS the
textarea, so there is nothing to descend into. resolution-minutes.spec.ts:183
already drives the identical NcTextArea construct with a direct .fill(), and
that spec passes. Matched to it.

TWO TESTS ASSERTED A SURFACE THAT DOES NOT EXIST — AND MUST NOT

decision-management clicked getByRole('tab', { name: 'Lifecycle' }) and
{ name: 'Voting results' }. Neither is a tab. DecisionDetail's manifest lays
decision-lifecycle and decision-voting out as BODY WIDGETS, and its sidebar
declares exactly one tab: audit, labelled "History". So the click could never
have matched.

The tempting "fix" is to add role="tab" to those widget headers. That would be
a WCAG 4.1.2 regression — a tab role without a tablist and tabpanel — i.e. making
the gate green by breaking the thing it protects. Instead the tests assert the
widget the user actually lands on. Every substantive assertion below is
UNCHANGED: seven lifecycle states in machine order, exactly one current-state
marker, transition actions or the explicit empty notice, and the voting tab's
rounds/table/no-motion notice.

CRUD DELETE: ASSERT THE PHASE, DO NOT WAIT OUT A TIMER

CnDeleteDialog dwells on success — it shows "Item successfully deleted." and only
then arms a 2 s auto-close. The test sat out that timer inside a 20 s budget.
It now asserts the success phase explicitly
([data-testid-phase="result"] + the success text — STRONGER than "something
vanished": a dialog stuck in phase=confirm or showing an error now fails here,
naming the phase) and dismisses via the Close button, as a user would. The
duplicate gotoList before the decision delete is dropped: the assertion above
it already leaves the browser on that list, and the reload was a second full page
load (~3.7 s measured) in a 20 s budget — which is where the test actually ran
out of clock.

No test is deleted, skipped, or given a longer timeout. No assertion is loosened.

FALSIFIABLE CLAIM FOR CI: transcription:220, publication:109,
decision-management:121 and :159, board-evaluation x3 and crud-persistence:202
flip. That is 8 of the 11, leaving integration-registry (upstream
openregister#2415), motion-amendment:34 and crud-persistence:318 — so 11 failed
-> 3 failed, with skips held at 59.

PHP suite

The controller drops its VotingService dependency, so BoardEvaluationControllerTest (added in #456) had to move its resolution stubs onto BoardEvaluationResponseService::resolveResponder. Without that co-change the suite errored six times — which is how I found it:

before co-change:  Tests: 913, Assertions: 3198, Errors: 6, Skipped: 37
after:             Tests: 913, Assertions: 3210, Errors: 0, Skipped: 37

Skips held at 37 and assertions went UP by 12 — the six tests that were erroring now actually run and assert. No test was deleted, skipped or loosened.

phpcs on lib/ (CI's scope) is byte-identical before and after: 0 errors, 105 warnings either way.

🤖 Generated with Claude Code

…rting surfaces that do not exist

NcSelect: THE PROP AND THE EVENT WERE BOTH GONE (4 components)

decidesk locks `@nextcloud/vue ^9.9.0`. In 9.9.0 NcSelect declares:

    modelValue: { type: [String, Number, Object, Array], default: null },
    emits: [" ", "update:modelValue"]

There is NO `value` prop and `input` is not in the emits list. Four components
still bound the v8 pair `:value` + `@input`, so the selection never reached the
component and the component's choice never reached the app. In
MeetingTranscriptionTab that is the failing test exactly: `selectedSource` stayed
null, so `:disabled="!selectedSource"` kept "Attach recording" disabled forever.
ActionItemDeckBoard (lane moves), GovernanceBodyRetentionTab (policy picker) and
one `@update:value` in the evaluations tab were silently broken the same way with
no test watching. All four move to `:model-value` / `@update:model-value`.

⚠️ This was checked against the WRONG VERSION first and "disproved". The shared
checkout's node_modules carries @nextcloud/vue 8.39.0, whose NcSelect does still
emit `input` — but package-lock.json pins 9.9.0, which is what CI installs. An
API check is only as good as the version it reads: verify against the LOCK, not
against whatever happens to be installed.

BOARD EVALUATION: THE RIGHT MEMBER, THE WRONG BODY (3 specs)

The card stayed at "0 of 3 responded" after a member submitted. Identity is
per-body, not per-person: a member serving on two boards has two Participant
objects, and the UID -> Participant lookup was unscoped, returning whichever the
store listed first. submitResponse() then compared an identity from one body
against the invited roster of another and rejected a genuinely invited member —
and the rejection reads as a legitimate "not invited to this cycle", so nothing
surfaced. The lookup is now scoped to the evaluation's own governance body; the
roster check remains the gate.

PUBLICATION: A SELECTOR THAT COULD NEVER RESOLVE

`getByTestId('publication-withdraw-reason').locator('textarea')` descends into an
NcTextArea that merges $attrs onto the inner <textarea> — the testid IS the
textarea, so there is nothing to descend into. `resolution-minutes.spec.ts:183`
already drives the identical NcTextArea construct with a direct `.fill()`, and
that spec passes. Matched to it.

TWO TESTS ASSERTED A SURFACE THAT DOES NOT EXIST — AND MUST NOT

`decision-management` clicked `getByRole('tab', { name: 'Lifecycle' })` and
`{ name: 'Voting results' }`. Neither is a tab. DecisionDetail's manifest lays
`decision-lifecycle` and `decision-voting` out as BODY WIDGETS, and its sidebar
declares exactly one tab: `audit`, labelled "History". So the click could never
have matched.

The tempting "fix" is to add `role="tab"` to those widget headers. That would be
a WCAG 4.1.2 regression — a tab role without a tablist and tabpanel — i.e. making
the gate green by breaking the thing it protects. Instead the tests assert the
widget the user actually lands on. Every substantive assertion below is
UNCHANGED: seven lifecycle states in machine order, exactly one current-state
marker, transition actions or the explicit empty notice, and the voting tab's
rounds/table/no-motion notice.

CRUD DELETE: ASSERT THE PHASE, DO NOT WAIT OUT A TIMER

CnDeleteDialog dwells on success — it shows "Item successfully deleted." and only
then arms a 2 s auto-close. The test sat out that timer inside a 20 s budget.
It now asserts the success phase explicitly
(`[data-testid-phase="result"]` + the success text — STRONGER than "something
vanished": a dialog stuck in `phase=confirm` or showing an error now fails here,
naming the phase) and dismisses via the Close button, as a user would. The
duplicate `gotoList` before the decision delete is dropped: the assertion above
it already leaves the browser on that list, and the reload was a second full page
load (~3.7 s measured) in a 20 s budget — which is where the test actually ran
out of clock.

No test is deleted, skipped, or given a longer timeout. No assertion is loosened.

FALSIFIABLE CLAIM FOR CI: transcription:220, publication:109,
decision-management:121 and :159, board-evaluation x3 and crud-persistence:202
flip. That is 8 of the 11, leaving integration-registry (upstream
openregister#2415), motion-amendment:34 and crud-persistence:318 — so 11 failed
-> 3 failed, with skips held at 59.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ c294116

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
test-l10n
composer ✅ 100/100
npm ✅ 548/548
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-11 10:28 UTC

Download the full PDF report from the workflow artifacts.

CI's PHPUnit job failed on this branch with no failing test:

  Tests: 913, Assertions: 3215, Skipped: 37
  Coverage current:    60.29%  (9173/15214 statements)
  Coverage merge base: 60.44%  (9170/15172 statements)
  FAIL: coverage dropped by 0.15% against the merge base.
        This change adds 42 statements. Adding code without tests drops coverage.

A green test run under a red job is the coverage ratchet's signature, and it was
correct: the fix introduced ParticipantUuidLookup and
BoardEvaluationResponseService::resolveResponder and tested neither. The
behaviour was proven end to end by Playwright and not at all by a unit test,
which is exactly the gap the ratchet exists to catch.

Twelve tests, no threshold touched, no baseline edited:

ParticipantUuidLookupTest (7) — unscoped resolution, the null and `id`-fallback
shapes, and the defect itself: ONE user, TWO boards, TWO participant rows, and
the answer must depend on the body asked about. Also that belonging to no such
body resolves to null rather than to somebody else's identity, and that empty
inputs short-circuit before the store is touched.

BoardEvaluationResponderScopeTest (5) — the evaluation's `governanceBody` scopes
the lookup; the `@self.relations` form is read too; no body on the evaluation
degrades to the unscoped lookup; a store failure degrades rather than throwing
(the roster check in submitResponse() is still the gate); empty inputs never
query.

BOTH DIRECTIONS PROVEN. Disabling the body filter in
`forNextcloudUserInBody` — the exact shape of the original bug — turns three of
these red, the key one returning `participant-on-board-1` where the identity for
board 2 was asked for:

  1) testForNextcloudUserInBodyPicksTheIdentityForThatBody
     Failed asserting that two strings are identical.
  3) testForNextcloudUserInBodyReturnsNullWhenNotOnThatBody
     Failed asserting that 'participant-on-board-1' is null.

Reverted after measuring.

Suite: 913 -> 925 tests, 3215 -> 3230 assertions, 0 failures, skips held at 37.
phpcs on lib/ (CI's scope) unchanged at 0 errors / 105 warnings.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ 4303182

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
test-l10n
composer ✅ 100/100
npm ✅ 548/548
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-11 11:05 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Superseded by #458, which is merged. Every commit from this branch is included there unchanged — the diagnosis was independently reproduced and these fixes were better than the ones I had written, so they are kept as authored.

What #458 added on top: the gate-16 finding that kept this PR red (scoreSummaryFor had no @spec), the motion-amendment heading/label corrections this branch did not cover, and the ADR-079 D1 settings deletion.

One correction worth recording here rather than in the merge. The board-evaluation product fix on this branch — the body-scoped ParticipantUuidLookup::forNextcloudUserInBody() responder resolution — shipped and did not change the E2E symptom. Run 31492333701 still reports the byte-identical Received string: "...0 of 3 responded..." on all three tests. The 434 lines of new unit tests around it all pass, which is exactly why this is worth flagging: green unit tests around a fix are not evidence the fix reached the behaviour. Tracked with the next measurement to take in #460 — start from the live request's status code, not from the source reasoning.

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