CHANGELOG.md

# Changelog

Completed roadmap tasks. For upcoming work, see [ROADMAP.md](ROADMAP.md).

---

## v0.4.1 (2026-02-04)

### Breaking Changes

**Coverage is now OFF by default (use `--cover` to enable)**

Changed coverage from on-by-default to off-by-default for faster test runs.

**Before (v0.4.0):**
```bash
mix test.json --quiet        # Coverage enabled by default
mix test.json --quiet --no-cover  # Disable coverage
```

**After (v0.4.1+):**
```bash
mix test.json --quiet        # No coverage (faster)
mix test.json --quiet --cover    # Enable coverage
```

**Rationale:**
- Coverage has ordering/timing issues that are hard to pin down
- Most test runs just need pass/fail, not coverage
- Explicit `--cover` is clearer than implicit coverage
- Faster default test runs without coverage overhead

### Bug Fixes

**Fix: `--cover --compact` no longer crashes**

Previously, using `--cover` with `--compact` would crash because `:json.decode` cannot parse JSONL (one JSON object per line). Now outputs a warning to stderr and skips coverage merge.

**Fix: Coverage works on clean builds**

Previously, coverage on a clean build would report empty data because `compile_project_modules()` ran before compilation. Now ensures `mix compile` runs before coverage instrumentation starts.

### Internal

- Added `test_apps/coverage_app/` for coverage regression testing
- Clean build test uses isolated `MIX_BUILD_PATH` instead of destructive `mix clean`
- Added `dialyzer_json` dependency for type checking

---

## v0.4.0 (2026-02-04)

### New Features

**Code Coverage (enabled by default)**

`mix test.json` now includes code coverage data in the JSON output. Coverage is collected using Erlang's `:cover` module and includes:

- Total coverage percentage
- Per-module coverage with uncovered line numbers
- Configurable module ignore list via `test_coverage: [ignore_modules: [...]]` in mix.exs

```json
{
  "coverage": {
    "total_percentage": 92.5,
    "total_lines": 400,
    "covered_lines": 370,
    "modules": [
      {
        "module": "MyApp.Users",
        "file": "lib/my_app/users.ex",
        "percentage": 95.0,
        "covered_lines": 38,
        "uncovered_lines": [45, 67]
      }
    ]
  }
}
```

Use `--no-cover` to disable coverage collection for faster test runs:

```bash
mix test.json --quiet --no-cover
```

**Files added:**
- `lib/ex_unit_json/coverage.ex` - Coverage collection module
- `test/ex_unit_json/coverage_test.exs` - Coverage unit tests

**Files modified:**
- `lib/mix/tasks/test_json.ex` - Added `--no-cover` flag, coverage integration
- `mix.exs` - Added `:tools` to extra_applications, test_coverage config
- `README.md`, `AGENTS.md` - Documentation for coverage feature

---

## v0.3.0 (2026-01-24)

### Breaking Changes

**Default behavior now shows only failed tests (AI-optimized)**

`mix test.json` now outputs only failed tests by default, equivalent to the previous `--failures-only` behavior. This is a breaking change optimized for AI agents where passing tests are noise.

**Before (v0.2.x):**
```bash
mix test.json --quiet  # Shows ALL tests
mix test.json --quiet --failures-only  # Shows only failures
```

**After (v0.3.0+):**
```bash
mix test.json --quiet  # Shows only failures (DEFAULT)
mix test.json --quiet --all  # Shows ALL tests
```

**When all tests pass:**
```json
{"version":1,"summary":{"total":50,"passed":50,"failed":0},"tests":[]}
```

**Rationale:**
- AI agents are the primary users of this library
- Passing tests are noise - AI needs to know what's broken
- Context is expensive and limited
- When all pass, "0 failures" is sufficient information

### New Features

**`--all` flag to show all tests**

Use `--all` when you need all tests in the output:
```bash
mix test.json --quiet --all
```

### Migration Guide

If you have scripts or workflows that depend on seeing all tests:
1. Add `--all` flag: `mix test.json --quiet --all`
2. Or continue using `--failures-only` explicitly (still works, now the default)

**Files modified:**
- `lib/ex_unit_json/filters.ex` - Changed default from `false` to `true`
- `lib/ex_unit_json/config.ex` - Updated `failures_only?/0` default
- `lib/mix/tasks/test_json.ex` - Added `--all` flag, updated docs
- `README.md`, `AGENT.md` - Updated documentation for new default
- Tests updated for new default behavior

---

## v0.2.14 (2026-01-23)

### Documentation

**Docs: Recommend `--failures-only` as default first run instead of `--summary-only`**

Changed the recommended workflow to eliminate a wasteful roundtrip. Previously, docs suggested:
1. `mix test.json --quiet --summary-only` (run all tests, see counts)
2. `mix test.json --quiet --failures-only` (run all tests AGAIN to see failures)

Now the recommended workflow is:
1. `mix test.json --quiet --failures-only` (run all tests, see failures directly)
2. `mix test.json --quiet --failed --first-failure` (iterate on failures)

This saves running the full test suite twice just to see what failed.

**Files updated:**
- `README.md` - Added recommended workflow section, updated examples
- `AGENT.md` - Updated default workflow and quick reference

---

