Skip to content

test(runtime): pin the api action door's 2xx reading of a { success: false } answer - #28007

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-27918-api-action-2xx-success-false
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-27918-api-action-2xx-success-false

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

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),所以按分诊结论"钉住现状并关卡"。
  • 风险与回滚:零运行时风险:生产代码只改了注释,其余是测试。回滚即 revert 本 PR。
  • 先看: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-level success: false, so today's reading is pinned rather than changed:

  • apiActionResultOf docblock: 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 through handleActions (200 { success: false, data } → 200 data; 200 { success: false, message } → 200 with the body as data) and three unit cases through the real apiActionResultOf export.

No production behaviour changes. skip-changeset: the only src edit 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 across packages/platform-objects (58, all sys_* identity actions), plugin-approvals (9), service-datasource (2, metadata-type actions), examples/app-showcase (4), and ui/ (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:

Target family Route owner success: false sites 2xx success: 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/* 100 hits; every one carries a literal 4xx/5xx, or a status derived from a vendor !resp.ok / a thrown error's status (mapAuthApiError, resolveThrownHttpError) none
/api/v1/auth/** stock better-auth routes better-auth@1.7.12 dist non-true success only on /organization/has-permission and /admin/has-permission ({ error: null, success: bool } at 200) none targeted: no declared action names either route
/api/v1/approvals/requests/{id}/* (9) packages/rest/src/approvals-routes.ts res.json(out) relays the service result; ApprovalDecisionResult / Recall / SendBack / Resubmit / { request } carry no success key; refusals go through handleApprovalError / 4xx-5xx none
/api/v1/datasources/:name/test, /migrate-credential packages/services/service-datasource/src/admin-routes.ts:762,786 sendOk → { success: true, data } always none
/api/v1/keys packages/runtime/src/domains/keys.ts 1 (the 405) none
/api/v1/data/sys_api_key/{id} (PATCH) packages/runtime/src/domains/data.ts deps.success / deps.error none
/api/v1/apps/showcase/recalc (flow endpoint) packages/runtime/src/endpoint-executor.ts classified refusals → 4xx; the unclassified residual answers 200 { success: true, data: { success: false } }, and the engine's one unclassified exit (flow not found) is pre-probed to 404 none at top level
/api/v1/print/records, /api/v1/forms/:slug/submit service-print/src/print-routes.ts, rest/src/form-routes.ts 0; sendError / mapped status none

Nested { success: true, data: { success: false } } answers (flow residual, a zero-row DELETE /data, a package rollback) come back through this door as the same bytes: unwrap data, re-wrap with deps.success. No live probe was run: the code read is not ambiguous for any row.

Verification

Ablation (scripts/ablation-replace.mjs; the test imports `../action-execution.js`, so src, no dist)
  1. Door reads a 2xx success: false as a refusal: actions.ts if (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).
  2. Unwrap skips a success: false body: '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 0
  • pnpm --filter @objectstack/runtime typecheck (tsc + check:test-typecheck): VERDICT command-exit 0
  • eslint --no-inline-config packages/runtime: exit 0
  • dispatch-gates --commands derived 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-loads exit 3, PREREQUISITE NOT MET (it reads every package's dist/, which needs a full pnpm build), so NOT MEASURED; CI runs it.
  • Dogfood battery: n/a (no ! in the title, no (narrowing) arm).

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F

…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>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/runtime/src/action-execution.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/runtime/src/action-execution.ts) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f4826f8105d0a7d6c62784ba341c19ea253d3715 → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

维护者速读(席位终稿)

  • 改了什么:不改行为。apiActionResultOf 的注释写明:目标返回 2xx 时,无论正文写什么都按成功处理,并给出实测依据。另加 4 个测试钉住今天的读法:1 个在门级,3 个直接调用该函数。
  • 为什么改:卡片怀疑 2xx { success: false } 会被当成成功。triage 要求先实测。本仓库声明的 77 个同源 type: 'api' 目标都实测了,没有一个会在 2xx 里回顶层 success: false,拒收一律走非 2xx。按 triage,这种情况钉住现状并关卡。
  • 风险与回滚:只改注释和测试。回滚 = revert 本 PR。
  • 先看:packages/runtime/src/action-execution.ts 中 apiActionResultOf 的注释。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run src/domains/api-action-forward.test.ts。
  • 需要你做的:无。由席位落地。

席位意见: ACCEPT。这一支是 triage 指定的实测结论,测试钉住了今天的读法。

席位核对

ACCEPT · head f442344db9ba134c3aef0b328d27c0927c518956 · 2026-10-30T07:54Z

  • 形态: 2 files, +57/−0: a docblock on apiActionResultOf and 4 test cases. No behaviour change, no packages/spec. Tier S. skip-changeset (comment and tests only).
  • Clause-②: no, Metadata-protocol: none.
  • 前提实测: the measurement triage 6515335719 asked for, in the os-dev-report 6517296044. All 77 declared same-origin targets were checked: 58 sys_* identity actions, 9 approvals, 2 datasource admin, 4 showcase, 4 in ui/. Each was traced to its route. None answers a 2xx with a top-level success: false. Stock better-auth's two has-permission routes do, and no action targets them.
  • 抽查读数: the seat read the diff. The docblock matches the code ('data' in body && 'success' in body unwraps on any 2xx). The ablation of the door's 2xx branch and of the unwrap went red (1/29, then 2/29), and the files were restored to the HEAD blob.
  • 门禁 check-runs: 57 on this head: 36 success, 21 skipped, 0 other (read 2026-10-30T07:54Z); mergeable_state: clean. Of the derived gates, 51/52 exited 0. The one NOT-MEASURED locally, check:dual-build-cjs-loads, needs a full build, and CI covers it.
  • 接受的偏差: the ride-along check-route-envelope.mjs wording is not changed. It is still true on main, because PR fix(runtime): a forwarded api action keeps the target's whole refusal, and decision outputs pass the param check #27915 has not merged; it was handed to fix(runtime): a forwarded api action keeps the target's whole refusal, and decision outputs pass the param check #27915 in note 6517332525. skip-changeset.
  • 范围外去向: none filed. The has-permission routes have no action target. The api door passes the nested { success: true, data: { success: false } } answers (unclassified flow result, zero-row delete, package rollback) through unchanged, which is correct. The cloud-served change-slug target lives outside this repository.
  • 散文面: the docblock, read by the seat.
  • 落地条款: relay pr_ready + automerge_enable through the merge queue. Merging closes runtime: the type: 'api' action door reads a target's HTTP 200 { success: false } answer as a result, so the rejection is lost #27918 via Fixes.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 07:55
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 30, 2026 07:55
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 0aac0bf Oct 30, 2026
58 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-27918-api-action-2xx-success-false branch October 30, 2026 08:11
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 tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runtime: the type: 'api' action door reads a target's HTTP 200 { success: false } answer as a result, so the rejection is lost

1 participant