Skip to content

QA run · area:approvals (18/18) · 94114c1f · 2026-10-16 · 17 PASS / 0 PARTIAL / 1 FAIL / 0 BLOCKED / 0 NOT-RUN #23926

Closed

Description

@objectstack-fleet

Run record for the full-checklist wave anchored on #23878 (selector all, run per area). Runner session session_01K3qUoyh1xFGoWzAACPMbXJ, dispatched by the domain:devx seat 1 PM. Protocol: .claude/skills/checklist-test/SKILL.md + docs/qa/platform-checklist/RUNNER.md.

Environment fingerprint

  • framework: 94114c1f269f6b3d885f24b6f4689dce29504574 (origin/main at run start; pnpm install --frozen-lockfile && pnpm build, 75/75 green)
  • resolver: node scripts/checklist-select.mjs all at that sha → 272 runnable · 2 blocked hidden · 1 planned
  • .objectui-sha: daeb77fc84dd824e74c95b87cdede85afdb248e7. ⚠ pnpm objectui:build FAILS on this sha (Console pin does not build on main: objectui daeb77fc imports checkPageComponentViewData, deleted from spec by #23784 #23908). Browser/mixed verdicts ran on a console built at that same pin against @objectstack/spec from a35f7a67^ (the commit before the deletion), served from a linked worktree at 94114c1f with identical framework dists. check:console-sha matches.
  • app: showcase · boot OS_PORT=P objectstack dev --ui --seed-admin -p P -d file:…/data.db · per-runner port and DB · seeded admin + demo personas
  • browser: Playwright 1.63 on /opt/pw-browsers/chromium (Chromium 141), one context per persona
  • timestamp: 2026-10-16 (UTC) · runners:
  • G5 approvals: ports [41100] · sequential · env 10 min / test 47 min

Scope

Selector area:approvals (from the all resolution) · 18 runnable item(s):

  • approvals.per-group-signoff r6 · P1 · browser → pass
  • approvals.quorum-m-of-n r4 · P1 · browser → pass
  • approvals.inbox-metadata-actions r4 · P1 · browser → pass
  • approvals.viewer-gating-submitter-side r6 · P1 · browser → pass
  • approvals.notification-deep-link r6 · P2 · browser → pass
  • approvals.decision-action-matrix r4 · P1 · mixed → pass
  • approvals.decision-only-via-service r1 · P1 · api → pass
  • approvals.dynamic-approver-routing r2 · P2 · mixed → pass
  • approvals.ooo-delegation-reroute r5 · P2 · mixed → pass
  • approvals.record-page-decisions r1 · P2 · browser → pass
  • approvals.sla-escalation r3 · P2 · api → pass
  • approvals.account-app-entry r2 · P0 · browser → pass
  • approvals.setup-nav-entry r1 · P2 · browser → pass
  • approvals.pending-count-surfaces r1 · P2 · browser → pass
  • approvals.email-action-token-door r2 · P1 · api → pass
  • approvals.approver-resolution-matrix r2 · P1 · api → fail
  • approvals.status-mirror-field r1 · P2 · api → pass
  • approvals.inbox-keyboard-flow r1 · P2 · browser → pass

Per-clause verdicts

item # clause (short) verdict evidence
approvals.per-group-signoff 0 initial pending, both groups 0/1, two distinct ids pass GET EXP-2001: pending, pending_approvers=[admin,Ada], groups {admin:[manager],Ada:[finance]}, decision_progress finance 0/1 manager 0/1
approvals.per-group-signoff 1 after manager approval still pending on finance only pass drawer Approve (account-app inbox) POST /approve 200; re-read pending, pending_approvers=[Ada], manager 1/1 satisfied, finance 0/1
approvals.per-group-signoff 2 drawer group chips match API each stage pass drawer text 'finance 0/1 manager 0/1' then 'finance 0/1 manager 1/1' (Ada's drawer), each paired with identical API decision_progress
approvals.per-group-signoff 3 finance approval finalizes; run resumes pass Ada (own session) drawer approve 200 -> status approved; run paused->completed (end nodes log no step; reject edge not taken)
approvals.per-group-signoff 4 timeline one approve per group, distinct actors, comments pass /actions: approve by admin 'manager ok from drawer', approve by usr_showcase_auditor_demo 'finance ok (Ada)'
approvals.per-group-signoff 5 actor derived from session, actorId is a hint pass drawer POST body {comment} only, rows record session users; forged actorId=admin by Mei -> 403 'cannot act as ... recorded against the authenticated caller', request+actions byte-identical
approvals.quorum-m-of-n 0 clamp contrast on stock EXP-DEMO pass slate [admin x3], decision_progress need 1 ('Approvals — 0 of 1'); one approve -> approved, 1 approve row (recorded as contrast, not M-of-N proof)
approvals.quorum-m-of-n 1 approves exactly at minApprovals distinct pass runtime fixture (Ada+finance, 3rd user lee+legal, per-group flow toggled off): QA-Q1 slate 3 distinct; Ada approve -> pending 1/2 slate [admin,lee]; lee approve -> approved; 2 approve rows, 2 actors
approvals.quorum-m-of-n 2 single rejection vetoes pass QA-Q2: Ada approve (1/2, run paused) then lee reject -> rejected, slate empty, run paused->completed; later approve 409 INVALID_STATE
approvals.quorum-m-of-n 3 third approver's pending task closed pass after QA-Q1 approved admin no longer in pending_approvers; admin My Pending tab 2 -> 1
approvals.quorum-m-of-n 4 drawer tally server-computed each stage pass drawer 'Approvals — 0 of 2' / '1 of 2' paired with API got/need; subtitle '(2 of 3)' ignored
approvals.inbox-metadata-actions 0 approver actions render from metadata on other-submitter request pass lee (non-override) drawer on nina-submitted INV-QA-N1: Approve, Reject, Reassign, Send back, Request info, Reply
approvals.inbox-metadata-actions 1 remind own-only; recall own or override pass own (admin INV-QA-A1): Send reminder+Recall; lee on N1: neither; admin (override) on N1: Recall, no Send reminder
approvals.inbox-metadata-actions 2 gating mirrors server viewer flags pass lee {can_act:t,is_submitter:f,can_override:f}; admin on N1 {t,f,t}; admin on A1 {t,t,t}; matches declared visible CEL on sys_approval_request
approvals.inbox-metadata-actions 3 spot-checked actions POST and round-trip pass drawer approve EXP-DEMO POST /approve 200 -> approved; drawer Send reminder POST /remind 200 body {comment}, status stays pending
approvals.inbox-metadata-actions 4 one timeline row per action pass EXP-DEMO actions [submit]->[submit,approve]; A1 [submit]->[submit,remind] actor admin
approvals.viewer-gating-submitter-side 0 drawer omits approve/reject, keeps remind/recall pass Mei drawer on EXP-2001: Send reminder, Recall, Reply + 'only the assigned approvers can approve or reject'
approvals.viewer-gating-submitter-side 1 viewer flags explain absence pass Mei GET: can_act false, is_submitter true, pending_approvers [admin,Ada] excludes her
approvals.viewer-gating-submitter-side 2 forged decision rejected server-side; entitled approver accepted pass Mei POST /approve -> 403 FORBIDDEN; admin (entitled) drawer approve on same request -> 200
approvals.viewer-gating-submitter-side 3 forged call left no trace pass request read and /actions byte-identical (diff) before/after both forged POSTs
approvals.notification-deep-link 0 notification actionUrl carries ?request=id pass sys_inbox_message for admin topic approval.requested action_url /system/approvals?request=areq_14a23b01 (pending INV-QA-A2)
approvals.notification-deep-link 1 cold load opens drawer, twice pass two fresh-page cold loads of /_console/system/approvals?request= -> drawer INV-QA-A2 (first capture at 3.5s was skeleton; re-run with 6s settle both full)
approvals.notification-deep-link 2 drawer = API request pass drawer INV-QA-A2 / Invoice Dual Sign-off / Pending / waiting nina,pat,Dev Admin x2,lee == GET /:id
approvals.notification-deep-link 3 both routes open same drawer pass component route /apps/com.objectstack.account/component/approvals/inbox?request= -> same INV-QA-A2 drawer
approvals.notification-deep-link 4 unknown id degrades without another request pass bogus id -> toast 'This request no longer exists', empty drawer shell, no other request shown
approvals.decision-action-matrix 0 each of nine actions POSTs route + one timeline row pass approve(EXP-DEMO), reject, revise, resubmit, recall, reassign, remind, request-info(as Ada), comment each 200 and appended exactly one row naming actor+kind
approvals.decision-action-matrix 1 approve/reject finalize; run resumes matching branch pass EXP-DEMO approved run completed; QA-REJ rejected, run completed with steps [start,manager_review] (no needs_exec => reject edge)
approvals.decision-action-matrix 2 revise -> returned, run parked at wait_revision pass status returned; run paused steps [...,wait_revision]
approvals.decision-action-matrix 3 resubmit mints new round request pass after resubmit new pending id 8975eadf on same record, slate freshly resolved [admin]; Ada resubmit -> 403 only the submitter
approvals.decision-action-matrix 4 resubmitted request stays returned pass 96c22d31 still returned after round 2 opened; two returned siblings before round 3
approvals.decision-action-matrix 5 maxRevisions guard auto-rejects 3rd send-back pass 3rd revise on round-3 request -> rejected, row 'Auto-rejected: revision limit (2) exceeded'
approvals.decision-action-matrix 6 recall submitter-only -> recalled pass Ada recall 403 FORBIDDEN; admin recall -> recalled, gone from pending list
approvals.decision-action-matrix 7 non-finalizing actions mutate only what they own pass reassign slate [admin]->[Ada]; remind/request-info/comment: status+slate snapshot unchanged, one row each; 2nd remind 429 THROTTLED
approvals.decision-action-matrix 8 lockRecord both sides pass PATCH project under manager_review 200; under exec_review 409 RECORD_LOCKED
approvals.decision-action-matrix 9 org-less READ_BACK_FAILED loud refusal pass pinned describe '#12769' 6/6 passed (approval-service.test.ts); live wire envelope blocked(fixture): no org-less request on stock seeds
approvals.decision-only-via-service 0 generic resume answers 403 for approval pause pass POST /automation/showcase_invoice_signoff/runs/:id/resume {} and {branchLabel:approve} -> both 403 PERMISSION_DENIED 'only its owning service may resume'
approvals.decision-only-via-service 1 forged attempts change nothing pass run still paused [start,dual_signoff]; /actions still [submit]
approvals.decision-only-via-service 2 approvals decision resumes run pass admin approve -> approved; run completed steps start,dual_signoff,notify_cleared
approvals.decision-only-via-service 3 screen-node pause resumes via same route pass showcase_reassign_wizard paused at collect; resume {inputs:{new_assignee}} 200 -> completed, task assignee written
approvals.dynamic-approver-routing 0 typed multi user picker pass dialog shows 'Next Reviewers*' record picker (search popover of sys_users, checkmarks, '2 selected'), no free-text box
approvals.dynamic-approver-routing 1 outputless approve refused pass POST /approve without outputs (and with []) -> 400 VALIDATION_FAILED 'next_reviewers required'; still pending, no new row
approvals.dynamic-approver-routing 2 stage-2 resolves exactly the picked users pass picked [admin] -> co_sign pending_approvers [admin]
approvals.dynamic-approver-routing 3 cardinality/order tracks the pick pass 2nd chain picked [Ada,lee] -> co_sign pending_approvers [usr_showcase_auditor_demo, lee] same order
approvals.dynamic-approver-routing 4 stage 2 completes the run pass both co_sign requests approved; runs completed [start,lead_review,co_sign]
approvals.ooo-delegation-reroute 0 baseline resolves [A] pass runtime-registered user-routed flow (POST /api/v1/automation, no writable package needed): pending_approvers [admin], actions [submit]
approvals.ooo-delegation-reroute 1 active delegation reroutes to B + audit row pass pending_approvers [Ada]; ooo_substitute row actor null comment 'admin -> Ada — Annual leave'
approvals.ooo-delegation-reroute 2 delegate notified pass Ada sys_inbox_message topic approval.ooo_substituted action_url /system/approvals?request=areq_3a3ae680
approvals.ooo-delegation-reroute 3 delegate decides as self pass Ada own token approve 200 -> approved; approve row actor usr_showcase_auditor_demo via_override false
approvals.ooo-delegation-reroute 4 expired window routes back to A pass valid_until set past; new request pending_approvers [admin], no ooo_substitute row
approvals.record-page-decisions 0 Approve/Reject in header primary slot pass QA-RP-REJ record page header: Approve (primary) then Reject (destructive) ahead of Reassign/Send back/...
approvals.record-page-decisions 1 Reject fires after exactly one dialog pass 1 dialog with inline Comment textarea; 0 POSTs before Confirm; Confirm -> exactly one POST /reject 200, 0 dialogs after
approvals.record-page-decisions 2 decision POST + status + comment round-trip pass reject -> rejected row comment 'record page reject'; approve path one dialog -> approved row 'record page approve'
approvals.record-page-decisions 3 locking hides inline edit; non-locking keeps it pass exec_review (lock) page: no 'Double-click to edit', badge 'Locked for approval', Edit disabled; manager_review page: 10 'Double-click to edit' + 'In approval · editable'
approvals.record-page-decisions 4 no decision buttons when routed away pass Ada on QA-EXEC (pending to admin): header has no Approve/Reject; Ada on QA-NONFIN (routed to her): Approve/Reject present
approvals.sla-escalation 0 sla_due_at = created_at + timeoutHours pass 4 requests: created 12:52:13.364Z due 13:52:13.364Z etc. (+1h exactly)
approvals.sla-escalation 1 escalates exactly once pass after +2h shim offset + periodic sweep: one escalate row each, actor_id absent; after the next 5-min sweep still 1 row each
approvals.sla-escalation 2 per-variant effect pass reassign slate [admin]->[lee] + approval.escalated to lee; auto_approve approved run completed; auto_reject rejected run completed; notify approval.sla_breached to lee
approvals.sla-escalation 3 notifySubmitter honored pass admin (submitter) got 4 approval.sla_breached rows, body "escalation action 'reassign' was taken" etc.
approvals.sla-escalation 4 escalation config strict pass registration door POST /api/v1/automation: bogusKey -> 400 'Unrecognized key ... bogusKey'; alias timeout -> 400 (timeoutHours missing + timeout unrecognized)
approvals.account-app-entry 0 registration survives bundling pass dist/.objectui-sha = .objectui-sha = daeb77fc; index-pepWV_Ju.js contains Co({ref:approvals:inbox,label:Approvals Inbox...})
approvals.account-app-entry 1 entry renders full inbox pass nina clicks Account > Inbox > Approvals: lands /apps/com.objectstack.account/component/approvals/inbox; tabs My Pending 3/Submitted by me/All, 3 rows, hint bar
approvals.account-app-entry 2 non-admin reaches it pass nina meta/app = [showcase_app, account]; ?id=setup items []; account nav keeps nav_account_approvals
approvals.account-app-entry 3 decision under component mount moves request pass nina drawer approve INV-QA-N2 POST 200; tally 0->1 of 4, nina dropped from slate, approve row, action bar replaced by 'Only the assigned approvers can act...'
approvals.account-app-entry 4 ?request= deep link on component route pass cold load ...inbox?request=INV-QA-N1 id as nina -> drawer INV-QA-N1
approvals.account-app-entry 5 record link keeps current app pass row href /_console/apps/com.objectstack.account/showcase_invoice/record/e06dOnBivOTEDbv6; nina GET record 200
approvals.account-app-entry 6 zh-CN wording pass zh-CN session: nav 待我审批, tabs 待我审批/我发起的/全部, status badge 待审批
approvals.setup-nav-entry 0 first child is Approvals Inbox pass meta/app setup group_approvals: [component approvals:inbox, sys_approval_request, sys_approval_action, sys_approval_delegation]; sidebar DOM order identical
approvals.setup-nav-entry 1 opens same inbox component pass lands /apps/com.objectstack.setup/component/approvals/inbox; same tabs + process/object filters
approvals.setup-nav-entry 2 zh-CN labels and order pass 审批中心, 审批申请, 审批历史, 审批委派(外出) in that order
approvals.pending-count-surfaces 0 popover unclamped, badge clamps pass admin: bell badge '9+', popover '11 total · 5 notifications + 6 pending approvals'
approvals.pending-count-surfaces 1 M agrees across surfaces, before/after decision pass 6/6/6/6 (popover, Home '6 pending approvals', My Pending 6, API); after one approve 5/5/5/5
approvals.pending-count-surfaces 2 Home card navigates within an openable app pass admin and nina land /_console/apps/com.example.showcase/system/approvals; showcase_app is in both meta/app lists
approvals.email-action-token-door 0 minting approver-bound and hashed pass pins approval-token-internal-hash + sys-approval-token-generic-door passed; live remind on INV-QA-A1 -> 4 approval.reminder payloads, one Approve+Reject URL per concrete approver; GET data/sys_approval_token 404
approvals.email-action-token-door 1 GET confirm page mutates nothing pass two anonymous GETs: 200 text/html, POST form, 'Acting as lee'; request + actions identical before/after
approvals.email-action-token-door 2 one POST decides as the bound approver pass anonymous POST -> 'Approved · 已通过'; approve row actor lee, comment 'Via action link', tally 0->1
approvals.email-action-token-door 3 single-use consume-first pass replay POST -> 'Already used · 链接已使用', actions still [submit,remind,approve]
approvals.email-action-token-door 4 every dead-token shape is a designed refusal page pass invalid (GET+POST) 'Invalid link' no summary; consumed no summary; not_pending 'Already decided' with summary; not_approver 'No longer your approval' with summary; all 200, no delta; expired from pin
approvals.email-action-token-door 5 token grants nothing else pass Bearer raw token on /approvals/requests 401, /data/showcase_invoice 401, get-session 401
approvals.approver-resolution-matrix 0 each type resolves its declared slate pass user->[Ada]; field f_users [Ada,lee]->2 slots in order; manager(owner_id, admin.manager_id=lee)->[lee]; team QA Team->[nina,lee]; role/org_membership_level owner->[admin]
approvals.approver-resolution-matrix 1 department includes descendants; inactive skipped pass bu_field_ops -> [Ada(west_coast child), pat(direct)]; west_coast active=false -> [pat]
approvals.approver-resolution-matrix 2 multi-value field fans out pass pending_approvers [usr_showcase_auditor_demo, lee] matches f_users value
approvals.approver-resolution-matrix 3 deprecated role warns + resolves like org_membership_level pass WARN "approver type 'role' is deprecated (ADR-0090 D3) — author 'org_membership_level'"; both slates [admin]
approvals.approver-resolution-matrix 4 empty-slate fork honors policy fail fail/auto_approve/fallback/fallback-to-nobody as specified, but admin_rescue opened on [admin] (org administrators, #22824), not the literal slot the clause requires
approvals.approver-resolution-matrix 5 queue dead slot stays loud fail queue warn present and request pending, but slate is [admin] via admin_rescue, not the literal queue:support slot the clause requires
approvals.status-mirror-field 0 each transition stamps its value pass f_textarea: pending -> approved / rejected / recalled / returned; revise loop past maxRevisions -> rejected
approvals.status-mirror-field 1 mirror crosses lock by whitelist pass submitter PATCH while pending -> 409 RECORD_LOCKED; same record already f_textarea='pending'
approvals.status-mirror-field 2 mirror carries acting user pass pin status-mirror-cascade 2/2 + '#3783' describe passed; live updated_by: pending=admin(submitter), approve/reject/returned=Ada(decider), recalled=admin
approvals.status-mirror-field 3 no key never writes pass flow without approvalStatusField: f_textarea null at pending and after approve
approvals.status-mirror-field 4 failing mirror non-fatal and loud pass nonexistent field: approve 200, approved, run completed; WARN '[approvals] mirrorStatusField failed: Unknown field'
approvals.inbox-keyboard-flow 0 j/k/arrows move ring, clamped; Enter opens focused row pass seq j0 j1 k0 Down1 Up0 k0 k0; 12xj -> 6 of 6; Enter on ring 1 opened 'Q3 field-ops rollout' co-sign drawer
approvals.inbox-keyboard-flow 1 a/r open same dialog; one POST same route pass a -> alertdialog, 0 POST before confirm, POST /approve body {actor_id}; r on INV-QA-B2 -> POST /reject, request rejected, one reject row
approvals.inbox-keyboard-flow 2 ownership guards pass search: ArrowDown/Enter focus 0->0, 0 dialog, 0 POST; typed jka went into the input; dialog open: j focus unchanged; drawer open: j focus unchanged; Enter in dialog hit focused Cancel (no POST)
approvals.inbox-keyboard-flow 3 a/r gated by decisionOutputs pass dynamic stage-1 row: a and r -> 0 dialogs, 0 POST; row buttons disabled 'This approval collects decision outputs'
approvals.inbox-keyboard-flow 4 drawer arrows walk; x/space pending-tab only pass ArrowRight -> INV-QA-B2 (Request 3 of 7), ArrowLeft -> back; x checks row ('1 selected'), space unchecks; Submitted-by-me tab has 0 checkboxes, x inert

Reproduction rules (fails)

Derived item verdicts

17 pass · 0 partial · 1 fail · 0 blocked · 0 not-run (of 18).

Pin runs (Tier 1, RUNNER rule 6):

  • approvals.decision-action-matrix: cd packages/plugins/plugin-approvals && pnpm exec vitest run --maxWorkers=1 src/approval-service.test.ts src/approval-token-internal-hash.integration.test.ts src/sys-approval-token-generic-door.integration.test.ts src/status-mirror-cascade.integration.test.ts -> 4 files, 374 passed / 0 failed
  • approvals.decision-only-via-service: cd packages/services/service-automation && pnpm exec vitest run --maxWorkers=1 src/resume-authority-gate.test.ts -> 22 passed / 0 failed
  • approvals.email-action-token-door: covered by the plugin-approvals batch above (internal-hash 1/1, generic-door 6/6, 'redeem: dead tokens' passed)
  • approvals.status-mirror-field: covered by the plugin-approvals batch above (status-mirror-cascade.integration 2/2 passed)

Fixture gaps

  • approvals.decision-action-matrix clause 9: no org-less request on stock seeds — wire READ_BACK_FAILED envelope unobserved (scored from pin)
  • approvals.email-action-token-door clause 4: expired token leg has no live door (72h TTL, token API closed) — scored from pinned unit test
  • approvals.email-action-token-door clause 0: email-rendering leg needs the log mail transport; not exercised

Checklist-accuracy findings from this area close out on the wave anchor #23878; they are not extracted.

Activity

  1. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Close-out for this record (checklist-test §5), 2026-10-16T19:18Z. 17 pass · 0 partial · 1 fail of 18. The one fail comes from two stale-spec clauses: the product holds and the item pins the pre-ruling shape. Both are extracted to the re-point family card:

    Extracted: #23950 · approvals.approver-resolution-matrix · clause 4
    Extracted: #23950 · approvals.approver-resolution-matrix · clause 5
    

    Ruling #22824 (pinned by admin-rescue-named-slate.integration.test.ts) sends an empty slate under admin_rescue to the organization's administrators, where the clauses expected the literal position:qa_nobody / queue:support slot. PR #24000 (merged 2026-10-16T18:02Z) re-pointed both clauses (item r3) and closed #23950. Its re-run record #23997 passes them. The fail, auto_approve and fallback branches behave as specified. Fixture gaps in this record (decision-action-matrix c9, email-action-token-door c0/c4, each scored from its pin) close out on #23878. Closed by the domain:devx seat 1 PM (seat post #6023) · os-sales · session_014eFvbvGK4FjXvscggDNTD4, as part of the paced close-out of the #23878 wave.


    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