## v0.2.13 (2026-01-23)

### Bug Fixes

**Fix: Use `:debug` instead of invalid `:all` Logger level**

v0.2.12 used `Logger.configure(level: :all)` but `:all` is not a valid Logger level. Changed to `:debug` (the lowest valid level that allows all messages through).

---

## v0.2.12 (2026-01-23)

### Bug Fixes

**Fix: `capture_log` now works with `--quiet` flag**

Fixed a regression where tests using `ExUnit.CaptureLog.capture_log/2` would fail when running with `--quiet` because Logger messages were being filtered globally.

**Root cause:** v0.2.10 used `Application.put_env(:logger, :level, :error)` to suppress Logger output from test_helper.exs, but this sets the global Logger level which filters messages before they reach ANY handler, including capture_log's handler.

**Fix:** In the formatter's init (which runs after test_helper.exs but before tests), reset the global Logger level to `:debug` while keeping the handler level at `:error`. This way:
- test_helper.exs Logger output is suppressed (global level :error during setup)
- capture_log works in tests (global level :debug after formatter init)
- Console output still suppressed (handler level :error)

**Files modified:**
- `lib/ex_unit_json/formatter.ex` - Reset global Logger level in init

---

## v0.2.11 (2026-01-23)

### Improvements

**`--quiet` now suppresses TIP warnings for completely clean piping**

When using `--quiet`, the "TIP: N previous failure(s) exist" message is now suppressed. This ensures `mix test.json --quiet | jq` works without needing `2>/dev/null`.

**Before (v0.2.10):**
```bash
MIX_QUIET=1 mix test.json --quiet --summary-only 2>&1 | jq
# jq: parse error (TIP message on stderr combined with stdout)
```

**After (v0.2.11):**
```bash
MIX_QUIET=1 mix test.json --quiet --summary-only 2>&1 | jq
# Works! TIP is suppressed with --quiet
```

Note: The ERROR message for `enforce_failed: true` is still shown even with `--quiet` since it blocks execution entirely.

**Files modified:**
- `lib/mix/tasks/test_json.ex` - Suppress TIP when `--quiet` is used

---

## v0.2.10 (2026-01-23)

### Bug Fixes

**Fix: `--quiet` now properly suppresses Logger output from test_helper.exs**

Fixed an issue where Logger.info/debug/warning messages from test_helper.exs would still appear in stdout even with `--quiet`, breaking jq piping.

**Root cause:** The `:logger.remove_handler(:default)` call in the Mix task was being undone when the test environment loaded and restarted the :logger application.

**Fix:** In addition to removing the handler, also set `Application.put_env(:logger, :level, :error)` before running tests. When the :logger app restarts during test setup, it initializes with `:error` level, suppressing info/debug/warning messages.

**Behavior:**
- `--quiet` now suppresses ALL Logger output (info, debug, warning) from test_helper.exs and test files
- Logger.error messages still appear (appropriate for actual errors)
- Without `--quiet`, Logger output works normally

**Files modified:**
- `lib/mix/tasks/test_json.ex` - Add `Application.put_env(:logger, :level, :error)` when `--quiet` is used

**Added test app:**
- `test_apps/logger_app/` - Test fixture with Logger.info in test_helper.exs for regression testing

**New integration tests:**
- `--quiet suppresses Logger output from test_helper.exs`
- `Logger output appears without --quiet`
- `--quiet produces clean JSON for jq piping`

---

## v0.2.9 (2026-01-23)

### Bug Fixes

**Fix: Buffer JSON output for clean piping without `--output`**

Fixed an issue where stdout pollution from `test_helper.exs` or other sources would corrupt the JSON stream when piping to jq, even with `--quiet` and `MIX_QUIET=1`.

**Problem:**
```bash
MIX_QUIET=1 mix test.json --quiet | jq '.summary'
# jq: parse error: Invalid numeric literal at line 2, column 1
```

This happened because some projects print to stdout in `test_helper.exs` (e.g., "✓ 3 testnet credentials registered") which appears before the JSON formatter runs.

**Solution:** When `--quiet` is used WITHOUT an explicit `--output` path, automatically buffer JSON to a temp file and output it at the very end (after all other stdout pollution).

**Behavior:**
- `mix test.json --quiet` → Buffers to temp file, outputs clean JSON at end
- `mix test.json --quiet --output FILE` → Writes to FILE (unchanged behavior)
- `mix test.json` (no --quiet) → Direct stdout (unchanged behavior)

**Files modified:**
- `lib/mix/tasks/test_json.ex` - Added `maybe_use_temp_output/1` and `output_buffered_json/1`

---

## v0.2.8 (2026-01-23)

### Bug Fixes

**Fix: Logger output still corrupting JSON stream with `--quiet`**

Fixed an issue where Logger messages would still go to stdout even with `--quiet`, corrupting the JSON stream when piping to jq.

**Root cause:** The attempt to redirect the default logger handler to stderr using `:logger.set_handler_config(:default, :config, %{type: :standard_error})` doesn't work because the handler's output type cannot be changed after initialization.

**Fix:** Remove the default handler entirely when `--quiet` is used:
```elixir
:logger.remove_handler(:default)
```

This completely suppresses Logger output, which is appropriate for `--quiet` mode where clean JSON output is the priority.

