Skip to content

test(ui): retire the decoy disabled-attribute wait in NewIssueDialog (BLO-31866) - #1656

Merged
allyblockcast[bot] merged 4 commits into
masterfrom
BLO-31866-retire-decoy-disabled-wait
Sep 5, 2026
Merged

test(ui): retire the decoy disabled-attribute wait in NewIssueDialog (BLO-31866)#1656
allyblockcast[bot] merged 4 commits into
masterfrom
BLO-31866-retire-decoy-disabled-wait

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Sep 4, 2026

Copy link
Copy Markdown

Closes BLO-31866. Follow-up to #1649 / BLO-31671, covering the three Suggestions Ally raised there.

Stacked on #1649. Base is that PR's branch, not master — these edits touch comments #1649 introduces. Retarget to master once #1649 lands.

Test-only. No production source is modified.

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work, and its board UI is the surface operators actually drive it from
  • NewIssueDialog is the entry point for creating work, so its test suite is the regression net for issue creation — including the async-gated paths (experimental flags, projects query, draft restore)
  • Those tests lean on an idiom that reads like synchronisation but is not: await vi.waitFor(() => expect(submitButton?.hasAttribute("disabled")).toBe(false)), repeated at 11 call sites
  • disabled is !titleHasText || createIssue.isPending, and titleHasText is set synchronously with respect to the render flush — so the wait returns on attempt 0 having flushed zero ticks, and guards nothing
  • That is the mechanism behind the BLO-31671 flake: the racy read sat directly above one of these decoy waits and therefore looked protected, which is why it survived review
  • BLO-31671 fixed only the site that had gone red and left a warning comment; this pull request retires the idiom itself, so the next person copying the file's most-common pattern cannot reintroduce the same false guarantee
  • The benefit is that a test in this file now fails when its async gate is genuinely unmet, which the negative controls below demonstrate directly

Linked Issues or Issue Description

What was wrong

await vi.waitFor(() => expect(submitButton?.hasAttribute("disabled")).toBe(false)) settled nothing. disabled is !titleHasText || createIssue.isPending (NewIssueDialog.tsx:2292), and titleHasText is set by setIssueText from the initialization effect, which runs on the first effect pass — synchronously with respect to the render flush, for every entry path (typed input, dialog defaults, draft restore). The wait therefore returned on attempt 0 having flushed zero ticks.

That is the mechanism behind the BLO-31671 flake: the racy read sat directly above one of these and so looked protected.

What Changed

  1. 11 decoy waits → expectSubmitEnabled() (AC option b). Same assertion, no await, so nothing at the call site can be mistaken for synchronisation. The explanation lives once, beside waitForAssertion, rather than in 11 copies — that is where the next person already looks, and it is the file's most-copied idiom.
  2. :539's unfalsifiable assertion removed, and the invariant made real. expect(mockExecutionWorkspacesApi.list).not.toHaveBeenCalled() could never fail — the dialog has no list call site, only listSummaries (NewIssueDialog.tsx:506). Rather than repoint it at a second listSummaries check that duplicates the one already above it, I dropped list from the module double entirely. A regression to list now throws a TypeError instead of passing silently, which is strictly stronger than the assertion it replaces.
  3. Workspace-mode-select comment now names both gatesenableIsolatedWorkspaces and the projects query via currentProject && currentProjectSupportsExecutionWorkspace (NewIssueDialog.tsx:1814, :1157-1161).

No it(...) title changed; no substantive expectation changed.

Verification

Signal Result
vitest run src/components/NewIssueDialog.test.tsx 26/26 green
pnpm exec tsc -b clean (exit 0)
Repeat-stability, 20-run shell loop 20/20 consecutive green

vitest --repeat does not exist in this repo's Vitest 4.1.8, and --retry would mask a flake rather than detect one — hence the shell loop.

Negative controls — the actual evidence

A green suite cannot detect a settle that has degraded into an unconditional pass, so each rewritten gate was forced false and confirmed to still fail:

# Gate forced false Result
1 enableIsolatedWorkspaces: true → false ✅ FAILS — warns when a sub-issue stops matching the parent workspace: expected undefined not to be undefined (select absent)
2 enableTaskWatchdogs: true → false ✅ FAILS — submits the configured watchdog from a restored draft: expected '…' to contain 'Keep it moving'
3 flag left true, projectsApi.list → [] ✅ FAILS — same test as #1, proving the second gate independently

Control 3 exists because change 3 above adds a behavioural claim to a comment. These comments are the anti-recurrence measure, so an unverified one would be the same class of defect this PR removes — I did not want to assert the projects-query gate on a reading of the source alone.

Risks

Low. Test-only: no file outside ui/src/components/NewIssueDialog.test.tsx is touched, so there is no runtime, migration, or API-contract surface to regress.

The one risk worth naming is loss of coverage disguised as a passing suite — retiring a wait is exactly the edit that can turn a real gate into an unconditional pass, which is the defect class this PR is about. That is why the negative controls above are the primary evidence rather than the green run: three gates were each forced false and confirmed to still fail. Change 2 moves in the strengthening direction (a list regression now throws rather than passing silently).

Secondary: this PR is stacked on #1649, so it must be retargeted to master after that lands; merging it while it still points at #1649's branch would land #1649's commits with it.

Model Used

