Skip to content

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

Closed

Description

@objectstack-fleet

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 (current origin/main at boot). The console was built from ui/ in the same tree with pnpm build --concurrency=2 (116/116 tasks, 0 cached), then pnpm console:build --concurrency=2. check-dev-prereqs exit 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 under setsid and stopped through its own process group. The banner read 32 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 password showcase123.
  • 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 in bu_east_coast (a child of bu_field_ops), via POST /api/v1/data/sys_business_unit_member → 201.
  • Team CbEbrsrhMgxvyLo5vEBR8Ci3GCfSbczO holding Ada and Mei, via create-team / add-team-member → 200.
  • Manager: Mei → Ada, via set-user-manager → 200.
  • Queue qa_matrix_queue with Ada and Mei as members, and qa_empty_queue with none, via sys_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)

  1. approvals.dynamic-approver-routing knownGaps[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 a variables key: GET /api/v1/automation/showcase_dynamic_approval/runs/RUNID on a stage-1-paused run answered status: paused with variables present ($record, record, …). The suspend path's recordLog writes variables: variablesSnapshot (packages/services/service-automation/src/engine.ts, the status: 'paused' recordLog). The clause's functional oracle still holds and passed. Only the stated reason for it is out of date.
  2. approvals.notification-deep-link steps[3] says "APP is setup on the bare /system/approvals route". Measured, the landing segment is com.objectstack.setup (and com.objectstack.account on 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_FOUND for 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, while limit/offset refuse empty.
  • Recipient scope: every sys_inbox_message row 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

  1. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Close-out (checklist-test §5) · 2026-10-29T02:14Z

    Parked-on: #25807 · approvals.dynamic-approver-routing acceptance[3] new leg, written from source, owed to the next approvals run (comment 6465372877)
    

    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). Closing completed.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions