Skip to content

QA run · search (FULL area) · 92f26f75 · 2026-08-11 · 2 PASS / 2 PARTIAL / 2 FAIL #7629

Closed

Description

@baozhoutao

Full search area run of the checklist-test skill — all 6 items driven against a live showcase (opus subagent, isolated boot + file DB, admin + a purpose-provisioned member persona). Text-only per RUNNER.md.

Result: 2 PASS · 2 PARTIAL · 2 FAIL.

Environment — framework 92f26f75 (branch claude/platform-test-checklist-ocwugl) · vendored console 6314e87f · showcase app · isolated port + file DB.

✅ PASS — 2 items

  • rls-both-personas (4/4 + both negatives) — a real second persona (sign-up + bound contributor position) cannot see a foreign invoice by id (404) and gets zero search hits for it on both wire spellings, in the ⌘K palette and on the /search page, while admin gets the row. The member's own creations are findable, so the restriction is subtractive rather than a blackout. Counts do not leak: the member's totals reconcile with their own 3-row visible set (15 for admin), and page-local totals were shown to be skip + records.length for admin too — carrying zero information about hidden rows.
    Fixture drift: the item says 8 seeded invoices, the seed now ships 12.
  • freshness-and-empty (6/6 + both negatives) — create / rename / delete are reflected on the first query attempt within 3–90 ms with no wait or retry (timestamped), and a rename flips both the new and the old term in the same second. A no-hit search returns the declared 200 envelope on both spellings, with designed empty states in the console and a clean browser console.
    The runner answered the item's question directly: this is an honest empty, not a stale index. Structurally there is no latin index ($search is a query-time WHERE expansion, so nothing can lag); behaviourally deletes vanish and creates appear with no wait; and the one materialized artifact — the __search companion — is written inside the same save (the 201 body already carries it), with its only lagging path (hook-bypassing writes) demonstrated and demonstrated reconciling at the next boot.

🔴 FAIL — 2 items

1. cross-field-object-search — $search is case-sensitive on textual fields, contrary to three declarations

Cross-field OR scanning works (an industry-only hit and a name-only hit come back from one unrestricted query, positively pinned by narrowing each way), and multi-term AND semantics are correct ("retail contoso" → 0, not an OR union).

Clause 2 fails. $search is case-sensitive on textual fields, although the item title, the search-filter.ts docblock ("Matching: case-insensitive") and the search-conformance ledger row all declare it case-insensitive.

Reproduction rule

POST /api/v1/data/showcase_account/query {"search":"Retail","searchFields":["name"]}
  → 200, 1 record "Acme Retail"
POST /api/v1/data/showcase_account/query {"search":"retail","searchFields":["name"]}
  → 200, records [] total 0

Also stark/Stark, wonka/Wonka, NORTHWIND. Reproduced on two fresh boots.

