Repository navigation
Commit 255a777
fix(plugin-approvals): approval notifications reach their recipient with their text, sent as body, the field messaging reads (#21888)
Fixes #21847
Clause-②: no
## What changes
Every notification the approvals service sends reached its recipient as
a title over an empty body. That covers a comment, a request-info
question, a send-back note, a reassignment, a reminder, an escalation,
an SLA breach and an out-of-office substitution. The service put the
text in `payload.message`. The messaging service builds the delivered
notification from `payload.title` and `payload.body` only: the inline
fan-out and the outbox snapshot in `messaging-service.ts`, and
`processRow` in `dispatcher.ts`. So `GET /api/v1/notifications` answered
`body: ""`, and the inbox row's `body_md` was empty.
- `packages/plugins/plugin-approvals/src/approval-service.ts`: the
twelve `notify()` call sites send their text as `body`. The texts are
byte-identical: each removed `message:` line equals its added `body:`
line once the key is stripped, indentation included (12 of 12). Who is
notified, and when, is unchanged.
- `notify()` now declares its payload as a local, non-exported type
`ApprovalNotificationPayload` (`title`, `body`, `actionUrl`, and a
reminder's `actions`). It used to take a free-form string-keyed record.
A call site that spells the text any other way is now a compile error
(Leg B below). No package entry gains an export.
- No edit to `service-messaging` or `packages/spec`, and no `message`
alias anywhere. There is one field, as `EmitInput` documents, per the
triage direction on the card.
- `.changeset/21847-approval-notification-body.md`: a `patch` for
`@objectstack/plugin-approvals`.
## Measured before the change (`origin/main` e864db5)
- **H1 holds.** All twelve call sites put the text in `payload.message`
(`approval-service.ts` :3076 :3087 :4209 :4252 :4512 :4571 :4594 :4788
:4835 :5880 :5898 :5911). Messaging reads only `payload.body`
(`messaging-service.ts` :1101 and :1221, `dispatcher.ts` :341). The red
reading is under Tests.
- **H2.** No call site bypasses `notify()`: the only `messaging.emit` in
the package is the helper's own. The helper forwards the payload each
call site builds, so the one fix inside the helper is the declared
payload type. The renames happen at the call sites.
- **H3. Nothing reads `payload.message` from an approval notification.**
- This repo: `git grep` for `payload.message`, `payload?.message` and
`{{ message }}` answered 5 hits. None reads a notification: an SMS
transport's HTTP error, CLI and runtime test fixtures, and a PM script.
- objectui at the `.objectui-sha` pin `0abd4f9f`: 7 hits, all HTTP
error-response readers. For example,
`apps/console/src/services/approvalsApi.ts:200` reads a failed request's
JSON. Control term `/notifications` answered 58 lines at the same tree.
- One possible reader stays unmeasured. `template-renderer.ts` spreads
the payload into the template context, so a tenant-authored
`sys_notification_template` row for an `approval.*` topic could have
written `{{ message }}`. This repo seeds no such row. The changeset
names the `{{ body }}` spelling.
- **H4.** The texts name the object and record, and for an out-of-office
substitution the two user ids. Every recipient is already a party to the
request. The texts are unchanged.
- No `content/docs/**` sentence is made false. The notification passages
in `content/docs/automation/approvals.mdx` describe topics and deep
links, not the field.
## Tests
Both pins read plugin-approvals **source**. The unit file imports
`./approval-service.js`. The dogfood `isolated` project aliases
`@objectstack/plugin-approvals` to `src/index.ts`. Leg A proves this:
`dist/` held the fix while the door pin went red.
- `plugin-approvals/src/approval-notification-body.integration.test.ts`
(new) drives the real `ApprovalService` on `ObjectQL` over `SqlDriver`
(in-memory better-sqlite3). It pins three things:
- the battery reaches every call site that can notify anyone (11 of 12);
- a comment, a request-info question and a send-back note are their
recipient's `body`;
- every payload carries a non-empty `body` and no field outside the
declared type.
- `packages/qa/dogfood/test/approval-notification-body.dogfood.test.ts`
(new) boots an app with real approvals, messaging and REST, and an
email-authored user approver. A submitter's comment and an approver's
request-info question are read back from each recipient's `GET
/api/v1/notifications`. Each is the one row of its topic and has a
title, so the old defect reads as a body that lost its text, not as a
missing notification.
**Red, at 68baad4** (the tests only, unfixed service):
- unit: `Tests 2 failed | 1 passed (3)`. The comment read `expected [ [
'u_alice' ], undefined ]`. There were 22 offenders, `body is undefined`
plus `undeclared payload field 'message'`, at each of the 11 reached
sites. The coverage pin was green.
- door: `AssertionError: the comment is the notification's body:
expected '' to be 'The signed contract is attached to th…'`. The row
count of 1 and the non-empty title both held.
**Green, at 7ca65ce:**
- `pnpm --filter @objectstack/plugin-approvals typecheck`: exit 0. The
test layer held 8 files, 324 errors and 27 pinned signatures, and the
new file is clean.
- `pnpm --filter @objectstack/plugin-approvals exec vitest run
--maxWorkers=2`: `Test Files 61 passed (61)`, `Tests 898 passed (898)`.
- `pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2
--project isolated test/approval-notification-body.dogfood.test.ts`:
`Tests 1 passed (1)`.
- `pnpm --filter @objectstack/dogfood typecheck`: exit 0.
- Build: `pnpm turbo run build --filter='@objectstack/dogfood^...'
--filter='@objectstack/plugin-approvals^...' --concurrency=1` ran 63 of
63 tasks, 48 cached. plugin-approvals was rebuilt at 7ca65ce.
## Ablation, at 7ca65ce (`scripts/ablation-replace.mjs`, wrap mode,
plus a trap restore)
- **Leg A: `message` put back in the helper.** The anchor `severity:
'info', ...event, payload, audience,` went from 1 hit to 0. It was
replaced by a call that sends `{ ...rest, message: body }`, and the
replacement went from 0 hits to 1. The blob moved from 9a18228 to
d066658afd62.
- unit: exit 1, 2 failed (comment `body` undefined; 22 offenders).
- door: exit 1, `expected '' to be 'The signed contract…'`.
- Restore: blob 9a18228 equals HEAD, and `git diff HEAD` is empty.
The trap's restore leg repeated that proof at exit.
- **Leg B: one call site spells the text `message`.** At the
request-info site, `body: input.comment.trim(),` became `message:
input.comment.trim(),`. `tsc --noEmit` exited 1 with
`src/approval-service.ts(4810,11): error TS2353: Object literal may only
specify known properties, and 'message' does not exist in type
'ApprovalNotificationPayload'.` Restore: blob equals HEAD.
## Gates, at 7ca65ce
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (no paths) derived 70 commands, and all 70
ran.
- On the first pass, 69 exited 0. `pnpm check:dual-build-cjs-loads`
exited 3 (PREREQUISITE NOT MET: 8 packages had no `dist/`).
- Those 8 were built (41 of 41 tasks cached) and the gate re-ran with
exit 0: 106 entry points across 66 packages load.
- `--ran` verdict: `70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN.`
- Narrowed lint: `pnpm exec eslint --no-inline-config --format json` on
the three touched `.ts` files. The JSON reports 3 files linted, with 0
errors and 0 warnings.
- Population: `eslint.config.mjs`'s `packages/**/*.{ts,…}` blocks match
all three, and none is ignored.
- Invariance: the config never enables type-aware linting (no
`parserOptions.project`), so this diff cannot change the verdict on any
untouched file.
- The full `pnpm lint`, the full dogfood suite and the full turbo test
are CI's.
## Acceptance notes
- **The reminder to a slot literal never reaches messaging.** That is
the remind fan-out's branch for `position:P`-style pending entries. Its
audience holds only slot literals, and `notify()` drops every audience
entry containing a colon, so it returns 0 before emitting. Its payload
is renamed with the rest and held by the compiler, and the producer
battery covers the other eleven call sites.
- Not filed: this run did not measure it at a public door.
- Whether a position holder should receive that nudge is a different
question from this card.
---
_Generated by [Claude
Code](https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 67c544c commit 255a777
4 files changed
Lines changed: 489 additions & 14 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 | + | |
| 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 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
0 commit comments