CHANGELOG.md

# Changelog

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

---

## 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