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 claims, as they stand after the decision surface /// /// 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. **The window stays, and explains itself.** The fixture opens through `AppModel.openBoard`, which /// is an **attended** open — and 01 (settled 2026-07-31) makes an attended refusal the decision /// surface's case: "one aggregated surface hosted by the pre-snapshot loading window — the loading /// content transforms in place, never a sheet over a spinner". So there *is* a window, it is the /// one that was opening, and what it shows names the offending file. /// 2. **No board behind it.** Fail-fast is all-or-nothing: the surface is a decision, not a partial /// board, so the good lanes are not on screen underneath it. /// 3. **Cancel is the old landing, on demand.** "**Cancel** aborts the open: the window retires and /// the board lands row-level on welcome, record-before-load unchanged" — which is the sequence /// this file used to assert *unconditionally*, now reached by the user's own choice. /// 4. **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 — and the surface writes /// only what the user chooses, which here is nothing. /// /// **What changed and why**: claims 1 and 2 used to read "no board window, welcome instead". That was /// the whole landing for *every* refusal before this milestone; it is now the **restored** landing /// (`RestoreBootstrapView`) and the environmental one. The fixture launch is neither — it opens a /// board the way a double-click does. /// /// ### Where each claim is checked /// /// The first three are here, because they are about *windows* and a window is what a unit test does /// not have. The fourth 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 } /// Claims 1 and 2: the surface appears in the board's own window, names the file, and shows no /// board. @MainActor func testMalformedBoardShowsTheDecisionSurface() throws { let app = XCUIApplication.launched(with: .malformed) // The surface, by the identifier it carries for exactly this // (`BoardDecisionSurface.accessibilityIdentifier`). let surface = app.descendants(matching: .any)["decision-surface"] XCTAssertTrue( surface.waitForExistence(timeout: XCUIApplication.uiTimeout), "an attended open of a refusing board did not transform into the decision surface" ) // It is the *opening window* that hosts it — the loading content transformed in place, so the // window is still called what the loading state called it. XCTAssertTrue( app.windows[FixtureBoard.malformed.windowTitle].exists, "the surface is not in the board's own window" ) XCTAssertFalse( app.windows["Welcome to Lanework"].exists, "an attended refusal must not retire — that is the restored landing" ) // The class section for unparseable frontmatter, and the specifics on its row. The UUIDs in // the 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.descendants(matching: .any)["decision-section-unreadable-frontmatter"].exists, "the defect was not grouped into its class section" ) XCTAssertTrue( app.element(labelContaining: "index.md").exists, "no row named the offending index.md — fail-fast's specifics did not reach the surface" ) XCTAssertTrue( app.element(labelContaining: "unparseable YAML").exists, "the row did not say what is wrong with the file" ) // Claim 2: a decision, not a partial board. The fixture's intact lane is not on screen. XCTAssertFalse( app.element(labelContaining: "A good card").exists, "a partial board rendered behind the surface — fail-fast is all-or-nothing" ) } /// Claim 3: Cancel is the old landing, reached by the user's own choice — the window retires and /// the board lands row-level on welcome with fail-fast's specifics on it. @MainActor func testCancelRetiresToWelcomeWithTheSpecifics() throws { let app = XCUIApplication.launched(with: .malformed) // Queried across every element type rather than as `app.buttons[...]`: the surface's footer // sits inside a `.contain` accessibility group, and which AX type a SwiftUI `Button` lands on // there is not something a test should be asserting about in passing. let cancel = app.descendants(matching: .any)["decision-cancel"] XCTAssertTrue( cancel.waitForExistence(timeout: XCUIApplication.uiTimeout), "the surface did not appear, so Cancel could not be pressed" ) cancel.click() XCTAssertTrue( app.windows["Welcome to Lanework"].waitForExistence(timeout: XCUIApplication.uiTimeout), "Cancel did not land on welcome" ) XCTAssertFalse( app.windows[FixtureBoard.malformed.windowTitle].exists, "the window did not retire" ) // The row is one combined accessibility element — name, location, caption — and the caption is // `BoardLoadFailure.description`. Matched on *value* as well as label: welcome's rows are // list cells whose combined text lands in the AX value, not in a label (see // `Self.element(textContaining:in:)`). XCTAssertTrue( Self.element(textContaining: "unparseable YAML", in: app) .waitForExistence(timeout: XCUIApplication.uiTimeout), "the welcome row did not carry fail-fast's specifics" ) XCTAssertTrue( Self.element(textContaining: FixtureBoard.malformed.windowTitle, in: app).exists, "the failure did not land on the failed board's row" ) } /// The first element whose **label or value** contains `fragment`. /// /// `XCUIApplication.element(labelContaining:)` matches the label alone, which is the right question /// for the decision surface's own rows (each is one combined element with an explicit /// accessibility label) and the wrong one for welcome's recents rows: those are list cells whose /// name-location-caption text arrives as the cell's AX *value*. @MainActor private static func element(textContaining fragment: String, in app: XCUIApplication) -> XCUIElement { app.descendants(matching: .any) .matching(NSPredicate(format: "label CONTAINS %@ OR value CONTAINS %@", fragment, fragment)) .firstMatch } /// Claim 4, 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.descendants(matching: .any)["decision-surface"].waitForExistence(timeout: XCUIApplication.uiTimeout), "the surface did not appear" ) // 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 — nothing here mints a rewrite of unparseable YAML" ) } 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.descendants(matching: .any)["decision-surface"].waitForExistence(timeout: XCUIApplication.uiTimeout), "the second launch opened the board the first one refused" ) XCTAssertTrue( second.element(labelContaining: "unparseable YAML").exists, "the second launch did not name the same defect" ) } // 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 } }