Skip to content

fix(core): lower modifyAllRecords in the managed write clamp where the write guard refuses edit or delete - #30474

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-1833wl
Nov 5, 2026
Merged

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

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#3 · os-bill · session_01Utp6CZUqByp4kJMhCQDWZ2

Requested by Bill · project thread
Fixes #30440

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:受管对象(better-auth / engine-owned / append-only)的权限视图里,显式的「修改全部记录」在写守卫拒绝编辑或删除时被降为「查看全部记录」,并保留对象仍开放的动作;未开放编辑的对象上「转移」也一并清掉(超级管理员之前在这些对象上被告知可以转移)。
  • 为什么改:之前只要权限集写了 modifyAllRecords,current_user.can() 和控制台按钮就说可以编辑、删除、转移,但写守卫实际一律拒绝。
  • 风险与回滚:只改服务端返回的能力答案,不改任何写入的放行与拒绝;回滚即还原本 PR 的单个函数。
  • 先看:packages/core/src/security/effective-object-permissions.ts 的 clampManagedObjectWrites 与新 helper lowerModifyAllRecords;packages/plugins/plugin-security/src/get-effective-object-permissions.test.ts 一致性表的 CLAMPED 与新行;测试块 [#30440]。
  • 两分钟验证:pnpm --filter @objectstack/core exec vitest run src/security/effective-object-permissions.test.ts,预期 55 个全绿。
  • 需要你做的:无,席位落地。

What changes

clampManagedObjectWrites (packages/core/src/security/effective-object-permissions.ts) cleared allowEdit / allowCreate / allowDelete on an object in a guarded managedBy bucket that does not opt the verb in through userActions, but never touched modifyAllRecords, which the spec's objectPermissionGrants reads as edit, delete and transfer on its own. A permission set naming such an object with { modifyAllRecords: true } therefore answered current_user.can(object, 'edit' | 'delete' | 'transfer') true while the write guard refused every one of those writes.

  • New private helper lowerModifyAllRecords(acc, kept): when the entry carries modifyAllRecords: true and a layer refuses at least one of edit / delete / transfer, the bit becomes false, viewAllRecords becomes true (the read it implies, so read is unchanged), and each verb kept names is carried over as its plain allow* bit.
  • clampManagedObjectWrites calls it with { allowEdit: edit, allowDelete: delete, allowTransfer: edit } from the object's userActions opt-ins.
  • Claim amendment 6658639729 (seat): clampManagedObjectWrites also clears a plain allowTransfer on a guarded object that does not opt edit in. The super-user wildcard fold copies the transfer bit modifyAllRecords grants onto every entry it covers, so the shipped platform admin was served can(object, 'transfer') true on every such object while the guard refused the update. Measured on c5800818: an engine-owned object opting nothing in, wildcard { allowRead, viewAllRecords, modifyAllRecords }, map entry allowTransfer: true, and assertEngineOwnedWriteAllowed(…, 'update', user) throws PermissionDeniedError. After the fix the same probe answers transfer false.
  • Same amendment, the agreement table in packages/plugins/plugin-security/src/get-effective-object-permissions.test.ts (can() over the served map vs PermissionEvaluator.checkObjectPermission, which does not model the guard): its CLAMPED set, the cells it forgives in the refuse direction on a guarded object, gains allowTransfer, and it gains a row [#30440] explicit modifyAllRecords entries on guarded managed objects (sys_user opting edit in, sys_member opting nothing in, private sys_secret).
  • clampExternalReadOnlyWrites (Effective object permissions ignore the external write gate, so the console offers New, Import, Edit and Delete on a read-only external object #30370, PR fix(core,objectql): effective object permissions read the external write gate, so a read-only external object is no longer offered New, Import, Edit or Delete #30416) now goes through the same helper with nothing kept; its behaviour is byte-identical (its existing tests are unchanged and green).

The partial opt-in choice (dispatch scope item 2). objectPermissionGrants answers edit as read && (allowEdit || modifyAll), delete as read && (allowDelete || modifyAll) and transfer as allowTransfer || modifyAll, so a false beside modifyAllRecords takes nothing away: an entry cannot keep modifyAllRecords and refuse only delete. So:

object opts in modifyAllRecords served can()
nothing (sys_automation_run, http_delivery) lowered to viewAllRecords read only
edit only (sys_user, sys_organization, sys_api_key) lowered; allowEdit + allowTransfer kept read, edit, transfer; no delete
edit and delete kept unchanged

Transfer follows edit because an owner change is an update: the engine-owned guard maps it to the edit affordance, and #30416's external clamp already treats it the same way. What is lost when the bit is lowered on an edit-only object is record-level "all records" scope for edit, which only the server evaluator reads (unchanged here); the served map's consumers read the object-level verbs (current_user.can, MePermissionsProvider's transfer check), and those keep edit and transfer.

Tests

New block [#30440] a guarded managed object grants no verb its write guard refuses, modifyAllRecords included in packages/core/src/security/effective-object-permissions.test.ts (the existing harness file for this map), through buildEffectiveObjectPermissions and the spec's real objectPermissionGrants:

  • explicit { modifyAllRecords: true } on an engine-owned object opting nothing in: no edit / delete / transfer, same read;
  • the same on a sys_user-shaped object (edit opted in): edit and transfer kept, delete gone;
  • an object opting edit and delete in keeps modifyAllRecords;
  • CONTROL: the same entry on an unguarded object keeps every verb modifyAllRecords grants;
  • a plain allowTransfer on an object not opting edit in answers no transfer; on sys_user (edit opted in) it stays;
  • CONTROL: the super-user wildcard fold answers exactly as on main on every object except the folded transfer bit on the object opting nothing in, which now reads false (the fold copies granted bits, never modifyAllRecords).

plugin-security's agreement table: one new row (above) and allowTransfer in CLAMPED; 68/68.

Acceptance notes

  • The allowTransfer half was first handed back as out of scope (first commit 47fe789a cleared it and red 13 rows of the agreement table; 5dd16ba3 took it out); the seat chose to take it here and amended the claim (6658639729); 04b5cbbe puts it back with the table change.
  • Observation, not filed: on the same measurement apiOperations for the engine-owned object lists create / update / delete. That annotation reads enable.apiMethods exposure, a different layer from the guard; noted only.

Verification

Measured on main first (c580081), probe deleted after use
PROBE map probe_engine_owned = {"modifyAllRecords":true,"allowEdit":false,"allowCreate":false,"allowDelete":false,…} · can: allowRead=true allowCreate=false allowEdit=true allowDelete=true allowTransfer=true
PROBE map sys_user_like = {"modifyAllRecords":true,"allowCreate":false,"allowDelete":false,…} · can: allowRead=true allowCreate=false allowEdit=true allowDelete=true allowTransfer=true
PROBE guard probe_engine_owned update: REFUSED PermissionDeniedError [Security] Access denied: 'probe_engine_owned' is engine-owned (managedBy:'engine-owned', …
PROBE guard probe_engine_owned delete: REFUSED PermissionDeniedError …

The same probe after the fix: probe_engine_owned can edit/delete/transfer = false/false/false, read true; sys_user_like edit/delete/transfer = true/false/true; the unguarded control unchanged.

Tests and ablation
  • pnpm --filter @objectstack/core test: Test Files 134 passed, Tests 3001 passed (lock VERDICT command-exit 0); pnpm --filter @objectstack/plugin-security typecheck exit 0.
  • pnpm --filter @objectstack/core typecheck: exit 0, test layer OK.
  • Downstream readers of the map, run against the rebuilt core: plugin-hono-server current-user-endpoints-effective-objects.test.ts + fold-wildcard-superuser.test.ts 20/20; plugin-security get-effective-object-permissions.test.ts 68/68.
  • Ablation (scripts/ablation-replace.mjs --delete of the lowerModifyAllRecords(…) call in clampManagedObjectWrites; anchor 1 → 0 on disk; the test imports the source module relatively, so no dist/ leg): on 5dd16ba3, 2 failed / 52 passed, the two explicit-modifyAllRecords cases; restored blob equals HEAD, git diff HEAD empty.
  • Ablation 2 on 04b5cbbe (--delete of if (!edit) acc.allowTransfer = false;, core rebuilt, ablation-dist-preflight --absent confirmed the marker gone from all 14 dist files): core file 2 failed / 53 passed (the plain-transfer case and the wildcard CONTROL); the agreement table stays 68/68, as expected: without the clear the map is broader, the evaluator agrees, and the table only flags the refuse direction. Restored blob equals HEAD; core rebuilt, preflight finds the marker back in dist/index.js and dist/index.cjs.
  • Ablation 3 on 04b5cbbe (the table's CLAMPED back to create / edit / delete): 14 failed / 54 passed, the 13 super-user rows on sys_member.transfer / sys_secret.transfer plus the new [#30440] row; restored blob equals HEAD.
Derived gate families on head 04b5cbb: 62 derived, 62 run exit 0

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 62 commands from the 4 changed paths (146 changed lines); each was run on 04b5cbbe and its exit code recorded before any pipe; --ran reconciles: "62 derived famil(ies) accounted for, 62 run, 0 NOT-MEASURED". The amendment's test file added check-tenant-audit-census (+ --self-test), check:i18n and check:i18n-stale-fill.

  • pnpm check:dual-build-cjs-loads (NOT MEASURED on 5dd16ba3) now exit 0: 116 require entry points across 71 packages load.
  • node scripts/check-plugin-teardown-shape.mjs --self-test exit 0 (48 cases) after git fetch origin 621a487607881c66b2899b7e3477115229a156b4 on the shallow clone.
  • Also run on 5dd16ba3: pnpm --filter @objectstack/spec check:liveness exit 0 (no spec path in the diff).
  • Not in the local set, left to CI: the type-check lanes, Build Core, Test Core shards, and the repo-wide scans.

Generated by Claude Code

… transfer where the guard refuses the update

clampManagedObjectWrites cleared allowEdit/allowCreate/allowDelete on a
guarded managed object that does not opt the verb in, but left
modifyAllRecords (which grants edit, delete and transfer on its own) and
allowTransfer (an owner change is an update). It now lowers
modifyAllRecords to viewAllRecords plus the plain bit of each verb the
object still opts in, through one helper shared with the external write
clamp, and clears allowTransfer unless edit is opted in.

Claude-Session: https://claude.ai/code/session_01Utp6CZUqByp4kJMhCQDWZ2
Co-authored-by: Claude <[email protected]>
…allowTransfer as it was

Clearing a plain allowTransfer on every guarded object also moved the
super-user wildcard admin's transfer answer, which the plugin-security
parity table (map vs evaluator) does not forgive. That half goes back to
the seat; this change keeps to the card: modifyAllRecords is lowered
where the guard refuses edit or delete.

Claude-Session: https://claude.ai/code/session_01Utp6CZUqByp4kJMhCQDWZ2
Co-authored-by: Claude <[email protected]>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 39 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 f506b79d85790a8f74a762ac4031090c7fbdae25 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from e555f95a7013762fa78739f3ef4d0e95db82c8fa — the merge of head 04b5cbbebd76fae78aeebe51681fe262654477be into base f506b79d85790a8f74a762ac4031090c7fbdae25, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e555f95a7013762fa78739f3ef4d0e95db82c8fa && git checkout e555f95a7013762fa78739f3ef4d0e95db82c8fa
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f506b79d85790a8f74a762ac4031090c7fbdae25 04b5cbbebd76fae78aeebe51681fe262654477be && git checkout -B drift-repro f506b79d85790a8f74a762ac4031090c7fbdae25 && git merge --no-ff 04b5cbbebd76fae78aeebe51681fe262654477be

node scripts/docs-audit/affected-docs.mjs --json f506b79d85790a8f74a762ac4031090c7fbdae25

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

…the guard refuses the update

The super-user wildcard fold copies the transfer bit onto every guarded
object it covers, so the platform admin was served can(object, 'transfer')
true where the guard maps transfer to edit and refuses it. The clamp now
clears allowTransfer unless edit is opted in. The plugin-security
agreement table forgives transfer as one of the clamp's own verbs, and
gains a row with explicit modifyAllRecords entries on guarded objects.

Claude-Session: https://claude.ai/code/session_01Utp6CZUqByp4kJMhCQDWZ2
Co-authored-by: Claude <[email protected]>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 04b5cbbebd76fae78aeebe51681fe262654477be
Local-runs: none
Scope: packages/core/src/security/effective-object-permissions.ts, packages/core/src/security/effective-object-permissions.test.ts, packages/plugins/plugin-security/src/get-effective-object-permissions.test.ts, .changeset/30440-managed-clamp-modify-all.md

Face: declaration (Clause-②: yes (widening) line-initial on the PR body and the changeset). No published schema, no governed path, no narrowing of an authorable surface, so the lane seat records this review at TIER_DEFAULT. Reads: the full diff (4 files, +137 −9), card #30440 and its triage 6657488620, claim 6657717135 and amendment 6658639729, the dev's measurement on c5800818.

① Derived judgments

  1. The clamp. clampManagedObjectWrites now also clears allowTransfer where edit is not opted in, and calls the new private lowerModifyAllRecords(acc, { allowEdit: edit, allowDelete: del, allowTransfer: edit }). Because objectPermissionGrants answers edit / delete / transfer on modifyAllRecords alone, lowering the bit is the only way a refused verb stops being granted; lowering to viewAllRecords keeps the read it implied, and the kept verbs come back as plain bits. An object opting both edit and delete in keeps modifyAllRecords (it grants nothing the guard refuses). Judged right.
  2. The external clamp. clampExternalReadOnlyWrites (fix(core,objectql): effective object permissions read the external write gate, so a read-only external object is no longer offered New, Import, Edit or Delete #30416) now calls the same helper with nothing kept; byte-for-byte the old branch (modifyAllRecords → false, viewAllRecords → true), its tests unchanged and green.
  3. Direction. The served map grants less only where the write guard already refused every caller, so no caller loses a write it could make; the change is the capability answer coming to match enforcement (same reading as fix(core,objectql): effective object permissions read the external write gate, so a read-only external object is no longer offered New, Import, Edit or Delete #30416). No new export: the helper is module-private.

② Semver level

@objectstack/core patch, Metadata-protocol: none: a non-protocol change ships patch (AGENTS.md step 3). The declared arm yes (widening) over-declares: the diff adds no public member and narrows no accept set, so the measured line is Clause-②: no. The over-declaration costs only this review and changes no level or banner; not a REWORK.

③ Boundary flags

  • File surface: the plugin-security agreement table is inside the amended claim (6658639729); the allowTransfer half rides the same defect class, mechanical, same gate family, named in the PR body with its measurement.
  • Refusal-shaped assertions: the map tests assert the served bits per verb on the explicit-modifyAllRecords cases and both CONTROLs; the write guard's own refusal is unchanged and not re-asserted here. Three ablations recorded, each red then restored.
  • Dev observation (not filed): apiOperations for an engine-owned object lists write operations; that reads enable.apiMethods exposure, a different layer from the guard. Noted only.

Implemented-by: claude/project-thread-1833wl
Reviewed-by: session_01Utp6CbPxYq1wQmUNWvMZkC

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC

维护者速读(席位终稿)

  • 改了什么:受管对象(better-auth / engine-owned / append-only)在权限视图里,显式的 modifyAllRecords 在写守卫拒绝编辑或删除时降为 viewAllRecords,并保留对象仍开放的动作;未开放编辑的对象上 allowTransfer 也一并清掉。
  • 为什么改:之前权限集写了 modifyAllRecords(或超级管理员通配折叠带来的转移位),current_user.can() 和控制台就说能编辑、删除、转移,写守卫却一律拒绝(Effective object permissions: the managed write clamp leaves an explicit modifyAllRecords on a guarded managed object, so current_user.can(object, 'edit') answers true where the write guard refuses #30440)。
  • 风险与回滚:只改服务端返回的能力答案,写入的放行与拒绝一处不动。回滚即 revert。
  • 先看:packages/core/src/security/effective-object-permissions.ts 的 clampManagedObjectWrites 与 lowerModifyAllRecords;plugin-security 一致性表的 CLAMPED 与新行。
  • 两分钟验证:pnpm --filter @objectstack/core exec vitest run src/security/effective-object-permissions.test.ts,全绿。
  • 需要你做的:无,席位落地(非 BREAKING,非受管面);席位复核记录 6659493860(Head-sha 04b5cbbebd76fae78aeebe51681fe262654477be,PASS)。

席位意见:同意落地。能力答案与写守卫对齐,和 #30416 同一做法、共用一个 helper。

席位核对

ACCEPT · head 04b5cbbebd76fae78aeebe51681fe262654477be · card #30440 (claim 6657717135, amended 6658639729) · 2026-11-05T13:42Z

  • Shape: draft PR to main, relay author, attribution block then Fixes #30440; no other closing keyword. 4 files, +137 −9. check-governed-merges.mjs --pr 30474 → exit 0, 0 governed, ≤ 3000. No merge=os-regen path. PR body revised by the seat via the relay from the dev's draft (read back identical).
  • Premise measured: on main c5800818 an explicit { modifyAllRecords: true } on an engine-owned object answered can edit / delete / transfer true while assertEngineOwnedWriteAllowed refused update and delete; after: false/false/false, sys_user-shaped true/false/true.
  • Gates (check-runs on this head): 67 runs, 56 success, 11 path-gated skipped, 0 failed; Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard, Check Changeset all success. Derived: 62 families, 62 exit 0, 0 NOT MEASURED.
  • Accepted deviation: surface widened by the seat to the plugin-security agreement table (amendment 6658639729); Clause-②: yes (widening) over-declares (measured no, review ②), no effect on level.
  • Out of scope: the apiOperations observation stays an Acceptance note (different layer), not filed.
  • Landing: non-BREAKING, not governed ⇒ seat lands it now (p0 fix(cloud-connection): stage the ledger adoption copy without mkdtempSync, so main's dispatch-gates lane is green again #30487 queued ahead).

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 13:44
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 5, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC

Queue kick, re-queued once · 2026-11-05T14:29Z

  • Failing check: Test UI (13/16) in merge-group CI run 42041889542 (job 127296571152). One test failed: packages/app-shell/src/console/ai/__tests__/AiChatPage.previewDraft-12968.test.tsx › "a page draft → that page: the Live Canvas opens at the draftPreviewPath target, in draft preview" (Unable to find an element by: [data-testid="live-canvas"], 4063 ms). The other 414 files in the shard passed.
  • Why not this PR: this diff changes packages/core/src/security/effective-object-permissions.ts and a plugin-security agreement table. A jsdom test of the AI chat page's draft-preview route does not import either.
  • Action: re-queued once with the same head 04b5cbbe and the same ACCEPT 6659517519. If this test fails a second time, the seat treats it as real.

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

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

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

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

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

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

    × a page draft → that page: the Live Canvas opens at the draftPreviewPath target, in draft preview 4063ms
    FAIL   dom  packages/app-shell/src/console/ai/__tests__/AiChatPage.previewDraft-12968.test.tsx > AiChatPage — the any-draft Preview opens the drafted surface (objectui#12968) > a page draft → that pag
      ↳ 失败原因: (这条 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,按失败测试文件聚合):

  • packages/app-shell/src/console/ai/__tests__/AiChatPage.previewDraft-12968.test.tsx — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 16 个失败构建(不含本次)。

分诊清单:

  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 d5f3bc5 Nov 5, 2026
68 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-1833wl branch November 5, 2026 15:54
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 Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant