Repository navigation
Commit 2351c8d
fix(plugin-approvals): an SLA reassign to a target that resolves to no one keeps the slate (#25600)
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuMdu6odAshaFZ1Ujr7q9LcW)_
Refs #25598 (the runtime leg; the registration-time leg is a
`packages/spec` change, left for the `domain:spec` lane — claim
6379436604, "Not this claim")
Clause-②: no
Metadata-protocol: none
## What changed
An approval node's `escalation: { action: 'reassign', escalateTo }`
expands `escalateTo` as a position. When nobody holds it, the sweep
falls back to the literal so that a config naming a user id keeps
working. Nothing judged that literal, so an unstaffed or mistyped
position, a user id with no account, or a `team:` literal moved the
overdue request off its real approvers and onto a slate nobody could
decide. That is the state #25592 closed for the reassign door.
The literal is now judged by the door's own predicate:
- **One predicate.** `assertReassignTargetResolves` (from #25592) is
split into `reassignTargetRefusal(to, org)`, which returns the refusal
sentence or `undefined`. The door's assert is now a thin throw over it,
and its text and behaviour are unchanged (the #25485 pins pass as
before). `escalateRequest` asks the same function. There is no second
predicate.
- **Only the literal fallback is judged.** When the position expansion
finds holders, those user ids go onto the slate as before.
| `escalateTo` with `action: 'reassign'` | Before | After |
|---|---|---|
| a position name with holders here | slate = holders | unchanged |
| a user id an account carries (member here, or no memberships) | slate
= id | unchanged |
| an email an account carries | slate = email | unchanged |
| `position:<p>` that someone here holds | slate = `position:<p>` |
unchanged |
| an unstaffed / mistyped name, a user id with no account | **slate =
the literal, request stranded** | slate kept, approvers notified |
| a user whose memberships are all in other organizations | **stranded**
| slate kept, approvers notified |
| `position:<p>` nobody holds here, a `team:` / `department:` /
`org_membership_level:` / `role:` literal | **stranded** | slate kept,
approvers notified |
- **What happens when the target resolves to no one.**
- `pending_approvers`, `pending_approver_labels` and the approver index
are left as they were.
- The sweep takes the `notify` branch: `approval.sla_breached` goes to
the current slate, and the unresolvable literal is not in the audience.
- No `approval.escalated` notice is sent.
- The `escalate` audit row reads `reassign → <target> — not applied:
<why> The pending approvers were kept and notified.` The row is still
the single-shot marker, so the escalation fires once.
- **The submitter notice names the action taken.** It used to report the
configured action (`escalation action 'reassign' was taken`) even when
no reassign happened. Now it reports `notify` in that case, and in the
existing case where no `escalateTo` was set.
- **A failed `sys_user` read** throws before the audit row. The sweep's
per-request isolation logs it and leaves the request untouched and
unmarked, and the next sweep retries it.
- **`action: 'notify'` is unchanged**, including its audience, which
still lists a literal `escalateTo`. So is every other escalation action.
- **No new error code or export, and nothing in `packages/spec` or
`packages/rest` changes.** Every `escalation` block that registers today
still registers.
## Tests
- `approval-service.test.ts` gains `SLA reassign target resolution
(#25598)`, 6 cases.
- Refusal cases. Each asserts the slate, labels and index unchanged, the
audit comment verbatim, the `sla_breached` audience (the current slate
only), no `approval.escalated`, the submitter notice saying `'notify'`,
and that a second sweep is a no-op. The cases are:
- a user id with no account;
- a position name held only in another organization;
- a user whose only membership is another organization;
- a `team:` literal and a `position:ghost` nobody holds.
- A failed `sys_user` read: nothing moves, no `escalate` row, no notice,
and the next sweep with the directory back escalates.
- Preservation: an account's user id, its email, and a held
`position:finance`. The slate becomes the target, the audit comment is
unchanged, and the submitter notice says `'reassign'`. The position's
holder then decides with the default actor.
- Two existing tests that escalated to `boss`, an id with no account,
now give it one (`staffDirectory`): `runEscalations: reassign replaces
the approver set with escalateTo` and `reassign and SLA-reassign rewrite
the index rows`.
- New
`packages/qa/dogfood/test/approval-sla-reassign-target-resolves.dogfood.test.ts`
runs on a booted stack with the REST door and the real ObjectQL. Two
approval flows reassign after one hour, one to a position nobody holds
and one to a member's email. Both requests are made overdue
(`created_at` moved back), and the sweep runs through the registered
`approvals` service.
- The first keeps its approver, who still finds it in "My Pending". Its
`escalate` row says why, and only `approval.sla_breached` is recorded
for it.
- The second moves to the member, and the member's "My Pending" lists
it.
## Verification
All on `4525d322`, whose tree is identical to the pushed head `d7a204a5`
(the amend reworded the commit message only). That commit is one commit
on `main` `a57643d3`, after #25592 merged.
- `pnpm --filter @objectstack/plugin-approvals exec vitest run`: 95
files, 1250 tests, all passed. `pnpm --filter
@objectstack/plugin-approvals typecheck` passed, `check:test-typecheck`
included (the ledger held at 324). `pnpm --filter @objectstack/dogfood
typecheck` passed. eslint on the 3 changed source and test files: clean.
- Ablation, run before the rebase with `approval-service.ts` restored
from #25592's head `00d5635a`:
- All 5 refusal and failed-read pins in the new block went red. The
preservation pin stayed green, as expected.
- After the plugin's `dist` was rebuilt, the dogfood pin went red on its
first assertion: `expected 'sla_reassign_nobody' to be
'sla-reassign-approver@example.com'`.
- After the restore, the tree matched HEAD and `git status --porcelain`
was empty.
- Local gates (`dispatch-gates --commands`, 76 families): all 76 exited
0.
- Full dogfood battery: 296 files, of which 292 passed, 1 failed and 3
were skipped. Tests: 2508 passed, 1 failed, 18 skipped.
- The new pin passed.
- The one failure is not this PR's:
`showcase-d3-d4-capabilities.dogfood.test.ts:135` (`[#9237] the grant
the rule reconciles…`) expects `/SHARING_NOT_ENABLED: 'showcase_project'
is not under record-sharing enforcement/`. `main` answers `…has a public
sharing model, so a share row on it is consulted only for an external
member of the record's organization…`, the text #25591 introduced, and
it reproduces with that file run alone.
- This diff touches no sharing code. It was reported to the lane for
routing.
## Acceptance notes
- The branch is the harness-assigned `claude/project-thread-h5z02d`,
restarted from `main` after #25592 merged. Claim 6379436604 on #25598 is
the identity record.
- `Refs`, not `Fixes`: the card's second leg is refusing a `team:` /
`department:` / `org_membership_level:` / `role:` `escalateTo` at
registration. Approval node config values are judged by the spec's
`ApprovalEscalationSchema` through `FlowSchema.parse`, the route #21848
took, so that leg is `packages/spec` work. The services seat left the
`domain:spec` seat a pointer on the card.
- Unchanged and noted: `notify` drops `type:value` audiences, so a
reassign to a held `position:<p>` literal announces itself to no one.
This is pre-existing and pinned as it is today.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01HopxxRtFE6z3z91wUD54tp)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01HopxxRtFE6z3z91wUD54tp
Co-authored-by: Claude <noreply@anthropic.com>1 parent f192123 commit 2351c8d
4 files changed
Lines changed: 392 additions & 23 deletions
File tree
- .changeset
- packages
- plugins/plugin-approvals/src
- qa/dogfood/test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1945 | 1945 | | |
1946 | 1946 | | |
1947 | 1947 | | |
1948 | | - | |
| 1948 | + | |
1949 | 1949 | | |
1950 | 1950 | | |
1951 | 1951 | | |
| |||
2084 | 2084 | | |
2085 | 2085 | | |
2086 | 2086 | | |
| 2087 | + | |
2087 | 2088 | | |
2088 | 2089 | | |
2089 | 2090 | | |
| |||
2132 | 2133 | | |
2133 | 2134 | | |
2134 | 2135 | | |
| 2136 | + | |
| 2137 | + | |
| 2138 | + | |
| 2139 | + | |
| 2140 | + | |
| 2141 | + | |
| 2142 | + | |
| 2143 | + | |
| 2144 | + | |
| 2145 | + | |
| 2146 | + | |
| 2147 | + | |
| 2148 | + | |
| 2149 | + | |
| 2150 | + | |
| 2151 | + | |
| 2152 | + | |
| 2153 | + | |
| 2154 | + | |
| 2155 | + | |
| 2156 | + | |
| 2157 | + | |
| 2158 | + | |
| 2159 | + | |
| 2160 | + | |
| 2161 | + | |
| 2162 | + | |
| 2163 | + | |
| 2164 | + | |
| 2165 | + | |
| 2166 | + | |
| 2167 | + | |
| 2168 | + | |
| 2169 | + | |
| 2170 | + | |
| 2171 | + | |
| 2172 | + | |
| 2173 | + | |
| 2174 | + | |
| 2175 | + | |
| 2176 | + | |
| 2177 | + | |
| 2178 | + | |
| 2179 | + | |
| 2180 | + | |
| 2181 | + | |
| 2182 | + | |
| 2183 | + | |
| 2184 | + | |
| 2185 | + | |
| 2186 | + | |
| 2187 | + | |
| 2188 | + | |
| 2189 | + | |
| 2190 | + | |
| 2191 | + | |
| 2192 | + | |
| 2193 | + | |
| 2194 | + | |
| 2195 | + | |
| 2196 | + | |
| 2197 | + | |
| 2198 | + | |
| 2199 | + | |
| 2200 | + | |
| 2201 | + | |
| 2202 | + | |
| 2203 | + | |
| 2204 | + | |
| 2205 | + | |
| 2206 | + | |
| 2207 | + | |
| 2208 | + | |
| 2209 | + | |
| 2210 | + | |
| 2211 | + | |
| 2212 | + | |
| 2213 | + | |
| 2214 | + | |
| 2215 | + | |
| 2216 | + | |
| 2217 | + | |
| 2218 | + | |
| 2219 | + | |
| 2220 | + | |
| 2221 | + | |
| 2222 | + | |
| 2223 | + | |
| 2224 | + | |
| 2225 | + | |
| 2226 | + | |
| 2227 | + | |
| 2228 | + | |
| 2229 | + | |
| 2230 | + | |
| 2231 | + | |
| 2232 | + | |
| 2233 | + | |
| 2234 | + | |
| 2235 | + | |
| 2236 | + | |
| 2237 | + | |
| 2238 | + | |
| 2239 | + | |
| 2240 | + | |
| 2241 | + | |
| 2242 | + | |
| 2243 | + | |
| 2244 | + | |
| 2245 | + | |
| 2246 | + | |
| 2247 | + | |
| 2248 | + | |
| 2249 | + | |
| 2250 | + | |
| 2251 | + | |
| 2252 | + | |
| 2253 | + | |
| 2254 | + | |
| 2255 | + | |
| 2256 | + | |
| 2257 | + | |
| 2258 | + | |
| 2259 | + | |
| 2260 | + | |
| 2261 | + | |
| 2262 | + | |
| 2263 | + | |
| 2264 | + | |
| 2265 | + | |
| 2266 | + | |
2135 | 2267 | | |
2136 | 2268 | | |
2137 | 2269 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5604 | 5604 | | |
5605 | 5605 | | |
5606 | 5606 | | |
5607 | | - | |
5608 | | - | |
5609 | | - | |
5610 | | - | |
5611 | | - | |
5612 | | - | |
5613 | | - | |
| 5607 | + | |
| 5608 | + | |
| 5609 | + | |
| 5610 | + | |
| 5611 | + | |
| 5612 | + | |
| 5613 | + | |
| 5614 | + | |
| 5615 | + | |
| 5616 | + | |
| 5617 | + | |
| 5618 | + | |
| 5619 | + | |
| 5620 | + | |
| 5621 | + | |
| 5622 | + | |
| 5623 | + | |
5614 | 5624 | | |
5615 | 5625 | | |
5616 | | - | |
5617 | | - | |
| 5626 | + | |
| 5627 | + | |
5618 | 5628 | | |
5619 | 5629 | | |
5620 | | - | |
| 5630 | + | |
5621 | 5631 | | |
| 5632 | + | |
5622 | 5633 | | |
5623 | 5634 | | |
5624 | 5635 | | |
5625 | 5636 | | |
5626 | 5637 | | |
5627 | | - | |
5628 | | - | |
| 5638 | + | |
| 5639 | + | |
5629 | 5640 | | |
5630 | 5641 | | |
5631 | 5642 | | |
5632 | 5643 | | |
5633 | 5644 | | |
5634 | | - | |
| 5645 | + | |
5635 | 5646 | | |
5636 | | - | |
5637 | | - | |
5638 | | - | |
| 5647 | + | |
5639 | 5648 | | |
5640 | 5649 | | |
5641 | 5650 | | |
| |||
7040 | 7049 | | |
7041 | 7050 | | |
7042 | 7051 | | |
| 7052 | + | |
7043 | 7053 | | |
7044 | 7054 | | |
7045 | 7055 | | |
7046 | 7056 | | |
7047 | | - | |
7048 | | - | |
| 7057 | + | |
| 7058 | + | |
| 7059 | + | |
| 7060 | + | |
| 7061 | + | |
| 7062 | + | |
| 7063 | + | |
| 7064 | + | |
| 7065 | + | |
| 7066 | + | |
| 7067 | + | |
| 7068 | + | |
| 7069 | + | |
| 7070 | + | |
| 7071 | + | |
7049 | 7072 | | |
7050 | 7073 | | |
7051 | 7074 | | |
| |||
7057 | 7080 | | |
7058 | 7081 | | |
7059 | 7082 | | |
7060 | | - | |
| 7083 | + | |
| 7084 | + | |
7061 | 7085 | | |
7062 | 7086 | | |
7063 | 7087 | | |
7064 | | - | |
| 7088 | + | |
7065 | 7089 | | |
7066 | 7090 | | |
7067 | 7091 | | |
| |||
7086 | 7110 | | |
7087 | 7111 | | |
7088 | 7112 | | |
7089 | | - | |
| 7113 | + | |
| 7114 | + | |
7090 | 7115 | | |
7091 | 7116 | | |
7092 | 7117 | | |
| |||
7106 | 7131 | | |
7107 | 7132 | | |
7108 | 7133 | | |
7109 | | - | |
| 7134 | + | |
7110 | 7135 | | |
7111 | 7136 | | |
7112 | 7137 | | |
| |||
0 commit comments