test(rest): pin the approvals FORBIDDEN → 403 row here too — and correct the record that it was unpinned - #14838
Merged
os-project-manager merged 3 commits intoSep 3, 2026
Conversation
`handleApprovalError`'s `[/^FORBIDDEN/, 403, 'FORBIDDEN']` row is the one every authorisation refusal rides, and it had no live-emission pin: the service suites assert the `FORBIDDEN:` message prefix at the throw site, which is a different fact from what the route answers on the wire. Adds one `it()` to `rest-approvals-wire-codes.test.ts` driving the real recall route with a service that throws the real refusal, asserting status 403, `code === 'FORBIDDEN'`, and that the [#13095] anchored strip removed the prefix. Losing the row fails closed (500 `APPROVAL_RECALL_FAILED` with the raw message), so the third assertion catches the strip half of the regression as well as the status half. Test-only; no production behaviour changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
… is false Measured, not read: deleting `[/^FORBIDDEN/, 403, 'FORBIDDEN']` from `handleApprovalError` reds THREE cases, not one — this new pin and both cases of `rest-data-door-code-prefix.test.ts` §7, which already drive the real approve route and already assert 403, `code: 'FORBIDDEN'` and the anchored strip. #14573 was filed and triaged on the reading that the row had no live-emission pin anywhere. That is wrong. What is true is narrower: the file that OWNS the approvals wire-code contract did not pin it, so an audit of wire codes here saw a gap a strip-contract file was silently covering. Naming §7 from here is half the fix — the unlabelled duplicate is what got the card mis-filed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
os-trump
marked this pull request as ready for review
September 3, 2026 05:19
os-trump
enabled auto-merge
September 3, 2026 05:19
This was referenced Sep 3, 2026
os-project-manager
disabled auto-merge
September 3, 2026 06:34
os-project-manager
enabled auto-merge
September 3, 2026 07:48
os-project-manager
deleted the
claude/issue-14573-approvals-forbidden-live-emission-pin
branch
September 3, 2026 10:50
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #14573.
FORBIDDEN → 403row ofhandleApprovalError"has no live-emission pin — only theservice-side prefix is pinned". It has one. It has two, in fact, and they discriminate.
Read the ablation below before deciding what to do with this branch.
File face
packages/rest/src/rest-approvals-wire-codes.test.ts— one file, test-only, no productioncode touched. Specifically NOT
packages/rest/src/error-response.ts, which PR #14713 is inflight on and card #14725 is queued on.
handleApprovalErrorlives inrest-server.ts; thisbranch reads it and never edits it.
Anchors, verified against
origin/main521eaf9e41handleApprovalErroratrest-server.ts:11557[/^FORBIDDEN/, 403, 'FORBIDDEN'],is row 5 of the table:11564rest-approvals-wire-codes.test.tsgreps 0 forFORBIDDEN|403THROTTLED→ 6)The falsification, measured
rest-data-door-code-prefix.test.ts§7 (describe('[#13095] the approvals door strips the code it answers, never a blanket pattern')) boots aRestServerwith an approvals servicewhose
deciderejects, drives the realPOST /api/v1/approvals/requests/:id/approveroute, and asserts
status === 403,body.code === 'FORBIDDEN'and the strip. That isexactly the shape the triage comment specified, already in the tree, on a different route.
The card enumerated that file and characterised it as pinning "the code-prefix strip … not the
403 row". That characterisation is wrong, and the triage re-ran the grep only against
rest-approvals-wire-codes.test.ts, so it inherited the error.Ablation — delete the row from
rest-server.ts, no rebuild (subject reached by a./rest-server.jsrelative in-package import, which vitest transforms from source; the redbelow is the proof that source is what ran):
Tests 27 passed (27)Tests 3 failed | 24 passed (27)Tests 27 passed (27)The three reds, all reading
AssertionError: expected 500 to be 403:rest-approvals-wire-codes.test.ts→ the case this PR addsrest-data-door-code-prefix.test.ts§7 → the well-formed idiom is unchanged… ← pre-existingrest-data-door-code-prefix.test.ts§7 → a LONGER token sharing the matched spelling… ← pre-existing24 neighbours stayed green, including every other case in both files — the red is specific
to the row, not a file-wide collapse.
Restore proven by blob-hash equality, not by an exit code:
POST blob ce00225a072be73a12eedd7143c5f50fa2a901c6= HEAD blob, andgit diff HEAD -- PATHempty. Mutation and restore both ran under
trap restore EXIT INT TERMon absolute paths.What is actually true, and what this PR does
The row is pinned. What was not pinned is narrower and still real: the file that owns the
approvals wire-code contract carried no
FORBIDDENcase, so an audit of approvals wire codesread a gap that a strip-contract file was silently covering. §7's two cases sit under a
describewhose declared subject is #13095's anchored strip — retire or refactor that contractand the 403 row loses its live pin with nobody noticing.
This PR adds one
it()closing that locality gap, through the real recall route with thereal catalog refusal sentence, asserting 403 +
code+ the strip — and, in the same commit,names §7 from the new test so the duplicate is labelled in both directions. An unlabelled
duplicate is what got this card mis-filed; a labelled one cannot.
This is a disposition call for the maintainer, not a done card. Three defensible answers:
land this as the labelled pin in the owning file; drop the
it()and keep only a pointer to §7;or close #14573 as already-covered. My recommendation is the first — one row silently pinned by
two files that do not know about each other is the shape that produced this card.
⛔ Not done here
is enough: one row, one closure, all four call sites).
packages/specread, never edited —BUILTIN_OPERATION_MESSAGESis imported from@objectstack/spec/system, the same constructionapproval-revise.test.tsalready uses.handleApprovalError's table has 9 rows; 3 nowhave live-emission pins here (
THROTTLED,READ_BACK_FAILED,FORBIDDEN) and 6 do not—
VALIDATION_FAILED,DUPLICATE_REQUEST,INVALID_STATE,REQUEST_NOT_FOUND,RESUME_TARGET_LOST,RESUME_FAILED. Counted, filed as its own card, not fixed here.Changeset fork:
skip-changeset, measured not assumedpackages/restdeclaresfiles: ["dist","README.md","CHANGELOG.md"]and the roottsup.config.tsbuildsentry: ['src/index.ts']— asrc/**/*.test.tsfile cannot reachdist, so this diff publishes nothing from any released package. Label, not a changeset.Verification
pnpm --filter '@objectstack/rest^...' build(VERDICTcommand-exit 0), then the targetedvitest above. Gate families derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(27 owed by path + kind). Per-run exit codes captured before anypipe. Full list of what ran, what was narrowed and what is left to CI is in the report on the
issue.
🤖 Generated with Claude Code
https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
Generated by Claude Code