You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit a6d0715
Browse filesBrowse the repository at this point in the historyBrowse files
fix(plugin-approvals): approval.requested names the record by label and title; the empty-slot warning states the slot, not the request's fate (#23592)
Part of #23556
Clause-②: no
Both texts the card names are server-authored, and both now say what is
true. Scope is the card's two "expected" lines, per triage.
## 1. `approval.requested` names the record by its object label and id
**Before:** `A decision on crm_opportunity/JCqLLy5dSug1MldB is waiting
on you.` **After:** `A decision on Opportunity JCqLLy5dSug1MldB is
waiting on you.` The object's API name appears only when its schema
declares no label, as in the inbox.
The body carries no title, from any source: not the live record's and
not the submit snapshot's. It stays that way until #23594 narrows the
inbox's subject-title read (`resolveSubjectDisplays`, the one fix point)
by each reader's field-level security, because notification text is
stored and may be e-mailed.
`nameSubjectRecord` reads only the label, through `objectLabelOf`, which
`resolveSubjectDisplays` also uses; `enrichRows` behaves exactly as
before.
## 2. The per-slot warning states the slot, not the request's fate
**Before:** `approver 'position:sales_manager' expanded to nobody — the
slot routes to no one and the request cannot advance until someone is
added or the approver is re-pointed`, followed at once by the node-level
line that opens the request on the organization's administrators.
**After:** `approver 'position:sales_manager' expanded to nobody — the
slot names no one, so no one is asked through it. Staff it or re-point
the approver; a slate left naming no one is decided by the node's
onEmptyApprovers.`
This is the card's second shape: the consequence is left to the
node-level line. The resolver sees one approver entry, not the slate.
Whether the request opens on the slate's other people, or the node's
`onEmptyApprovers` decides an empty slate, is known only to
`openNodeRequest`, which already logs that decision on every branch:
`admin_rescue` with and without administrators, `fallback`,
`auto_approve`, and `fail`, which throws. So the routing is stated once,
by that line. The old ending was false on the default path. It was also
false on a mixed `first_response` slate, where the request opens on the
other people.
## Premise check, measured on `origin/main` at `fa6a6085c0`
- Both producer lines were where the card says
(`approval-service.ts:3598` and `:2216`-`:2219`). Each was pinned by the
test the card names.
- The dispatch assumed the list resolves `record_title` through a lookup
that honours the target's declared exposure. **It does not.** The
exposure check from `896a4342e6` (`servesPayloadDisplayTarget`) applies
only to lookup targets named inside the payload snapshot
(`payload_display`). The subject record's own title is read under the
system context, with no exposure check and no field-level narrowing. The
body matches that resolution exactly, so a recipient is shown nothing
their own inbox row does not already show them. The gap in the list
itself is in Acceptance notes.
## Pins (moved, not added beside)
- `approval-requested-notification.integration.test.ts` (real `ObjectQL`
over `SqlDriver`):
- the moved body pin now reads `A decision on Deal "ladder" is waiting
on you.`;
- a parity check: the body equals the recipient's own `listRequests`
row's `object_label` and `record_title`;
- a new negative case: a live record with no title, whose submit
snapshot still carries one, is named `Deal D_untitled` and the snapshot
title is absent.
- `approval-service.test.ts` (the moved WARN pin, four graph types): the
line names the slot and `onEmptyApprovers`, and does not match `cannot
advance`.
- `admin-rescue-named-slate.integration.test.ts`: the card's acceptance
line at its real scene, an `admin_rescue` node whose organization has
two administrators. The per-slot line and the routing line each fire
once, and no line matches `cannot advance`. This file is outside the
claim's declared file surface. It is the only fixture with an
organization administrator, and building one elsewhere would have
duplicated it.
## Ablations (one-time, run at `452d891d16`, restore proven by
`scripts/ablation-replace.mjs`: blob equal to HEAD, `git diff HEAD`
empty)
| ablation | expected | observed |
|---|---|---|
| the old WARN text put back | red on the WARN pins | 5 failed / 371
passed: the 4 graph-type cells, and the admin_rescue case at its `cannot
advance` assertion, with both scene counts green |
| the body built by running `enrichRows` over the unredacted row (the
tempting shortcut) | the negative snapshot case red, the parity case
green | 1 failed / 9 passed: the body read `Deal "Snapshot-only title"`
|
## Tests and gates, final head `8e5919cb46`
- `pnpm --filter @objectstack/plugin-approvals exec vitest run
--maxWorkers=2`: 74 files, 1054 tests passed.
- `pnpm --filter @objectstack/plugin-approvals typecheck`: exit 0, test
layer included (`check:test-typecheck: OK`, debt held).
- Derived local families: `dispatch-gates --commands` named 67 for this
change set; all 67 ran with exit 0. `--ran` reconciliation: 67 derived,
67 run, 0 NOT-MEASURED, 0 UNRUN. On the first pass,
`check:dual-build-cjs-loads` and `check:i18n` answered 3 (PREREQUISITE
NOT MET, no dist); after a workspace build both answered 0.
- `origin/main` was merged once before opening, at `c980d114d3`. It
brought no change under `plugin-approvals`. The approvals closure was
rebuilt, `spec check:generated` passed, and the suite and gates above
were rerun after it.
- The full farm (Test Core, Build Core, dogfood, type-check lanes, the
artifact-roster families) is CI's, through this PR's check-runs.
## Changeset
`.changeset/23556-approval-request-names-record.md`, `patch` on
`@objectstack/plugin-approvals`. Both are server-authored texts in a
released package. No schema, translation key, export or notification
field moves.
## Acceptance notes
- **Measured, out of scope here, handed to the seat:** the inbox's
`record_title`, and with it the 920bc12 toasts, the one-tap action page,
and now this body, can carry a field the reader's field-level security
hides. Probe: an approver whose readable and queryable set is `id,
amount` gets the payload served without `title`. The same approver's
`getRequest` and `listRequests` rows still carry `record_title:
"Synthetic secret title"`, because the subject read runs under the
system context. The remedy belongs in `resolveSubjectDisplays`, the one
function both readers now share; the notification would then pass each
recipient. The stake for this PR is that notification text is stored, so
titles already sent cannot be withdrawn.
- **Same family, not changed here (scope):** nine sibling approval
notification bodies still name the record as `object/id`:
`approval.ooo_substituted`, `approval.ooo_skipped`, the reassignment,
both reminder bodies, the escalation pair, the escalation-action line
and the revision-limit rejection. Each is now a one-line change through
`nameSubjectRecord`.
- `#22824` is the routing this text now defers to. It is not changed
here.
## Seat re-scope note (seat's append, patch round 2, head `7c8b072027`)
Appended by `domain:services` seat 1
(`session_017WLmd5in9RXu7AjyHR8fKQ`); the dev writes this body once and
never PATCHes it, per the dev contract. Seat decision `6183426689` on
#23556: the notification carries no title until #23594 lands, so this PR
is `Part of #23556`, and #23556 keeps the title half (`Blocked-by:
#23594`). Section 1 above is replaced with the dev's round-2 text
(report `6183760676`). Where the sections below still describe round 1,
round 2 supersedes them:
- **Premise check:** the last two sentences ("The body matches that
resolution exactly…") describe round 1. The body no longer reads the
subject record at all.
- **Pins:** the moved body pin reads `A decision on Deal D_ladder is
waiting on you.`, and the parity check against `record_title` is
dropped. The negative case is "[#23594] a record whose title resolves is
still named by label and id". There the live title and the snapshot
title differ, and both are asserted absent; the inbox resolving the live
title is the scene control.
- **Ablations:** row 2 is superseded by round 2's ablation, run at
`7c8b072027`. Letting the title through gives 2 failed / 8 passed, and
the restore is proven.
- **Acceptance notes:** in the first bullet, "and now this body" no
longer holds.
- **Tests and gates:** the final head is `7c8b072027`, with the same
counts: 74 files / 1054 tests, and 67 of 67 gates.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017WLmd5in9RXu7AjyHR8fKQ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(plugin-approvals): the `approval.requested` notification names the record by its object label and id, and the empty-slot warning no longer says the request "cannot advance"
6
+
7
+
Clause-②: no
8
+
9
+
-**The notification names the record's object by its label.** The body of `approval.requested` used to read `A decision on crm_opportunity/JCqLLy5dSug1MldB is waiting on you.`, with the object's API name. It now reads `A decision on Opportunity JCqLLy5dSug1MldB is waiting on you.`, using the label the Approvals Inbox row shows for that object. The API name is used only when the object's schema declares no label, as in the inbox. The body carries the record's id, as before, and not its title, from any source. Title, topic, action link and recipients are unchanged.
10
+
-**The empty-slot warning states the slot, not the request's fate.** An approver that expands to nobody used to log `the slot routes to no one and the request cannot advance until someone is added or the approver is re-pointed`. On the default `onEmptyApprovers: 'admin_rescue'` path the next line opened the request on the organization's administrators, who then decided it. The line now reads `the slot names no one, so no one is asked through it. Staff it or re-point the approver; a slate left naming no one is decided by the node's onEmptyApprovers.` What the node does with an empty slate is stated once, by the node-level line that already follows it.
11
+
12
+
A patch: both are server-authored texts. No schema, translation key, export or notification field moves.
0 commit comments