Repository navigation
QA run · area:approvals slice-C ids (5/5) · 07d18e69 · 2026-10-28 · 5 PASS / 0 PARTIAL / 0 FAIL / 0 BLOCKED / 0 NOT-RUN #27134
Description
Targeted checklist-test run of the five approvals items PR #27112 rewrote (wave anchor #25807, slice C run). Every step was driven live, in a browser and over the API, on a fresh showcase boot. No clause was read from source alone.
Runner: cloud session session_01QSeKYpK8js3AA3SHa3SyJH, dispatched by seat domain:devx#2 (claim 6462738260). The run was sequential, one runner and no subagents.
Environment fingerprint
- Framework sha:
07d18e692(currentorigin/mainat boot). The console was built fromui/in the same tree withpnpm build --concurrency=2(116/116 tasks, 0 cached), thenpnpm console:build --concurrency=2.check-dev-prereqsexit 0. - Boot:
OS_PORT=41807 OS_STORAGE_LOCAL_ROOT=/tmp/qa-25807/uploads pnpm -C examples/app-showcase exec objectstack dev --ui --seed-admin -p 41807 -d file:/tmp/qa-25807/data.db, started undersetsidand stopped through its own process group. The banner read32 flow(s) 19 bound, and scheduled work was off (the default). - Seed: the stock showcase plus
--seed-admin. The personas were the dev admin (org owner), Mei Phone (usr_showcase_phone_demo) and Ada Auditor (usr_showcase_auditor_demo), the last two with the passwordshowcase123. - Browser: Playwright chromium, headless, one context per persona, absolute origin
http://localhost:41807. - Clock: the run went from 2026-10-28T07:15Z (boot) to 07:33Z (teardown).
Scope
The selector was area:approvals filtered to five ids. node scripts/checklist-select.mjs ID --json pinned these revisions:
| item | priority | surface | revision |
|---|---|---|---|
| approvals.viewer-gating-submitter-side | P1 | browser | 7 |
| approvals.notification-deep-link | P2 | browser | 8 |
| approvals.dynamic-approver-routing | P2 | mixed | 4 |
| approvals.inbox-keyboard-flow | P2 | browser | 3 |
| approvals.approver-resolution-matrix | P1 | api | 9 |
None of the five carries automated.ref, so every clause was driven by hand (tier 2). The selection contains no planned items.
Per-clause verdicts
approvals.viewer-gating-submitter-side (r7) — PASS
| clause | verdict | oracle evidence |
|---|---|---|
| 0 decision panel omits approver levers, keeps Send reminder / Recall | pass | Signed in as Mei through the form. Account app → Approvals → /_console/apps/com.objectstack.account/component/approvals/inbox; the "Submitted by me" tab listed EXP-2001 and EXP-DEMO. Clicking the EXP-2001 row navigated to /apps/com.objectstack.account/sys_approval_request/record/areq_b860b571-…. The screenshot shows the decision panel rendering exactly Send reminder, Recall and Reply, with no Approve, Reject, Reassign, Send back or Request info. The overflow menu holds only Add to favorites / Open in new tab / Copy link. |
| 1 viewer flags say why | pass | Mei called GET /api/v1/approvals/requests/areq_b860b571-… and got 200: viewer: {can_act:false, is_submitter:true, can_override:false}, pending_approvers: [admin, usr_showcase_auditor_demo], which does not contain her id. |
| 2 forged decision rejected; entitled side succeeds | pass | Mei's POST …/approve {comment} answered 403 FORBIDDEN "actor 'usr_showcase_phone_demo' is not a pending approver". With actor_id: usr_showcase_auditor_demo it answered 403 FORBIDDEN "cannot act as … recorded against the authenticated caller". On the entitled side, Ada's POST …/approve answered 200, and the actions list gained approve · usr_showcase_auditor_demo. |
| 3 forged call left no trace | pass | Admin reads of the request and of /actions before and after both forged calls were byte-identical (true/true). The only action row was submit. |
approvals.notification-deep-link (r8) — PASS
| clause | verdict | oracle evidence |
|---|---|---|
| 0 actionUrl carries ?request= | pass | Retitling announcement 0N0VR0G6hg4dqOHb opened areq_a4b0b338-…, pending for the admin. The admin's sys_inbox_message row VSC041n8huJT_NG3 reads topic approval.requested, action_url: /system/approvals?request=areq_a4b0b338-…. |
| 1 cold load lands on the request page, twice | pass | Two fresh pages loaded /_console/system/approvals?request=areq_a4b0b338-…. Both landed on /_console/apps/com.objectstack.setup/sys_approval_request/record/areq_a4b0b338-… with history.length 2, so the deep link was replaced. Both drew "Announcement Dynamic Approval", step Lead Review, status Pending. |
| 2 page = API for that id | pass | GET /api/v1/approvals/requests/areq_a4b0b338-… answered process_label "Announcement Dynamic Approval", status pending, current_step_label "Lead Review" and pending_approver_names {admin: "Dev Admin"}. The page showed the same title, status, step and Waiting on: Dev Admin. |
| 3 both routes land | pass | /_console/apps/com.objectstack.account/component/approvals/inbox?request=areq_a4b0b338-… landed on /_console/apps/com.objectstack.account/sys_approval_request/record/areq_a4b0b338-… and drew the same request. The /system/approvals route is covered in clause 1. |
| 4 unknown id → not-found, never another request | pass | /_console/system/approvals?request=areq_does-not-exist-qa25807 landed on /apps/com.objectstack.setup/sys_approval_request/record/areq_does-not-exist-qa25807. The screenshot shows "Record not found — The record you are looking for does not exist or may have been deleted.", with no request content. The API answers 404 REQUEST_NOT_FOUND for that id. |
approvals.dynamic-approver-routing (r4) — PASS
| clause | verdict | oracle evidence |
|---|---|---|
| 0 typed sys_user multi-select | pass | On the request page, Approve opened a dialog. Its Next Reviewers * field is a Select… record-picker button, not a text input. The popover lists sys_user rows (Dev Admin, Ada Auditor (demo), Mei Phone (demo)) with a search box. Picking two showed 2 selected with both chips. |
| 1 outputless approve refused, request stays pending | pass | POST …/areq_a4b0b338-…/approve {comment} answered 400 VALIDATION_FAILED "decision output(s) next_reviewers are required to approve this request". The same with outputs.next_reviewers: [] also answered 400. In the UI, Confirm with no pick posted nothing and showed "Next Reviewers is required". A re-read showed status pending, actions [submit] only. |
| 2 stage 2 resolves exactly the picked users | pass | The dialog's POST body was {"outputs":{"next_reviewers":["admin"]}}. The stage-2 request areq_4c16dd92-… (step co_sign) has pending_approvers [admin]. |
| 3 N picks → N approvers, in order | pass | A second chain on announcement kjc0_5pFsdzYXfOz posted {"outputs":{"next_reviewers":["usr_showcase_auditor_demo","admin"]}}. The stage-2 request areq_f60cafa8-… has pending_approvers [usr_showcase_auditor_demo, admin]: 1→1 and 2→2, in the picked order. |
| 4 deciding stage 2 completes the run | pass | Run run_c62dbb36-… went from paused to completed after the admin approved areq_4c16dd92-…. Run run_66508f9b-… stayed paused after Ada's approval (unanimous, admin still pending) and went to completed after the admin's. |
approvals.inbox-keyboard-flow (r3) — PASS
| clause | verdict | oracle evidence |
|---|---|---|
| 0 j/k/arrows move a clamped ring; Enter opens the focused row | pass | The admin opened the inbox via the account app's Approvals entry, and the hint bar reads "Keyboard: j/k move · Enter open · x select · a approve · r reject". The screenshot confirmed the list of four rows. The ring (ring-2 row class) moved j→0, j→1, k→0, ArrowDown→1, ArrowUp→0. It clamped: k at 0 stayed 0, and j past the end stayed at 3. Enter on row 1 (EXP-2001) landed on …/sys_approval_request/record/areq_b860b571-…, whose API read is record_title EXP-2001. |
| 1 a/r use the same dialog and the same server transition | pass | r on EXP-DEMO opened an alertdialog, "Reject "EXP-DEMO"? … Cancel Reject", with zero POSTs before the confirm. Confirming sent exactly one POST /api/v1/approvals/requests/areq_11e24c06-…/reject {"actor_id":"admin"} → 200; the re-read showed status rejected and actions [submit, reject]. a on INV-1008 sent one POST …/areq_80e02596-…/approve {"actor_id":"admin"} → 200, approved. The row's own Approve button (EXP-2001) produced the same dialog shape and the same route and body, {"actor_id":"admin"}, with actions going 2→3. The My Pending badge went 4→3→2. |
| 2 keyboard ownership guards | pass | Input guard: a keydown dispatched on the focused search input for each of j, k, a, r and Enter left the ring at [1], opened no dialog, did not navigate and sent no POST. A positive control, the same synthetic keydown on body, moved the ring 1→2→1. Real typing into the input is confounded by the list filter (focus is clamped when the filter empties the list), which is why the synthetic leg exists. Dialog guard: with the reject alertdialog open, j left the ring unchanged and sent no POST. Enter was not pressed inside the dialog, because focus sits on its own Cancel button and Enter would activate that button natively. On this page the decide dialog and the alertdialog are one element, so those two guards are exercised together. |
| 3 a/r gated by quick-decidability | pass | On the dynamic-approval stage-1 row (declared next_reviewers), a and r opened no dialog and sent no POST. The row's buttons are replaced by "This approval collects decision outputs — open it to decide." |
| 4 x/space select only on the pending tab | pass | On My Pending, x on row 1 put it in the selected set and the bulk bar read "1 selected"; space toggled it off again. On "Submitted by me", x selected nothing, and that tab renders no row checkboxes. |
approvals.approver-resolution-matrix (r9) — PASS
The provisioning ran as the admin:
- BU memberships: Ada in
bu_field_ops(parent) and Mei inbu_east_coast(a child ofbu_field_ops), viaPOST /api/v1/data/sys_business_unit_member→ 201. - Team
CbEbrsrhMgxvyLo5vEBR8Ci3GCfSbczOholding Ada and Mei, viacreate-team/add-team-member→ 200. - Manager: Mei → Ada, via
set-user-manager→ 200. - Queue
qa_matrix_queuewith Ada and Mei as members, andqa_empty_queuewith none, viasys_queue/sys_queue_member→ 201.
Each leg registered a scratch active flow through POST /api/v1/automation (200), triggered it by a showcase_field_zoo create, read the requests, and deregistered the flow with DELETE /api/v1/automation/NAME (200).
| clause | verdict | oracle evidence |
|---|---|---|
| 0 per-variant slates | pass | user usr_showcase_auditor_demo → [Ada]. field f_users = [Mei, Ada] → [Mei, Ada]. manager over f_user = Mei → [Ada]. team → [Ada, Mei]. department bu_field_ops → [Mei, Ada]. queue → [Ada, Mei]. org_membership_level owner → [admin], the only sys_member row with role owner. role owner → [admin]. expression: see dynamic-approver-routing clauses 2–3 of this run. position: the seeded INV-1008 (finance ∧ legal) opened on [admin, admin], whose sys_user_position rows hold finance and legal. |
| 1 department walks active descendants | pass | The parent-unit slate was [Mei (descendant bu_east_coast), Ada (parent)]. After PATCH sys_business_unit/bu_east_coast {active:false} and a re-trigger, the slate was [Ada]. The unit was re-activated afterwards. |
| 2 field fans out per id | pass | pending_approvers was two entries, __resolvedFrom {"field:f_users":[Mei, Ada]}, and no CSV literal appeared. |
| 3 role warns and resolves identically | pass | The log line reads WARN [approvals] approver type 'role' is deprecated (ADR-0090 D3) — author 'org_membership_level' instead {"deprecated":"role","canonical":"org_membership_level"}. The registration advisory approval-approver-type-deprecated also fired. The slates were equal: role [admin] and org_membership_level [admin]. |
| 4 empty-slate forks | pass | admin_rescue (default, unstaffed position:qa_unstaffed_pos): pending, pending_approvers [admin] (the organization's only administrator, sys_member role owner), __resolvedFrom {"admin_rescue:abandoned":["position:qa_unstaffed_pos"],"admin_rescue:reason":"position 'qa_unstaffed_pos' has no holder"}. The action is a system comment (no actor), "Routed to this organization's administrators because the approver slate named no one: position 'qa_unstaffed_pos' has no holder.", and the WARN reads "… opening on this organization's administrators per onEmptyApprovers: 'admin_rescue'". fail: no request row; the run failed with NO_APPROVERS: approval node 'appr' … onEmptyApprovers policy is 'fail'. auto_approve: no request row; the run completed with steps start, appr, mark all success, the approve-edge marker wrote f_textarea = qa-approve-edge-marker, and the WARN reads "auto-approving per onEmptyApprovers: 'auto_approve' (no request opened)". fallback ({type:'user', value: Ada}): opened on [Ada] with no literal slot, and the WARN reads "opening on the declared fallbackApprovers". fallback resolving to nobody (another unstaffed position): the WARN reads "AND its declared fallbackApprovers resolved to nobody either — falling back to admin_rescue". It opened on [admin] with reason "…; the declared fallbackApprovers resolved to no one either". |
| 5 queue routes to members; empty queue is loud | pass | Staffed: pending_approvers [Ada, Mei], which equals the planted sys_queue_member rows, with no queue: entry. Empty (qa_empty_queue) and no-row (qa_no_such_queue): the WARN reads "approver 'queue:qa_empty_queue' expanded to nobody — the queue has no members in this organization … It adds no slot to this request". The request stayed pending on [admin], with __resolvedFrom {"admin_rescue:abandoned":[],"admin_rescue:reason":"no approver on the node resolved to a person"} (read on the data door) and the routed-to-administrators comment in its actions. |
The negatives held throughout. Every graph expansion to nobody carried the "expanded to nobody" WARN naming type and value: department:bu_hq_finance beside a staffed user, position:qa_unstaffed_pos(2), queue:*. No empty slate finalized under admin_rescue, and no admin_rescue request opened on a literal slot alone.
Derived item verdicts
- approvals.viewer-gating-submitter-side: pass (4/4)
- approvals.notification-deep-link: pass (5/5)
- approvals.dynamic-approver-routing: pass (5/5)
- approvals.inbox-keyboard-flow: pass (5/5)
- approvals.approver-resolution-matrix: pass (6/6)
The run recorded no fail, so no reproduction rule is owed. No fixture gap blocked a clause.
Checklist-accuracy notes (for the wave anchor #25807, not extracted)
approvals.dynamic-approver-routingknownGaps[0], and the "⛔ Do NOT prescribe reading the run's variables snapshot" sentence in acceptance[3], are stale. A paused run's detail now carries avariableskey:GET /api/v1/automation/showcase_dynamic_approval/runs/RUNIDon a stage-1-paused run answeredstatus: pausedwithvariablespresent ($record,record, …). The suspend path'srecordLogwritesvariables: variablesSnapshot(packages/services/service-automation/src/engine.ts, thestatus: 'paused'recordLog). The clause's functional oracle still holds and passed. Only the stated reason for it is out of date.approvals.notification-deep-linksteps[3] says "APP is setup on the bare /system/approvals route". Measured, the landing segment iscom.objectstack.setup(andcom.objectstack.accounton the component route). The clauses use the APP placeholder, so no verdict moves.
Off-clause probes (RUNNER rule 8)
These took about 3 minutes on the same boot, as Mei, Ada and the admin.
- Boundary ids: an unknown id answered 404
REQUEST_NOT_FOUNDfor both read and approve. - Opposite posture: Mei's forged reject answered 403, her reassign on a request she is not on answered 403, and her remind on a request she did not submit answered 403. Her read of a request she is neither on nor submitted answered 404 (not disclosed).
- List door: an unknown param answered 400
VALIDATION_ERROR. An empty?status=answered 200 with the unfiltered list. That is an observation, not filed: an empty value reads as absent there, whilelimit/offsetrefuse empty. - Recipient scope: every
sys_inbox_messagerow visible to Mei and to Ada is addressed to that persona. Each was notified only for requests whose slate names them.
None of this found a filable defect.
Timing
Environment prep (install, framework build, console build, boot) took about 37 minutes. Driving the five items plus probes took about 18 minutes.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 29, 2026
Close-out (checklist-test §5) · 2026-10-29T02:14Z
5/5 PASS, with no FAIL, PARTIAL, BLOCKED or NOT-RUN. Both checklist-accuracy notes landed in PR #27164 (slice D, merged as
65354572b9), and the seat marked this record final (comments 6464451167, 6465372877). Closingcompleted.Generated by Claude Code