Skip to content

test(pg-js): give the envelope gate an armored-body fixture - #238

Merged
rubenhensen merged 3 commits into
mainfrom
test/envelope-armored-fixture
Aug 19, 2026
Merged

test(pg-js): give the envelope gate an armored-body fixture#238
rubenhensen merged 3 commits into
mainfrom
test/envelope-armored-fixture

Conversation

@dobby-coder

@dobby-coder dobby-coder Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

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 main before touching anything: delete extractArmoredCiphertext and looksLikeArmoredPostGuard from src/email/extract.ts, run tests/envelope-archival.test.ts, and it reports 13 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.ts does unit-test both armor readers, so a deletion is not completely silent — the Node/Bun/Deno lanes would fail. What it tests is a hand-written block holding Zm9vYmFy, 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.json is not hand-written. HEAD cannot produce armor, and teaching createEnvelope to emit it just to have something to record is the change extract.ts's own header forbids. So the generator calls a published old sender instead: pg-js-legacy-armor in devDependencies is npm:@e4a/pg-js@0.10.0, the last version that armored bodies, aliased the same way the forward test aliases pg-js-reader-v1/v2.

Two things fell out of that:

  • 0.10.0 predates the tier split, so its result has no tier and no uploadUuid. The fixture records tier: null. It does carry the attachment, because senders of that era shipped both.
  • The version boundary in 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 in CHANGELOG.md about 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-54 asserted the tiers are exactly ['tier1','tier2','tier3']. An armor fixture is not a tier. Rather than loosening that, it is now three assertions:

  • tier-bearing fixtures still cover all three exactly (the armored one is filtered out, not added to the list)
  • every untiered fixture must carry an expect.armoredBase64Sha256, so tier: null is not a way to opt out of tier coverage
  • the corpus must hold at least one armored fixture, so the armor case cannot go quiet

And 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 extractArmoredCiphertext on 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.

mutation archival suite
extractArmoredCiphertext deleted outright 5 failed
always returns null 1 failed
hands back url-safe base64 1 failed
leaves the newlines in 1 failed
keeps the BEGIN marker in the payload 1 failed
armored fixture removed from the corpus 1 failed
untiered fixture with no armor expectation 2 failed
control: semantics-preserving rewrite of the extractor 21 passed

The 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 build green at the root; 352 tests in packages/pg-js
  • pnpm envelope:check: 5 fixtures present, 1 added on this branch, none modified or removed
  • node scripts/smoke.mjs: 8 checks passed on Node 22
  • No changeset: nothing under src/ changed, so there is nothing to publish, and etc/pg-js.api.md is unchanged.
  • packages/pg-js is one of the three packages CLAUDE.md lists as bypassing the prettier hook, and both files I edited already fail prettier --check on main. Left unformatted rather than reformatted, since that would bury the diff.
  • apps/tb-addon build fails locally for want of PKG_URL/CRYPTIFY_URL/POSTGUARD_WEBSITE_URL. It fails the same way on main.
  • The lockfile picked up one unrelated line, a 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 producedBy names a clean tree: the test and generator land first, the fixture is generated against that commit, then a comment fix.

🤖 Generated with Claude Code

dobby-coder Bot and others added 3 commits August 18, 2026 09:26
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>
@dobby-coder
dobby-coder Bot requested a review from rubenhensen August 18, 2026 09:30

@dobby-coder dobby-coder Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 separate some(...). So the armored fixture cannot paper over a missing tier, and tier: null cannot opt out of tier coverage.
  • a-skip-flag-guard-must-pin-both-sides: the disabling direction is pinned too. An absent armoredBase64Sha256 means the reader must recover nothing (expect(recovered).toBeNull()), and widening that branch across the whole corpus is caught by the some(...) 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 inside htmlBody and plainTextBody is an escaped \n, so a core.autocrlf=true checkout rewrites only structural whitespace that JSON.parse discards. I converted the fixture to CRLF and re-parsed it: identical digests, identical htmlBody. check-envelope-fixtures.mjs compares through git 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 the pg-js-reader-v1/v2 pattern already on main, it is dev-only so @e4a/pg-wasm@0.5.10 never reaches consumers, and no changeset is owed because nothing under src/ 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.

@rubenhensen
rubenhensen merged commit 578af8d into main Aug 19, 2026
30 checks passed
@rubenhensen
rubenhensen deleted the test/envelope-armored-fixture branch August 19, 2026 12:52
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.

the envelope gate has no armored-body fixture, so deleting armor support turns nothing red

1 participant