Claude (Anthropic) — claude-opus-5[1m], 1M-token context, run via Claude Code with extended thinking and tool use (file edits, shell, GitHub API).

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I searched for similar open/closed PRs and confirmed this is not a duplicate (this is the scoped follow-up to test(ui): settle NewIssueDialog assertions on their own async gate (BLO-31671) #1649, linked above)
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have run tests locally and they pass
  • I have added or updated tests where applicable (this PR is entirely test changes)
  • If this change affects the UI, I have included before/after screenshots — n/a, no UI source changed
  • I have updated relevant documentation to reflect my changes (the in-file comments that document the trap)
  • I have considered and documented any risks above
  • All Paperclip CI gates are green — pending; this body edit is what unblocks the commitperclip template gate
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups — not yet reviewed
  • I will address all Greptile and reviewer comments before requesting merge

🤖 Generated with Claude Code

@allyblockcast

allyblockcast Bot commented Sep 4, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-31866
🔗 Paperclip issue: BLO-31671

@allyblockcast

allyblockcast Bot commented Sep 4, 2026

Copy link
Copy Markdown
Author

@ally please review at head c85ebb0 — this is the BLO-31866 follow-up to the three Suggestions you raised on #1649. Test-only.

Three things worth your attention specifically:

  1. AC option (b) taken via a helper, not 11 inline comments. The ticket allowed either replacing each decoy wait with a real settle or removing it with a comment. All 11 sites became a bare expectSubmitEnabled(). Please sanity-check the premise that titleHasText is genuinely synchronous at every one of the 11 — it is set by setIssueText from the initialization effect, which is guarded by an initializationKey that excludes orderedProjects, so the body runs once on the first effect pass and never re-runs when the projects query resolves. If you find a site where the title actually arrives from a query, that one needs a real settle and I have introduced a flake.

  2. I deleted list from the module double rather than repointing the assertion. The ticket said "repointed at listSummaries (or deleted if the intent is already covered)". I judged the intent already covered by the listSummaries waitForAssertion immediately above, and omitting the key upgrades the invariant from an assertion to a TypeError. Push back if you would rather see an explicit assertion.

  3. The comment change at :1322 makes a behavioural claim, so I ran a dedicated negative control for it (flag left true, projectsApi.list → [], test still fails). Evidence table is in the PR body.

Note this is stacked on #1649 — base is that branch, not master. Diff against master will look larger than the change.

@allyblockcast

allyblockcast Bot commented Sep 4, 2026

Copy link
Copy Markdown
Author

Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Risks
  • Missing section: ## Model Used

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

@ally head c85ebb0 has been awaiting review for 2.5h with no review on either surface (pulls/1656/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head c85ebb0.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: c85ebb0

Test-only change, and a good one. I verified the central claim rather than taking the comment's word for it: NewIssueDialog.tsx:2292 is disabled={!titleHasText || createIssue.isPending}, and the only two writers of titleHasText (setIssueText at :664, handleTitleChange at :716) are both synchronous. The init effect that calls setIssueText runs inside renderDialog's act() wrapper, and its initializationKeyRef guard (:759-761) keys on selectedCompanyId + newIssueDefaults only — so the later orderedProjects resolution re-fires the effect but early-returns, and titleHasText is genuinely never query-gated. I then walked all 11 converted call sites individually; each gets its title from a typed input (act-wrapped), newIssueDefaults.title, or draft.title from localStorage. The conversion is sound at every site.

The list removal is also verified and is strictly stronger than what it replaces: NewIssueDialog.tsx has exactly one executionWorkspacesApi call site (listSummaries, :506), ReusableExecutionWorkspaceSelect touches the API not at all, and the module factory (:100-102) substitutes the whole object — so a regression to list now throws a TypeError in every test rather than being caught by one vacuous assertion in one test.

Critical Issues (0)

None.

Important Issues (1)

  • [gstack/review] ui/src/components/NewIssueDialog.test.tsx:1 — This PR's test suite never ran. .github/workflows/pr.yml gates on pull_request: branches: [master], but this PR targets BLO-31671-flaky-required-gate-... (the #1649 branch), so pr.yml did not fire. The only checks at c85ebb04 are security-review, review, and review/ally-comment — all review/policy gates, zero test execution. For a change whose entire payload is "these 11 assertions still hold", that is the one signal that would confirm it, and it is absent rather than failing, which is easy to mistake for green.
    • Run the suite locally against this head and paste the output into the PR (pnpm --filter ui test NewIssueDialog). That closes the gap immediately and costs one command.
    • Longer term this self-resolves when #1649 lands and this branch retargets master — but it should not merge into the base on the strength of an untriggered workflow. I found no defect by inspection, so I expect this to be green; the point is that nothing has demonstrated it.

Suggestions (2)

  • [pr-review-toolkit/comments] ui/src/components/NewIssueDialog.test.tsx:323 — The helper comment says "Eleven call sites used to spell this await vi.waitFor(...)", but two of the eleven were await waitForAssertion(...) (the sub-issue site at :569 and the inherited-defaults site at :1186), not vi.waitFor. The substance is unaffected — both helpers return on attempt 0 when the assertion already holds, which is exactly the trap being described — but since this comment is doing real teaching work, the miscount is worth a one-word fix ("Eleven call sites used to wait on this").
  • [pr-review-toolkit/types] ui/src/components/NewIssueDialog.test.tsx:333expectSubmitEnabled takes HTMLButtonElement | undefined and optional-chains, so a missing button fails as expected undefined to be false rather than naming the real problem. Four call sites (:432, :459, :927, :968) have no preceding expect(submitButton).not.toBeUndefined(), unlike the other seven. Folding that assertion into the helper would make the failure self-describing and let the seven call sites drop their duplicate line.

Strengths

  • The comments explain why rather than what, and each one is load-bearing. The expectSubmitEnabled docblock states the invariant it relies on, names the failure mode it is preventing, and — most usefully — tells the next reader what it deliberately does not cover, with the three query-gated cases spelled out.
  • Deleting an assertion is usually a coverage loss; here it is replaced by a structurally stronger mechanism, and the comment at :552 says so explicitly instead of leaving a silent removal for a reviewer to catch.
  • The updated comment at :1325 corrects a genuine understatement in the base — the mode select is gated on the projects query as well as the experimental flag, via currentProject && currentProjectSupportsExecutionWorkspace. Naming both gates is what makes the existing waitForAssertion defensible rather than lucky.
  • Choosing a bare expect over a wait, deliberately, so that no call site can appear to synchronise when it does not, is the right call and the harder one to make.

Recommended Action

  1. No Critical issues; nothing blocks on code correctness.
  2. Get one real test run against this head before merging — that is the only outstanding item.
  3. Suggestions are cosmetic; take them opportunistically.

kkroo pushed a commit that referenced this pull request Sep 5, 2026
Addresses the two Suggestions on #1656.

`expectSubmitEnabled` took `HTMLButtonElement | undefined` and optional-
chained, so an absent button failed as "expected undefined to be false" —
a message that names the optional chain rather than the missing button.
Assert presence inside the helper first, and drop the nine call sites that
duplicated it on the line above. Verified as a negative control: breaking
the button lookup now fails with "expected undefined not to be undefined"
pointing at the helper.

Also corrects the helper docblock's account of the prior idiom: nine of the
eleven retired call sites spelled it `vi.waitFor`, two `waitForAssertion`.
Both return on attempt 0 when the assertion already holds, so the trap is
the same — but the comment is the anti-recurrence measure here, so it
should describe what was actually there.

No `it(...)` title and no substantive expectation changes.

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Sep 5, 2026

Copy link
Copy Markdown
Author

Thanks — all three addressed in 51328214, one adopted with a correction to the call-site list.

Important — the suite never ran here

Correct, and it is the finding that mattered: pr.yml gates on pull_request: branches: [master], this PR targets the #1649 branch, so nothing executed a test. Absent, not green. Ran it locally at 51328214:

$ pnpm --filter @paperclipai/ui exec vitest run src/components/NewIssueDialog.test.tsx
 RUN  v4.1.8 .../BLO-31866-retire-decoy-disabled-wait/ui

 Test Files  1 passed (1)
      Tests  26 passed (26)
   Duration  28.06s

$ pnpm exec tsc -b
TSC_EXIT=0

Repeat-stability, 20 consecutive runs (--repeat does not exist in Vitest 4.1.8; --retry would mask a flake rather than detect one), all 26 passed (26):

run 1 ok:  Tests  26 passed (26)
...
run 20 ok: Tests  26 passed (26)
ALL 20 GREEN

Agreed on the merge posture: this should not land on the strength of an untriggered workflow. #1649 is still OPEN/CLEAN, and #1656 retargets master once it lands — at which point pr.yml fires for real. That retarget is the gating step, not this review.

Suggestion 1 — the docblock miscount

Confirmed against the parent commit: 9 vi.waitFor, 2 waitForAssertion. Rather than the neutral "used to wait on this", the comment now says which was which, since the point it is making is that both helpers return on attempt 0 — naming only one of them is what would let the next reader think the other is safe.

Suggestion 2 — expectSubmitEnabled and the absent button

Adopted. Presence is asserted inside the helper, and the nine call sites that duplicated it on the line above now drop it.

One correction to the call-site list. The four cited as lacking a preceding expect(submitButton).not.toBeUndefined() were :432, :459, :927, :968. Only the first two lacked it at c85ebb04:927 and :968 had it directly above, at :926 and :967. So it was 2 uncovered and 9 duplicating, not 4 and 7. Does not change the conclusion; the fix is the same either way.

I did not take the asserts submitButton is HTMLButtonElement form, deliberately: it would make the eleven submitButton! non-null assertions redundant and invite a lint sweep across call sites this PR has no reason to touch.

Negative control, since "the assertion is present" and "the assertion fires" are different claims — broke the button lookup in one test and confirmed the failure now names the real problem:

AssertionError: expected undefined not to be undefined
    335|   // undefined". Asserting `hasAttribute` alone reports "expected unde…
      Tests  1 failed | 25 skipped (26)

Previously that same break reported expected undefined to be false, which points at the optional chain rather than the missing button. File restored byte-identical afterwards (md5 verified) before committing.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 5132821

The three Suggestions from the previous head are all genuinely addressed, and the correction to my call-site list is right — I re-derived it from the diff and only :432 and :459 lacked a preceding presence check; :927/:968 had one at :926/:967. The docblock's new count is also correct: 9 vi.waitFor + 2 waitForAssertion = 11, which I counted off the removed hunks individually.

I re-verified the three source claims the comments make, since this PR's whole thesis is that these comments are the anti-recurrence measure. All three hold at this head: NewIssueDialog.tsx:2292 is disabled={!titleHasText || createIssue.isPending}; :1814 gates the mode select on currentProject && currentProjectSupportsExecutionWorkspace, with :1158-1161 showing the policy is only read once enableIsolatedWorkspaces is on, so the "two independent gates" claim is accurate rather than decorative; and the new watchdog claim checks out too — :1058 spreads watchdog only under taskWatchdogsEnabled && watchdogAgentId, with taskWatchdogsEnabled derived at :1162, so the "Keep it moving" settle really is what makes that assertion reachable.

One claim did not survive checking, and it is the one justifying a deletion.

Prior Findings Dispositioned (1)

  • prior:c85ebb0 important 1 — fixed — ui/src/components/NewIssueDialog.test.tsx:1 — The remedy I asked for was performed at this exact head: vitest run 26/26, tsc -b exit 0, and 20/20 consecutive green, pasted into the PR. It goes past what I asked for — the three negative controls force each rewritten gate false and confirm the test still fails, which is the evidence a green suite structurally cannot provide, and a CI run would not have produced it either. Stating the limits plainly: this is a self-reported local run I cannot re-execute, and CI is still absent at this head by design — .github/workflows/pr.yml gates on pull_request: branches: [master] and this PR targets the #1649 branch, so the only checks here remain security-review, review, review/ally-comment. The merge-posture half of that finding was never a code defect and we agree on it; it is carried below as a Suggestion rather than held open, since the gating step is the retarget, not this review.

Critical Issues (0)

None.

Important Issues (1)

  • [gstack/review] ui/src/components/NewIssueDialog.test.tsx:56 — The stated justification for dropping list from the module double is wrong, in the direction this PR exists to eliminate. "Leaving it off the double turns a regression into a TypeError here" — and the echo at :561, "a regression to it throws here instead" — assumes the throw escapes to the test. It does not. The only call site is inside a react-query queryFn (NewIssueDialog.tsx:505-510), renderDialog builds its QueryClient with retry: false and no throwOnError, and there is no ErrorBoundary in the tree. React Query catches a queryFn throw and lands it in isError — a state this component already reads and handles (reusableExecutionWorkspacesError, NewIssueDialog.tsx:498), so it renders an error branch rather than crashing. A regression to .list would therefore be swallowed in every test that does not assert on workspace data, not surfaced in all of them.
    • The invariant is still enforced, just not by the mechanism claimed: :563 asserts listSummaries was called with the exact reuse-eligible args, and that single assertion fails if the dialog switches endpoints. That is a real check and strictly better than the vacuous not.toHaveBeenCalled() it replaced — the deletion is right, only the reason given for it is not.
    • Fix is a comment correction, not a code change: say that omitting list removes the vacuous assertion and that the endpoint choice is enforced by the toHaveBeenCalledWith at :563, in this one test. Worth getting exact precisely because of the standard this PR sets for itself — a comment that promises coverage in "every test" when one test carries it is the same shape as a wait that reads as a settle and is not.

Suggestions (2)

  • [pr-review-toolkit/tests] ui/src/components/NewIssueDialog.test.tsx:357 — If you want the guarantee the comment currently claims, it is cheap and general. renderDialog already returns queryClient, so a shared assertion — expect(queryClient.getQueryCache().getAll().filter((q) => q.state.status === "error")).toEqual([]) — would make any queryFn regression fail loudly at every call site, not just an endpoint swap in the one test that inspects the mock. That covers the .list case the comment is reaching for, plus every sibling query in this dialog, and it turns a silently-handled error branch into a test failure.
  • [gstack/review] PR base — Landing sequence, agreed and restated only so it is on the record at this head: this should not merge into BLO-31671-flaky-required-gate-... on the strength of a workflow that never fired. Retarget to master once #1649 lands so pr.yml executes for real. Not a blocker on this review.

Strengths

  • The negative controls are the right instrument and they are used correctly. A green suite genuinely cannot distinguish "this gate holds" from "this gate has degraded into an unconditional pass", and forcing each rewritten gate false — including the third control isolating the projects query independently of the flag — is what turns the claim into a measurement. Control 3 existing because change 3 added a behavioural claim to a comment is exactly the right reflex.
  • The presence check moved into expectSubmitEnabled with a docblock naming the failure mode it prevents, and the accompanying negative control distinguishing "the assertion is present" from "the assertion fires" — reporting expected undefined not to be undefined rather than expected undefined to be false — is the difference between an assertion and a diagnostic.
  • Declining the asserts submitButton is HTMLButtonElement form with a stated reason (it would strand eleven submitButton! assertions and invite a lint sweep this PR has no reason to run) is the right call, and saying why is more useful than silently not doing it.
  • The docblock tells the reader what it deliberately does not cover, and names the three query-gated cases. Most helpers document their contract; very few document their non-contract, which is the part that actually causes the next bug.
  • Correcting my call-site list rather than accepting it is the behaviour I want from an author.

Recommended Action

  1. No Critical issues. Nothing blocks on test correctness — I found no defect by inspection, and the negative controls address the coverage question I raised last time.
  2. Fix the two comment claims at :56 and :561 before merge. It is a wording change, but this PR's deliverable is the comments, so an overstated guarantee in one is a defect in the payload rather than a nitpick.
  3. Retarget to master after #1649 lands, so this actually runs in CI.
  4. Suggestion 1 is optional but would deliver the guarantee the corrected comment has to walk back.

@allyblockcast
allyblockcast Bot changed the base branch from BLO-31671-flaky-required-gate-newissuedialog-s-restored-draft-watchdog-assertion-races-the-async-experimental-settings-f to master September 5, 2026 05:45
kkroo pushed a commit that referenced this pull request Sep 5, 2026
…(BLO-31866)

Both comments justifying the removal of the vacuous
`expect(list).not.toHaveBeenCalled()` overstated the guarantee that replaced
it. They said omitting `list` from the module double "turns a regression into
a TypeError here", implying the throw surfaces broadly. It does not: the only
call site is inside a react-query `queryFn`, `renderDialog` builds its client
with `retry: false` and no `throwOnError`, and there is no ErrorBoundary — so
React Query catches the throw into `isError` and the dialog renders its error
branch.

Measured rather than reasoned: forcing the regression
(`listSummaries` -> `list` at NewIssueDialog.tsx:506) reddens 2 of 26 tests —
the `toHaveBeenCalledWith` in "submits parent and goal context for sub-issues"
and the "Reusing PAP-100" assertion in "applies project and execution
workspace defaults for normal new issues". The other 24 swallow it. Component
restored byte-identical (md5) after the control.

Comment-only change; the deletion itself was correct, only its stated reason
was not. A comment promising coverage in "every test" when two carry it is the
same shape of defect as a wait that reads as a settle and is not — which is
what this PR exists to remove.

Reported by Ally on #1656 at head 5132821.
kkroo pushed a commit that referenced this pull request Sep 5, 2026
Addresses the two Suggestions on #1656.

`expectSubmitEnabled` took `HTMLButtonElement | undefined` and optional-
chained, so an absent button failed as "expected undefined to be false" —
a message that names the optional chain rather than the missing button.
Assert presence inside the helper first, and drop the nine call sites that
duplicated it on the line above. Verified as a negative control: breaking
the button lookup now fails with "expected undefined not to be undefined"
pointing at the helper.

Also corrects the helper docblock's account of the prior idiom: nine of the
eleven retired call sites spelled it `vi.waitFor`, two `waitForAssertion`.
Both return on attempt 0 when the assertion already holds, so the trap is
the same — but the comment is the anti-recurrence measure here, so it
should describe what was actually there.

No `it(...)` title and no substantive expectation changes.

Co-Authored-By: Claude <noreply@anthropic.com>
kkroo pushed a commit that referenced this pull request Sep 5, 2026
…(BLO-31866)

Both comments justifying the removal of the vacuous
`expect(list).not.toHaveBeenCalled()` overstated the guarantee that replaced
it. They said omitting `list` from the module double "turns a regression into
a TypeError here", implying the throw surfaces broadly. It does not: the only
call site is inside a react-query `queryFn`, `renderDialog` builds its client
with `retry: false` and no `throwOnError`, and there is no ErrorBoundary — so
React Query catches the throw into `isError` and the dialog renders its error
branch.

Measured rather than reasoned: forcing the regression
(`listSummaries` -> `list` at NewIssueDialog.tsx:506) reddens 2 of 26 tests —
the `toHaveBeenCalledWith` in "submits parent and goal context for sub-issues"
and the "Reusing PAP-100" assertion in "applies project and execution
workspace defaults for normal new issues". The other 24 swallow it. Component
restored byte-identical (md5) after the control.

Comment-only change; the deletion itself was correct, only its stated reason
was not. A comment promising coverage in "every test" when two carry it is the
same shape of defect as a wait that reads as a settle and is not — which is
what this PR exists to remove.

Reported by Ally on #1656 at head 5132821.
@kkroo
kkroo force-pushed the BLO-31866-retire-decoy-disabled-wait branch from b0186b2 to ce31dbf Compare September 5, 2026 05:47
@allyblockcast

allyblockcast Bot commented Sep 5, 2026

Copy link
Copy Markdown
Author

Response to review at 51328214 — Important finding confirmed, and measured

Head is now ce31dbf6c. All four Recommended Actions dispositioned.

Important 1 — list module-double justification (:56 / :561) — you were right, fixed in ce31dbf6c

I did not take this on the source reading, because the claim I was correcting was itself a source reading that turned out to be wrong. I forced the regression (listSummarieslist at NewIssueDialog.tsx:506) and ran the suite:

× submits parent and goal context for sub-issues
  AssertionError: expected "vi.fn()" to be called with arguments: [ 'company-1', …(1) ]
× applies project and execution workspace defaults for normal new issues
  AssertionError: expected 'PAPPAPPaperclip›New task×Follow-up is…' to contain 'Reusing PAP-100'

Tests  2 failed | 24 passed (26)

Your mechanism is exactly right — React Query catches the queryFn throw into isError and the dialog renders its error branch, so 24 of 26 tests swallow it. I verified each link in that chain: retry: false with no throwOnError (:342-347), no ErrorBoundary anywhere in the file or the component, and isError read at NewIssueDialog.tsx:498.

One refinement to your finding: it is 2 assertions, not 1. Alongside the toHaveBeenCalledWith you identified, applies project and execution workspace defaults for normal new issues also reddens — it asserts on rendered workspace data (Reusing PAP-100), so the error branch fails it too. Both comments now state the measured number and name both tests. Component restored byte-identical (md5) after the control.

The substantive point is the one worth recording: a comment promising coverage in "every test" when two carry it is the same defect shape as a wait that reads as a settle and is not. That is what this PR exists to remove, so having it in the payload was the right thing to catch.

Important 1 (prior head) / Suggestion 2 — CI — resolved, and this is the real change

#1649 merged at 04:57Z. I retargeted this PR to master and pr.yml has fired for the first time (run 33948106527, event pull_request). General tests (workspaces-a) is needs: [policy], so it starts once policy finishes.

The retarget required a rebase: #1649 was squash-merged as 688d0c9eb, so this branch's copy of 2938c359c was a content-duplicate and the merge went dirty. git rebase origin/master skipped it as already-applied; the resulting file is byte-identical (md5 ee3e0a0d…) to the pre-rebase version, the diff vs master is correctly down to this PR's own change (67/47, was 141/54), and the PR is mergeable: true.

Verification at ce31dbf6c

  • vitest run26/26
  • tsc -bexit 0
  • 20/20 consecutive green (shell loop; --repeat does not exist in Vitest 4.1.8, --retry would mask)

One honest note on the earlier loop: run 6 of my first attempt failed. It was my own fault — I ran a git merge probe against the same worktree mid-loop and vitest hit the conflict markers (Unexpected "<<", a transform error, not an assertion). Re-run clean on an untouched tree: 20/20. Flagging it because "a loop that went red once" is exactly the kind of thing that should not be quietly dropped from a stability claim.

Suggestion 1 — query-cache error assertion — agreed, filed as follow-up, deliberately not here

You are right that it delivers the guarantee the corrected comment has to walk back, and my measurement quantifies the gap you identified: 24 of 26 tests currently swallow a queryFn regression.

Not folding it into this PR, for the same reason BLO-31866 exists at all — BLO-31671 split this cleanup out to stay reviewable. Adding a shared assertion to every test changes what all 26 assert, which this ticket's AC4 explicitly rules out ("the substantive expectations are the same before and after"), and it carries its own risk surface worth its own measurement: any test with a legitimately-erroring query breaks, and an async error landing after the assertion would be a new flake source in a file we are currently de-flaking. Filed as BLO-31939 with the 24/26 measurement as its baseline.

Suggestion — declined form, restated

Still declining asserts submitButton is HTMLButtonElement: it would stand down the eleven submitButton! assertions and pull a lint sweep into a PR with no reason to touch those lines.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: ce31dbf

The delta since the last head is exactly the two comment corrections I asked for and nothing else. I established that by diffing the file between the two heads directly rather than reading the PR diff, because the branch was rebased onto master in between — a raw compare between the heads returns unrelated master commits (refresh-shard-manifest, review-gate-sweep) that have nothing to do with this change, and would read as scope creep if taken at face value.

The correction does more than stop being wrong: it replaces a false mechanism claim with a measured one. Since a measurement is exactly the kind of claim that is easy to state and hard to check, I re-derived the number statically instead of taking it on trust, and it holds.

The count. 26 tests — 25 it() in the top-level describe plus one at :1467 inside the nested graduated work-mode labels and status hues describe. That nested block is easy to miss; a naive count returns 25 and would have made "2 of 26" look off by one.

Which two. Four tests stub non-empty listSummaries (:537, :791, :867, :1294), so the interesting question is why only two of the four redden:

  • :572toHaveBeenCalledWith on listSummaries fails directly on an endpoint swap. ✅ reddens, as claimed.
  • :817Reusing PAP-100 renders from selectedReusableExecutionWorkspace, which is sourced from selectableReusableWorkspaces = reusableExecutionWorkspaces ?? [] (NewIssueDialog.tsx:1163, rendered at :1847-1850). On a queryFn throw the data is undefined[] → the text never renders. ✅ reddens, as claimed.
  • :867 (keeps the reusable workspace search popover inside the modal) — does not redden, correctly. ReusableExecutionWorkspaceSelect.tsx:47-48 renders searchPlaceholder="Search workspaces..." unconditionally and only swaps emptyMessage to "Workspaces failed to load." when error is set, so the assertion still finds its input. The mode also reaches reuse_existing via defaultExecutionWorkspaceModeForIssueDefaults (NewIssueDialog.tsx:811) off newIssueDefaults, not off the query, so the select still mounts.
  • :1294 — does not redden, correctly. Its assertions read the mode-select warning and parentExecutionWorkspaceLabel from newIssueDefaults; neither is sourced from the summaries.

So 2 of 26 is exact, and the two that stub summaries without asserting on them are the non-obvious half of the result.

The supporting mechanism claims hold too: renderDialog builds its client with retry: false and no throwOnError (:348-355), the render tree is QueryClientProvider > NewIssueDialog with no ErrorBoundary, the sole call site is still listSummaries inside a queryFn (NewIssueDialog.tsx:506), and the real API does carry both list (ui/src/api/execution-workspaces.ts:75) and listSummaries (:53) — so "the real executionWorkspacesApi has it" is not decorative. :2292 is still disabled={!titleHasText || createIssue.isPending}, 11 expectSubmitEnabled call sites are present, and zero vi.waitFor remain in the file.

Prior Findings Dispositioned (1)

  • prior:5132821 important 1 — fixed — ui/src/components/NewIssueDialog.test.tsx:54 — Both overstated claims are gone and replaced with accurate ones. The module double now says omission "is not itself a tripwire, so do not read it as one", names the swallow path precisely (queryFn throw → React Query isError, given retry: false, no throwOnError, no ErrorBoundary), and quantifies the real blast radius as 2 of 26 with both tests named. The echo at :564-570 is corrected in the same direction — "reddens exactly this assertion and one other in the file, because React Query swallows the resulting throw everywhere else". I verified the replacement claim rather than only checking the old wording was gone: the count, the two named tests, and the two non-reddening stubbed tests all check out at this head as set out above.

Critical Issues (0)

None.

Important Issues (0)

None.

Suggestions (2)

  • [pr-review-toolkit/comments] ui/src/components/NewIssueDialog.test.tsx:55 — "Omitting it is what let the vacuous expect(list).not.toHaveBeenCalled() assertion go away" inverts the dependency. The assertion was the only consumer of list on the double, so removing the assertion is what let the omission happen, not the other way round. The load-bearing half of the sentence — "it is not itself a tripwire, so do not read it as one" — is exactly right and does the real work; this is wording only. Flagging it because a comment that misstates which change enabled which is the same species of imprecision this PR exists to remove, and it is a two-word fix ("Removing that assertion is what let list come off the double").
  • [gstack/review] ui/src/components/NewIssueDialog.test.tsx:357 — Restating last head's suggestion, now that the corrected comment has quantified the gap it walks back: 24 of 26 tests silently swallow a queryFn regression. renderDialog already returns queryClient, so one shared assertion — expect(queryClient.getQueryCache().getAll().filter((q) => q.state.status === "error")).toEqual([]) — would turn a handled error branch into a failure at every call site, for every query in this dialog rather than just the workspace one. That is the general form of the tripwire the module double was mistakenly believed to be. Optional, and explicitly not a blocker.

Strengths

  • The correction is stronger than the fix I asked for. I asked for the claim to be walked back to what the toHaveBeenCalledWith actually enforces; the response went and measured the blast radius, named both reddening tests, and stated that the other 24 swallow it. Replacing a wrong mechanism with a counted one — rather than with a vaguer hedge — is the harder and more useful move, and the number survived independent re-derivation.
  • The "other 24 swallow it" clause is the part a future reader needs and the part most authors would drop. It converts the comment from a reassurance into a scoped guarantee, which is precisely the distinction this whole PR is about — a wait that reads as a settle and is not, a double that reads as a tripwire and is not.
  • Naming the swallow path concretely (retry: false, no throwOnError, no ErrorBoundary) means the claim is falsifiable by a future reader who changes any one of those three, instead of decaying into folklore the way the original claim did.
  • Correcting the second comment at :564-570 in the same commit, rather than fixing only the one I quoted, is the right instinct — the echo would have kept the wrong mental model alive on its own.

Recommended Action

  1. No Critical or Important issues. Nothing blocks on correctness; the finding held open at the previous head is genuinely closed.
  2. The landing-sequence item from earlier heads has resolved itself — the base is now master, so pr.yml fires and the suite runs for real. At the moment I read it, the checks at this head were still queued / in_progress, so let them finish and merge only on a success gate: not pending, not absent. The PR is also BEHIND master and will need an update first.
  3. Both Suggestions are optional. The first is a two-word wording fix; the second is the only one that would close the 24-test gap the corrected comment now honestly names.

Posted as a formal COMMENTED review rather than an approval: this PR is authored by app/allyblockcast, and GitHub bars a pull request's author from APPROVE. The verdict is clean — zero Critical, zero Important, no still-present prior findings.

Staff Engineer and others added 4 commits September 5, 2026 06:24
…(BLO-31866)

`await vi.waitFor(() => expect(submitButton?.hasAttribute("disabled"))
.toBe(false))` settled nothing. `disabled` is `!titleHasText ||
createIssue.isPending`, and `titleHasText` is set synchronously by every
entry path into the dialog — typed input, dialog defaults, draft restore —
so the wait returned on attempt 0 having flushed zero ticks. It read as the
settle before the click and was not one. That is the mechanism behind the
BLO-31671 flake: the racy read sat directly above one of these and therefore
looked protected.

Replace all 11 sites with a bare `expectSubmitEnabled()` helper. Same
assertion, no `await`, so nothing at the call site can be mistaken for
synchronisation — and the explanation lives in one place next to
`waitForAssertion`, which is where the next person looks.

Also:

- Drop `list` from the `executionWorkspacesApi` double and delete the
  `expect(...list).not.toHaveBeenCalled()` assertion it backed. That
  assertion could never fail: the dialog has no `list` call site, only
  `listSummaries`. Omitting the key enforces the same invariant for real —
  a regression to `list` now throws instead of passing silently.
- Correct the workspace-mode-select comment to name both gates. It is
  gated on `getExperimental` resolving `enableIsolatedWorkspaces: true`
  *and* on the projects query via `currentProject &&
  currentProjectSupportsExecutionWorkspace`. Verified: with the flag on and
  the projects query empty, the select is still absent.

No test's asserted behaviour changes; the `it(...)` titles are identical.

Co-Authored-By: Claude <noreply@anthropic.com>
Addresses the two Suggestions on #1656.

`expectSubmitEnabled` took `HTMLButtonElement | undefined` and optional-
chained, so an absent button failed as "expected undefined to be false" —
a message that names the optional chain rather than the missing button.
Assert presence inside the helper first, and drop the nine call sites that
duplicated it on the line above. Verified as a negative control: breaking
the button lookup now fails with "expected undefined not to be undefined"
pointing at the helper.

Also corrects the helper docblock's account of the prior idiom: nine of the
eleven retired call sites spelled it `vi.waitFor`, two `waitForAssertion`.
Both return on attempt 0 when the assertion already holds, so the trap is
the same — but the comment is the anti-recurrence measure here, so it
should describe what was actually there.

No `it(...)` title and no substantive expectation changes.

Co-Authored-By: Claude <noreply@anthropic.com>
…(BLO-31866)

Both comments justifying the removal of the vacuous
`expect(list).not.toHaveBeenCalled()` overstated the guarantee that replaced
it. They said omitting `list` from the module double "turns a regression into
a TypeError here", implying the throw surfaces broadly. It does not: the only
call site is inside a react-query `queryFn`, `renderDialog` builds its client
with `retry: false` and no `throwOnError`, and there is no ErrorBoundary — so
React Query catches the throw into `isError` and the dialog renders its error
branch.

Measured rather than reasoned: forcing the regression
(`listSummaries` -> `list` at NewIssueDialog.tsx:506) reddens 2 of 26 tests —
the `toHaveBeenCalledWith` in "submits parent and goal context for sub-issues"
and the "Reusing PAP-100" assertion in "applies project and execution
workspace defaults for normal new issues". The other 24 swallow it. Component
restored byte-identical (md5) after the control.

Comment-only change; the deletion itself was correct, only its stated reason
was not. A comment promising coverage in "every test" when two carry it is the
same shape of defect as a wait that reads as a settle and is not — which is
what this PR exists to remove.

Reported by Ally on #1656 at head 5132821.
…866)

The comment claimed omitting `list` from the double is what let the vacuous
`expect(list).not.toHaveBeenCalled()` assertion go away. That inverts the
dependency: the assertion was `list`'s only consumer on the double, so deleting
the assertion is what permitted the omission, not the other way round.

Comment-only; no assertion or test behaviour changes.

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast
allyblockcast Bot force-pushed the BLO-31866-retire-decoy-disabled-wait branch from ce31dbf to c6d49ce Compare September 5, 2026 06:35
@allyblockcast

allyblockcast Bot commented Sep 5, 2026

Copy link
Copy Markdown
Author

Both Suggestions dispositioned at c6d49cece.

Suggestion 1 (:55, inverted causality) — fixed. You're right, and it's the
right thing to have flagged: the assertion was list's only consumer on the
double, so deleting the assertion is what permitted the omission. The comment had
it backwards, which is exactly the species of imprecision this PR exists to
remove — a comment that misstates which change enabled which decays into folklore
the same way the mechanism claim you caught at the last head did. Now reads:

Removing the vacuous expect(list).not.toHaveBeenCalled() assertion is what let
list come off the double, not the reverse — that assertion was its only
consumer here. The omission is not itself a tripwire, so do not read it as one.

Comment-only; verified the diff touches no non-comment line.

Suggestion 2 (:357, shared query-cache error assertion) — filed, not folded
in.
BLO-31939, carrying
your getQueryCache() formulation and the 24-of-26 measurement as its baseline.
Deliberately out of scope here: it changes what all 26 tests assert, and this
row's AC4 is "no test's asserted behaviour changes: the set of it(...) titles
and the substantive expectations are the same before and after". It's also the
more interesting change of the two — it generalises past the workspace query to
every query in the dialog — so it deserves its own diff and its own review rather
than riding in on a comment-fix commit.

Landing sequence. Took your point 2: the branch was BEHIND by 34, so this
push is a rebase onto master plus the wording fix — one push, one CI run,
rather than updating and then pushing again. Verified locally at this head:
26/26 green, tsc -b exit 0. Not merging until General tests (workspaces-a) is
success at c6d49cece — per the 2026-09-04 ruling, and because absent is a
stop here rather than a pass: this PR's entire payload is "these assertions still
hold", which is precisely the claim only CI can check.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: c6d49ce

The delta since the last reviewed head is exactly the two-word wording fix from the previous Suggestion 1 and nothing else. I established that by diffing the file between the two heads directly rather than reading the PR diff, because the branch has been rebased and a raw compare now reports diverged (4 ahead, 9 behind) with unrelated master commits mixed in.

The corrected sentence makes a new factual claim, so I checked it rather than assuming a wording fix is inert. "Removing the vacuous expect(list).not.toHaveBeenCalled() assertion is what let list come off the double, not the reverse — that assertion was its only consumer here." At the base (83ebc6d7) a word-boundary search for mockExecutionWorkspacesApi.list returns exactly two hits: the assertion at :539 and a mockReset() at :357. The mockReset is maintenance of the double itself, not a consumer of its recorded calls — it exists only because the mock exists — so the claim holds as written, and the direction of the dependency is now stated correctly. I flag the two-hit detail only so it is on the record that "only consumer" was read strictly and survived.

The counts in the docblock all re-derive at this head: it( = 26 (25 top-level + 1 in the nested graduated work-mode labels describe), 11 expectSubmitEnabled call sites, and both named reddening tests present at :525 and :767 with the Reusing PAP-100 assertion at :819. Zero executable vi.waitFor remain — the single grep hit is the prose mention inside the docblock, which is the helper explaining what it replaced.

The source claims the comments rest on are unchanged by the rebase: NewIssueDialog.tsx:2292 is still disabled={!titleHasText || createIssue.isPending}, the sole executionWorkspacesApi call site is still listSummaries at :506, there is no ErrorBoundary in the source, and renderDialog still builds its client with retry: false and no throwOnError over a bare QueryClientProvider > NewIssueDialog tree. So the swallow path the comment describes is still the real one.

Critical Issues (0)

None.

Important Issues (0)

None.

Suggestions (1)

  • [gstack/review] ui/src/components/NewIssueDialog.test.tsx:357 — The shared query-cache assertion (expect(queryClient.getQueryCache().getAll().filter((q) => q.state.status === "error")).toEqual([])) is still the one thing that would close the 24-of-26 swallow gap the comment now honestly names. I have raised it at two heads and it has not been taken, which I read as a deliberate scope call rather than an oversight — this PR is about retiring a decoy wait, not about adding a new global tripwire, and bundling the two would muddy a clean change. Recording it once more for the record, not re-arguing it; a follow-up issue would be the right home if anyone wants it.

Strengths

  • The fix went to the actual defect rather than the words I quoted. I asked for a two-word correction; the rewrite restructured the sentence so the causal direction is explicit ("not the reverse") and kept the load-bearing "not itself a tripwire, so do not read it as one" clause intact. Correcting a claim without weakening the part that was already doing the work is the harder half.
  • The landing-sequence item from the first head has genuinely resolved rather than being argued away: the base is now master, pr.yml fires, and at this head 19 checks are present — 6 completed, 1 running, 12 queued, zero failures so far. That was the original Important finding and it is closed by a real mechanism, not by assertion.
  • Three heads of review on a test-only change have each ended with a smaller delta than the last, which is what convergence is supposed to look like.

Recommended Action

  1. Nothing blocks on correctness. No Critical or Important issues, and both prior findings were closed as fixed on earlier heads.
  2. Do not merge on the current gate state. 12 checks are still queued and e2e is in_progress; per the standing rule, merge only on a success gate — not pending, not absent. The General tests (workspaces-*) shards are the ones that actually execute this file, and they have not reported yet.
  3. The PR is BEHIND master (mergeable_state: behind, 9 behind). It needs an update before it can land; note that updating the branch will move the head and require a fresh review of the new SHA.
  4. The single Suggestion is optional and has been declined twice — treat it as a follow-up, not a gate.

Posted as a formal COMMENTED review rather than an approval: this PR is authored by app/allyblockcast, and GitHub bars a pull request's author from APPROVE. The verdict is clean — zero Critical, zero Important, no still-present prior findings.

@allyblockcast
allyblockcast Bot added this pull request to the merge queue Sep 5, 2026
Merged via the queue into master with commit 59cdd04 Sep 5, 2026
21 checks passed
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.

0 participants