Round R3 of the #9296 QA wave. One selector, one run, one issue. Sibling rounds: #9351 (records-forms).
Environment fingerprint
| reading | value |
|---|
| framework sha | e4e5c6e3c608b1b807c83a0d5b734f213eb1a1dd (2026-08-17 11:29:50Z) |
.objectui-sha (pin) | 665661ab093263f39f2e660a295ea615dbcee35a |
| console dist stamp | 665661ab093263f39f2e660a295ea615dbcee35a — stamp == pin, pnpm check:console-sha exits 0 |
| app | showcase, --seed-admin (admin@objectos.ai / admin123) |
| port / db | 3202 · file:/tmp/qa-r3a/ap.db (fresh, rm -f before boot) |
| runner | R3 runner session, 2026-08-17 |
Spec-forward step: identical to #9351 — PR #9309 has not merged, and its diff does not contain approvals.json (all its area-file changes are +N −0 on other areas), so this area's spec is byte-identical either way. Nothing was committed.
Scope
node scripts/checklist-select.mjs area:approvals --json → 11 runnable items (the resolver excludes 3 blocked items by design).
Result — this round is thin, and says so
0 PASS · 1 PARTIAL · 0 FAIL · 10 NOT-RUN · 3 BLOCKED(fixture, pre-existing).
Only decision-only-via-service was driven. The round ran out of budget after the records-forms area (#9351), which consumed most of the session — including ~21 minutes of unavoidable cold-tree building. No approvals item is being reported as passing on the strength of anything but the evidence quoted below, and the ten untouched items are not-run, not pass. This area needs a dedicated round.
Per-clause verdicts
approvals.decision-only-via-service rev 1 (P1) — partial (3 pass, 1 partial)
The item's automated pin was run first (RUNNER rule 6):
pnpm --filter @objectstack/service-automation exec vitest run src/resume-authority-gate.test.ts
→ Test Files 1 passed (1) · Tests 22 passed (22)
| # | oracle | verdict | evidence (one line) |
|---|
| 0 | api | pass | showcase_invoice_signoff run run_d424743b… sits paused at dual_signoff. POST /api/v1/automation/showcase_invoice_signoff/runs/<runId>/resume with {} → 403; the same route with {branchLabel:'approve'} → 403. Neither 2xx nor a 404 that would mask the gate — the run demonstrably exists (it is readable and later resumes) |
| 1 | api | pass | after both forged POSTs: the run re-reads status: paused, last step still dual_signoff; GET /approvals/requests/<id>/actions count is 1 before and 1 after, byte-identical — the forged attempts recorded no decision and changed nothing |
| 2 | api | pass | the approvals door works: POST /approvals/requests/<id>/approve {comment:'qa approve 1'} → 200 satisfied the slate; request re-reads status: approved; the run transitions paused → completed with steps [start, dual_signoff, notify_cleared] — the notify_cleared step executed, exactly as the clause requires |
| 3 | api | partial | the discriminating half is proven — the same generic route on a screen node is NOT dead: POST /automation/showcase_reassign_wizard/runs/<runId>/resume {inputs:{new_assignee:'QA Assignee'}} → 200, and the run advanced start → collect → apply. The clause also asks the run to complete; it ended failed at apply, because I supplied the free-text string 'QA Assignee' to a step that writes showcase_task.assignee (a user reference). That is my input's fault, not the gate's, so the clause is partial rather than pass and definitely not a fail |
Worth recording for the next runner, since it sharpens the gate's shape: an empty-input resume on the screen node returns 400 VALIDATION_ERROR — Screen field "new_assignee" is required — declared fields: 'new_assignee'. So the three responses are cleanly separated: 403 = approval-node authority gate, 400 = the screen node's own input validator (route reached the node), 200 = valid resume. That 400/403 split is independent confirmation that the 403 is node-scoped authority and not a broken route.
Not-run items (10)
per-group-signoff (rev 4) · inbox-metadata-actions (rev 3) · notification-deep-link (rev 4) · decision-action-matrix (rev 2) · dynamic-approver-routing (rev 2) · ooo-delegation-reroute (rev 2) · record-page-decisions (rev 1) · account-app-entry (rev 1, P0) · setup-nav-entry (rev 1) · pending-count-surfaces (rev 1)
⚠️account-app-entry is a P0 and was not run. It should lead the next round.
Groundwork done that the next round can reuse
Not verdicts — just state observed on this boot, so the next runner does not re-derive it:
- Three requests are seeded pending on a fresh boot:
showcase_expense_report/dual_signoff (pending slate = two distinct users, the admin and usr_showcase_auditor_demo — the per-group fixture the per-group-signoff item needs), showcase_expense_report/committee (quorum, slate collapses onto the admin ×3 — the shape quorum-m-of-n is blocked on), and showcase_invoice/dual_signoff (consumed by this round's decision). decision-action-matrix needs a fresh budget request; the trigger is budget != previous.budgetand above the threshold. My first attempt silently did nothing because the seeded Mobile App project already had budget: 200000 — patching it to the same value is not a change. Bumping to 350000 opened areq_ca40f5da… at manager_review. Use a project whose budget you have first read.- The list projection
GET /approvals/requests?status=pending does not carry pending_approver_groups (it reads undefined there); the per-group tallies the item asks for need the single-request read GET /approvals/requests/:id.
Fixture gaps (pre-existing, resolver-excluded — re-confirmed, unchanged)
| item | blocked.by | reason recorded on the item |
|---|
quorum-m-of-n | fixture | #3358 — quorum slate collapses onto the admin; showcase design call pending. Re-confirmed on this boot: the committee request's slate is the same user three times |
viewer-gating-submitter-side | fixture | #3358 — needs a request routed to a position the viewing submitter does not hold |
sla-escalation | fixture | hour-granular SLA needs a clock-control / runEscalations timing harness (timeoutHours min 1, sweep on a real-time interval) — no stock-fixture way to advance past the deadline in-session |
Additionally, and blocking several of the not-run items: no second real login was provisioned this round.per-group-signoff, ooo-delegation-reroute and account-app-entry all need one (the seeded personas are sys_user rows without better-auth accounts). The items' own knownGaps record the recipe — the credential account's issuer must equal local:credential (packages/plugins/plugin-auth/src/backfill-account-issuer.ts), or sign-in fails INVALID_EMAIL_OR_PASSWORD behind a misleading "User not found" warn.
Browser note
Every remaining approvals item is browser or mixed. Chromium is launchable in this container with an explicit path (/opt/pw-browsers/chromium-1194/chrome-linux/chrome, verified working against the live server), but the repo's pinned @playwright/test@1.62.1 resolves revision 1234 and fails instantly — see #9351 for the full evidence. So the next round can drive these; it just must not rely on the repo's own playwright config.
Links back to #9296.
Round R3 of the #9296 QA wave. One selector, one run, one issue. Sibling rounds: #9351 (records-forms).
Environment fingerprint
e4e5c6e3c608b1b807c83a0d5b734f213eb1a1dd(2026-08-17 11:29:50Z).objectui-sha(pin)665661ab093263f39f2e660a295ea615dbcee35a665661ab093263f39f2e660a295ea615dbcee35a— stamp == pin,pnpm check:console-shaexits 0--seed-admin(admin@objectos.ai / admin123)file:/tmp/qa-r3a/ap.db(fresh,rm -fbefore boot)Spec-forward step: identical to #9351 — PR #9309 has not merged, and its diff does not contain
approvals.json(all its area-file changes are+N −0on other areas), so this area's spec is byte-identical either way. Nothing was committed.Scope
node scripts/checklist-select.mjs area:approvals --json→ 11 runnable items (the resolver excludes 3blockeditems by design).Result — this round is thin, and says so
0 PASS · 1 PARTIAL · 0 FAIL · 10 NOT-RUN · 3 BLOCKED(fixture, pre-existing).
Only
decision-only-via-servicewas driven. The round ran out of budget after the records-forms area (#9351), which consumed most of the session — including ~21 minutes of unavoidable cold-tree building. No approvals item is being reported as passing on the strength of anything but the evidence quoted below, and the ten untouched items arenot-run, notpass. This area needs a dedicated round.Per-clause verdicts
approvals.decision-only-via-servicerev 1 (P1) — partial (3 pass, 1 partial)The item's automated pin was run first (RUNNER rule 6):
showcase_invoice_signoffrunrun_d424743b…sitspausedatdual_signoff.POST /api/v1/automation/showcase_invoice_signoff/runs/<runId>/resumewith{}→ 403; the same route with{branchLabel:'approve'}→ 403. Neither 2xx nor a 404 that would mask the gate — the run demonstrably exists (it is readable and later resumes)status: paused, last step stilldual_signoff;GET /approvals/requests/<id>/actionscount is 1 before and 1 after, byte-identical — the forged attempts recorded no decision and changed nothingPOST /approvals/requests/<id>/approve {comment:'qa approve 1'}→ 200 satisfied the slate; request re-readsstatus: approved; the run transitions paused → completed with steps[start, dual_signoff, notify_cleared]— thenotify_clearedstep executed, exactly as the clause requiresPOST /automation/showcase_reassign_wizard/runs/<runId>/resume {inputs:{new_assignee:'QA Assignee'}}→ 200, and the run advancedstart → collect → apply. The clause also asks the run to complete; it endedfailedatapply, because I supplied the free-text string'QA Assignee'to a step that writesshowcase_task.assignee(a user reference). That is my input's fault, not the gate's, so the clause ispartialrather thanpassand definitely not afailWorth recording for the next runner, since it sharpens the gate's shape: an empty-input resume on the screen node returns 400
VALIDATION_ERROR—Screen field "new_assignee" is required — declared fields: 'new_assignee'. So the three responses are cleanly separated: 403 = approval-node authority gate, 400 = the screen node's own input validator (route reached the node), 200 = valid resume. That 400/403 split is independent confirmation that the 403 is node-scoped authority and not a broken route.Not-run items (10)
per-group-signoff(rev 4) ·inbox-metadata-actions(rev 3) ·notification-deep-link(rev 4) ·decision-action-matrix(rev 2) ·dynamic-approver-routing(rev 2) ·ooo-delegation-reroute(rev 2) ·record-page-decisions(rev 1) ·account-app-entry(rev 1, P0) ·setup-nav-entry(rev 1) ·pending-count-surfaces(rev 1)account-app-entryis a P0 and was not run. It should lead the next round.Groundwork done that the next round can reuse
Not verdicts — just state observed on this boot, so the next runner does not re-derive it:
showcase_expense_report/dual_signoff(pending slate = two distinct users, the admin andusr_showcase_auditor_demo— the per-group fixture theper-group-signoffitem needs),showcase_expense_report/committee(quorum, slate collapses onto the admin ×3 — the shapequorum-m-of-nis blocked on), andshowcase_invoice/dual_signoff(consumed by this round's decision).decision-action-matrixneeds a fresh budget request; the trigger isbudget != previous.budgetand above the threshold. My first attempt silently did nothing because the seededMobile Appproject already hadbudget: 200000— patching it to the same value is not a change. Bumping to350000openedareq_ca40f5da…atmanager_review. Use a project whose budget you have first read.GET /approvals/requests?status=pendingdoes not carrypending_approver_groups(it readsundefinedthere); the per-group tallies the item asks for need the single-request readGET /approvals/requests/:id.Fixture gaps (pre-existing, resolver-excluded — re-confirmed, unchanged)
blocked.byquorum-m-of-nviewer-gating-submitter-sidesla-escalationrunEscalationstiming harness (timeoutHoursmin 1, sweep on a real-time interval) — no stock-fixture way to advance past the deadline in-sessionAdditionally, and blocking several of the not-run items: no second real login was provisioned this round.
per-group-signoff,ooo-delegation-rerouteandaccount-app-entryall need one (the seeded personas aresys_userrows without better-auth accounts). The items' ownknownGapsrecord the recipe — the credential account'sissuermust equallocal:credential(packages/plugins/plugin-auth/src/backfill-account-issuer.ts), or sign-in failsINVALID_EMAIL_OR_PASSWORDbehind a misleading "User not found" warn.Browser note
Every remaining approvals item is
browserormixed. Chromium is launchable in this container with an explicit path (/opt/pw-browsers/chromium-1194/chrome-linux/chrome, verified working against the live server), but the repo's pinned@playwright/test@1.62.1resolves revision 1234 and fails instantly — see #9351 for the full evidence. So the next round can drive these; it just must not rely on the repo's own playwright config.Links back to #9296.