**Files modified:**
- `lib/mix/tasks/test_json.ex` - Use `remove_handler` instead of `set_handler_config`

---

## v0.2.7 (2026-01-23)

### Documentation

**Docs: `MIX_QUIET=1` required for jq piping when compilation occurs**

Updated documentation to show that `MIX_QUIET=1` must be set when piping to jq if code changes trigger recompilation.

**Issue:**
```bash
$ mix test.json --quiet --summary-only | jq '.summary'
jq: parse error: Invalid numeric literal at line 1, column 10
```

**Root cause:** Mix outputs compilation messages ("Compiling 1 file (.ex)", "Generated app_name app") to stdout before the JSON formatter runs. This happens because compilation occurs before the task code that sets `Mix.shell(Mix.Shell.Quiet)` can execute.

**Fix:** Use `MIX_QUIET=1` environment variable which suppresses Mix output at a lower level:
```bash
MIX_QUIET=1 mix test.json --quiet --summary-only | jq '.summary'
```

Alternatively, use `--output FILE` which avoids piping issues entirely:
```bash
mix test.json --quiet --output /tmp/results.json
jq '.summary' /tmp/results.json
```

**Files modified:**
- `README.md` - Updated jq section with `MIX_QUIET=1`
- `AGENT.md` - Updated jq section with `MIX_QUIET=1`
- `lib/mix/tasks/test_json.ex` - Added comments explaining the limitation

---

## v0.2.6 (2026-01-21)

### Bug Fixes

**Fix: `capture_log` returns empty string when using `--quiet`**

Fixed an issue where `ExUnit.CaptureLog.capture_log/2` returned empty strings when running tests with `mix test.json --quiet`.

**Error:**
```elixir
@tag capture_log: false
test "capture_log works" do
  log = capture_log(fn -> Logger.info("test message") end)
  assert log =~ "test message"  # FAILS: log is ""
end
```

**Root cause:** The `--quiet` flag was calling `Logger.configure(level: :error)`, which sets the **global** Logger level. This filters messages *before* they reach any handler, including `capture_log`'s capture handler.

**Fix:** Changed to `:logger.set_handler_config(:default, :level, :error)` instead. This sets the **console handler's** level while leaving the global level unchanged:
- Console output is still suppressed (handler ignores messages below `:error`)
- `capture_log` works because messages still reach its capture handler

**Files modified:**
- `lib/ex_unit_json/formatter.ex` - Use handler-level suppression
- `lib/mix/tasks/test_json.ex` - Use handler-level suppression

---

## v0.2.5 (2026-01-20)

### Bug Fixes

**Fix: `mix test.json | jq` now works correctly**

Fixed an issue where piping `mix test.json` output to jq would fail with parse errors when previous test failures existed.

**Error:**
```
$ mix test.json --quiet | jq '.summary'
jq: parse error: Invalid numeric literal at line 1, column 10
```

**Root cause:** The TIP message ("TIP: 3 previous failure(s) exist...") was written to stdout via `Mix.shell().info()`, contaminating the JSON stream.

**Fix:** Changed both the TIP warning and ERROR message (for `enforce_failed` mode) to use `IO.puts(:stderr, ...)` instead of `Mix.shell()` functions. This ensures stdout contains only valid JSON.

**After the fix:**
```bash
# Works - TIP goes to stderr, JSON to stdout
mix test.json --quiet | jq '.summary'

# TIP still visible in terminal (stderr)
mix test.json --quiet
```

**Files modified:**
- `lib/mix/tasks/test_json.ex` - Route TIP and ERROR messages to stderr

---

## v0.2.4 (2026-01-18)

### Bug Fixes

**Fix: Protocol.UndefinedError for non-atom failure kinds**

Fixed a crash when a test fails due to a linked process exit (e.g., WebSocket connection failure). ExUnit reports these failures with a tuple kind `{:EXIT, pid}` instead of an atom like `:error`, `:exit`, or `:throw`.

**Error:**
```
** (Protocol.UndefinedError) protocol String.Chars not implemented for Tuple.
Got value: {:EXIT, #PID<0.797.0>}
```

**Root cause:** `encode_failure_kind/2` used `to_string(kind)` which fails for tuples since `String.Chars` is not implemented for them.

**Fix:** Changed `to_string(kind)` to `inspect(kind)` which safely handles any Elixir term.

**Files modified:**
- `lib/ex_unit_json/json_encoder.ex` - Use `inspect/1` for unknown failure kinds
- `test/ex_unit_json/json_encoder_test.exs` - Added test for tuple failure kinds

---

## v0.2.3 (2026-01-11)

### Bug Fixes

**Fix: JSON formatter not used in Phoenix projects**

Fixed a race condition where `mix test.json` would output CLI format (dots) instead of JSON in Phoenix projects due to timing issues with `ExUnit.configure`.

**Root cause:** Calling `ExUnit.configure(formatters: [...])` before delegating to `mix test` could be overwritten by stale compilation state or timing issues with `test_helper.exs` loading.

**Solution:** Use `--formatter` flag instead of `ExUnit.configure`:
```elixir
# Before (race condition prone)
ExUnit.configure(formatters: [ExUnitJSON.Formatter])
Mix.Task.run("test", test_args)

# After (robust)
Mix.Task.run("test", ["--formatter", "ExUnitJSON.Formatter" | test_args])
```

