LearnNewsExamplesServices
Frontmatter
id19028
titleThe fixed-sleep guard walks the tier with 1 sleep, not the two with 277
stateClosed
labels
enhancementaitestingbuild
assigneesneo-opus-ada
createdAtSep 21, 2026, 12:19 PM
updatedAtSep 21, 2026, 2:16 PM
githubUrlhttps://github.com/neomjs/neo/issues/19028
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 21, 2026, 2:16 PM

The fixed-sleep guard walks the tier with 1 sleep, not the two with 277

Closed v13.2.0/archive-v13-2-0-chunk-22 enhancementaitestingbuild
neo-opus-ada
neo-opus-ada commented on Sep 21, 2026, 12:19 PM

🔴 Author correction, 2026-09-21T11:30Z — the number this ticket was filed on was the wrong population. The original body said the guard "scans the tier with 1 waitForTimeout, not the two with 277". That 277 was a grep waitForTimeout count. It is not the guard's population: check-fixed-sleeps.mjs flags only unjustified waits at or above THRESHOLD_MS = 1000. Run against each tier with the guard's own findUnjustifiedSleeps, the real counts are component 0 and e2e 3.

That is my own "a name-grep is not a census" rule, violated in a ticket I filed hours after citing it. The scope below is rewritten around the measured population, and the half that actually explains #19024 has been separated out rather than smuggled in.

Context

Found while reviewing #19024, where a component spec used await page.waitForTimeout(400) with no stated reason. I went to cite the house rule and discovered the rule does not reach that tier.

Live latest-open sweep: checked the latest 20 open issues at 2026-09-21T10:22Z; no equivalent found.

The Problem

buildScripts/util/check-fixed-sleeps.mjs exists to stop a fixed second-scale wait that does not say what it is waiting for. Its workflow states the invariant it is built on:

"The invariant is scanned ⊆ watched. The guard walks every unit spec, so every unit spec must re-run it."

That sentence is true and it is about the unit tier only. SCAN_ROOT = 'test/playwright/unit' at line 103, and .github/workflows/fixed-sleep-lint.yml filters on test/playwright/unit/**/*.mjs. Two other tiers hold specs and neither is scanned.

What the guard would actually find if it looked. Measured at dev by calling the guard's exported findUnjustifiedSleeps against each tier's .mjs files:

tier spec files unjustified sites ≥ 1000 ms scanned today
unit 411 3 — all three already in the baseline yes
component 107 0 no
e2e 145 3 no

The three e2e hits, read at source rather than counted:

  • test/playwright/e2e/grid/RowPinning.spec.mjs:157 — await new Promise(r => setTimeout(r, 12000)) under the comment "Let it run while playwright scrolls". A genuine twelve-second unexplained fixed sleep. This is the real find.
  • test/playwright/e2e/dashboard/TearOutMatrixRows4To7NL.spec.mjs:248 — a 2000 ms setTimeout inside a Promise.race, i.e. a timeout guard rather than a wait.
  • test/playwright/e2e/utils/browser-test-helpers.mjs:77 — a MutationObserver timeout guard, cleared on success.

The latter two are exactly what the existing out-waits: justification marker is for; they want a marker, not a rewrite.

And the honest consequence for the finding that produced this ticket: widening the scan would not have caught it. #19024's waitForTimeout(400) is below THRESHOLD_MS. No change proposed here would have flagged it. That half is the threshold question and it is split out below.

The Architectural Reality

  • buildScripts/util/check-fixed-sleeps.mjs:103 — SCAN_ROOT, a single string.
  • :209 — SCAN_SURFACE, frozen and derived from SCAN_ROOT; the module's comment names it "the workflow-parity SSOT", and a sibling scanned-subset-of-watched spec reads it to demand the workflow watch everything the guard reads.
  • :243 — findUnjustifiedSleeps defaults its spec list to path.join(rootDir, SCAN_ROOT).
  • :104 — THRESHOLD_MS = 1000, the line that decides whether a wait is a site at all.
  • buildScripts/util/check-fixed-sleeps-baseline.json — three entries, all DragCoordinator.spec.mjs timers under test.
  • .github/workflows/fixed-sleep-lint.yml:9-23 — the paths: filters, which must stay a superset of whatever the scan becomes.

SCAN_ROOT being singular is the whole constraint: widening it is a plural change through SCAN_SURFACE, findUnjustifiedSleeps and the workflow filter together, and the parity spec is what keeps the "scanned ⊆ watched" invariant honest across the change.

The Fix

  1. SCAN_ROOT becomes SCAN_ROOTS, a list; SCAN_SURFACE and the default spec collection derive from it.
  2. Add e2e and component. e2e is where the real find is; component finds nothing today and is added so that a future second-scale sleep there is caught rather than discovered by a reviewer.
  3. Extend the workflow paths: filters to match, in both the pull_request and push blocks, so scanned stays a subset of watched.
  4. Disposition the three e2e hits rather than baselining them wholesale: RowPinning.spec.mjs:157 states what it waits for or stops being a sleep; the two timeout guards take the out-waits: marker the guard already accepts.
  5. Update the module docblock at line 3, which says "Fails a unit spec…", and the workflow comment that asserts the unit-only invariant.

The baseline file should not grow. Three sites are few enough to disposition, and a baseline entry is a debt marker — adding three when the alternative is fixing three would be using the mechanism to avoid the work it exists to expose.

Contract Ledger Matrix

SCAN_SURFACE is an exported, consumed surface — ScanSurfaceAlignment.spec.mjs imports it as the authority for what the mirror workflow must watch — so its shape change belongs in a ledger rather than only in a diff. @neo-gpt asked for this on PR #19031 and he is right that it was missing.

Target Surface Source of Authority Proposed Behavior Fallback / Edge Case Docs Evidence
SCAN_ROOTS (check-fixed-sleeps.mjs) this ticket replaces the SCAN_ROOT string with a frozen list; it is the single authority the workflow filter mirrors a root added here without the matching workflow paths: entry fails ScanSurfaceAlignment.spec.mjs, not silently module docblock + the constant's own comment 40 passed across guard/alignment/parity specs
SCAN_SURFACE (exported) check-guard-ci-parity.mjs still String[] of globs; now derived from SCAN_ROOTS rather than one interpolated root shape unchanged, so every existing consumer keeps working without edit JSDoc on the export every mirror selects every tracked file of its guard's surface passes
findUnjustifiedSleeps({files}) default this ticket with no files, collects across every root instead of one explicit files behaves exactly as before — the widening is opt-out by passing paths @param on options.files the guard's own spec passes unchanged; the AC-3 controls pass files implicitly via the CLI
fixed-sleep-lint.yml paths: SCAN_SURFACE gains the component and e2e globs in both the pull_request and push blocks a filter narrower than the scan reds the alignment spec rather than landing ungated the workflow's own trigger comment alignment spec's scanned ⊆ watched arm

No consumer signature changes, so nothing downstream needs a migration step.

Acceptance Criteria

  • AC-1 — the guard scans the unit, component and e2e tiers, and the workflow paths: filter covers everything it scans; the existing scanned-⊆-watched parity spec passes unchanged.
  • AC-2 — the three e2e sites are dispositioned in the PR, each by one of the three shapes the guard already sanctions: a justification marker, a rewrite to a condition wait, or one of the two marker-free exemptions (a named-constant delay, or a self-naming failure deadline). The PR body names which each received and why. The third arm was missing from the original wording and @neo-gpt's review is what found it: a Promise.race losing arm and a cleared observer deadline are not waits at all, so marking them out-waits: enters time nobody spends into a backlog that counts wall clock still being paid.
  • AC-3 — a deliberately added unjustified ≥ 1000 ms sleep in a component spec and in an e2e spec each fail the guard locally — two positive controls, both shown in the PR body, because component is the tier with zero current hits and therefore the one whose coverage is otherwise unfalsifiable.
  • AC-4 — check-fixed-sleeps-baseline.json gains no entries.
  • AC-5 — the module docblock and the workflow comment state which tiers are scanned and what threshold counts, replacing the unit-only wording.

Out of Scope — and the successor that owns the rest

The sub-second threshold question is deliberately not in this ticket, and it is the half that explains #19024. THRESHOLD_MS = 1000 means an unexplained 400 ms wait is not a site. The population behind that question, measured as timed calls rather than guard hits:

tier timed calls ≥ 1000 ms timed calls < 1000 ms
unit 22 147
component 4 60
e2e 52 178

385 sub-threshold calls is a different decision with a different blast radius, and folding it in here would turn a five-file change into a repo-wide one. A successor should ask whether the guard's name — second-scale — is still the right boundary, and it is the question that would have caught #19024.

Filing promise retired, 2026-09-21T11:45Z. This section previously read "Filing it is part of this lane." It is not, and leaving that standing would be a commitment nobody is tracking. #19030 — filed from this lane — covers the call-shape gap and explicitly excludes sub-second waits, so it is not the successor this sentence promised. The threshold question is recorded in #19030's Out of Scope with the measured population above and stays an open question rather than becoming a fifth ticket: two of the four filed from this session (#19026, #19030) are already parked unowned, and filing against a queue we are not draining is the anti-pattern. Anyone who wants it has the numbers to file it. Raised by @neo-gpt reviewing #19031.

Also out of scope: rewriting justified sleeps, and any tier that does not exist today.

Related

  • #19024 — the review that surfaced this, and the sleep that is below the threshold and therefore not fixed by it.
  • .husky/pre-commit — the local mirror the workflow backs up; it has to widen with the guard or the two disagree.

Origin Session ID: 2c9d83d3-7879-46f6-b49d-590b631d4f55

Retrieval Hint: query_raw_memories("fixed sleep guard scan root unit tier component e2e coverage threshold").

tobiu referenced in commit 26a4701 - "fix(build): the fixed-sleep guard walks the component and e2e tiers, not just unit (#19028) (#19031) on Sep 21, 2026, 2:16 PM
tobiu closed this issue on Sep 21, 2026, 2:16 PM