You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit f7ff693
Browse filesBrowse the repository at this point in the historyBrowse files
fix(app-shell): a flow-backed api action reads the run's inner refusal, not the node chain (#27630)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgDFH4S5pR1C8YvYCWWsZfXx)_
## 维护者速读
- 改了什么:`api` 类型的动作(记录页 ⋯
菜单、列表行和工具栏)调用流程端点失败时,不再把引擎的节点堆栈原样弹给用户,改为显示内层拒绝的白话提示(权限拒绝就显示和记录保存一样的本地化权限提示)。
- 为什么改:成员点「Recalculate Estimate」看到的是 `Node 'recalc_each' failed: map …
subflow … update_record(showcase_task) failed: …`
这种开发者文字;流程界面早已有提取规则,api 动作入口没有接上。
- 风险与回滚:只影响 api 动作收到 `400 FLOW_FAILED` 的分支,其他错误照旧显示原消息;回滚即还原本 PR。
- 先看:`ui/packages/app-shell/src/utils/flowResponse.ts`(新导出
`apiActionFailureMessage`)、`views/RecordDetailView.tsx`、`hooks/useConsoleActionRuntime.tsx`
的 `!res.ok` 分支。
- 两分钟验证:`pnpm --dir ui exec vitest run
packages/app-shell/src/utils/__tests__/flowResponse.test.ts`,预期全绿,其中
objectstack#27487 一组 5 例。
- 需要你做的:无,席位落地。showcase 那条 map → subflow 链要等服务端卡 #27601 落地后才会显示白话提示。
Refs #27487 (item 1: the console action door)
Clause-②: no
Metadata-protocol: none
## What changes
A `type: 'api'` action whose target is a declared `type: 'flow'`
endpoint (showcase "Recalculate Estimate" →
`/api/v1/apps/showcase/recalc`) gets the trigger door's `400
FLOW_FAILED` when the run fails, `error.details.innerRefusal` and the
top-level `userMessage` included (runtime `endpoint-executor.ts` builds
it with the trigger door's own `flowFailureDetails`). Both api handlers
answered that envelope's `message`, the engine's node chain:
- `RecordDetailView`'s `apiHandler` (the record header ⋯ menu, the
card's repro door) read `error.message`;
- `useConsoleActionRuntime`'s `apiHandler` (list rows, toolbar, shell)
read `actionErrorDetail`.
Neither path goes through `flowLaunch` / `interpretFlowResponse`, so
#25938's extraction never ran there.
`flowResponse.ts` now exports `apiActionFailureMessage(res, json,
fallback, locale)`. On `400 FLOW_FAILED` it reads the envelope through
the failed arm's existing rule (`flowFailureMessage`: authored
`errorMessage`, then `userMessage`, then `innerRefusal` rendered as the
record form renders it, then `message`). Every other answer reads
`actionErrorDetail`, as before. The 400 arm of `interpretFlowResponse`
and the new export share one helper (`flowFailedEnvelopeMessage`), and
`flowFailureMessage` takes the permission line's locale explicitly,
since the api door knows no flow to build a `FlowCopyLocale` for; it
passes the host's active `language`. No new message map.
Both handlers call it on a non-2xx. In `RecordDetailView.tsx` that is
the call-site line plus its import and the `language` entry in the
handler's dependency list (declared in the report).
## Tests
- `utils/__tests__/flowResponse.test.ts`: new `apiActionFailureMessage`
group. One-level and nested map → subflow envelopes (each proven real by
`ApiErrorSchema` and `FlowFailureDetailsSchema`) with `innerRefusal:
PERMISSION_DENIED` read the form's permission line in `en` and `zh-CN`;
a validation refusal reads its sentence; `userMessage` and the authored
`errorMessage` outrank it; an envelope with no carriers reads its
message; a 403, a non-`FLOW_FAILED` 400 and a bodiless 502 read as
before.
-
`hooks/__tests__/useConsoleActionRuntime.apiFlowRefusal-27487.test.tsx`:
the hook's `apiHandler` against the nested envelope answers the `zh`
permission line from the host's language; control: a 403 reads its
message.
- `views/RecordDetailView.apiFlowRefusal-27487.test.tsx`: the handler
set the record page hands its `ActionProvider` sends `recordId` to the
endpoint and answers the permission line; control: no carriers → the
message.
<details><summary>Local runs (head 3219ed5)</summary>
- vitest (6 files: the three above plus `apiNewTabUrl-26747`,
`headerApiInterpolation`, `flowLaunch.translation-12075`): Test Files 6
passed, Tests 85 passed.
- `pnpm --filter @object-ui/app-shell run type-check`: exit 0.
- Ablation (`scripts/ablation-replace.mjs`, marker `!'ABLATED_27487'`
short-circuiting the `FLOW_FAILED` test in `apiActionFailureMessage`): 5
tests red (the 3 new flowResponse cases that read carriers, both door
cases); every control stayed green; restore verified `git diff HEAD`
empty.
- Derived gates (`dispatch-gates --commands`): 34 derived, 34 run, all
exit 0; `--ran` reconciliation: 0 NOT-MEASURED, 0 UNRUN (full lines in
the report comment).
- `pnpm --dir ui run check:new-line-citations`: 0 new citations.
</details>
## Acceptance notes
- This PR is item 1 only. The showcase repro (map item → subflow →
`update_record`) still shows the chain after it lands:
`service-automation`'s `map-node.ts` and `subflow-node.ts` return a new
failure with the child's `error` text and drop the child run's
`innerRefusal` / `userMessage`, so the endpoint's envelope carries no
`innerRefusal` for that chain. Server half filed as #27601. Once it
lands, this door shows the permission line with no further UI change
(pinned by the nested-envelope tests).
- The card's other expectation, not offering the action to someone who
cannot perform its write, is not addressed here: the triage scoped this
card to the toast copy (the refusal itself is correct).
- Observed, not changed: on a `200` whose outer envelope is `success:
true` but whose `data.success` is `false`, both api handlers report
success. The endpoint door keeps that `200` only for an unclassified
failure (its own comment says so); no reach measured, so noted only.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6Ca8en1EvoQ3voL2nWm)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(console): a flow-backed api action shows the run's refusal, not the engine's node chain
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
A `type: 'api'` action whose target is a declared `type: 'flow'` endpoint gets the trigger door's `400 FLOW_FAILED` when the run fails. The record page's action menu and the list and toolbar actions showed that envelope's message, so a member refused on the flow's write read text like `Node 'recalc_each' failed: map … subflow … update_record(showcase_task) failed: …`. Both api handlers now read the answer by the rule a screen flow already uses: the flow's own `errorMessage`, then the producer-marked `userMessage`, then the refusal the write met (`innerRefusal`), rendered as the record form renders it (a permission denial reads the form's permission line in the console's language), and only then the message. Any other error answer reads as before.
11
+
12
+
A failure inside a `map` item or a `subflow` reaches this door with no `innerRefusal` until the engine carries it out of those nodes, so that chain still shows the message.
0 commit comments