惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
博客园 - 司徒正美
WordPress大学
WordPress大学
爱范儿
爱范儿
小众软件
小众软件
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
罗磊的独立博客
博客园_首页
V
V2EX
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
Tailwind CSS Blog
大猫的无限游戏
大猫的无限游戏
The Cloudflare Blog
MyScale Blog
MyScale Blog
IT之家
IT之家
H
Help Net Security
Blog — PlanetScale
Blog — PlanetScale
Microsoft Security Blog
Microsoft Security Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Recent Announcements
Recent Announcements
F
Fortinet All Blogs
The GitHub Blog
The GitHub Blog
Y
Y Combinator Blog
人人都是产品经理
人人都是产品经理

Echo JS

billboard.js 4.1.0: Live resizing, configurable subchart, React subpath & CSP-safe worker From 1,256ms to 96ms: Fixing INP in a Massive React Dropdown GitHub - evoluteur/cymatics: Play a frequency and watch the sand settle into its Chladni figure, computed from the wave equation. Memdeklaro - The Basics of Decentralized Identity (DID) and Self-Sovereign Identity (SSI) How Railmid Works GitHub - evoluteur/platonic-solids: Turn the five Platonic solids in 3D, show their duals, read their measurements, and print the nets to fold your own. Sharing Application State in a URL GitHub - evoluteur/sacred-geometry: Sacred Geometry Generator: draw, tune, and export Vesica Piscis, Seed of Life, Flower of Life, Metatron's Cube, and the Golden Spiral as SVG Best of Self-Sovereign Identity: Digitalcourage, World Passport and Memdeklaro Reads Are Subscriptions - Migrating from Zustand to Coaction GitHub - evoluteur/binaural-beats: Simple web page to play binaural beats for sleep, meditation, relaxation, and focus: Delta, Theta, Alpha, Beta, and Gamma brainwave frequencies, with an optional pink or brown noise bed. toast-queue — Accessible, customizable toast notifications Building a High-Performance Data Grid in React, Vue, and Svelte I built a flight recorder for AI sessions React Authentication With JWT, Zustand, and Axios | JavaScript Tools Blog My idempotency library had one job. A dropped connection made it run the payment twice. "half-open" twice is not the same state: the bug that shaped breakwater 1.0 GitHub - evoluteur/evolutility-server-node: Framework for building REST APIs for CRUD with models rather than code (using Node.js, Express, and PostgreSQL). React Router v8 in Action: Lazy Loading and Nested Routes One $ for every environment | Xec The type-safe data layer for Kysely | Kysera What JavaScript Obfuscation in the AI Era | JavaScript Tools Blog Using Mongoose Studio with Apache Cassandra via Data API GitHub - trekhleb/yesbrainer: 🧠 A council of AI models for the decisions that aren't no-brainers — they answer in parallel, debate to consensus, or get judged to a verdict. Browser-only, open source, bring your own keys (BYOK), no backend. Node.js has plenty of circuit breakers. So why did I build another one? My Redis library said the write succeeded. Redis was down. GitHub - Techthos/gadget: Prebuilt, interactive HTML widgets for MCP Apps in Go — data tables and forms, self-contained in a single binary, host-themed, spec-compliant. GitHub - evoluteur/react-morph-charts: React component for bubble chart, bar chart, and pie chart, with animated morphing transitions between charts, on hover, and on window resize. Interactive Metaballs Tutorial
My test suite had 100% coverage. Mutation testing still f...
Pedro Rogério · 2026-08-03 · via Echo JS

breakwater is my resilience toolkit for Node.js — retry, circuit breaker, timeout, bulkhead, rate limiting, all composable, with observability built in. It's the library that stands between your service and a dying dependency, which means its tests are not decoration. If the circuit breaker's threshold logic is wrong, somebody's incident gets worse.

The suite had 100% line coverage on the core. Every branch tool was green. I was feeling pretty good about it.

Then I ran Stryker and it graded my suite at 80.52%.

Coverage measures execution. Mutation measures assertion.

If you haven't used mutation testing: the tool takes your source, makes one small change at a time — flips a >= to >, replaces a condition with true, empties a block — and runs your suite against each mutant. If some test fails, the mutant is killed. If everything stays green, it survived: your tests executed that line and asserted nothing about it.

Coverage tells you the line ran. Mutation testing tells you whether anyone would notice if the line were wrong. Those are very different claims, and the gap between them is exactly where production bugs live.

Stryker generated 1,114 mutants from my source. 199 survived.

What the survivors actually were

I went through every single one. They fell into five buckets, and each bucket taught me something different.

1. A real bug, in production, that day

A cluster of survivors pointed at the code that injects a registry name into policy options. Writing the test that should have killed them uncovered an actual defect: a named policy entry with an explicit display name reported the pipeline under one name and its rate limiter under another — the same policy showing up as two series in a metrics dashboard, in direct contradiction of the documented "explicit names always win" rule.

That fix shipped to npm the same week. No user had reported it. Coverage had been green the whole time.

2. A test that was lying about what it tested

One test existed specifically to pin a floating-point rounding fix: retryAfterMs must always be sufficient — wait exactly that long and you're admitted. Its comment proudly said the configuration reproduced a case where the naive ceil() lands one millisecond short.

Except it didn't anymore. The configuration (interval: 8737) no longer triggered the correction loop at all — the test passed with or without the code it claimed to protect. I searched the parameter space for a configuration that actually hits the rounding error (interval: 161, if you're curious: 161 * (1/161) is 0.999…) and rewrote the test around it.

A test that asserts something true but unrelated to its purpose is worse than no test: it's documentation that lies.

3. Documented contracts nobody ever asserted

