Skip to content

Conversation

@dcrockwell
Copy link
Contributor

Summary

This PR improves the HTTP client's recording and playback system by switching from a single-file strategy to individual files per recording. This change eliminates performance bottlenecks, enables concurrent test recording, and provides better organization for test fixtures.

Why This Change Was Made

The previous single-file recording strategy (recordings.json) had several critical issues:

  • O(n) performance penalty: Every new recording required reading all existing recordings, modifying the list, and writing everything back - this got progressively slower as test suites grew
  • Concurrent test failures: Multiple tests writing to the same file caused race conditions and flaky test behavior
  • Poor developer experience: Large test suites ended up with hundreds of recordings in one unwieldy file that was impossible to inspect, diff, or debug effectively
  • No granular control: Couldn't commit specific fixtures, diff individual recordings, or delete obsolete ones without manually editing the JSON

What Changed

Core Implementation

  • Per-file recording strategy: Each recording is now saved as {method}_{host}_{path}_{hash}.json
  • Human-readable filenames: Method, host, and path are visible in the filename for easy identification
  • Collision-free with hashing: SHA256 hash ensures uniqueness even for requests with different query params, headers, or body
  • O(1) write performance: No more read-modify-write cycles - just write one file
  • Concurrent-safe: No file contention between parallel tests

Test Improvements

  • Added 8 comprehensive tests covering filename uniqueness, sanitization, overwrite behavior, and more
  • All recordings now saved to test/fixtures/recordings/ and committed to git
  • Tests can run in Playback mode without the mock server (using committed fixtures)
  • Fixed all tests to use localhost:9876 instead of api.example.com
  • Eliminated nested cases throughout test suite

API Changes

User-facing recorder API unchanged - this is backward compatible. Internal storage module functions now require a matching_config parameter, but most users don't call these directly.

Test Plan

  • All 106 tests passing
  • Pre-commit checks passing (formatting, builds, no warnings)
  • Fixtures committed and verified loadable
  • Playback mode works without mock server
  • Concurrent test scenarios verified
  • Filename uniqueness verified (query params, headers, body differences)
  • Long paths handle truncation safely
  • Special characters sanitized correctly

Version

Bumped to 3.0.1 (non-breaking enhancement)

## Why This Change Was Made
- Single-file recording strategy (recordings.json) had O(n) performance - every new recording required reading ALL existing recordings, modifying the list, and writing everything back
- Concurrent tests had file contention - multiple tests writing to the same file caused race conditions and test flakiness
- Large test suites accumulated hundreds of recordings in one file, making it impossible to inspect, debug, or version control individual recordings
- Users wanted granular control - commit specific fixtures, diff individual recordings, delete obsolete ones

## What Was Changed
- storage.gleam: Rewrote to use individual files with format {method}_{host}_{path}_{hash}.json
- Added build_filename() using SHA256 hash for uniqueness while keeping human-readable base
- Added sanitize_for_filename() to handle slashes, special chars, and path truncation (50 char limit)
- load_recordings() scans directory for all .json files instead of reading single file
- save_recording_immediately() writes one file directly - O(1) instead of O(n)
- Added gleam_crypto dependency for SHA256 hashing
- Updated storage API to require matching_config parameter (internal change, recorder API unchanged)
- Added 8 comprehensive tests: filename uniqueness, sanitization, overwrite behavior, fixture-based playback, etc.
- All tests now write to test/fixtures/recordings/ (committed to git for CI without mock server)
- Fixed all tests to use localhost:9876 instead of api.example.com
- Eliminated all nested cases from test suite
- Bumped version to 3.0.1

## Note to Future Engineer
- The hash is generated from the FULL matching signature, not just visible filename parts - this means GET /text and GET /text?page=2 have different hashes even though base filename looks identical
- test/fixtures/recordings/ is COMMITTED to git - this is intentional so CI can run tests in Playback mode without spinning up the mock server
- If a recording isn't loading, check the matching config - the hash includes headers/body if match_headers/match_body are enabled
- The matching_config parameter exists because filename uniqueness depends on what parts of the request we're matching on
- Yes, accumulating fixture files in git seems weird, but it's actually the entire point - proves the recording workflow works and gives you inspectable, version-controlled test data. Embrace it or go write flaky integration tests that depend on external APIs like a savage.
@dcrockwell dcrockwell merged commit 9566e51 into develop Dec 10, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants