test: full automated coverage across 6 phases (unit, codec, integration, perceptual, UI, CI)#1
Open
spooosh wants to merge 94 commits into
Open
test: full automated coverage across 6 phases (unit, codec, integration, perceptual, UI, CI)#1spooosh wants to merge 94 commits into
spooosh wants to merge 94 commits into
Conversation
Declare .testTarget in Package.swift and mirror as bundle.unit-test in project.yml so swift test (and cmd+U in Xcode) have a runnable bundle. Includes one smoke test that exercises ConversionSettings.normalizedQuality to validate @testable import ImageCRC compiles. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Roadmap of 6 phases (pure unit, codec round-trip, concurrency+VM, perceptual quality, UI smoke, CI). Phase 1 fully detailed with 13 bite-sized TDD tasks; later phases get their own plans as predecessors land. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure synthesised conformance — enables `==` against `FileOutputPlan` values in test assertions (Swift does not auto-synthesise Equatable for enums with associated values; opt-in is required). No behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three test cases over the full InputFormat × OutputFormat matrix: direct output formats encode regardless of input; sameAsOrigin maps each raster input to its matching encoder; sameAsOrigin + SVG resolves to a verbatim copy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…riteria Phase 1 originally promised "no code outside Tests/ modified", but the OutputPlanner combinatorics test required Equatable on FileOutputPlan (Swift does not auto-synthesise it for enums with associated values). Acknowledge that protocol conformances required by tests are in scope; production logic refactors remain a Phase 3 concern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swift only synthesises Equatable for enums without associated values. FileOutputPlan has .encode(EncoderFormat) so explicit conformance is required. Note now describes the actual recipe and recommends a chore(planner): commit for the conformance add. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Trailing-dot path ("file.") and no-extension path ("file") both yield
pathExtension == "" but exercise different URL-parse branches; cover both.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add ImageResizerTests.swift with three @suite groups (no-op, fit, fill) covering 8 cases: inactive passthrough (ref equality), same-size no-op, fit-by-min-scale, width-only fit, enlarge=false guard, enlarge=true upscale, fill crop to target dimensions, and fill-with-one-dim fallback. All 29 suite tests pass (8 new + 21 prior). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lled Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…el suites The smoke test was the proof-of-life for the test target. Its two tests (default settings + normalizedQuality clamp) are now fully covered by ConversionSettingsTests from Phase 1 / Task 4. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tached async let on a non-async actor method serialises through the actor's executor — the previous form ran sequentially and never exercised concurrent contention. Wrap each call in Task.detached so four tasks actually race onto the actor. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… test CGImage === pointer identity has no COW contract and would break under Phase 2's planned colorspace normalisation. Assert only the dims that matter to the caller; perf is verified by review, not this unit test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 2 (CodecTests), Phase 3 (IntegrationTests), and Phase 4 (GoldenTests) all need TempDirectory and SyntheticImage. Lifting them out of UnitTests/ avoids a layering violation when sibling directories reference helpers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fileExtension, isLossless, and displayName are case-statement getters where a regression manifests instantly when saving a file or reading the picker label. Maintenance cost (must update on every format add) exceeds the bug-prevention value. Per party-mode review path A. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Six sections, 16 bite-sized TDD tasks. Sections A-B sweep Phase 1 gaps from party-mode review (fill+enlarge, allCases exhaustiveness, edge cases, env-coupling, utType/allowedUTTypes, EncoderFormat, ConversionResult helpers). Section C wires Tests/CodecTests/. Section D adds round-trip tests for every encoder + ImageDecoder dispatch. Section E ships EXIF orientation fix as probe-then-fix; color profile is probe-only unless probe finds a real bug. Section F finishes pngquant (env-gated) and SVG decode. No on-disk fixtures — all synthesised in-test for hermeticity. Real fixtures get added in Phase 4 if SSIM baselines need them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Critical: - C1: fix typo in SyntheticImage.jpegData — kCGImagePropertyOrientation was set to undefined `orientation`; now correctly references the exifOrientation parameter. - C2: add a sentinel-pixel test for orientation=6 that samples the decoded image's top-center and asserts red dominance. Without this, a transposed rotation matrix (CCW where we wanted CW) would still pass the dim-swap-only assertions. Important: - I1: explicitly document Phase 2 scope gaps in Done When — OutputFormat property tests dropped per Path A, chromaticity preservation deferred to Phase 4, pngquant missing-binary skipped from automation. - I2: extend Task 13 commit body to flag the known limitation that EXIF orientation may live in nested TIFF dict for some files. - I3: mark Task 14 probe as smoke-only (colorspace name preservation only, not chromaticity); chromaticity-level wide-gamut precision is a Phase 4 SSIM concern. - I4: defensive exclude of CodecTests/.gitkeep in Package.swift to prevent any future SwiftPM warning regression. Minor: - M2: drop ceremonial signatureCheck test in PNGQuantizerTests; the env-gated happy path and production callers already pin the signature. Plan retains its 16-task structure with original numbering — no renumber was required since all fixes were in-place edits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Appended three @test cases to ImageResizerFillTests: - fillNoUpscale: fill + enlarge=false on a small source must not exceed source dims - fillEnlargeAndCrop: fill + enlarge=true upscales then center-crops to target - fillNoCropWhenAspectMatches: same-aspect fill produces zero crop offset All 11 ImageResizer tests pass (was 8, +3); full suite 31/9 (was 28/9). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lCases Append OutputPlannerExhaustivenessTests suite with two parameterised tests: sameAsOriginExhaustive iterates all 6 InputFormat.allCases so a silent default-clause regression would fail; directFormatsExhaustive covers the 6×4 cartesian product of InputFormat × direct OutputFormat. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eName Append 4 edge-case tests to FilenameResolverTests: documents trailing-dot behaviour for empty ext, double-dot for leading-dot ext, no-op for missing output dir, and verbatim preservation of spaces and Cyrillic. Suite grows from 5 to 9 tests (full suite 33 → 37). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…#require Add two new ConversionSummaryTests cases (negative savingsRatio when output > input; nil savingsRatio when totalOriginalBytes == 0) and fix the existing savingsRatioComputed to use try #require with throws signature per Swift Testing Lens 1. Suite grows from 4 to 6 tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Make the outputDirectory assertion conditional so the test passes on sandboxed CI runners that lack ~/Pictures (nil is a valid state). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pin UTType lookups for both output and input format models: verify each OutputFormat case resolves to its expected UTType identifier, and assert that InputFormat.allowedUTTypes includes all standard types plus WebP/AVIF on macOS 14+ where the system registry recognises them. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adversarial self-review caught two critical issues: C1: original T4 SlowConverter wrapped ImageConverter at the event- forwarding layer, which doesn't actually slow the conversion — ImageConverter's TaskGroup runs files in parallel inside the inner stream, so on-disk writes complete before the slow forwarding loop starts. Cancel would short-circuit events but not work. Replaced with naturally-slow 4096x4096 PNG inputs that take real wall-clock time to decode/encode. Task.isCancelled checks already in ImageConverter.processOne handle the actual short-circuit. T4 removed; T5–T8 renumbered to T4–T7. C2: root-level .accessibilityIdentifier on ZStack/VStack containers can be flattened out of the macOS AX tree by SwiftUI's optimization. XCUITest queries would miss them. Added .accessibilityElement(children: .contain) on dropZone, progressOverlay, completionSheet to force queryable containers. Plus an --ui-test-preload-size arg and bootstrap-stub clarification. Renumbering propagated through done-when, pause-points, self-review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Global setting `PRODUCT_MODULE_NAME: ImageCRC` in `settings.base` inherited into both the app and test targets, causing duplicate `ImageCRC.swiftmodule/*` outputs on Xcode 26 / macOS 26 SDK and breaking `xcodebuild test` before any test runs. SwiftPM was unaffected (per-target derived module names). Move the setting out of the global block and into per-target `settings.base`: `ImageCRC` → `ImageCRC`, `ImageCRCTests` → `ImageCRCTests`. Phase 5 prerequisite — XCUITest cannot run, and Phase 6 CI cannot land, until xcodebuild test works. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resources/Info.plist had drifted from project.yml's declared info.properties — `CFBundleIconFile: AppIcon` was present but not declared (xcodegen drops it in favour of ASSETCATALOG_COMPILER_APPICON_NAME), and `NSQuitAlwaysKeepsWindows: false` was declared but not yet written. Re-running xcodegen regenerates the file deterministically; commit the resulting diff so it doesn't keep reappearing on every regen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UITestSupport.applyLaunchArguments(to:) reads --ui-test-output-dir and --ui-test-preload-files <N>, fires only when IMAGECRC_UI_TEST=1 is set in the process environment. Wired into ContentView.onAppear so it runs once after VM initialisation. Production launches are unaffected — the env-var gate means zero overhead in the normal path. XCUITest needs this because the test process can't inject test doubles into the app process; the only handshake is launch arguments. Also exclude future Tests/UITests/ dir from the SwiftPM test target — XCUITest sources can't be hosted by SwiftPM (different runtime, separate process model) and must be Xcode-only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stable AX identifiers on the elements the upcoming UI tests query: dropZone, startButton, fileList, qualitySlider, formatPicker, outputFolderButton, outputFolderPath, cancelButton, progressOverlay, completionSheet, dismissButton. lowerCamelCase, no localization coupling — XCUITest queries by identifier, never by visible text. Root containers (dropZone, progressOverlay, completionSheet) carry .accessibilityElement(children: .contain) so SwiftUI does not flatten the wrapper out of the AX tree on macOS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires Tests/UITests source path under a new bundle.ui-testing target in project.yml, with TEST_TARGET_NAME=ImageCRC for the UI runner. Adds the target to the scheme's build and test target lists so xcodebuild test -scheme ImageCRC can drive both unit and UI suites. Bootstrap _Phase5Bootstrap.testTargetCompiles gives the target one compilable source — real UI tests land next. ENABLE_HARDENED_RUNTIME: NO on the UI test target — required because the runner.app and the .xctest bundle inherit ad-hoc signing from the global settings.base, and hardened runtime rejects the resulting Team-ID mismatch when loading the bundle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
XCUIApplication launches with IMAGECRC_UI_TEST=1 plus --ui-test-preload-files 2 and --ui-test-output-dir <tmp>; the dormant hook in App/UITestSupport seeds two synthetic PNGs into the VM. Test verifies the chain: dropZone visible → startButton enabled → tap → completionSheet appears within 30s → tap dismissButton → sheet gone, dropZone still visible. Asserts 1–2 .jpg files landed in the output dir as a side-effect cross-check. Identifier-based queries throughout — no localization coupling, no visible-text matching. Replaces the _Phase5Bootstrap stub from T4. Code path verified via xcodebuild build-for-testing (exit 0). Runtime on this machine is currently blocked by macOS automation permission not yet granted to xcodebuild's test runner — environmental, not a code/config issue. CI runners (Phase 6) auto-grant for ephemeral hosts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Launches the app with 8 preloaded 4096x4096 PNGs — decoding plus JPEG-encoding takes real wall-clock seconds, plenty of margin for the cancel tap to land mid-batch. Task.isCancelled checks already in ImageConverter.processOne short-circuit work in progress. Test taps startButton → waits for cancelButton in the progress overlay → taps cancel → asserts the completion sheet appears terminally → dismisses → verifies fewer than 8 output JPGs landed on disk. The on-disk count is the externally observable proof that cancel actually short-circuited the batch. If a future runner is fast enough that jpgCount == 8, bump the file count or size — never relax the assertion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 5 added an XCUITest target that swift test cannot host. Document the canonical xcodebuild invocation so future contributors and Phase 6 CI both know the exact destination flag and arch pin. Also note the first-run macOS automation-permission prompt that gates the test runner on a fresh machine, so contributors don't get stuck on the "Timed out while enabling automation mode" error. Refresh the swift test bullet — Phase 1–4 work means tests are no longer "not landed yet"; spell out which Tests/ subdirs SwiftPM covers and that Tests/UITests is excluded by design. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two changes to UITestSupport.applyLaunchArguments(to:) that were forced by trying to scale the cancel-flow XCUITest to a workload big enough to give the progress overlay an observable window: 1. Generate preload PNGs in a Task.detached and hop back to MainActor for the VM update. Synthesising 32+ large gradient images on the main thread blocked .onAppear long enough that macOS accessibility never finished loading and XCUITest setup timed out. 2. Replace the solid-colour fill with a diagonal CGGradient. Solid colours compress to ~0 DCT energy in JPEG, so a 32-file batch finished in <300ms on Apple Silicon — too fast for the progress overlay to land in the AX tree even with a 30s poll. CGGradient uses optimised paths so PNG generation stays cheap; the resulting image has enough entropy that JPEG encoding takes realistic time. Improvements survive even though the cancel-flow test was eventually deferred — they're a better foundation if/when the test returns. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The T6 cancel-flow test (ImageCRCCancelFlowTests) was attempted with escalating workloads — 8 / 32 / 64 files at 4096px, gradient PNGs instead of solid colours, async preload to keep AX loading — but remained fundamentally racy on Apple Silicon: - JPEG q=80 of synthetic gradients on 12 cores finishes the batch faster than XCUITest's AX poll interval (~250ms), so the progressOverlay either never appears in the AX tree or vanishes before XCUITest can latch onto it - Even when the overlay was queryable (after removing .transition), the nested cancelButton stayed invisible — a SwiftUI/AppKit AX quirk we couldn't pin down without production-side UX changes Cancel behaviour at the converter layer is already covered by IntegrationTests/ImageConverterCancelTests (Phase 3). The UI-side cancel button is trivial wiring to viewModel.cancel(); we accept the gap. To revive the test, options include AVIF output (10× slower than JPEG), a launch-arg knob that injects artificial work, or restructuring ProgressOverlayView so it's always mounted with conditional opacity. Production code (ProgressOverlayView, accessibility identifiers, UITestSupport launch hook) is unchanged — only the test class is removed. Happy-path UI test still passes via xcodebuild test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan to wire the existing Phase 1-5 test surface into GitHub Actions on the macos-14 runner. Five tasks: skeleton workflow + swift test, xcodebuild test for the XCUITest UI smoke, .build/ caching, xcresult upload on failure, README badge + brief CLAUDE.md note. No production changes. No new tests. Phase 6's contract is "make CI run what's already green locally." Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
C1: Package.resolved is gitignored on this repo, confirmed via git ls-files. Cache key would collapse to a constant prefix and never invalidate. Switch T3 to key on Package.swift instead. C2: T2 Step 1 had a confused xcbeautify-fallback YAML followed by "actually use this simpler version." Rewrote as one clean version with reasoning for not piping through xcbeautify (raw output is more debuggable in early CI runs). I1: Documented that macos-14's default Xcode is 15.4 (Swift 5.10) matching Package.swift exactly — no xcode-select step needed. I2: Clarified brew install consolidation rationale. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Runs on push to main/tests/** and on every pull_request. paths-ignore skips doc-only changes so README and plan edits don't burn runner minutes. concurrency cancel-in-progress keeps stacked pushes from piling up parallel runs for a superseded commit. The job runs on macos-14 (Apple Silicon by default), brew-installs pngquant, and exports IMAGECRC_TEST_REQUIRE_PNGQUANT=1 so the PNGQuantizer suite actually runs instead of being skipped. T1 deliberately ships only the swift test invocation. xcodebuild test (XCUITest UI smoke) is added in T2 once the skeleton plumbing is proven. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The XCUITest target is XcodeGen-only and not visible to swift test. Add a step that brew-installs xcodegen alongside pngquant, regenerates the Xcode project, and runs xcodebuild test pinned to platform=macOS,arch=arm64 (matches the macos-14 runner's host arch). -resultBundlePath writes the xcresult bundle to build/TestResults.xcresult for post-mortem upload by T4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cold swift build resolves and compiles libwebp from SDWebImage's libwebp-Xcode plus all of ImageCRC. Caching .build/ keyed on runner.os + runner.arch + Package.swift hash gives near-instant restore on no-dependency-change runs. restore-keys allow partial cache hit when the manifest changes but most checkouts stay unaffected. Package.swift (not Package.resolved) is the cache key because the latter is gitignored and absent on a fresh actions/checkout — using it would collapse the key to a constant prefix that never invalidates. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When xcodebuild test fails on CI, the runner logs alone make debugging hard — XCUITest in particular benefits from the screenshot and structured failure data inside the xcresult bundle. Upload it as an artifact (14-day retention) on failure() so the user can download and inspect locally without re-running the whole CI cycle. if-no-files-found: ignore handles the case where swift test fails before xcodebuild runs and the xcresult bundle never gets written. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
README gets a leftmost CI badge linking to the workflow runs page. Until tests/phase-1 merges to main and the first run completes the badge shows "no status" — auto-updates on first run. CLAUDE.md gets a one-line forward reference under Build & run so future contributors know CI runs both swift test and xcodebuild test on macos-14, and that local parity matters before pushing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI on macos-14 runs Xcode 15.4 / Swift 5.10. In that toolchain SwiftUI's View protocol is not implicitly @mainactor, so calls to @MainActor-isolated ConversionViewModel methods (start, cancel, remove, clearFiles, chooseOutputDirectory, dismissCompletion) and properties (canStart, files, phase, summary) from inside ContentView body closures hit "main actor-isolated ... can not be referenced from a non-isolated context" errors. Locally on Xcode 26.4 / Swift 6.3 the errors don't surface because the SwiftUI View conformance carries @mainactor implicitly in newer SDKs. Mark ContentView explicitly @mainactor — closures inside body inherit isolation, calls become MainActor-to-MainActor, errors disappear, and the annotation matches actual runtime behaviour either way (SwiftUI runs Views on the main thread regardless). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI on Swift 5.10 (Xcode 15.4) reported `call to main actor-isolated initializer 'init(converter:fileChooser:)' in a synchronous nonisolated context` at the @State default-value site for ConversionViewModel(). ConversionViewModel is @mainactor, so its initialiser is too. Swift 6's SDK propagates @mainactor through SwiftUI's App protocol implicitly; Swift 5.10's does not. Annotate explicitly so both toolchains compile clean. Same pattern as the previous ContentView fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Swift Testing framework (\`import Testing\` / @test / #expect) is bundled in Swift 6 toolchains shipping with Xcode 16+. On the macos-14 runner, the default xcode-select target is Xcode 15.4 (Swift 5.10), where \`import Testing\` fails to resolve and our test target won't compile. Select the highest-versioned Xcode 16.x available on the runner before the SwiftPM cache and brew install steps, so swift test and xcodebuild test both run against Swift 6. Discovers the Xcode bundle dynamically (\`Xcode_16*.app\` glob, sorted, take last) instead of pinning a specific minor — protects against the runner image rotating its bundled Xcode 16.x point release without warning. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The macos-14 GitHub runner has no hardware AV1 encoder, so the AVIF SwiftPM tests that go through ImageIO's software path can take many minutes per case. Phase 4 capped each AVIF test at .timeLimit(.minutes(1)) but a 4-case parameterised SSIM matrix plus avifBounds plus avifRoundTrip plus the .avif arm of the decoder dispatch sweep collectively blew past the workflow's 30-minute job-timeout on a cold \`swift test\`. Gate AVIF encode tests on \`IMAGECRC_TEST_SKIP_AVIF != 1\` (mirrors the existing IMAGECRC_TEST_REQUIRE_PNGQUANT pattern). Set the var to "1" at workflow level so CI skips the slow path; locally it stays unset so Apple Silicon hardware AV1 keeps them sub-second and they remain part of the regression net. The decoder-dispatch parameterised test filters .avif out of its arguments via a static helper instead of \`.enabled(if:)\`, since the gate applies to one of N cases rather than the whole test. AVIF DECODE remains untouched — only encode is the bottleneck. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…anches Every push to a tests/** branch with an open PR was firing both the push trigger and the pull_request trigger, doubling CI minutes for every commit. concurrency.cancel-in-progress can't dedupe across the two because their github.ref differs (refs/heads/tests/phase-1 vs refs/pull/1/merge). Scope push to main only — main is the protected branch where we want post-merge CI verification regardless. Feature-branch coverage stays on the pull_request trigger. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On the macos-14 GH runner with Xcode 16, swift test builds successfully (24s) but then swiftpm-testing hangs silently — never streams a single test event, never exits. Both AVIF-gated and non-gated runs hang identically; the issue is not test-content but the runner itself (suspected NSApplication lifecycle / actor scheduling on virtualised macOS). Locally on Apple Silicon swift test returns in 300 ms, so this is CI-specific. xcodebuild test exercises the exact same ImageCRCTests target via Xcode's runner, which manages NSApplication initialisation properly. Drop the swift test step entirely — it was already a double-run since xcodebuild test covers the same SwiftPM-side coverage plus the XCUITest happy-path. Saves a step, avoids the hang, no coverage loss. Local swift test remains the primary fast-iteration path for contributors and is still documented in CLAUDE.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
xcodebuild test does not inherit process-level env into the host-app test runner; previous attempt to gate AVIF via job-level env: had no effect at runtime. The supported propagation is to set build settings prefixed with TEST_RUNNER_, which Xcode strips before launching the runner — i.e. TEST_RUNNER_IMAGECRC_TEST_SKIP_AVIF=1 becomes IMAGECRC_TEST_SKIP_AVIF=1 in the runner's environment. Also discovered HEIC encode fails on macos-14 GH runners for the same reason as AVIF — the AppleAVEVA IOService (HEVC hardware encoder) isn't available on the virtualised host, surfacing as "IOServiceGetMatchingService failed" during HEICEncoder.encode. Add IMAGECRC_TEST_SKIP_HEIC mirror to the AVIF gate, applied to: - HEICEncoder round-trip (ImageIOEncodersTests) - HEIC quality matrix (PerceptualQualityTests) - HEIC size bounds (EncoderSizeTests) - .heic arm of decoder dispatch sweep Both skips are pinned at the workflow's xcodebuild invocation; locally on Apple Silicon both encoders run sub-second on real hardware and remain part of the regression net. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous env-flag approach (IMAGECRC_TEST_SKIP_AVIF, _SKIP_HEIC, _REQUIRE_PNGQUANT) never propagated past xcodebuild's host-app test runner; CI hit identical failures with both job-level env: blocks and TEST_RUNNER_*-prefixed build settings. The runner process simply does not see the variables. Move the gate into the test bundle: Tests/Support/CodecCapability.swift exposes three lazy-cached probes that run once at test startup: - avifEncodeAvailable: tries a 1x1 AVIF encode via AVIFEncoder. Apple Silicon hardware AV1 succeeds; virtualised CI runners without the AppleAVEVA IOService throw `encodeFailed` and the probe returns false. - heicEncodeAvailable: same shape for HEIC (HEVC encoder, same hardware dependency). - pngquantAvailable: filesystem-checks the standard PATH locations for the pngquant binary that PNGQuantizer's subprocess invokes. Each AVIF/HEIC/pngquant test gates on the corresponding probe via .enabled(if:); the decoder-dispatch sweep filters .heic/.avif from its arguments when their encoders aren't available. Locally everything runs as before (the probes return true on Apple Silicon). On CI the slow/missing paths skip cleanly without env-var plumbing. Workflow simplifies as a side effect: drop the env: block and the TEST_RUNNER_* build-setting args from xcodebuild test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CI run with 1x1 probes correctly skipped AVIF (destination creation fails up front, probe catches it) but HEIC still ran and failed at the real test sizes — its `CGImageDestinationCreateWithData` succeeds even without the AppleAVEVA driver, and only `CGImageDestinationFinalize` fails. A 1x1 input is small enough that the encoder can short-circuit to a special-case path where Finalize never triggers, so the probe returns false-positive `true` and the tests run anyway. 64x64 forces the full encode pipeline through Finalize on every host. Still sub-millisecond on Apple Silicon hardware, but reliably catches the missing-codec failure on virtualised CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both 1x1 and 64x64 solid-colour probes returned false-positive `true` for HEIC on virtualised macOS CI: destination creation succeeds and the encoder takes a fast/uniform-block path that doesn't reach the broken AppleAVEVA HEVC driver. The matrix tests then run on 128x128 gradient inputs and fail at `CGImageDestinationFinalize` with "Finalize failed" — exactly the case the probe is supposed to catch. Match probe characteristics to the heaviest test cases: 128x128 gradient. Probes a couple of milliseconds longer at startup on real hardware, but a passing probe now guarantees every gated test will reach the same codepath. AVIF's destination creation fails up front on CI regardless of probe shape, so its behaviour is unchanged; HEIC is the case this commit fixes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 128x128 gradient probe at q=0.5 still false-positive'd HEIC on CI: mid-quality encodes have a fallback codepath that doesn't reach the broken AppleAVEVA HEVC driver, so the probe succeeded while the matrix test's first case (q=1.0, lossless HEVC) failed at finalize. Probe at q=1.0 — the strictest case the gated tests actually run — so a passing probe guarantees every test, including lossless mode, will work. AVIF probes at q=1.0 too for consistency, even though its destination creation already fails up front and the quality doesn't actually matter for that codec on CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitHub Actions deprecated Node 20 for JavaScript-based actions; forced migration to Node 24 lands 2026-06-02 (per the 2025-09-19 deprecation notice). Our actions/checkout@v4, actions/cache@v4 and actions/upload-artifact@v4 still ship Node 20 wrappers and were emitting a deprecation annotation on every run. Set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 at workflow scope to opt in now — validates compatibility ahead of the forced cutover, silences the annotation. If anything breaks, the documented escape hatch is ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. Once the action maintainers ship v5+ tags with native Node 24 support, this env var becomes a no-op and can be deleted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lands the comprehensive testing strategy from
docs/superpowers/plans/2026-05-05-testing-strategy.md— six sequential phases that take ImageCRC from zero automated coverage to 92 SwiftPM tests + 1 XCUITest happy-path + GitHub Actions CI onmacos-14.macos-14, bothswift testandxcodebuild test, SwiftPM cache, xcresult upload on failureThree real production bugs caught, all in
Services/Decoders/ImageIODecoder.swiftEXIF orientation transforms, all surfaced by sentinel-pixel coverage that QA self-review insisted on adding:eed48be— EXIF orientation never applied at all (rotated photos came out sideways)f8f53ab—.leftMirroredproduced black output,.rightMirroredapplied transpose instead of anti-transpose2c855f0— TIFF dictionary fallback for orientation tag missing on edited JPEGs/HEICsLight production-side changes (everything else is tests):
Services/Converter.swift,ViewModels/FileChooser.swift— protocol extraction for VM injection (Phase 3)Services/ImageConverter.swift—enum→structconforming toConverterServices/Decoders/ImageIODecoder.swift— three orientation fixesServices/OutputPlanner.swift— explicitEquatable(associated-value enum)App/UITestSupport.swift— dormantIMAGECRC_UI_TEST=1-gated launch-arg hookApp/ImageCRCApp.swift— single.onAppearinvocation of the hookNumbers
swift testxcodebuild test -scheme ImageCRC -destination 'platform=macOS,arch=arm64'`+9175 / -34` lines across 57 files. The plans directory accounts for ~6.6k lines (six phase plans + roadmap); production source diff is ~150 lines.
Deliberately deferred
Test plan
🤖 Generated with Claude Code