Skip to content

test(plugin-chatbot): the pending-count pulse pin waits for the count render, not for the button - #26742

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-vpqn8c
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-vpqn8c

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Irene · project thread
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

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

Queue failure log (job 122069386772, run 40468287272)
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:
  发布
Loaded loops: origin/main vs this branch
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
Forced slow read: old pin vs new pin (scratch, not committed)
× OLD pin: findByTestId then a one-shot text assertion
Expected element to have text content:
  发布 (2)
Received:
  发布
Tests  1 failed | 1 passed (2)
Count-suffix mutation against the new pin
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 2233fd2fe9ef -> 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 (2233fd2fe9ef) and `git diff HEAD` is empty
Gates (derived by dispatch-gates --commands at b192892)

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.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (eslint clean on the changed file; repo-wide run is CI's)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (publishes nothing: skip-changeset)
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (not touched)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

… render, not for the button

The draft card draws its 发布 button before the first fetchPendingDraftCount
read answers; the count lands on a later render. Both tests read the count
with a one-shot toHaveTextContent right after findByTestId, which resolves on
the button alone, so under load the check could run before that render and
read 发布 instead of 发布 (2) — the merge-queue ejections' signature. The
pin now waits for the render that carries the count. Product code unchanged.

Claude-Session: https://claude.ai/code/session_01J8VVAkq9LgnKiBeyJBZqSp
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 27, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读 · 席位定稿

  • 改了什么:只改一个测试文件,「发布 (N)」计数断言改为等计数渲染出来再读;产品代码零改动。
  • 为什么改:按钮先画出、待发布数随后异步读回,旧断言在机器忙时读到「发布」,已两次把合并队列里的 PR 踢出(Queue-flake anchor: packages/plugin-chatbot/src/__tests__/ChatbotEnhanced.pendingCountPulse-26471.test.tsx #26647)。
  • 风险与回滚:极低,只动测试;回滚即 revert 本 PR。
  • 先看:ui/packages/plugin-chatbot/src/__tests__/ChatbotEnhanced.pendingCountPulse-26471.test.tsx 新增的 expectPublishButtonText。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/plugin-chatbot/src/__tests__/ChatbotEnhanced.pendingCountPulse-26471.test.tsx,预期 2 passed。
  • 需要你做的:无,席位落地。
  • 席位意见:ACCEPT。断言字符串与调用次数逐字未动,只把一次性读改为等待同一断言成立;不是 sleep、重试或隔离,反向验证证明新 pin 仍能抓错计数。

席位核对

domain:ui seat 2 · session_01J8VVAkHqAv2eFKVSt9LqkC · reviewed at 2026-10-27T18:17Z on head b192892a6165ff485c2fe990cd04021b9c8e9c01 · verdict ACCEPT

  1. Shape: draft → main; first line Fixes #26647, the only closing keyword in the body. One file, +13 / −3 (get_files), inside the claimed surface ui/packages/plugin-chatbot/**.
  2. Premise measured: the dev reproduced the queue signature on origin/main f6ffd723 (1/24 under load, Received: 发布) and the fix held 24/24. The seat's hypothesis named the pulse re-render; the measured race is the first count read's render. Corrected here.
  3. Gates (pinned to head): 57 check-runs, 43 success, 14 skipped, 0 failed; Lint & Repo Gates success, TypeScript Type Check success. mergeable_state: clean. check-governed-merges --pr 26742: NOT governed, 16 changed lines.
  4. Accepted deviations: none. skip-changeset checked by the seat: the diff is one __tests__ file, which ships in no package files[].
  5. Out of scope: none reported; Acceptance notes carry only the hypothesis correction above.
  6. Landing: ordinary queue landing: pr_ready + automerge_enable through the relay in this window.

Pointer on the card: ACCEPT → this comment.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 18:20
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 27, 2026 18:20
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 4c29f38 Oct 27, 2026
58 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-vpqn8c branch October 27, 2026 18:47
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 40561422008 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test UI (10/16) — 失败步骤: Run the ui tests (shard 10/16)

    × lists every key the spec ActionSchema declares 15ms
    FAIL   unit  packages/core/src/actions/__tests__/actionKeys.pin.test.ts > action key inventory (objectstack#4075 step 1) > lists every key the spec ActionSchema declares
      ↳ 失败原因: AssertionError: expected { missing: [ 'disabledReason' ], …(1) } to deeply equal { missing: [], stale: [] }
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 8 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Queue-flake anchor: packages/plugin-chatbot/src/__tests__/ChatbotEnhanced.pendingCountPulse-26471.test.tsx

2 participants