Releases
Faultbox release history. Every tagged version ships with full release notes
on GitHub and a matching entry in
CHANGELOG.md.
For the next version in flight, see the
Issues board filtered by
version label (v0.16.x, etc.).
Current
- v0.18.0 — 2026-08-14 — Field-report fixes from the first onboarding of a large production Go service (Uber Fx, ~30 gateways, MySQL + Redis + Kafka). Seven reported issues, plus one found while building the regression corpus for the first of them. The headline: two of the reports - a host-binary SUT losing every outbound socket, and the proxy data plane freezing - were one defect, and it was not the proxy.
SECCOMP_IOCTL_NOTIF_RECVreturnsENOENTwhen a notification has already been discarded, which says nothing about the listener fd; the classifier matched it anyway by substring-searching the errno text, so the notification loop returnednilon the first one and stopped supervising for good - silently, because a nil error reads as a clean shutdown. The child kept its seccomp filter with nobody to answer it, so every intercepted syscall blocked forever. That accounts for reads and new dials both dying, for it never recovering, and for a busy connection pool always losing while a quiet client survived. A loop that ends while its target is alive now fails the test loudly, outranking every other verdict. A fault written the normal way now installs a filter: multi-line calls and spaces around=matched neither of the literal substrings the scan looked for, so the fault silently did nothing - and where the scan still cannot see a fault, the runtime now refuses it (FAULT_NOT_FILTERABLE) instead of installing something inert, so a fault either fires or the run fails saying why.mock_service()works from a containerized SUT (it bound host loopback and was injected aslocalhost:<port>, which resolves to the SUT itself). The Docker network survives the whole run - it was destroyed after each test and never recreated, so from test 2 every container landed on the default bridge with no name resolution. Timeouts are no longer blamed on faults that were not there, withTIMEOUT_NO_FAULT_FIREDandTIMEOUT_NO_FAULTScovering whatTIMEOUT_DURING_FAULTused to absorb.replayloads specs in a subdirectory, and theReplay:hint it prints is a commandreplayaccepts. Packet faults reach a containerized consumer - the gateway address was allocated behind a proxy-fault gate that packet faults are invisible to. Also:grpc.call()completed its first round trip against a real server (it handed[]byteto grpc-go’s proto codec and failed client-side before reaching the wire), and all 13 step protocols now have a real-server spec. SeeCHANGELOG.md.
v0.17.x — Agent-first surface
- v0.17.0 — 2026-07-30 —
faultbox checkand diagnostics for suites that cannot fail (RFC-052 gaps 1, 2 and 8).faultbox check spec.starvalidates a spec without launching a process, pulling an image, or needing Docker - milliseconds against the tens of seconds a run costs. The runtime could always do this; it simply was not exposed, so the only way to learn a spec was malformed was to run it. Findings carry machine-readable codes and a suggested next move, exit 0 for clean or warnings-only and 2 for errors, and the same code backs the MCP toolcheck_spec- a check that behaved differently through MCP would give an agent a wrong model of the tool. Two new diagnostics come from an uncomfortable finding: a CI spec exercised a broken Postgres client on every pull request for three releases and passed, because its only assertion was that a query fails under an injected fault - which a client that cannot connect at all satisfies identically. Its own comment stated the intent, and the care is what hid the bug.NO_POSITIVE_CONTROLfires when an interface is stepped but no test ever asserts a step on it succeeds; it is suite-level, which is what makes it new - a single fault-injection test asserting failure is correct, but a suite where that is the only assertion an interface receives proves nothing, and no per-test lint sees it.TEST_NO_ASSERTIONSfires when a test passed having evaluated nothing. Also ships the deprecation removals promised for v0.14.0 and never made. SeeCHANGELOG.md.
v0.16.x — Filesystem observation
-
v0.16.1 — 2026-07-30 — The protocol audit: eleven fixes, ten of them pre-existing. v0.16.0 fixed two Postgres bugs that survived because no spec had ever asserted on the result of a database step; this release repeats that audit across the other twelve protocols - one spec per protocol, a real server, every result checked.
ready()could never succeed for eight protocols (a v0.16.0 regression - the check resolved toredis://host:portand eight plugins dialled that string verbatim, burning the whole timeout; Redis went from a 60 s failure to a 407 ms pass). MySQL steps could not authenticate or select a database, and once the credential fix let a step reach the command phase for the first time it exposed a second bug: the MySQL proxy hung forever on every result set, and becauseStop()waited unbounded on the connection WaitGroup, one stuck handler hung the entire run after the test body had finished. All twelve proxies now bound teardown atProxyStopTimeout(5 s) and emitproxy_stop_timeoutrather than hanging silently. Also fixed:args=was accepted by the spec loader and silently dropped for container services; dict and list step arguments were mangled (every int, float, bool and nested value inside a dict became"", self-consistently enough that round trips looked correct); the NATS proxy corrupted every line it forwarded by mixingbufio.Scannerline-stripping with\n-only writes against a CRLF protocol; NATSpublishreported success without confirming delivery;ready()made a single protocol attempt for MongoDB and Cassandra at the moment the server is least likely to be up; and every container leaked its anonymous volume - measured at 290 orphaned volumes and 18.7 GB on the dev VM, which filled a 30 GB disk and surfaced as a flaky test three specs later. New: credentials read fromenv=for MySQL, Redis, MongoDB, ClickHouse, Cassandra and NATS, resolved in one place so healthchecks and steps authenticate identically. Docs:faultbox setup-traceadded to the CLI reference, feature-manifest rows for everything in v0.14.1 and v0.16.0, all nine protocol pages moved fromtcp()toready(), new spec-patterns Pattern 0: assert on every step (the habit both credential bugs needed to survive), and 36 broken internal links fixed. Ten of thirteen step protocols now have real-server coverage; http2, udp and grpc remain unit-tested only. Six consecutive cleango test -race ./...full-suite runs. SeeCHANGELOG.md. -
v0.16.0 — 2026-07-29 — Filesystem observation (RFC-056), and two bugs found by being the first thing to assert on a database. New
watch(service, files=, ops=, run=)reports a service’s file I/O with resolved paths. Faultbox could already countwritecalls but could not reliably say which file - a syscall carries a descriptor, and resolving it meant reading/procout of band, racing the SUT. This closesfs-unmediated, a determinism category that had emitted no events since RFC-040, so file I/O outside declared paths was silently undetected. Built in v0.14.0 and withdrawn before release:runsc trace createinstruments only tasks created after it attaches, so a network-driven query against a running Postgres produced 2 trace points where the same SQL from a freshly spawned process produced 1054 - a watch that observes nothing still runs, and every assertion under it still passes.--pod-init-configinstalls the session at sandbox boot instead: 236 points on that same workload. Completeness is enforced, not assumed - the canonical assertion is negative (“never wrote outside its data directory”) and is only as strong as the trace behind it, so a run fails when no sandbox connected, when points were dropped during the window, when points matched no launched service, or on a decode error.read,closeandconnectare opt-in viasetup-trace --with-read: on a read-heavy workload, enabling reads took a run from 25,015 points and zero drops to 48,576 and 1,488, which under that rule fails the test. Newfaultbox setup-tracedoes the one-time host registration (the flag lives indaemon.json, not per container) - idempotent, reports every change and what it left alone, and prints the Docker restart rather than performing it, because that restart stops every container on the host. Newready(timeout=), protocol-aware readiness:tcp()asks whether something is listening, which for a container is true the moment Docker’s port proxy binds - measured at 0 ms against a Postgres needing ~10 s.ready()asks the service instead, via its interface’s protocol plugin and declared credentials (for Postgres, a realSELECT 1retried to the timeout); on the RFC-056 corpus it replaced a hand-tuned 25-secondsleep()with a ~2.4 s check that is correct by construction. Fixed: Postgres steps could never authenticate - the connection string carried no credentials (lib/pq fell back to the OS user,rootunder sudo), and the auth handshake was relayed server→client only, so SCRAM-SHA-256 (the postgres:14+ default), MD5 and cleartext all deadlocked until the client’s 60-second read deadline. Observation only: a trace point fires after the syscall, and gVisor has nofsyncpoint, so write ordering is provable and durability is not -ops=["fsync"]is rejected rather than quietly returning nothing. New tutorial chapter: Watching the filesystem. SeeCHANGELOG.md.
v0.15.x — Contract-driven clients
- v0.15.0 — 2026-07-29 — Contract-driven clients (RFC-055). Faultbox already read your OpenAPI and protobuf contracts to build the dependencies you can’t run; this release reads them from the other side, to build the callers that drive your service - and makes each caller a named actor in the trace rather than one anonymous
testlane. Newclient()turns an OpenAPI 3.x document or a protobufFileDescriptorSetinto a named caller bound to a service interface, with its operations generated as callable attributes:mobile = client("mobile-app", target = orders.public, openapi = "./orders.yaml", validate = "response")thenmobile.get_order(order_id = 42)- no path, no verb, no field names. The half that isn’t ergonomics: with the contract loaded caller-side, “under fault, the service still returned what it published” becomes a one-kwarg check.validate="response"checks each response against the schema declared for its status code, records the verdict onresp.contract_ok/resp.contract_error, and emits acontract_violationevent - it deliberately does not raise, because a contract violation under fault is usually the finding, not a harness error, and an undeclared status code counts as a violation, which is how the undocumented degraded path surfaces. Clients are first-class trace actors: calls emitclient_call/client_returnon the client’s own swim lane with its own vector-clock participant, so three named callers against one API render as three lanes instead of one anonymous driver, and those events work as temporal anchors with no new matcher syntax (match.event(type="client_return", client="gRPC-Courier", success="false")). Clients compose with everything - calls dial through the same proxy resolution as step methods, sofault(iface, ...)applies unchanged, and TLS (RFC-038) and remote targets (RFC-036) are inherited from the interface; they are trace actors, not processes, so they take no seccomp filter and are never a fault target. Newfaultbox inspect --clients <spec.star>prints each client’s generated operation table.interface(name, protocol, port, spec=)is now read - the kwarg had been parsed and stored since early versions with nothing consuming it, and aclient()declaring no contract of its own now inherits it. Fixed:mock_service(openapi=)andmock_service(descriptors=)resolved relative paths against the process working directory rather than the spec’s own directory, so"./api.yaml"only loaded whenfaultboxhappened to run from the spec’s directory; andservice()/client()now reject a name already taken by the other, since sharing one silently folded two participants into a single lane and vector clock. v1 is unary-only, stateless per call, and does not synthesize request data. New tutorial chapter: Contract-Driven Clients; new guide: Running in a k8s dev namespace. SeeCHANGELOG.md.
v0.14.x — Packet-level network faults
-
v0.14.1 — 2026-07-29 — Searching fault timing, and fixing what that search exposed. v0.14.0 shipped packet faults and a Raft harness that could express a partition but not vary when it lands. Closing that gap took one new primitive and turned up four defects, one of them a documented fault that silently did something else. New
sleep(duration, clock="wall")- a wall-clock wait indifferent to event traffic. Faultbox had two ways to wait and both were conditional on the SUT:await_stable()returns on quiescence,await_event()on a matching event, and neither can hold a fault open for a fixed time.await_stablefails at it in exactly the situation the wait is for - an active fault emits the events that prevent quiescence: measured on a 3-nodehashicorp/raftcluster under partition, 6681 events in three minutes with a longest quiet gap of 338 ms, so a fault-timing search that should have run 18 configurations ran 6 and hung on 12, spending 36 minutes to produce no signal. Withsleep()the same search runs all 18 in about two minutes. Newbandwidth(rate, dir=, queue=)andmtu(size)- the two link shapers deferred from v0.14.0. They take no matcher: apacket_*rule says what happens to packets that look a certain way, a shaper says what kind of link this is. A barerate="1000000"is rejected because it could be bits or bytes and guessing would be a silent factor-of-eight error in the one number the fault depends on;queue=bounds the backlog in time, so the shaper drops when saturated the way a real bottleneck does. Fixed:packet_delay(dir="s2c")was a silent drop, not a delay - egress built a batch list and freed it on return, so a packet released later by the defer queue’s timer was appended to a list nobody would ever write;packet_reorderon egress had the same fate, and both existing delay tests drove the ingress path, so nothing caught it. The packet gateway’s TUN device no longer bricks a host when a run is interrupted - it was the shared, persistent constantfaultbox0, so concurrent runs collided and any run that died without teardown failed every later packet-fault run withdevice or resource busy, recoverable only bysudo ip link delete faultbox0and documented nowhere; devices are now per-process (fbox<pid>),SIGTERMis handled alongsideSIGINT, and every run sweeps orphaned devices whose owning process is gone. An interrupted suite no longer invents verdicts for tests it never ran -RunAllhad no cancellation check, so Ctrl-C left each remaining test to start, inherit a dead context and be recorded INCONCLUSIVE; the newAbortedcounter is kept distinct, since “indeterminate” and “never started” call for different responses.faultbox planwas blind tochoose()- the construct most likely to blow a budget - so a spec that runs 24 leaves reportedTotal: 2 plan instances, under-reporting the--check-cost --max-instances Ngate by 12×; axes are now read statically from the spec AST, with computed option lists reported as(computed — size unknown)and the total asat least N. Inconclusive verdicts now print their reason, which had been carried onTestResult.Reasonand discarded by the summary the whole time. SeeCHANGELOG.md. -
v0.14.0 — 2026-07-28 — Packet-level network faults (RFC-054). Faultbox mediated at two layers - individual syscalls and parsed L7 protocol messages - with nothing in between. A packet was not an object anywhere in the codebase, so “drop this TCP segment”, “delay every ACK from the server” and “advertise a zero receive window” were inexpressible. This adds that layer using gVisor’s userspace TCP/IP stack (
gvisor.dev/gvisor/pkg/tcpip) as a plain Go dependency - no fork, no runsc. The capability that motivates it: a dropped packet sends no RST. The existingdrop()closes the connection, and well-written clients handleECONNRESETcorrectly on the first try; a socket stuck inESTABLISHEDwriting into a void until a keepalive fires - the failure that actually takes production down - was unreachable. It is now one line. Eight new faults (packet_drop,packet_delay,packet_reorder,packet_duplicate,packet_corrupt,packet_reset,packet_window,packet_pass), opt in withdeterminism(runtime = "gvisor")on Linux withCAP_NET_ADMIN. The packet matcher takesdir,proto,flags("PSH,ACK","!RST"),port,len/len_gt/len_lt,payload_prefix,payload_contains, plus the samenth/after/everyoccurrence selectors andprobability/max_fires/modesemantics as syscall faults, and awhere=lambda escape hatch over a read-onlyPacketvalue. Peer-mesh topologies now work: gateway address allocation was gated on a proxy address existing, and a mesh is a cycle, so for at least one link the proxy was always absent when the consumer’s env was built - that link went unmediated. Measured on a 3-nodehashicorp/raftcluster, a leader cut off from both followers committed 88 applies before the fix and 0 after.source=now reaches rule installation (it was parsed, stored and traced, then dropped, so the documentedfault(kafka.main, source=worker, drop(...))fired for every consumer), andpartition()is rebuilt on the packet gateway withdirection=andpartition_start()/partition_stop()- the old implementation deniedconnect(), which only blocks connection setup, so against any service that pools connections it silently did nothing. Also fixed:**path globs now cross directories (op(path = "/data/**")matched nothing, so a rule targeting a database that nests its files never fired - no fault, no diagnostic, test passed), andevents(where = ...)was blind to most event types, filtering to four families before invoking the lambda so the documentede.type == "proxy"predicate could never have worked. New tutorial chapter: Packet-Level Faults. Known limits at this release:watch()deferred to v0.16.0,bandwidth()/mtu()deferred to v0.14.1, packet faults act below TLS, and they do not yet participate infault_matrix()fan-out. SeeCHANGELOG.md.
v0.13.x — Verification model
-
v0.13.3 — 2026-07-14 — Bug fixes + stricter spec loading, from the July 2026 documentation audit. Five silent no-ops are fixed:
drop(query=/command=)was meant to scope a drop to a SQL statement or Redis command but silently dropped all traffic (an empty pattern matched everything) — now it matches; Kafkaduplicate(topic=)was a dead rule — now it re-sends the produce so the consumer sees the message twice while the producer still gets one ack; proxy events dropped theaction/protocolfields, soassert_eventually(...action == "error")could never fire — now they carry them. Spec loading is stricter:deny()rejects errno names outside the supported table (widened to include the documentedENOSYS/EDEADLK/ELOOP/EDQUOT/ENOLCK), andservice()plus every proxy fault builtin (response/error/drop/delay/duplicate) reject unknown keyword arguments with migration hints instead of silently ignoring them — specs relying on silently-dropped kwargs (cmd=,http=,tcp=,name=) must switch to the documented forms.subject=is now an acceptedtopic=alias for NATS matchers, andproxy_conn_close/proxy_stallevents carry theprotocolfield. Full host suite +go test -race+go vetgreen; Lima sweep 21/21 PASS. SeeCHANGELOG.md. -
v0.13.2 — 2026-07-06 — Finite-trace verdict semantics (RFC-049) + the documentation and site restructure around the six bug classes. Temporal verdicts are now grounded in finite-trace logic (D4 of the RFC-047 research agenda). A
test()that ends by hitting itstimeoutis now always INCONCLUSIVE, never PASS — even if everyeventually()it declared was satisfied before the deadline — because the body never reached a declared completion (natural return orterminate_when=), so the run is a truncated prefix and a green verdict would over-claim. An unboundedalways(p)(nobetween=) under timeout is INCONCLUSIVE for the same reason (LTL₃ prefix); at natural completion orterminate_when=it stays a definitive PASS (LTL_f end-of-trace). Boundedalways(p, between=(a,b))is unchanged. Newvacuous_propertywarning event: when analways(p, between=)start anchor never fires, the window never opens and the predicate is never evaluated — the verdict stays PASS (the window may be legitimately untriggered, e.g.between=(error, recovery)in a run with no error) but the warning surfaces a typo’d or misnamed anchor instead of hiding as a silent green. Legacy synchronousdef test_*()functions are unaffected. Full host suite +go vetgreen; Lima sweep 21/21 PASS across the 6 integration spec suites. SeeCHANGELOG.md. -
v0.13.1 — 2026-06-18 — Fixes from the first field evaluation of v0.13.0 against a real service (truck-api).
--testmatching no tests now exits non-zero instead of reading as a green suite in CI, and a collapsedfault_matrixname (test_matrix_create_order) selects every expanded cell under it. An unstartable target now fails fast with the named path (exec <path>: no such file or directory) instead of a 60-second healthcheck timeout with a misleadingexit_code=0— both the binary-mode launch and the container shim carry the path, and the healthcheck races against session exit.step_recvtrace events record the response body (2 KB) on non-2xx HTTP responses, so debugging a 400/500 reads straight off the trace or report. Newmake install-limacross-compiles and installs bothfaultboxandfaultbox-shiminto the Lima VM’s/usr/local/bin(container mode needs both side by side); a stale hardcoded shim path and an incorrectVM_PROJECTwere cleaned up. Themonitor()signature change from v0.13.0 keeps its hard error (accepted breaking change — no real users to migrate yet). Full host suite +go vetgreen; testops corpus re-verified on the Lima VM (real seccomp + Docker). SeeCHANGELOG.md. -
v0.13.0 — 2026-05-29 — Five RFCs ship together as one coordinated epic: determinism levels (RFC-040), temporal properties (RFC-041), exploration plan (RFC-042), non-deterministic operators (RFC-043), and spec language simplification (RFC-044). The new
determinism()top-level builtin declares the spec’s reproducibility contract — defaults to L1 (mediated-event determinism) with strict mode on, so any unmediated I/O (clock reads, RNG, DNS to a non-Faultbox resolver,connect()to an undeclared address) fails the test at the first untolerated leak. RFC-041 adds five temporal primitives —eventually(p),always(p, between=),await_event(matcher),await_stable(quiescence_window=), and a rewrittenmonitor(name, on=, state_init=, update=, check=)— plus a declarativetest(name, body=, expect=, timeout=, terminate_when=)builtin and the PASS/FAIL/INCONCLUSIVE three-valued verdict. RFC-042 introducesfaultbox plan(static plan-tree analysis without launching services),plan.jsonin every bundle, the report’s Plan tab,--coverage/--suggest/--check-cost --max-instances N, and the body-re-execution engine that turns namedchoose("name", [opts])axes, syscall-level probability fan-out (max_fires=/mode="exhaustive"), andparallel(..., interleavings=)orderings into multi-leaf test executions with stableLeafIDattribution flowing throughTestResult→ bundle manifest → HTML report. RFC-043 ships the four operators (choose,nondet,halt,assume) with per-leafassume=evaluation and an AST denylist sandbox. RFC-044 withdraws RFC-013 (param()superseded bychoose()) and RFC-002 (domain()—service()/interface()proved sufficient); unifies the three plan-tree fan-out axis kinds under oneNonDeterministicChoiceinterface; collapses the event-source and decoder surfaces underobserve.stdout/observe.stderranddecoder("name", ...); deprecatesfaultbox generatein favor offaultbox plan --suggest. Two new tutorial chapters (Part 4 — Safety & Verification) walk the operator and fan-out vocabulary end-to-end. Full repogo test -race ./...green; Lima sweep 21/21 PASS across the same 6 integration spec suites as v0.12.29.
v0.12.x — TLS-aware proxy
-
v0.12.29 — 2026-05-02 — Remote services (RFC-036). New
service(remote=...)kwarg points Faultbox at an externally-running endpoint — typically a real pod in a customer’s k8s dev cluster — without launching a process. The proxy datapath from RFC-024 dials the remote upstream and the SUT reaches it through the proxy unchanged, so every protocol-level fault (response(),error(),slow(), gRPC method targeting, SQL matchers) keeps working. Process-level kwargs (seed=,reset=,reuse=,volumes=,ports=,args=,seccomp=,observe=,ops=) and syscall-level faults are rejected at spec load with explicit error messages naming the offending kwarg and pointing at protocol faults ormock_service(). Composes with RFC-038tls=tls_cert(...)for TLS-required upstreams (the auto-generated proxy cert covers127.0.0.1so SUT-side verification works against the env-rewritten loopback addr). New typedremotes({"iface": "host:port"})value for services whose interfaces live on different hosts. New@faultbox/discovery/k8s.starstdlib helper exposingk8s.service,k8s.endpoint, andk8s.local— pure string sugar over<name>.<namespace>.svc.cluster.local, no runtime k8s client. The.fbbundle’senv.jsonrecords every(service, interface, host, protocol)tuple from a remote-using run;faultbox replaywarns when replaying such a bundle and points at RFC-037 (the open companion design RFC for the offline-replay determinism story). Cluster connectivity is the user’s responsibility — Telepresence connect, kubectl port-forward, in-cluster execution, or VPN — documented in the new Connectivity guide. 49 new tests across spec-load validation (32), runtime/proxy lifecycle (10 incl. TLS×remote interop), bundle round-trip (2), replay warning (2), and string-grep doc gates (3). Full repogo test ./...green;go vet ./...clean;make demo-containerLima smoke 4/4 PASS (proxy datapath refactor confirmed non-regressive against the seccomp + Postgres + Redis path). -
v0.12.28 — 2026-05-02 — TLS-aware proxy (RFC-038) + proxy traffic observability (RFC-034) + container fault paths (RFC-035). Twelve patch versions consolidated into one release; the per-version detail lives in
CHANGELOG.md. Headline: declareinterface(..., tls=tls_cert(...))on a service interface and the proxy terminates TLS at its listener and re-establishes TLS dialing the upstream — all protocol-aware fault rules (http.error(path=...),grpc.error(method=...),kafka.drop(topic=...),redis.error(key=...)) keep firing on the plaintext between the two TLS legs. Six plugins ship migrated this release (http, http2, gRPC, Kafka, Redis, TCP); the remaining 8 (postgres, mysql, mongodb, cassandra, clickhouse, memcached, nats, amqp) are tracked in RFC-039 — declarations against unmigrated plugins emit aproxy_tls_pendingevent. Thetls_cert(...)builtin is kwargs-only with full spec-load validation (cert/key pairing, file existence, CA PEM parse,insecure=True+ca=exclusion); emptytls_cert()auto-generates a self-signed proxy cert in memory for dev/test. Two TLS plumbing patterns landed across the 6 plugins: listener wrap-and-dial viaproxy.ListenTLS(serverCfg)+proxy.Dial(ctx, target, clientCfg)(http, http2, kafka, redis, tcp) and framework credentials viagrpc.Creds(credentials.NewTLS(...))for gRPC (whose server owns its own TLS handshake). Also in this release: RFC-034 connection lifecycle observability —proxy_conn_open,proxy_conn_closewith byte counts and reason classification,proxy_handshake_complete, andproxy_stall(1Hz watchdog, 5s warn / 30s extend tiers) — wired into 13 of 15 plugins (udp/grpc deferred). RFC-035 container-consumer fault paths on Linux Docker (proxy bind defaults to 0.0.0.0 so container consumers reach proxies viahost.docker.internal). Newstderr()event source. Container-modeobserve=[stdout(...)]now works for container services (was binary-mode only). Race fix ontcp.go::handle— both io.Copy goroutines were writingcloseReasonconcurrently. Newinternal/proxy/{http,grpc,kafka,redis,tcp}_tls_test.gosuites (~30 tests). Full repogo test ./... -racegreen; Lima sweep 21/21 PASS across 6 integration spec suites.
v0.12.x — Scalable HTML Reports
-
v0.12.16 — 2026-04-30 — Report UX overhaul. Driven by inDrive Freight triage feedback on the v0.12.15.x customer report; entirely scoped to
internal/reportand the docs that describe it (no bundle format or spec-language changes). Causal links now follow cause, not chronology —findCausalAncestorsswitched from vector-clock partial order (which on real bundles routinely had only the lifecycle events with complete clocks, so spaghetti pointed atservice_readyinstead ofproxy_fault_applied) to seq-based strict precedence, restricted to faults / violations / errored steps. Hovering an ordinary success step now draws zero lines. New timeline filter bar above every Event Trace block: three presets (Compact default, hides framework lifecycle chatter; Anchors only strips everything except cause-relevant events; All events historical default) plus free-text search across event type / headline / fields.proxy_fault_applied/proxy_fault_removedare now first-class fault markers (red, not default-blue) across markerKind/severityScore/isAnchorEvent/eventHeadline; added to Phase 3 anchorTypes so they survive downsampling. Per-test “Faults applied” section pairs proxy_fault_applied with proxy_fault_removed (one row per assumption: service · protocol · interface · assumption · seq window). Recent block in the Assertion drill-down interleaves fault events between captured step rows by seq, with a fade-and-expand cap (220px max-height + mask-image gradient + Show full assertion toggle, 320ms transition) so long Recent lists / Actual reprs stop dominating the dialog. Group-members table on folded markers (paginated 100/page, sticky header, scrollable) replaces the one-line “collapsed run” hint — runs that hide a 5xx among 99 successes are now legible. Fullscreen toggle on the test details modal (⤢ ↔ ⤡). STDOUT JSON renders as a 2-column key/value table with dot-path flattening for nested objects. Source block falls back tofault_matrix(...)for matrix-generated test names and surfaces three jump links (scenariodef get_order_feed(), faultmysql_slow = fault_assumption(...), matrix call site) — was previously “Could not locate def…” for every matrix cell. Plus tooltip vertical-text fix (width:max-content + four-side viewport clamping), detail panel summary no longer truncates, folded marker click routes throughmarkerEvBySeqso the Group-members table actually appears. -
v0.12.15.2 — 2026-04-30 — Hotfix on top of v0.12.15.1. Customer verified the redis RESP3 fix landed clean (cold-start path green end-to-end, smoke PASS in 16.3 s) but the failure moved to the reuse path: cell 1 of the dbmatrix run passes, cells 2–18 all fail identically with
error connect to db: invalid connection(go-sql-driverErrBadConn) orread: connection reset by peer(go-redis). Root cause:Manager.EnsureProxyrooted each proxy’s Accept goroutine at the caller’s ctx.preStartProxiesruns underRunTest’s per-testtestCtxwhich cancels viadefer cancel()at end of test — at end of cell 1 that cancellation took down the goroutine while the listener fd stayed bound (onlyStop()closes it) and the cachedm.proxies[key]entry stayed in place. Cells 2..N hitEnsureProxy→ cache hit →proxy_active(reused)fired → but nobody was Accept()-ing, so the kernel completed the TCP handshake and then RST-ed. v0.12.15.2 roots the proxy’spCtxatcontext.Background()so its lifetime is bound toManager.StopAll/StopService(which already drive explicit teardown). Why this surfaced now: pre-v0.12.13 no-seccomp containers were destroyed every cell, forcing fresh proxies via the cold path; v0.12.13 fixed reuse so containers AND proxies stay alive across cells, exposing the latent ctx-rooting bug. New regression testTestManagerEnsureProxy_SurvivesCallerCtxCancel. Lima sweep 21/21 PASS. -
v0.12.15.1 — 2026-04-29 — Hotfix on top of v0.12.15. Customer verified the MySQL fix landed clean (Finding H closed) but the failure moved one step forward: truck-api hung 6s on its first Redis
Ping(). Root cause: go-redis v9 unconditionally sendsHELLO 3frominitConn, which switches the server to RESP3; v0.12.15’s redis proxyreadRESPRawonly knew RESP2 framing (+,-,:,$,*) and on a%Nmap header fell through to the default branch — returned just the header line and left the map body unread on the upstream socket. Wire-level proof:redis-cli -p $PROXY -3 PINGtimed out 6s,redis-cli -p 16379 -3 PING(direct) returned PONG in 8ms. v0.12.15.1 extendsreadRESPRawto cover RESP3 aggregates (%map,~set,>push,|attribute) and scalars (_#,(single-line;=!bulk-string-framed). Four new regression tests includingTestRedisProxy_RESP3_HelloMapreproducing the customer’s exact map shape. -
v0.12.15 — 2026-04-29 — Hotfix on top of v0.12.14. Customer verified v0.12.14 didn’t unblock Finding H — both
caching_sha2_passwordandmysql_native_password --default-authstill hung 8s through the proxy. Root cause: v0.12.14’s handshake loop assumed strict client/server alternation, butcaching_sha2_passwordfast-auth-success (taken when the user is in the server’s auth cache) emits two server-side packets back-to-back —AuthMoreData(0x01, 0x03)thenOK(0x00)— with no client packet between. The proxy read theAuthMoreData, tried to read from the client, deadlocked. Customer’sseed_dbpopulated the auth cache via direct MySQL connections, so every proxy connection hit fast-auth. v0.12.15 peeks the second byte of everyAuthMoreDataand treats0x03(fast_auth_success) as “expect another server packet” rather than “expect client reply”. New regression test guards the path. -
v0.12.14 — 2026-04-29 — First attempt at Finding H. Loops the handshake until OK/ERR, alternating directions on
AuthMoreData/AuthSwitchRequest. Fixedcaching_sha2_passwordcold-cache full-auth (covered byTestMySQLProxy_Handshake_CachingSha2FullAuth), but missed fast-auth-success (back-to-back server packets) — superseded by v0.12.15 the same day. Also bumpsstep.summarypreview cap from 80 → 500 chars. -
v0.12.13 — 2026-04-28 — Hotfix on top of v0.12.12. Pre-existing bug (RFC-015 vintage): container services without seccomp filters bypassed
rt.sessionsregistration, sostopServices’reusedset silently ignoredreuse=Truefor proxy-only Docker upstreams — container destroyed every cell, but the proxy inproxyMgrwas kept pointing at the dead host port. Fix populatesrt.sessionsin the no-seccomp branch and nil-guardsClearDynamicFaultRulesin the reuse path. -
v0.12.12 — 2026-04-27 — Proxy-address surface for host-binary SUT + Docker upstream (RFC-033). New
iface.proxy_addr/proxy_host/proxy_portinterface attributes give a working spec-language path for wiring host-binary SUTs through the fault-injection proxy — late-bound at spec-load, resolved atbuildEnvtime, norsplit()games. Also emitsproxy_activeevents in thereuse=Truepath so per-cell trace partitions reflect what’s wired up at cell start. -
v0.12.11 — 2026-04-26 — Compact fold-count labels. Run-marker badges now render counts as
× 3.9k/× 86k/× 4Minstead of full numerals; the exact count remains in the badge’s hover tooltip. Truncates rather than rounds —× 3.9kalways represents ≥ 3900 events. -
v0.12.10 — 2026-04-26 — Spec-anchored event highlighting. The runtime tags
step_send/step_recvevents withfields.spec = <test_name>whenever the call originates from inside the currently-running test function (helper functions still register — the test frame is on the stack). Renderer paints these markers with a warm gold ring, prefixes the balloon / log headline with★, bumps severity by +50 so they win their slot, and bypasses the lane fold so the user’s own calls always render individually against background traffic. -
v0.12.9 — 2026-04-26 — Three UX polishes. Run-marker discs scale with
log10(fold count)— magnitude is visible at-a-glance even when adjacent chips’ badges would otherwise overlap (count text now sits inside the disc). Drill-down “All fields” / “Vector clock” expansion state persists across pin changes. Step summaries pair the directional arrow with explicitcall/replywords —→ call · truck-api.get /orders,← reply · truck-api.get /orders [500]. -
v0.12.8 — 2026-04-26 — Lane filter now folds by key (
target.method.summary) before slot bucketing — 1787 identical SELECT 1 errors collapse to a single× 1787chip instead of 50 indistinguishable red dots. Causal hover lines restored for v0.12.7’s lane routing (cross-lane detection useslaneFor; folded ancestors resolve to their containing chip via_runMembers). Type filter axis becomes click-to-add — the toolbar stays empty until the user clicks a Type cell in the table. -
v0.12.7 — 2026-04-25 — Step events now lane on their target service:
db.exec(...)lands on thedblane,truck-api.get(...)on thetruck-apilane, instead of all of them piling onto the test driver lane. Event-log filter applies to the full event set (not just the first 200 loaded rows), so filtering by a service finds matches anywhere in the trace. -
v0.12.6 — 2026-04-25 — Lane markers color by severity (failed steps + 5xx → red, 4xx → amber); slot picker prefers severity over first-anchor; Recent trail ellipsizes long lines with hover-tooltip; two-axis event-log filter (Service + Type) replaces the v0.11 single-select chip bar — click a cell in the table to filter to its value.
-
v0.12.5 — 2026-04-25 — Hard per-lane marker budget. Walks back the v0.12.2-4 dedup/window approaches (none had an upper bound on DOM node count). New filter buckets each lane’s events into 50 visual slots in seq order; each slot picks one representative (anchor > fold-key head > first event). Hard guarantee: ≤ 50 markers per lane regardless of input size — the customer’s 86874-event lane now renders 50 markers (≈ 250× DOM reduction) at uniformly-spaced visual positions.
-
v0.12.4 — 2026-04-25 — Lane filter rewritten as anchor windows + global cardinality fold (faults / violations / errored steps + ±10 around each render per-event; outside groups by
(target, method, summary)and folds large buckets to a× Nchip at the median rank). Customer’s noisy 80k-event test now shows ~5 distinct chips. Assertion drill-down gains a “Recent” trail snapshotting the last few step events at fail time soresp.status = 500is visible inline. -
v0.12.3 — 2026-04-25 — Drill-down ergonomics. Assertion block now lifts the original expression text from the spec source (
assert_true(resp.status in [200, 201])showsExpression: resp.status in [200, 201], not justActual: False), with a clickablespec.star:42location link. Lane dedup also keys on summary so mixed SQL doesn’t flatten into one chip. Click on a lane marker no longer scrolls the page. -
v0.12.2 — 2026-04-25 — Step-event readability. Runtime enriches
step_send/step_recvwithsql/query/path/args/status_code/duration_ms/errorplus a one-linesummarypreview; lane dedups consecutive(target, method)runs into a single× Nmarker (a 1500-iterationdb.execloop renders one chip instead of 3000). FAQ entry added on bundle freezing — old bundles can’t invent new fields; re-run the suite on v0.12.2 to benefit. -
v0.12.1 — 2026-04-25 — Drill-down polish: structured
assert_eq/assert_trueExpected vs Actual block, services list now shown for proxy-mode runs (event-log fallback), swim-lane switched to rank-based axis with syscalls relegated to the event-log table — keeps timelines legible at 80k+ events. -
v0.12.0 — 2026-04-25 — Report architecture redesign (RFC-031). 23 MB reports become ~150 KB by default;
--full-eventsopt-out preserves every event. Plus panic-safe bundles, binary-digest pinning, actionable lock drift,grpc.retryable(), proxy-coverage CI gate, positioning doc.
v0.11.x — Interactive HTML Reports
- v0.11.3 — 2026-04-25 — MySQL driver EOF noise suppressed, CHANGELOG + per-release pages land.
- v0.11.2 — 2026-04-24 — Hotfix: gRPC proxy passthrough +
fault_matrixmock-target panic.--testglob/regex. Capability matrix in README. - v0.11.1 — 2026-04-24 — Five-outcome matrix:
expectation_violated(amber),fault_bypassed(grey, opt-in viarequire_faults_fire). - v0.11.0 — 2026-04-24 — Interactive single-file HTML reports.
faultbox report <bundle.fb>. Swim-lane trace viewer.
v0.10.x — Reproducibility trio completes
- v0.10.1 — 2026-04-23 — Assumption
ProxyRulesapplied infault_scenario/fault_matrix. testops corpus: Critical tier 100% green. - v0.10.0 — 2026-04-23 —
faultbox replay <bundle>,faultbox lock+faultbox.lock(image-digest pinning, RFC-030).
v0.9.x — Reproducibility, docs, primitives
- v0.9.9 — 2026-04-23 — JWT/JWKS mock, documentation overhaul (~1500 lines, 6 new pages).
- v0.9.8 — 2026-04-23 —
load_file()/load_yaml()/load_json(),expect_*predicates, gRPC status shorthands. - v0.9.7 — 2026-04-22 —
.fbbundle format (RFC-025),faultbox inspect, always-on reproducibility.
Older
See the complete CHANGELOG for entries prior to v0.9.7 and the GitHub Releases page for the canonical per-version notes.