import AppKit import Dependencies import Foundation import Testing @testable import supacode struct RepositoryIconDetectorTests { // MARK: - Fixtures private func pngData(width: Int, height: Int) -> Data { let bitmap = NSBitmapImageRep( bitmapDataPlanes: nil, pixelsWide: width, pixelsHigh: height, bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0 ) guard let bitmap, let data = bitmap.representation(using: .png, properties: [:]) else { Issue.record("Could not create PNG fixture") return Data() } return data } private var svgData: Data { let svg = #""# + #""# return Data(svg.utf8) } /// Mimics Astro's default favicon: `fill="none"` on the root with /// paths colored only through an embedded `"# return Data(svg.utf8) } private struct ManifestEntry { var filename: String var size: String var scale: String? } private func appIconSetManifest(_ entries: [ManifestEntry]) -> Data { let images = entries.map { entry in var image = ["filename": entry.filename, "size": entry.size, "idiom": "mac"] if let scale = entry.scale { image["scale"] = scale } return image } guard let data = try? JSONSerialization.data(withJSONObject: ["images": images]) else { Issue.record("Could not encode manifest fixture") return Data() } return data } private let flutterPubspec = Data("name: demo\ndependencies:\n flutter:\n sdk: flutter\n".utf8) private let reactNativePackageJSON = Data( #"{"name":"demo","dependencies":{"react":"19.0.0","react-native":"0.80.0"}}"#.utf8 ) // MARK: - Apple @Test func appleCatalogPicksLargestReferencedRaster() async throws { try await withTemporaryProjectDirectory( entries: ["App.xcodeproj/"], contents: [ "App/Assets.xcassets/AppIcon.appiconset/Contents.json": appIconSetManifest([ .init(filename: "small.png", size: "16x16", scale: "1x"), .init(filename: "large.png", size: "512x512", scale: "2x"), ]), "App/Assets.xcassets/AppIcon.appiconset/small.png": pngData(width: 16, height: 16), "App/Assets.xcassets/AppIcon.appiconset/large.png": pngData(width: 1024, height: 1024), ] ) { root in let candidate = await RepositoryIconDetector.detect(at: root) #expect(candidate?.evidence == .appleAssetCatalog) #expect(candidate?.imageURL.lastPathComponent == "large.png") } } @Test func appleCatalogFallsBackWhenLargestEntryFileIsMissing() async throws { try await withTemporaryProjectDirectory( entries: ["Package.swift"], contents: [ "Sources/App/Assets.xcassets/AppIcon.appiconset/Contents.json": appIconSetManifest([ .init(filename: "missing.png", size: "512x512", scale: "2x"), .init(filename: "present.png", size: "128x128", scale: "1x"), ]), "Sources/App/Assets.xcassets/AppIcon.appiconset/present.png": pngData(width: 128, height: 128), ] ) { root in #expect(await RepositoryIconDetector.detect(at: root)?.imageURL.lastPathComponent == "present.png") } } @Test func appleCatalogWithMalformedManifestYieldsNothing() async throws { try await withTemporaryProjectDirectory( entries: ["App.xcodeproj/"], contents: [ "Assets.xcassets/AppIcon.appiconset/Contents.json": Data("not json".utf8), "Assets.xcassets/AppIcon.appiconset/icon.png": pngData(width: 64, height: 64), ] ) { root in #expect(await RepositoryIconDetector.detect(at: root) == nil) } } @Test func appleCatalogInsideDependencyDirectoryIsIgnored() async throws { try await withTemporaryProjectDirectory( entries: ["App.xcodeproj/"], contents: [ "node_modules/pkg/Assets.xcassets/AppIcon.appiconset/Contents.json": appIconSetManifest([ .init(filename: "icon.png", size: "64x64", scale: "1x") ]), "node_modules/pkg/Assets.xcassets/AppIcon.appiconset/icon.png": pngData(width: 64, height: 64), ] ) { root in #expect(await RepositoryIconDetector.detect(at: root) == nil) } } @Test func nearerAppleCatalogWinsOverDeeperOne() async throws { try await withTemporaryProjectDirectory( entries: ["App.xcworkspace/"], contents: [ "Assets.xcassets/AppIcon.appiconset/Contents.json": appIconSetManifest([ .init(filename: "near.png", size: "64x64", scale: "1x") ]), "Assets.xcassets/AppIcon.appiconset/near.png": pngData(width: 64, height: 64), "Modules/Deep/Assets.xcassets/AppIcon.appiconset/Contents.json": appIconSetManifest([ .init(filename: "deep.png", size: "1024x1024", scale: "1x") ]), "Modules/Deep/Assets.xcassets/AppIcon.appiconset/deep.png": pngData(width: 1024, height: 1024), ] ) { root in #expect(await RepositoryIconDetector.detect(at: root)?.imageURL.lastPathComponent == "near.png") } } // MARK: - Android @Test func androidLauncherPrefersHighestDensity() async throws { try await withTemporaryProjectDirectory( entries: ["gradlew", "settings.gradle"], contents: [ "app/src/main/res/mipmap-mdpi/ic_launcher.png": pngData(width: 48, height: 48), "app/src/main/res/mipmap-xxxhdpi/ic_launcher.png": pngData(width: 192, height: 192), ] ) { root in let candidate = await RepositoryIconDetector.detect(at: root) #expect(candidate?.evidence == .androidLauncher) #expect(candidate?.imageURL.path(percentEncoded: false).contains("mipmap-xxxhdpi") == true) } } @Test func androidAdaptiveOnlyProjectYieldsNothing() async throws { try await withTemporaryProjectDirectory( entries: ["gradlew"], contents: [ "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml": Data("".utf8) ] ) { root in #expect(await RepositoryIconDetector.detect(at: root) == nil) } } // MARK: - Flutter / React Native @Test func flutterPrefersIOSRunnerCatalogOverAndroidLauncher() async throws { try await withTemporaryProjectDirectory( entries: [], contents: [ "pubspec.yaml": flutterPubspec, "ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json": appIconSetManifest([ .init(filename: "Icon-App-1024x1024@1x.png", size: "1024x1024", scale: "1x") ]), "ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png": pngData(width: 1024, height: 1024), "android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png": pngData(width: 192, height: 192), ] ) { root in let candidate = await RepositoryIconDetector.detect(at: root) #expect(candidate?.evidence == .appleAssetCatalog) #expect(candidate?.imageURL.path(percentEncoded: false).contains("ios/Runner") == true) } } @Test func flutterFallsBackToAndroidLauncher() async throws { try await withTemporaryProjectDirectory( entries: [], contents: [ "pubspec.yaml": flutterPubspec, "android/app/src/main/res/mipmap-xhdpi/ic_launcher.png": pngData(width: 96, height: 96), ] ) { root in #expect(await RepositoryIconDetector.detect(at: root)?.evidence == .androidLauncher) } } @Test func pureDartPackageWithoutFlutterYieldsNothing() async throws { try await withTemporaryProjectDirectory( entries: [], contents: [ "pubspec.yaml": Data("name: pure_dart\nenvironment:\n sdk: ^3.0.0\n".utf8), "android/app/src/main/res/mipmap-xhdpi/ic_launcher.png": pngData(width: 96, height: 96), ] ) { root in #expect(await RepositoryIconDetector.detect(at: root) == nil) } } @Test func reactNativeUsesIOSCatalogThenAndroid() async throws { try await withTemporaryProjectDirectory( entries: [], contents: [ "package.json": reactNativePackageJSON, "ios/Demo/Images.xcassets/AppIcon.appiconset/Contents.json": appIconSetManifest([ .init(filename: "AppIcon.png", size: "1024x1024") ]), "ios/Demo/Images.xcassets/AppIcon.appiconset/AppIcon.png": pngData(width: 1024, height: 1024), ] ) { root in let candidate = await RepositoryIconDetector.detect(at: root) #expect(candidate?.evidence == .appleAssetCatalog) } } // MARK: - Web @Test func webManifestIconWinsOverFavicon() async throws { try await withTemporaryProjectDirectory( entries: [], contents: [ "package.json": Data(#"{"name":"site"}"#.utf8), "public/manifest.json": Data( #"{"icons":[{"src":"icons/icon-192.png","sizes":"192x192"},{"src":"icons/icon-512.png","sizes":"512x512"}]}"# .utf8 ), "public/icons/icon-192.png": pngData(width: 192, height: 192), "public/icons/icon-512.png": pngData(width: 512, height: 512), "public/favicon.png": pngData(width: 32, height: 32), ] ) { root in let candidate = await RepositoryIconDetector.detect(at: root) #expect(candidate?.evidence == .webAsset) #expect(candidate?.imageURL.lastPathComponent == "icon-512.png") } } @Test func htmlRelIconIsResolvedAgainstRoot() async throws { try await withTemporaryProjectDirectory( entries: [], contents: [ "package.json": Data(#"{"name":"site"}"#.utf8), "index.html": Data( #""#.utf8 ), "assets/fav.png": pngData(width: 64, height: 64), ] ) { root in #expect(await RepositoryIconDetector.detect(at: root)?.imageURL.lastPathComponent == "fav.png") } } @Test func staticFolderWithIndexHTMLAndFaviconQualifies() async throws { try await withTemporaryProjectDirectory( entries: [], contents: [ "index.html": Data("".utf8), "favicon.svg": svgData, ] ) { root in let candidate = await RepositoryIconDetector.detect(at: root) #expect(candidate?.evidence == .webAsset) #expect(candidate?.imageURL.lastPathComponent == "favicon.svg") } } @Test func rootLogoIsLastResortForWebProjects() async throws { try await withTemporaryProjectDirectory( entries: [], contents: [ "package.json": Data(#"{"name":"site"}"#.utf8), "logo.svg": svgData, ] ) { root in #expect(await RepositoryIconDetector.detect(at: root)?.imageURL.lastPathComponent == "logo.svg") } } @Test func nonWebProjectRootLogoFallsToGenericTier() async throws { // Originally rejected outright; the generic fallback tier now // accepts a near-square root logo for any project kind. try await withTemporaryProjectDirectory( entries: ["go.mod"], contents: [ "logo.svg": svgData ] ) { root in #expect(await RepositoryIconDetector.detect(at: root)?.evidence == .genericAsset) } } @Test func remoteAndDataIconReferencesAreRejected() async throws { try await withTemporaryProjectDirectory( entries: [], contents: [ "package.json": Data(#"{"name":"site"}"#.utf8), "index.html": Data( #""#.utf8 ), ] ) { root in #expect(await RepositoryIconDetector.detect(at: root) == nil) } } // MARK: - Validation @Test func undecodableImageIsRejected() async throws { try await withTemporaryProjectDirectory( entries: [], contents: [ "package.json": Data(#"{"name":"site"}"#.utf8), "logo.png": Data("this is not a png".utf8), ] ) { root in #expect(await RepositoryIconDetector.detect(at: root) == nil) } } @Test func oversizedRasterIsRejected() async throws { try await withTemporaryProjectDirectory( entries: [], contents: [ "package.json": Data(#"{"name":"site"}"#.utf8), "logo.png": pngData(width: 5000, height: 16), ] ) { root in #expect(await RepositoryIconDetector.detect(at: root) == nil) } } @Test func symlinkEscapingTheRepositoryIsRejected() async throws { let fileManager = FileManager.default let outside = fileManager.temporaryDirectory .appending(path: "outside-\(UUID().uuidString).png") try pngData(width: 64, height: 64).write(to: outside) defer { try? fileManager.removeItem(at: outside) } try await withTemporaryProjectDirectory( entries: [], contents: [ "package.json": Data(#"{"name":"site"}"#.utf8) ] ) { root in try fileManager.createSymbolicLink( at: root.appending(path: "logo.png"), withDestinationURL: outside ) #expect(await RepositoryIconDetector.detect(at: root) == nil) } } @Test func emptyRepositoryYieldsNothing() async throws { try await withTemporaryProjectDirectory(entries: ["README.md"]) { root in #expect(await RepositoryIconDetector.detect(at: root) == nil) } } // MARK: - Icon Composer @Test func iconComposerBundleWinsOverAssetCatalog() async throws { let rendered = FileManager.default.temporaryDirectory .appending(path: "rendered-\(UUID().uuidString).png") try pngData(width: 512, height: 512).write(to: rendered) defer { try? FileManager.default.removeItem(at: rendered) } try await withTemporaryProjectDirectory( entries: ["App.xcodeproj/"], contents: [ "assets/AppIcon.icon/icon.json": Data(#"{"fill":{"solid":"srgb:1,1,1,1"},"groups":[]}"#.utf8), "assets/AppIcon.icon/Assets/layer.svg": svgData, "Assets.xcassets/AppIcon.appiconset/Contents.json": appIconSetManifest([ .init(filename: "legacy.png", size: "512x512", scale: "1x") ]), "Assets.xcassets/AppIcon.appiconset/legacy.png": pngData(width: 512, height: 512), ] ) { root in let requested = LockIsolated<[URL]>([]) let candidate = await RepositoryIconDetector.detect(at: root) { bundle in requested.withValue { $0.append(bundle) } return rendered } #expect(candidate?.evidence == .appleIconComposer) #expect(candidate?.imageURL == rendered) #expect(candidate?.ownsImageFile == true) #expect(requested.value.first?.lastPathComponent == "AppIcon.icon") } } @Test func unrenderableIconComposerBundleFallsBackToAssetCatalog() async throws { try await withTemporaryProjectDirectory( entries: ["App.xcodeproj/"], contents: [ "AppIcon.icon/icon.json": Data(#"{"groups":[]}"#.utf8), "Assets.xcassets/AppIcon.appiconset/Contents.json": appIconSetManifest([ .init(filename: "legacy.png", size: "512x512", scale: "1x") ]), "Assets.xcassets/AppIcon.appiconset/legacy.png": pngData(width: 512, height: 512), ] ) { root in let candidate = await RepositoryIconDetector.detect(at: root) { _ in nil } #expect(candidate?.evidence == .appleAssetCatalog) } } @Test func iconDirectoryWithoutManifestIsNotRendered() async throws { try await withTemporaryProjectDirectory( entries: ["Package.swift", "Some.icon/"] ) { root in let candidate = await RepositoryIconDetector.detect(at: root) { _ in Issue.record("Renderer must not run without a valid icon.json") return nil } #expect(candidate == nil) } } // MARK: - Tauri @Test func tauriBundleIconIsUsed() async throws { try await withTemporaryProjectDirectory( entries: [], contents: [ "package.json": Data(#"{"name":"app"}"#.utf8), "src-tauri/tauri.conf.json": Data( #"{"bundle":{"icon":["icons/32x32.png","icons/icon.icns","icons/icon.png"]}}"#.utf8 ), "src-tauri/icons/32x32.png": pngData(width: 32, height: 32), "src-tauri/icons/icon.png": pngData(width: 1024, height: 1024), ] ) { root in let candidate = await RepositoryIconDetector.detect(at: root) #expect(candidate?.evidence == .tauriBundle) #expect(candidate?.imageURL.lastPathComponent == "icon.png") } } @Test func tauriV1ConfigShapeIsSupported() async throws { try await withTemporaryProjectDirectory( entries: [], contents: [ "src-tauri/tauri.conf.json": Data( #"{"tauri":{"bundle":{"icon":["icons/icon.png"]}}}"#.utf8 ), "src-tauri/icons/icon.png": pngData(width: 512, height: 512), ] ) { root in #expect(await RepositoryIconDetector.detect(at: root)?.evidence == .tauriBundle) } } // MARK: - package.json icon field @Test func packageJSONIconFieldOutranksFavicon() async throws { try await withTemporaryProjectDirectory( entries: [], contents: [ "package.json": Data(#"{"name":"ext","icon":"images/ext-icon.png"}"#.utf8), "images/ext-icon.png": pngData(width: 128, height: 128), "public/favicon.png": pngData(width: 32, height: 32), ] ) { root in let candidate = await RepositoryIconDetector.detect(at: root) #expect(candidate?.evidence == .webAsset) #expect(candidate?.imageURL.lastPathComponent == "ext-icon.png") } } // MARK: - Generic fallback @Test func genericTierAcceptsNearSquareRootIconForAnyKind() async throws { try await withTemporaryProjectDirectory( entries: ["go.mod"], contents: [ "icon.png": pngData(width: 256, height: 256) ] ) { root in let candidate = await RepositoryIconDetector.detect(at: root) #expect(candidate?.evidence == .genericAsset) #expect(candidate?.imageURL.lastPathComponent == "icon.png") } } @Test func genericTierFindsAssetsAndGithubLocations() async throws { try await withTemporaryProjectDirectory( entries: ["Cargo.toml"], contents: [ ".github/logo.png": pngData(width: 300, height: 260) ] ) { root in #expect(await RepositoryIconDetector.detect(at: root)?.evidence == .genericAsset) } } @Test func genericTierRejectsWideWordmarkLogo() async throws { try await withTemporaryProjectDirectory( entries: ["go.mod"], contents: [ "assets/logo.png": pngData(width: 1200, height: 300) ] ) { root in #expect(await RepositoryIconDetector.detect(at: root) == nil) } } @Test func kindProbeStillWinsOverGenericTier() async throws { try await withTemporaryProjectDirectory( entries: ["gradlew"], contents: [ "app/src/main/res/mipmap-xhdpi/ic_launcher.png": pngData(width: 96, height: 96), "icon.png": pngData(width: 256, height: 256), ] ) { root in #expect(await RepositoryIconDetector.detect(at: root)?.evidence == .androidLauncher) } } // MARK: - Hostile manifests @Test func hostileAppIconManifestNumbersDoNotTrap() async throws { // Infinity/NaN products must not reach `Int(_:)`, and the clamped // hostile entry must not block a valid fallback candidate. try await withTemporaryProjectDirectory( entries: ["App.xcodeproj/"], contents: [ "Assets.xcassets/AppIcon.appiconset/Contents.json": appIconSetManifest([ .init(filename: "evil.png", size: "1e308x1e308", scale: "1e308x"), .init(filename: "weird.png", size: "nanxnan", scale: "nanx"), .init(filename: "good.png", size: "128x128", scale: "1x"), ]), "Assets.xcassets/AppIcon.appiconset/evil.png": Data("not an image".utf8), "Assets.xcassets/AppIcon.appiconset/good.png": pngData(width: 128, height: 128), ] ) { root in #expect(await RepositoryIconDetector.detect(at: root)?.imageURL.lastPathComponent == "good.png") } } @Test func hostileWebManifestSizesDoNotTrap() async throws { // Int.max × 2 would overflow checked multiplication; out-of-range // dimensions are ignored, the icon itself remains usable. try await withTemporaryProjectDirectory( entries: [], contents: [ "package.json": Data(#"{"name":"site"}"#.utf8), "manifest.json": Data( #"{"icons":[{"src":"icon.png","sizes":"9223372036854775807x2 -5x-5"}]}"#.utf8 ), "icon.png": pngData(width: 256, height: 256), ] ) { root in let candidate = await RepositoryIconDetector.detect(at: root) #expect(candidate?.evidence == .webAsset) #expect(candidate?.imageURL.lastPathComponent == "icon.png") } } @Test func invisiblyRenderingSVGFaviconFallsBackToRasterFavicon() async throws { try await withTemporaryProjectDirectory( entries: [], contents: [ "package.json": Data(#"{"name":"site"}"#.utf8), "public/favicon.svg": styleColoredSVGData, "public/favicon.png": pngData(width: 32, height: 32), ] ) { root in let candidate = await RepositoryIconDetector.detect(at: root) #expect(candidate?.evidence == .webAsset) #expect(candidate?.imageURL.lastPathComponent == "favicon.png") } } @Test func invisiblyRenderingSVGAloneYieldsNothing() async throws { try await withTemporaryProjectDirectory( entries: [], contents: [ "package.json": Data(#"{"name":"site"}"#.utf8), "public/favicon.svg": styleColoredSVGData, ] ) { root in #expect(await RepositoryIconDetector.detect(at: root) == nil) } } @Test func oversizedSVGCanvasIsRejected() async throws { let hugeSVG = #""# + #""# try await withTemporaryProjectDirectory( entries: ["package.json"], contents: [ "logo.svg": Data(hugeSVG.utf8) ] ) { root in #expect(await RepositoryIconDetector.detect(at: root) == nil) } } @Test func genericTierIgnoresUnrelatedFilenames() async throws { try await withTemporaryProjectDirectory( entries: ["go.mod"], contents: [ "banner.png": pngData(width: 256, height: 256), "assets/screenshot.png": pngData(width: 256, height: 256), ] ) { root in #expect(await RepositoryIconDetector.detect(at: root) == nil) } } }