Skip to content

test(e2e): make the harness worktree-safe — one source for the port, one for the database - #49

Merged
guarzo merged 3 commits into
mainfrom
fix/e2e-worktree-isolation
Aug 3, 2026
Merged

test(e2e): make the harness worktree-safe — one source for the port, one for the database#49
guarzo merged 3 commits into
mainfrom
fix/e2e-worktree-isolation

Conversation

@guarzo

@guarzo guarzo commented Aug 3, 2026

Copy link
Copy Markdown
Owner

The e2e harness had two hardcoded shared resources. In a repo routinely worked from several concurrent worktrees, both collide.

Port 3111 + reuseExistingServer: !process.env.CI. If a sibling worktree held 3111, Playwright silently attached to it and the suite exercised that worktree's code against that worktree's database — and passed. Reproduced on this branch: a fully green 10 passed (11.3s) from a run that never touched its own code.

Two independent DATABASE_URL declarations. playwright.config.ts set the server's; e2e/helpers.ts had its own hardcoded default for seeding. Override one and not the other and the tests seed one database while the pages read another — every assertion fails on missing content.

Port isolation alone does not fix the second one: resetDb() TRUNCATEs on every beforeEach, so two worktrees on different ports still wipe each other's rows in a shared authgd_test.

What changed

  • e2e/env.ts — the single source. Hashes the worktree's absolute path into a dev-server port and a database port (stable within a worktree, distinct between worktrees), and derives the database URL. Imported by both playwright.config.ts and e2e/helpers.ts, so configuring one without the other is unrepresentable. E2E_PORT, E2E_DB_PORT, TEST_DATABASE_URL override.
  • e2e/provision.ts — starts a per-worktree Postgres container and migrates it. Runs at config load, not globalSetup: Playwright orders webServer startup before global setup.
  • e2e/server-guard.ts — resolves reuseExistingServer. Reuse is granted only when the process holding the port is proved to belong to this worktree and to be reading this run's database. Anything else is a hard error naming the override, never a silent attach.
  • scripts/e2e-clean.ts / npm run test:e2e:clean — remove this worktree's container.
  • docs/ops.md — replaces the superseded manual CREATE DATABASE authgd_test_mine workaround.

npm run test:e2e is unchanged and remains the single entry point.

Decisions

reuseExistingServer: conditional, not flat. Once the port is per-worktree a collision is unlikely, but hashes collide and people set E2E_PORT by hand — and the failure mode is a false green, which nothing prompts you to investigate. So reuse is earned, not assumed: the guard reads /proc/<pid>/cwd and /proc/<pid>/environ and restarts the server if it predates a recreated database or points at a different one. Under CI it stays false, the same answer !process.env.CI gave.

Auto-provision over a documented manual step. A manual step is exactly what docs/ops.md already had, and its existence is why the bug survived — a setup instruction that can be skipped will be. The cost is a Docker dependency; when Docker is absent the error names TEST_DATABASE_URL as the way out. The container is kept warm between runs rather than torn down: a throwaway would pay initdb plus a full migration every time, and would strand a reused dev server against a database that no longer exists.

CI

No change needed, and none made. ci.yml stands up a Postgres service on host 5433 and deliberately sets no override; GitHub Actions sets CI=true automatically. e2e/env.ts keeps that exact default behind an IS_CI branch — per-worktree isolation arrives by override, never by changing the shared default.

Verification

Two worktrees, concurrent suites:

### exit A=0  exit B=0
A: 10 passed (15.1s)   B: 10 passed (16.7s)

A 0.5s sampler across the run saw exactly two distinct tuples over 44 samples, with 21 iterations where both servers were live simultaneously:

port=3222 pid=2420838 cwd=.../fix-e2e-worktree-isolation db=postgres://...@localhost:5806/authgd_test
port=3549 pid=2420676 cwd=.../e2e-iso-verify            db=postgres://...@localhost:5695/authgd_test

Guard, with worktree B forced onto A's port while A's server was up:

Error: [e2e] http://localhost:3222 is held by pid 2454778, which does not belong to this worktree.
  this worktree: .../e2e-iso-verify
  port holder:   .../fix-e2e-worktree-isolation
Refusing to attach: the suite would test that process's code against its database and
report a pass that never touched this branch.

Cold run after npm run test:e2e:clean (container fully removed, reprovisioned and migrated): 10 passed (12.6s).
npm run typecheck clean. npm run lint — 0 errors, 4 pre-existing no-img-element warnings. npm run format:check — "All matched files use Prettier code style!"

Scope

No application code and no test assertions changed. tests/helpers/db.ts carries a third copy of the 5433 default for the unit suite; left alone deliberately as out of scope for this harness change.

Summary by CodeRabbit

  • New Features

    • Added isolated end-to-end test environments with worktree-specific application ports and PostgreSQL containers.
    • Added automatic database provisioning, readiness checks, migrations, and safe development-server reuse.
    • Added npm run test:e2e:clean to remove test containers and reset migration state.
  • Documentation

    • Documented E2E environment reuse, cleanup, configuration overrides, and port-conflict behavior.

guarzo added 2 commits August 3, 2026 18:16
playwright.config.ts and e2e/helpers.ts each carried an independent copy of
the test database URL and of port 3111. Overriding one and not the other
seeded one database while the pages read another; a sibling worktree holding
3111 caused Playwright to attach to it and report a pass that never touched
the branch under test.

e2e/env.ts is now the only place either value is decided, imported by both.
The dev server port and a dedicated Postgres container are derived from a hash
of the worktree path, so concurrent worktrees cannot collide. Reuse of an
existing server is granted only when its cwd and DATABASE_URL prove it belongs
to this worktree and this run.

CI is unaffected: it sets CI=true, which keeps the historical
localhost:5433/authgd_test default and disables provisioning.
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 22 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b5810ef1-73dd-4c4d-96dd-4a6b39aadd88

📥 Commits

Reviewing files that changed from the base of the PR and between 37dce52 and 3b4fc41.

📒 Files selected for processing (7)
  • .github/workflows/ci.yml
  • docs/ops.md
  • e2e/env.ts
  • e2e/provision.ts
  • e2e/server-guard.ts
  • playwright.config.ts
  • scripts/e2e-clean.ts
📝 Walkthrough

Walkthrough

Changes

Isolated E2E environment

Layer / File(s) Summary
Shared E2E environment contract
e2e/env.ts, e2e/helpers.ts
E2E settings now use deterministic worktree-specific ports, container names, database URLs, and shared base URLs.
Database provisioning and cleanup
e2e/provision.ts, playwright.config.ts, scripts/e2e-clean.ts, package.json, docs/ops.md
The runner creates or reuses a PostgreSQL container, applies migrations, reports provisioning details, and provides cleanup commands and documentation.
Guarded development server reuse
e2e/server-guard.ts, playwright.config.ts
Playwright validates server ownership and database configuration before reuse, and stops same-worktree servers when configuration changes require a restart.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • guarzo/authGD#2: Uses PostgreSQL-backed integration and end-to-end tests supported by this shared E2E database configuration.

Poem

A rabbit checks the ports at night,
While PostgreSQL starts just right.
Worktrees claim their private space,
Servers reuse with careful grace.
Migrations stamp the trail—
Clean commands finish the tale.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: making the E2E harness safe for concurrent worktrees.
Description check ✅ Passed The description explains the problem, implementation, design decisions, CI impact, verification results, and scope in sufficient detail.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/e2e-worktree-isolation
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/e2e-worktree-isolation

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/ops.md`:
- Around line 469-479: Update the earlier concurrency warning in docs/ops.md to
remove the claim that npm test and npm run test:e2e share authgd_test; retain or
qualify the warning about concurrent npm test runs if that remains accurate,
consistent with the isolated database behavior described in the e2e section.

In `@e2e/env.ts`:
- Around line 70-75: Update the APP_PORT and DB_PORT configuration in e2e/env.ts
to validate explicit E2E_PORT and E2E_DB_PORT overrides as finite integers
between 1 and 65535; reject invalid values such as non-numeric strings, negative
numbers, zero, fractions, and Infinity by throwing before the runner starts,
while retaining portFor defaults when the variables are unset.

In `@e2e/provision.ts`:
- Around line 38-44: Update docker() to invoke spawnSync with a bounded timeout
so blocked Docker commands cannot prevent ensureTestDatabase() from returning;
when the command times out, return a failed result with stderr describing the
timeout and including the Docker arguments for context, while preserving normal
stdout/stderr handling for completed commands.

In `@e2e/server-guard.ts`:
- Around line 129-139: Update the process handling around the databaseUrl and
dbRecreated checks in the server guard so stop(pid) is called only after
conclusive harness ownership is established, such as validating a PID record
created by the managed E2E server. When ownership cannot be proven, reject the
run with an actionable error instead of sending SIGTERM; preserve the restart
behavior for confirmed-owned processes.

In `@scripts/e2e-clean.ts`:
- Around line 18-25: Update the Docker cleanup result handling around
removed.status and removed.stdout in the e2e cleanup flow: distinguish
successful removal from a confirmed “no such container” response, remove the
migration stamp only in those cases, and throw for other Docker failures so
cleanup cannot silently proceed with a running container.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 62ee3c35-ee4d-4ef5-94d6-b7411e8a5a3e

📥 Commits

Reviewing files that changed from the base of the PR and between d4fb44b and 37dce52.

📒 Files selected for processing (8)
  • docs/ops.md
  • e2e/env.ts
  • e2e/helpers.ts
  • e2e/provision.ts
  • e2e/server-guard.ts
  • package.json
  • playwright.config.ts
  • scripts/e2e-clean.ts

Comment thread docs/ops.md
Comment thread e2e/env.ts Outdated
Comment thread e2e/provision.ts
Comment thread e2e/server-guard.ts
Comment thread scripts/e2e-clean.ts Outdated
@guarzo
guarzo merged commit ae78763 into main Aug 3, 2026
5 checks passed
guarzo added a commit that referenced this pull request Aug 4, 2026
The prior mobile pass proved there was no horizontal *page* scroll at
320px. It did not prove the admin tables were usable, and they were not:
globals.css had one layout breakpoint and no position:sticky anywhere.
To reach a row's controls you scrolled the region right, which carried
the character name off the left edge — so you pressed a 28px control
that changes someone's tier with nothing on screen saying whose. On a
product whose promise is "derole, don't boot", that is a
wrong-person-deroled bug waiting for a bad night.

Sticky headers on accounts and audit, and a pinned first column on both,
so the identifying value survives the scroll right. The pin needs an
opaque cell, which cannot inherit the translucent row hover; it is
flattened with a measured color-mix that lands within 1/255 of the
neighbouring cells. Not applied to the runs tables: getSyncStatus caps
them at five rows, so they have no vertical scroll range and a sticky
header there could not be tested.

Narrow paths for the two tables that forced the most sideways scroll.
Below 40rem the runs table gives up its 44rem floor and its ISO stamp
for a relative time, and audit does the same: 992px wide with a pin
eating 69% of the region becomes 544px with a pin at 28%, taking forced
scroll at 320px from 706px to 258px. The exact instant stays in a
visually-hidden span in both, so nothing is destroyed to fit. 258px is
the honest floor, not a way station — four legible columns plus a
non-zero Details column exceed a 286px region no matter how it is cut.

Two pre-existing defects surfaced on the way. Audit's Details column
measured 0px at every viewport: table-layout:fixed plus a colgroup
summing 47.75rem left nothing for the one unsized column, so the payload
disclosure was unclickable. And the accounts identity chain used ??,
which walks straight past an empty string, so a blank character name
rendered aria-label="Note for " — a row with no identity at all, in the
column whose entire job is saying whose tier is about to change. Blank
names are now filtered before the pick ("" sorts first and would win it)
and the tier, cryo, auto and note controls all carry the row identity.

Two cascade traps found by reading computed style rather than the
stylesheet: the runs and audit width floors were declared after the
40rem block, so every same-specificity narrow override was silently
void; and audit's colgroup widths were inline styles, which outrank a
media query outright. Both moved.

Every geometry assertion in this change was shown to fail without its
rule before being kept. toBeInViewport is not used for any of them — an
element painted over by a sticky cell still passes it, so occlusion is
measured rect-vs-rect via e2e/geometry.ts. One pre-existing audit
assertion was found to have been silently disabled by this branch's own
visually-hidden span, whose overflow:hidden matched the test's
clipped-cell skip guard; the guard now excludes it.

Superseded by main in the meantime and deliberately not carried over:
an E2E_PORT override for the test harness, which #49 replaced with
per-worktree hashed ports and server-ownership proof.
guarzo added a commit that referenced this pull request Aug 4, 2026
The prior mobile pass proved there was no horizontal *page* scroll at
320px. It did not prove the admin tables were usable, and they were not:
globals.css had one layout breakpoint and no position:sticky anywhere.
To reach a row's controls you scrolled the region right, which carried
the character name off the left edge — so you pressed a 28px control
that changes someone's tier with nothing on screen saying whose. On a
product whose promise is "derole, don't boot", that is a
wrong-person-deroled bug waiting for a bad night.

Sticky headers on accounts and audit, and a pinned first column on both,
so the identifying value survives the scroll right. The pin needs an
opaque cell, which cannot inherit the translucent row hover; it is
flattened with a measured color-mix that lands within 1/255 of the
neighbouring cells. Not applied to the runs tables: getSyncStatus caps
them at five rows, so they have no vertical scroll range and a sticky
header there could not be tested.

Narrow paths for the two tables that forced the most sideways scroll.
Below 40rem the runs table gives up its 44rem floor and its ISO stamp
for a relative time, and audit does the same: 992px wide with a pin
eating 69% of the region becomes 544px with a pin at 28%, taking forced
scroll at 320px from 706px to 258px. The exact instant stays in a
visually-hidden span in both, so nothing is destroyed to fit. 258px is
the honest floor, not a way station — four legible columns plus a
non-zero Details column exceed a 286px region no matter how it is cut.

Two pre-existing defects surfaced on the way. Audit's Details column
measured 0px at every viewport: table-layout:fixed plus a colgroup
summing 47.75rem left nothing for the one unsized column, so the payload
disclosure was unclickable. And the accounts identity chain used ??,
which walks straight past an empty string, so a blank character name
rendered aria-label="Note for " — a row with no identity at all, in the
column whose entire job is saying whose tier is about to change. Blank
names are now filtered before the pick ("" sorts first and would win it)
and the tier, cryo, auto and note controls all carry the row identity.

Two cascade traps found by reading computed style rather than the
stylesheet: the runs and audit width floors were declared after the
40rem block, so every same-specificity narrow override was silently
void; and audit's colgroup widths were inline styles, which outrank a
media query outright. Both moved.

Every geometry assertion in this change was shown to fail without its
rule before being kept. toBeInViewport is not used for any of them — an
element painted over by a sticky cell still passes it, so occlusion is
measured rect-vs-rect via e2e/geometry.ts. One pre-existing audit
assertion was found to have been silently disabled by this branch's own
visually-hidden span, whose overflow:hidden matched the test's
clipped-cell skip guard; the guard now excludes it.

Superseded by main in the meantime and deliberately not carried over:
an E2E_PORT override for the test harness, which #49 replaced with
per-worktree hashed ports and server-ownership proof.
guarzo added a commit that referenced this pull request Aug 4, 2026
The prior mobile pass proved there was no horizontal *page* scroll at
320px. It did not prove the admin tables were usable, and they were not:
globals.css had one layout breakpoint and no position:sticky anywhere.
To reach a row's controls you scrolled the region right, which carried
the character name off the left edge — so you pressed a 28px control
that changes someone's tier with nothing on screen saying whose. On a
product whose promise is "derole, don't boot", that is a
wrong-person-deroled bug waiting for a bad night.

Sticky headers on accounts and audit, and a pinned first column on both,
so the identifying value survives the scroll right. The pin needs an
opaque cell, which cannot inherit the translucent row hover; it is
flattened with a measured color-mix that lands within 1/255 of the
neighbouring cells. Not applied to the runs tables: getSyncStatus caps
them at five rows, so they have no vertical scroll range and a sticky
header there could not be tested.

Narrow paths for the two tables that forced the most sideways scroll.
Below 40rem the runs table gives up its 44rem floor and its ISO stamp
for a relative time, and audit does the same: 992px wide with a pin
eating 69% of the region becomes 544px with a pin at 28%, taking forced
scroll at 320px from 706px to 258px. The exact instant stays in a
visually-hidden span in both, so nothing is destroyed to fit. 258px is
the honest floor, not a way station — four legible columns plus a
non-zero Details column exceed a 286px region no matter how it is cut.

Two pre-existing defects surfaced on the way. Audit's Details column
measured 0px at every viewport: table-layout:fixed plus a colgroup
summing 47.75rem left nothing for the one unsized column, so the payload
disclosure was unclickable. And the accounts identity chain used ??,
which walks straight past an empty string, so a blank character name
rendered aria-label="Note for " — a row with no identity at all, in the
column whose entire job is saying whose tier is about to change. Blank
names are now filtered before the pick ("" sorts first and would win it)
and the tier, cryo, auto and note controls all carry the row identity.

Two cascade traps found by reading computed style rather than the
stylesheet: the runs and audit width floors were declared after the
40rem block, so every same-specificity narrow override was silently
void; and audit's colgroup widths were inline styles, which outrank a
media query outright. Both moved.

Every geometry assertion in this change was shown to fail without its
rule before being kept. toBeInViewport is not used for any of them — an
element painted over by a sticky cell still passes it, so occlusion is
measured rect-vs-rect via e2e/geometry.ts. One pre-existing audit
assertion was found to have been silently disabled by this branch's own
visually-hidden span, whose overflow:hidden matched the test's
clipped-cell skip guard; the guard now excludes it.

Superseded by main in the meantime and deliberately not carried over:
an E2E_PORT override for the test harness, which #49 replaced with
per-worktree hashed ports and server-ownership proof.
guarzo added a commit that referenced this pull request Aug 4, 2026
The prior mobile pass proved there was no horizontal *page* scroll at
320px. It did not prove the admin tables were usable, and they were not:
globals.css had one layout breakpoint and no position:sticky anywhere.
To reach a row's controls you scrolled the region right, which carried
the character name off the left edge — so you pressed a 28px control
that changes someone's tier with nothing on screen saying whose. On a
product whose promise is "derole, don't boot", that is a
wrong-person-deroled bug waiting for a bad night.

Sticky headers on accounts and audit, and a pinned first column on both,
so the identifying value survives the scroll right. The pin needs an
opaque cell, which cannot inherit the translucent row hover; it is
flattened with a measured color-mix that lands within 1/255 of the
neighbouring cells. Not applied to the runs tables: getSyncStatus caps
them at five rows, so they have no vertical scroll range and a sticky
header there could not be tested.

Narrow paths for the two tables that forced the most sideways scroll.
Below 40rem the runs table gives up its 44rem floor and its ISO stamp
for a relative time, and audit does the same: 992px wide with a pin
eating 69% of the region becomes 544px with a pin at 28%, taking forced
scroll at 320px from 706px to 258px. The exact instant stays in a
visually-hidden span in both, so nothing is destroyed to fit. 258px is
the honest floor, not a way station — four legible columns plus a
non-zero Details column exceed a 286px region no matter how it is cut.

Two pre-existing defects surfaced on the way. Audit's Details column
measured 0px at every viewport: table-layout:fixed plus a colgroup
summing 47.75rem left nothing for the one unsized column, so the payload
disclosure was unclickable. And the accounts identity chain used ??,
which walks straight past an empty string, so a blank character name
rendered aria-label="Note for " — a row with no identity at all, in the
column whose entire job is saying whose tier is about to change. Blank
names are now filtered before the pick ("" sorts first and would win it)
and the tier, cryo, auto and note controls all carry the row identity.

Two cascade traps found by reading computed style rather than the
stylesheet: the runs and audit width floors were declared after the
40rem block, so every same-specificity narrow override was silently
void; and audit's colgroup widths were inline styles, which outrank a
media query outright. Both moved.

Every geometry assertion in this change was shown to fail without its
rule before being kept. toBeInViewport is not used for any of them — an
element painted over by a sticky cell still passes it, so occlusion is
measured rect-vs-rect via e2e/geometry.ts. One pre-existing audit
assertion was found to have been silently disabled by this branch's own
visually-hidden span, whose overflow:hidden matched the test's
clipped-cell skip guard; the guard now excludes it.

Superseded by main in the meantime and deliberately not carried over:
an E2E_PORT override for the test harness, which #49 replaced with
per-worktree hashed ports and server-ownership proof.
guarzo added a commit that referenced this pull request Aug 4, 2026
The prior mobile pass proved there was no horizontal *page* scroll at
320px. It did not prove the admin tables were usable, and they were not:
globals.css had one layout breakpoint and no position:sticky anywhere.
To reach a row's controls you scrolled the region right, which carried
the character name off the left edge — so you pressed a 28px control
that changes someone's tier with nothing on screen saying whose. On a
product whose promise is "derole, don't boot", that is a
wrong-person-deroled bug waiting for a bad night.

Sticky headers on accounts and audit, and a pinned first column on both,
so the identifying value survives the scroll right. The pin needs an
opaque cell, which cannot inherit the translucent row hover; it is
flattened with a measured color-mix that lands within 1/255 of the
neighbouring cells. Not applied to the runs tables: getSyncStatus caps
them at five rows, so they have no vertical scroll range and a sticky
header there could not be tested.

Narrow paths for the two tables that forced the most sideways scroll.
Below 40rem the runs table gives up its 44rem floor and its ISO stamp
for a relative time, and audit does the same: 992px wide with a pin
eating 69% of the region becomes 544px with a pin at 28%, taking forced
scroll at 320px from 706px to 258px. The exact instant stays in a
visually-hidden span in both, so nothing is destroyed to fit. 258px is
the honest floor, not a way station — four legible columns plus a
non-zero Details column exceed a 286px region no matter how it is cut.

Two pre-existing defects surfaced on the way. Audit's Details column
measured 0px at every viewport: table-layout:fixed plus a colgroup
summing 47.75rem left nothing for the one unsized column, so the payload
disclosure was unclickable. And the accounts identity chain used ??,
which walks straight past an empty string, so a blank character name
rendered aria-label="Note for " — a row with no identity at all, in the
column whose entire job is saying whose tier is about to change. Blank
names are now filtered before the pick ("" sorts first and would win it)
and the tier, cryo, auto and note controls all carry the row identity.

Two cascade traps found by reading computed style rather than the
stylesheet: the runs and audit width floors were declared after the
40rem block, so every same-specificity narrow override was silently
void; and audit's colgroup widths were inline styles, which outrank a
media query outright. Both moved.

Every geometry assertion in this change was shown to fail without its
rule before being kept. toBeInViewport is not used for any of them — an
element painted over by a sticky cell still passes it, so occlusion is
measured rect-vs-rect via e2e/geometry.ts. One pre-existing audit
assertion was found to have been silently disabled by this branch's own
visually-hidden span, whose overflow:hidden matched the test's
clipped-cell skip guard; the guard now excludes it.

Superseded by main in the meantime and deliberately not carried over:
an E2E_PORT override for the test harness, which #49 replaced with
per-worktree hashed ports and server-ownership proof.
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