The biggest bucket. Things the README and docs promised, that all worked, and that no test would defend against regression:

  • The circuit breaker's open, close and halfOpen events — only the generic stateChange was ever checked. Payloads, correlation IDs, one-event-per-transition: all unpinned.
  • Every distributed state-store path. The breaker has a pluggable StateStore designed for sharing circuit state across instances — stores that answer asynchronously, losing a compare-and-set race to a peer, another instance winning the half-open probe election. The interface existed, the local implementation was tested, and not one distributed behavior had a test.
  • Boundary semantics: a failure rate landing exactly on the threshold, a retry delay landing exactly on the deadline.
  • Jitter that actually spreads. My tests asserted delays stayed within [0, max] — a jitter implementation returning a constant would have passed while doing nothing about thundering herds.
  • A metrics collector implementing none of its optional callbacks. This one hid behind deliberate error-swallowing (monitoring must never break execution), so the fix asserts the error reporter stays silent too.

4. Dead code — the report doubles as a detector

Some survivors survived because no input can reach them. Two examples from the timeout policy: a guard in the timer callback protecting an assignment that nothing could ever read (the catch block re-checks the same condition first, and signals never un-abort), and one arm of a condition that was provably always false at its only call site.

I didn't take "provably" on faith — I rebuilt the old version and ran a differential matrix across cooperative/aggressive modes, abort orderings and rejection identities. Zero divergence. Both paths deleted. The invariant they pretended to guard now lives in exactly one place, with a test pinning its sharpest corner: an external cancellation that lands after the deadline fired still wins, and the timeout event stays silent.

5. Equivalent mutants — know when to stop

Not everything should be killed. { once: true } on an abort listener is unkillable by any black-box test, because an AbortSignal fires abort at most once in its lifetime — the mutant is behaviorally identical. Same for a fast-path that skips building a composite signal: pure allocation optimization.

Chasing those means asserting listener counts and internal allocations — testing implementation, which is the same disease as coverage-theater in the opposite direction. I documented each accepted survivor and moved on. The honest end state isn't 100%: it's every survivor analyzed and either killed or justified.

Final score: 95.11%, with the suite growing from 157 to 223 tests. And one bonus find that wasn't a mutant at all: two of my new tests would have hung CI forever instead of failing if their behavior regressed, because node:test has no default timeout. If you use the built-in runner, set one:

"test": "node --import tsx --test --test-timeout=10000 'tests/**/*.test.ts'"

The setup, if you want this on node:test

Stryker's tap runner drives the native runner just fine, TypeScript included:

{
  "testRunner": "tap",
  "tap": {
    "testFiles": ["tests/**/*.test.ts"],
    "nodeArgs": ["--import", "tsx", "--test", "--test-reporter=tap", "--experimental-test-isolation=none"]
  },
  "mutate": ["src/**/*.ts"],
  "coverageAnalysis": "perTest",
  "reporters": ["json", "html", "progress"],
  "timeoutMS": 20000
}

coverageAnalysis: "perTest" matters — it runs only the tests covering each mutant, which took a full run of my suite to under six minutes. I keep it as a manual gate (npm run test:mutation) rather than a CI job: it's a code-review tool, not a merge blocker.

Why I did all this before shipping observability

Because the newest release of breakwater is an observability feature, and shipping a tool that watches production on top of tests I couldn't trust felt absurd.

breakwater@0.7.0 adds breakwater/prometheus — ready-made prom-client collectors for every signal the library emits:

import { resilience } from 'breakwater'
import { prometheusCollector } from 'breakwater/prometheus'

const payments = resilience({
  name: 'payments-api',
  retry: { attempts: 3 },
  circuitBreaker: { consecutiveFailures: 5 },
  timeout: 2_000,
  metrics: prometheusCollector()
})

That's the whole integration. Eight metrics come out: executions and a duration histogram by outcome, retries, timeouts, fallbacks, rejections by reason (circuit_open, bulkhead_full, rate_limited…), the circuit state as an enum gauge, and a transitions counter. prom-client is an optional peer dependency — the core keeps its zero runtime dependencies, and importing plain breakwater never loads it.

Two design notes that came straight out of caring about honest observability:

Labels are low-cardinality by construction. Correlation IDs and attempt numbers never become label values. The only free-form label is your policy's name.

Absence means healthy. The state gauge is event-driven, so a circuit that has never tripped exports no state series yet. The docs say so explicitly, and the suggested alert targets the gauge (breakwater_circuit_state{state="open"} == 1) instead of increase() on a transitions counter — a counter series born by its first-ever transition needs two samples before increase() sees anything, which is exactly how you miss your first incident.

There's a ready-to-import Grafana dashboard covering all eight metrics, and a runnable demodocker compose up brings up a breakwater-protected app with a scripted outage every 90 seconds, Prometheus scraping it, and Grafana already provisioned. You get to watch the circuit open, fallbacks take over, and a half-open probe close the loop, on a real dashboard, without wiring anything.

And yes: the adapter's module was born under the mutation gate. First report: 100% — zero survivors. It turns out writing honest tests is much easier when you do it from the start.

Takeaways

  • Coverage is a floor, not a verdict. 100% coverage coexisted with 199 unasserted behaviors in my suite.
  • Read every survivor. The value isn't the score — it's the taxonomy: real bugs, lying tests, unpinned contracts, dead code, and equivalents you consciously accept.
  • Mutation reports find dead code that no linter flags, because reachability is a semantic property.
  • Set a test timeout with node:test. A test that hangs instead of failing is a CI outage waiting for a regression.
  • Don't chase 100%. Killing equivalent mutants means testing internals. Analyze, justify, stop.

breakwater is on GitHub and npm — MIT, docs for every policy, and the comparison with opossum/cockatiel is in the README. If you run it behind Prometheus, I'd genuinely like to hear what your dashboards catch.