All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Unreleased

0.47.2

Added

  • DoubleDown.Contract.Dispatch.handler_active?/1 — public boolean API to check whether the calling process has a test handler installed for a given contract module. Returns true when a handler is active (via Double.fake/2, expect/3, etc.), false otherwise. Respects the $callers chain. Useful for test infrastructure that needs to skip real-DB side-effects (e.g. Carbonite session variables) when an in-memory handler is intercepting Repo calls.

0.47.1

Fixed

  • Code formatting.

0.47.0

Fixed

  • Transaction rollback now covers all failure paths. run_in_transaction restores pre-transaction state on {:error, _} returns, raised exceptions (re-raised after restore), and failed Ecto.Multi tuples — not just explicit Repo.rollback/1 calls. Previously, error branches left partially-mutated fake state. (GitHub #1)

  • restore_state uses correct owner pid. restore_state/3 now accepts owner_pid as an explicit parameter, resolved via resolve_test_handler at rollback time. Transactions run inside a Task now correctly restore state to the owning test process instead of silently no-oping. (GitHub #1)

  • Ecto.Multi bulk ops now execute instead of returning {0, nil}. MultiStepper routes insert_all, update_all, and delete_all steps through repo_facade — each fake's own dispatch handles state mutation. Previously these were hardcoded no-ops. (GitHub #2)

  • insert_all raises for missing non-autogenerated PKs. When maybe_autogenerate_id returns an error for a schema without autogeneration, insert_all now raises ArgumentError — matching single-row insert behaviour. Previously the error was swallowed and rows collapsed onto a nil key. (GitHub #3)

  • @primary_key false schemas support multiple rows. No-PK schemas are now stored as lists (reverse insertion order) instead of a %{nil => record} map. All store accessors (put_record, get_record, delete_record, records_for_schema) and bulk ops (delete_all, update_all) handle the dual map/list representation. (GitHub #4)

  • get_by! raises Ecto.NoResultsError when PK matches but extra clauses don't. Previously returned nil for both get_by and get_by! in this case, diverging from real Ecto. (GitHub #5)

  • rollback/1 outside a transaction raises RuntimeError. Uses a process dictionary flag set by run_in_transaction and cleared via try/after. Both InMemoryShared and Stub updated. Previously an uncaught throw({:rollback, _}) surfaced. (GitHub #6)

Changed

  • Breaking: All handler function signatures gained contract as the first parameter. This affects FakeHandler.dispatch (/3/4, /4/5), set_stateful_handler fns (3-arity → 4-arity, 4-arity → 5-arity), fallback_fn (3-arity → 4-arity), and restore_state (/2/3). The contract module now flows through the entire handler chain — invoke_handler, canonical_handler, dispatch_via_fallback, try_fallback — eliminating the hardcoded DoubleDown.Repo in transaction rollback. Enables handlers to know which contract they are serving.

0.46.3

Fixed

  • FK backfill now recursively inserts parent structs when the parent's PK is nil. This fixes the ExMachina pattern where build(:parent) produces a struct with nil PK that hasn't been inserted yet — matching real Ecto.Repo behaviour of recursively inserting belongs_to parents before the child.

0.46.2

Fixed

  • Added test coverage for FK backfill with parent struct returned from a prior insert (the exact ExMachina factory pattern). Confirms backfill correctly uses related_key from association metadata, not a hardcoded :id field.

0.46.1

Fixed

  • FK backfill now explicitly skips %Ecto.Association.NotLoaded{} associations rather than relying on Map.get returning nil. Defensive fix for a reported FK backfill failure via the ExMachina insert! path.

  • Added integration test for insert! bare struct through the Double.fake facade dispatch path to verify FK backfill works end-to-end.

0.46.0

Added

  • query/1,2,3 and query!/1,2,3 added to DoubleDown.Repo contract. Raw SQL operations from ecto_sql — adding them to the contract makes them interceptable via expects/stubs so code paths that call Repo.query! can be tested without a database.

  • FK backfill on insert. When inserting a struct with a loaded belongs_to association but a nil FK field, InMemory now copies the parent's PK into the FK field — matching real Ecto.Repo behaviour. Makes ExMachina factories work transparently: insert(:child, parent: parent) automatically sets the FK. Implemented in Repo.Impl.EctoParity.backfill_foreign_keys/1.

  • Association fields are reset to %Ecto.Association.NotLoaded{} on insert, matching real Ecto.Repo behaviour. Struct equality comparisons between insert(:thing) and Repo.get!(Thing, id) now work without comparing individual fields. Implemented in Repo.Impl.EctoParity.reset_associations/1. Runs after FK backfill (which needs the loaded association to extract the FK).

  • Repo.Impl.EctoParity — new module for Ecto schema-introspection concerns that make the in-memory fakes behave more like real Ecto.

Changed

0.45.0

Added

  • In-memory transaction rollback support. rollback/1 in the stateful test adapters (Repo.InMemory and Repo.OpenInMemory) now restores the store to its pre-transaction state — inserts, updates, and deletes within a rolled-back transaction are undone.

    Implemented by snapshotting the store at transact start and restoring via Contract.Dispatch.restore_state/2 on rollback. Only the Repo contract's state is restored; other contracts are unaffected.

  • DoubleDown.Contract.Dispatch.get_state/1 — read the current domain state for a contract. Returns fallback_state for Double-managed handlers, raw state for set_stateful_handler.

  • DoubleDown.Contract.Dispatch.restore_state/2 — replace a single contract's state in NimbleOwnership, leaving the handler function and all other contracts' state untouched. Scoped to a single contract by design.

0.44.0

Added

  • Bang write operations: insert!/1,2, update!/1,2, delete!/1,2 added to DoubleDown.Repo contract and all three test doubles. These were lost when auto-bang generation was removed in v0.38.0. Needed for ExMachina integration (ExMachina calls Repo.insert!).

  • insert/insert! now accept bare structs in addition to changesets, matching Ecto.Repo behaviour. delete/delete! now accept changesets in addition to structs.

  • ExMachina integration tests demonstrating the factory + InMemory pattern: factory-inserted records readable via all, get, get_by, exists?, aggregate — no database, async: true, at in-memory speed. ex_machina ~> 2.7 added as a test-only dependency.

  • ExMachina integration documentation in docs/repo.md with worked example: factory definition, test setup, reads, aggregates, read-after-write, failure simulation. Cross-referenced from docs/getting-started.md.

Changed

  • Breaking: DoubleDown.Repo.Port (test facade) renamed to DoubleDown.Test.Repo. Natural alias gives Repo.* without as: clause.

0.43.0

Changed

  • Breaking: DoubleDown.Facade renamed to DoubleDown.ContractFacade. Symmetric <qualifier>Facade naming across all three facade builders: ContractFacade, BehaviourFacade, DynamicFacade.

  • Breaking: DoubleDown.Dynamic renamed to DoubleDown.DynamicFacade.

  • Breaking: DoubleDown.Dispatch renamed to DoubleDown.Contract.Dispatch. The dispatch machinery is keyed by contract module and belongs under Contract, not at the top level. Child modules (Defer, FakeHandler, StubHandler, Passthrough) moved accordingly. Moved to "Internals" doc group.

  • Breaking: DoubleDown.Repo.Test renamed to DoubleDown.Repo.Stub. The name now communicates what the module is — a stateless stub — matching the test-double taxonomy (stub/mock/fake).

  • Breaking: DoubleDown.Repo.InMemory renamed to DoubleDown.Repo.OpenInMemory (open-world, fallback-based). DoubleDown.Repo.ClosedInMemory renamed to DoubleDown.Repo.InMemory (closed-world, recommended default). The unqualified InMemory name now refers to the closed-world store — the one most users should reach for, especially with ExMachina factories.

  • Breaking: DoubleDown.Repo.Autogenerate renamed to DoubleDown.Repo.Impl.Autogenerate. DoubleDown.Repo.MultiStepper renamed to DoubleDown.Repo.Impl.MultiStepper. DoubleDown.Repo.InMemory.Shared renamed to DoubleDown.Repo.Impl.InMemoryShared. Internal helpers moved to Repo.Impl.* namespace.

  • DoubleDown.BehaviourFacade.CompileHelper renamed to DoubleDown.Facade.CompileHelper. The Facade.* namespace is shared internal infrastructure for all facade builders.

  • Updated all documentation (README, getting-started, testing, dynamic, repo, migration, process-sharing, logging) for the new module names.

0.42.0

Added

  • DoubleDown.Repo.ClosedInMemory — closed-world stateful Repo fake. Unlike Repo.InMemory (open-world, where absence means "I don't know"), ClosedInMemory treats the state as the complete truth — if a record isn't in the state, it doesn't exist. This makes the adapter authoritative for bare schema queryables without needing a fallback function:

    • PK reads: get/get! return nil/raise on miss (no fallback)
    • Clause reads: get_by/get_by! scan and filter all records
    • Collection reads: all, one/one!, exists? scan state
    • Aggregates: count/sum/avg/min/max computed from state
    • Bulk writes: insert_all, delete_all, update_all (with set: updates)

    Ecto.Query queryables still fall through to the fallback function (or raise), since evaluating query expressions requires a query engine. The fallback is the escape hatch, not the default path.

    Enables the pattern of using ExMachina factories to write test data into an in-memory store and testing against it without a database:

    DoubleDown.Double.fake(DoubleDown.Repo, DoubleDown.Repo.ClosedInMemory)
    insert(:user, name: "Alice", email: "alice@example.com")
    
    assert [%User{}] = MyApp.Repo.all(User)
    assert %User{} = MyApp.Repo.get_by(User, email: "alice@example.com")
  • DoubleDown.Repo.InMemory.Shared — extracted shared helpers (state access, writes, transactions, fallback dispatch, query helpers) from Repo.InMemory into a shared module for reuse by ClosedInMemory. Pure refactor of Repo.InMemory — no behaviour change.

  • Updated documentation across docs/repo.md (ClosedInMemory section with comparison table and ExMachina example), README (features table), and mix.exs (module groups).

0.41.1

Fixed

  • BehaviourFacade compilation failure on clean builds when the behaviour and facade are in the same elixirc_paths batch. Code.Typespec.fetch_callbacks/1 needs the behaviour's .beam file on disk, but during mix compile all files in the same batch are compiled together — .beam files aren't written until the batch finishes.

Added

  • DoubleDown.BehaviourFacade.CompileHelper.ensure_compiled!/1 — explicitly compiles a behaviour source file and writes its .beam to the build directory. Only needed when the behaviour and facade are in the same compilation batch (e.g. both in test/support/). In normal usage the behaviour would be in lib/ or a dependency, already compiled in a prior batch.

  • BehaviourIntrospection now falls back to :code.get_object_code/1 when Code.Typespec.fetch_callbacks/1 can't find the .beam on the standard code path.

0.41.0

Added

  • DoubleDown.BehaviourFacade — generates dispatch facades for vanilla Elixir @behaviour modules. Reads @callback declarations from compiled behaviour modules via Code.Typespec.fetch_callbacks/1 and generates the same dispatch facade, @spec declarations, and __key__ helpers as DoubleDown.Facade. Supports all dispatch paths (test_dispatch?, static_dispatch?, config-based).

    defmodule MyApp.Todos do
      use DoubleDown.BehaviourFacade,
        behaviour: MyApp.Todos.Behaviour,
        otp_app: :my_app
    end

    Use this for behaviours you don't control — third-party libraries, existing @behaviour modules, or any module with @callback declarations that you don't want to convert to defcallback. For behaviours you do control, DoubleDown.Facade with defcallback remains recommended (richer features: pre_dispatch transforms, @doc tag sync, combined contract + facade, compile-time spec mismatch warnings).

  • DoubleDown.Facade.Codegen — extracted shared code generation (generate_facade, generate_key_helper, dispatch option resolution, static impl resolution, moduledoc generation) from DoubleDown.Facade into a shared module. Used by both Facade and BehaviourFacade. Pure refactor — no behaviour change.

  • DoubleDown.Facade.BehaviourIntrospection — reads @callback declarations from compiled vanilla behaviour modules and converts them to the operation map format used by Facade.Codegen. Handles annotated params (id :: String.t()), bare types (map()), type variables from when clauses, zero-arg callbacks, and mixed param styles.

  • when clause support in generated @spec declarations. Specs with bounded type variables (e.g. @callback transform(input) :: output when input: term(), output: term()) now preserve the when constraints in the facade's @spec.

  • DoubleDown.BehaviourFacade and DoubleDown.Dynamic added to groups_for_modules in ex_doc config.

  • Updated documentation across README, getting-started.md, dynamic.md, and all facade module @moduledocs with the three-facade taxonomy and comparison tables.

0.40.0

Added

  • DoubleDown.Double.dynamic/1 — convenience for setting up a dynamically-faked module with its original implementation as the fallback. Pipes naturally with expects and stubs:

    SomeClient
    |> Double.dynamic()
    |> Double.expect(:fetch, fn [_] -> {:error, :timeout} end)

    Raises if the module hasn't been set up with Dynamic.setup/1.

0.39.0

Added

  • DoubleDown.Dynamic — Mimic-style dynamic dispatch facades. Dynamic.setup(Module) copies a module's bytecode to a backup and replaces it with a dispatch shim, enabling the full Double API (expects, stubs, fakes, stateful responders, passthrough, cross-contract state access) without defining a contract or facade. Call in test_helper.exs before ExUnit.start(). Tests that don't install a handler get the original module's behaviour automatically. Async-safe.
  • Guardrails: Dynamic.setup/1 refuses DoubleDown contracts, DoubleDown internals, NimbleOwnership, and Erlang/OTP modules.
  • Dynamic.setup?/1 — check if a module has been set up.
  • Dynamic.original_module/1 — get the backup module name.
  • docs/dynamic.md — full documentation with setup, usage, comparison table, and migration path from dynamic to contract-based facades.

0.38.0

Added

  • Static dispatch facades now generate direct function calls (Module.function(args)) with @compile {:inline, ...} instead of apply(Module, :function, [args]). This allows the BEAM to inline facade functions at call sites — zero dispatch overhead, zero extra stack frames. Falls back to apply for operations with pre_dispatch: transforms where args are computed at runtime.

Changed

  • Breaking: Removed auto-bang variant generation from defcallback. The :bang option is no longer supported. Previously defcallback insert(...) :: {:ok, T} | {:error, E} would auto-generate insert!/1. This added complexity (bang_mode, extract_success_type, has_ok_error_pattern?) for limited value — Ecto already provides its own bang functions, and the generic wrappers produced unhelpful error messages.
  • bang: false is no longer needed on get!, get_by!, one!, transact, and rollback declarations — they are now regular defcallback operations with no special treatment.
  • bang_mode removed from __callbacks__/0 introspection maps.

Fixed

  • Module fakes (Double.fake(contract, Module)) now run via %Defer{} in the calling process instead of inside the NimbleOwnership GenServer. Fixes DBConnection.OwnershipError when using Double.fake(Repo, Backend.Repo) for integration tests with real Ecto implementations.

0.37.2

Fixed

  • Module fakes (Double.fake(contract, Module)) now run in the calling process instead of the NimbleOwnership GenServer process. Previously invoke_module_fallback called apply(module, op, args) directly inside get_and_update, which meant real implementations doing I/O (e.g. Ecto queries) ran in the GenServer — a process with no Ecto sandbox checkout. Now uses %Defer{} to move the apply outside the lock, matching how transact already works. This fixes DBConnection.OwnershipError when using Double.fake(Repo, Backend.Repo) in integration tests.

0.37.1

Added

  • DoubleDown.Double.allow/2,3 — convenience delegate to DoubleDown.Testing.allow/2,3 for discoverability when using the Double API exclusively.
  • Documented :warn_on_typespec_mismatch? option in defcallback @doc.

Fixed

  • Moved DoubleDown.Defer to DoubleDown.Dispatch.Defer — it's an internal dispatch mechanism, not user-facing.
  • invoke_stateful_fallback now validates return tuple shape consistently with invoke_expect and invoke_stub. A stateful fake returning a bare value instead of {result, new_state} now raises a descriptive ArgumentError instead of a raw MatchError.
  • stub_handler?/1 and fake_handler?/1 now check @behaviour declarations instead of duck-typing function exports. Previously any module with new/2 would match stub_handler?.
  • Testing.allow/2 now has an explicit @spec (was missing for the 2-arity form generated by the default argument).
  • verify! error message no longer incorrectly says verify!/0 when called via verify!/1.

0.37.0

Added

  • DoubleDown.Dispatch.StubHandler behaviour for stateless stub handler modules. Implement new/2 to make a stub usable by module name in Double.stub:

    Double.stub(Repo, Repo.Test)
    Double.stub(Repo, Repo.Test, fn :all, [User] -> [] end)
  • Repo.Test implements StubHandler. new/2 accepts a fallback function as the first arg and opts as the second. The legacy new(fallback_fn: fn ...) keyword form is still supported.

  • Double.stub/2 auto-detects StubHandler modules. Double.stub/3 disambiguates StubHandler modules from per-operation stubs by checking if the second arg is a loaded module implementing the behaviour.

0.36.0

Added

  • DoubleDown.Dispatch.FakeHandler behaviour for stateful fake handler modules. Implement new/2 and dispatch/3 (or /4) to make a fake usable by module name in Double.fake:

    Double.fake(Repo, Repo.InMemory)
    Double.fake(Repo, Repo.InMemory, [%User{id: 1}])
    Double.fake(Repo, Repo.InMemory, [%User{id: 1}], fallback_fn: fn ...)
  • Repo.InMemory implements FakeHandler. new/2 accepts seed data as the first arg (list of structs or pre-built store map) and opts as the second. The legacy new(seed: [...], fallback_fn: ...) keyword form is still supported.

  • Double.fake/2 auto-detects FakeHandler modules — if the module implements the behaviour, it's used as a stateful fake with default state. Non-FakeHandler modules are still treated as module fakes.

0.35.0

Added

  • Stateful per-operation stubs. DoubleDown.Double.stub/3 now accepts 2-arity and 3-arity responder functions that can read and update the fake's state, with the same semantics as stateful expect responders. All arities can return Double.passthrough() to conditionally delegate to the fallback/fake. This enables the pattern "intercept every call, decide per-call whether to handle or delegate, without knowing the call count."

0.34.0

Added

  • DoubleDown.Double.passthrough/0 — returns a sentinel value that expect responders can return to conditionally delegate to the fallback/fake. The expect is still consumed for verify! counting. This enables patterns like "fail if duplicate, otherwise let the fake handle it" without duplicating the fake's logic. Works with all responder arities (1, 2, 3).

0.33.0

Added

  • Stateful expect responders. DoubleDown.Double.expect now accepts 2-arity and 3-arity responder functions that can read and update the stateful fake's state:

    • 2-arity: fn [args], state -> {result, new_state} end
    • 3-arity: fn [args], state, all_states -> {result, new_state} end (cross-contract state access)

    Stateful responders require Double.fake/3 to be called first. ArgumentError is raised at expect time if no stateful fake is configured, and at dispatch time if the responder doesn't return a {result, new_state} tuple. 1-arity expects are unchanged.

Fixed

  • Removed stale "Limitation: no inline passthrough" notes from DoubleDown.Double moduledoc and docs/testing.md — this limitation no longer exists with stateful expect responders.
  • Fixed historical .Port. module name references in docs.
  • Removed stale Skuld reference in contract.ex comment.

0.32.0

Added

  • 4-arity stateful handlers with read-only cross-contract state access. Handlers registered with fn operation, args, state, all_states -> {result, new_state} end receive a snapshot of all contract states as the 4th argument. This enables the "two-contract" pattern where a Queries handler reads the Repo InMemory store. Works with both DoubleDown.Double.fake/3 and DoubleDown.Testing.set_stateful_handler/3. Existing 3-arity handlers are unchanged (non-breaking).
  • DoubleDown.Contract.GlobalState sentinel key in the global state map. If a handler accidentally returns the global map instead of its own state, a clear ArgumentError is raised.

Fixed

  • Exceptions inside stateful handlers no longer crash the NimbleOwnership GenServer. Raises, throws, and exits that occur inside NimbleOwnership.get_and_update are now caught and transported to the calling process via %Defer{}, where they re-raise safely. Previously these would crash the ownership server — a singleton for the entire test run — aborting the suite.

0.31.1

Fixed

  • Exceptions inside stateful handlers no longer crash the NimbleOwnership GenServer. Raises, throws, and exits that occur inside NimbleOwnership.get_and_update (e.g. a module fallback hitting a dead Ecto sandbox connection during test teardown) are now caught and transported to the calling process via %Defer{}, where they re-raise safely. Previously these would crash the ownership server — a singleton for the entire test run — aborting the suite.

0.31.0

Added

  • Compile-time spec mismatch detection between defcallback type specs and the production implementation's @spec declarations. When a facade is compiled with a known static impl, param types and return types are compared and a CompileError is raised on mismatch. This catches the class of bug where a defcallback declares a narrower type than the impl accepts (e.g. keyword() vs list()), which would otherwise only surface as a non-local Dialyzer error.
  • warn_on_typespec_mismatch?: true option on defcallback to downgrade the compile error to a warning for individual operations during migration.
  • DoubleDown.Contract.SpecWarnings — private module handling spec fetching, type AST normalization, and comparison.

0.30.1

Fixed

  • transact return type spec now includes the Ecto.Multi 4-tuple error shape: {:error, term(), term(), term()}. Previously the spec only declared {:ok, term()} | {:error, term()}, causing Dialyzer to conclude that code handling Multi's {:error, failed_op, failed_value, changes_so_far} return was unreachable.

0.30.0

Added

  • Opts-accepting variants for all DoubleDown.Repo contract operations. Every operation now has both a base arity and an + opts arity (e.g. insert/1 and insert/2, get/2 and get/3), matching Ecto.Repo's actual API where every function accepts an optional opts keyword list. This fixes UndefinedFunctionError when Ecto.Multi.update/4 (and insert/4, delete/4) receive a function argument — Multi's internal :run callbacks call repo.update(changeset, opts) with 2 args, which previously had no matching facade function.
  • Repo.Test and Repo.InMemory handle opts-accepting dispatches by stripping opts and delegating to base-arity logic.

0.29.0

Added

  • get_by/get_by! in Repo.InMemory now use 3-stage dispatch (state → fallback → error) when the queryable is a bare schema module and the clauses include all primary key fields. PK lookup uses the existing store index — no scan required. If found, any additional non-PK clauses are verified against the record. If not found in state, falls through to the fallback function (absence is not authoritative). Non-PK clauses, Ecto.Query queryables, and partial composite PKs still delegate to the fallback as before.
  • Composite PK support in get_by/get_by! — all PK fields must be present in the clauses for a direct state lookup.

0.28.1

Changed

  • defcallback macro @doc now includes full rationale for why defcallback is used instead of plain @callback (parameter names, combined contract+facade, LSP docs, additional metadata).
  • repo.md: rollback section, operation dispatch table updated, {:defer, fn} references updated to %DoubleDown.Defer{}.
  • DoubleDown.Contract @moduledoc: "typed port contracts" → "contract behaviours".

0.28.0

Added

  • rollback/1 added to DoubleDown.Repo contract (now 17 operations). Throws {:rollback, value} via %Defer{}, caught by transact which returns {:error, value}. Matches Ecto.Repo.rollback/1 API. Both Repo.Test and Repo.InMemory support rollback — state mutations from earlier operations are not undone (documented limitation).
  • Nested transact tests for both Repo.Test and Repo.InMemory, including via Double.stub and Double.fake.

0.27.0

Added

  • %DoubleDown.Defer{fn: fun} struct — dedicated deferred execution marker, replacing the {:defer, fn} tuple convention. Eliminates clash risk with legitimate return values and enables deferred execution in all dispatch paths (fn, module, stateful).
  • Repo.Test now returns %Defer{} for transact operations, so Double.stub(contract, Repo.Test.new()) works correctly with transact — no NimbleOwnership deadlock.
  • Regression tests for transact-via-Double.stub scenario.

Changed

  • Breaking: {:defer, fn} tuple convention replaced by %DoubleDown.Defer{fn: fun} throughout. Affects Repo.Test, Repo.InMemory, DoubleDown.Dispatch, and DoubleDown.Double. Only relevant if you were returning {:defer, fn} from custom stateful handlers — replace with %DoubleDown.Defer{fn: fun}.

Fixed

  • NimbleOwnership deadlock when using Double.stub(contract, Repo.Test.new()) with contracts that include re-entrant operations like transact.
  • Async test race condition: added Code.ensure_loaded before function_exported? in contract tests.
  • Documentation: "contract behaviour" and "dispatch facade" compound forms at first-mention points, intro paragraphs on all doc pages, production Repo as zero-cost passthrough.

0.26.0

Changed

  • Breaking: DoubleDown.Handler renamed to DoubleDown.Double. stub for module and stateful fallbacks split out into fake:
    • Double.stub(contract, :op, fun) — per-operation stub (canned value)
    • Double.stub(contract, fun) — 2-arity function fallback
    • Double.fake(contract, module) — module fake
    • Double.fake(contract, fun, init_state) — stateful fake
  • Breaking: DoubleDown.Log API simplified — match and reject no longer take a contract parameter. The contract is specified once at verify! time. verify! now returns {:ok, log} on success.
  • Handler error messages now include the contract name and args.
  • .formatter.exs updated for defcallback rename.

Added

  • DoubleDown.Log.verify! returns {:ok, log} on success and includes the full dispatch log in all error messages — useful for REPL debugging.
  • :static_dispatch? option on use DoubleDown.Facade — resolves the implementation module at compile time and generates direct function calls, eliminating Application.get_env overhead entirely. Defaults to fn -> Mix.env() == :prod end.
  • Comprehensive docs review: restructured testing.md with Double as primary API, updated all examples to use Double.expect/stub/fake instead of raw set_*_handler APIs, consistent terminology throughout.

0.25.0

Changed

  • Breaking: defport renamed to defcallback, __port_operations__/0 renamed to __callbacks__/0. The defcallback macro uses the same syntax as @callback — replace the keyword and you're done.
  • Breaking: DoubleDown.Repo.Contract renamed to DoubleDown.Repo. Less verbose in Handler.stub and Handler.expect calls.
  • Breaking: DoubleDown.Log API simplified — match and reject no longer take a contract parameter. The contract is specified once at verify! time: Log.match(:op, fn _ -> true end) |> Log.verify!(MyContract).

Added

  • :static_dispatch? option on use DoubleDown.Facade — resolves the implementation module at compile time and generates direct function calls, eliminating Application.get_env overhead entirely. Defaults to fn -> Mix.env() == :prod end. Falls back to runtime config dispatch when compile-time config is unavailable.
  • README rewritten with new "Why DoubleDown?" section, Mox comparison, failure scenario example, and implementation snippet.
  • Comprehensive docs review: "port" → "contract" throughout, terminology updated, fail-fast pattern documented, Skuld references simplified, LSP docs bullet added to defcallback rationale.

0.24.0

Changed

  • Breaking: Library renamed from hex_port / HexPort to double_down / DoubleDown. All module names, app name, package name, and GitHub URLs updated. The emphasis has shifted from hexagonal architecture boundaries to the distinctive test double capabilities.

0.23.0

Changed

  • Breaking: DoubleDown.Double API simplified — expect and stub now write directly to NimbleOwnership with immediate effect. Removed %DoubleDown.Double{} struct, new/0, and install!/1. All functions return the contract module atom for Mimic-style piping:

    MyContract
    |> DoubleDown.Double.stub(MyImpl)
    |> DoubleDown.Double.expect(:get, fn [id] -> %Thing{id: id} end)

    A canonical handler function is installed on first touch and reads dispatch config from state — no builder assembly step needed.

0.22.0

Added

  • DoubleDown.Double.expect/4..5 now accepts :passthrough as the handler argument. A :passthrough expect delegates to the configured fallback (fn, stateful, or module) while consuming the expect for verify! counting. Supports times: n. Enables call-counting without changing behaviour, and can be mixed with function expects for patterns like "first insert succeeds through InMemory, second fails".
  • Documentation in docs/repo.md for using DoubleDown.Double with Repo.Test and Repo.InMemory for failure scenario testing, including error simulation, :passthrough call counting, and combined Handler + Log assertions.

Fixed

  • Added @spec clauses for all stub/2..4 forms to satisfy Dialyzer.

0.21.0

Added

  • DoubleDown.Double.stub/3 (with accumulator: stub/4) for module fallback — delegates unhandled operations to a module implementing the contract's @behaviour. Validated at install! time.
  • DoubleDown.Double.stub/3 (with accumulator: stub/4) for stateful fallback — accepts a 3-arity fn operation, args, state -> {result, new_state} end with initial state, same signature as set_stateful_handler. Integrates stateful fakes (e.g. Repo.InMemory) into the Handler dispatch chain. Expects that short-circuit (e.g. error simulation) leave the fallback state unchanged.
  • Fallback types are now a tagged union ({:fn, fun}, {:stateful, fun, init_state}, {:module, module}) — mutually exclusive, setting one replaces the other.

0.20.0

Added

Fixed

0.19.0

Added

  • DoubleDown.Double.stub/2 and stub/3 (with accumulator) for 2-arity contract-wide fallback stubs. Accepts fn operation, args -> result end — the same signature as set_fn_handler — as a catch-all for operations without a specific expect or per-operation stub. Dispatch priority: expects > per-operation stubs > fallback stub > raise.

0.18.0

Added

  • DoubleDown.Double — Mox-style expect/stub handler builder. Builds stateful handler functions from a declarative specification with multi-contract chaining and ordered expectations. API: expect/3..5, stub/3..4, install!/1, verify!/0.
  • DoubleDown.Log — log-based expectation matcher. Declares structured expectations against the dispatch log after execution, matching on the full {contract, operation, args, result} tuple. Supports loose (default) and strict matching modes, times: n counting, and reject expectations. API: match/3..5, reject/2..3, verify!/1..2.
  • Terminology mapping and glossary in README and getting-started guide, mapping DoubleDown concepts (contract, facade, test double, port) to familiar Elixir/Mox equivalents with a stub/mock/fake breakdown.

0.17.0

Changed

  • Breaking: Renamed generated key helper from key/N to __key__/N on facade modules, following the Elixir convention for generated introspection functions. This avoids clashes with user-defined defcallback key(...) operations.

Fixed

  • Added :mix to plt_add_apps in mix.exs so Dialyzer can resolve the compile-time Mix.env/0 call in DoubleDown.Facade.__using__/1.

0.16.1

Fixed

  • Added :mix to plt_add_apps in mix.exs so Dialyzer can resolve the compile-time Mix.env/0 call in DoubleDown.Facade.__using__/1.

Changed

  • Documentation updates for :test_dispatch? in docs/getting-started.md (dispatch resolution section) and docs/testing.md (setup section).

0.16.0

Added

  • :test_dispatch? option for use DoubleDown.Facade — controls whether the generated facade includes the NimbleOwnership-based test handler resolution step. Accepts true, false, or a zero-arity function returning a boolean, evaluated at compile time. Defaults to fn -> Mix.env() != :prod end, so production builds get a config-only dispatch path with zero NimbleOwnership overhead (no GenServer.whereis ETS lookup).
  • DoubleDown.Dispatch.call_config/4 — config-only dispatch function that skips test handler resolution entirely. Used by facades compiled with test_dispatch?: false.

0.15.0

Added

  • pre_dispatch option for defcallback — a generic mechanism for transforming arguments before dispatch. Accepts a function (args, facade_module) -> args declared at the contract level, spliced into the generated facade function as AST.
  • Repo.Test tests split into dedicated test/double_down/repo/test_test.exs module.

Changed

  • 1-arity transact functions are now wrapped into 0-arity thunks at the facade boundary via pre_dispatch. The thunk closes over the facade module, so calls inside the function (e.g. repo.insert(cs)) go through the facade dispatch chain. This ensures facade-level concerns (logging, telemetry) apply in both test and production.
  • Repo.Test and Repo.InMemory adapters no longer handle 1-arity transaction functions — they always receive 0-arity thunks (from pre_dispatch wrapping) or Ecto.Multi structs.
  • The hardcoded :transact special-case in DoubleDown.Facade has been removed. The Repo-specific facade injection is now declared on the defcallback in DoubleDown.Repo using the generic pre_dispatch mechanism.

Fixed

  • User-supplied fallback functions in Repo.InMemory that raise non-FunctionClauseError exceptions (e.g. RuntimeError, ArgumentError) no longer crash the NimbleOwnership GenServer. Exceptions are captured and re-raised in the calling test process via {:defer, fn -> reraise ... end}.

0.14.0

Added

  • DoubleDown.Repo.insert_all/3 — standalone bulk insert operation, dispatched via fallback in both test adapters.
  • DoubleDown.Testing.set_mode_to_global/0 and set_mode_to_private/0 — global handler mode for testing through supervision trees, Broadway pipelines, and other process trees where individual pids are not accessible. Uses NimbleOwnership shared mode. Incompatible with async: true.
  • DoubleDown.Repo.Autogenerate — shared helper module for autogenerating primary keys and timestamps in test adapters. Handles :id (integer auto-increment), :binary_id (UUID), parameterized types (Ecto.UUID, Uniq.UUID, etc.), and @primary_key false schemas.
  • docs/migration.md — incremental adoption guide covering the two-contract pattern, coexisting with direct Ecto.Repo calls, and the fail-fast test config pattern.
  • Process-testing patterns in docs/testing.md — decision table, GenServer example, supervision tree example.

Changed

  • Test adapters (Repo.Test, Repo.InMemory) now check changeset.valid? before applying changes — invalid changesets return {:error, changeset}, matching real Ecto.Repo behaviour.
  • Test adapters now populate inserted_at/updated_at timestamps via Ecto's __schema__(:autogenerate) metadata. Custom field names and timestamp types are handled automatically.
  • 1-arity transact functions now receive the facade module instead of nil, enabling fn repo -> repo.insert(cs) end patterns.
  • The internal opts key for threading the facade module through transact was renamed from :repo_facade to DoubleDown.Repo.Facade for proper namespacing.
  • Primary key autogeneration is now metadata-driven — supports :binary_id (UUID), Ecto.UUID, and other parameterized types. Raises ArgumentError when autogeneration is not configured and no PK value is provided.
  • Autogeneration logic extracted from Repo.Test and Repo.InMemory into shared DoubleDown.Repo.Autogenerate module.
  • Repo contract now has 16 operations (was 15).

Fixed

  • Invalid changesets passed to Repo.Test or Repo.InMemory insert/update no longer silently succeed — they return {:error, changeset}.
  • Repo.InMemory store is unchanged after a failed insert/update with an invalid changeset.

0.13.0

Added

  • Fail-fast documentation for impl: nil test configuration.

Changed

  • Improved error messages when no implementation is configured in test mode.

0.12.0

Changed

  • Removed unused Ecto wrapper macro.
  • Version now read from VERSION file.

0.11.1

Changed

  • Documentation improvements (README, hexdocs, testing guide).
  • Removed unnecessary reset calls from test examples.

0.11.0

Fixed

  • Fixed compiler warnings.

0.10.0

Added

  • Facade without implicit Contractuse DoubleDown.Facade with an explicit :contract option for separate contract modules.
  • Documentation explaining why defcallback is used instead of standard @callback declarations.

0.9.0

Added

  • Single-module Contract + Facadeuse DoubleDown.Facade without a :contract option implicitly sets up the contract in the same module.

Changed

  • Dispatch references the contract module, not the facade.

0.8.0

Added

  • DoubleDown.Repo — built-in 15-operation Ecto Repo contract with Repo.Test (stateless) and Repo.InMemory (stateful) test doubles.
  • MultiStepper for stepping through Ecto.Multi operations without a database.

Changed

  • Renamed Port to Facade throughout.
  • Removed separate .Behaviour module — behaviours are generated directly on the contract module.

0.7.0

Changed

  • Repo.InMemory fallback function now receives state as a third argument (operation, args, state), enabling fallbacks that compose canned data with records inserted during the test.

0.6.0

Fixed

  • Made DoubleDown.Contract.__using__/1 idempotent — safe to use multiple times.

0.5.0

Changed

  • Improved Repo.Test stateless handler.

0.4.0

Added

  • Repo.InMemory — stateful in-memory Repo implementation with read-after-write consistency for PK-based lookups.
  • NimbleOwnership-based process-scoped handler isolation for async: true tests.

0.3.1

Fixed

  • Expand type aliases at macro time in defcallback to resolve Dialyzer unknown_type errors.

0.3.0

Added

  • transact defcallback with {:defer, fn} support for stateful dispatch — avoids NimbleOwnership deadlocks.
  • Repo.transact! for Ecto.Multi operations.

0.2.0

Changed

0.1.0

Added