From 0df7cb3db026472129871d7a7d56d8944b1b588f Mon Sep 17 00:00:00 2001 From: phillipc Date: Wed, 22 Apr 2026 23:50:34 +0200 Subject: [PATCH 1/7] feat: add plan to fill test-coverage gaps identified by `bun run test:coverage` --- plans/coverage-gap-fill.md | 229 +++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 plans/coverage-gap-fill.md diff --git a/plans/coverage-gap-fill.md b/plans/coverage-gap-fill.md new file mode 100644 index 000000000..af6b19272 --- /dev/null +++ b/plans/coverage-gap-fill.md @@ -0,0 +1,229 @@ +# Plan: Fill Test-Coverage Gaps Identified by `bun run test:coverage` + +## Context + +PR #364 wired up `bun run test:coverage` (vitest + `@vitest/coverage-v8`, +chromium project, source-map remapped to TS). The first run produced +`COVERAGE.md` (text summary) and `coverage/` (HTML + `coverage-summary.json` ++ `lcov.info`). Overall: **92.71% statements / 87.63% branches / 90.29% +functions / 92.82% lines** across the public `@tko/*` surface. + +That number hides a long tail of low-coverage files concentrated in the +provider stack and a few utils modules. Because TKO is a published low-level +framework with an unknown audience (AGENTS.md "Context for every agent"), gaps +in those layers are the riskiest: they're the substrate every binding sits on +and every consumer hits at startup. + +This plan closes the largest gaps first, in order of risk-adjusted return: +small files with simple uncovered branches, then the provider stack, then +DOM utilities. It is a coverage-driven plan, not a redesign — every commit +adds tests, no production code changes unless a test surfaces a real bug +(in which case the fix is a separate commit per AGENTS.md "Always Improve"). + +## Source of truth + +Regenerate with `bun run test:coverage` + +- `coverage/coverage-summary.json` — per-file totals (machine-readable) +- `coverage/lcov-report/index.html` — drill-down with uncovered line ranges +- `COVERAGE.md` — committed text snapshot + +Refresh before working on this plan: `bun run test:coverage` then re-read the +relevant `coverage/lcov-report//.ts.html` for exact line numbers. + +## Current gaps (snapshot, lines %) + +### Per-package (sorted, public `@tko/*` only) + +| Package | Lines covered | +|--------------------------|---------------| +| `provider` | 51.22% (21/41) | +| `provider.attr` | 69.23% (18/26) | +| `builder` | 70.59% (12/17) | +| `provider.databind` | 75.00% (6/8) | +| `utils` | 87.48% (601/687) | +| `utils.functionrewrite` | 88.89% (8/9) | +| `provider.component` | 91.11% (41/45) | +| `binding.foreach` | 91.48% (204/223) | +| `utils.parser` | 92.17% (636/690) | +| `binding.template` | 93.17% (232/249) | +| `lifecycle` | 93.33% (42/45) | +| `binding.core` | 94.16% (387/411) | +| (everything else ≥ 95%) | | + +### Worst individual files (lines %, then branches %) + +| File | Lines | Branches | Notes | +|------|-------|----------|-------| +| `utils/src/string.ts` | 0.00% (0/5) | n/a | `parseJson` has no spec | +| `binding.core/src/descendantsComplete.ts` | 33.33% (1/3) | 0% | only the `static override` getter is hit | +| `provider/src/Provider.ts` | 43.75% (14/32) | 38.23% | base class branches not exercised | +| `utils/src/dom/info.ts` | 61.90% (13/21) | 50.00% | tag/inline-detection branches | +| `utils/src/dom/html.ts` | 69.09% (38/55) | 77.19% | parser/insert edge cases | +| `provider.attr/src/AttributeProvider.ts` | 69.23% (18/26) | 50.00% | namespace + value-fn paths | +| `builder/src/Builder.ts` | 70.58% (12/17) | n/a | options merging branches | +| `utils/src/tasks.ts` | 70.83% (34/48) | 64.00% | error/runaway-loop guards | +| `utils/src/dom/event.ts` | 75.00% (30/40) | 67.44% | jQuery-vs-native branch | +| `binding.template/src/templateEngine.ts` | 75.00% (12/16) | 70.00% | template lookup fallbacks | +| `provider.databind/src/DataBindProvider.ts` | 75.00% (6/8) | n/a | error path | +| `provider/src/BindingHandlerObject.ts` | 77.77% (7/9) | 66.66% | registration branches | +| `binding.template/src/nativeTemplateEngine.ts` | 80.00% (8/10) | n/a | text-template fallback | +| `binding.core/src/submit.ts` | 81.81% (9/11) | n/a | non-form / no-handler | +| `binding.core/src/textInput.ts` | 83.33% (45/54) | 66.66% | IME / legacy paths | +| `computed/src/proxy.ts` | 83.33% (30/36) | 75.00% | proxy fallbacks | +| `observable/src/Subscription.ts` | 84.61% (11/13) | n/a | dispose-twice / teardown | + +Open `coverage/lcov-report//.ts.html` to see exact uncovered +ranges before writing tests — line numbers above are from the first run and +will drift as tests land. + +## Goals (measurable) + +- Bring every public `@tko/*` package to **≥ 90% lines** and **≥ 85% + branches** (target reflects the bar already cleared by 19 of 24 packages). +- No file in a public package below **70% lines** unless a follow-up issue + documents why (e.g. legacy compat path that needs a real DOM quirk). +- Overall totals: **≥ 95% lines / ≥ 92% branches**. +- `COVERAGE.md` regenerated and committed with the final commit. + +Non-goals: +- No new features, no refactors, no public API changes. +- No production code edits unless a test reveals a real bug (separate commit + per finding, with reproduction). +- No coverage thresholds enforced in CI yet — that's a separate plan once + the tail is closed (so CI doesn't flap on every drift). + +## Approach + +Per AGENTS.md "Implementation discipline": each commit adds tests for one +file (or a tight cluster of related files). Keep diffs reviewable. Match the +existing spec style of the package being tested — Chai `expect`, Sinon for +spies/timers, real-DOM fixtures via the existing helpers. + +Order by risk × ease: + +### Phase 1 — Trivial wins (single small file each) + +One commit per file. Each adds a `*.spec.ts` or appends to an existing one, +and re-runs `bun run test:coverage` for that package to confirm the file +hits ≥ 90%. + +1. `utils/src/string.ts` — `parseJson`: valid JSON, whitespace-only string, + non-string input, malformed JSON (throws). +2. `binding.core/src/descendantsComplete.ts` — `onDescendantsComplete` with + a function value (callback fires with `$element`), with a non-function + value (no-op), and `allowVirtualElements` getter already hit. +3. `observable/src/Subscription.ts` — disposing twice, disposing during + notification. +4. `binding.core/src/submit.ts` — non-form element, missing handler, + `preventDefault` return-true semantics. +5. `provider/src/BindingHandlerObject.ts` — register, override, lookup miss. + +### Phase 2 — Provider stack (highest-leverage gap) + +The provider package family is the lowest-covered cluster and the most +load-bearing — every binding string flows through it. Cover end-to-end +through the existing provider specs rather than poking internals. + +6. `provider/src/Provider.ts` — base-class branches: missing context, + non-element nodes, multiple handlers per node, error propagation. +7. `provider.attr/src/AttributeProvider.ts` — namespaced attributes, + value-as-function path, attributes that look like bindings but aren't. +8. `provider.databind/src/DataBindProvider.ts` — malformed `data-bind` + string error path (currently the only uncovered branch). +9. `provider.component/src/*` — push to ≥ 95% lines (small file count). + +### Phase 3 — Builder + binding edges + +10. `builder/src/Builder.ts` — options merging (defaults, overrides, + duplicate handler registration). +11. `binding.template/src/templateEngine.ts` and `nativeTemplateEngine.ts` + — anonymous template fallback, missing template id, non-element source. +12. `binding.core/src/textInput.ts` — IME composition events, legacy + propertychange path (covered branches → 85%+; document any path that + truly needs a real browser quirk we can't replay). + +### Phase 4 — DOM + tasks utilities + +13. `utils/src/dom/info.ts` — `tagNameLower`, inline-vs-block detection + branches. +14. `utils/src/dom/html.ts` — `parseHtmlFragment` table/tbody quirks, + `setHtml` with observable values, empty input. +15. `utils/src/dom/event.ts` — jQuery branch (already imported by spec + setup) vs native branch (skip jQuery in fixture). +16. `utils/src/tasks.ts` — runaway-loop guard (`MAX_OBSERVABLE_DEPTH`), + error in scheduled task (rethrow + drain). +17. `utils/src/dom/disposal.ts` — multi-disposer registration, + disposeNode on detached node. + +### Phase 5 — Tail cleanup + +18. Walk the remaining files between 85% and 95% lines, add the one or two + tests each needs to clear 95%. Skip anything that requires a real + browser quirk we can't reproduce in headless chromium — file a + follow-up issue and cite the URL in the commit message. + +## Files touched + +- `packages//spec/**/*.ts` — new or extended spec files (one per + commit, scoped to the file under test). +- `COVERAGE.md` — regenerated in the final commit of each phase (so the + history shows the gap closing). + +No production code changes expected. If a phase surfaces a real bug, the +fix lands in a **separate commit before the test commit** with its own +adversarial-review audit line. + +## Verification + +Per commit: +1. `bun run test` — full real-browser matrix passes (authoritative). +2. `bun run test:coverage` — new tests show in the report; targeted file + hits its phase target. +3. `bun run check` — biome clean. +4. `bunx tsc` — types clean. + +Per phase: +- `COVERAGE.md` regenerated and diff reviewed for unexpected regressions + in unrelated files (a sign a test is leaking state). + +Per merge: +- `bun run verify` passes (AGENTS.md "Before you start"). + +## Adversarial review checklist + +Each commit gets an audit line per AGENTS.md "Review Your Own Change +Adversarially". For coverage-fill commits, the second-agent prompt is: +"Here is a new spec file. Find a test that passes by accident, asserts on +the wrong thing, leaks state into other specs, or only exercises a happy +path the production code already handles trivially." Specific failure modes +to probe: + +- **Tests that pass without exercising the code** — assertion fires before + the call under test, or the call is wrapped in a `try` that swallows + failures. +- **Coverage gamed by `expect(true).to.be.true`** — covers the line, proves + nothing. +- **Real-DOM fixtures not torn down** — leaks listeners, breaks subsequent + specs in the same file (visible as flake in `bun run test`). +- **Reliance on test order** — Vitest may parallelize; assertions must not + depend on prior spec state. +- **Coverage delta claimed but not measured** — re-run `test:coverage` and + cite the before/after % in the commit message. +- **Backwards-compat surface drift** — if a new test pins behavior in + `@tko/build.knockout`, confirm it matches the legacy contract, not just + current TKO behavior. + +## Out of scope + +- Enforcing coverage thresholds in CI (`coverage.thresholds` in + `vitest.config.ts`). File once the tail is closed; otherwise the first + PR after merge will flap. +- Adding coverage to `builds/*` (those are bundles; their specs already + ride the per-package coverage). +- Switching coverage provider to istanbul. v8 + source maps is working; + `@vitest/coverage-istanbul` is available as a peer if a future need + arises. +- Coverage of the `cli-happy-dom` project. The chromium project is + authoritative; the happy-dom project is additive runtime coverage and + will be wired into a separate coverage report only if it diverges. From bdf2c599fb9d359feece17aee7d50c8b177e4419 Mon Sep 17 00:00:00 2001 From: phillipc Date: Thu, 23 Apr 2026 00:07:39 +0200 Subject: [PATCH 2/7] test(utils): cover parseJson string parsing edge cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parseJson lacked a spec entirely (string.ts: 0% lines / 0% branches). Adds stringBehaviors.ts covering: valid JSON parsing, primitives and arrays, leading/trailing whitespace, empty and whitespace-only strings, non-string runtime input (returns null instead of coercing or throwing), and malformed JSON (rethrows JSON.parse — pinned so a future 'be helpful and return null' change is caught). Lifts utils/src/string.ts from 0% to 100% lines and branches. Adversarial-review audit: subagent reviewed; cleared as solid (assertions exercise production code, type casts intentional). --- packages/utils/spec/stringBehaviors.ts | 48 ++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 packages/utils/spec/stringBehaviors.ts diff --git a/packages/utils/spec/stringBehaviors.ts b/packages/utils/spec/stringBehaviors.ts new file mode 100644 index 000000000..8e14bcc0d --- /dev/null +++ b/packages/utils/spec/stringBehaviors.ts @@ -0,0 +1,48 @@ +import { expect } from 'chai' + +import { parseJson } from '../dist' + +describe('parseJson', function () { + it('parses a valid JSON string into an object', function () { + expect(parseJson('{"a":1,"b":"two"}')).to.deep.equal({ a: 1, b: 'two' }) + }) + + it('parses primitives and arrays', function () { + expect(parseJson('42')).to.equal(42) + expect(parseJson('"hello"')).to.equal('hello') + expect(parseJson('true')).to.equal(true) + expect(parseJson('null')).to.equal(null) + expect(parseJson('[1,2,3]')).to.deep.equal([1, 2, 3]) + }) + + it('trims surrounding whitespace before parsing', function () { + expect(parseJson(' {"x":10} ')).to.deep.equal({ x: 10 }) + }) + + it('returns null for an empty string', function () { + expect(parseJson('')).to.equal(null) + }) + + it('returns null for a whitespace-only string', function () { + expect(parseJson(' \n\t ')).to.equal(null) + }) + + it('returns null when the input is not a string', function () { + // Inputs whose runtime type is not 'string' must short-circuit to null, + // not be coerced or thrown on. The signature is typed `string` but the + // function is callable from data-bind expressions where the runtime + // value can be anything. + expect(parseJson(undefined as unknown as string)).to.equal(null) + expect(parseJson(null as unknown as string)).to.equal(null) + expect(parseJson(42 as unknown as string)).to.equal(null) + expect(parseJson({} as unknown as string)).to.equal(null) + }) + + it('throws when the input is a non-empty malformed JSON string', function () { + // parseJson does not swallow JSON.parse errors — callers that want to + // tolerate bad input must wrap the call themselves. Pinning this + // behavior so a future "be helpful and return null" change is caught. + expect(() => parseJson('{not json')).to.throw() + expect(() => parseJson('undefined')).to.throw() + }) +}) From 8b02f23d38edd2bf8c0973d0496918f1c843d4f3 Mon Sep 17 00:00:00 2001 From: phillipc Date: Thu, 23 Apr 2026 00:07:51 +0200 Subject: [PATCH 3/7] test(observable): cover Subscription unsubscribe and closed getter Subscription.ts had 84.61% lines / 50% functions: the TC39 Observable-style unsubscribe() alias and the closed getter were never exercised by any spec. Adds two it() blocks to subscribableBehaviors: - unsubscribe() acts as an alias for dispose(): closed flips true and later notifications do not reach the callback. - closed reports false before dispose, true after. In combination with existing computed/observable specs that exercise disposeWhenNodeIsRemoved(), this lifts Subscription.ts to 100% lines and branches. Adversarial-review audit: an earlier draft asserted dispose() was idempotent ("survive a second dispose"). Subscription.dispose() has no _isDisposed guard, so a second call re-fires _disposeCallback and could emit duplicate 'asleep' notifications via afterSubscriptionRemove. The weak "does not throw" assertion was removed to avoid pinning unverified behavior; if double-dispose hardening is wanted it lands as a separate production fix with its own spec. --- .../observable/spec/subscribableBehaviors.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/observable/spec/subscribableBehaviors.ts b/packages/observable/spec/subscribableBehaviors.ts index 1f99ca1b0..6edb4fbc0 100644 --- a/packages/observable/spec/subscribableBehaviors.ts +++ b/packages/observable/spec/subscribableBehaviors.ts @@ -50,6 +50,27 @@ describe('Subscribable', function () { expect(notifiedValue).to.equal(undefined) }) + it('Should expose a TC39 Observable-style `unsubscribe()` alias for dispose', function () { + const instance = new subscribable() + let notified = 0 + const subscription = instance.subscribe(function () { + notified++ + }) + expect(subscription.closed).to.equal(false) + subscription.unsubscribe() + expect(subscription.closed).to.equal(true) + instance.notifySubscribers('value') + expect(notified).to.equal(0) + }) + + it('Should report `closed` as true once disposed', function () { + const instance = new subscribable() + const subscription = instance.subscribe(function () {}) + expect(subscription.closed).to.equal(false) + subscription.dispose() + expect(subscription.closed).to.equal(true) + }) + it("Should be able to specify a 'this' pointer for the callback", function () { const model = { someProperty: 123, From 0f6d7c1adc98187aadc0177c11dc19e738ccaf07 Mon Sep 17 00:00:00 2001 From: phillipc Date: Thu, 23 Apr 2026 00:08:17 +0200 Subject: [PATCH 4/7] test(binding.core): cover submit non-function and preventDefault paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit submit.ts had 81.81% lines / 50% branches: the throw-on-non-function guard and the 'handler returned true → skip preventDefault' branch were uncovered. Adds three it() blocks: - Bound value that is not a function throws with the documented message. - Handler returning true leaves event.defaultPrevented false (test attaches its own listener to read defaultPrevented BEFORE calling its own preventDefault, so the assertion observes the binding's behavior, not the test's own cleanup). - Handler returning a non-true value sets event.defaultPrevented true. Lifts submit.ts from 81.81% to 90.90% lines and from 50% to 83.33% branches. Remaining uncovered branches are the IE-only event.returnValue fallback (no preventDefault), unreachable in headless chromium. Adversarial-review audit: subagent verified the preventDefault tests read defaultPrevented before the test's own preventDefault call and so genuinely observe binding behavior. Subagent flagged a pre-existing state leak in this file (no afterEach restoring options.bindingProvider- Instance); that is an established pattern across binding.* specs and is out of scope for this commit. --- packages/binding.core/spec/submitBehaviors.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/binding.core/spec/submitBehaviors.ts b/packages/binding.core/spec/submitBehaviors.ts index e96837e37..1caa4fe09 100644 --- a/packages/binding.core/spec/submitBehaviors.ts +++ b/packages/binding.core/spec/submitBehaviors.ts @@ -39,4 +39,39 @@ describe('Binding: Submit', function () { expect(model.wasCalled).to.equal(true) expect(firstParamStored).to.equal(formNode) }) + + it('Should throw when the bound value is not a function', function () { + testNode.innerHTML = '
' + expect(() => applyBindings({}, testNode)).to.throw(/value for a submit binding must be a function/) + }) + + it('Should not prevent the default form submission when the handler returns true', function () { + testNode.innerHTML = "" + const handler = function () { + return true + } + applyBindings({ doCall: handler }, testNode) + const formNode = testNode.children[0] as HTMLFormElement + let defaultPrevented: boolean | undefined + formNode.addEventListener('submit', function (event) { + defaultPrevented = event.defaultPrevented + // stop the actual form navigation in case preventDefault was skipped + event.preventDefault() + }) + triggerEvent(formNode, 'submit') + expect(defaultPrevented).to.equal(false) + }) + + it('Should prevent the default form submission when the handler returns a non-true value', function () { + testNode.innerHTML = "" + applyBindings({ doCall: function () {} }, testNode) + const formNode = testNode.children[0] as HTMLFormElement + let defaultPrevented: boolean | undefined + formNode.addEventListener('submit', function (event) { + defaultPrevented = event.defaultPrevented + event.preventDefault() + }) + triggerEvent(formNode, 'submit') + expect(defaultPrevented).to.equal(true) + }) }) From 150685465d86edbc7c6210dddf4b9f9e5d7638f4 Mon Sep 17 00:00:00 2001 From: phillipc Date: Thu, 23 Apr 2026 00:08:51 +0200 Subject: [PATCH 5/7] test(provider): cover BindingHandlerObject set/get error paths BindingHandlerObject.ts had 77.77% lines / 66.66% branches: the two options.onError calls (object + extraneous value, and non-string non- object key) were never exercised, and the dotted-name lookup in get() was lightly covered. Adds BindingHandlerObjectBehaviors with six it() blocks: - set(name, value) registers a single handler (lookup via get). - set(object) registers multiple handlers via Object.assign. - set(object, value) reports onError with the documented message. - set(undefined) and set(42) report onError with the documented message. - get('attr.title') and get('attr.style.color') resolve to the root segment 'attr' (pins the dotted-handler convention). - get('nope') returns undefined for unknown handlers. beforeEach captures the original options.onError; afterEach restores it so a stub does not leak into other specs even if a test fails mid-run. Lifts BindingHandlerObject.ts from 77.77% to 100% lines and from 66.66% to 91.66% branches. Remaining uncovered branch is the Object.assign fall-through after the (object + extra value) error path, which the production code intentionally still runs. Adversarial-review audit: subagent verified options.onError stub setup and teardown isolate per test; no leakage path identified. --- .../spec/BindingHandlerObjectBehaviors.ts | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 packages/provider/spec/BindingHandlerObjectBehaviors.ts diff --git a/packages/provider/spec/BindingHandlerObjectBehaviors.ts b/packages/provider/spec/BindingHandlerObjectBehaviors.ts new file mode 100644 index 000000000..67b0550a9 --- /dev/null +++ b/packages/provider/spec/BindingHandlerObjectBehaviors.ts @@ -0,0 +1,72 @@ +import { expect } from 'chai' +import sinon from 'sinon' + +import { options } from '@tko/utils' + +import { BindingHandlerObject } from '../src' + +describe('BindingHandlerObject', function () { + let originalOnError: typeof options.onError + + beforeEach(function () { + originalOnError = options.onError + }) + + afterEach(function () { + options.onError = originalOnError + }) + + it('registers a single handler when set() is called with (name, value)', function () { + const handlers = new BindingHandlerObject() + const handler = { init: () => {} } + handlers.set('myHandler', handler) + expect((handlers as any).myHandler).to.equal(handler) + expect(handlers.get('myHandler')).to.equal(handler) + }) + + it('registers multiple handlers when set() is called with an object', function () { + const handlers = new BindingHandlerObject() + const a = { init: () => {} } + const b = { update: () => {} } + handlers.set({ a, b }) + expect(handlers.get('a')).to.equal(a) + expect(handlers.get('b')).to.equal(b) + }) + + it('reports onError when set() is called with both an object and a value', function () { + const handlers = new BindingHandlerObject() + const onError = sinon.stub() + options.onError = onError + handlers.set({ a: () => {} }, 'extraneous') + expect(onError.calledOnce).to.equal(true) + expect(onError.firstCall.args[0]).to.be.instanceOf(Error) + expect(onError.firstCall.args[0].message).to.match(/extraneous `value` parameter/) + }) + + it('reports onError when set() is called with a non-string, non-object key', function () { + const handlers = new BindingHandlerObject() + const onError = sinon.stub() + options.onError = onError + handlers.set(undefined as unknown as string) + expect(onError.calledOnce).to.equal(true) + expect(onError.firstCall.args[0].message).to.match(/bad binding handler type/) + + onError.resetHistory() + handlers.set(42 as unknown as string) + expect(onError.calledOnce).to.equal(true) + expect(onError.firstCall.args[0].message).to.match(/bad binding handler type/) + }) + + it('get() resolves dotted handler names to the root segment', function () { + const handlers = new BindingHandlerObject() + const attr = { init: () => {} } + handlers.set('attr', attr) + expect(handlers.get('attr.title')).to.equal(attr) + expect(handlers.get('attr.style.color')).to.equal(attr) + }) + + it('get() returns undefined for an unknown handler', function () { + const handlers = new BindingHandlerObject() + expect(handlers.get('nope')).to.equal(undefined) + }) +}) From 8eeeaedf6a32c0d0a5785853ea454d318b744156 Mon Sep 17 00:00:00 2001 From: phillipc Date: Thu, 23 Apr 2026 00:09:15 +0200 Subject: [PATCH 6/7] docs: refresh COVERAGE.md after phase 1 of coverage-gap-fill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 of plans/coverage-gap-fill.md is complete (4 of 5 items): - utils/string.ts: 0.00% → 100.00% lines - observable/Subscription.ts: 84.61% → 100.00% lines - provider/BindingHandlerObject.ts: 77.77% → 100.00% lines - binding.core/submit.ts: 81.81% → 90.90% lines Overall: 92.71% → 92.96% statements, 87.63% → 87.95% branches, 90.29% → 90.72% functions, 92.82% → 93.10% lines. Phase 1 item 2 (binding.core/descendantsComplete.ts) is dropped: the plan was annotated 'Allegedly dead code' and verification confirms it. applyBindings.ts:405-422 special-cases the descendantsComplete binding name and invokes the value accessor directly, bypassing the DescendantsCompleteHandler subclass entirely. onDescendantsComplete() has no caller in the framework. Writing a unit test that constructs the class and calls the method would lift the line counter without proving anything user-facing — that violates the plan's adversarial checklist ('coverage gamed'). Removal of the dead code, or rewiring the binding to use the lifecycle method, belongs in its own PR. Adversarial-review audit: not in scope (docs + plan only). --- COVERAGE.md | 273 ++++++++++++++++++------------------- plans/coverage-gap-fill.md | 3 +- 2 files changed, 138 insertions(+), 138 deletions(-) diff --git a/COVERAGE.md b/COVERAGE.md index 786ebfcd5..56748fe4e 100644 --- a/COVERAGE.md +++ b/COVERAGE.md @@ -1,143 +1,142 @@ # Coverage-Report Date: 2026-04-22 -------------------------------------|---------|----------|---------|---------|-------------------------------- -File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s -------------------------------------|---------|----------|---------|---------|-------------------------------- -All files | 92.71 | 87.63 | 90.29 | 92.82 | - bind/src | 97.1 | 91 | 98.82 | 96.36 | - BindingHandler.ts | 93.33 | 83.33 | 100 | 92 | 80,97 - BindingResult.ts | 100 | 100 | 100 | 100 | - DescendantBindingHandler.ts | 100 | 100 | 100 | 100 | - LegacyBindingHandler.ts | 96.22 | 88.57 | 92.3 | 94.87 | 19,110 - applyBindings.ts | 95.47 | 88.59 | 100 | 94.47 | 480,494,497,524-532 - arrayToDomNodeChildren.ts | 99.31 | 95.6 | 100 | 99 | 104 - bindingContext.ts | 98.86 | 96.38 | 100 | 98.48 | 110 - bindingEvent.ts | 100 | 66.66 | 100 | 100 | 20,29 - binding.component/src | 97.95 | 95.08 | 100 | 97.91 | - componentBinding.ts | 97.22 | 95.55 | 100 | 97.18 | 49,183 - slotBinding.ts | 100 | 93.75 | 100 | 100 | 32 - binding.core/src | 92.9 | 86.17 | 83.83 | 93.7 | - attr.ts | 94.11 | 88.23 | 100 | 94.11 | 24 - checked.ts | 98.27 | 96.29 | 100 | 98.27 | 108 - click.ts | 100 | 100 | 100 | 100 | - css.ts | 100 | 100 | 100 | 100 | - descendantsComplete.ts | 33.33 | 0 | 50 | 33.33 | 8-9 - enableDisable.ts | 100 | 100 | 100 | 100 | - event.ts | 100 | 96.66 | 100 | 100 | 59 - hasfocus.ts | 93.33 | 88.88 | 100 | 93.33 | 27,68 - html.ts | 100 | 100 | 100 | 100 | - let.ts | 100 | 100 | 100 | 100 | - options.ts | 97.33 | 94.59 | 100 | 97.33 | 60,194 - selectedOptions.ts | 100 | 90 | 100 | 100 | 38 - style.ts | 92.85 | 85.71 | 100 | 92.3 | 17 - submit.ts | 81.81 | 50 | 100 | 81.81 | 8,21 - text.ts | 100 | 100 | 100 | 100 | - textInput.ts | 82.43 | 65.78 | 57.14 | 80.35 | 17,39,48,112-134,152-154,156 - uniqueName.ts | 100 | 50 | 100 | 100 | 3 - using.ts | 100 | 100 | 100 | 100 | - value.ts | 87.27 | 88.63 | 80 | 92.15 | 30-32,106 - visible.ts | 100 | 100 | 100 | 100 | - binding.foreach/src | 92.08 | 85.1 | 95.34 | 91.47 | - foreach.ts | 92.08 | 85.1 | 95.34 | 91.47 | ...399-401,415-421,472,561,575 - binding.if/src | 96.47 | 93.87 | 92.59 | 96.15 | - ConditionalBindingHandler.ts | 95.65 | 100 | 83.33 | 95.23 | 40,62 - else.ts | 91.66 | 84.61 | 100 | 91.66 | 36 - ifUnless.ts | 100 | 100 | 100 | 100 | - with.ts | 100 | 87.5 | 100 | 100 | 31 - binding.template/src | 93.17 | 90.45 | 91.3 | 93.17 | - foreach.ts | 100 | 100 | 100 | 100 | - nativeTemplateEngine.ts | 80 | 50 | 100 | 80 | 22-23 - templateEngine.ts | 75 | 70 | 60 | 75 | 74-78,87,97 - templateSources.ts | 90.9 | 90.69 | 87.5 | 90.9 | 94,102-105 - templating.ts | 95.95 | 92.45 | 96.66 | 95.95 | 41,94,138,154,263-264,331 - builder/src | 70.58 | 50 | 33.33 | 70.58 | - Builder.ts | 70.58 | 50 | 33.33 | 70.58 | 99-102,301,318-321 - computed/src | 97.16 | 95.56 | 87.09 | 97.16 | - computed.ts | 99.13 | 97.32 | 97.29 | 99.13 | 360,485 - proxy.ts | 83.33 | 75 | 63.15 | 83.33 | 69-84 - throttleExtender.ts | 100 | 100 | 100 | 100 | - when.ts | 100 | 75 | 100 | 100 | 18 - lifecycle/src | 93.44 | 86 | 100 | 93.33 | - LifeCycle.ts | 93.44 | 86 | 100 | 93.33 | 28,47,100 - observable/src | 95.64 | 92.3 | 92.15 | 96.25 | - Subscription.ts | 84.61 | 100 | 60 | 84.61 | 35-38 - defer.ts | 92.3 | 75 | 100 | 92.3 | 9 - dependencyDetection.ts | 95.65 | 92.85 | 100 | 95.65 | 36 - extenders.ts | 95.83 | 88.88 | 100 | 95.83 | 27 - mappingHelpers.ts | 100 | 100 | 100 | 100 | - observable.ts | 100 | 95.83 | 100 | 100 | 165,209 - observableArray.changeTracking.ts | 100 | 95.16 | 100 | 100 | 53-57,92 - observableArray.ts | 94.93 | 92.3 | 95.23 | 94.73 | 205,240,295-296 - subscribable.ts | 87.83 | 83.33 | 80 | 91.3 | 16,189,202-208 - subscribableSymbol.ts | 100 | 100 | 100 | 100 | - provider.attr/src | 71.42 | 50 | 100 | 69.23 | - AttributeProvider.ts | 71.42 | 50 | 100 | 69.23 | 20,48,54-59,68-72 - provider.bindingstring/src | 97.05 | 100 | 83.33 | 95.23 | - BindingStringProvider.ts | 97.05 | 100 | 83.33 | 95.23 | 57 - provider.component/src | 91.66 | 90.62 | 91.66 | 91.11 | - ComponentProvider.ts | 91.66 | 90.62 | 91.66 | 91.11 | 30,43,57,72 - provider.databind/src | 75 | 50 | 100 | 75 | - DataBindProvider.ts | 75 | 50 | 100 | 75 | 18,25 - provider.multi/src | 100 | 93.75 | 100 | 100 | - MultiProvider.ts | 100 | 93.75 | 100 | 100 | 67 - provider.mustache/src | 93.75 | 78.89 | 84.61 | 96.11 | - AttributeMustacheProvider.ts | 83.92 | 71.11 | 80.76 | 89.74 | 46-49,100 - TextMustacheProvider.ts | 100 | 94.44 | 100 | 100 | 36 - mustacheParser.ts | 100 | 80.43 | 83.33 | 100 | 14-31,42-85 - provider.native/src | 100 | 92.85 | 100 | 100 | - NativeProvider.ts | 100 | 92.85 | 100 | 100 | 27 - provider.virtual/src | 100 | 100 | 100 | 100 | - VirtualProvider.ts | 100 | 100 | 100 | 100 | - provider/src | 51.78 | 45.65 | 47.82 | 51.21 | - BindingHandlerObject.ts | 84.61 | 66.66 | 100 | 77.77 | 9,17 - Provider.ts | 41.86 | 38.23 | 42.85 | 43.75 | 27,47,70-82,94,111-143 - utils.component/src | 95.36 | 92.22 | 97.56 | 95.33 | - ComponentABC.ts | 100 | 100 | 100 | 100 | - loaders.ts | 93.25 | 89.47 | 95.65 | 93.18 | 214-217,223,251,280,289 - registry.ts | 98 | 96.42 | 100 | 98 | 97 - utils.functionrewrite/src | 88.88 | 83.33 | 100 | 88.88 | - functionRewrite.ts | 88.88 | 83.33 | 100 | 88.88 | 16 - utils.jsx/src | 94.27 | 88.88 | 88.75 | 96.15 | - JsxObserver.ts | 96.66 | 92.93 | 92.42 | 97.61 | 133,150,220,286 - jsx.ts | 90.9 | 78.57 | 66.66 | 86.66 | 26,52 - jsxClean.ts | 80 | 68.18 | 75 | 92 | 53-54 - utils.parser/src | 92.01 | 89.4 | 94.95 | 92.17 | - Arguments.ts | 100 | 100 | 100 | 100 | - Expression.ts | 83.33 | 50 | 100 | 83.33 | 17 - Identifier.ts | 92 | 85.41 | 100 | 92 | 115-118,150 - Node.ts | 95.74 | 93.1 | 100 | 95.65 | 26,117 - Parameters.ts | 91.66 | 90.9 | 83.33 | 91.66 | 23,34 - Parser.ts | 91.27 | 89 | 95.83 | 91.43 | ...593,643,771,773,810,825,876 - Ternary.ts | 100 | 100 | 100 | 100 | - identifierExpressions.ts | 100 | 100 | 100 | 100 | - operators.ts | 89.02 | 85.71 | 92.1 | 89.74 | 52,64-68,95,101-116,142 - preparse.ts | 100 | 94.82 | 100 | 100 | 43,54,93 - utils/src | 87.57 | 82.19 | 89.39 | 87.58 | - array.ts | 92.07 | 88.23 | 93.33 | 92.63 | 74,99-105 - async.ts | 100 | 100 | 100 | 100 | - css.ts | 88.88 | 83.33 | 100 | 88.88 | 14 - error.ts | 88.88 | 50 | 100 | 88.88 | 10 - memoization.ts | 95.55 | 90.9 | 100 | 95 | 18,74 - object.ts | 97.5 | 88.23 | 100 | 97.5 | 42 - options.ts | 86.95 | 72.72 | 90 | 86.36 | 63-68,122 - string.ts | 0 | 0 | 0 | 0 | 6-12 - tasks.ts | 70 | 64 | 63.63 | 70.83 | 18-37 - utils/src/dom | 87.29 | 81.25 | 90.47 | 87.4 | - data.ts | 89.28 | 85.71 | 100 | 100 | 45,52,61 - disposal.ts | 87.93 | 77.5 | 81.81 | 87.03 | 74,104,117-123,134 - event.ts | 77.27 | 67.44 | 80 | 75 | 55,59-64,69,77-78,82-86 - fixes.ts | 100 | 100 | 100 | 100 | - html.ts | 70.68 | 77.19 | 75 | 69.09 | ...0,60-67,109,120-121,128,133 - info.ts | 61.9 | 50 | 100 | 61.9 | 16-24,46,54 - manipulation.ts | 100 | 72.72 | 100 | 100 | 26-34,45 - selectExtensions.ts | 97.56 | 97.43 | 100 | 97.43 | 39 - virtualElements.ts | 94.16 | 91.11 | 94.11 | 94.73 | 189-198,231 -------------------------------------|---------|----------|---------|---------|-------------------------------- +File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s +------------------------------------|---------|----------|---------|---------|----------------------- +All files | 92.96 | 87.95 | 90.72 | 93.1 | + bind/src | 97.1 | 91 | 98.82 | 96.36 | + BindingHandler.ts | 93.33 | 83.33 | 100 | 92 | 80,97 + BindingResult.ts | 100 | 100 | 100 | 100 | + DescendantBindingHandler.ts | 100 | 100 | 100 | 100 | + LegacyBindingHandler.ts | 96.22 | 88.57 | 92.3 | 94.87 | 19,110 + applyBindings.ts | 95.47 | 88.59 | 100 | 94.47 | 480,494,497,524-532 + arrayToDomNodeChildren.ts | 99.31 | 95.6 | 100 | 99 | 104 + bindingContext.ts | 98.86 | 96.38 | 100 | 98.48 | 110 + bindingEvent.ts | 100 | 66.66 | 100 | 100 | 20,29 + binding.component/src | 97.95 | 95.08 | 100 | 97.91 | + componentBinding.ts | 97.22 | 95.55 | 100 | 97.18 | 49,183 + slotBinding.ts | 100 | 93.75 | 100 | 100 | 32 + binding.core/src | 93.56 | 87.12 | 85.56 | 94.4 | + attr.ts | 94.11 | 88.23 | 100 | 94.11 | 24 + checked.ts | 98.27 | 96.29 | 100 | 98.27 | 108 + click.ts | 100 | 100 | 100 | 100 | + css.ts | 100 | 100 | 100 | 100 | + descendantsComplete.ts | 33.33 | 0 | 50 | 33.33 | 8-9 + enableDisable.ts | 100 | 100 | 100 | 100 | + event.ts | 100 | 96.66 | 100 | 100 | 59 + hasfocus.ts | 93.33 | 88.88 | 100 | 93.33 | 27,68 + html.ts | 100 | 100 | 100 | 100 | + let.ts | 100 | 100 | 100 | 100 | + options.ts | 97.33 | 94.59 | 100 | 97.33 | 60,194 + selectedOptions.ts | 100 | 90 | 100 | 100 | 38 + style.ts | 92.85 | 85.71 | 100 | 92.3 | 17 + submit.ts | 90.9 | 83.33 | 100 | 90.9 | 21 + text.ts | 100 | 100 | 100 | 100 | + textInput.ts | 84.72 | 66.66 | 61.53 | 83.33 | ...12-128,139-140,142 + uniqueName.ts | 100 | 50 | 100 | 100 | 3 + using.ts | 100 | 100 | 100 | 100 | + value.ts | 87.27 | 88.63 | 80 | 92.15 | 30-32,106 + visible.ts | 100 | 100 | 100 | 100 | + binding.foreach/src | 92.08 | 85.1 | 95.34 | 91.47 | + foreach.ts | 92.08 | 85.1 | 95.34 | 91.47 | ...15-421,472,561,575 + binding.if/src | 96.47 | 93.87 | 92.59 | 96.15 | + ConditionalBindingHandler.ts | 95.65 | 100 | 83.33 | 95.23 | 40,62 + else.ts | 91.66 | 84.61 | 100 | 91.66 | 36 + ifUnless.ts | 100 | 100 | 100 | 100 | + with.ts | 100 | 87.5 | 100 | 100 | 31 + binding.template/src | 93.17 | 90.45 | 91.3 | 93.17 | + foreach.ts | 100 | 100 | 100 | 100 | + nativeTemplateEngine.ts | 80 | 50 | 100 | 80 | 22-23 + templateEngine.ts | 75 | 70 | 60 | 75 | 74-78,87,97 + templateSources.ts | 90.9 | 90.69 | 87.5 | 90.9 | 94,102-105 + templating.ts | 95.95 | 92.45 | 96.66 | 95.95 | ...38,154,263-264,331 + builder/src | 70.58 | 50 | 33.33 | 70.58 | + Builder.ts | 70.58 | 50 | 33.33 | 70.58 | 99-102,301,318-321 + computed/src | 97.16 | 95.56 | 87.09 | 97.16 | + computed.ts | 99.13 | 97.32 | 97.29 | 99.13 | 359,484 + proxy.ts | 83.33 | 75 | 63.15 | 83.33 | 69-84 + throttleExtender.ts | 100 | 100 | 100 | 100 | + when.ts | 100 | 75 | 100 | 100 | 18 + lifecycle/src | 93.44 | 86 | 100 | 93.33 | + LifeCycle.ts | 93.44 | 86 | 100 | 93.33 | 28,47,100 + observable/src | 96.12 | 92.3 | 94.11 | 96.75 | + Subscription.ts | 100 | 100 | 100 | 100 | + defer.ts | 92.3 | 75 | 100 | 92.3 | 9 + dependencyDetection.ts | 95.65 | 92.85 | 100 | 95.65 | 36 + extenders.ts | 95.83 | 88.88 | 100 | 95.83 | 27 + mappingHelpers.ts | 100 | 100 | 100 | 100 | + observable.ts | 100 | 95.83 | 100 | 100 | 165,209 + observableArray.changeTracking.ts | 100 | 95.16 | 100 | 100 | 53-57,92 + observableArray.ts | 94.93 | 92.3 | 95.23 | 94.73 | 205,240,295-296 + subscribable.ts | 87.83 | 83.33 | 80 | 91.3 | 16,189,202-208 + subscribableSymbol.ts | 100 | 100 | 100 | 100 | + provider.attr/src | 71.42 | 50 | 100 | 69.23 | + AttributeProvider.ts | 71.42 | 50 | 100 | 69.23 | 20,48,54-59,68-72 + provider.bindingstring/src | 97.05 | 100 | 83.33 | 95.23 | + BindingStringProvider.ts | 97.05 | 100 | 83.33 | 95.23 | 57 + provider.component/src | 91.66 | 90.62 | 91.66 | 91.11 | + ComponentProvider.ts | 91.66 | 90.62 | 91.66 | 91.11 | 30,43,57,72 + provider.databind/src | 75 | 50 | 100 | 75 | + DataBindProvider.ts | 75 | 50 | 100 | 75 | 18,25 + provider.multi/src | 100 | 93.75 | 100 | 100 | + MultiProvider.ts | 100 | 93.75 | 100 | 100 | 67 + provider.mustache/src | 93.75 | 78.89 | 84.61 | 96.11 | + AttributeMustacheProvider.ts | 83.92 | 71.11 | 80.76 | 89.74 | 46-49,100 + TextMustacheProvider.ts | 100 | 94.44 | 100 | 100 | 36 + mustacheParser.ts | 100 | 80.43 | 83.33 | 100 | 14-31,42-85 + provider.native/src | 100 | 92.85 | 100 | 100 | + NativeProvider.ts | 100 | 92.85 | 100 | 100 | 27 + provider.virtual/src | 100 | 100 | 100 | 100 | + VirtualProvider.ts | 100 | 100 | 100 | 100 | + provider/src | 55.35 | 52.17 | 47.82 | 56.09 | + BindingHandlerObject.ts | 100 | 91.66 | 100 | 100 | 7 + Provider.ts | 41.86 | 38.23 | 42.85 | 43.75 | ...7,70-82,94,111-143 + utils.component/src | 95.36 | 92.22 | 97.56 | 95.33 | + ComponentABC.ts | 100 | 100 | 100 | 100 | + loaders.ts | 93.25 | 89.47 | 95.65 | 93.18 | ...17,223,251,280,289 + registry.ts | 98 | 96.42 | 100 | 98 | 97 + utils.functionrewrite/src | 88.88 | 83.33 | 100 | 88.88 | + functionRewrite.ts | 88.88 | 83.33 | 100 | 88.88 | 16 + utils.jsx/src | 94.27 | 88.88 | 88.75 | 96.15 | + JsxObserver.ts | 96.66 | 92.93 | 92.42 | 97.61 | 133,150,220,286 + jsx.ts | 90.9 | 78.57 | 66.66 | 86.66 | 26,52 + jsxClean.ts | 80 | 68.18 | 75 | 92 | 53-54 + utils.parser/src | 92.01 | 89.45 | 94.95 | 92.17 | + Arguments.ts | 100 | 100 | 100 | 100 | + Expression.ts | 83.33 | 50 | 100 | 83.33 | 17 + Identifier.ts | 92 | 85.41 | 100 | 92 | 115-118,150 + Node.ts | 95.74 | 93.1 | 100 | 95.65 | 26,117 + Parameters.ts | 91.66 | 90.9 | 83.33 | 91.66 | 23,34 + Parser.ts | 91.27 | 89 | 95.83 | 91.43 | ...71,773,810,825,876 + Ternary.ts | 100 | 100 | 100 | 100 | + identifierExpressions.ts | 100 | 100 | 100 | 100 | + operators.ts | 89.02 | 87.5 | 92.1 | 89.74 | ...-68,95,101-116,142 + preparse.ts | 100 | 94.82 | 100 | 100 | 43,54,93 + utils/src | 89.17 | 84.29 | 90.9 | 89.26 | + array.ts | 92.07 | 88.23 | 93.33 | 92.63 | 74,99-105 + async.ts | 100 | 100 | 100 | 100 | + css.ts | 88.88 | 83.33 | 100 | 88.88 | 14 + error.ts | 88.88 | 50 | 100 | 88.88 | 10 + memoization.ts | 95.55 | 90.9 | 100 | 95 | 18,74 + object.ts | 97.5 | 88.23 | 100 | 97.5 | 42 + options.ts | 86.95 | 72.72 | 90 | 86.36 | 63-68,122 + string.ts | 100 | 100 | 100 | 100 | + tasks.ts | 70 | 64 | 63.63 | 70.83 | 18-37 + utils/src/dom | 87.29 | 81.25 | 90.47 | 87.4 | + data.ts | 89.28 | 85.71 | 100 | 100 | 45,52,61 + disposal.ts | 87.93 | 77.5 | 81.81 | 87.03 | 74,104,117-123,134 + event.ts | 77.27 | 67.44 | 80 | 75 | ...-64,69,77-78,82-86 + fixes.ts | 100 | 100 | 100 | 100 | + html.ts | 70.68 | 77.19 | 75 | 69.09 | ...09,120-121,128,133 + info.ts | 61.9 | 50 | 100 | 61.9 | 16-24,46,54 + manipulation.ts | 100 | 72.72 | 100 | 100 | 26-34,45 + selectExtensions.ts | 97.56 | 97.43 | 100 | 97.43 | 39 + virtualElements.ts | 94.16 | 91.11 | 94.11 | 94.73 | 189-198,231 +------------------------------------|---------|----------|---------|---------|----------------------- =============================== Coverage summary =============================== -Statements : 92.71% ( 4457/4807 ) -Branches : 87.63% ( 2992/3414 ) -Functions : 90.29% ( 1024/1134 ) -Lines : 92.82% ( 3999/4308 ) +Statements : 92.96% ( 4467/4805 ) +Branches : 87.95% ( 3001/3412 ) +Functions : 90.72% ( 1027/1132 ) +Lines : 93.1% ( 4009/4306 ) ================================================================================ \ No newline at end of file diff --git a/plans/coverage-gap-fill.md b/plans/coverage-gap-fill.md index af6b19272..e9fb0d969 100644 --- a/plans/coverage-gap-fill.md +++ b/plans/coverage-gap-fill.md @@ -110,7 +110,8 @@ hits ≥ 90%. 1. `utils/src/string.ts` — `parseJson`: valid JSON, whitespace-only string, non-string input, malformed JSON (throws). -2. `binding.core/src/descendantsComplete.ts` — `onDescendantsComplete` with +2. Correction: Allegedly dead code -> + `binding.core/src/descendantsComplete.ts` — `onDescendantsComplete` with a function value (callback fires with `$element`), with a non-function value (no-op), and `allowVirtualElements` getter already hit. 3. `observable/src/Subscription.ts` — disposing twice, disposing during From ee7e40bd1b540ecf5a53429ef9499a0d24db2739 Mon Sep 17 00:00:00 2001 From: phillipc Date: Thu, 7 May 2026 15:27:55 +0200 Subject: [PATCH 7/7] docs(coverage): edit plan for filling test-coverage gaps --- ...fill.md => 2026-04-22-coverage-gap-fill.md} | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) rename plans/{coverage-gap-fill.md => 2026-04-22-coverage-gap-fill.md} (94%) diff --git a/plans/coverage-gap-fill.md b/plans/2026-04-22-coverage-gap-fill.md similarity index 94% rename from plans/coverage-gap-fill.md rename to plans/2026-04-22-coverage-gap-fill.md index e9fb0d969..106551480 100644 --- a/plans/coverage-gap-fill.md +++ b/plans/2026-04-22-coverage-gap-fill.md @@ -4,9 +4,9 @@ PR #364 wired up `bun run test:coverage` (vitest + `@vitest/coverage-v8`, chromium project, source-map remapped to TS). The first run produced -`COVERAGE.md` (text summary) and `coverage/` (HTML + `coverage-summary.json` -+ `lcov.info`). Overall: **92.71% statements / 87.63% branches / 90.29% -functions / 92.82% lines** across the public `@tko/*` surface. +`coverage/` (HTML + `coverage-summary.json` + `lcov.info`). Overall: +**92.71% statements / 87.63% branches / 90.29% +functions / 92.82% lines** across the public `@tko/*` surface (Date:2026-04-22). That number hides a long tail of low-coverage files concentrated in the provider stack and a few utils modules. Because TKO is a published low-level @@ -26,7 +26,6 @@ Regenerate with `bun run test:coverage` - `coverage/coverage-summary.json` — per-file totals (machine-readable) - `coverage/lcov-report/index.html` — drill-down with uncovered line ranges -- `COVERAGE.md` — committed text snapshot Refresh before working on this plan: `bun run test:coverage` then re-read the relevant `coverage/lcov-report//.ts.html` for exact line numbers. @@ -84,7 +83,8 @@ will drift as tests land. - No file in a public package below **70% lines** unless a follow-up issue documents why (e.g. legacy compat path that needs a real DOM quirk). - Overall totals: **≥ 95% lines / ≥ 92% branches**. -- `COVERAGE.md` regenerated and committed with the final commit. +- Final verification includes a fresh local `bun run test:coverage` run and + review of `coverage/` artifacts. Non-goals: - No new features, no refactors, no public API changes. @@ -102,7 +102,7 @@ spies/timers, real-DOM fixtures via the existing helpers. Order by risk × ease: -### Phase 1 — Trivial wins (single small file each) +### Phase 1 — Trivial wins (single small file each) (DONE) One commit per file. Each adds a `*.spec.ts` or appends to an existing one, and re-runs `bun run test:coverage` for that package to confirm the file @@ -168,8 +168,6 @@ through the existing provider specs rather than poking internals. - `packages//spec/**/*.ts` — new or extended spec files (one per commit, scoped to the file under test). -- `COVERAGE.md` — regenerated in the final commit of each phase (so the - history shows the gap closing). No production code changes expected. If a phase surfaces a real bug, the fix lands in a **separate commit before the test commit** with its own @@ -185,8 +183,8 @@ Per commit: 4. `bunx tsc` — types clean. Per phase: -- `COVERAGE.md` regenerated and diff reviewed for unexpected regressions - in unrelated files (a sign a test is leaking state). +- Fresh local `bun run test:coverage` run reviewed for unexpected + regressions in unrelated files (a sign a test is leaking state). Per merge: - `bun run verify` passes (AGENTS.md "Before you start").