Root cause. search-filter.ts fieldClausesForTerm emits {field:{$contains:term}} for textual types, and $contains is contractually case-sensitive (#4706 Q2=A) — the #6518 SQLite LIKE→GLOB change removed SQLite's incidental ASCII fold. The case-insensitive operator is $icontains: {name:{$icontains:'retail'}} does return "Acme Retail". Only enum/select terms survive, because optionValuesMatching lowercases both sides in JS before emitting $in — which is exactly why the select-label half still passes.

Why the existing pin stayed green: the dogfood pin (4/4) does not cover lowercase textual matching — its only case assertion is a select label. A single lowercase-name assertion would catch this.

2. field-scoped-narrowing — the __search companion column, declared "invisible to clients", is echoed in every record body

Narrowing splits exactly as declared, and a bad $searchFields is refused loudly rather than silently widened — an unknown field and a real-but-unsearchable field both 400 INVALID_FIELD with located guidance (the #4254 widening leak does not reproduce), on both the POST and GET spellings, with byte-identical messages.

Clause 2 (half B) fails.

Reproduction rule

POST /api/v1/data/showcase_contact/query {"search":"zhangwei","limit":1}
  → the returned record contains "__search":"zhangwei zw"
GET  /api/v1/data/showcase_account/<cjk-id>
  → "__search":"huaningkeji hnkj"
GET  /api/v1/search?q=INV-1003
  → each hit.record carries a __search key

The 201 body of a create carries it too. Still echoed after restarting with OS_SEARCH_PINYIN_ENABLED=false.

Root cause. search-companion.ts provisions the column hidden + system + readonly + searchable:false, which only keeps it out of auto-views, the $search auto-default and $searchFields overrides — no read path strips it from the default projection (hidden system columns generally come back, as organization_id does). The $searchFields half is correctly enforced (__search as an override → 400 "is hidden").

Impact assessment: disclosure risk is low — the value derives from the display field only, and secret/FLS-restricted sources are excluded. But the declared client-invisibility contract is broken and every payload carries a private index column.

🟡 PARTIAL — 2 items

console-global-search — the palette's recall is strictly narrower than the $search executor's

Passing: the palette opens via Ctrl+K, the header button and a ?palette=1 deep link (idempotent); it issues exactly one GET /api/v1/search (the searchAll path, not a per-object fanout); hits are grouped under a per-object heading (#3371 does not reproduce); Enter navigates to the record (aria-selected asserted before the keypress, so automation-input is excluded); empty and 1-char input fire zero requests and show "Recently viewed"; the RLS deny side is clean on both API and UI for a real member.

Clause 2 is partial. hnkj / huaningkeji return 0 hits from the global endpoint while POST /data/<obj>/query {search:…} returns the CJK row, and the seeded CJK account is unreachable by huaning from this endpoint.

Root cause (located). metadata-protocol searchAll builds its own filter (TEXT_TYPES + f.searchable===true, falling back to the title field) instead of calling resolveSearchFields/expandSearchToFilter, and never ORs the SEARCH_COMPANION_FIELD. Its comment claiming "case-insensitive substring matching via $contains" is also stale (same case bug as cross-field-object-search above).

Net effect for users: pinyin recall works in the list quick-search but not in the ⌘K command palette.

pinyin-flag-both-sides — substance proven, but the clause's stated oracle is unobtainable

Flag on: full-pinyin, initials and native CJK all hit, per-variant on two boots. Additive-and-scoped proven by contrast (a non-name CJK field is not pinyin-searchable; companion values confirm name-only materialization). Flag off: companion recall dies while native CJK still works via source columns, all 200 — and the runner disambiguated an apparent survivor (a term that still hit under OFF turned out to match a seeded email substring, not pinyin) by creating a clean probe row during the OFF window and proving full-pinyin is dead for it. The first-boot backfill line was captured, plus a bonus proof of the stale-row reconcile (a row written while the plugin was unloaded became pinyin-findable after a restart with the flag on).

Clause 3 is partial on its oracle only. The named log line never appears, because the gate is one layer upstream: with the flag off, the CLI does not add pinyin-search to requires at all, so the plugin is never loaded (47 vs 48 plugins; a 1.2 MB debug boot log contains zero occurrences of "pinyin"). The clause's substance is proven more strongly than the log line would prove it — the item should restate its evidence as the plugin-count/requires difference, since the stated string is unobtainable in a CLI-served deployment.

Checklist maintenance falling out of this run

  1. cross-field-object-search and console-global-search both hinge on the same $contains case decision — whichever way $regex on driver-sql is not a regex — it compiles to a substring LIKE, so it both over-matches and silently matches nothing #4706 Q2 is resolved, three places currently declare the opposite of the behaviour (item title, search-filter.ts docblock, search-conformance ledger row).
  2. pinyin-flag-both-sides clause 3's evidence string needs replacing (see above).
  3. rls-both-personas fixture count is stale (8 → 12 seeded invoices).

Activity

  1. baozhoutao commented on Aug 11, 2026

    @baozhoutao
    ContributorAuthor

    Correction — console pin in the Environment line.

    This run's Environment line says vendored console 6314e87f. That is the repo-root .objectui-sha (updated 2026-08-11 06:35) — the pin main wants. The bundle the server actually served during this run is packages/console/dist/.objectui-sha = 09987b680d53801c79f67d969b14e9bb732b8a22 (built 2026-08-10 09:58). Root and dist disagree because dist has not been rebuilt against the newer pin.

    Nothing in this run's verdicts changes. The one console-lane finding here — console-global-search's narrower recall — was located in framework source (metadata-protocol searchAll building its own filter instead of calling resolveSearchFields/expandSearchToFilter), not console source, and the palette behaviour was observed live against the running bundle either way.

    Flagging it because anyone reproducing a console-lane finding should check out 09987b680, not 6314e87f. The two .objectui-sha files disagreeing is itself worth knowing about.


    Generated by Claude Code

  2. huangyiirene commented on Aug 11, 2026

    @huangyiirene
    Collaborator

    Extraction ledger — COMPLETE. Closing.

    Every FAIL, PARTIAL and checklist-maintenance item extracted; all three code defects re-verified live on origin/main, zero duplicates.

    Run item Finding Card Lane
    cross-field-object-search c2 (FAIL) $search is case-sensitive on textual fields, contrary to three declarations — fieldClausesForTerm emits $contains (case-sensitive per #4706 Q2=A; #6518 LIKE→GLOB removed SQLite's incidental fold); $icontains matches #7641 domain:engine-core
    field-scoped-narrowing c2(B) (FAIL) the __search companion column, declared client-invisible, is echoed in every record body — no read path strips it from the default projection #7642 domain:engine-core
    console-global-search c2 (PARTIAL) ⌘K palette recall strictly narrower than the executor — metadata-protocol searchAll builds its own filter, never ORs the companion field, stale $contains comment #7643 domain:metadata
    Checklist maintenance ×3 case-insensitive declared in 3 places vs case-sensitive behaviour; pinyin-flag-both-sides oracle unobtainable; rls-both-personas fixture 8→12 #7647 domain:devx

    The 2 PASS items produced no cards.

    #7641 carries a decision, flagged not buried

    $search case-sensitivity is a real behaviour-vs-contract break, but the direction of the fix is a maintainer call: (A) make $search emit $icontains so behaviour matches the three existing declarations (recommended in the card — search is near-universally expected case-insensitive, and three places already declare it so), or (B) change the declarations to say case-sensitive. Whichever way #4706 Q2 finally lands, the three declarations (item title, search-filter.ts docblock, search-conformance ledger row) must be made consistent — that reconciliation is tracked in #7647.

    Cross-cutting: #7641 and #7643 share the same $contains-case root (#6518); #7643 is cross-linked to #7641 in its body. One decision resolves both behaviours plus the three stale declarations.

    Provenance: maintainer standing rule that every QA problem becomes an issue; run records close at extraction, same exit as #7514/#7463.


    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

    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