Skip to content

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

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.tsfieldClausesForTerm emits {field:{$contains:term}} for textual types, and $contains is contractually case-sensitive (#4706 Q2=A) — the #6518 SQLite LIKEGLOB 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 oneGET /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-protocolsearchAll 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).

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