Skip to content

QA run · studio-authoring (FULL area) · 92f26f75 · 2026-08-11 · 3 PASS / 2 PARTIAL / 7 FAIL #7695

Description

@baozhoutao

Full studio-authoring area run of the checklist-test skill — all 12 items driven against a live showcase across 3 opus subagents, each on its own port + file DB. Text-only per RUNNER.md.

Result: 3 PASS · 2 PARTIAL · 7 FAIL — the weakest area of the sweep.

⚠️One security-sensitive finding from this area is NOT described here. It surfaced inside expression-editors and was delivered privately as FOLLOW-UPS.md D13, per the maintainer ruling on #7463. Its companion defect — a loud, safe refusal — is described in full below.

Environment — framework 92f26f75/98e5d655 · console bundle 09987b680 (packages/console/dist/.objectui-sha; the repo-root pin 6314e87f2 is newer and not yet built into dist) · showcase app · isolated port + file DB per batch · writable runtime packages for all authoring, no repo file and no shipped page/object mutated.

✅ The P0 first-run loop is green

first-run-loop (6/6 + both negatives) — the whole zero-code path (package → object → record → app → end-user list) completed inside one server boot: the boot log has exactly one "HTTP server started successfully", zero shutdown lines, and the same pid throughout, with only client page loads. A new package appears in the switcher with no reload (repeated with a throwaway package, full-page-load count 0). The published picklist carries exactly the three authored options and the end-user list renders the label ("In Progress"), not the raw value. Forged writes into the read-only package are refused at the server: 422 WRITABLE_PACKAGE_REQUIRED for a new object, 403 NOT_OVERRIDABLE for a non-destructive edit (with and without ?package), 409 DESTRUCTIVE_CHANGE for a destructive one.

knownGap 6 no longer holds: the Create-app dialog now ships a checked "Add this package's objects as menu items", and the create PUT already carried the nav item — the manual Interfaces wiring the item describes is not needed.

object-designer-roundtrip (4/4) — both the picklist-label rename and the field reorder survive a second publish in the persisted metadata (key order and options diffed across two GETs), not just the canvas. Audit finding 2 (API name frozen at field_N) does not reproduce — label blur derives the name and the stored column proves it. The automated.ref was run: 3 of 4 pass; the 4th fails only because its route is stale (package-create moved to /_console/studio), and that contract passes on the current surface.

custom-page-source-tiers (6/6 + all four negatives) — parse-never-execute proven negatively: the html page's KPI tiles are authored sample copy (128 against a real total of 11) and the full request log for that page load contains zeroshowcase_*/analytics calls, so no author JS ran. The react tier by contrast is genuinely data-driven with executed handlers. The OS_PAGE_REACT gate was exercised both ways — with it off, react pages render only the named CapabilityDisabledNotice and the faulted react page does not even reach its runtime error, proving the source was never evaluated, while the html tier is unaffected. Both fault classes produce named panels; an unregistered tag is refused at compile ("is not an allowed component"), so the whitelist gate holds.

🔴 FAIL — 7 items

1. view-authoring-live — the documented view-container authoring path is inert at runtime

Reproduction rulePUT /api/v1/meta/view/<name>?mode=draft&package=<writable> with the ViewSchema container shape ({object, list:{…}, form:{…}}) → 200 state:'draft'; POST …/publish → 200. Then GET /api/v1/meta/view?object=<obj>.
Expected the authored views. Actual{"items":[]} — while GET /api/v1/meta/view/<name> returns 200 with the full authored body, so the item is stored, just never served. The end-user list keeps rendering synthesized default columns. A deliberate full server restart does not help — it is inert, not restart-required. Reproduced on two objects and with the container stored under both an arbitrary name and the object name.

Control that isolates it: publishing the same list and form bodies as pre-expanded ViewItems ({name, object, viewKind, config}) makes the list columns and the authored form section render live after a client reload only.

