Repository navigation
Commit 1ff43b6
fix(app-showcase): write the expense approvals' decision back onto the report (#25525)
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuACdPWFJTLDcTrByFCHLzqb)_
Fixes #25469
Clause-②: no
Metadata-protocol: none
## What changed
`showcase_expense_signoff` (per-group 会签, node `dual_signoff`) and
`showcase_committee_quorum` (2-of-3, node `committee`) ran `start →
approval → end`, with bare `end` nodes on both outcome arms. The
approval request finalized, but nothing wrote the decision onto the
expense report, so EXP-2001 kept reading `status: submitted` after both
groups approved.
Each flow now runs an `update_record` on each outcome arm and has
`runAs: 'system'`:
- approve → `mark_approved` → `status: 'approved'`
- reject → `return_to_draft` → `status: 'draft'` (the report goes back
to the submitter, who can rework and resubmit; this is the same
revert-on-reject `showcase_invoice_signoff`'s `flag_held` does)
`runAs: 'system'` follows the invoice flow's precedent. The write-back
is an outcome of the approval, not an act of the submitter, so it has to
land whatever the submitter's edit rights are.
Landing: `examples/app-showcase/src/automation/flows/index.ts` (the file
the claim names) plus the new pin
`examples/app-showcase/test/expense-approval-write-back.test.ts`. No
package source changed.
## Why `update_record` per branch and not the `approvalStatusField`
mirror
I checked how the mirror behaves in
`packages/plugins/plugin-approvals/src/approval-service.ts`
(`mirrorStatusField` / `mirroredStatusValue`):
- It writes the request's status words exactly as they are: `pending`
when the request opens, `approved` / `rejected` on a decision,
`recalled` on a recall or dead-run sweep, `returned` on a send-back, and
`rejected` on the revision-limit auto-reject.
- It has no value mapping. The spec key is a bare `approvalStatusField:
z.string()`, and adding a mapping would mean editing `packages/spec`,
which is out of bounds for this card.
- It writes only values the target field lists among its options. Any
other value is skipped and the service logs a warning (`approval status
'X' was not written to OBJECT.FIELD`). The only exception is a
transition that abandons the request, where it restores the value from
before submission.
`showcase_expense_report.status` declares `draft / submitted / approved
/ reimbursed`. So a mirror on `status` would:
- skip `pending` and log a warning on every submit
- write `approved` correctly
- skip `rejected`, so a **rejected** report would keep reading
"Submitted", which is the same defect on the other arm
The spec also says the mirror target "should be readonly on the object",
and `status` is the field the submitter edits. When the decision drives
a business-lifecycle field, the right tool is an explicit write per
branch. Both flows' docblocks now explain this.
## Committee quorum flow
`showcase_committee_quorum` had the same gap, with bare `end` nodes on
both arms. It gets the same fix.
## Pin
`test/expense-approval-write-back.test.ts` boots the production stack: a
real `ObjectKernel` with the ObjectQL, automation and approvals plugins
over real sqlite, and the app's real `ExpenseReport` / `ExpenseLine`
objects and real flows. It opens each request the way the seed's
`launchSignoff` does, then decides it through `ApprovalService.decide`.
`describe.each` runs it over both flows:
- **approve**: after the first of two approvals the report still reads
`submitted` and the request is not finalized. After the finalizing
approval the run is `completed`, it stepped `mark_approved` and not
`return_to_draft`, and the report reads `approved`.
- **reject**: the run steps `return_to_draft` and the report reads
`draft`, a value the status field declares.
- **CONTROL**: the flow is rebuilt in its pre-fix shape (bare `end`
arms, no `update_record` nodes) on the same harness. The same approvals
finalize and resume the run, and the report stays at `submitted`, which
is the defect as reported.
The committee case adds an expense line of 8,900 so the `total_amount >=
5000` start gate opens (the seeded EXP-DEMO is 8,900). The per-group
case uses 1,500, under the threshold like EXP-2001.
### Ablation (`scripts/ablation-replace.mjs`, on committed HEAD
`3e661a73f6`)
The mutation pointed the signoff approve edge back at the bare end node:
anchor `{ id: 'e2', source: 'dual_signoff', target: 'mark_approved',
label: 'approve' },` was replaced with `target: 'approved'`.
```
ablation-replace: anchor x1 -> x0
ablation-replace: blob a47c39e -> 78466ece32b2df854bf4e326c1c6ab5f8618a75f
× approve: the report reads `approved` once the approval finalizes, not before
AssertionError: expected [ 'start', 'dual_signoff' ] to include 'mark_approved'
Tests 1 failed | 5 passed (6)
ablation-replace: ok restored: blob == HEAD (a47c39e) and `git diff HEAD` is empty
```
The test went red under the mutation and green again once the file was
restored. The example is source-imported by the test (relative `../src`
import), so there is no `dist/` leg.
## Verification (head `3e661a73f6`)
- `pnpm --filter @objectstack/example-showcase exec vitest run
test/expense-approval-write-back.test.ts`: 6/6 passed
- `pnpm --filter @objectstack/example-showcase typecheck && pnpm
--filter @objectstack/example-showcase test`: lock VERDICT command-exit
0; Test Files 38/38, Tests 461/461. The new test file is in the tsc
program (`--listFiles`: 1 hit).
- `node scripts/pm/dispatch-gates.mjs --commands`: 45 commands, each run
with its exit code recorded. 44 exited 0. `pnpm
check:dual-build-cjs-loads` exited 3 (PREREQUISITE NOT MET: unbuilt
packages outside this diff's closure), so it is **NOT MEASURED** locally
and left to CI.
- `--ran` reconciliation: `45 derived famil(ies) accounted for — 44 run,
1 NOT-MEASURED (1 DERIVED from a recorded exit 3)`.
No changeset: `@objectstack/example-showcase` is `private: true`, so
nothing publishes (`skip-changeset`).
## Acceptance notes
- `docs/qa/platform-checklist/areas/approvals.json` →
`approvals.status-mirror-field` still records under `knownGaps` that no
showcase flow declares `approvalStatusField`. That stays true after this
PR, deliberately (see above). The checklist is not edited here.
- `showcase_expense_signoff`'s start gate has no amount ceiling, so a
report of $5,000 or more matches both flows' start conditions on submit.
The seed routes the two demos to different records on purpose. I did not
measure what a live submit of such a report does, so this is noted here
and not filed.
- On the issue's `lockRecord` question: the write-back runs after the
request has left `pending`, and the lock is keyed on pending. It also
runs as the system principal, which the lock hook exempts. The pin shows
the write lands under `lockRecord: true`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 794b769 commit 1ff43b6
2 files changed
Lines changed: 307 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1747 | 1747 | | |
1748 | 1748 | | |
1749 | 1749 | | |
| 1750 | + | |
| 1751 | + | |
| 1752 | + | |
| 1753 | + | |
| 1754 | + | |
| 1755 | + | |
| 1756 | + | |
| 1757 | + | |
| 1758 | + | |
| 1759 | + | |
| 1760 | + | |
| 1761 | + | |
| 1762 | + | |
| 1763 | + | |
| 1764 | + | |
| 1765 | + | |
| 1766 | + | |
| 1767 | + | |
| 1768 | + | |
| 1769 | + | |
| 1770 | + | |
| 1771 | + | |
| 1772 | + | |
1750 | 1773 | | |
1751 | 1774 | | |
1752 | 1775 | | |
1753 | 1776 | | |
1754 | 1777 | | |
1755 | 1778 | | |
1756 | 1779 | | |
| 1780 | + | |
1757 | 1781 | | |
1758 | 1782 | | |
1759 | 1783 | | |
| |||
1779 | 1803 | | |
1780 | 1804 | | |
1781 | 1805 | | |
| 1806 | + | |
| 1807 | + | |
| 1808 | + | |
| 1809 | + | |
| 1810 | + | |
| 1811 | + | |
| 1812 | + | |
| 1813 | + | |
| 1814 | + | |
| 1815 | + | |
| 1816 | + | |
| 1817 | + | |
| 1818 | + | |
| 1819 | + | |
| 1820 | + | |
| 1821 | + | |
| 1822 | + | |
| 1823 | + | |
| 1824 | + | |
| 1825 | + | |
1782 | 1826 | | |
1783 | 1827 | | |
1784 | 1828 | | |
1785 | 1829 | | |
1786 | 1830 | | |
1787 | | - | |
1788 | | - | |
| 1831 | + | |
| 1832 | + | |
| 1833 | + | |
| 1834 | + | |
1789 | 1835 | | |
1790 | 1836 | | |
1791 | 1837 | | |
| |||
1797 | 1843 | | |
1798 | 1844 | | |
1799 | 1845 | | |
| 1846 | + | |
| 1847 | + | |
| 1848 | + | |
| 1849 | + | |
| 1850 | + | |
1800 | 1851 | | |
1801 | 1852 | | |
1802 | 1853 | | |
1803 | 1854 | | |
1804 | 1855 | | |
1805 | 1856 | | |
1806 | 1857 | | |
| 1858 | + | |
1807 | 1859 | | |
1808 | 1860 | | |
1809 | 1861 | | |
| |||
1830 | 1882 | | |
1831 | 1883 | | |
1832 | 1884 | | |
| 1885 | + | |
| 1886 | + | |
| 1887 | + | |
| 1888 | + | |
| 1889 | + | |
| 1890 | + | |
| 1891 | + | |
| 1892 | + | |
| 1893 | + | |
| 1894 | + | |
| 1895 | + | |
| 1896 | + | |
| 1897 | + | |
| 1898 | + | |
| 1899 | + | |
| 1900 | + | |
| 1901 | + | |
| 1902 | + | |
| 1903 | + | |
| 1904 | + | |
1833 | 1905 | | |
1834 | 1906 | | |
1835 | 1907 | | |
1836 | 1908 | | |
1837 | 1909 | | |
1838 | | - | |
1839 | | - | |
| 1910 | + | |
| 1911 | + | |
| 1912 | + | |
| 1913 | + | |
1840 | 1914 | | |
1841 | 1915 | | |
1842 | 1916 | | |
| |||
| 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 | + | |
0 commit comments