Skip to content

tests(attachments): pin unscoped-multi-delete clause C3 end to end, and correct the ledger that still calls it broken - #11326

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-9483-attachments-authz-remaining-clauses
Aug 23, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-9483-attachments-authz-remaining-clauses

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #9483

QA run #9401 listed attachments-storage.attach-requires-parent-edit clause C3 —
"an unscoped multi-delete (no id AND no where) is refused outright (#4757)" — as
unpinned. #9724 correctly declined to pin it: measured end to end, the refusal did
not fire, and pinning the behaviour of the day would have turned the eventual fix
red. That product gap (#9719) was closed by PR #9797 and widened to the update verb
by #9974.

So the first deliverable here was re-verification, not test-authoring. Walking
the card's clause list against the merged ref:

clause set verdict on main evidence
download-authz-both-sides C3 pinned already (#9724) attachments-public-read-acl.dogfood.test.ts, ref accurate
download-authz-both-sides C4 manual by design browser denial copy; ref's scope note says so
read-inherits-parent-rls C1 + C3 pinned already (#9724) count-parity + scan-cap pins, refs accurate
attach-requires-parent-edit C3 behaviour pinned, ledger stale, dogfood blind this PR

Nothing #9724 landed is re-pinned.

The ledger was wrong in the dangerous direction

automated.ref and knownGaps still read "⛔ CLAUSE 3 IS NOT PINNED … the #4757
'refused outright' rule does NOT fire through ObjectQL.delete"
. That has been false
since 2026-08-19. The next Tier-1 runner would have been told that a shipped, working
safety guard is broken.

The measured detection gap — why a new pin was needed at all

"Refused outright" and "the per-row gate refused one of the rows" answer with the
same code, so they are indistinguishable on a fixture whose rows split
entitled/not — which is exactly the fixture the landed block uses.

Measured, not argued: with dispatchUnscopedMultiWrite removed from both
sys_attachment registrations, service-storage rebuilt and the ablation proven
live in dist/ (ablation-dist-preflight.mjs → "marker absent from all 6 built
files"
), the existing block stays 5/5 GREEN. It cannot see the clause it is
named for.

The new block seeds the one fixture that separates them — a caller who uploaded
every matched row, whom the per-row gate would wave through — plus the
empty-table (zero-match) limb, which no handler-only fix could ever reach.

Ablation (both legs rebuilt, both dist-preflighted)

Same mutation, against the extended suite: 3 failed | 7 passed, exactly as
predicted, each red for its predicted reason — a matching count alone is not a pass.

case predicted observed
block 1 · "refused OUTRIGHT" red — wrong message expected Error: Cannot delete attachment … to match object (the per-row gate answered)
block 2 · uploader of every row red — resolves, the wipe promise resolved "2" instead of rejecting — #9719's measurement reproduced
block 2 · empty table red — resolves promise resolved "+0" instead of rejecting
block 2 · scoped sweep (both sides) green — no cascade green

Restore leg: "marker present in 2 built files"; tree clean, src count back to 2.

Also corrected in the item

Verification — union at e3da4cda (the final commit)

Test Files 1 passed (1) / Tests 10 passed (10).

Gates green, each read from its own verdict line: check:platform-checklist (15
areas, 205 items) · check:nul-bytes (6349 files) · check:engine-double-contract ·
check:where-matcher · check:test-source-alias · check:cross-package-test-inputs ·
check:type-check-coverage · check:query-options-erasure · check:doc-authoring ·
check:doc-formula-expressions · check:published-files · check:slot-lookup ·
check:type-source-resolution · check-ci-filter-parity · check-plugin-teardown-shape ·
check-affected-docs · spec check:empty-state / check:liveness /
check:strictness-ledger / check:variant-docs · @objectstack/dogfood typecheck.

check:type-check-debt --re-measure refused first on an unbuilt closure — recorded
as NOT MEASURED, then the closure was built exactly as lint.yml does and it re-ran
green: "33 ledger entr(ies) re-measured in 233.9s, 1897 raw tsc error(s) total, none
above its recorded number."
No baseline moved.

ESLint narrowed to the diff and the narrowing is measured, not assumed: 2 files
submitted, --format json reports the .ts file linted with 0 errors / 0 warnings
and the .json "ignored because no matching configuration was supplied" — so eslint's
own config puts it outside the linted population. Type-aware linting is not enabled
(eslint.config.mjs states no parserOptions.project, no typed rules), so this diff
cannot move the verdict on any untouched file.

No changeset: @objectstack/dogfood is private: true and the diff is tests plus
checklist prose, so this releases nothing — skip-changeset applied, per this repo's
route 2 (an empty-frontmatter changeset is explicitly the wrong route here).


Generated by Claude Code

…hat still calls it broken

QA run #9401 listed `attachments-storage.attach-requires-parent-edit` clause C3
("an unscoped multi-delete is refused outright", #4757) as unpinned. #9724 could
not pin it: the refusal was unreachable through the wired engine (#9719). That
was fixed by PR #9797 and extended to the update verb by #9974, so the clause is
now pinnable AS WRITTEN.

Re-verified on the merged ref, then measured a detection gap the earlier pin
could not see: the existing dogfood block splits its rows entitled/not, so the
per-row gate answers with the same code and the block stays 5/5 GREEN with
`dispatchUnscopedMultiWrite` ablated from both registrations.

- second dogfood block seeds a caller entitled to EVERY matched row -- the only
  shape that separates "refused outright" from "every row was denied" -- plus
  the empty-table (zero-match) limb, with both sides held by a scoped sweep
- existing block: assert status + the #4757 message head, so it can tell the two
  refusals apart; `where: {}` re-documented as the per-row rule it actually is
- checklist: clause 3 flips to pinned; oracle `api` -> `test` and step 6 rewritten,
  because the REST lane cannot express the shape (#3897) and a schema rejection
  must not be scored as the refusal

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tests(attachments): pin the unpinned authorization clauses of the attachments chain — coverage gaps from QA run #9401

2 participants