import XCTest /// **A board that will not load, from the outside** (01-storage-format.md § Malformed input; /// 02-architecture.md § Launch and window lifecycle). /// /// ### The claim /// /// > A restored board that fails surfaces on welcome, row-level: its window doesn't open; welcome /// > appears alongside whatever did restore, the failed board's recents row carrying fail-fast's /// > specifics (load error) … never a silent drop. /// /// The `malformed` fixture is a well-formed board with exactly one unparseable card `index.md` /// (`UITestLaunch.malformedIndexText` — a frontmatter flow sequence that is never closed). Building /// it succeeds; loading it must not, and *how* it fails is the whole of this file: /// /// 1. **No board window.** Not an empty one, not one with the good lanes in it — fail-fast is /// all-or-nothing, so a partial board on screen would be the worse failure. /// 2. **Welcome, loudly.** The recents row for that board wears the loader's own sentence, which /// names the offending file. A row that fell back to "Unavailable", or to a count, would be the /// app declining to say what it found. /// 3. **Nothing repaired.** The bytes on disk are the bytes the fixture wrote. The loader is a pure /// function of the tree and writes nothing, ever — the Repair precedent — so a board it refused /// must still be refusable, byte for byte. /// /// ### Where each claim is checked /// /// The first two are here, because they are about *windows* and a window is what a unit test does not /// have. The third is checked **both** here and in `KanbanTests` — unconditionally there /// (`UITestMalformedFixtureBoardTests`, which builds the fixture and re-reads the tree), and /// opportunistically here, because reaching the app's container from the runner depends on how the /// app under test was signed and installed. Where the container is not reachable this file says so /// and leans on the unit suite rather than inventing a pass. final class FailFastLaunchTests: XCTestCase { override func setUp() { super.setUp() continueAfterFailure = false } /// The whole of claims 1 and 2, in one launch: no board window, welcome instead, and the row /// carrying the loader's specifics. @MainActor func testMalformedBoardFailsLoudlyAndOpensNoWindow() throws { let app = XCUIApplication.launched(with: .malformed) // Welcome is where a failed open lands (`BoardWindowHost.start`: record the failure, refresh // the recents, open welcome, dismiss the board window). XCTAssertTrue( app.windows["Welcome to Lanework"].waitForExistence(timeout: XCUIApplication.uiTimeout), "the welcome window did not appear after a failed open" ) // Claim 1. Checked *after* welcome has appeared, so this is "the board window never came", // not "the board window has not come yet". XCTAssertFalse( app.windows[FixtureBoard.malformed.windowTitle].exists, "a board window opened for a board the loader rejected" ) // Claim 2. The row is one combined accessibility element — name, location, caption — and the // caption is `BoardLoadError.description`: "⟨lane⟩/⟨card⟩/index.md: unparseable YAML at line // N: …". The UUIDs in that path are minted at launch and unknowable here, so the assertion is // on the parts that are the *app's* to keep stable: the offending file is named, and the // reason is stated. XCTAssertTrue( app.element(labelContaining: "index.md").waitForExistence(timeout: XCUIApplication.uiTimeout), "no welcome row named the offending index.md — fail-fast's specifics did not reach the surface" ) XCTAssertTrue( app.element(labelContaining: "unparseable YAML").exists, "the welcome row did not say why the board was refused" ) // And it is the malformed board's own row that says it. XCTAssertTrue( app.element(labelContaining: FixtureBoard.malformed.windowTitle).exists, "the failure did not land on the failed board's row" ) } /// Claim 3, twice over: the malformed bytes survive the refusal, and a second launch is refused /// the same way rather than opening a board the app quietly fixed. /// /// The relaunch is not redundant with the byte check — it is what the byte check *means* from the /// user's side, and it is the half that holds even where the container cannot be read. @MainActor func testMalformedBoardIsNeverRepaired() throws { let app = XCUIApplication.launched(with: .malformed) XCTAssertTrue( app.windows["Welcome to Lanework"].waitForExistence(timeout: XCUIApplication.uiTimeout), "the welcome window did not appear after a failed open" ) // The bytes, where the runner can reach them. `NSTemporaryDirectory()` inside the sandboxed // app resolves to its container, which an unsandboxed test runner can read — but only when // the app under test is installed where this path expects, so a miss is reported rather than // failed. `UITestMalformedFixtureBoardTests` makes the same claim unconditionally. if let malformed = Self.malformedIndexOnDisk() { XCTAssertTrue( malformed.contains(Self.malformationMarker), "the malformed index.md no longer carries its marker — something rewrote a file the loader refused to read" ) XCTAssertTrue( malformed.contains("order: [1024"), "the malformed frontmatter was repaired — fail-fast must not write" ) } else { // Not a failure, and not silence either: the run says which half of the claim it made. XCTContext.runActivity(named: "container not reachable from the runner") { _ in print(""" The app's fixture scratch directory could not be read from the test runner, so \ the on-disk half of "nothing was repaired" was not checked here. It is pinned \ unconditionally by KanbanTests ▸ UITestMalformedFixtureBoardTests. """) } } // The relaunch. A fresh launch rebuilds the fixture from scratch (the scratch root is wiped // per launch), so what this proves is the durable half: the app has no repair path that // would make the second attempt succeed where the first failed. app.terminate() let second = XCUIApplication.launched(with: .malformed) XCTAssertTrue( second.windows["Welcome to Lanework"].waitForExistence(timeout: XCUIApplication.uiTimeout), "the second launch did not reach welcome" ) XCTAssertFalse( second.windows[FixtureBoard.malformed.windowTitle].exists, "the second launch opened the board the first one refused" ) XCTAssertTrue( second.element(labelContaining: "index.md").waitForExistence(timeout: XCUIApplication.uiTimeout), "the second launch did not name the offending file" ) } // MARK: - Reading the app's container /// The string that appears only in the malformed file — `UITestLaunch.malformationMarker`, /// mirrored here for `FixtureBoard`'s reason. private static let malformationMarker = "lanework-ui-test-malformed-fixture" /// The malformed card's `index.md`, read from the app's sandbox container — or `nil` when the /// runner cannot reach it. /// /// The card folder's name is a UUID minted at launch, so the file is found by its content rather /// than by its path: exactly one `index.md` under the fixture board carries the marker, which is /// what the marker is for. @MainActor private static func malformedIndexOnDisk() -> String? { let home = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) let board = home .appendingPathComponent("Library/Containers/dev.rzen.indie.Kanban/Data/tmp", isDirectory: true) .appendingPathComponent("LaneworkUITestFixture", isDirectory: true) .appendingPathComponent("\(FixtureBoard.malformed.windowTitle).kanban", isDirectory: true) guard let walker = FileManager.default.enumerator(atPath: board.path) else { return nil } for case let relative as String in walker where relative.hasSuffix("index.md") { guard let data = try? Data(contentsOf: board.appendingPathComponent(relative)) else { continue } let text = String(decoding: data, as: UTF8.self) if text.contains(malformationMarker) { return text } } return nil } }