Skip to content

[finding] object-form 与 object-master-detail-form 在 binding-reach 探针的自动 fixture 下渲染成错误卡(reading 'map')—— 断言仍成立,但两个分支的崩溃守卫因此没法一起开 #3840

Closed

Description

@yinlianghui

发现于 #3808 的实施过程:给 public-block-binding-reach.test.tsx 的 ledgered 分支加崩溃守卫时,顺手把守卫加到了两个分支上,于是这两个块立刻红了 —— 它们一直在这个探针里渲染成错误卡,只是没人看。#3808 的 PR 已把守卫收窄回 ledgered 分支(那才是它的改动可能造成的空绿),这两条另记本单。

按 observation-class 只贴 finding、不排队:这两个块的断言本身仍然成立(见下),所以今天没有假绿,请 triage 决定是产品缺陷还是 fixture 缺陷。

事实(objectui origin/main @ c85268256 + #3808 分支)

apps/console/src/__tests__/public-block-binding-reach.test.tsx 用 sampleFor 按每个 block 声明的 inputs 自动填一份 schema,然后挂载并记录数据层调用。把「渲染没崩」这一条也断言上之后:

× object-form asks the data layer for its objectName
  <object-form> threw during render …
  Component "object-form" failed to render
  Cannot read properties of undefined (reading 'map')

× object-master-detail-form asks the data layer for its objectName
  Component "object-master-detail-form" failed to render
  Cannot read properties of undefined (reading 'map')

其余 14 个候选块全部渲染正常。

为什么今天不是假绿

这两个块不在 NO_DATA_REACH 里,所以它们走的是「必须有数据调用」那一支:

expect(reached.length, …).toBeGreaterThan(0);

实测它们确实发出了带 PROBE_OBJECT 的调用 —— 崩溃发生在取到数据之后的某一趟渲染里,不是在 fetch 之前。所以断言是挣来的,不是靠「什么都没产出」蒙的。

对比 ledgered 分支(如 record:related_list)就完全不同:那里的通过条件是「没有数据调用」,一个崩掉的块恰好满足它 —— 那才是必须堵的空绿,#3808 堵的就是那一处。

两种可能,需要 triage 分辨

分辨成本很低:把 sampleFor 生成的 schema 打出来,逐个 input 换成 undefined 二分,定位到具体那一条,再看该 input 的 spec 形状是不是允许样本那种值。

附带的建议

无论 (a) 还是 (b),这个探针最终都应该像它的兄弟 record-block-record-reach.test.tsx 那样对两个分支都断言「渲染没崩」(那个文件里叫 assertRendered)。现在只有 ledgered 分支有,是因为 #3808 不该把两个 pre-existing 崩溃拖进一个补 inputs 声明的 PR。这两条修掉之后,守卫就可以提到断言之前、对所有候选生效。

参考位置

  • apps/console/src/__tests__/public-block-binding-reach.test.tsx —— sampleFor(样本生成)、dataCallsFor(现在会把 html 一起带出来)、ledgered 分支的崩溃守卫
  • apps/console/src/__tests__/record-block-record-reach.test.tsx —— assertRendered,两分支通用守卫的现成形状

关联:#3808、#3838、#3149、objectstack#4472、objectstack#4413

Activity

  1. claude commented on Aug 8, 2026

    @claude
    Contributor

    Findings round: PROMOTED — finding → pm:queue.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. added
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    on Aug 21, 2026
  3. self-assigned this
    on Aug 24, 2026
  4. os-warren commented on Aug 24, 2026

    @os-warren
    Collaborator

    Claim: PM loop round 3
    Session: session_01PQ3NihCHE9LUtHoGxo6A9f
    Branch: claude/issue-3840-binding-reach-form-crash
    Worktree: objectui-issue-3840
    Domain: domain:devx (@ objectui, seat #5748)
    File surface: apps/console/src/__tests__/public-block-binding-reach.test.tsx; plus, only if the fork below resolves to (b), the one renderer that bare-takes — name it in the report before you edit it. (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus — the fork is a judgement call, not a mechanical edit. ⚠️ scripts/pm/dispatch-gates.mjs is objectstack-resident; objectui has no scripts/pm/, so this tier is this seat's explicit per-card decision, ⛔ not a --tier reading that does not exist here.
    Clause-②: no — a test fixture and at most one bare take. No contract acceptance behaviour moves, no public surface grows.
    Serial constraints cleared: no open PR touches the probe file. Repo-wide open PRs read by head at claim time — #5925 (packages/app-shell/src/preview/DraftChangesPanel.tsx, comment-only), #5922 (layout docs), #5843 (docs/adr, governed), #5774 (.github/workflows/ + ci-cd-pipeline.md), #5400 (release), #4958 (dependabot) — none intersect. Concurrent in this same batch: #5458, which touches apps/console/src/sdui-workbench-preview.tsx and two packages/app-shell/src/views/** files — disjoint from this card's surface by file, but if your fork lands on (b) in a shared renderer, stop and report rather than editing across it.

    Premise re-verified on origin/main @ 2074a59, and it holds

    ⛔ Not inherited from the card, which is 16 days old. Measured now:

    • The probe file is present, 453 lines.
    • Its ledgered branch carries the crash guard (html.includes('failed to render') → toBe(false)), and the non-ledgered branch still does not.
    • The code names this card by number. Verbatim from the comment block above the guard: "Deliberately NOT applied to the other branch: object-form and object-master-detail-form do paint an error card under this fixture (Cannot read properties of undefined (reading 'map')) while still making their data calls, so their verdicts are earned rather than vacuous. Whether that card is a product bug or this fixture handing them an implausible configuration — the lesson the header records four instances of — is objectui#3840, not something to decide by widening a guard here."

    So the card is live, and main is explicitly waiting on it.

    What you are deciding — the fork is the card

    Do not widen the guard first and see what breaks. Resolve which of these it is, with a measurement:

    The card names the cheap discriminator and it is still right: print the schema sampleFor generates, then bisect the inputs to undefined until the crash moves. That names the input; then read whether the spec shape for that input actually permits the sample's value. That last step is what separates (a) from (b) — ⛔ do not stop at "the crash went away when I changed the sample", which is true under both readings.

    The endpoint, once the two blocks are green

    Both branches assert render, the way the sibling probe record-block-record-reach.test.tsx already does with assertRendered. The guard moves ahead of the branch split and applies to all 16 candidates. That is the durable half — today the probe can only catch a crash on the branch where a crash is the pass condition.

    PM mechanism assumptions — test these, do not trust them

    Rank-ordered as assumptions, ⛔ not rulings, and I would rather be shown wrong than have you build on them:

    1. I read (a) as more likely than (b), on the base rate of that header's four prior entries. Weak. The card's author, who had the file open, deliberately left it 50/50.
    2. reading 'map' reads like an array-shaped input. Also weak — it could equally be a nested object the renderer maps over after a take.

    Premise-first

    The issue body is a lead, not a specification. If the two blocks no longer crash on today's main — someone may have fixed the renderer meanwhile — that is a legitimate and valuable delivery: report premise_still_valid: false with the measurement, open no PR, and say what changed. ⛔ Do not manufacture a diff to have something to show. Equally, if you find the fork resolves to (b) and the fix is larger than one bare take, stop and report rather than widening.

    Read the card's full body and every comment on GitHub yourself, and check the body for sanitizer truncation before you rely on it.


    Generated by Claude Code

  5. os-warren commented on Aug 24, 2026

    @os-warren
    Collaborator
    {
      "issue": 3840,
      "status": "done",
      "branch": "claude/issue-3840-binding-reach-form-crash",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5943",
      "premise_still_valid": true,
      "fork_resolution": "(a) fixture defect — decided on the input's spec shape, not on the crash disappearing",
      "files_changed": [
        "apps/console/src/__tests__/public-block-binding-reach.test.tsx",
        ".changeset/chilled-donkeys-shave.md"
      ],
      "renderer_touched": false,
      "summary": "Bisected the generated schema to the `sections` input: ObjectForm.tsx:1166 reads `section.fields.map(...)` and the generic array sample is the bare string ['name']. The discriminating step: FormViewSchema.sections REJECTS ['name'] ('expected object, received string') and requires `fields` per entry, and ObjectFormSection.fields is required in packages/types — so the sample is not publishable metadata and the error card is the fixture's fault. Fixed the sample, added the fifth/sixth lesson entries with the spec quotes inline, and hoisted the crash guard ahead of the branch split as `assertRendered` covering all 14 candidates.",
      "premise_drift": "object-master-detail-form no longer crashes on today's main, but is not fixed — it declares formType as a bare `string`, so the generic sample is 'x', which skips the gate at ObjectForm.tsx:1134 (`!schema.formType || schema.formType === 'simple'`) that guards the crashing loop. Forcing formType:'simple' reproduces the identical crash. Its formType sample is corrected too, so the block now genuinely exercises the path. The dispatch premise itself (guard on the ledgered branch only, in-code comment naming #3840) held exactly as re-verified.",
      "gates": [
        { "name": "vitest run apps/console/src/__tests__/", "verdict": "PASS", "evidence": "Test Files 19 passed (19) / Tests 242 passed (242)" },
        { "name": "@object-ui/console type-check", "verdict": "PASS", "evidence": "exit 0; script name echoed (tsc --noEmit && tsc -b tsconfig.node.json --force), so not a zero-match silent pass" },
        { "name": "@object-ui/console lint", "verdict": "PASS", "evidence": "exit 0; 171 files from --format json, 0 errors, 202 pre-existing warnings" },
        { "name": "check-changeset-presence.mjs", "verdict": "PASS", "evidence": "declares 1 changeset(s) ... Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption" },
        { "name": "check-changeset-fixed.mjs", "verdict": "PASS", "evidence": "All workspace packages are in the changeset fixed group." },
        { "name": "check-changeset-no-major.mjs", "verdict": "PASS", "evidence": "No changeset declares a `major` bump." },
        { "name": "check-control-bytes.mjs", "verdict": "PASS", "evidence": "check-control-bytes: OK (scanned 4929 tracked text file(s); skipped 85 binary)" }
      ],
      "gates_union_sha": "58dda7a",
      "tests": "All gates above re-run at the final pushed commit 58dda7a with a clean tree; every exit code captured before any pipe (redirect-then-capture), and each verdict quoted from the gate's own output line. Dependency closure built first (pnpm --filter '@object-ui/console^...' build) — without it type-check emits 40+ phantom TS2882 errors. REVERSE-VERIFICATION: direction predicted before running (revert only the sections sample => exactly 2 reds, via the new guard rather than the reach assertion); observed exactly that — 'Tests 2 failed | 14 passed (16)', both failures 'threw during render — that is a crash, not a binding verdict' carrying 'Cannot read properties of undefined (reading map)'. This confirms the card's no-false-green claim: the reach assertions still passed, so only the new guard catches it. No rebuild was needed (the mutation is in the test file itself, transformed directly by vitest, not in a dependency's dist). Mutation confirmed ON DISK before the run: probe_section occurrences 1 -> 0, ABLATED_3840 marker count 1, non-empty git diff --stat; restored by a trap on EXIT/INT/TERM, and the restore verified (git status clean, probe_section back to 1).",
      "deviations": [
        "Fixed the `formType` sample in addition to `sections`, inside the same declared file. Not strictly required to green the two blocks (only `sections` was), but without it object-master-detail-form stays green by accident and the widened guard asserts render on a path no author can reach. Same defect class, measured green, declared in the PR body.",
        "Lint narrowed to @object-ui/console rather than repo-wide `turbo run lint`, declared with three measurements: population from eslint's own config resolution, 171 files from --format json, and no type-aware linting configured (no projectService / parserOptions.project) so the diff cannot move an untouched file's verdict."
      ],
      "scope_respected": "Ruling 2: the fork resolved to (a), so the conditional renderer half of the file surface never opened — no renderer edited. Ruling 5: the guard was NOT widened to discover anything; it was hoisted only after the fork was resolved by measurement. Ruling 6: sdui-workbench-preview.tsx and packages/app-shell/src/views/** untouched, no intersection with #5458.",
      "sanitizer_check": "Issue body checked for truncation before being relied on: complete through all six sections, ending at the `关联:` line, no mid-sentence cut and no unterminated code fence. Separately, the PR body's first publish lost `<object-form>` from a code block to the short-angle-bracket sanitizer; corrected on edit, and the signoff footer uses the session-URL form so it survives edits.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #5939: object-master-detail-form declares formType as a bare `string` while the renderer switches on the object-form variant enum — an out-of-vocabulary value silently matches no branch and hid this card's crash",
        "filed as #5940: object-master-detail-form issues getObjectSchema(undefined) for a detail collection whose object it never resolved, instead of declining to fetch"
      ]
    }

    Generated by Claude Code

  6. os-warren commented on Aug 24, 2026

    @os-warren
    Collaborator

    os-dev-report

    (Supersedes comment 5390087791, whose HTML-comment marker was eaten by the body sanitizer on save — verified by read-back. Same payload, marker as literal text so the PM scan can see it.)

    {
      "issue": 3840,
      "status": "done",
      "branch": "claude/issue-3840-binding-reach-form-crash",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5943",
      "premise_still_valid": true,
      "fork_resolution": "(a) fixture defect — decided on the input's spec shape, not on the crash disappearing",
      "files_changed": [
        "apps/console/src/__tests__/public-block-binding-reach.test.tsx",
        ".changeset/chilled-donkeys-shave.md"
      ],
      "renderer_touched": false,
      "summary": "Bisected the generated schema to the `sections` input: ObjectForm.tsx:1166 reads `section.fields.map(...)` and the generic array sample is the bare string ['name']. The discriminating step: FormViewSchema.sections REJECTS ['name'] ('expected object, received string') and requires `fields` per entry, and ObjectFormSection.fields is required in packages/types — so the sample is not publishable metadata and the error card is the fixture's fault. Fixed the sample, added the fifth/sixth lesson entries with the spec quotes inline, and hoisted the crash guard ahead of the branch split as `assertRendered` covering all 14 candidates.",
      "premise_drift": "object-master-detail-form no longer crashes on today's main, but is not fixed — it declares formType as a bare `string`, so the generic sample is 'x', which skips the gate at ObjectForm.tsx:1134 (`!schema.formType || schema.formType === 'simple'`) that guards the crashing loop. Forcing formType:'simple' reproduces the identical crash. Its formType sample is corrected too, so the block now genuinely exercises the path. The dispatch premise itself (guard on the ledgered branch only, in-code comment naming #3840) held exactly as re-verified.",
      "gates": [
        { "name": "vitest run apps/console/src/__tests__/", "verdict": "PASS", "evidence": "Test Files 19 passed (19) / Tests 242 passed (242)" },
        { "name": "@object-ui/console type-check", "verdict": "PASS", "evidence": "exit 0; script name echoed (tsc --noEmit && tsc -b tsconfig.node.json --force), so not a zero-match silent pass" },
        { "name": "@object-ui/console lint", "verdict": "PASS", "evidence": "exit 0; 171 files from --format json, 0 errors, 202 pre-existing warnings" },
        { "name": "check-changeset-presence.mjs", "verdict": "PASS", "evidence": "declares 1 changeset(s) ... Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption" },
        { "name": "check-changeset-fixed.mjs", "verdict": "PASS", "evidence": "All workspace packages are in the changeset fixed group." },
        { "name": "check-changeset-no-major.mjs", "verdict": "PASS", "evidence": "No changeset declares a `major` bump." },
        { "name": "check-control-bytes.mjs", "verdict": "PASS", "evidence": "check-control-bytes: OK (scanned 4929 tracked text file(s); skipped 85 binary)" }
      ],
      "gates_union_sha": "58dda7a",
      "tests": "All gates above re-run at the final pushed commit 58dda7a with a clean tree; every exit code captured before any pipe (redirect-then-capture), and each verdict quoted from the gate's own output line. Dependency closure built first (pnpm --filter '@object-ui/console^...' build) — without it type-check emits 40+ phantom TS2882 errors. REVERSE-VERIFICATION: direction predicted before running (revert only the sections sample => exactly 2 reds, via the new guard rather than the reach assertion); observed exactly that — 'Tests 2 failed | 14 passed (16)', both failures 'threw during render — that is a crash, not a binding verdict' carrying 'Cannot read properties of undefined (reading map)'. This confirms the card's no-false-green claim: the reach assertions still passed, so only the new guard catches it. No rebuild was needed (the mutation is in the test file itself, transformed directly by vitest, not in a dependency's dist). Mutation confirmed ON DISK before the run: probe_section occurrences 1 -> 0, ABLATED_3840 marker count 1, non-empty git diff --stat; restored by a trap on EXIT/INT/TERM, and the restore verified (git status clean, probe_section back to 1).",
      "deviations": [
        "Fixed the `formType` sample in addition to `sections`, inside the same declared file. Not strictly required to green the two blocks (only `sections` was), but without it object-master-detail-form stays green by accident and the widened guard asserts render on a path no author can reach. Same defect class, measured green, declared in the PR body.",
        "Lint narrowed to @object-ui/console rather than repo-wide `turbo run lint`, declared with three measurements: population from eslint's own config resolution, 171 files from --format json, and no type-aware linting configured (no projectService / parserOptions.project) so the diff cannot move an untouched file's verdict.",
        "First report comment (5390087791) lost its HTML-comment marker to the sanitizer; this comment re-delivers the payload with a literal-text marker. Left in place rather than deleted, since no comment-edit tool is available from this seat."
      ],
      "scope_respected": "Ruling 2: the fork resolved to (a), so the conditional renderer half of the file surface never opened — no renderer edited. Ruling 5: the guard was NOT widened to discover anything; it was hoisted only after the fork was resolved by measurement. Ruling 6: sdui-workbench-preview.tsx and packages/app-shell/src/views/** untouched, no intersection with #5458.",
      "sanitizer_check": "Issue body checked for truncation before being relied on: complete through all six sections, ending at the closing relations line, no mid-sentence cut and no unterminated code fence. Separately, two sanitizer casualties were caught by read-back and corrected: the PR body's first publish dropped an angle-bracketed block name from a code block (fixed on edit), and this report's HTML-comment marker (re-delivered here as literal text). PR signoff footer uses the session-URL form so it survives edits.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #5939: object-master-detail-form declares formType as a bare `string` while the renderer switches on the object-form variant enum — an out-of-vocabulary value silently matches no branch and hid this card's crash",
        "filed as #5940: object-master-detail-form issues getObjectSchema(undefined) for a detail collection whose object it never resolved, instead of declining to fetch"
      ]
    }

    Generated by Claude Code

  7. os-warren commented on Aug 24, 2026

    @os-warren
    Collaborator

    ACCEPT — PR #5943. Landing held only on CI convergence.

    Reviewer of record: domain:devx @ objectui seat (#5748), round R3, PM session session_01PQ3NihCHE9LUtHoGxo6A9f. Reviewed against GitHub and the diff, ⛔ not against the report's self-description.

    check verdict
    PR shape draft ✅, base main ✅, first body line Fixes #3840 ✅
    file surface vs. claim exact — apps/console/src/__tests__/public-block-binding-reach.test.tsx + one changeset. No renderer touched, which is the correct consequence of the fork landing on (a)
    ACCEPT path fork get_files read first: no governed path. Ordinary ready/queue path applies
    changeset empty-frontmatter, matching this repo's declared convention for test-only changes; check-changeset-presence quoted its own exemption line
    ruling 5 (⛔ don't widen the guard to discover things) honoured — the guard was hoisted only after the fork was resolved by measurement
    ruling 6 (⛔ stay off #5458's files) honoured — no sdui-workbench-preview.tsx, no packages/app-shell/src/views/**

    The fork was resolved the way I asked, and that is the whole review

    Ruling 1 said stopping at "the crash went away when I changed the sample" does not discriminate, because it is true under both readings. The dev did not stop there. Read off the diff:

    • Bisected to the sections input; the read is ObjectForm.tsx:1166, section.fields.map(...), with the generic array sample being the bare string ['name'].
    • Then asked the spec — the step that actually decides it. FormViewSchema.sections rejects ['name'] ("expected object, received string"), and [{}] fails on "0.fields: expected array, received undefined"; ObjectFormSection.fields is required in packages/types. So ['name'] is not metadata any author could publish ⇒ fixture defect, (a).
    • Sharpest detail: it names the schema that cannot answer the question. ComponentPropsMap['object-form'].sections is z.array(z.unknown()) and accepts [42], [null], anything — it is the schema nearest to hand and consulting it would have produced the wrong answer. Recording which evidence source is disqualifying is exactly the discipline this file's header keeps asking for.

    The premise drift it found is worth more than the card

    object-master-detail-form no longer crashed on today's main — and the reason is not that anyone fixed it. That block declares formType as a bare string rather than the enum object-form declares, so the generic sample was 'x', and the crashing loop is gated on ObjectForm.tsx:1134 (!schema.formType || schema.formType === 'simple'). 'x' skipped straight past it. Measured, not inferred: with sections left malformed, forcing formType: 'simple' reproduces the identical crash.

    So the block was green for a reason unrelated to its correctness — the fixture was routing around the code it existed to exercise. That is a strictly worse failure than the one the card was filed for, and it was invisible. Fixing the formType sample is a declared deviation inside the declared file, and I accept it: without it the widened guard would assert render on a path no author can reach, which is a guard that cannot fail.

    Reverse-verification, with the direction predicted first

    Reverting only the sections sample was predicted to redden exactly two tests through the new guard rather than the reach assertion — observed exactly that: Tests 2 failed | 14 passed (16), both failures from assertRendered. That confirms the card's own "no false green today" claim from the other side: those two blocks were always earning their data-reach verdicts, and it was the crash that was invisible. Mutation confirmed on disk before the run and restored by an EXIT/INT/TERM trap.

    The durable half landed

    assertRendered now runs ahead of the branch split, covering all 14 candidates, matching the sibling probe record-block-record-reach.test.tsx. The in-code comment that deferred this to #3840 is gone, replaced by the reasoning for why a crash mis-reads on both branches — on the ledgered branch a crash confirms the ledger entry; on the other it points the reader at wiring that is fine. And the lesson list got its fifth and sixth entries with the spec quotes inline, so the next reader can check the claim instead of trusting it. That was ruling 3 and it was met in full.

    Two findings filed, both verified open

    #5939 (the formType declaration divergence — an out-of-vocabulary value silently matches no branch) and #5940 (getObjectSchema(undefined) for a detail collection whose object was never resolved). Correctly filed unassigned rather than fixed here. ⚠️ #5939 is the more interesting one: it is a class, not an instance — any block declaring a prop as bare string where the renderer switches on an enum has the same hole.

    ⏳ Landing

    mergeable_state: unstable — checks are still running on 58dda7a. Enqueue resource is every check green, ⛔ not the required subset, so this stays draft until they land, then I flip ready and enqueue. Nothing is failing.

    Housekeeping, ⛔ not this PR's fault: the report arrived twice (5390087791 lost its HTML-comment marker to the body sanitizer; 5390094268 carries the literal-text fallback, verified by read-back). No comment-edit path exists from this seat.


    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

Labels

domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions