Skip to content

test(client): pin the adoption comparison to the same content-hash digests - #125

Open
XieX wants to merge 1 commit into
xie/agent-skillsfrom
xie/skills-adoption-hash-contract
Open

XieX wants to merge 1 commit into
xie/agent-skillsfrom
xie/skills-adoption-hash-contract

Conversation

@XieX

@XieX XieX commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Follow-on to test(client): pin the content-hash rule to the service's own digests (already on xie/agent-skills), closing the half of that rule it does
not reach.

TestContentHashContract pins verified_bytes, which the accessor and the
pre-write re-verify share. Adoption does not go through it: it hashes the bytes
it read off disk with an expression of its own —
hashlib.sha256(on_disk).hexdigest() == content_hash in skills_fs.py — and so
it is a second statement of the same rule that the accessor-side test cannot
reach.

It is also the statement with the most riding on it. Adoption decides whether an
unmanaged file is claimed or refused, so a comparison that normalized would
adopt a planted file whose bytes merely normalize to the delivered content,
leave those foreign bytes on disk, and report skipped_current — an action
documented to mean that the bytes on disk already are the resolved content.

What this adds

TestAdoptionContentHashContract in test_skills_fs.py, beside the existing
adoption class, using the same literal digest TestContentHashContract pins:

  • one file whose bytes are exactly the pinned content, which must be adopted
    as skipped_current and recorded;
  • the same three alterations — CRLF→LF, an appended trailing newline, NFD
    decomposition — each planted on disk and paired with the unaltered digest,
    which must be refused rather than adopted, with the planted bytes left
    untouched and nothing claimed into the manifest.

The digest is copied rather than imported, and the hazard content is written
with a é escape rather than the literal character. A literal's whole
value is that it is not derived from anything, and an escape cannot be changed
by a tool that re-normalizes the source file.

The fixture writes bytes, not text. Path.write_text opens in text mode,
where newline=None translates \n to os.linesep — a no-op on POSIX, but on
Windows it would rewrite the CRLF these cases are entirely about before the test
ran. The implementation is already careful in the other direction:
_read_regular_file passes O_BINARY so a Windows descriptor does not
translate CRLF on the read.

Two of the three variants are longer than the hazard content, so they are only
sound because the compare read stops at max_bytes + 1; at exactly max_bytes
their leading bytes would be the hazard content, hash equal, and wrongly adopt.

The fixture _hash helpers now say what they are. All three recompute the
rule with the implementation's own expression, which is what makes them useless
as an oracle. Each carries a docstring saying so and naming where the rule is
actually pinned.

Verification

By mutation on the adoption comparison. The source was restored and confirmed
clean afterwards.

mutation result
CRLF → LF before hashing the exact match is refused instead of adopted
trailing newline appended same
NFC-normalize before hashing the NFD-planted file is adopted as skipped_current

The third is the hole itself, and only the refusal cases catch it — which is
what confirms they are not redundant with the positive control.

ruff format --check and ruff check clean, mypy packages/*/src clean
(57 files), full suite 2166 passed / 11 skipped. Tests only — no src/ changes.

Parity

The TypeScript SDK carries the same two suites against the same two literal
digests (launchdarkly/js-ai-sdk#102). Both SDKs had closed the accessor half and
left adoption open; this closes it on this side.

Based on xie/agent-skills rather than main, which does not have the skills
feature.

🤖 Generated with Claude Code


Note

Overview
Tests-only follow-up that pins the filesystem adoption content-hash rule—the path that hashes on-disk bytes in skills_fs and is separate from TestContentHashContract / verified_bytes.

Adds TestAdoptionContentHashContract in test_skills_fs.py with the same literal hazard content and LaunchDarkly service digest as the accessor contract (copied, not imported). It asserts byte-exact unmanaged files are adopted as skipped_current, while CRLF→LF, an extra trailing newline, or NFD Unicode decomposition are refused (error, no manifest claim, bytes unchanged). Introduces _plant_unmanaged using write_bytes so Windows text mode does not rewrite CRLF fixtures.

Documents _hash in test_skills.py, test_skills_fdv2.py, and test_skills_fs.py as fixture convenience only—not an independent oracle—and points readers to the literal digest contract tests.

Reviewed by Cursor Bugbot for commit d6b0d77. Bugbot is set up for automated code reviews on this repo. Configure here.

…gests

`TestContentHashContract` covers `verified_bytes`, which the accessor and the
pre-write re-verify share. It does not reach the other place the rule is written
down: adoption hashes the bytes it reads off disk with an expression of its own
and compares that to `content_hash`.

That one decides whether an unmanaged file is claimed or refused, so a
comparison that normalized would adopt a planted file whose bytes merely
*normalize* to the delivered content, leave those foreign bytes in place, and
report `skipped_current` — an action documented to mean the opposite. Pins it
against the same literal digest, with the same three alterations asserted to be
refused rather than adopted.

The fixture writes bytes rather than `write_text`, whose text mode translates
`\n` to `os.linesep`: a no-op on POSIX, but on Windows it would rewrite the CRLF
these cases are about before the test ran.

Also documents the three fixture `_hash` helpers. Each is the expression the
implementation uses, which is what makes it useless as an oracle, and the
docstring says so and names where the rule is actually pinned.

Matches the TypeScript SDK, which carries the same two suites, so neither
implementation is the only one holding the cross-service rule.

Verified by mutation on the adoption comparison: normalizing line endings and
appending a final newline each refuse the exact match that should adopt;
NFC-normalizing adopts the NFD-planted file as `skipped_current`, which only the
refusal cases catch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant