Skip to content

Design spec and implementation plan for migrating off xcresulttool --legacy (#391) - #426

Closed
tylervick wants to merge 6 commits into
mainfrom
tylervick/legacy-migration-391
Closed

Design spec and implementation plan for migrating off xcresulttool --legacy (#391)#426
tylervick wants to merge 6 commits into
mainfrom
tylervick/legacy-migration-391

Conversation

@tylervick

@tylervick tylervick commented Aug 11, 2026

Copy link
Copy Markdown
Member

Planning only for #391 — no implementation. The strategy choice is not cheaply reversible, so this lands the decision and its evidence for review before any code is written.

Closes nothing; #391 stays open for the implementation.

Decision

Read the new xcresulttool format directly and drop XCResultKit, behind a reader abstraction that keeps the legacy path alive until Apple removes it.

Under all three options in the issue we have to write a new-format reader — XCResultKit's public API is the legacy object graph, so there is no version of this where we swap a flag. Once that reader exists the 3,549-line dependency is dead weight, which makes the direct read strictly less work than vendoring or upstreaming for the same outcome.

The finding that reframed it

The new format is not a superset of the legacy one. Measured on freshly generated fixtures under Xcode 26.2, not inferred:

Legacy New format
activity activityType (5 constants → CSS classes) absent; only isAssociatedWithFailure
activity start and finish startTime only
attachment name (user-supplied) holds the legacy filename instead
attachment uniformTypeIdentifier absent
ActionTestFailureSummary file/line/issueType one pre-joined string
log emittedOutput absent
exportRecursiveJson() (drives --json) no equivalent
repetitions first-class nodeType: "Repetition" — improved

So byte-identical output across the two backends is not achievable. The bar is a declared, CI-asserted diff, not an empty one.

Two measurements that shaped the plan

Reports are not byte-reproducible. Rendering SanityResults twice with today's binary gives 18 differing lines, all synthesized UUIDs — byte-identical after one regex. Any diff harness must normalize first; without that step it reports a difference every run and proves nothing. The plan builds the normalizer as task 1 and pins it with a test that first asserts the raw renders differ, so the normalized comparison can't pass vacuously.

Performance is a non-issue, and I'm claiming nothing. Same-runner interleaved A/B, 5 reps alternating per rep: legacy 27 spawns / 0.74s median, modern 20 spawns / 0.76s median. Equal within noise on one machine and one bundle size. Measured only to rule out the modern path being dramatically worse because activities is per-test-id. Not a speedup, and not comparable to numbers from another runner.

The parity trap worth reviewing closely

A Test Case node in the new format carries its own result, which is not the legacy status:

RetryTests/testRetryOnFailure()
  legacy:  iteration 1 = Failure, iteration 2 = Success  → .mixed
  modern:  Test Case result = "Passed", Repetition 1 = Failed, Repetition 2 = Passed

Reading the parent's result turns a mixed test green. The plan derives multi-repetition status from the children and ignores the parent, and RetryResults covers it.

Second trap: FaultCollector turns degradation into exit 3, so if the modern backend's structural nils were recorded as faults, every run would exit non-zero. Called out in the spec and asserted in task 11.

Decisions taken (yours, recorded here)

  • Dual-path with runtime selection, rather than a clean cutover
  • Modern renders the natural flat tree; synthesizing the Selected tests / *.xctest wrappers was rejected because one of those two labels would always be fabricated
  • --json moves to our own documented schema, identically on both backends — a breaking change landing in 3.0

What's here

  • docs/superpowers/specs/2026-08-10-xcresulttool-legacy-migration-design.md — decision, evidence, field-by-field mapping, parity rules, verification design, risks
  • docs/superpowers/plans/2026-08-10-xcresulttool-legacy-migration.md — 15 tasks, each ending in a green test run and a commit

Removing XCResultKit is deliberately not in scope: while the legacy commands exist, the differential is the only continuous evidence the modern reader is right, and deleting it early throws that away. Tracked as a follow-up gated on Apple actually removing them.

Verification

Docs-only diff. Baseline confirmed green on this branch before writing: swift test → 23 tests, 1 skipped, 0 failures. Every command, JSON shape, and number quoted above was executed against real fixtures in this worktree.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Documentation
    • Added a comprehensive migration plan for moving away from legacy xcresulttool processing.
    • Documented support for legacy and modern result readers, backend selection, normalized result data, attachment and log export, and JSON reporting.
    • Added design guidance covering compatibility, known format differences, testing strategy, CI coverage, migration phases, and release documentation.

tylervick and others added 2 commits August 10, 2026 22:29
Records the strategy decision and the evidence behind it: read the new
xcresulttool format directly and drop XCResultKit, behind a reader
abstraction that keeps the legacy path alive until Apple removes it.

The central finding is that the new format is not a superset of the
legacy one. Activity types and finish times, user-supplied attachment
names, attachment UTIs, structured failure locations, and log
emittedOutput all have no new-format equivalent, and exportRecursiveJson
has no replacement at all. Byte-identical output across the two backends
is therefore not achievable, so the bar is a declared and CI-asserted
diff rather than an empty one.

Every measurement in the spec was taken on freshly generated fixtures
under Xcode 26.2 rather than inferred, including two that shape the
plan: reports are not byte-reproducible (18 differing lines on the
smallest fixture, all synthetic UUIDs, identical after one regex), and
the two read paths are equal within noise on a same-runner interleaved
A/B, so no performance claim is made in either direction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fifteen tasks, each ending in a green test run and a commit, following the
spec's phasing: extract a backend-neutral port, add the modern reader, prove
parity differentially, then flip selection on.

Two ordering decisions carry most of the risk reduction. The UUID normalizer
and a pre-refactor baseline capture land before the large renderer refactor,
so "behaviour-preserving" is something the implementer diffs rather than
asserts. And the differential harness lands with the migration rather than
after it, because it only works while xcresulttool still supports both
formats.

Fixtures are regenerated on every CI run, so checked-in golden HTML is
impossible; every comparison in the plan is between two renders produced
within one run. Both assertions that could pass on empty input are paired
with an explicit non-vacuity guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@tylervick, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 34 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e4e05ad9-a871-4847-820e-da733a71d35b

📥 Commits

Reviewing files that changed from the base of the PR and between 6398511 and d6da635.

📒 Files selected for processing (2)
  • docs/superpowers/plans/2026-08-10-xcresulttool-legacy-migration.md
  • docs/superpowers/specs/2026-08-10-xcresulttool-legacy-migration-design.md
📝 Walkthrough

Walkthrough

The PR adds a migration plan and design specification for replacing xcresulttool --legacy with backend-neutral parsed results, legacy and modern readers, payload handling, backend selection, parity testing, CI coverage, and a new --json contract.

Changes

XCResultTool migration

Layer / File(s) Summary
Backend-neutral contracts and architecture
docs/superpowers/plans/..., docs/superpowers/specs/...
Defines ParsedResult types, reader and payload-provider protocols, format differences, fault rules, and implementation phases.
Legacy and modern reader paths
docs/superpowers/plans/..., docs/superpowers/specs/...
Specifies legacy translation, renderer migration, modern xcresulttool execution, schema decoding, status mapping, and result-tree construction.
Payload, backend, and JSON integration
docs/superpowers/plans/..., docs/superpowers/specs/...
Specifies attachment and log export, attachment type mapping, backend selection, CLI wiring, fallback behavior, and backend-independent JSON output.
Parity, CI, and release verification
docs/superpowers/plans/..., docs/superpowers/specs/...
Adds reproducibility tests, differential parity checks, CI coverage, documentation tasks, final verification, and deferred XCResultKit removal.

Estimated code review effort: 2 (Simple) | ~15 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the design specification and implementation plan added for migrating away from the legacy xcresulttool dependency.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tylervick/legacy-migration-391

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-08-10-xcresulttool-legacy-migration.md`:
- Around line 1749-1755: Update exportPayloadData to handle Data(contentsOf:)
failures explicitly instead of using try?, recording .payloadExportFailed with
the attachment reference and read error before returning nil. Preserve the
existing sourceURL failure handling and successful Data return.
- Around line 1797-1826: The attachment export flow around exportDirectory must
remove the UUID-named temporary directory after all payload reads finish. Track
the directory lifecycle so cleanup occurs only after the payload store no longer
needs the exported files, while retaining the current export and
manifest-processing behavior.
- Around line 823-830: Update the expected verification output in Step 4 to
account for tests added by Tasks 1, 2, and 4, using the cumulative test count;
alternatively, remove the fixed test count and state that the build and test
commands must complete successfully with zero failures and the expected
skipped-test behavior.
- Around line 420-424: Add pipefail handling to every shell block containing
piped verification commands such as swift build or test, including the
referenced locations, so the block returns a failure when the build or test
command fails rather than only reflecting tail’s status. Preserve the existing
output-trimming behavior.
- Around line 2344-2370: The testAllowListEntriesStillDescribeRealDifferences
test must compare normalized legacy and modern HTML across every fixture,
asserting that all differences are covered by the allow-list and no
unallowlisted changes remain. Add validation for entries with empty markers so
they cannot bypass coverage, while retaining the existing marker-presence checks
as stale-entry guards.
- Around line 2373-2397: Update
testAttachmentPayloadsAreByteIdenticalAcrossBackends so it compares attachment
records keyed by attachment identity and filename rather than Set<Data>.
Preserve duplicate attachments and their association with each file, and assert
that each record’s bytes match between legacy and modern summaries.
- Around line 1167-1170: Update TestResultsTests and the related test-results
models around testRuns so omitted collection keys decode as empty collections
rather than causing keyNotFound errors. Use optional or defaulted collection
properties while preserving the existing collection behavior when keys are
present, and add fixtures covering absent testNodes and testRuns keys.
- Around line 232-247: Update the baseline-capture loop around the resource list
to fail when any of TestResults, SanityResults, or RetryResults is missing
instead of continuing. After writing each normalized report, verify the
corresponding output file exists and is non-empty, and ensure exactly all three
baseline files are captured before the test succeeds.
- Around line 1728-1738: The exportPayload function must prevent attachment
names from escaping bundleURL. Reduce fileName to its basename, build and
standardize the destination, verify it remains within the standardized bundle
directory before any removal or copy, and record .payloadExportFailed with a nil
return when validation fails; add a test covering traversal and path-separator
input.
- Around line 401-404: Update the ResultReader.read contract to throws ->
ParsedResult and propagate genuine schema, subprocess, and activity-query
failures instead of converting them to nil or empty results. Ensure
modern-reader call sites record read errors through FaultCollector and only use
empty values for unsupported backend fields; apply the same behavior to the
referenced reader implementations and CLI flow.
- Around line 2097-2125: Update Summary.init’s backend setup and legacy-read
flow to catch failures from the actual LegacyResultReader operation. Preserve
explicit .legacy behavior by rethrowing its errors, but when the original
backend is .auto, construct the modern ModernPayloadStore/ModernResultReader
path and retry using it; do not rely solely on ResultBackend.resolved(), which
only checks availability.
- Around line 2132-2147: Thread SummaryOptions.resultReader through the
XCTestHtmlReport Summary construction so the selected backend is not ignored.
Resolve the backend with precedence explicit CLI option, then
XCHR_RESULT_READER, then auto, and pass that resolved value to Summary without
allowing the environment to override an explicit option. Add tests covering both
explicit-over-environment and environment-over-default precedence.
- Around line 1411-1433: Update ModernResultReader.read() to create a ParsedRun
for every destination in tests.devices rather than selecting
tests.devices?.first, preserving each destination’s associated testables. Before
any legacy/modern run comparison using zip, assert that legacy.runs.count equals
modern.runs.count so unmatched runs cannot be silently ignored.
- Around line 968-1002: Update run(_:) and legacyCommandsAvailable to execute
xcresulttool through a bounded Swift 5.5/macOS 10.15-compatible helper that
enforces a timeout, terminates timed-out processes, closes both stdout and
stderr pipe handles, and always reaps the process. Preserve normal output and
executionFailed behavior, but map an availability-probe timeout in
legacyCommandsAvailable to an unavailable backend. Add tests covering subprocess
timeout handling and the availability mapping.
- Around line 1172-1184: Update parseGroup and parseTestCase to preserve
TestNode entries whose nodeType is “Failure Message,” including their
details/text in ParsedActivity so RetryResults retains failure diagnostics;
alternatively, add a captured-response assertion proving activities exposes
equivalent message text.

In `@docs/superpowers/specs/2026-08-10-xcresulttool-legacy-migration-design.md`:
- Line 66: Update the four fenced code blocks in the document, including the
blocks near the referenced sections, to use the text language identifier
(```text) instead of unlabeled fences. Preserve their existing contents.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e63391c0-9311-4111-8cbe-f19d69052803

📥 Commits

Reviewing files that changed from the base of the PR and between 822129f and 3623e9e.

📒 Files selected for processing (2)
  • docs/superpowers/plans/2026-08-10-xcresulttool-legacy-migration.md
  • docs/superpowers/specs/2026-08-10-xcresulttool-legacy-migration-design.md

Comment thread docs/superpowers/plans/2026-08-10-xcresulttool-legacy-migration.md Outdated
Comment thread docs/superpowers/plans/2026-08-10-xcresulttool-legacy-migration.md
Comment thread docs/superpowers/plans/2026-08-10-xcresulttool-legacy-migration.md
Comment thread docs/superpowers/plans/2026-08-10-xcresulttool-legacy-migration.md
Comment thread docs/superpowers/plans/2026-08-10-xcresulttool-legacy-migration.md
Comment thread docs/superpowers/plans/2026-08-10-xcresulttool-legacy-migration.md
Comment thread docs/superpowers/plans/2026-08-10-xcresulttool-legacy-migration.md
Comment thread docs/superpowers/plans/2026-08-10-xcresulttool-legacy-migration.md Outdated
Comment thread docs/superpowers/plans/2026-08-10-xcresulttool-legacy-migration.md
Comment thread docs/superpowers/specs/2026-08-10-xcresulttool-legacy-migration-design.md Outdated
tylervick and others added 2 commits August 10, 2026 22:51
The differential test was the weak point. It only checked that declared
markers appeared on legacy and vanished on modern, which says nothing about
lines nobody declared — an undeclared regression would have passed. Checking
the marker strings against HTMLTemplates.swift also showed three of them did
not exist: activity durations render as a bare "(0.00s)" suffix and attachment
names as bare text, with no class to key on.

Replaced with masking: strip exactly the declared losses from both renders and
require what remains to be byte-identical. That is implementable against the
real markup and is a stronger claim than the marker match ever was. Each rule
maps 1:1 to an allow-list entry, and the masked comparison asserts every test
title survives masking so an over-broad mask cannot make it pass vacuously.

Baseline capture no longer skips missing fixtures. It previously continued past
one, which would let Task 5 diff two partial directories and report them
identical — the same vacuous-verification shape.

Also from review: the attachment export leaked its temp directory (a full copy
of every screen recording) on every run; attachment comparison used Set<Data>,
which collapses duplicates; testNodes and testRuns were non-optional and would
throw keyNotFound on a bundle where xcresulttool omits the key; a failed
activities query returned an empty list with no fault, so a visibly gutted
report would exit 0; and Task 5 quoted a stale expected test count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three more from review. The --result-reader option was declared on
SummaryOptions but never passed into Summary, so it would have parsed,
validated, and done nothing; the end-to-end check now renders through both
readers and requires the wrapper-group counts to differ, which fails if the
flag is not reaching Summary.

ResultBackend.resolved() only consulted the version string, so an explicit
--result-reader legacy on a post-removal toolchain would have selected a
backend that cannot work. It now demotes to modern with a warning, which is
the degradation rule the spec already stated. The accompanying test is
conditional on toolchain capability rather than asserting .legacy -> .legacy
outright, since that assertion would start failing exactly when the fallback
becomes load-bearing.

The spec listed read() as throwing while the plan returned an optional. Settled
on the optional, matching the existing getInvocationRecord() contract that
Summary.init already guards with a fault, and documented why the sub-level
failures are the ones that needed a new fault kind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tylervick

Copy link
Copy Markdown
Member Author

Worked through CodeRabbit's 16 comments. Addressed 13, skipped 2 with reasons, 1 was a duplicate.

The one that mattered most: the differential test didn't do what it claimed.

It only checked that declared markers appeared on legacy and vanished on modern. That says nothing about lines nobody declared, so an undeclared regression would have passed — the exact vacuous-verification shape this repo has been bitten by before, in the central check of the whole migration.

Checking the marker strings against HTMLTemplates.swift also showed three of the five were fabricated: activity durations render as a bare (0.00s) suffix in [[TITLE]] ([[TIME]]), and attachment names as bare text inside <p class="attachment list-item">. Neither carries a class to key on, so per-line marker matching could never have worked for them.

Replaced with masking: strip exactly the declared losses from both renders, then require what remains to be byte-identical. That is implementable against the real markup and is a strictly stronger claim than the marker match. Each rule maps 1:1 to an allow-list entry, an entry naming a rule with no implementation fails testEveryAllowListRuleIsImplemented, and the comparison asserts every test title survives masking so an over-broad mask can't make it pass on anything.

Also fixed

  • Baseline capture continued past a missing fixture, which would let Task 5 diff -r two partial directories and call them identical — same vacuous shape.
  • --result-reader was declared but never passed into Summary. Dead flag. The end-to-end check now renders through both readers and requires the wrapper-group counts to differ, which fails if the flag isn't landing.
  • ResultBackend.resolved() didn't implement the demotion the spec promised; an explicit --result-reader legacy on a post-removal toolchain would have picked a backend that cannot work.
  • ModernPayloadStore leaked its temp export directory — a full copy of every attachment, screen recordings included — on every run.
  • Attachment comparison used Set<Data>, which collapses duplicates; a backend dropping one of two identical recordings would still compare equal. Now keyed by filename with counts.
  • testNodes/testRuns were non-optional and would throw keyNotFound where xcresulttool omits the key.
  • A failed activities query returned [] with no fault, so a gutted report would exit 0. Added .missingActivities.
  • Inline payload read failures returned a silent nil.
  • Task 5 quoted a stale expected test count (23) after earlier tasks add tests.
  • pipefail noted as a global constraint rather than patched into ~20 blocks.
  • Labelled the bare code fences in the spec.

Skipped

  • Path traversal on attachment filenames. Real, but pre-existing: legacy ResultFile.exportPayload already does url.appendingPathComponent(resolvedName) with the same untrusted input. Not introduced by this migration, and hardening it should cover both backends. Worth its own issue rather than smuggling a security fix into a format migration.
  • read() should throw rather than return an optional. Kept the optional: it mirrors the existing getInvocationRecord() contract that Summary.init already guards with .missingInvocationRecord, so a nil read is reported, not swallowed. The genuinely silent path was the activities query, and that now records a fault. Spec updated to match, since it had said throws.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/specs/2026-08-10-xcresulttool-legacy-migration-design.md`:
- Around line 138-145: Update the design around ModernResultReader and
Summary.init to explicitly require recording .missingActivities on the passed
FaultCollector when the activities query fails before returning an empty list.
Add an integration test verifying the fault appears in summary.faults and causes
the expected CLI degradation, and describe Summary.validate() only as checking
unresolved attachments, not producing this fault.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0b67ee49-7042-499f-b35a-2c26530f73ce

📥 Commits

Reviewing files that changed from the base of the PR and between 3623e9e and 6398511.

📒 Files selected for processing (2)
  • docs/superpowers/plans/2026-08-10-xcresulttool-legacy-migration.md
  • docs/superpowers/specs/2026-08-10-xcresulttool-legacy-migration-design.md

tylervick and others added 2 commits August 10, 2026 22:58
The plan recorded .missingActivities when an activities query fails, but
nothing asserted the fault reaches summary.faults. Since a failed query
degrades to an empty activity list rather than aborting, unproven plumbing
means the CLI could exit 0 on a report whose tests have no activities — the
exact outcome the fault exists to prevent.

Added an XCResultToolInvoking seam so a client that fails only on `activities`
can be injected, and a test that reads through it and asserts both halves: the
read still succeeds, and the fault lands on the caller's collector. The
collector is the one Summary.init owns, so the existing exit-3 path covers the
rest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three more from review, two of them substantive.

The modern reader was sourcing failure text from the activities document. The
tests tree's Failure Message nodes are strictly better: measured on
TestResults, they give "FirstSuite.swift:66: XCTAssertTrue failed - Test
failed" where the activity title gives only "XCTAssertTrue failed - Test
failed". The plan was reading from the lossier of two available sources and
the spec described that loss as unavoidable. Both corrected; skip reasons ride
the same node.

The modern reader also collapsed every destination to devices.first, where
legacy emits one run per ActionRecord. Every fixture boots a single simulator,
so no test would have caught the difference — and the differential's zip()
truncates to the shorter sequence, so it would have compared the runs that did
exist and passed. Now one run per device, with an explicit run-count assertion
before the zip, and the coverage gap stated in both documents rather than
implied to be tested.

Also: the legacy-capability probe never drained stderr, which deadlocks if the
pipe fills — the same bug the codebase already documents in TestSupport and
XCResultToolClient.run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tylervick

Copy link
Copy Markdown
Member Author

Second review round: 3 further findings, all addressed. Replied inline on all 17 threads.

Failure text was coming from the lossier of two sources. The reader sourced failures from activities; the tests tree's Failure Message nodes are strictly better. Measured on TestResults:

Source FirstSuite/testTwo() failure text
tests, Failure Message node FirstSuite.swift:66: XCTAssertTrue failed - Test failed
activities, activity title XCTAssertTrue failed - Test failed

The activities path drops file and line entirely — and the spec had described that as an unavoidable loss. It isn't. Now reads Failure Message nodes; skip reasons ride the same node. Both documents corrected.

Multi-destination collapsed to one run, and the differential would have hidden it. The reader used devices.first where legacy emits one run per ActionRecord. Compounding it, the differential compared runs with zip, which truncates to the shorter sequence — a backend producing fewer runs would have compared equal on the ones it did produce and passed. Now one run per device, with a count assertion before the zip.

Being explicit about the limit: every fixture boots a single simulator, so this path is not covered by the suite. The mapping is written from the format, not verified against a two-destination bundle. Both documents say so rather than implying otherwise.

The capability probe could deadlock. legacyCommandsAvailable set standardError to a Pipe() and never drained it — the same bug the codebase already documents in TestSupport.xchtmlreportCmd. Now drains on a background queue.

Declined, with reasoning (also inline):

  • Subprocess timeouts. Neither backend bounds xcresulttool today and XCResultKit doesn't either, so this is a behaviour change rather than a regression fix — and a wrong timeout turns a slow report on a large bundle into a failed one. Recorded under Scope as deliberately out.
  • Attachment path traversal. Pre-existing: legacy ResultFile.exportPayload has the identical hole with the identical input. Fixing only the modern path would leave legacy exposed while looking fixed. Deserves its own issue covering both.

Across both rounds the pattern was consistent, and worth naming: the findings that mattered weren't bugs in the design, they were verification steps that would have passed on wrong input — a differential that didn't diff, a baseline that tolerated missing fixtures, a flag whose test passed without the flag working, a zip that hid a count mismatch, a fault that was recorded but never proven to arrive.

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.

1 participant