Skip to content

Commit 4c29f38

Browse files
test(plugin-chatbot): the pending-count pulse pin waits for the count render, not for the button (#26742)
<!-- claude-projects-attribution: {"github_login":"huangyiirene"} --> _Requested by **Irene** · [project thread](https://claude.ai/code/project/chan_01TJXtQ1tVD26ecLDZxFS67x?thread=cmsg_01TJXtQ1tVD26ecLDZxFS67xWykojkGiiPqmQgGV32BZcw)_ Fixes #26647 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:只改一个测试文件。草稿卡「发布 (N)」的计数断言,改成等到计数渲染出来再读,不再在按钮刚出现那一刻读文字。产品代码零改动。 - 为什么改:按钮先画出来,待发布数量是随后异步读回、下一次渲染才带上的。旧断言在按钮出现时就读,机器一忙就读到「发布」而不是「发布 (2)」,已两次把合并队列里的 PR 踢出去。 - 风险与回滚:极低,只动测试;回滚即 revert 这一个提交。 - 先看:`ui/packages/plugin-chatbot/src/__tests__/ChatbotEnhanced.pendingCountPulse-26471.test.tsx` 里新增的 `expectPublishButtonText` 和它替换掉的两处 `findByTestId` 一次性断言。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/plugin-chatbot/src/__tests__/ChatbotEnhanced.pendingCountPulse-26471.test.tsx`,预期 2 passed。 - 需要你做的:无,席位落地。 ## What changes `ChatbotEnhanced.pendingCountPulse-26471.test.tsx` only. Both tests read the draft card's count as ```ts expect(await screen.findByTestId('draft-publish')).toHaveTextContent('发布 (2)'); ``` and now read it through one helper, `expectPublishButtonText`, which is a `waitFor` around the same `getByTestId('draft-publish')` + `toHaveTextContent` assertion. Nothing else in the file moves: the call counts, the pulse leg, the unmount leg and every expected string are byte-identical. `ChatbotEnhanced.tsx` is untouched. ## Why The draft card draws its publish button in the same render as the card. At that moment `fetchPendingDraftCount` has not answered, `livePendingCount` is `undefined`, and the button reads `发布` with no count. The count reaches the DOM on a second render, committed when the read resolves and `setPendingByPackage` lands. `findByTestId` resolves on its first check that finds the button, which is the first render, so the one-shot `toHaveTextContent` that follows races the second render. On an idle box that render usually lands first; under load it sometimes does not, and the assertion reads `发布`. That is exactly what the queue saw. Both ejections (queue runs 40468287272 and 40493270857) failed `unmount unsubscribes: a later pulse reads nothing` with `Expected element to have text content: 发布 (2) / Received: 发布`, at that test's opening `findByTestId('draft-publish')` assertion, before any pulse fires in it. **The count is not wrong** (triage asked to read the red as real until shown otherwise): the button shows no count while the count is unknown and `发布 (2)` once it is known, which is the component's stated design ("Leave the package unknown — the card falls back to the in-memory snapshot rather than guessing"). The pin now waits for the render that carries the count instead of the render that carries the button. No sleep, retry, skip, timeout change or quarantine. ## Tests All local, on this branch's head `b192892a` unless marked `origin/main` (`f6ffd723`); 4-core container, so absolute durations are shared-box figures. - **Reproduced on `origin/main` under load:** 24 runs of the unchanged file with 6 CPU busy-loops: 23 passed, **1 failed** in `unmount unsubscribes` with `Received: 发布`, the queue's signature byte for byte. - **Fix under the same load** (plus a gate sweep running beside it): 24 runs, **24 passed**, 0 failed. - **Deterministic reverse check** (scratch file, deleted after the run, never committed): with a `fetchPendingDraftCount` that answers 30 ms later, the old pin fails every time (`Received: 发布`) and the new pin passes. - **The new pin still catches a wrong count:** with the count suffix removed from the button in `ChatbotEnhanced.tsx` (via `scripts/ablation-replace.mjs`), both tests go red with `Received: 发布`; restored, blob equals `HEAD` (`2233fd2f`), `git diff HEAD` empty. - `pnpm --dir ui exec vitest run packages/plugin-chatbot/`: 86 files, 816 tests passed. - `pnpm --filter @object-ui/plugin-chatbot run type-check`: exit 0; `tsc -p tsconfig.test.json --listFiles` lists this test file. ## Acceptance notes - **Mechanism hypothesis, measured.** The dispatch guessed the red line races the *pulse's* re-render. The red is real and load-dependent, but it races the *first count read's* re-render: the failing assertion runs before the test fires any pulse. The pulse leg in the first test is wrapped in `await act(...)`, which flushes that update before the assertions after it, and was never the red line. - **No same-class sibling here.** In `ui/packages/plugin-chatbot/src`, a one-shot text assertion straight after a `findBy*` occurs only at these two calls. The other suite that wires `fetchPendingDraftCount` (`ChatbotEnhanced.changeCardV2-12894`) reads the count inside `await act(...)`. - **No changeset, `skip-changeset` instead.** The diff is one test file. `@object-ui/plugin-chatbot` ships `dist`, `README.md`, `CHANGELOG.md` and `LICENSE`. After a build, `dist` holds 0 hits for `expectPublishButtonText` or `unmount unsubscribes`, and 2 files for the positive control `draft-publish`. So nothing publishes, and a patch changeset would put a test-only line in the released CHANGELOG. - `origin/main` gained one commit after this branch's base (`43731031`, `ui/packages/auth` only), outside this package's dependency closure; not merged in. ## Verification <details><summary>Queue failure log (job 122069386772, run 40468287272)</summary> ``` FAIL dom packages/plugin-chatbot/src/__tests__/ChatbotEnhanced.pendingCountPulse-26471.test.tsx > objectstack#26471 — the draft card re-reads its pending count on the metadata pulse > unmount unsubscribes: a later pulse reads nothing Error: expect(element).toHaveTextContent() Expected element to have text content: 发布 (2) Received: 发布 ``` </details> <details><summary>Loaded loops: origin/main vs this branch</summary> ``` LABEL=main-loaded6 runs=24 burners=6 pass=23 assertion_fail=1 other=0 1 FAIL |dom| packages/plugin-chatbot/src/__tests__/ChatbotEnhanced.pendingCountPulse-26471.test.tsx > objectstack#26471 — the draft card re-reads its pending count on the metadata pulse > unmount unsubscribes: a later pulse reads nothing LABEL=fix-loaded6 runs=24 burners=6 pass=24 assertion_fail=0 other=0 ``` </details> <details><summary>Forced slow read: old pin vs new pin (scratch, not committed)</summary> ``` × OLD pin: findByTestId then a one-shot text assertion Expected element to have text content: 发布 (2) Received: 发布 Tests 1 failed | 1 passed (2) ``` </details> <details><summary>Count-suffix mutation against the new pin</summary> ``` ablation-replace: ok mutation landed: anchor 1 -> 0, blob 2233fd2 -> 7e324d005b78 × 发布 (2), then a publish elsewhere fires the pulse and the card stops offering 发布 1093ms × unmount unsubscribes: a later pulse reads nothing 1024ms Tests 2 failed (2) ablation-replace: ok restored: blob == HEAD (2233fd2) and `git diff HEAD` is empty ``` </details> <details><summary>Gates (derived by dispatch-gates --commands at b192892)</summary> 24 derived commands, all exit 0, reconciled by `dispatch-gates --ran`: "24 derived famil(ies) accounted for — 24 run, 0 NOT-MEASURED (a DERIVED zero)". Among them `node scripts/check-sdui-manifest.mjs` (and `--self-test`), `pnpm check:nul-bytes`, `pnpm check:published-files`, `pnpm check:test-source-alias`, `pnpm check:cross-package-test-inputs`, `pnpm check:type-check-coverage`, `pnpm check:type-check-debt`, and `pnpm --dir ui exec vitest run scripts/` (178 files passed, 2 skipped). Also `pnpm check:new-line-citations` (0 new), and eslint on the changed file (clean). Repo-wide lint, the type-check lanes and the full UI farm are CI's. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (eslint clean on the changed file; repo-wide run is CI's) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (publishes nothing: `skip-changeset`) - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (not touched) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01J8VVAkq9LgnKiBeyJBZqSp)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent abecc12 commit 4c29f38

1 file changed

Lines changed: 13 additions & 3 deletions

File tree

‎ui/packages/plugin-chatbot/src/__tests__/ChatbotEnhanced.pendingCountPulse-26471.test.tsx‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
*/
2222
import '@testing-library/jest-dom/vitest';
2323
import { afterEach, describe, expect, it, vi } from 'vitest';
24-
import { act, cleanup, render, screen } from '@testing-library/react';
24+
import { act, cleanup, render, screen, waitFor } from '@testing-library/react';
2525
import { ChatbotEnhanced, type ChatbotEnhancedMessage } from '../ChatbotEnhanced';
2626
import { detectDraftResult } from '../mapMessages';
2727

@@ -81,14 +81,24 @@ function chat(pulse: ReturnType<typeof makePulse>, fetchPendingDraftCount: (pkg:
8181
);
8282
}
8383

84+
/**
85+
* The button draws with the card, before the first count read answers; the
86+
* count lands on a later render. `findByTestId` resolves on the button alone,
87+
* so a one-shot text check after it races that render (objectstack#26647):
88+
* wait for the render that carries the count.
89+
*/
90+
async function expectPublishButtonText(text: string) {
91+
await waitFor(() => expect(screen.getByTestId('draft-publish')).toHaveTextContent(text));
92+
}
93+
8494
describe('objectstack#26471 — the draft card re-reads its pending count on the metadata pulse', () => {
8595
it('发布 (2), then a publish elsewhere fires the pulse and the card stops offering 发布', async () => {
8696
const pulse = makePulse();
8797
let pending = 2;
8898
const fetchPendingDraftCount = vi.fn(async (_pkg: string) => pending);
8999
const { rerender } = render(chat(pulse, fetchPendingDraftCount));
90100

91-
expect(await screen.findByTestId('draft-publish')).toHaveTextContent('发布 (2)');
101+
await expectPublishButtonText('发布 (2)');
92102
expect(fetchPendingDraftCount).toHaveBeenCalledTimes(1);
93103
expect(fetchPendingDraftCount).toHaveBeenCalledWith(PACKAGE_ID);
94104

@@ -113,7 +123,7 @@ describe('objectstack#26471 — the draft card re-reads its pending count on the
113123
const fetchPendingDraftCount = vi.fn(async (_pkg: string) => 2);
114124
const { unmount } = render(chat(pulse, fetchPendingDraftCount));
115125

116-
expect(await screen.findByTestId('draft-publish')).toHaveTextContent('发布 (2)');
126+
await expectPublishButtonText('发布 (2)');
117127
expect(pulse.listeners.size).toBe(1);
118128

119129
unmount();

0 commit comments

Comments
 (0)