Root cause (located). View-container expansion runs only on the artifact/package boot-load pathpackages/metadata/src/plugin.ts (~643-668), iterating ARTIFACT_FIELD_TO_TYPE over the builtobjectstack.json. The runtime metadata write door stores the container verbatim and never calls expandViewContainer, and MetadataManager.getViewsByObject() (metadata-manager.ts:1432-1444) filters on v.viewKind — which a container does not carry.

Draft isolation is clean (pre-publish both consumer doors report absent; only ?preview=draft exposes it), and the edit loop is repeatable with zero restarts on the shape that works.

2. authoring-validation-not-persisted — the flat view container is accepted silently

Reproduction rulePUT /api/v1/meta/view/<n>?mode=draft with a flat body {name, type:'grid', columns:[…], data:{…}}200state:'draft' (and 200state:'active' on the direct door), publishes 200, and reads back with _diagnostics:{valid:true}. Reproduced on 3 names.

The guidance exists and fires elsewhere: defineView(body) on the same body throws "Unrecognized key(s) on this view container: type, columns, data. • type belongs to a single VIEW, not to the container. Wrap it: defineView({ list: { … } })".

Root cause.getMetadataTypeSchema('view') resolves to the 4-member wire union (view.zod.ts ~2941) whose "inline view config" arm accepts the body, while the guidance-carrying strictViewSchema (line 2358) is only on the defineView/build path. Verified by hand: ui.ViewSchema.safeParse(body).success = false with the guidance; getMetadataTypeSchema('view').safeParse(body).success = true.

Consequence measured:expandViewContainer(...)[] and GET /meta/view?object=… omits it, while GET /meta/view lists it as a bare unbound row — registered, reported valid, renders nothing. That is precisely the outcome the strict container shape was closed to prevent.