**Fix: Noisy debug logs about failures file**

Removed spurious `[debug] Could not parse failures file` messages that appeared on every run. The failures file format changed in Elixir 1.17+ from a list to `{version, map}` tuple.

**Fix: Correctly parse new failures file format**

Updated `count_previous_failures/1` to handle both:
- New format (Elixir 1.17+): `{version, %{test_id => path}}`
- Old format: `[test_id, ...]`

**Files modified:**
- `lib/mix/tasks/test_json.ex` - Use `--formatter` flag, fix failures file parsing

**Added:**
- `test_apps/phoenix_app/` - Phoenix 1.8 test fixture for regression testing
- `test/mix/tasks/test_json_test.exs` - Phoenix integration tests

---

## v0.2.2 (2026-01-11)

### Improvements

**More defensive error handling in `count_previous_failures/1`:**
- Changed `rescue ArgumentError` to `rescue _` to catch all potential parsing errors
- Ensures graceful fallback to 0 when failures file is corrupted or malformed

**Improved test helper `decode_json/1`:**
- Replaced fragile regex with simpler line-based JSON extraction
- More robust parsing of test output with compilation messages

**Code quality:**
- Fixed Credo line length issue in `@valid_options` (config.ex)
- Added test case for malformed binary failures file

**Files modified:**
- `lib/mix/tasks/test_json.ex` - More defensive rescue clause
- `lib/ex_unit_json/config.ex` - Reformatted long line
- `test/mix/tasks/test_json_test.exs` - Improved decode_json, new test case

---

## v0.2.1 (2026-01-10)

### Bug Fix: `enforce_failed` Now Works Correctly

Fixed a bug where `enforce_failed: true` configuration had no effect because the library was looking for the failures file in the wrong location.

**The problem:**
- ExUnit writes failures to `_build/test/lib/<app>/.mix/.mix_test_failures` (Erlang term format)
- ex_unit_json was checking `.mix_test_failures` in the project root (and treating it as text)

**What's fixed:**
- `failures_file/0` now returns the correct path matching ExUnit's location
- `count_previous_failures/1` now correctly decodes Erlang term format using `:erlang.binary_to_term/1`
- Both warning and enforcement modes now work as documented

**Files modified:**
- `lib/mix/tasks/test_json.ex` - Fixed path computation and file format parsing

---

## v0.2.0 (2026-01-10)

### Warn-by-Default for `--failed` Usage

When previous test failures exist (`.mix_test_failures`) and you're running the full test suite, a helpful tip is now shown:

```
TIP: 3 previous failure(s) exist. Consider:
  mix test.json --failed
  mix test.json test/unit/ --failed
  mix test.json --only integration --failed
(Use --no-warn to suppress this message)
```

**Why this matters:** AI assistants (Claude Code, Cursor, etc.) often forget to use `--failed` when iterating on test fixes, wasting time re-running the entire suite. This warning happens automatically - no flag needed.

**Behavior:**
- Warning shown by default when `.mix_test_failures` exists and full suite is run
- Warning skipped when:
  - `--failed` is already used
  - A specific file or directory is targeted (`test/my_test.exs`, `test/unit/`)
  - Tag filters are used (`--only`, `--exclude`)
  - `--no-warn` flag is passed

**Strict enforcement (optional):**
```elixir
# config/test.exs
config :ex_unit_json, enforce_failed: true
```

With strict enforcement, running the full suite with failures will exit with an error instead of just warning.

**New flag:**
- `--no-warn` - Suppress the "use --failed" warning

**Files modified:**
- `lib/mix/tasks/test_json.ex` - Added `check_failed_usage/2`, `focused_run?/1`, `--no-warn` flag
- `test/mix/tasks/test_json_test.exs` - Added 17 new tests
- `README.md` - Added "Iteration Workflow" and "Strict Enforcement" sections
- `AGENT.md` - Updated workflow documentation

---

## v0.1.3 (2026-01-09)

### Published to Hex.pm 🎉

