diff --git a/supacode/Features/Canvas/Models/CanvasCardLayout.swift b/supacode/Features/Canvas/Models/CanvasCardLayout.swift index 131e50cd4..b8e3a20a5 100644 --- a/supacode/Features/Canvas/Models/CanvasCardLayout.swift +++ b/supacode/Features/Canvas/Models/CanvasCardLayout.swift @@ -388,6 +388,10 @@ final class CanvasLayoutStore { initiallyLoadedCardKeys = Set(stored.cardLayouts.keys) } + // No actor-bound cleanup is needed. Avoid inferred isolated deinit, which can + // hit the TaskLocal invalid-free bug in older system Swift runtimes (swiftlang/swift#85204). + nonisolated deinit {} + func shouldAutoArrangeOnInitialEntry(for cardKeys: [String]) -> Bool { guard !cardKeys.isEmpty else { return false } return !cardKeys.contains { initiallyLoadedCardKeys.contains($0) } diff --git a/supacodeTests/CanvasLayoutStoreTests.swift b/supacodeTests/CanvasLayoutStoreTests.swift index 09febcca1..26b579197 100644 --- a/supacodeTests/CanvasLayoutStoreTests.swift +++ b/supacodeTests/CanvasLayoutStoreTests.swift @@ -6,6 +6,35 @@ import Testing @MainActor struct CanvasLayoutStoreTests { + @Test func releasesSynchronouslyWithTaskLocalStorageOutsideATask() async { + // Swift Testing runs in a Task. A main-queue callback reproduces synchronous + // UI teardown with thread-local TaskLocal storage instead of task-owned storage. + await withCheckedContinuation { continuation in + DispatchQueue.main.async { + MainActor.assumeIsolated { + #expect(Thread.isMainThread) + withUnsafeCurrentTask { #expect($0 == nil) } + let defaults = makeDefaults() + defer { defaults.removePersistentDomain(forName: defaultsSuiteName(defaults)) } + let layouts = ["tab-a": CanvasCardLayout(position: CGPoint(x: 10, y: 20))] + + CanvasLayoutStoreTestLocal.$marker.withValue(1) { + var store: CanvasLayoutStore? = CanvasLayoutStore(defaults: defaults) + weak let releasedStore = store + store?.setCardLayouts(layouts) + store = nil + + #expect(releasedStore == nil) + #expect(CanvasLayoutStoreTestLocal.marker == 1) + #expect(CanvasLayoutStore(defaults: defaults).cardLayouts == layouts) + } + #expect(CanvasLayoutStoreTestLocal.marker == 0) + continuation.resume() + } + } + } + } + @Test func loadsLegacyCardLayoutDictionary() throws { let defaults = makeDefaults() defer { defaults.removePersistentDomain(forName: defaultsSuiteName(defaults)) } @@ -76,6 +105,10 @@ struct CanvasLayoutStoreTests { } } +private enum CanvasLayoutStoreTestLocal { + @TaskLocal static var marker = 0 +} + private func makeDefaults() -> UserDefaults { let suiteName = "CanvasLayoutStoreTests-\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suiteName)!