Skip to content

Gate the test files against duplicate top-level declarations - #362

Merged
Apolloccrypt merged 1 commit into
mainfrom
fix/duplicate-test-declarations
Sep 2, 2026
Merged

Gate the test files against duplicate top-level declarations#362
Apolloccrypt merged 1 commit into
mainfrom
fix/duplicate-test-declarations

Conversation

@Apolloccrypt

Copy link
Copy Markdown
Owner

What broke, and why nothing caught it

Run 33667508924 put main red at 18:29Z. Two required checks, relay - unit suite (no native deps) and static, every name must exist, both on the same cause:

relay/test/pricing-page.test.js:498  Identifier 'tiers' has already been declared
tests/ui-truthfulness.test.mjs:821   Identifier 'pricingVisible' has already been declared

Every suite in this repo is a flat top-level script. Fixtures are declared with a bare const at column 0 and read all the way down the file. That shape is what makes the suites readable, and it also makes each file a single namespace that every open pull request writes into.

#333, #334, #336, #339 and #354 each appended a block to that namespace within the hour. Each one was green on its own, because a pull request is checked against the base it was opened on. The collision exists only in the merge, and it appeared on main the moment the last of them landed. A duplicate top-level const is an early SyntaxError, so the two files did not lose an assertion, they stopped running entirely and took the eslint gate down with them.

The dedupe itself is #359. This pull request is the gate.

The gate

