Skip to content

QA run · api-backend (FULL area) · a86db175 · 2026-08-10 · 6 PASS / 2 PARTIAL / 3 FAIL #7463

Description

@baozhoutao

Full api-backend area run of the checklist-test skill — all 11 items driven against a live showcase (3 opus subagents, one isolated boot each; ports 4011–4013). Convention labels: qa-run + bug.

Result: 6 PASS · 2 PARTIAL · 3 FAIL. Text-only report per RUNNER.md; every verdict rests on a live-HTTP / gate-output / test oracle.

⚠️ One additional SECURITY-SENSITIVE finding from this run is deliberately NOT described here, pending the maintainer's decision on disclosure (same handling as the earlier FOLLOW-UPS.md D1). It is a cross-persona data-disclosure defect with a located root cause and a 2/2 reproduction. Ask for the private write-up.

Environment

framework a86db175 — branch claude/platform-test-checklist-ocwugl (PR #7304)
app · driver showcase · SqlDriver(better-sqlite3) · fresh isolated file DB + port per batch
personas admin@objectos.ai + purpose-signed-up member/contributor (created in-run rather than declaring items blocked)

🔴 FAIL — 3 items, 7 defects

query-contract-matrix (P0) — 4 defects

The matrix itself is solid: 16 operators, $-params, pagination windows, sort (+INVALID_SORT both doors), $in:[]/$nin:[] arity, and the #5869 scalar-on-collection gate all verified across three doors (POST /query {where}, POST {filter} AST, GET ?$filter=) with exact id-set equality. Four contract breaks:

1. Dotted projection silently WIDENS. POST /data/showcase_invoice/query {"fields":["name","account.name"]} → 200 returning every business field and no resolved account.name. Same on GET ?$select=name,account.name and on showcase_task {fields:["title","project.name"]}; control {fields:["title"]} correctly narrows. A nonexistent dotted column is likewise swallowed (200 + every field) while a nonexistent plain column answers 400 INVALID_FIELD — whose own message names this exact failure mode. Root cause: assertProjectionFieldsExist validates only f.split('.')[0], so the dotted entry passes ingress then defeats the projection.

2. Unknown field inside where/$filter answers 200/0 instead of 400 INVALID_FIELD. {"where":{"not_a_field":"x"}}200 records:[] total:0, no code; identically for the $filter and AST doors. Same-run control ?not_a_field=x400 INVALID_FIELD. The two doors disagree about one mistake, and "indistinguishable from no data" is exactly what #4134 closed for the bare-key door.

3. like is folded to $contains — wildcards bound as literals. ["name","like","%Industries"]0 rows, while ["name","like","Industries"] → the 2 matching rows, byte-identical to the $contains control. Contradicts canonicalAstOperator's own comment ("driver-sql passes them to SQL verbatim, so the caller binds the wildcards. Folding them onto contains would silently wrap the value in %…% and change what the query means"). The driver's like/ilike bind arm is unreachable from the wire since #5158.

4. expand is a silent no-op when fields omits id. {"expand":{"account":{"object":"showcase_account","fields":["name"]}}} → 200 with account left as the raw FK id; adding "id" expands correctly (2/2 on a second object pair too). Root cause: engine.ts expandRelatedRecords forwards nestedAST.fields to the sub-find then keys recordMap on rec.id, so a projection omitting id yields an empty map and the inject falls through to recordMap.get(val) ?? val. This spelling is the one prescribed verbatim in two spec retirement messages (FIELD_NODE_OBJECT_FORM_REMOVED, QUERY_JOINS_REMOVED) and in this item's own steps.

batch-transactional-discovery — per-object batch default blocks valid rows

POST /data/showcase_private_note/batch with no options and a failing middle record → 200 {total:3, succeeded:1, failed:1, results:[idx0 ok, idx1 VALIDATION_FAILED]} — only 2 results for 3 records, no NOT_ATTEMPTED for idx2, succeeded+failed (2) != total (3), and the third valid row returns 0 rows on re-read (never attempted). Expected per ADR-0119 D4 (atomic defaults false on this door): valid rows land, neither blocks the other. Reproduced 2/2; two controls isolate it — continueOnError:true → 3 results with both valid rows persisted, atomic:true → the correct ROLLED_BACK/VALIDATION_FAILED/NOT_ATTEMPTED triple. Root cause: the non-atomic loop breaks out on failure while buildBatchDataResponse reports total = records.length, so the un-attempted tail is invisible.
(The rest of the item passes: discovery bit matches live behaviour incl. $ref parenting and BATCH_UNRESOLVED_REF; atomic:false on the cross-object door is refused 400 BATCH_NOT_ATOMIC; cross-object rollback held at both write-time and validation-time failures; BATCH_TOO_LARGE bracketed exactly at 200/201 on all three doors.)

route-ledger-live-parity — 2 defects

5. A ledgered dispatcher route is never mounted (#3361 class). GET /api/v1/meta/objects/showcase_task/state/status?from=in_review404 {"error":"Not found"}byte-identical to an unmounted-path control, not a handler answer. Expected per route-ledger.ts (ADR-0020 D3.3): 200 {object,field,from,next:["done","in_progress"]}. Root cause: zero route registrations deliver a 4-segment /meta path; the domain handler exists and ships in dist (so not stale-dist), but REST registers ~17 /meta routes maxing at 3 segments, so it dies at Hono's notFound. Same seam: GET /meta/types is swallowed by REST's /meta/:type.

6. A discovery capability bit contradicts live behaviour. capabilities.search = {enabled:false} (and services.search: unavailable) while GET /api/v1/search?q=audit serves 200 with 5 real hits. A client that trusts the bit skips a working surface. Root cause: the bit derives from a registered search service slot, while registerSearchEndpoints only 501s when protocol.searchAll is missing — and the protocol implements searchAll regardless.

7. Raw TypeError leaks in the error envelope (surfaced by the API-Console batch, belongs to error-envelope-ledger)

POST /api/v1/data/showcase_task body {"title": 12345} (a number into a declared text field) → 400 {"error":"TypeError: not a function","object":"showcase_task"}no code, no fields[]. Expected the ledgered envelope the other invalid cases produce ({}400 VALIDATION_FAILED with fields[]; a bad enum → invalid_option with the allowed list). Reproduced 3×. Suspected: a coercion path calls a string method on a number before validation reports it.


🟡 PARTIAL — 2 items, both checklist drift, not product defects

  • declarative-endpoint-execution — live endpoints serve with their declared cache-control: private, max-age=30, flow-target endpoints run real flows, anonymous is default-closed (401), unknown path/wrong method give the bare 404. The clause's literal 501 NOT_IMPLEMENTED for script/proxy targets is unreachable: the publish gate refuses them at boot with a located message (17.x 立项:建设声明式 ApiEndpoint 执行器(挂载 + matchEndpoint + authRequired/cacheTtl/inputMapping/outputMapping 逐键接线) #5040 §7-3) — stricter than the clause asks. automated.ref dogfood suites 22/22 green. → revise the item text.
  • server-timing-admin-gated — admin gets full spans plus the admin-only SQL-shape detail header; a non-admin gets no header in per-request mode (the security half holds; both personas measured live in the same run). Two clauses are contradicted by the implemented two-mode design rather than by a leak: with OS_SERVER_TIMING=true (the flag the item's own steps set) the basic header goes to everyone including anonymous — documented deliberate "global" mode — while the sensitive detail header stays admin-only; and with the flag off, an admin with the debug header still gets spans (only serverTiming:false removes the middleware). Also: no hooks span exists anywhere — that name in the contract is aspirational. → revise the item text.

✅ PASS — 6 items

error-envelope-ledger (6/6 — every sampled code a ledger/standard member, fields[] shape + catalog, zero fieldErrors across 13 bodies, exact status↔code table, OBJECT_NOT_FOUND vs RECORD_NOT_FOUND distinguishable, zero 5xx; the flat 401 dialect confirmed sanctioned under the ADR-0112 amendment) · bulk-write-contract (6/6, and the #2982 negative proven: a member's updateMany/deleteMany against an invisible admin-owned row → FORBIDDEN/PERMISSION_DENIED with the row verifiably unchanged) · package-rest-lifecycle (6/6 incl. duplicate → 409 with no #2995 clobber, and a true partial PATCH) · formula-gates (4/4 against the built dist with a resolved-path guard; date arithmetic refused at build with daysBetween/addDays prescribed) · enforce-or-remove-authoring-gates (4/4 across four registries; every rejection names its replacement) · api-console-discovery-execute (5/5; tree matches /discovery 10/10, real 200/404/400 rendered with the server's own envelopes).

Smaller observations

  • Unledgered live mounts: /api/settings, /api/v1/datasources, /api/v1/datasources/drivers, GET /api/v1/health, GET /api/v1/meta/types all answer 200 but appear in no route ledger (PENDING-GAPS §E still open).
  • POST /api/v1/packages/publish answers a misleading 405 (Allowed: DELETE, GET, HEAD, PATCH) — the path is only reachable because /packages/:id absorbs it; the REST package registrar isn't mounted on showcase.
  • API Console never renders the Storage group on any host: its catalog key is storage while /discovery names the service file-storage, so the fail-closed branch fires and 3 endpoints are unreachable from that page.
  • Per-object bulk counters under-report whenever a row fails without continueOnError (same family as defect [WIP] Fix error in step four of the action run #5 above).
  • Doc nit: skill.zod.ts's docblock still says a permissions key is "silently stripped", but SkillSchema now rejects it — behaviour is safer than the prose.

Screenshots

None — all oracles are live HTTP / build-gate output / cited test runs. The 7 public defects carry reproduction rules above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions