Skip to content

Work through DEFECTS.md: sixteen of seventeen entries - #163

Merged
roncodes merged 3 commits into
test/coverage-campaignfrom
fix/defects-sweep
Aug 25, 2026
Merged

roncodes merged 3 commits into
test/coverage-campaignfrom
fix/defects-sweep

Conversation

@roncodes

Copy link
Copy Markdown
Member

Works through every open entry in DEFECTS.md except #15, which is deferred by decision. Each fix is pinned by a test, and every status in the tracker now records what was decided and why.

The fixes

# Component What changed
1 chat-window/attachment A filename with no dot (README, Dockerfile) produced a null extension, getWithDefault asserted on it, and the component threw during render. Guarded the way file-icon.js already did.
2 overlay/header @titleEllipsis opts an open overlay into the truncation only minimizing used to trigger; @titleEllipsisLength sets the threshold the useEllipsis getter always encoded (default 15).
3 report-builder/condition-value Boolean columns got a free-text field. Added a True/False radio group.
4 layout/sidebar The ±2 statement wobble. See below.
5 layout/resource/panel The template wired @onToggle to an action that did not exist.
6 chat-tray The unread badge summed only the channels currently loaded; the server total now wins.
7 metadata-editor The template read @label directly, so the getter's 'Metadata' default never applied.
8 countdown restartCountdown was unreachable; both end callbacks now receive { restartFn }.
11 filters-picker #rebuildFilters(onColumn) was called with no argument at all three call sites.
14 template-builder/properties-panel Dropped value="target.value", which {{fn}} ignores.
9, 10, 12 — Deleted, as decided.

Details worth a second look:

  • use node to resolve asset import paths #2 — a minimized overlay still truncates regardless of the new argument, so no existing call site changes behaviour. The misspelled internal getter is now titleWithEllipsis.
  • patch intl-tel-input css import #3 — normalises values that round-trip as 'true' or 1, and gives each rendered editor its own radio group name via guidFor, so two boolean conditions in one report cannot clear each other.
  • updated github workflow #6 — the task is restartable, so a slow earlier response cannot overwrite a newer one, and a failure leaves the summed count in place rather than blanking the badge. Neither was in the brief; shipping without them would have traded a stale badge for a broken tray.
  • Fix stylesheets #7 — ?? is nullish-coalescing, so @label="" is now how you opt out of the heading, since omitting the argument no longer does.
  • Add resend verification email modal #8 — existing consumers that declare no parameters are unaffected.

#4, and why my first two attempts were wrong

sidebar.js moved the suite total by ±2 statements between identical runs.

The racing statements are not the requestAnimationFrame callback — that runs reliably. They are the cancel branches in flushResizeFrame() and teardown(), reached only when a frame is still pending, which depended on whether the browser painted first.

My first fix awaited the frame before releasing the gutter. That made it strictly worse: it guaranteed nothing was pending by the time stopResize ran, permanently closing the only path to those lines. The artifact caught it — the mechanism I had reasoned my way to matched the ±2 signature exactly and was still the wrong one.

The fix is two tests that dispatch synchronously, with no await between the events, so no paint can intervene. That idiom was already in use a few tests earlier in the same file.

sidebar.js: 199/215 → 203/215, with those four lines reported covered rather than sometimes-covered.

#16 — detection, not a cure

scripts/stamp-coverage-run.js clears coverage/ and stamps the run start; test:coverage runs it first. The gate now rejects a missing stamp, an unreadable one, an artifact older than the run, and a coverage-final.json that was never written — before reading a single percentage. The self-test grew from 10 cases to 17, including the dangerous one: a green suite that leaves the previous artifact in place.

The cause is untouched. 7 of 9 fast filtered runs this session produced no coverage/ directory at all, while every full run produced one — consistent with the addon POSTing coverage at test end and a short run tearing down first. That makes it a local-development tax rather than a CI risk, since CI runs the full suite.

The entry also retracts two things I claimed earlier in the session: rm -rf coverage does not make collection reliable (the 7-of-9 figure is with it), and the Node 18 ESM failure cannot affect CI, which pins NODE_VERSION: 22.x.

#15 — deferred

Confirmed as intended behaviour (fetch from a url with params) and left for its own session rather than half-built. The entry lists what it needs first: an endpoint contract, an auth story, loading and error states, a shape for query_response_path, something on the render side that consumes a query-backed table, and reconciliation with the __queries__ variable route that already solves the same problem.

The branch stays uncovered rather than suppressed — an istanbul ignore here would sit on the opening if, and ignore else there also swallows the variable branch, which real tests cover.

Three of my own tests failed before this landed

Recorded because each was a different kind of mistake and none was visible by reading:

  • A stale expectation. FontAwesome 6 renders the file-alt alias under its canonical name file-lines — the component was right, the assertion was wrong.
  • A harness bug. The countdown test's setInterval stub captures only the first 1000 ms interval, so a restart fell through to the real timer and leaked a live interval into the remaining ~4,700 tests.
  • A wrong assumption. Overlay reports a toggle only from toggle(); open() and close() each report their own event, which is exactly why the panel's context exposes toggle separately.

One assertion of mine was also replaced for being unable to fail (first.active ? first.value : undefined compares a value to itself when active).

Verification

Last complete run before this commit: 5130 tests, 5129 pass, the single failure being this batch's own panel test, fixed here. Coverage 96.13% statements, 93.12% branches, 98.60% functions, 96.37% lines, up from 95.79 / 92.73 / 98.15 / 96.02.

rm -f .eslintcache && pnpm run lint exits 0.

A final full run confirming the panel fix is in progress; I will post its numbers on this PR rather than leave these as the last word.

The comments indexed findings under an earlier scheme running to #160, which
this file restarted at #1. That numbering still appears in source comments —
full-calendar-test.js cites "DEFECTS.md #94 for Leaflet" — and resolved to
nothing once the comments went.

Appendix A maps the old numbering to what actually happened. I verified each
against current source rather than copying the claims across, and every finding
in that set has since been resolved: filter/multi-option's task-called-as-a-
function, overlay.resize clamping the wrong dimension, custom-field/form (the
file is gone), set-height's "px", leaflet's never-set `initialized`,
transition-to's self-satisfying assert, resource-context-panel's validate order,
the money arm, and the whole dead-member list. #148 in particular — the three
query-builder validate* actions nothing performed, so a panel kept sorting by
deselected columns — is wired to {{did-update}} in all three panels now.

Appendix B keeps the seven categories the remaining gaps fall into, including
the distinction that cost the most time: a default on a plain function is real
reachable surface, a default on a framework-invoked signature is dead, and
istanbul's branch map cannot tell them apart. Category 7 is annotated with what
has happened since — most of the "harness question" files turned out to be
drivable, so it is now flagged as unverified-until-tried.

Appendix C keeps the habits, led by the one that keeps proving itself: a green
test is not evidence that the branch you aimed at ran.

Nothing is imported as an open item. The tracker's open list is unchanged at
#1-#17.
Also names what the workflow's existing `test -s coverage/lcov.info` step does
and does not catch: it guards the crudest form of the missing-artifact case, but
it checks lcov.info rather than coverage-final.json and only tests existence, so
a stale artifact from a previous run passes it.
Every open entry in the tracker except #15, which is deferred by decision.
Each fix is pinned by a test, and every status in DEFECTS.md now records what
was decided and why.

FIXES

  #1  chat-window/attachment — a filename with no dot (README, Dockerfile)
      returned a null extension, getWithDefault asserted on it, and the
      component threw during render. Guarded the way file-icon.js already did.
  #2  overlay/header — @titleEllipsis opts an open overlay into the truncation
      that only minimizing used to trigger, and @titleEllipsisLength sets the
      threshold the useEllipsis getter has always encoded (default 15). A
      minimized overlay still truncates regardless, so no existing call site
      changes. The misspelled internal getter is now titleWithEllipsis.
  #3  report-builder/condition-value — boolean columns got a free-text field
      because the template had no boolean arm. Added a True/False radio group
      using the addon's own <RadioButton>, normalising values that round-trip
      as 'true'/1 and giving each editor its own group name so two boolean
      conditions cannot clear each other.
  #4  layout/sidebar — see below.
  #5  layout/resource/panel — the template wired @onToggle to an action that
      did not exist. Defined it, forwarding through contextComponentCallback
      like onOpen and onClose.
  #6  chat-tray — the unread badge summed only the channels currently loaded.
      getUnreadCount now runs after countUnread on load and reload so the
      server total wins. Restartable, so a slow earlier response cannot
      overwrite a newer one, and a failure leaves the summed count rather than
      blanking the badge.
  #7  metadata-editor — the template read @Label directly, so the getter's
      'Metadata' default never applied. It reads this.label now. Note that
      ?? is nullish-coalescing, so @Label="" is how you opt out of the heading.
  #8  countdown — restartCountdown was unreachable. It is an @action now and
      both end callbacks receive { restartFn }, so a consumer can write
      handleEnd({ restartFn }) { restartFn(); }.
  #11 filters-picker — #rebuildFilters(onColumn) was called with no argument at
      all three call sites, so the guard ran and the callback never did. It
      reads this.args.onColumn, which is the hook it was written for.
  #14 template-builder/properties-panel — dropped value="target.value", which
      {{fn}} ignores ({{action}} is what understands it).
  #9, #10, #12 — deleted, as decided: an orphaned duplicate of ActionItem's
      onClick, a superseded page-list getter, and a second country-select
      change action. Also removed the arrayRange import #10 orphaned.

#4 — AND WHY THE FIRST TWO ATTEMPTS WERE WRONG

sidebar.js moved the suite total by ±2 statements between identical runs. The
racing statements are NOT the requestAnimationFrame callback, which runs
reliably — they are the cancel branches in flushResizeFrame() and teardown(),
reached only when a frame is still pending, which depended on whether the
browser painted first.

My first fix awaited the frame before releasing the gutter. That made it
strictly worse: it guaranteed nothing was pending by the time stopResize ran,
permanently closing the only path to those lines. The artifact is what caught
it — the mechanism I had reasoned my way to matched the ±2 signature exactly
and was still the wrong one.

The fix is two tests that dispatch synchronously, with no await between the
events, so no paint can intervene. sidebar.js: 199/215 -> 203/215, and the
final run reports those four lines covered rather than sometimes-covered.

#16 — DETECTION, NOT A CURE

scripts/stamp-coverage-run.js clears coverage/ and stamps the run start;
test:coverage runs it first. The gate now rejects a missing stamp, an
unreadable one, an artifact older than the run, and a coverage-final.json that
was never written — before reading a single percentage. Self-test: 10 cases to
17, including the dangerous one, a green suite that leaves the previous
artifact in place.

The cause is untouched. 7 of 9 fast filtered runs this session produced no
coverage/ directory at all, while every full run produced one — consistent with
the addon POSTing coverage at test end and a short run tearing down first. That
makes it a local-development tax rather than a CI risk. DEFECTS #16 records the
unstarted next step and retracts two things I claimed earlier: `rm -rf coverage`
does not make collection reliable, and the Node 18 ESM failure cannot affect CI,
which pins Node 22.

#15 is deferred with a written-down list of what it needs first, including
reconciling it with the __queries__ variable route that already solves the same
problem.

THREE OF MY OWN TESTS FAILED BEFORE THIS LANDED

Recorded because each was a different kind of mistake and none was visible by
reading:
  - a stale expectation: FontAwesome 6 renders the file-alt alias as file-lines,
    so the component was right and the assertion was wrong;
  - a harness bug: the countdown stub only captures the first 1000ms interval,
    so a restart fell through to the real timer and leaked a live interval into
    the remaining ~4,700 tests;
  - a wrong assumption: Overlay reports a toggle only from toggle(); open() and
    close() each report their own event, which is why the context exposes
    toggle separately.
One assertion of mine was also replaced for being unable to fail.
@roncodes
roncodes merged commit 00c8f1d into test/coverage-campaign Aug 25, 2026
@roncodes
roncodes deleted the fix/defects-sweep branch August 25, 2026 06:48
@roncodes

Copy link
Copy Markdown
Member Author

Confirming run — clean

lint            exit 0
test:coverage   exit 0
# tests 5130
# pass  5130
# skip  0
# fail  0
metric covered / total %
statements 8366 / 8702 96.13%
branches 5826 / 6255 93.14%
functions 2199 / 2230 98.60%
lines 7940 / 8239 96.37%

One correction to the PR body: it says branches 93.12% (5825/6255). The clean run reads
93.14% (5826/6255) — one branch higher. The difference is the panel test that was failing when
those figures were taken: context.toggle() now runs, covering a path that a failing test never
reached. The body's other three metrics are unchanged.

sidebar.js is at 203/215 with the four cancel-branch statements covered, so #4 holds on a run
where nothing else was failing.

coverage:check exits 1 with 274 problems, as expected — and importantly, all 274 are real per-file
gaps. Not one is a staleness or missing-artifact complaint, which is the freshness guard doing its
job on a run that legitimately produced its artifact.

The files this PR touched

chat-window/attachment.js          100% 100% 100% 100%
report-builder/condition-value.js  100% 100% 100% 100%
chat-tray.js                       100% 100% 100% 100%
layout/resource/panel.js           100% 100% 100% 100%
table/cell/dropdown.js             100%  87.5% 100% 100%
overlay/header.js                  100% 88.88% 100% 100%
metadata-editor.js                98.16% 98.33% 100% 97.97%
filters-picker.js                 96.15% 83.87% 94.44% 96%
countdown.js                      95.23% 92.85% 100% 95.23%
layout/sidebar.js                 94.41% 85.71% 100% 96.60%
pagination.js                        84%  100% 100% 84%
country-select.js                 88.23%  100% 100% 87.5%

Four files reached 100% outright. The rest still carry gaps that predate this work — pagination.js
and country-select.js in particular went up in percentage terms only because deleting their dead
members shrank the denominator, not because anything new was covered there. Worth being explicit
about that: those two are not "nearly done", they simply have less dead weight now.

@roncodes

Copy link
Copy Markdown
Member Author

Second full run — identical, which is the real proof for #4

A second independent full run finished after the one above. Every figure matches:

run 1 run 2
pass / fail 5130 / 0 5130 / 0
statements 8366 / 8702 8366 / 8702
branches 5826 / 6255 5826 / 6255
functions 2199 / 2230 2199 / 2230
lines 7940 / 8239 7940 / 8239
sidebar.js 203 / 215 203 / 215
gate problems 274 274

This is worth more than the per-file number on its own. #4 was defined by two full runs
disagreeing
— 8479 vs 8477 covered statements, with sidebar.js the only file that differed
between the two artifacts. Two full runs now agreeing exactly, on that same file, is the direct
refutation of the original observation rather than an argument about mechanism.

Which matters here specifically, because the mechanism reasoning was wrong twice: first blaming the
requestAnimationFrame callback (it runs reliably), then applying a fix that closed the only path
to the lines it was meant to cover. Both times the reasoning matched the ±2 signature exactly. Only
the artifact settled it, and now only repeated runs confirm it.

roncodes added a commit that referenced this pull request Aug 25, 2026
…(DEFECTS #16)

The cause, at last, and it is a known upstream bug rather than anything odd
about this repo.

sendCoverage() POSTs ~1.9 MB to /write-coverage — 401 instrumented files,
because a per-file 100% gate needs forceModulesToBeLoaded() to evaluate
everything so untested files stay in the denominator. In CI mode testem tears
the browser down the moment QUnit reports the run finished, truncating the
upload mid-body. raw-body aborts, coverageHandler is never reached, and nothing
is written.

That is why it looked like flakiness rather than a bug: it depends on how long
the run took. After 139 tests testem has almost nothing to serialize and kills
the browser in milliseconds; after 5130 tests, emitting the results buys the
upload enough time to land. Measured 2 of 9 artifacts on fast filtered runs
against 100% on full runs, with `BadRequestError: request aborted` correlating
1:1 with every failure.

Testem.afterTests hands testem a callback it WAITS for, so the upload finishes
before teardown. It does not fire under --server, hence the branch on
config.APP.isRunningWithServerArgs.

Upstream, all with the identical raw-body:245 stack:
  ember-cli-code-coverage#420 (Aug 2024), #421, testem#1577
Our measurements are added to #420.

Measured: 2 of 2 fast filtered runs now produce an artifact with no abort.
Full suite unchanged — 5130 pass, 0 fail, lint 0.

Ruled out and recorded so nobody retries them: keepalive:true and sendBeacon
(the Fetch spec caps both at 64 KiB, ~30x under our payload), and moving
forceModulesToBeLoaded() to QUnit.begin (0 of 3, and against the guidance in
the addon's own source, which says to call it after the suite).

Also recorded a debugging trap: instrumenting coverageHandler shows nothing on a
failing run, which reads as "the POST never arrived". It does arrive and dies
inside bodyParser, before the handler. I drew the wrong conclusion from that and
had to retract it; instrument in front of bodyParser, not behind it.

The freshness gate from PR #163 stays. It is what made this failure loud instead
of silent, and it is still the guard against reading a stale report.

DEFECTS #18 opened: branch totals still vary by +/-1 between identical runs
(5825 vs 5826 across three otherwise-identical full runs). Same class of gate
blocker #4 was, culprit not yet located.
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