Repository navigation
fix(plugin-approvals): a user approver naming no account expands to nobody and takes onEmptyApprovers - #27749
Conversation
…obody A user approver's id is now checked against the directory with the one predicate the reassign door already asks. One that names no account (or names a user provably outside the organization) takes the graph types' expanded-to-nobody warning and adds no slot, so a slate it leaves empty reaches the node's onEmptyApprovers (default admin_rescue). Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
…nts their user approvers name Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
…counts that never act Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
… user it routes to Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
…e test-layer ledger shrinks by two Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
…er-approver-unresolvable
Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3a4fb1f926754bf697d9493002c529ce2aabb8da && git checkout 3a4fb1f926754bf697d9493002c529ce2aabb8da
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 81d00a4ed938eb896859630498fc59d91183cf71 10a61cc4b370976aad5fce482d7873eeb8035a24 && git checkout -B drift-repro 81d00a4ed938eb896859630498fc59d91183cf71 && git merge --no-ff 10a61cc4b370976aad5fce482d7873eeb8035a24
node scripts/docs-audit/affected-docs.mjs --json 81d00a4ed938eb896859630498fc59d91183cf71
|
|
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F 维护者速读(席位终稿)
席位意见: ACCEPT。一个谓词、一条警告、复用已有的空名单路径; 席位核对ACCEPT · head
|
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
Requested by Sales · project thread
Fixes #27397
Clause-②: no
Metadata-protocol: none
Skills-docs: none — no
skills/**text describes auserapprover's resolution; the one docs page that did (content/docs/automation/approvals.mdx, two sentences that this change makes false) is corrected here维护者速读
user类型审批人现在会先查目录(sys_user,按 id 或邮箱)。查不到这个账号时,它和部门、团队、岗位这些图类型一样:打同一条expanded to nobody警告,不占槽位。名单因此变空时,走节点的onEmptyApprovers,默认是admin_rescue。sys_user读成功、而且确实没有这一行时才丢槽位:读失败时保留原槽位;被封禁的账号、别的组织目录里的人、以及 OOO 委托到真实代理人的情况都不变。回滚就是 revert 这个 PR。packages/plugins/plugin-approvals/src/approval-service.ts的resolveApproverSpec里type === 'user'分支和共用的警告分支;admin-rescue-named-slate.integration.test.ts新增的 describe;approval-service.test.ts的ROUTED_USERS与putAccounts(夹具改动)。pnpm --filter @objectstack/plugin-approvals exec vitest run src/admin-rescue-named-slate.integration.test.ts,预期 22/22 通过,其中包含 "auserapprover naming no user expands to nobody" 这一组。What changes
userapprover branch inresolveApproverSpec. Auservalue is now asked of the directory through the predicate the reassign door already uses (reassignTargetRefusal). That predicate looks for asys_userrow carrying the id, or the email for an email-shaped value. It is asked with no organization, so its tenancy screen does not run:useris directory-less, and ADR-0105 D9 leaves it untouched (the existingapprover-cross-orgpin holds that). When no account carries the value, the branch falls through to the graph types' existing warning (same text, now reached forusertoo) and returns no slot. A slate it leaves empty reaches the node'sonEmptyApproversby the existing path; there is no new branch and no new warning text.sys_userread that returns no row. These cases still keep the slot:expandTeamUsersandexpandBusinessUnitUsers.TYPE:VALUEliteral.slateNamesAPersonalready reads that as naming no one.sys_userid ("… while u_ooo is out of office", "… was routed to u_bob …") — the person half of the approval-notification wording #24065 notification-body pin).content/docs/automation/approvals.mdxsaid that auserentry never resolves to nobody. Two sentences are corrected.sys_user, so everyuserapprover in them was a phantom by the new rule. They now start with the accounts each suite routes to (ROUTED_USERS, plusputAccountswhere a test writes a named row). Three dogfood fixtures used an address deliberately carried by no account:CO_APPROVER(two of them) andDEPARTED_APPROVER. Each pin now signs that address up as an account that never acts, so the scene each pin measures is unchanged. The test-typecheck ledger shrinks by two, because a replaced test took two pre-existing errors with it.Tests
The PIN is through the real
ApprovalService.openNodeRequeston a real ObjectQL plus SqlDriver, with real administrators. It is a new describe inadmin-rescue-named-slate.integration.test.ts:userapprover namingusr_does_not_existtakes the defaultadmin_rescueroute. The request opens onu_adminandu_owner, and they are told.approver 'user:usr_does_not_exist' expanded to nobodyis logged once, and the routing line is logged once.pending_approversholds nousr_does_not_exist, andadmin_rescue:abandonedis[].[u_alice, usr_does_not_exist]opens on['u_alice']alone, with the phantom's warning logged once and no administrator routing.The unit pins are in
approval-service.test.ts. The old "stays quiet foruser" test, which pinned the phantom slot, is replaced by three tests:sys_userread keeps the slot.Red first, then green
Before the fix, the new describe at base
a75ddba1gave 4 failed and 17 passed (21):expected [ 'usr_does_not_exist' ] to deeply equal [ 'u_admin', 'u_owner' ]·expected [ 'u_alice', 'usr_does_not_exist' ] to deeply equal [ 'u_alice' ]· the email leg · (and the other-org leg, which was later turned into the D9 control).At final head
10a61cc4, with the closure rebuilt after theorigin/mainmerge, the run under the verify lock gave:pnpm --filter @objectstack/plugin-approvals exec vitest run: Test Files 105 passed (105), Tests 1338 passed (1338).pnpm --filter @objectstack/plugin-approvals typecheck: exit 0,check:test-typecheck: OK(8 files / 321 errors / 27 pinned signatures).pnpm --filter @objectstack/http-conformance exec vitest run src/hono-approvals-act.conformance.test.ts: 4/4. It covers an unregisteredsys_user: the read throws and the slot is kept.Ablations (scripts/ablation-replace.mjs; the suite reads src, so no dist leg)
if (refusal === undefined) return routed;was replaced withreturn routed;, which removes the existence check. Result: 4 failed and 405 passed (409). The failures were the three integration legs and the unit warning pin, for exampleexpected [ 'usr_does_not_exist' ] to deeply equal [ 'u_admin', 'u_owner' ]. The controls stayed green. The restore was checked: blobae44df7ffede== HEAD, andgit diff HEADwas empty.if (GRAPH_APPROVER_TYPES.has(type) || type === 'user') {was replaced withif (GRAPH_APPROVER_TYPES.has(type)) {, which removes the shared warning. Result: 3 failed and 406 passed (409):expected [] to have a length of 1 but got +0(twice) andno warning for user: expected undefined to be truthy. The restore was checked: blob == HEAD, andgit diff HEADwas empty.Acceptance notes
type: 'field'has the same unchecked shape. A record field holding a user id that no account carries opens a phantom slot in the same way (resolveApproverSpec, thefieldbranch,csvSplit(record[value])straight toapplyOooDelegation). Not filed by this PR; it is named in the report for the seat.delegate_idis not existence-checked either. An OOO hop to a removed account would open a phantom slot the same way. Noted, not filed.examples/app-showcaseapprover-bindings.flow.tscarries{ type: 'user', value: '' }on anobsoleteflow. If that flow ever runs, it now logsapprover 'user:' expanded to nobody, where before it was silent. The empty slot was already dropped before this change.if:excludes them), so the dogfood fixture edits are verified by the local run below, not by CI on this PR.Verification
Gates on `10a61cc4`
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 94 command(s) on10a61cc4. I ran every one at that head and captured each exit code before any pipe: 94/94 exit 0.dispatch-gates --ran:Run reconciliation — 94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN·a DERIVED zero — all 94 recorded an exit code and none of them is 3.I also ran these locally, outside the derived set:
--no-inline-configoverpackages/plugins/plugin-approvalsand the six dogfood files: exit 0.userapprovers (override-composite, my-pending-position, position-address-readers, admin-rescue-slate, notification-body, pending-approver-record-read, record-lock-read, sla-reassign-target-resolves, seed-ownership-claim-dispatch). Run under the verify lock after building the dogfood closure: Test Files 9 passed (9), Tests 9 passed (9).merge-treeagainst the three open PRs that touchpackages/qa/dogfood/test/(#27713, #27667, #27649) shows no conflict on any file this PR changes.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F