Describe here the purpose of your PR.
tuist hash selective-testing was loading the graph through GeneratorFactory.defaultGenerator, while tuist test --build-only loads it through the testing generator, which inserts an EE-specific mapper chain (ForeignBuildGraphMapper, AutogeneratedWorkspaceSchemeGraphMapper(forceWorkspaceSchemes: true), ModuleMapMapper, ExternalProjectsPlatformNarrowerGraphMapper, StaticXCFrameworkModuleMapGraphMapper, …) before TestsCacheGraphMapper computes hashes at step 10 of the pipeline.
Targets whose dependency closure touches those mappers (anything depending on a module-map’d target, a static xcframework, or platform-narrowed external projects, transitively) ended up with different hashes in the two pipelines. So the hashes the command printed did not match the hashes the selective-testing cache is keyed on — silently misleading users debugging a low cache-hit rate. Pure-Swift targets without those dependencies happened to agree across both pipelines, which made the divergence even harder to spot.
Real-world reproduction
For a large iOS workspace exercising the split --build-only / --without-building flow, tuist hash selective-testing and the build phase’s cache lookup disagreed for ~all targets in the active test scheme on the same commit and same Tuist version. A representative test target hashed to (for example) 24a9d1dc8136d715b197d51cf5db854b via the command and 9d693b5f96750c4a3afd06f43cb12245 via the build-phase TestsCacheGraphMapper — i.e. the value the cache was actually keyed on. Pure-Swift utility test targets without complex external dependencies hashed identically in both paths; only targets whose dependency closure touched the EE-specific mappers diverged.
The fix
Routes HashSelectiveTestingCommandService through the same testing generator as TestService, with ignoreBinaryCache: true and ignoreSelectiveTesting: true, and reads hashes from MapperEnvironment.targetTestHashes that TestsCacheGraphMapper populates unconditionally (it stores hashes before the ignoreSelectiveTesting early-return). The command now emits exactly the hashes the build phase queries the cache with.
Also adds a loadWithEnvironment hook on Generating so callers can observe the mapper environment without triggering project generation — generateWithGraph was the only existing way to access the environment, and it also writes the project to disk.
Tests
The TDD verification is layered across three existing suites; each one would fail in isolation if the corresponding contract regresses.
Wiring — `TuistKitTests/HashSelectiveTestingCommandServiceTests`:
- `run_usesTheTestingGenerator_notTheDefaultOne` — pins that the command invokes `generatorFactory.testing(…)` with `ignoreBinaryCache: true` and `ignoreSelectiveTesting: true`. Pre-fix it called `defaultGenerator(…)`, so the testing stub was never invoked and this assertion would fail.
- `run_outputsTheHashes_fromMapperEnvironment` — stubs the new `loadWithEnvironment` hook and verifies the command surfaces `MapperEnvironment.targetTestHashes` in its output. Pre-fix the command ran a separate `SelectiveTestingGraphHasher` and never read this field, so the values stubbed here would not appear.
- `run_outputsAWarning_when_noHashes` — preserves the empty-environment warning.
Pre-hash mapper behaviour — `TuistDependenciesTests/Mappers`: `ExternalProjectsPlatformNarrowerGraphMapperTests` and `PruneOrphanExternalTargetsGraphMapperTests` already pin the input-side transforms that change content-hash inputs in the testing pipeline (e.g. narrowing `target.destinations`, which `TargetContentHasher` folds into the target hash at cli/Sources/TuistHasher/TargetContentHasher.swift:271).
Downstream contract — `TuistCacheEETests/TestsCacheMapperTests/test_when_ignore_selective_testing`: pins that `TestsCacheGraphMapper` populates `targetTestHashes` even when `ignoreSelectiveTesting: true` — the assumption the new service path depends on.
Together these guarantee that the command output equals what the cache is keyed on, by construction. Synthesising the exact numeric divergence on a small in-memory graph isn’t tractable — it depends on a combination of pre-hash mapper ordering and graph structure (external xcframeworks, module maps, complex test plans) that doesn’t manifest on a minimal fixture — but the structural contract is fully covered.
How to test locally
- `tuist generate TuistCacheEE TuistCacheEETests TuistKit TuistKitTests –no-open`
- `xcodebuild test -workspace Tuist.xcworkspace -scheme TuistUnitTests -only-testing TuistKitTests/HashSelectiveTestingCommandServiceTests -only-testing TuistKitTests/SelectiveTestingGraphHasherTests CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY=””`
- `xcodebuild test -workspace Tuist.xcworkspace -scheme TuistCacheEEUnitTests -only-testing TuistCacheEETests/TestsCacheMapperTests CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY=””`
For end-to-end verification on a real project, run `tuist hash selective-testing –verbose` and `tuist test –build-only –verbose – -destination …` on the same commit and confirm the per-target hashes now match for any target with a non-trivial dependency closure (previously they would diverge).