The inline arm looks deliberate (#6391/#5599/#5074 notes), so the fix may be to re-word the item or to make that arm require an object binding. The defect as measured is that the door accepts metadata it then cannot expand. Everything else in the item passes, including a located Studio Source-editor banner ("Fields → amount → type: Invalid option…") — not a silent dead Save.

3. org-override-registry-gate — the field overlay lock is not enforced

Reproduction rulePUT /api/v1/meta/field/showcase_task.title {name:'title', label:'Tampered', type:'text'} with an admin bearer → 200state:'active', though the registry declares field allowOrgOverride=false and the field is artifact-backed. Reproduced on showcase_task.status.

The row is persisted and readable with _diagnostics.valid=trueand the accepted write is also inert: GET /meta/object/showcase_task still reads title.label = 'Title'. A brand-new field written the same way never appears in the object's fields either.

Suspected root cause. The gate keys on isArtifactBacked(type, name)registry.getArtifactItem('field', 'showcase_task.title'), but fields are not registered as standalone field artifact items (they live inside the object), so the lookup misses, the write is classified as a runtime-only create, and field.allowRuntimeCreate=true lets it through.

Why the pin stayed green:overlay-precedence.test.ts (27 passed) pins the protocol-level denial; the live field route is not in its coverage.

The object, view, dashboard and job variants all behave correctly, and locked-override vs open-create were proven independent in the same run. Note a trimmed object body hits DESTRUCTIVE_CHANGE first — the full body is needed to reach the registry gate.

4. expression-editors — Save is not gated on CEL errors, and RLS policies can't be saved at all

The editor/engine parity this item exists for is clean. Nine probes compared the editor's verdict against the framework's own @objectstack/formula for the same expression and site — type inference (Number / Unknown with the double()/int() hint), unknown-field with did-you-mean, bare-ref with the record.<field> fix, parse faults, the RLS pushdown warning, and the test-run allow/deny/non-boolean triple — with no divergence in tier or text. Completion is root-scoped (record./previous. offer the field catalog; current_user., parent. and a bare prefix offer 0). Automated refs: 34 + 32 tests green, and all five CEL sources are md5-identical between the served pin and objectui HEAD, so these verdicts apply to the running bundle.

(a) A parse-fault formula saves and publishes. With record.est_hours * in the formula box the inline error shows, but "Save draft" stays enabled; clicking it PUTs → 200 with a success toast, and publishing makes the malformed expression the live field definition (GET reads back expression: 'record.est_hours *'). Reproduced twice. Root cause:ObjectFieldInspector.tsx mounts CelPredicateField for the formula (line 478) and the three conditional rules (615/628/641) without any onCelErrorsChange prop, while PermissionAdvancedFacetsis given onCelErrorsChange={setCelErrorCount} — which is why the RLS editor correctly disables Save with "Fix the CEL syntax errors before saving."

(b) The console emits the retired priority key, so the environment door 422s. A new policy is seeded with rowLevelSecurity[0].priority = 0 (PermissionAdvancedFacets.tsx:368), and the server answers a located 422: "rowLevelSecurity[].priority was removed in @objectstack/spec 17.0.0 (#3896 security audit)… Delete the key". The identical body withoutpriority is accepted 200 and reads back with the policy intact.

⚠️There is a second half to the RLS story on the package door, delivered privately as D13. Taken together, an RLS policy cannot currently be saved from the console on either door. The refusal above is the safe failure of the two.

5. record-page-roundtrip — Studio page-create (already filed) plus two new facts

Clause 0 fails on the already-filed create-seed defect (cited, not re-filed): the create PUT seeds recordChrome and items, which ADR-0089 D3a strictly rejects → 422, nothing stored, reproduced twice. Two additional facts not in the filed issue: the create form's Page type defaults to list, so the item's steps as written never produce a record page at all (and the list seed genuinely isregions: []); and the record seed contains no record:highlights block even before the rejection. #1541 is not regressed for a record page — the seed is non-empty.

Clauses 1–3 pass against a spec-valid fixture PUT through the API, and the authored page provably drives the end-user render — proven with a differential (re-authored highlights fields plus a "QA CARD MARKER" card title, after which the same record rendered the new field and the marker), plus a control object with no authored page rendering the default.

6. draft-publish-lifecycle — the audit trail misses publish and rollback

Reproduction rule — stage a draft, publish, roll back (all 200, all with X-Actor), then GET /meta/<type>/<n>/audit.
Expected rows for save, publish and rollback. Actual only save rows — Counter({'save': 5}) after 3 publishes and 2 rollbacks. Reproduced twice on two actors. The 409 METADATA_CONFLICT denial is likewise absent.

Root cause (source-confirmed).protocol.ts writes an allowed-outcome audit row at exactly two sites — line 9833 (save) and 12429 (delete). publishMetaItem and rollbackMetaItem call recordMetadataAuditonly through assertLockAllowsWrite(), which records on the deny path and returns null on allow — so a successful publish or rollback never writes a row.

Secondary:rest-server.ts computes the actor as x-actor ?? req.user?.id ?? req.userId, but req.user/req.userId are unset on the /meta PUT path — so a bearer-authenticated admin save is stamped actor:'system' (audit) / null (history) unless X-Actor is sent by hand.

Everything else passes: draft staging, the publish flip, OCC (409 with a passing control), package-wide publish-drafts atomicity (a namespace-gate abort leaves both names 404), the #4463 draft→active gate, the Studio unpublished badge + Changes counter, /history carrying both publish events, and rollback flipping the served body. Clause 8 is partial: the diff isolates the changed key but only to top-level widgets (from/to carry the whole array), and the natural previous-vs-current query comes back empty because the publish event shares the prior hash.

7. custom-page-render-and-blocksmy-work's personal queue silently ignores its owner filter

All four shipped pages render clean with every declared block composed (block-by-block DOM inventory, not just absence of errors); KPI tiles are server-computed and reconcile with the data plane; all five chart SVGs are sized with multi-bucket data cross-checked against the dataset aggregate API.

Reproduction rule — create a task owned by another user, then open /_console/apps/showcase_app/page/showcase_my_work with the network recorder on.
Expected$filter=[["owner_id","=","<signed-in id>"]]. Actual the wire carries no $filter at all, and the grid lists every row including the foreign-owned probe. Reproduced twice with fresh logins.

Root cause — a key-name mismatch.my-work.page.ts:52 authors the pluralfilters: while objectui's ObjectGrid reads only schema.filter (or legacy defaultFilters); filters is read nowhere. PageComponentSchema.properties is a passthrough z.record, so the spec accepts the typo silently. Contrast on the same run: active-projects' interfaceConfig.filterBydoes reach the wire. Not a stale bundle — origin/main still reads only schema.filter. filters appears exactly once in the whole showcase; every other page uses filter.

Scope, stated precisely: this is not an authorization bypass. The unfiltered read is still RLS-constrained, so the caller sees only rows they may see (admin sees all because admin may see all). The defect is that a declared personal-scope filter silently never applies — and that a typo'd key is accepted without complaint. The second half is the more general problem.

🟡 PARTIAL — 2 items

page-variables-and-actions — blocked on the stale vendored bundle, deliberately not failed

Page variables work: initialized empty and gating dependent blocks on first render; a ref-targeted picker selection flips the gate live with 0 main-frame navigations; one text input's write drives another component's visibility; a reload with no selection re-initializes empty.

Clauses 4–5 are blocked(dependency): the submit POSTs a literally empty body because bodyExtra is dropped by ElementButtonRenderer's key whitelist at the served pin 09987b680 (built 2026-08-09 02:35) — while objectui origin/main forwards it via 7e5bb5d4e"fix(actions): forward bodyExtra end-to-end through the action chain (#3924)", landed 2026-08-09 05:38, i.e. after the pin. git merge-base confirms the pin is an ancestor of the fix, and the repo-root pin already contains it. The server half is proven healthy independently — a real POST created a showcase_inquiry with source:'web', status:'new'. Re-run after the vendored console is rebuilt.

permission-matrix-editor-ux — four clauses blocked on the already-filed field-table defect

The field sub-table renders no fields for any object, so its >6-field filter, the filter-scoped bulk and Clear cannot be exercised live. The runner confirmed the stated root cause first-hand at the served pin and proved it is not merely the read-only gate: after rebooting with OS_METADATA_WRITABLE=permission the editor became fully writable (Save present, all 864 checkboxes enabled) and the field sub-table still said "No fields registered for this object."

Passing: the B3 responsive clause is genuinely fine — measured at a 720px viewport the matrix is a 960px min-width table inside an overflow-x:auto container (scrollWidth 960 > clientWidth 720) and the Bulk column becomes fully visible after scrolling, not clipped. The read-only badge tracks the real writable computation, proven both ways.

Corrections and notes

  1. Correction to an earlier run card (QA run · access-security (FULL area) · 92f26f75 · 2026-08-11 · 8 PASS / 2 PARTIAL / 8 FAIL / 1 BLOCKED #7637). With OS_METADATA_WRITABLE=permission set, the Studio read-only badge does clear on this build/pin (GET /meta reports permission {allowOrgOverride:true, overrideSource:'env'}) and the editor becomes fully writable. The earlier observation that the badge asserts a lock the server is not applying does not reproduce here.
  2. Ledger note:publish-drafts' namespace-gate abort answers HTTP 200 with data.success:false rather than a 4xx.
  3. Route note: a flow authored over the metadata API is not editable in Studio (it opens behind "provided by an installed package") because ResourceEditPage treats an item as artifact-backed when layered.code != null && _packageId !== 'sys_metadata' and flow declares allowOrgOverride:false. The editable path is the console's own create page.
  4. Known spurious banner (not re-filed): a freshly saved valid draft shows "Unrecognized key(s) on this object: _diagnostics" because the designer re-validates the server's own annotation on read-back.
  5. The metadata-admin designer declares an object "provided by an installed package, so it is read-only at runtime" even for objects published into a writable package, while the Studio pillar treats them as editable and the server accepts the PUT.
  6. Two automated.ref specs are stale rather than red:studio-record-page.spec.ts fills the Object control as an <input> when it is now a role=combobox button, and studio-editor.spec.ts targets a "Layout" heading that no longer exists plus a shipped page the editor correctly locks. Both need re-pointing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingqa-run

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions