Skip to content

QA run · records-forms (FULL area) · e4e5c6e3 · 2026-08-17 · 2 PASS / 10 PARTIAL / 1 FAIL #9351

Description

@os-zhuang

Round R3 of the #9296 QA wave. One selector, one run, one issue.

Environment fingerprint

readingvalue
framework shae4e5c6e3c608b1b807c83a0d5b734f213eb1a1dd (2026-08-17 11:29:50Z)
.objectui-sha (pin)665661ab093263f39f2e660a295ea615dbcee35a
packages/console/dist/.objectui-sha (stamp)665661ab093263f39f2e660a295ea615dbcee35astamp == pin, pnpm check:console-sha exits 0
appshowcase (examples/app-showcase), 23 objects / 130 seed rows / 48 plugins
ports3200 (boot smoke), 3201 (area run) — band 3200–3299
dbfile:/tmp/qa-r3a/smoke.db, file:/tmp/qa-r3a/rf.db (fresh, rm -f before boot)
seed--seed-adminadmin@objectos.ai / admin123
runnerR3 runner session, 2026-08-17

Boot smoke before the first item: /api/v1/health → 200, /_console/ → 200.

Console build note (#9307, worked around, not re-diagnosed):pnpm objectui:build on a cold tree was preceded by pnpm exec turbo run build --filter=@objectstack/client... (32 tasks, 6m49s) per the standing workaround. One deviation worth recording for the next runner: the client closure does not include @objectstack/cli, so the first objectstack dev failed command dev not found; a second turbo run with --filter=@objectstack/cli... --filter=@objectstack/example-showcase... was needed before the app would boot. objectui:refresh was never run.

Spec-forward step (#9309)

git checkout origin/main -- docs/qa/platform-checklist/ leaves the tree clean — the validator still reports 15 areas, 182 items (182 active); coverage: 28 kinds mapped, 2 waived, because PR #9309 has not merged (open, mergeable_state: blocked).

Rather than block the round on it, I checked whether it could affect this area. It cannot: #9309's diff touches FOLLOW-UPS.md, SWEEP.md, coverage.json and the access-security / api-backend / cli / integration-system / platform-core area files only — records-forms.json is not in the PR at all, and every area-file change is +N −0. So the records-forms spec is byte-identical before and after #9309, and no verdict below can be invalidated by its merge. Nothing was committed.

Scope

Selector area:records-forms, resolved deterministically with node scripts/checklist-select.mjs area:records-forms --json32 runnable items (the resolver excludes 1 blocked item by design). Per-item revision is pinned in the table below.

Item verdicts

itemrevPverdict
crud-roundtrip3P0fail
validation-rule-type-matrix1P1pass
named-import-mapping1P2pass
encrypted-field-behavior1P1partial
field-type-matrix2P1partial
field-type-constraints1P1partial
object-hook-lifecycle1P1partial
cascading-multilevel-and-clear1P1partial
concurrent-edit-conflict1P1partial
related-list-server-pagination3P1partial
list-view-capabilities1P1partial
action-param-widgets2P1partial
view-type-gallery1P1partial
import-wizard-encoding-and-hints2P2blocked(dependency)
remaining 18 items (listed below)not-run

1 FAIL · 2 PASS · 10 PARTIAL · 1 BLOCKED · 18 NOT-RUN.

⚠️This round did not complete the area. 18 of 32 items were not driven at all, and most partial verdicts are partial because their screenshot/dom clauses were not driven. This is recorded as not-run, not as pass — see Coverage honesty at the end. The area needs a second round.

Per-clause verdicts (text oracle evidence)

records-forms.crud-roundtrip rev 3 (P0) — fail

#oracleverdictevidence (one line)
0apipassPOST /api/v1/data/showcase_account {name:'os-qa-r3a1',industry:'finance',status:'active'} → 201 id P9TKAGEy_e0a6jwL; filtered re-read returns the row with all three values verbatim and website: null (empty optional not defaulted)
1apipassfull-row GET before/after PATCH {annual_revenue:12345.67} differ in exactly annual_revenue (null→12345.67) and the audit column updated_at; all 17 other columns byte-identical
2apifailDELETE /api/v1/data/showcase_account/<id>400 INVALID_FILTER; the row survives ($filter=[["name","=","os-qa-r3a1"]] still total:1). Reproduction rule below
3screenshotnot-runbrowser clauses not driven this round
4domnot-runbrowser clauses not driven this round
5apipassCJK create os-qa-r3a1-华宁 reads back byte-identical; $search=华宁 → 2 rows incl. the seeded 华宁科技; $search=huaning (pinyin, ADR-0061) → same 2 rows
6apipartialclone → 201 with id: avyn_0XMDYomabbesourceId: P9TKAGEy_e0a6jwL; business values (industry,annual_revenue) copied, name override applied, created_at/updated_at re-derived, owner_id = signed-in cloner. Not discriminating: on this fixture the cloner is the source owner, so "owned by the CLONER, not the source's owner" is not falsifiable without a second identity
7apinot-runneeds a persona with no read on the source row; no second auth account provisioned this round → fixture gap

Reproduction rule — clause 2 (P0)

Any showcase_account cannot be deleted at all. Purely schema-driven; no particular data required.

POST /api/v1/data/showcase_account {"name":"anything","status":"active"} → 201, id
DELETE /api/v1/data/showcase_account/<id> → 400 INVALID_FILTER

Expected: 200 and the row gone. Actual: 400, body code: "INVALID_FILTER", message:

The bare equality spelling { "f_lookups": value } WAS NOT APPLIED: "f_lookups" is a multi-value (or otherwise JSON-valued) field, stored by this driver as a JSON TEXT column … Use "$contains"

Mechanism (read at the pin, packages/objectql/src/engine.ts:10113, inside cascadeDeleteRelations):

dependents=awaitthis.find(childName,{where: {[fieldName]: id}, context }asany);

The dependents probe builds a bare equality filter for every lookup/master_detail field pointing at the object being deleted — including fields declared multiple: true. showcase_field_zoo.f_lookups is Field.lookup('showcase_account', { multiple: true }), stored as a JSON array column, so the driver correctly refuses that spelling with INVALID_FILTER. The probe's catch (engine.ts:10147) is if (isMissingTableError(error)) continue; throw error; — an INVALID_FILTER is not a missing-table error, so it propagates and the whole delete fails.

Discriminating controls run:

controlresultwhat it rules out
repeated on 3 distinct fresh ids400 every timeflake
DELETE showcase_announcement/<id>200"DELETE route is broken"
DELETE showcase_category/<id>200ditto — f_tree is type tree, not scanned by the cascade
deleted allshowcase_field_zoo rows (→ 0 rows), then retriedstill 400data-dependence; the refusal is schema-driven, not row-driven

Automation self-check: driven by direct HTTP with no body and no query string — the offending filter is server-constructed, and f_lookups is not a field on showcase_account and was never named by the driver. No browser, so hydration-race / automation-input / stale-console-bundle are all inapplicable. Independently re-derived from the captured evidence alone before writing (RUNNER rule 7).

Blast radius: every object that is the target of any multiple: true lookup from any registered object is undeletable over REST. On stock showcase that is showcase_account.

Related, and the reason this surfaces now:#8895 (merged 2026-08-16, the day before this pin) replaced catch { continue } with discriminate-or-propagate. That hardening is correct on its own terms — but the probe's filter spelling was never fixed for multi-value reference fields, so a condition that used to be swallowed (while silently skipping that relation's integrity guard, exactly #8895's complaint) is now a hard refusal. I did not bisect this, so treat the causal link as a strong inference from the source at the pin, not a proven regression point.

records-forms.validation-rule-type-matrix rev 1 — pass (4/4)

#oracleverdictevidence
0apipassall six ledgered codes proven individually, none inferred from a sibling — invalid_format (tax_id:'BAD' → 400, fields[0].code=invalid_format), invalid_format again on the named email branch (billing_email:'not-an-email'), json_schema_violation (support_config:{tier:12345}), invalid_json (support_config:'{not valid json'), rule_violation (conditional: status:'churned' with no churn_reason), invalid_transition (PATCH status active→prospect), invalid_initial_state (POST showcase_project {status:'completed'}; initialStates:['planned'] with events:['insert','update'])
1apipassfields[].field targets the declared field per variant: tax_id, billing_email, support_config, status, status. The conditional rule reports field:"_record" — recorded as observed; the clause names only cross_field and format targeting explicitly, so this is not scored against it
2apipasspost-rejection filtered re-reads for vr-format / vr-email / vr-json / vr-cond all total: 0 — no rejected write persisted
3apipassevery happy-path twin lands 2xx: tax_id:'12-3456789' 201, billing_email:'a@b.com' 201, support_config:{tier:'premium',seats:5} 201 (stored verbatim), churned+churn_reason 201, prospect→active 200, project status:'planned' 201

Note for the next runner: my first json_schema happy twin used tier:'gold' and was correctly rejected — the schema allows standard|premium|enterprise. That was a driver error, not a product finding.

records-forms.named-import-mapping rev 1 — pass (3/3)

#oracleverdictevidence
0apipassPOST /api/v1/data/showcase_inquiry/import {mappingName:'showcase_inquiry_feed',format:'csv',csv} → 200, created:1. Row reads back name:'QA Importer' (from Full Name), email (from E-mail), company (from Company), message (from Message), and source:'website' — the map transform translated the source system's Channel:'Webform' code. Foreign headers, not positional guessing
1apipassre-POSTing the identical file → created:0, updated:1, ok:1; the filtered re-read stays total:1 — upsert on the mapping's upsertKey:['email'] is idempotent
2apipassmappingName:'no_such_mapping'404 MAPPING_NOT_FOUND, No mapping artifact named "no_such_mapping" is registered; zero rows written — no silent positional fallback

Note: the request key is csv, not data (packages/rest/src/import-prepare.ts:314). My first attempt sent data: and got a 400 INVALID_REQUEST — driver error, not a finding.

records-forms.encrypted-field-behavior rev 1 — partial (4 pass, 1 not-run)

#oracleverdictevidence
0apipassafter POST {f_secret:'topsecret-value'}, direct sqlite read of /tmp/qa-r3a/rf.db shows the business column holding secret:sec_6ec921a6a73268389a6b53c6ce4d0883 (SECRET_REF_PREFIX), and sys_secret holds 1 ciphertext row; a full-text scan of that table for topsecret-valuefalse. No API read echoed the written value
1apipassGET of the record returns f_secret: '••••••••' and f_password: '••••••••'; scan of the whole response for either plaintext → false. (f_passwordis plaintext at rest in the business column — which the item documents as correct for the generic password type: plaintext-at-rest, masked-on-read)
2apipassPATCH {f_secret:'••••••••'} (the mask echoed back) → 200; the stored ref is stillsecret:sec_6ec921a6… on the post-PATCH sqlite read — the echoed mask was dropped, not written. The secret was not wiped
3apipassvia the unit pin the item itself nominates: packages/objectql/src/secret-fields.test.ts green (see test run below). The stock boot logs CryptoProvider configured for secret fields, so the unwired-boot half is a unit-pin verdict by the item's own instruction
4apinot-runcould not establish the correct door. $groupBy/$aggregate on /data → 400 UNSUPPORTED_QUERY_PARAM (not supported params there at all); /api/v1/analytics/query requires a declared cube and string measures, which stock showcase does not expose for showcase_field_zoo. Recorded as not-run rather than guessed — the inference-oracle guard is unverified this round

records-forms.concurrent-edit-conflict rev 1 — partial

#oracleverdictevidence
0apipassA PATCH with fresh If-Match → 200; B PATCH with the now-stale If-Match409 CONCURRENT_UPDATE, body names both versions (current 2026-08-17T15:42:44.076Z, expected …43.892Z) and carries currentVersion. Independent re-read shows A's value (111) intact — no silent last-write-wins
3apipassthe overwrite retry re-keyed to the 409-reported currentVersion → 200; final re-read is exactly one winner (222)
1,2,4,5dom/api/networknot-runconflict dialog, Reload-discards, and the inline two-surface single-PATCH clauses are browser-driven; not driven this round

records-forms.related-list-server-pagination rev 3 — partial

#oracleverdictevidence
1networkpassNorthwind's children: server total: 26. Walked $top=10 / $top=10&$skip=10 / $top=10&$skip=20 → page sizes 10/10/6; pairwise intersections empty; union = 26 = total. Deterministic partition, no row repeated or skipped
0networkpartialthe server side is proven ($top/$skip scoped by the parent filter answer correctly); the "no child-list request without $top anywhere on the page" half needs a browser network capture — not driven
2,3,4,5dom/apinot-runrelated-list rendering, authored columns, row navigation, and the both-ends read gate not driven

Items carried by automated pins (RUNNER rule 6)

All ten pin files below were run at this pin and are green. Cited as evidence rather than re-driving.

pnpm --filter @objectstack/dogfood exec vitest run \
test/field-zoo-roundtrip.dogfood.test.ts test/field-zoo-value-shape.test.ts \
test/showcase-search.dogfood.test.ts test/action-params-contract.dogfood.test.ts \
test/showcase-public-form.dogfood.test.ts test/hook-error-format.dogfood.test.ts
→ Test Files 6 passed (6) · Tests 107 passed (107)
pnpm --filter @objectstack/objectql exec vitest run \
src/secret-fields.test.ts src/engine-lookup-referential-integrity.test.ts \
src/engine-autonumber-runtime-owned.test.ts src/validation/rule-validator.option-visibility.test.ts
→ Test Files 4 passed (4) · Tests 101 passed (101)
itemrevclauses carriedstill not-run
field-type-matrix2c0 (HTTP round-trip matrix, test oracle) pass; c3 (credential masking, masked vectors) passc1 the 49-row variant→widget DOM table; c2/c4/c5/c6 not separately driven
field-type-constraints1round-trip + masking via field-zoo-roundtrip; reference_not_found (#4441) via engine-lookup-referential-integrity; autonumber strip (#5503) via engine-autonumber-runtime-ownedthe remaining per-constraint API probes not driven individually
object-hook-lifecycle1the beforeInsert stamp and the beforeDeleteonError:'abort' → REST error body, via showcase-public-form + hook-error-formatthe four log-oracle clauses (transition audit, over-budget warn, priority ordering) — the item's own automated.ref says these are not pinned and must be hand-driven
cascading-multilevel-and-clear1the server WRITTEN-value gate via rule-validator.option-visibilitythe client clear-on-change half (objectui e2e)
list-view-capabilities1CJK/pinyin $search recall via showcase-search (and independently re-confirmed live, see crud c5)10 of 11 capability variants
action-param-widgets2the params contract via action-params-contractthe two dom widget clauses

records-forms.view-type-gallery rev 1 — partial

Clause 5's named pin is pnpm -C examples/app-showcase test:smoke. It ran, its [WebServer] booted cleanly (✓ Server is ready, 47 plugins, console mounted), and then all 34 specs failed in 3–5 ms each — an instant failure with no test body executed. Cause, read off the log:

Error: browserType.launch: Executable doesn't exist at
/opt/pw-browsers/chromium_headless_shell-1234/chrome-headless-shell-linux64/chrome-headless-shell

This container ships chromium revision 1194; @playwright/test@1.62.1 (the version the showcase pins) wants 1234. That is the documented remote-environment caveat, and playwright install is explicitly not to be run here. So this is blocked(environment) for the clause — not a fail, and emphatically not evidence that any view surface is broken: no surface was ever loaded. The remaining clauses (per-variant visualization screenshots, kanban aggregate, temporal placement) were not driven.

To be precise about what this does and does not block: I verified that Chromium does launch here when given the path explicitly —

chromium.launch({ executablePath: '/opt/pw-browsers/chromium-1194/chrome-linux/chrome', args:['--no-sandbox'] })
→ OK, navigated to http://localhost:3201/api/v1/health

So browser oracles are reachable in this container; what is blocked is running the showcase's pinned suite unmodified, because its resolved revision is baked in and the repo config must not be edited from a QA run. The screenshot/dom clauses in this area therefore read not-run (round ran out of budget), not blocked — only clause 5 here, which names that specific suite, is blocked(environment).

records-forms.import-wizard-encoding-and-hints rev 2 — blocked(dependency)

Its only automated.ref is objectui: packages/plugin-grid/src/importParsers.test.ts. objectui is a separate repo and is not checked out as a sibling here; the console build shallow-clones it to .cache/objectui-665661ab0932, so the source exists but its test toolchain was not provisioned in this container. Recorded blocked(dependency) rather than skipped.

Not-run items (18)

Not driven at all this round. Listed explicitly so the next round can pick them up without re-deriving scope:

form-view-gallery (1) · conditional-rules-header (1) · conditional-rules-grid (1) · cascading-options (1) · master-detail-atomic-save (2) · upload-guard-blocks-confirm (2) · action-location-matrix (4) · gantt-interactions (1) · kanban-drag-persistence (1) · form-dirty-guard (1) · adhoc-filter-sort-builder (1) · calendar-interactions (1) · record-discussion-mentions (1) · grid-personalization (1) · saved-view-management (1) · bulk-select-all-matching (1) · lookup-picker-create-new (1) · record-edit-undo (1)

Fixture gaps

  • import-job-undo-cancel (rev 1) — excluded by the resolver, blocked(fixture): IMPORT_CONSOLE_LIVE / import-harness gate — objectui e2e/import-console/import-console-undo.spec.ts self-skips unless IMPORT_CONSOLE_LIVE=1 and an import-job-capable client is wired; PENDING-GAPS §C. Confirmed still accurate — unchanged this round.
  • No second identity was provisioned, so every both-sides gate in this area that needs a non-admin persona is unproven: crud-roundtrip c7 (clone of an RLS-invisible source), related-list-server-pagination c5, and the discriminating half of crud-roundtrip c6 (cloner-vs-source ownership). The area's items assume a persona that stock --seed-admin does not create.
  • encrypted-field-behavior c4 — no reachable ad-hoc aggregate door for an arbitrary object field on stock showcase; the analytics route is cube-scoped.

Checklist-accuracy finding (not a product defect)

records-forms.crud-roundtrip rev 3's third negative reads:

a direct API POST missing required status → 400 VALIDATION_FAILED with fields[] carrying code 'required' (server enforces, not just the form)

Driven, this returns 201, not 400 — because showcase_account.status declares { label: 'Prospect', value: 'prospect', default: true }. A required field with a declared default is satisfied by that default; the write is correct.

This is not a fail of the item — the negative is factually wrong about this field. Control proving the required machinery is live and correctly coded: POST /api/v1/data/showcase_project {name,status} with no account400 VALIDATION_FAILED, fields[0] = {field:'account', code:'required'}.

Suggested edit for whoever owns the next checklist revision (not made here — this run writes nothing to the repo): retarget that negative at a required field with no default, e.g. showcase_project.account, and bump revision + append history.

Coverage honesty

Time in this round went roughly 60% to environment (cold monorepo: ~7 min client closure, ~14 min console build, plus the undocumented CLI-closure build) and 40% to testing. The area was not completed. Nothing here is ticked by association: every pass cell above names the request and the response that produced it, every browser-oracle clause I did not drive is not-run, and the one clause whose named pin failed to execute is blocked(environment) rather than fail.

Links back to #9296.

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