test(pg-js): give the envelope gate an armored-body fixture - #238
Conversation
The archival direction of the envelope-compat gate never called extractArmoredCiphertext, and the corpus held no armored body, so deleting armor support from src/email/extract.ts outright left the suite green — measured, not inferred. Every fixture now asserts what a reader must recover from its body: nothing for the four tier fixtures HEAD produces, or exactly the recorded block. The corpus-shape assertion keeps requiring tier1/tier2/tier3 exactly among tier-bearing fixtures, and additionally requires that an untiered fixture carry an armor expectation and that the corpus hold at least one, so the armored case cannot go quiet. The fixture itself lands in the next commit, generated from @e4a/pg-js 0.10.0 via the new pg-js-legacy-armor alias — HEAD cannot produce armor, and teaching it to would be the change extract.ts forbids. Refs #235 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bytes from the last published sender that emitted an in-body ASCII armor block. 0.10.0 predates the tier split, so the fixture carries tier: null and no uploadUuid; the attachment is present because senders of that era shipped both. Its expectation is derived from the payload, not from running HEAD's reader over the body — an expectation computed with the code under test is satisfied by definition, including when that code is wrong. Refs #235 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The check is whether the legacy sender wrote these exact bytes, so sharing HEAD's constants would make it agree with HEAD by construction. Refs #235 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
VERDICT: approve
Rules Dobby 2, cycle 1. No blocking findings. Review Dobby 2 handed over zero findings, and the per-rule sweep came back clean once its one candidate was checked.
Rules checked against this diff
Selected from the memory repo for a test/fixture plus devDependency change:
a-coverage-set-must-not-include-non-covering-references: the corpus-shape assertion keeps the two directions apart instead of reusing one set. The tier check draws only from tier-bearing fixtures, and the armor check is a separatesome(...). So the armored fixture cannot paper over a missing tier, andtier: nullcannot opt out of tier coverage.a-skip-flag-guard-must-pin-both-sides: the disabling direction is pinned too. An absentarmoredBase64Sha256means the reader must recover nothing (expect(recovered).toBeNull()), and widening that branch across the whole corpus is caught by thesome(...)assertion rather than passing vacuously.a-flags-only-assertion-cannot-pin-an-ordering-fix: the teeth are shown by mutation rather than by inspection, and the previous step reproduced the table independently.digest-pinned-file-needs-gitattributes: raised by the sweep, dropped after checking. The repo has no.gitattributes, but these digests are computed over JSON string values, not over the file's bytes. Every newline insidehtmlBodyandplainTextBodyis an escaped\n, so acore.autocrlf=truecheckout rewrites only structural whitespace thatJSON.parsediscards. I converted the fixture to CRLF and re-parsed it: identical digests, identicalhtmlBody.check-envelope-fixtures.mjscompares throughgit diff --name-status, which normalises as well.- dependency and release rules: the alias is genuinely used (
await import('pg-js-legacy-armor')in the generator), it follows thepg-js-reader-v1/v2pattern already on main, it is dev-only so@e4a/pg-wasm@0.5.10never reaches consumers, and no changeset is owed because nothing undersrc/changed. - prose rules: clean. The CLAUDE.md bullet matches the style the surrounding section already uses on main.
Recomputed rather than taken on trust
Both digests, from the fixture's own attachment: sha256 of the decoded 512 bytes is 6b9496...6a52, and sha256 of the base64 string is 9f441c...d83e. Both match expect, and the armor block stripped of whitespace equals the attachment base64 exactly.
The uncovered HTML-tag strip is stated in the PR body instead of being papered over. That reads right: catching it needs a fixture recorded from a client that re-wraps the block, not from a sender.
CI is settled (24 checks pass, nothing in flight), so this goes ready for review with the sign-off.
What this does
Closes #235.
The archival direction of the envelope-compat gate never called
extractArmoredCiphertext, and the corpus held no armored body. So the reader that COMPATIBILITY.md's stored-artifact guarantee exists to protect had no fixture behind it.Measured on
mainbefore touching anything: deleteextractArmoredCiphertextandlooksLikeArmoredPostGuardfromsrc/email/extract.ts, runtests/envelope-archival.test.ts, and it reports13 passed. Armor support could be removed outright without turning the gate red.One correction to the issue's framing, because it changes nothing about the fix but a reviewer should know:
tests/detect.test.tsdoes unit-test both armor readers, so a deletion is not completely silent — theNode/Bun/Denolanes would fail. What it tests is a hand-written block holdingZm9vYmFy, and it is named after the function, so a "remove dead code" pass takes the test out with the function. The append-only corpus is the part that cannot be deleted alongside what it protects, and that is where the hole was.The fixture
legacy-armored-body.jsonis not hand-written. HEAD cannot produce armor, and teachingcreateEnvelopeto emit it just to have something to record is the changeextract.ts's own header forbids. So the generator calls a published old sender instead:pg-js-legacy-armorin devDependencies isnpm:@e4a/pg-js@0.10.0, the last version that armored bodies, aliased the same way the forward test aliasespg-js-reader-v1/v2.Two things fell out of that:
tierand nouploadUuid. The fixture recordstier: null. It does carry the attachment, because senders of that era shipped both.extract.ts's comments ("pg-js >= 1.1 stopped emitting body armor") does not match the tarballs. 0.10.0 emits the armor div; 1.0.1 already does not, and there is nothing inCHANGELOG.mdabout the drop. I left the comment alone rather than rewrite prose about a history I only measured from the outside. If you know which boundary is right, say so and I will fix the comment.The expectation is derived from the payload, not from running HEAD's reader over the body. An expectation computed with the code under test is satisfied by definition, including when that code is wrong. The generator does its own whitespace strip and throws if the legacy block does not hold the fixture's ciphertext.
The corpus-shape assertion
envelope-archival.test.ts:50-54asserted the tiers are exactly['tier1','tier2','tier3']. An armor fixture is not a tier. Rather than loosening that, it is now three assertions:expect.armoredBase64Sha256, sotier: nullis not a way to opt out of tier coverageAnd every fixture now asserts what a reader must recover from its body: nothing for the four HEAD produces, or exactly the recorded block. That is what puts
extractArmoredCiphertexton the archival gate's import list, so deleting it now fails the suite at import.Verified by mutation, not by inspection
Seven mutations, applied one at a time, plus a control.
extractArmoredCiphertextdeleted outrightThe first row is the issue's "Done when", checked against the committed tree.
Not covered, stated rather than papered over: the reader's HTML-tag strip. 0.10.0 writes plain wrapped base64 inside the block, so a mutation removing
.replace(/<[^>]+>/g, '')stays green. That path exists for clients that re-wrap the block in transit, and catching it needs a fixture recorded from such a client rather than from a sender.Checks
pnpm typecheck,pnpm test,pnpm buildgreen at the root; 352 tests inpackages/pg-jspnpm envelope:check:5 fixtures present, 1 added on this branch, none modified or removednode scripts/smoke.mjs: 8 checks passed on Node 22src/changed, so there is nothing to publish, andetc/pg-js.api.mdis unchanged.packages/pg-jsis one of the three packages CLAUDE.md lists as bypassing the prettier hook, and both files I edited already failprettier --checkonmain. Left unformatted rather than reformatted, since that would bury the diff.apps/tb-addon buildfails locally for want ofPKG_URL/CRYPTIFY_URL/POSTGUARD_WEBSITE_URL. It fails the same way onmain.deprecated:marker on@xmldom/xmldom@0.8.13. That is registry metadata pnpm refreshed, not something the new alias pulled in, and CLAUDE.md says not to revert lines pnpm actually generates.Three commits, split so the fixture's
producedBynames a clean tree: the test and generator land first, the fixture is generated against that commit, then a comment fix.🤖 Generated with Claude Code