Skip to content

fix(app-shell): a package-door permission save keeps the pending draft's values - #27080

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-5ielpf
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-5ielpf

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread

Fixes #26837

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:Studio 包内权限集的保存,合并基底从「已发布版本」改为「当前待发布草稿」;没有草稿时仍用已发布版本。
  • 为什么改:经 API 或其他入口写进草稿的 description、packageId 等编辑器不管的键,在 Studio 里勾一个格子就被已发布值覆盖,静默丢数据。
  • 风险与回滚:保存前多读一次草稿;草稿读取失败时拒绝保存,提示与已发布重读失败相同(作者可再次编辑重试)。回滚即还原本 PR 的修复提交。
  • 先看:PermissionMatrixEditor.tsx 的 doSave 包门分支(base 一行与草稿读失败判断);新测试 PermissionMatrixEditor.packageDoorDraftBase-26837.test.tsx;同文件新增的 pendingDraftBody。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.packageDoorDraftBase-26837.test.tsx,预期 3 passed。
  • 需要你做的:无,席位落地。

What changes

  • doSave's package-door arm now reads the pending draft through the load's own door, readDraftForSave, at the address the save writes (in parallel with the existing layered re-read), and merges this package's slice onto the draft when one exists. With no pending draft the base is the published effective, and the 404 shape keeps the ?? payload base, both as before.
  • A REJECTED save-time draft read refuses the save with the same perm.save.rereadFailed text as the layered refusal (objectui#9420); it never falls back to the published body. readDraftForSave resolves a rejection as no answer and no pin, while a read that found no draft pins If-None-Match: *, so that pair is the failure signal.
  • mergePermissionSlice and EDITOR_AUTHORED_KEYS are unchanged: measured, the key list already takes only the authored facets from the edited body. The defect was the base only.
  • The unwrap of a served draft envelope (strip read decorations, objectui#8181) moves into one module-level helper, pendingDraftBody, used by the load and by the save base, so the two can't drift apart.
  • The environment door (no packageId) keeps its whole-record live save unchanged.

Why

The package door's save writes the package DRAFT, but its merge base was the PUBLISHED body. Keys the editor does not author (description, packageId, anything another door wrote) came from that base, so one Studio edit replaced a pending draft's values with published ones (or with none). The response was 200 and nothing showed an error.

Tests

New PermissionMatrixEditor.packageDoorDraftBase-26837.test.tsx drives the real page, load and autosave:

  1. A pending draft whose description, packageId and an out-of-scope row differ from published: every non-authored key and the out-of-scope rows reach the wire equal to the draft, and the edited row lands.
  2. Control, no pending draft: the base is the published body (its description, no packageId, its rows).
  3. The save-time draft read rejects: client.save is never called, the refusal is on the page's error strip, and getDraftCalls === 2 proves the read ran.

Acceptance notes

Verification

Head 4dd4403f (the fix commit d87310b4 plus a merge of origin/main at f708cf22).

Ablation (two legs, both restored to the HEAD blob)

node scripts/ablation-replace.mjs under the verify lock, tests import the editor by relative path (no dist leg):

  • Base back on fresh?.effective ?? payload: anchor 1 → 0, blob ae1632ba → 55370e9d; the draft-survival test went red with expected 'Published description' to deeply equal 'Set through the API' (1 failed, 2 passed); restored, blob equals HEAD, git diff HEAD empty.
  • The draft-read refusal line deleted: anchor 1 → 0; the refusal test went red with one PUT where none was expected (1 failed, 2 passed); restored, blob equals HEAD, git diff HEAD empty.
Tests and type-check
  • All 45 test files that reference the permission editor: 45/45 files, 321/321 tests passed on head 4dd4403f (and on d87310b4 before the merge).
  • 24 further app-shell test files naming the permission type (Studio Access pillar, list pages, previews, draft conflict): 24/24 files, 392/392 tests passed on d87310b4.
  • pnpm --filter @object-ui/app-shell run type-check (both tsc projects, tests included): exit 0.
  • pnpm --dir ui run check:new-line-citations: 0 new citations.
Derived gates (24 families, all exit 0)

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 4dd4403f derived 24 families; each was run and its exit recorded before any pipe; --ran reconciliation: 24 derived, 24 run, 0 NOT-MEASURED, 0 UNRUN. --tier: no path-derived mandate. Changed lines 281, under the 3000 threshold.

No changeset: @object-ui/app-shell is private (skip-changeset).

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (CI runs the repo lint)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (private package, no changeset)
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (not touched)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (the 维护者速读 block is the os-dev order's required Chinese summary)

Generated by Claude Code

…draft

The package door's save re-read the published body as its merge base,
so a pending draft's non-authored keys (description, packageId, any key
another door wrote) were replaced on the next Studio edit. Read the
pending draft through readDraftForSave at the address the save writes
and merge onto it when present; a rejected draft read refuses the save
like the layered re-read does.

Claude-Session: https://claude.ai/code/session_01JQwxba8E9EE7oBM24GbdNZ
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 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · seat domain:ui#1 · session_01JQwxbZeBv2y91towxasSUT · head 4dd4403f · 2026-10-28T04:45Z

席位意见:包作用域下保存权限集时,合并基底改为当前待发布草稿(没有草稿才用已发布版本),别的入口写进草稿的 description、packageId 等不再被已发布值覆盖;草稿读取失败时拒绝保存,不再回落。

  1. Shape: Fixes #26837; 2 files, +263/−18: PermissionMatrixEditor.tsx +45/−18 (one pendingDraftBody unwrap shared by the load and the save base, and the save-time draft read) and one new test file (3 tests). permission-slice.ts is untouched. Labelled skip-changeset (@object-ui/app-shell is private).
  2. Premise measured: the seat read the source diff on the head, with readDraftForSave / draftPinOf on origin/main. The package-door save now reads layered and readDraftForSave (the load's own door, at the address the save writes) together, and the base is pendingDraftBody(draft) ?? fresh.effective ?? payload. mergePermissionSlice and EDITOR_AUTHORED_KEYS are unchanged, so only authored keys come from the editor. Both deliberate arms hold: a rejected layered read still refuses, and the 404 shape still falls to payload. A rejected draft read is told apart from "no draft" by answer === null && pin === null (readDraftForSave catches to that pair; a found-nothing read pins If-None-Match: *), and refuses with the same perm.save.rereadFailed. The save-time read does not re-record the load's pin, so a draft that moved since the load still hits the version-conflict guard. The tests pin draft keys surviving byte-for-byte, the published base with no draft (control), and the refusal. Ablations (dev): base back on fresh.effective and the refusal removed each turn a test red.
  3. Gates / check-runs: PR check-runs on 4dd4403f: 43 success, 15 skipped, 0 failure; mergeable_state clean. Dev's local run: 24/24 derived gates exit 0 (--ran 24/24), type-check 0, and 69 editor test files green.
  4. Accepted deviations: the load-path unwrap was folded into pendingDraftBody so the load and the save strip read decorations (_diagnostics, _draft) the same way; the load's behaviour is unchanged. PR fix(app-shell): list a package's draft objects in the Access matrix, badged as drafts #27064 (Studio Access: the permission matrix lists no objects while the package's objects are still drafts ("0 objects"), so a new package cannot be secured before its first publish #26840, queued) and PR fix(app-shell): an expression field shows the draft's server verdict at its own path #26999 (Studio: an expression field that says "Valid CEL" should read the draft's server verdict at its own path (RLS predicate, flow edge condition) #26858) edit the same file; seat trial merges of this head with each are clean.
  5. Out of scope: none found. Skills-docs: none — UI editor only.
  6. Landing: not Tier H; the seat readies and arms auto-merge.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 28, 2026 04:46
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 28, 2026 04:46
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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

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

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

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

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

    × keeps every key the editor does not author from the pending draft, byte-for-byte 1101ms
    × CONTROL — with no pending draft the base is still the published body 1031ms
    × REFUSES the save when the save-time draft read rejects, rather than fall back to the published body 1025ms
    FAIL   dom  packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.packageDoorDraftBase-26837.test.tsx > PermissionMatrixEditPage — the PACKAGE door saves onto the pending draft (objectsta
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    FAIL   dom  packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.packageDoorDraftBase-26837.test.tsx > PermissionMatrixEditPage — the PACKAGE door saves onto the pending draft (objectsta
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    FAIL   dom  packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.packageDoorDraftBase-26837.test.tsx > PermissionMatrixEditPage — the PACKAGE door saves onto the pending draft (objectsta
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    
  • Test UI (11/16) — 失败步骤: Run the ui tests (shard 11/16)

    × the package door draws the `*` entry too, and its save keeps the stored `*` entry 1045ms
    FAIL   dom  packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.objectGrantInherited-27010.test.tsx > every object-row control writes only what the admin sees change (objectstack#27010)
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    

↳ 失败原因 是判读的关键:超时(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 队列共有 20 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit dc87dfb Oct 28, 2026
59 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-5ielpf branch October 28, 2026 05:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants