Skip to content

fix(core): interpolate api action targets before a registered api handler - #26826

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-3v3vi9
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-3v3vi9

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread

Fixes #26722

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:记录页上 type: "api" 的动作,目标地址里的 ${ctx.recordId} 等占位符现在会先被替换成真实值再发请求,和 url 动作用的是同一套替换与编码。
  • 为什么改:控制台注册了自己的 api 处理器,绕过了 ActionRunner 里唯一做替换的 executeAPI,请求带着字面占位符发出(…/$%7Bctx.recordId%7D),记录级 PATCH/POST 动作在每条记录上都打到不存在的记录。
  • 风险与回滚:只影响「已注册 api 处理器 + 目标里带 ${ 占位符」的动作;不带占位符的动作原对象原样交给处理器。回滚即 revert 本 PR。
  • 先看:ui/packages/core/src/actions/ActionRunner.ts 中 dispatchCollected 的已注册处理器分支;ActionRunner.apiHandlerTarget-26722.test.ts。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/core/src/actions/__tests__/ActionRunner.apiHandlerTarget-26722.test.ts,预期 7 个用例全绿。
  • 需要你做的:无,席位落地。

What changes

ActionRunner.dispatchCollected now runs interpolateTarget on an api action's string target before handing the action to a registered api handler. It is the same call the built-in executeAPI and executeUrl make, so an api target gets the same scopes (ctx, param) and the same per-value encodeURIComponent as a url target. There is no second interpolator and no change in app-shell.

  • The landing site differs from the card's first guess. executeAPI already interpolated, but the Console registers its own api handler (useConsoleActionRuntime's apiHandler, wired through ActionProvider's handlers), and that handler replaces executeAPI entirely. The handler only filled {field} row tokens, so ${ctx.recordId} reached authFetch literally.
  • An api target with no token reaches the handler as the same action object. A registered handler for any other type (flow, script, modal) still gets its target exactly as authored.
  • Comments on interpolateTarget now name the new caller.

Why

The spec documents ${ctx.recordId} for api targets, and a url action with the same token already resolved. Record-level API actions could not be built without code because they failed on every record.

Tests

New ui/packages/core/src/actions/__tests__/ActionRunner.apiHandlerTarget-26722.test.ts (7 cases):

  • a PATCH and a POST api action on a record page hand the registered handler /api/v1/data/gapdata_item/gd_001;
  • an id with a slash and a space is encoded the same way for an api target and a url target (a%2Fb%20c);
  • with no handler registered, the built-in executor sends the same resolved path;
  • a url action with the same token is unchanged;
  • a registered flow handler still gets its target as authored;
  • an untokenized api action reaches the handler as the same object.

Acceptance notes

  • The url path also runs the expression evaluator after substitution; the api path did not before and does not now. The order asked for the same variables and encoding, which this delivers, and nothing more.
  • ${ctx.recordId} on a list row action resolves from the runner's context (the page record), not from the clicked row. That is how url targets behave today too, so it is unchanged here. Row actions keep using the {field} row tokens the Console's handler already fills.

Verification

Ablation (one-time, on head 333eef5, mutation restored)

With actionType === 'api' replaced by false in the new branch (via scripts/ablation-replace.mjs, anchor 1 → 0, blob restored to HEAD's), the new file reads 3 failed / 4 passed: the PATCH, POST and encoding cases go red, and the built-in, url, flow and same-object controls stay green.

Suites and type-check (head 333eef5)
  • pnpm --dir ui exec vitest run packages/core/ --maxWorkers=1 under the verify lock: Test Files 215 passed (215), Tests 4112 passed, 27 skipped.
  • pnpm --filter @object-ui/core run type-check: exit 0.
  • pnpm --dir ui exec vitest run scripts/ --maxWorkers=2 under the verify lock: Test Files 178 passed, 2 skipped (180); Tests 5065 passed, 2 skipped.
  • pnpm check:new-line-citations (from ui/): 0 new citations. eslint on both changed files: 0 errors; the 15 warnings are existing no-explicit-any lines the diff does not touch.
  • Narrowing: app-shell and components suites were not run locally. A git grep over ui tests finds no test outside packages/core that registers an api handler and dispatches a target carrying a ${ token, and untokenized actions reach the handler as the same object. CI's Test UI runs those suites.
Derived gates (head 333eef5)

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 34 commands. 33 ran locally, each exit 0, and with the scripts suite --ran reconciles all of them (Run reconciliation — 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN). The 34th, pnpm --dir ui exec vitest run scripts/, is in the suites block above.


Generated by Claude Code

A registered `api` handler (the console's) replaces `executeAPI`, the only
caller of `interpolateTarget` on the api path, so `${ctx.recordId}` went out
literally. The runner now hands the handler an interpolated target through
the same substitution and encoding a url target uses.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jTDpMW8KaXLg1DFU6f
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 27, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · seat domain:ui#4 · session_01QsR2jTfvjNTaR29ehLk26T · 2026-10-27T18:08Z · head 333eef546cb9d2cb71b9231a2c5506c02fc225ea

  1. Shape: draft, base main, first line Fixes #26722. Clause-②: no and Metadata-protocol: none match the claim (6448265382), in the body and in the changeset (@object-ui/core patch). Three files, inside the surface: ui/packages/core/src/actions/ActionRunner.ts, one new test beside it, one root changeset. No packages/spec/**, server, app-shell or new key.

  2. Premise, measured: the seat read the diff. The built-in executeAPI already ran interpolateTarget; the Console registers its own api handler, which replaces executeAPI wholesale and fills only {field} row tokens, so ${ctx.recordId} went out literally. Now dispatchCollected runs the same interpolateTarget on an api string target before handing the action to a registered api handler: one substitution, same scopes, same per-value encodeURIComponent as url. A target with no token reaches the handler as the same object, and a registered handler for any other type still gets its target as authored. No second interpolator, per triage.

  3. Gates, pinned to the head: check-runs at 333eef54: 52 total, 43 success, 9 skipped, 0 failed · check-governed-merges --pr 26826: not governed (0 of 3 paths). mergeable_state: clean.

  4. Accepted deviations:

    • the landing site is the registered-handler hand-off in dispatchCollected, not the api executor, since the executor already interpolated;
    • app-shell and components suites were not run locally (narrowed to ui core 215 files, 4112 tests); CI's Test UI shards are the reading;
    • open question answered A (no): no app-shell pin through the real console apiHandler. The fix and its pins sit in core, at the one hand-off every registered api handler passes through.
  5. Out of scope: none. Skills-docs: is not owed.

  6. Prose face, checked sentence by sentence against the diff: each changeset claim matches a hunk and a test:

    • a PATCH and a POST api action hand the registered handler the resolved record path;
    • values are encoded as a url target's are;
    • the built-in executor sends the same path, unchanged;
    • a url action and a non-api registered handler are unchanged; a token-free target passes the same object.

    Ablation turned the PATCH, POST and encoding cases red with the 4 controls green; restored.

Landing: no contract review is owed (no published schema, no governed text, Clause-②: no). The seat marks the PR ready and arms auto-merge. On MERGED, Fixes #26722 closes the card.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 18:10
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 27, 2026 18:10
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit e1bfde3 Oct 27, 2026
53 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-3v3vi9 branch October 27, 2026 18:45
@github-actions

Copy link
Copy Markdown
Contributor

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

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

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

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

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

    × lists every key the spec ActionSchema declares 10ms
    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 队列共有 9 个失败构建(不含本次)。

分诊清单:

  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

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console: an api-type action target with ${ctx.recordId} is sent uninterpolated (…/$%7Bctx.recordId%7D) — url targets interpolate, api targets do not

2 participants