scripts/check-test-declarations.sh, over tests/*.mjs, tests/*.js, relay/test/*.js, relay/crypto/*.test.js and admin/test/*.js. Two checks, for the two halves of the problem.

1. PARSE. node --check on every suite. It reports the file, the line and the cause, and adds the sentence eslint cannot: two top-level blocks declare the same name, which is what a merge collision looks like. In CI it also emits a ::error file=...,line=... annotation, so the job summary names the file instead of leaving a bare Parsing error at the tail of a whole-repo eslint run. That was the ask on static, every name must exist, and it is why this step runs ahead of eslint rather than after it.

2. SHADOW. Duplicate top-level var and function names. These are legal JavaScript in a CommonJS suite: the second declaration silently replaces the first, the file parses, eslint is clean, the suite runs green, and every assertion above the collision is now measured against the definition below it. Same merge collision, no red anywhere. Measured on this branch: appending a second function ok(...) to relay/test/pricing-page.test.js parses, lints and runs fine, and this scan reports function ok lines 23 592.

The SHADOW half reads column-0 declarations, which is a heuristic and says so in the header: these suites put every top-level declaration at column 0 and indent everything nested. Destructuring (const { default: tiers } = ...) is left to check 1, which handles it exactly.

Wired in two places: the static, every name must exist job, ahead of eslint, and as check 11 of tests/static-sanity.sh, the pre-commit gate.

Evidence

  • On the broken main (7c07b99) the guard is red and names exactly the two files and lines that took the run down.
  • Rebased on main with Link the product pages, state the hourly ceiling, and write one monthly form #359 in: OK: 110 suites parse and declare each top-level name once. No false positive anywhere in the repo, including the new blocks Link the product pages, state the hourly ceiling, and write one monthly form #359 moved into function scope.
  • Red on demand, verified by appending const tiers = require('../lib/tiers'); to relay/test/pricing-page.test.js again: the guard fails, tests/static-sanity.sh fails with it, and the CI annotation reads ::error file=relay/test/pricing-page.test.js,line=700.
  • SHADOW verified red on an injected duplicate function and an injected duplicate var, neither of which node or eslint objects to.
  • Battery on this branch, all green: root integration suites 152 tests (150 pass, 2 declared skips), relay unit suite 175 tests, admin unit suite 40 tests, pricing-fold and navigation-shell 5 tests under Chromium, eslint@9 over the repo, check-cache-bust, check-csp-inline, bash -n over every shell script, and tests/static-sanity.sh (11 checks, PASS).

The half a repo file cannot fix

This gate makes the failure fast, local and legible. It does not make it impossible, and it is worth being honest about why.

GitHub checks a pull request against a merge commit with the base at the time the check ran. Once #333 merged, the checks already recorded on #334 and the rest were green over a base that no longer existed, and nothing re-ran them. Five branches merged inside an hour against a moving main is exactly the window this opens.

Two settings close it, and both live in branch protection rather than in a file here:

  • Require branches to be up to date before merging on main. Every pull request then re-runs against the current base before it can land, and this guard fires on the branch instead of on main.
  • Or a merge queue, which does the same thing without serialising the humans.

The second, cheaper structural change is to the tests themselves: a suite per page group rather than one growing file per concern, or each block wrapped in its own function scope. Neither is in this pull request, because both touch the files #359 is fixing right now. Worth doing next; tests/ui-truthfulness.test.mjs is 981 lines and relay/test/pricing-page.test.js is 591, and both are still one shared namespace.

Every suite is a flat top-level script, so each open pull request appends to the same namespace. Five did within the hour on 2026-09-02 and main went red on the merge of the last one: const tiers twice in relay/test/pricing-page.test.js, const pricingVisible twice in tests/ui-truthfulness.test.mjs. Each branch was green on its own; the collision existed only in the merge, and two files that no longer parse take the unit suite and the eslint gate down with them.

scripts/check-test-declarations.sh runs node --check over tests/*.mjs, tests/*.js, relay/test/*.js, relay/crypto/*.test.js and admin/test/*.js, and reports the file, the line and the cause instead of the bare "Parsing error" eslint prints at the tail of a whole-repo run. It emits a GitHub error annotation for the file as well. A second scan reports duplicate top-level var and function names, which parse, lint clean, and silently replace the earlier definition the assertions above them were written against. That half no parser and no lint rule reports.

Wired into the "static, every name must exist" job ahead of eslint, and as check 11 of tests/static-sanity.sh. Clean over 110 suites; red on the two files that broke main.
@Apolloccrypt
Apolloccrypt merged commit 56694a1 into main Sep 2, 2026
10 checks passed
Apolloccrypt added a commit that referenced this pull request Sep 2, 2026
The three pages a buyer reaches after the homepage were written for someone
who already believes the product. /about opened on the mission and put the
founder in section 03, below the cryptography. /security opened on defence in
depth and kept the one row a buyer came for, the jurisdiction table, seven
screens down. /trust never said who it was for.

They now run the order docs/brand/messaging.md fixes: what this is, who it is
for, who is behind it, a next step, then the proof including the honest
limits. Copy, hierarchy and order only. No restyle. index.html, apply-nav.py
and js/nav-auth.js are untouched.

/about is the founder page.

  The lede is plain language: sign and send documents so only you and the
  recipient can read them, so anyone can check later that the document is
  genuine, on servers in Germany under EU law. Under it, in the first phone
  screen, who it is for, then Mick Beer with the title the guide fixes, then
  two buttons. Measured at 390x844: the founder sits at y=463 and the second
  button at y=608. The section number "00" is hidden in the mobile override
  instead of landing under the H1 as a stray number.

  Two sentences are gone. "The cryptography is post-quantum, which is the
  proof that it still holds up in ten years" was not a proof and not
  checkable, on the page whose argument is that everything on it is
  checkable. The founder paragraph explained the free plan with a
  jurisdiction claim ("should not depend on a US subscription"), which the
  guide forbids beside his name. In its place stands the paragraph #332
  landed on main while this branch was in review: the Community plan is his
  way of giving something back to society, the business plans pay for it,
  that is the whole arrangement, and it is why the Community plan is not a
  trial and has no end date. Taken from main verbatim, moved with the section
  into the top half, and pinned so the two copies cannot drift.

  The tier block names the free plan Community, which is what /pricing prints
  on the card since #328.

/security answers "why would I trust you" before it answers "how it works".

  The promise carries its own scope. It read "even if our own server is
  broken into, nobody can read your documents" flat out, while ten screens
  lower the page says the Chromium and Outlook extensions take a server-side
  encryption path. For an extension user the flat version is untrue today, so
  the exception now travels with the promise, in the hero.

  It also says what that exception costs the reader, in words rather than in
  ours: the extensions encrypt on our server, which means we can read what
  you upload through them until that is changed. "Treat those uploads as
  relay-side" was the internal phrasing, and relay is exactly the word this
  branch removed from the /trust hero. Neither hero uses it now, and a test
  says so.

  The first screen also carries who is behind the page (Paramantis Solutions
  B.V., Harderwijk, KvK 42115132, and the founder line) and the next step.
  The buttons used to sit at roughly y=11400 on a phone; "See pricing" is now
  at y=656 and "Verify a document" at y=716.

  The free plan is Community here too. A first version of this branch left
  "ParaSign Free and ParaSend Free cost EUR 0" standing at r.448, because the
  only check that forbade the old name was scoped to /about. That check now
  covers all three pages, and the sitewide sweep in ui-truthfulness gained the
  shape it was missing, so any page using it fails. The page description no
  longer sells "relay architecture" either.

  The audit block says what /docs#audits actually adds up to: three external
  audits in April 2026, two by R. Zwarts and one by Ryan Williams of Smart
  Cyber Solutions, forty findings, four of them critical, with the resolving
  commits in the table. The previous round claimed "the audit reports
  themselves are not published" and pinned it. That was false:
  docs/security-audit-2026-04.md is the full Smart Cyber Solutions writeup
  and it ships in the site tree. Only the raw pentest output is missing,
  which is what that document itself calls the raw report. Both pages now say
  that, and link the report.

  The CLOUD Act row and the EU-law card move to the data-path wording, as
  section 9.2 of the guide requires: no US provider in the data path, with
  Resend named as the one exception in the same breath. The old row read "not
  applicable: no US infrastructure, no US company", which is broader than
  /privacy allows.

/trust names its reader, then gives that reader somewhere to go.

  The hero addresses organisations running their own relay and anyone
  checking a supplier, and now offers them two buttons instead of one text
  link mid-paragraph (y=416 and y=476). The first sentence under the hero no
  longer says "the operator who runs the relay": relay is not a word a
  supplier reviewer knows.

  The page called itself Trust & Verification in its title and H1 while the
  social card and the structured data still said Trust & Transparency. #334
  then rewrote every head on the site, so the literal strings belong there and
  tests/seo-contract.test.mjs pins them. What this branch pins is the
  relation: the title, og:title, twitter:title and the JSON-LD name must name
  the page the same, and that name must contain the words the H1 uses.
  Punctuation and case are not the point; Transparency versus Verification
  was. Its plan sentence named two of the three paid
  ParaSign tiers; it names all three, and both free tiers as Community.

Tests

  tests/ui-truthfulness.test.mjs pins each of the above, and each one was
  sabotaged in place to confirm it goes red: the ten-year promise, the
  give-back sentence, the US-subscription framing, the Community rename, the
  CLOUD Act row, the Resend exception dropped from the card, the finding
  counts, the "reports not published" sentence, the auditor names on
  /security as well as /trust, the two hero buttons on each page, the
  who-is-behind line, the bounded promise, the /trust og:title and JSON-LD
  name, the jargon in the first screen, and the paid tiers. Twenty-seven
  sabotages, twenty-seven red, no gaps.

  tests/site-claims.test.mjs block 11 reads the numbers out of the /pricing
  tier card that owns them and requires /about to repeat them; it follows the
  Community rename and now also forbids "ParaSign Free" on /about.

  node --test over the CI glob: 173 pass, 0 fail. check-csp-inline,
  check-cache-bust and eslint exit 0, bron-seo/apply_seo_head.py --check
  reports 0 pages, and tests/static-sanity.sh is PASS on all eleven checks,
  including #362's new test-scope guard. At 390px all three pages have
  scrollWidth === clientWidth === 390.

Following main through four parallel merges

  #359 landed the deduplication this branch was carrying (the second const
  tiers in relay/test/pricing-page.test.js, and pricingVisible renamed to
  pricingText), so both local fixes are dropped in favour of main's.

  This branch's own block in tests/ui-truthfulness.test.mjs now sits inside a
  bare block scope and declares nothing at module level. Four PRs merged into
  that file in parallel on 2 September and two of them collided on a top-level
  const, which is a SyntaxError: not one assertion in the file runs, on any
  branch. A block that declares nothing at module level cannot do that to the
  next branch.

  tests/site-claims.test.mjs block 12 stopped spelling the units itself. #336
  renamed both /pricing section headings and put ParaSign first, and #359
  reworded "2 signatures per month" to "2 signatures a month". The block now
  finds the headings by product prefix, orders them by position, and lifts each
  fact out of the card as a whole phrase, so /about repeats what /pricing says
  rather than what this file guesses /pricing says. /about follows the new
  wording: "2 signatures a month", "24 hour link expiry", "up to 10 reads per
  link".

Still open, deliberately

  The eIDAS level (SES on /about, advanced (AES) in the /pricing FAQ) is a
  real contradiction and needs a decision, not a copy edit. This branch stops
  it spreading and pins that it stays on the one page that has always carried
  it.
Apolloccrypt added a commit that referenced this pull request Sep 2, 2026
The three pages a buyer reaches after the homepage were written for someone
who already believes the product. /about opened on the mission and put the
founder in section 03, below the cryptography. /security opened on defence in
depth and kept the one row a buyer came for, the jurisdiction table, seven
screens down. /trust never said who it was for.

They now run the order docs/brand/messaging.md fixes: what this is, who it is
for, who is behind it, a next step, then the proof including the honest
limits. Copy, hierarchy and order only. No restyle. index.html, apply-nav.py
and js/nav-auth.js are untouched.

/about is the founder page.

  The lede is plain language: sign and send documents so only you and the
  recipient can read them, so anyone can check later that the document is
  genuine, on servers in Germany under EU law. Under it, in the first phone
  screen, who it is for, then Mick Beer with the title the guide fixes, then
  two buttons. Measured at 390x844: the founder sits at y=463 and the second
  button at y=608. The section number "00" is hidden in the mobile override
  instead of landing under the H1 as a stray number.

  Two sentences are gone. "The cryptography is post-quantum, which is the
  proof that it still holds up in ten years" was not a proof and not
  checkable, on the page whose argument is that everything on it is
  checkable. The founder paragraph explained the free plan with a
  jurisdiction claim ("should not depend on a US subscription"), which the
  guide forbids beside his name. In its place stands the paragraph #332
  landed on main while this branch was in review: the Community plan is his
  way of giving something back to society, the business plans pay for it,
  that is the whole arrangement, and it is why the Community plan is not a
  trial and has no end date. Taken from main verbatim, moved with the section
  into the top half, and pinned so the two copies cannot drift.

  The tier block names the free plan Community, which is what /pricing prints
  on the card since #328.

/security answers "why would I trust you" before it answers "how it works".

  The promise carries its own scope. It read "even if our own server is
  broken into, nobody can read your documents" flat out, while ten screens
  lower the page says the Chromium and Outlook extensions take a server-side
  encryption path. For an extension user the flat version is untrue today, so
  the exception now travels with the promise, in the hero.

  It also says what that exception costs the reader, in words rather than in
  ours: the extensions encrypt on our server, which means we can read what
  you upload through them until that is changed. "Treat those uploads as
  relay-side" was the internal phrasing, and relay is exactly the word this
  branch removed from the /trust hero. Neither hero uses it now, and a test
  says so.

  The first screen also carries who is behind the page (Paramantis Solutions
  B.V., Harderwijk, KvK 42115132, and the founder line) and the next step.
  The buttons used to sit at roughly y=11400 on a phone; "See pricing" is now
  at y=656 and "Verify a document" at y=716.

  The free plan is Community here too. A first version of this branch left
  "ParaSign Free and ParaSend Free cost EUR 0" standing at r.448, because the
  only check that forbade the old name was scoped to /about. That check now
  covers all three pages, and the sitewide sweep in ui-truthfulness gained the
  shape it was missing, so any page using it fails. The page description no
  longer sells "relay architecture" either.

  The audit block says what /docs#audits actually adds up to: three external
  audits in April 2026, two by R. Zwarts and one by Ryan Williams of Smart
  Cyber Solutions, forty findings, four of them critical, with the resolving
  commits in the table. The previous round claimed "the audit reports
  themselves are not published" and pinned it. That was false:
  docs/security-audit-2026-04.md is the full Smart Cyber Solutions writeup
  and it ships in the site tree. Only the raw pentest output is missing,
  which is what that document itself calls the raw report. Both pages now say
  that, and link the report.

  The CLOUD Act row and the EU-law card move to the data-path wording, as
  section 9.2 of the guide requires: no US provider in the data path, with
  Resend named as the one exception in the same breath. The old row read "not
  applicable: no US infrastructure, no US company", which is broader than
  /privacy allows.

/trust names its reader, then gives that reader somewhere to go.

  The hero addresses organisations running their own relay and anyone
  checking a supplier, and now offers them two buttons instead of one text
  link mid-paragraph (y=416 and y=476). The first sentence under the hero no
  longer says "the operator who runs the relay": relay is not a word a
  supplier reviewer knows.

  The page called itself Trust & Verification in its title and H1 while the
  social card and the structured data still said Trust & Transparency. #334
  then rewrote every head on the site, so the literal strings belong there and
  tests/seo-contract.test.mjs pins them. What this branch pins is the
  relation: the title, og:title, twitter:title and the JSON-LD name must name
  the page the same, and that name must contain the words the H1 uses.
  Punctuation and case are not the point; Transparency versus Verification
  was. Its plan sentence named two of the three paid
  ParaSign tiers; it names all three, and both free tiers as Community.

Tests

  tests/ui-truthfulness.test.mjs pins each of the above, and each one was
  sabotaged in place to confirm it goes red: the ten-year promise, the
  give-back sentence, the US-subscription framing, the Community rename, the
  CLOUD Act row, the Resend exception dropped from the card, the finding
  counts, the "reports not published" sentence, the auditor names on
  /security as well as /trust, the two hero buttons on each page, the
  who-is-behind line, the bounded promise, the /trust og:title and JSON-LD
  name, the jargon in the first screen, and the paid tiers. Twenty-seven
  sabotages, twenty-seven red, no gaps.

  tests/site-claims.test.mjs block 11 reads the numbers out of the /pricing
  tier card that owns them and requires /about to repeat them; it follows the
  Community rename and now also forbids "ParaSign Free" on /about.

  node --test over the CI glob: 173 pass, 0 fail. check-csp-inline,
  check-cache-bust and eslint exit 0, bron-seo/apply_seo_head.py --check
  reports 0 pages, and tests/static-sanity.sh is PASS on all eleven checks,
  including #362's new test-scope guard. At 390px all three pages have
  scrollWidth === clientWidth === 390.

Following main through four parallel merges

  #359 landed the deduplication this branch was carrying (the second const
  tiers in relay/test/pricing-page.test.js, and pricingVisible renamed to
  pricingText), so both local fixes are dropped in favour of main's.

  This branch's own block in tests/ui-truthfulness.test.mjs now sits inside a
  bare block scope and declares nothing at module level. Four PRs merged into
  that file in parallel on 2 September and two of them collided on a top-level
  const, which is a SyntaxError: not one assertion in the file runs, on any
  branch. A block that declares nothing at module level cannot do that to the
  next branch.

  tests/site-claims.test.mjs block 12 stopped spelling the units itself. #336
  renamed both /pricing section headings and put ParaSign first, and #359
  reworded "2 signatures per month" to "2 signatures a month". The block now
  finds the headings by product prefix, orders them by position, and lifts each
  fact out of the card as a whole phrase, so /about repeats what /pricing says
  rather than what this file guesses /pricing says. /about follows the new
  wording: "2 signatures a month", "24 hour link expiry", "up to 10 reads per
  link".

Still open, deliberately

  The eIDAS level (SES on /about, advanced (AES) in the /pricing FAQ) is a
  real contradiction and needs a decision, not a copy edit. This branch stops
  it spreading and pins that it stays on the one page that has always carried
  it.
@Apolloccrypt
Apolloccrypt deleted the fix/duplicate-test-declarations branch September 5, 2026 18:56
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