First public release! Available at [hex.pm/packages/ex_unit_json](https://hex.pm/packages/ex_unit_json)

**Features included in v0.1.3:**
- JSON output for ExUnit test results
- `--summary-only`, `--failures-only`, `--compact` output modes
- `--filter-out`, `--group-by-error`, `--first-failure` for AI workflows
- `--quiet` flag to suppress Logger noise
- `--output FILE` for file output
- Smart `--failed` hint for iteration workflows
- Full passthrough of ExUnit flags (`--only`, `--exclude`, `--seed`, etc.)

### Documentation

#### Improved jq usage guidance

**Issue:** Piping `mix test.json` directly to jq can fail with parse errors when compilation warnings or other non-JSON output appears before the JSON.

**Solution:** Updated documentation to clarify:
- `--summary-only` produces clean, minimal output that pipes safely to jq
- For full test details, use `--output FILE` then jq the file

**Files modified:**
- `AGENT.md` - Updated "Using jq" section with safety guidance, simplified Troubleshooting
- `README.md` - Added "Using with jq" section

---

## v0.1.2 (2026-01-09)

### Bug Fixes

#### Fix: `--quiet` flag not suppressing Logger output

**Fixed:** 2026-01-09

**Issue:** The `--quiet` flag wasn't working - Logger output still appeared even when the flag was used.

**Root cause:** Two issues:
1. `:quiet` was missing from `@valid_options` in `Config.ex`, so it was being filtered out by `validate_opts/1` and never reached the formatter
2. `Logger.configure(level: :error)` was called before `Mix.Task.run("test")`, but when the test task runs it loads application config from `config/test.exs` which overwrites the Logger level

**Fix:**
1. Added `:quiet` to `@valid_options` in `Config.ex`
2. Added `Logger.configure(level: :error)` call in formatter's `init/1` (runs after app config loads)

**Files modified:**
- `lib/ex_unit_json/config.ex` - Added `:quiet` to `@valid_options`
- `lib/ex_unit_json/formatter.ex` - Added Logger config in `init/1`
- `test/ex_unit_json/config_test.exs` - Added tests for `:quiet` option
- `test/mix/tasks/test_json_test.exs` - Added integration test for `--quiet`

---

## v0.1.1 (2026-01-09)

### Smart `--failed` Hint

**Added:** 2026-01-09

When `.mix_test_failures` exists and you're running without `--failed`, prints a helpful hint to stderr:

```
Hint: 3 test(s) failed previously. Use --failed to re-run only those.
```

Also warns if the failures file is stale (>2 hours old):

```
Note: .mix_test_failures is 3 hours old. Consider a full run if you changed shared setup.
```

**Behavior:**
- Hint only shown when:
  - `.mix_test_failures` file exists
  - `--failed` flag is NOT already being used
  - No specific test file is targeted (e.g., `test/my_test.exs`)
- Stale warning shown when file is older than 2 hours
- All output goes to stderr (doesn't pollute JSON stdout)
- Human-readable age formatting: "less than a minute", "5 minutes", "2 hours"

**Files modified:**
- `lib/mix/tasks/test_json.ex` - Added `maybe_hint_failed/1`, `maybe_hint_stale/1`, `test_path?/1`, `count_previous_failures/1`, `format_age/1`
- `test/mix/tasks/test_json_test.exs` - Added 10 unit tests for hint helper functions
- `AGENT.md` - Added "Start Here" section with recommended workflow

---

## Phase 2 Features

### `--group-by-error` Flag

**Added:** 2026-01-09

Group failures by similar error message, showing root causes at a glance.

```bash
mix test.json --group-by-error
```

**Use case:** When 100 tests fail with the same root cause (e.g., connection refused, missing credentials), see it summarized once instead of scrolling through 100 identical errors.

**Output:**
```json
{
  "error_groups": [
    {
      "pattern": "Connection refused",
      "count": 47,
      "example": {
        "name": "test API call",
        "module": "MyApp.APITest",
        "file": "test/api_test.exs",
        "line": 25
      }
    }
  ]
}
```

**Behavior:**
- Groups failed tests by the first line of their error message
- Sorts groups by count (descending) - most common errors first
- Includes one example test for each group
- Truncates long patterns at 200 characters
- Works alongside other options (`--failures-only`, etc.)
- `error_groups` key only added when option is enabled and failures exist

**Files modified:**
- `lib/ex_unit_json/config.ex` - Added `:group_by_error` option
- `lib/mix/tasks/test_json.ex` - Added `--group-by-error` flag parsing
- `lib/ex_unit_json/formatter.ex` - Added `build_error_groups/1`, `extract_error_pattern/1`, `truncate_pattern/1`
- Tests added to config_test.exs, formatter_test.exs, test_json_test.exs

---

### `--filter-out` Flag

**Added:** 2026-01-09

Mark failures matching a pattern as `"filtered": true` in JSON output. Can be used multiple times to filter multiple patterns.

```bash
mix test.json --filter-out "credentials" --filter-out "rate limit"
```

**Use case:** Filter expected failures (missing API credentials, rate limits, timeouts) to focus on real bugs. Tests still appear in output but are marked as filtered so AI tools can distinguish them.

**Behavior:**
- Runs all tests (full suite)
- Summary counts remain unchanged (filtered failures still count as failures)
- Failed tests whose error message contains any pattern get `"filtered": true` added
- Non-matching failures remain unmarked
- Passing/skipped tests are never marked
- Works with both regular JSON and `--compact` JSONL output (uses `"x": true` in compact mode)

**Files modified:**
- `lib/ex_unit_json/config.ex` - Added `:filter_out` option
- `lib/mix/tasks/test_json.ex` - Added `--filter-out` flag parsing with list accumulation
- `lib/ex_unit_json/formatter.ex` - Added `apply_filter_out/2` and `failure_matches_pattern?/2`
- `test/ex_unit_json/config_test.exs` - Added tests for filter_out_patterns/0
- `test/mix/tasks/test_json_test.exs` - Added parsing and integration tests

---

### `--quiet` Flag

**Added:** 2026-01-09

Suppress Logger output for cleaner JSON. Sets Logger level to `:error` before running tests.

```bash
mix test.json --quiet
```

**Use case:** When applications under test have Logger debug/info output, this prevents log noise from appearing before the JSON output.

**Behavior:**
- Sets `Logger.configure(level: :error)` before running tests
- Only error-level logs will appear
- JSON output remains clean and parseable

**Files modified:**
- `lib/mix/tasks/test_json.ex` - Added `--quiet` flag parsing and Logger configuration

---

### `filtered` Summary Count

**Added:** 2026-01-09

When using `--filter-out`, the summary now includes a `filtered` count showing how many failures matched the filter patterns.

**Output:**
```json
{
  "summary": {
    "total": 100,
    "failed": 50,
    "filtered": 40,
    ...
  }
}
```

**Behavior:**
- `filtered` only appears when `--filter-out` is used AND patterns match failures
- Shows how many of the `failed` count were filtered out
- Absent when no patterns provided or no matches (avoids noise)

**Files modified:**
- `lib/ex_unit_json/filters.ex` - Added `count_filtered_failures/2`
- `lib/ex_unit_json/formatter.ex` - Updated `build_summary/3` to include filtered count

---

### Fix: `--filter-out` Not Filtering Error Groups

**Fixed:** 2026-01-09

**Issue:** When using `--filter-out` with `--group-by-error`, filtered failures still appeared in `error_groups`. Expected behavior: filtered failures should be excluded from error groups entirely.

**Fix:** Added `Filters.reject_filtered_failures/2` function and updated `maybe_add_error_groups` to exclude tests matching filter_out patterns from groups.

**Files modified:**
- `lib/ex_unit_json/filters.ex` - Added `reject_filtered_failures/2`
- `lib/ex_unit_json/formatter.ex` - Updated `maybe_add_error_groups` to apply filter_out

---

### `--first-failure` Flag

**Added:** 2026-01-09

Quick iteration mode - outputs only the first failed test in detail while still running the full suite.

```bash
mix test.json --first-failure
```

**Use case:** When fixing failing tests one at a time, reduces output noise by showing only the first failure. Summary still reflects the full suite status.

**Behavior:**
- Runs all tests (full suite)
- Summary shows counts for all tests
- Tests array contains only the first failed test (by file, line, name order)
- Returns empty tests array if no failures

**Files modified:**
- `lib/ex_unit_json/config.ex` - Added `:first_failure` option
- `lib/mix/tasks/test_json.ex` - Added `--first-failure` flag parsing
- `lib/ex_unit_json/formatter.ex` - Updated filter logic
- `test/ex_unit_json/config_test.exs` - Added tests for first_failure?/0
- `test/mix/tasks/test_json_test.exs` - Added parsing and integration tests

---

## Bug Fixes

### Fix: Graceful error handling for file output

**Fixed:** 2026-01-09

**Issue:** When `--output` pointed to an invalid path (e.g., non-existent directory), the formatter would crash with `File.write!/2` raising an exception.

**Fix:** Replaced `File.write!/2` with `File.write/2` and graceful error handling:
- Prints clear error message to stderr with reason
- Tests continue to pass (exit code reflects test results, not file write)
- GenServer doesn't crash on file write failure

**Also improved:**
- Added integer guards to `extract_duration/1` for defensive programming
- Clarified `terminate/2` callback documentation (OTP compliance)

**Files modified:**
- `lib/ex_unit_json/formatter.ex` - Added `write_output/2` with error handling
- `test/mix/tasks/test_json_test.exs` - Updated test for graceful behavior

---

### Fix: Mix task not found with `only: :test` dependency config

**Fixed:** 2026-01-09

**Issue:** When configured with `only: :test`, the `mix test.json` task was not found:
```
** (Mix) The task "test.json" could not be found. Did you mean "test"?
```

**Cause:** Mix runs in the `:dev` environment by default. Mix tasks must be available in `:dev` to be discovered, but the formatter only needs to run in `:test`.

**Fix:** Updated installation instructions to use both environments:
```elixir
{:ex_unit_json, "~> 0.1.0", only: [:dev, :test], runtime: false}
```

**Files modified:**
- `README.md` - Updated installation instructions with explanation

---

### Fix: ExUnit flags (--only, --exclude, --seed, etc.) not passed through

**Fixed:** 2026-01-09

**Issue:** ExUnit filtering flags like `--only integration` weren't working:
```bash
mix test.json --only integration
# Expected: Only tests tagged @tag :integration run
# Actual: All tests ran
```

**Cause:** `OptionParser.parse/2` in non-strict mode treats unknown switches as boolean flags. So `["--only", "integration"]` became `{"--only", nil}` and `"integration"` was separated into remaining args, breaking the flag-value pairing.

**Fix:** Replaced OptionParser with explicit pattern matching that only consumes our three switches (`--summary-only`, `--failures-only`, `--output`) and passes everything else through unchanged:

```elixir
# Before (broken): OptionParser mangled unknown switches
{opts, remaining, passthrough} = OptionParser.parse(args, switches: @switches)

# After (fixed): Pattern matching preserves all unknown args
defp extract_json_opts(["--summary-only" | rest], opts, remaining), do: ...
defp extract_json_opts([arg | rest], opts, remaining), do: ...  # passthrough
```

**Files modified:**
- `lib/mix/tasks/test_json.ex` - New `extract_json_opts/1` function
- `test/mix/tasks/test_json_test.exs` - Added tests for --only and --exclude

**Also added:**
- `ensure_test_env!/0` - Clear error message when run in wrong environment

---

## Phase 1: MVP Core Features

### Task 1: Project Structure Setup

**Completed:** 2026-01-08

**What was done:**
- Created `lib/ex_unit_json/formatter.ex` - GenServer stub with struct and typespecs
- Created `lib/ex_unit_json/json_encoder.ex` - Encoder module with function stubs and specs
- Created `lib/mix/tasks/test_json.ex` - Mix task stub with option parsing
- Updated `lib/ex_unit_json.ex` with comprehensive moduledoc
- Added 4 module existence tests

**Files created:**
- `lib/ex_unit_json/formatter.ex`
- `lib/ex_unit_json/json_encoder.ex`
- `lib/mix/tasks/test_json.ex`

**Verification:**
- `mix compile --warnings-as-errors` passes
- `mix test` passes (4 tests)

---

### Task 2: JSON Encoder - Basic Test Serialization

**Completed:** 2026-01-09

**What was done:**
- Implemented `encode_test/1` - converts `%ExUnit.Test{}` to JSON-serializable map
- Implemented `encode_state/1` - converts test state tuples to strings (nil→passed, failed, skipped, excluded, invalid)
- Implemented `encode_tags/1` - filters internal ExUnit keys and converts values to JSON-safe types
- Added `encode_tag_value/1` - handles atoms, strings, numbers, booleans, lists, maps, and non-serializable values
- Added `encoded_test` type with full field documentation
- Handles `:ex_unit_no_meaningful_value` marker
- Filters keys starting with `ex_` prefix
- 28 comprehensive unit tests covering all edge cases

**Key implementation details:**
- Struct type enforced in function signature: `def encode_test(%ExUnit.Test{} = test)`
- Pattern matching in function heads for state encoding
- Boolean guards checked before atom guards (booleans are atoms in Elixir)
- Non-serializable values (PIDs, refs) safely converted via `inspect/1`
- Nested structures (maps, lists) recursively encoded

**Files modified:**
- `lib/ex_unit_json/json_encoder.ex` - Full implementation
- `test/ex_unit_json/json_encoder_test.exs` - 28 tests

**Also in this commit:**
- Removed unused `jason` dependency (using built-in `:json`)
- Updated GitHub URL to `ZenHive/ex_unit_json`

**Verification:**
- `mix test` passes (32 tests)
- `mix dialyzer` passes (0 warnings)
- `mix doctor` passes (100% coverage)

---

### Task 3: JSON Encoder - Failure Serialization

**Completed:** 2026-01-09

**What was done:**
- Implemented `encode_failure/1` - extracts failure details from `{:failed, failures}` state
- Implemented `encode_single_failure/1` - handles `{kind, error, stacktrace}` tuples
- Implemented `encode_stacktrace/1` - converts stacktrace to JSON-serializable frames
- Added assertion error handling with `left`, `right`, and `expr` extraction
- Implemented truncation for very long assertion values (10,000 char limit)
- Added `encode_failure_kind/2` - detects assertion errors vs error/exit/throw
- Handles non-serializable values (PIDs, refs) via `inspect/2`
- 21 comprehensive tests for failure serialization

**Key implementation details:**
- Truncation limits defined as module attributes at top of file
- `@value_char_limit 10_000` for inspected values
- `@collection_item_limit 100` for collections
- `@printable_limit 4096` for printable strings
- Stacktrace frames include: module, function, arity, file, line, app
- Arity normalization handles both integer and list-of-args formats
- All private functions have `@doc false` + explanatory comments

**Files modified:**
- `lib/ex_unit_json/json_encoder.ex` - Failure/stacktrace encoding
- `test/ex_unit_json/json_encoder_test.exs` - 21 additional tests

**Verification:**
- `mix test` passes (53 tests)
- `mix dialyzer` passes (0 warnings)
- `mix format --check-formatted` passes
- `mix credo --strict` passes (staged files)

---

### Task 4: Formatter GenServer - Event Collection

**Completed:** 2026-01-09

**What was done:**
- Created `ExUnitJSON.Config` module for centralized option handling
- Implemented `ExUnitJSON.Formatter` GenServer event handlers
- Handles `{:suite_started, opts}` - captures seed from suite options
- Handles `{:test_finished, test}` - accumulates encoded test results
- Handles `{:module_finished, module}` - tracks setup_all failures
- Silently ignores unknown events (no crashes)
- Added `:get_state` call handler for testing
- 39 new tests (12 Config, 27 Formatter)

**Key implementation details:**
- Config module validates and filters option keys
- Options merged from Application env and start_link args
- Tests prepended to list for O(1) accumulation (reversed later in Task 5)
- Module failures only tracked when state is `{:failed, _}`
- Full integration test simulating complete test suite lifecycle

**Files created:**
- `lib/ex_unit_json/config.ex` - Option parsing/validation
- `test/ex_unit_json/config_test.exs` - 12 tests
- `test/ex_unit_json/formatter_test.exs` - 27 tests

**Files modified:**
- `lib/ex_unit_json/formatter.ex` - Full event handler implementation

**Verification:**
- `mix test` passes (91 tests)
- `mix dialyzer` passes (0 warnings)
- Coverage: 90% total (Formatter: 100%, Config: 100%)

---

### Task 5: Formatter GenServer - JSON Output

**Completed:** 2026-01-09

**What was done:**
- Implemented `handle_cast({:suite_finished, times_us}, state)` - outputs complete JSON document
- Added `build_document/2` - assembles root document with version, seed, summary, tests
- Added `build_summary/2` - calculates test counts and overall result
- Added `sort_tests/1` - deterministic ordering by file, line, name
- Added `filter_tests/2` - supports summary_only and failures_only options
- Handles module failures (setup_all) separately in output
- Outputs to stdout by default, or to file when configured
- 11 new tests for suite_finished functionality

**Key implementation details:**
- Uses `:json.encode/1` for JSON serialization (no external dependencies)
- Uses `IO.write/1` (not `IO.puts/1`) to avoid trailing newline in JSON
- Tests reversed from accumulation order before output
- Summary counts include: total, passed, failed, skipped, excluded, invalid
- Overall result is "failed" if any test failed or is invalid
- Tests use file output instead of capture_io (GenServer group leader isolation)

**Output structure:**
```json
{
  "version": 1,
  "seed": 12345,
  "summary": {
    "total": 10, "passed": 8, "failed": 2, "skipped": 0,
    "excluded": 0, "invalid": 0, "duration_us": 123456,
    "result": "failed"
  },
  "tests": [...],
  "module_failures": [...]
}
```

**Files modified:**
- `lib/ex_unit_json/formatter.ex` - suite_finished handler and helpers
- `test/ex_unit_json/formatter_test.exs` - 11 new tests

**Verification:**
- `mix test` passes (102 tests)
- All acceptance criteria verified
- JSON output validated against schema v1

---

### Task 6: Mix Task - Basic Implementation

**Completed:** 2026-01-09

**What was done:**
- Created `Mix.Tasks.Test.Json` module with full documentation
- Parses `--summary-only`, `--failures-only`, `--output` switches
- Passes remaining args through to `mix test` (file paths, line numbers)
- Configures ExUnit to use `ExUnitJSON.Formatter`
- Exit codes preserved from delegated test task
- Added `@shortdoc` and `@moduledoc` with examples
- 15 tests covering option parsing, module attributes, and integration

**Key implementation details:**
- Uses `OptionParser.parse!/2` with strict mode for argument parsing
- Options stored in Application env (ExUnit formatter API limitation)
- Delegates to `Mix.Task.run("test", test_args)` preserving exit codes
- `mix help test.json` shows full documentation

**Files created:**
- `lib/mix/tasks/test_json.ex` - Mix task implementation
- `test/mix/tasks/test_json_test.exs` - 15 tests

**Files modified:**
- `mix.exs` - Added `cli/0` for preferred_envs config

**Verification:**
- `mix test` passes (117 tests)
- `mix help test.json` displays documentation
- `mix test.json` produces valid JSON output
- Exit code 0 on pass, non-zero on failure

---

### Task 7: Filtering Options

**Completed:** 2026-01-09

**What was done:**
- Implemented `filter_tests/2` in formatter with summary_only and failures_only support
- `--summary-only` omits the `tests` array entirely (only summary in output)
- `--failures-only` filters tests array to include only failed tests
- Summary statistics always reflect full suite regardless of filter flags
- When both flags are used, `--summary-only` takes precedence
- Added 3 integration tests for filtering flags
- Added 2 unit tests for filtering logic in formatter

**Key implementation details:**
- Filtering handled in `build_document/2` via `filter_tests/2`
- Returns `nil` for summary_only (omits key), filtered list for failures_only
- Summary counts computed from full test list before filtering
- Options flow from Mix task → Application env → Config → Formatter

**Files modified:**
- `lib/ex_unit_json/formatter.ex` - `filter_tests/2` implementation
- `test/ex_unit_json/formatter_test.exs` - 2 unit tests for filtering
- `test/mix/tasks/test_json_test.exs` - 3 integration tests

**Verification:**
- `mix test` passes (120 tests)
- `mix test.json --summary-only` outputs summary only
- `mix test.json --failures-only` outputs only failed tests
- Both flags combined works correctly

---

### Task 8: Output File Option & Polish

**Completed:** 2026-01-09

**What was done:**
- Verified `--output FILE` option already implemented in Mix task, Config, and Formatter
- Changed option parsing from `strict` to `switches` mode to allow passthrough of mix test options
- Added invalid file path edge case test
- Added golden test suite with 11 tests for JSON schema v1 conformance
- Complete README rewrite with usage examples and full schema documentation
- All tests passing (132 tests)

**Key implementation details:**
- `File.write!/2` raises on invalid paths (no directory, permission denied)
- Unknown options now pass through to mix test (not rejected)
- Golden tests verify schema structure for all test states
- README documents complete JSON schema v1 specification

**Files modified:**
- `lib/mix/tasks/test_json.ex` - Changed to switches mode for option passthrough
- `test/mix/tasks/test_json_test.exs` - Added invalid path test, updated option parsing tests
- `test/golden_test.exs` - New golden test suite (11 tests)
- `README.md` - Complete rewrite with documentation

**Verification:**
- `mix test` passes (132 tests)
- `mix hex.build` succeeds
- README complete with schema documentation and examples