Repository navigation
test(runtime): pin the api action door's 2xx reading of a { success: false } answer - #28007
Merged
objectstack-fleet[bot] merged 2 commits intoOct 30, 2026
Merged
objectstack-fleet[bot] merged 2 commits into
objectstack-fleet[bot] merged 2 commits into
Conversation
…false } answer A same-origin type:'api' target's 2xx answer is the action's result whatever its body says. The measured census found no declared target that answers a 2xx with a top-level success: false, so today's reading is pinned, with the census on apiActionResultOf's docblock. Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
…i-action-2xx-success-false
Contributor
📓 Docs Drift Check
What this run could not see
Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Contributor
Author
|
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F 维护者速读(席位终稿)
席位意见: ACCEPT。这一支是 triage 指定的实测结论,测试钉住了今天的读法。 席位核对ACCEPT · head
|
objectstack-fleet
Bot
deleted the
claude/issue-27918-api-action-2xx-success-false
branch
October 30, 2026 08:11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
Requested by Sales · project thread
Fixes #27918
Clause-②: no
Metadata-protocol: none
Skills-docs: none — no behaviour changes; no skill or doc describes how this door reads a 2xx body
维护者速读
type: 'api'动作转发门对目标 2xx 答复的读法保持原样,只把它钉住:docblock 写明"2xx 一律按成功读,不看 body 的 success",并加了单元测试 + 一个门级测试。type: 'api'动作目标后,没有任何一个会以 2xx 答顶层success: false(拒绝全部是非 2xx),所以按分诊结论"钉住现状并关卡"。packages/runtime/src/action-execution.ts(apiActionResultOf的 docblock,实测清单的结论在这里)→packages/runtime/src/domains/api-action-forward.test.ts(新增 4 个用例)。pnpm --filter @objectstack/runtime exec vitest run src/domains/api-action-forward.test.ts,预期 29 passed。What changes
Measure-first, per triage 6515335719. No same-origin
type: 'api'target in this repository answers HTTP 2xx with a top-levelsuccess: false, so today's reading is pinned rather than changed:apiActionResultOfdocblock: a 2xx is read as success whatever its body says, and why (the measured fact, plus "a target that starts answering that shape has the defect itself; the remedy goes in that target, never a body check here").api-action-forward.test.ts: a door-level case throughhandleActions(200{ success: false, data }→ 200data; 200{ success: false, message }→ 200 with the body asdata) and three unit cases through the realapiActionResultOfexport.No production behaviour changes.
skip-changeset: the onlysrcedit is a comment (fast lane).Acceptance notes
The measured census (tree: objectstack-ai/objectstack @ 5f6dc96, re-read after merging main @ 4a5396b)
Declarations:
git grep -nE "type:\s*['\"]api['\"]"outside tests/dist/docs = 244 hits; the action declarations among them name 77 targets acrosspackages/platform-objects(58, allsys_*identity actions),plugin-approvals(9),service-datasource(2, metadata-type actions),examples/app-showcase(4), andui/(1 dev harness + 3 grid demos on/api/demo/*, which no server serves). JSON/YAML metadata: 0 action declarations. Dogfood fixtures: one test-injected target,/api/v1/cloud/organizations/{id}/change-slug, served by the cloud repo (out of this tree).Routes those targets resolve to, and what each answers on a refusal:
success: falsesitessuccess: false?/api/v1/auth/**objectstack mounts (admin/, sso/, sys-session/revoke, sys-oauth-application/register, organization add-member, send-verification-email …)packages/plugins/plugin-auth/src/*!resp.ok/ a thrown error's status (mapAuthApiError,resolveThrownHttpError)/api/v1/auth/**stock better-auth routesbetter-auth@1.7.12disttruesuccessonly on/organization/has-permissionand/admin/has-permission({ error: null, success: bool }at 200)/api/v1/approvals/requests/{id}/*(9)packages/rest/src/approvals-routes.tsres.json(out)relays the service result;ApprovalDecisionResult/Recall/SendBack/Resubmit/{ request }carry nosuccesskey; refusals go throughhandleApprovalError/ 4xx-5xx/api/v1/datasources/:name/test,/migrate-credentialpackages/services/service-datasource/src/admin-routes.ts:762,786sendOk→{ success: true, data }always/api/v1/keyspackages/runtime/src/domains/keys.ts/api/v1/data/sys_api_key/{id}(PATCH)packages/runtime/src/domains/data.tsdeps.success/deps.error/api/v1/apps/showcase/recalc(flow endpoint)packages/runtime/src/endpoint-executor.ts{ success: true, data: { success: false } }, and the engine's one unclassified exit (flow not found) is pre-probed to 404/api/v1/print/records,/api/v1/forms/:slug/submitservice-print/src/print-routes.ts,rest/src/form-routes.tssendError/ mapped statusNested
{ success: true, data: { success: false } }answers (flow residual, a zero-rowDELETE /data, a package rollback) come back through this door as the same bytes: unwrapdata, re-wrap withdeps.success. No live probe was run: the code read is not ambiguous for any row.actions.tsledger note inscripts/check-route-envelope.mjs("the non-2xx relay (deps.errorspread)") "after PR fix(runtime): a forwarded api action keeps the target's whole refusal, and decision outputs pass the param check #27915". fix(runtime): a forwarded api action keeps the target's whole refusal, and decision outputs pass the param check #27915 is an open draft that has not merged, and onmainthe non-2xx relay still goes throughdeps.errorspread (actions.ts:851), so the note is accurate today. Rewording it here would make it wrong onmain. The rewording belongs to fix(runtime): a forwarded api action keeps the target's whole refusal, and decision outputs pass the param check #27915, which changes that code.git merge-treeagainst the three open PRs that touch these files (feat(spec,metadata-protocol,rest,objectql,runtime): GET /meta/object serves switched-off packaged actions beside the item, and the disabled-action refusal reads as a plain sentence #27964, fix(mcp): an action on a sys_* object whose author exposed it to AI is listed and run over MCP, behind every gate #27940, fix(runtime): a forwarded api action keeps the target's whole refusal, and decision outputs pass the param check #27915): all merge clean.Verification
Ablation (scripts/ablation-replace.mjs; the test imports `../action-execution.js`, so src, no dist)
success: falseas a refusal:actions.tsif (answer.status >= 200 && answer.status < 300) {→… && (answer.body as any)?.success !== false) {. Mutation landed (anchor 1→0, blob da0ea3439e21→0967d680dd2b). Red:Tests 1 failed | 28 passed (29),relays a 2xx { success: false, … } answer as a 200 result→expected false to be true. Restored: blob after == HEAD (da0ea3439e21).success: falsebody:'data' in body && 'success' in body→'data' in body && (body as any).success !== false. Landed (a809a3f76e9d→ec5627410e23). Red:Tests 2 failed | 27 passed (29): the door case and the{ success: false, data }unit case,expected { success: false, …(1) } to deeply equal { reason: 'already_banned' }. Restored: blob after == HEAD (a809a3f76e9d).Local gates @ f442344
pnpm --filter @objectstack/runtime test: Test Files 482 passed (482) · Tests 7844 passed, 19 skipped · VERDICT command-exit 0pnpm --filter @objectstack/runtime typecheck(tsc + check:test-typecheck): VERDICT command-exit 0eslint --no-inline-config packages/runtime: exit 0dispatch-gates --commandsderived 52 families; all run, each exit captured before any pipe;--ran: "52 derived famil(ies) accounted for — 51 run, 1 NOT-MEASURED". The one:pnpm check:dual-build-cjs-loadsexit 3, PREREQUISITE NOT MET (it reads every package'sdist/, which needs a fullpnpm build), so NOT MEASURED; CI runs it.!in the title, no(narrowing)arm).Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F