From replay
HTTP recording, playback, and stubbing for Swift tests using the Replay framework (HAR fixtures + Swift Testing traits). Use whenever the user mentions Replay, the .replay(...) trait, HAR fixtures, VCR-style testing in Swift, stubbing URLSession / AsyncHTTPClient, recording network traffic for tests, redacting secrets from fixtures, parallel test execution / scope .test / .playbackIsolated / Replay.session isolation, or troubleshooting 'No Matching Entry in Archive' / 'Replay Archive Missing' errors. Also use when writing or reviewing a Swift @Test that hits the network, when adding fixtures under Replays/, when parallelizing a Replay-using test suite, or when configuring REPLAY_RECORD_MODE / REPLAY_PLAYBACK_MODE for CI.
How this skill is triggered — by the user, by Claude, or both
Slash command
/replay:replayThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
[Replay](https://github.com/mattt/Replay) intercepts HTTP traffic in Swift Testing via HAR fixtures. This skill covers:
Replay intercepts HTTP traffic in Swift Testing via HAR fixtures. This skill covers:
.replay(...) traitURLProtocol interception can't workReplay ships as a Swift Package. It is a test-only dependency — never add it to an app target. Minimums: Swift 6.1+, macOS 10.15+ / iOS 13+.
| User situation | Go to |
|---|---|
| Adding Replay to a project for the first time | Setup |
Writing a new @Test that calls the network | Authoring tests |
| "Replay Archive Missing" on first run | Recording |
| "No Matching Entry in Archive" on subsequent runs | Matchers |
HAR file contains Authorization / cookies / PII | Redaction |
| Tests need to run in parallel | Parallel tests |
Code uses AsyncHTTPClient (external SwiftNIO-based HTTP client), not URLSession | AsyncHTTPClient |
| Managing fixtures from the command line | Tooling |
Add to Package.swift:
dependencies: [
.package(url: "https://github.com/mattt/Replay.git", from: "0.4.0")
],
targets: [
.testTarget(
name: "YourTests",
dependencies: [.product(name: "Replay", package: "Replay")],
resources: [.copy("Replays")] // ship HAR files into the test bundle
)
]
Then create Tests/YourTests/Replays/ to hold HAR files.
Add the package under File → Add Packages…, attach Replay to the test target only, then add a Replays/ group to the test target and confirm its files are members of the test bundle.
HAR file resolution in Xcode / xcodebuild / Tuist projects: Replay has built-in logic that resolves archives relative to the test source file via sourceLocation.fileID. In many Xcode projects this works out of the box — try it first before adding workarounds.
When the built-in resolution fails (e.g., Tuist-generated projects where source paths don't map cleanly, or when using Bundle(for:) which resolves into DerivedData), define a custom convenience whose rootURL is derived from #filePath — that's the source path at compile time, so both record and playback hit the source tree directly:
// Tests/YourTests/Support/ReplayTestSupport.swift
import Foundation
import Replay
import Testing
private let replaysRootURL: URL = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent() // Support/
.deletingLastPathComponent() // YourTests/
.appendingPathComponent("Replays", isDirectory: true)
extension Trait where Self == ReplayTrait {
static func replayFromSource(
_ name: String,
matchers: [Matcher] = .default,
filters: [Filter] = [],
scope: ReplayScope = .global
) -> Self {
ReplayTrait(name, matchers: matchers, filters: filters, rootURL: replaysRootURL, scope: scope)
}
}
Adjust the number of .deletingLastPathComponent() hops to match where the support file sits relative to your Replays/ directory. #filePath is evaluated at compile time, so the absolute path is whichever machine compiled the binary — CI compiles its own tests, so this is fine; the failure mode is only "cross-machine binary copy", which isn't a real flow.
Replay's URLProtocol interception and Swift Testing traits are only useful in tests, and shipping it in a release binary would pull in unused record/capture code. Keep it scoped.
Preferred shape — inject URLSession into your client so tests can swap it for Replay.session when needed. Accepting a session also unlocks parallel test execution later.
import Foundation
import Testing
import Replay
@Suite(.playbackIsolated(replaysFrom: Bundle.module))
struct UserAPITests {
@Test(.replay("fetchUser"))
func fetchUser() async throws {
let user = try await APIClient.shared.fetchUser(id: 42)
#expect(user.id == 42)
}
}
What this does:
@Suite(.playbackIsolated(replaysFrom: Bundle.module)) tells Replay to resolve archives from the test bundle's Replays/ resources. For Xcode projects without SPM modules, use the Bundle(for:) form shown in the README..replay("fetchUser") loads Replays/fetchUser.har and intercepts any HTTP traffic during the test..replay()) derives it from the test name — convenient but less greppable. Prefer explicit names.Each HAR file can hold many request/response entries. If a test makes three requests, record them all into one file — do not stack multiple .replay(...) traits. Stacking is invalid and the framework will reject it.
strict: a missing archive or an unmatched request fails the test.none: recording is explicit, not accidental. This is intentional — it prevents a passing CI run from silently rewriting fixtures.The first run of a new test fails with "Replay Archive Missing". That is the signal to record:
REPLAY_RECORD_MODE=once swift test --filter YourSuite.fetchUser
xcodebuild / Xcode users: do not rely on plain REPLAY_RECORD_MODE=… xcodebuild test.
Apple documents TEST_RUNNER_<VAR> in man xcodebuild: xcodebuild passes those variables to every test runner process with the prefix stripped. This is standard xcodebuild behavior (not Replay-specific), so prefix Replay's env vars with TEST_RUNNER_:
TEST_RUNNER_REPLAY_RECORD_MODE=once xcodebuild test \
-workspace … -scheme … -destination … \
-only-testing:MyTests/MySuite/myTest
The test process receives REPLAY_RECORD_MODE=once and Replay reads it normally. The same applies to TEST_RUNNER_REPLAY_PLAYBACK_MODE. swift test passes env vars directly and doesn't need the prefix. Avoid build-for-testing / .xctestrun editing just to pass Replay env vars; use those only when the project already needs a separate build/test split for other reasons.
Tuist / mise wrappers: if a repo runs tests through Tuist or a task runner that ultimately invokes xcodebuild test, keep that guidance separate from plain swift test and raw xcodebuild examples. Prefer making the wrapper translate the developer-facing variables before invoking xcodebuild:
if [[ -n "${REPLAY_RECORD_MODE:-}" && -z "${TEST_RUNNER_REPLAY_RECORD_MODE:-}" ]]; then
export TEST_RUNNER_REPLAY_RECORD_MODE="$REPLAY_RECORD_MODE"
fi
if [[ -n "${REPLAY_PLAYBACK_MODE:-}" && -z "${TEST_RUNNER_REPLAY_PLAYBACK_MODE:-}" ]]; then
export TEST_RUNNER_REPLAY_PLAYBACK_MODE="$REPLAY_PLAYBACK_MODE"
fi
That keeps commands ergonomic while still using Apple's documented xcodebuild mechanism:
REPLAY_RECORD_MODE=once mise run test MyScheme
REPLAY_RECORD_MODE=once tuist test MyScheme
Modes:
REPLAY_RECORD_MODE | Behavior |
|---|---|
none (default) | Never record. CI should always use this. |
once | Record only if the archive is missing. Safe default for dev. |
rewrite | Overwrite the archive from scratch. Use when the API changed. |
REPLAY_PLAYBACK_MODE | Behavior |
|---|---|
strict (default) | Fixtures required; unmatched requests fail. |
passthrough | Use fixtures when available, otherwise hit the real network. |
live | Ignore fixtures; always hit the real network. |
CI rule of thumb: never set REPLAY_RECORD_MODE in CI. Recording is a developer action. If CI records, the fixture becomes whatever the API returned that day — and secrets may land in the archive.
Preferred workflow:
REPLAY_RECORD_MODE=once → fixture is created.swift package replay inspect …), redact, commit.strict mode.See references/redaction.md before committing — HAR files very often carry Authorization, Cookie, or response bodies with PII.
By default, Replay matches requests on HTTP method + full URL string. That's strict — any volatile query param (timestamp, cursor, cache-buster) causes a miss. When you see "No Matching Entry in Archive", relaxing the matcher is usually the right call:
@Test(.replay("fetchUser", matching: [.method, .path]))
Full matcher list and guidance in references/matchers.md. Matchers compose with AND — all must match.
Prefer redacting at record time via filters: so secrets never touch disk:
@Test(
.replay(
"fetchUser",
matching: [.method, .path],
filters: [
.headers(removing: ["Authorization", "Cookie", "Set-Cookie"]),
.queryParameters(removing: ["token", "api_key"]),
]
)
)
For body-level redaction and after-the-fact scrubbing via the swift package replay filter plugin, see references/redaction.md.
For trivial cases — a single predictable response, or error paths you can't easily reproduce against the real API — use inline stubs instead of a HAR file:
@Test(
.replay(stubs: [
.get("https://example.com/greeting", 200,
["Content-Type": "text/plain"], { "Hello, world!" })
])
)
func greeting() async throws { /* ... */ }
HAR files are better when the response is realistic or large; stubs are better when the response is trivial or must be crafted (e.g., 500s, malformed payloads).
scope: .test)By default Replay uses global URLProtocol registration with a lock — tests run serialized. To parallelize, opt into per-test isolation:
@Suite(.playbackIsolated(replaysFrom: Bundle.module))
struct ParallelTests {
@Test(.replay("fetchUser", matching: [.method, .path], scope: .test))
func fetchUser() async throws {
// MUST use Replay.session, not URLSession.shared
let client = APIClient(session: Replay.session)
_ = try await client.fetchUser(id: 42)
}
}
Why the session swap matters: per-test scope routes via a custom HTTP header, and only Replay.session (or Replay.makeSession()) attaches it. URLSession.shared silently falls back to the global store and you'll get cross-test bleed.
XCTest or manual control is supported via lower-level APIs — Playback.session(configuration:), Capture.session(configuration:), HAR.load(from:) / HAR.save(_:to:). See the framework README for full signatures. The trait-based API covers 95% of cases; reach for these only when Swift Testing isn't available.
.url, custom matchersAsyncHTTPClient SwiftPM package traitswift package replay subcommands (status / record / inspect / validate / filter).replay(...) traits on one test. One archive per test; record multiple entries into it.REPLAY_RECORD_MODE in CI. Recording is a local dev action.URLSession.shared with scope: .test. Use Replay.session..custom(...) matchers before trying .method + .path + .query. Built-in matchers cover almost everything.Provides a checklist for code reviews covering functionality, security, performance, maintainability, tests, and quality. Use for pull requests, audits, team standards, and developer training.
npx claudepluginhub kikeenrique/laia-skills --plugin ios-simulator-ui-flow