现象
frontend/src/pages/workflow-editor/index.test.tsx 的
上传角色参考图期间禁止生成,成功后写回 WorkflowRun
间歇失败:
AssertionError: expected { id: 'character-setup', …(7) } to match object { input: { …(2) } }
拿到的节点没有 input 字段——说明断言跑在写回完成之前。
复现率
在一个零前端改动的分支上(前端与 main 逐字节一致)本地连跑 6 次:
| 次 |
结果 |
| 1 / 2 / 3 |
29 passed |
| 4 |
1 failed |
| 5 / 6 |
29 passed |
约 1/6。CI 上也复现过一次(432 项里就它一个红)。
根因方向
index.test.tsx:131-143:
await act(async () => {
pendingUpload.resolve('opaque-reference-1' as MediaReference)
await pendingUpload.promise
})
await waitFor(() =>
expect(session.controller.getWorkflow().nodes[0]).toMatchObject({
input: { prompt: ..., referenceMedia: ['opaque-reference-1'] },
}),
)
await pendingUpload.promise 只等到那个 promise 自己兑现,不保证它的 .then 链(真正执行写回的那段)已经跑完。waitFor 本该兜住这个,但它默认的轮询间隔与 act 的调度在偶发时序下会错开——断言在第一次轮询就取到了尚未写回的节点,而 toMatchObject 对"缺字段"是立即失败、不重试到超时。
影响
它会随机把无关 PR 的 CI 染红。 已经发生:一个零前端改动的后端 PR 因为它 Frontend checks 失败。这类失败最贵的地方不是重跑一次,是让人开始怀疑自己的改动,以及久了之后大家默认"前端红了再跑一次就好"——那时真失败也会被当成 flaky 忽略掉。
建议
- 让断言等到写回真的发生,而不是等 promise 自己兑现:
waitFor 里改成先断言一个必然出现的中间态(如 已关联 1 个参考媒体 这条文案),再断言节点结构;或给 waitFor 显式的 timeout / interval
- 同一文件里其它用了
deferred + act 的用例值得一起看,可能是同型
验收
连跑 30 次零失败(npx vitest run src/pages/workflow-editor/index.test.tsx --repeat 30 或脚本循环)。只跑一次通过不算修好——本 issue 的复现率就是 1/6。
现象
frontend/src/pages/workflow-editor/index.test.tsx的间歇失败:
拿到的节点没有
input字段——说明断言跑在写回完成之前。复现率
在一个零前端改动的分支上(前端与
main逐字节一致)本地连跑 6 次:约 1/6。CI 上也复现过一次(432 项里就它一个红)。
根因方向
index.test.tsx:131-143:await pendingUpload.promise只等到那个 promise 自己兑现,不保证它的.then链(真正执行写回的那段)已经跑完。waitFor本该兜住这个,但它默认的轮询间隔与act的调度在偶发时序下会错开——断言在第一次轮询就取到了尚未写回的节点,而toMatchObject对"缺字段"是立即失败、不重试到超时。影响
它会随机把无关 PR 的 CI 染红。 已经发生:一个零前端改动的后端 PR 因为它
Frontend checks失败。这类失败最贵的地方不是重跑一次,是让人开始怀疑自己的改动,以及久了之后大家默认"前端红了再跑一次就好"——那时真失败也会被当成 flaky 忽略掉。建议
waitFor里改成先断言一个必然出现的中间态(如已关联 1 个参考媒体这条文案),再断言节点结构;或给waitFor显式的timeout/intervaldeferred+act的用例值得一起看,可能是同型验收
连跑 30 次零失败(
npx vitest run src/pages/workflow-editor/index.test.tsx --repeat 30或脚本循环)。只跑一次通过不算修好——本 issue 的复现率就是 1/6。