Files
lanework/KanbanUITests/FailFastLaunchTests.swift
T
rzen 31fee00c73 The decision surface — a refused open becomes a live repair, in place
Phase 3 of the decision surface, completing the card (01 ▸ Malformed
input, settled 2026-07-31). An attended open's fail-fast walk transforms
the loading window's content into one aggregated surface — never a
sheet, never a chain: defects grouped by class, each class stated once
with its files listed (Reveal in Finder + Open in Editor per row), a
class-level default preselected, per-item override behind a disclosure.
Only honest choices: YAML and malformed-schema get Editor + Re-check
(Skip below the root); newer-than-app gets Skip alone and blocks the
board at the root; the two root repairs — minted index, schema: 1 stamp
— are defaults. Repair and Open applies fixes in one store-less write
bracket and re-walks: clean proceeds, remainder re-aggregates into the
same surface. Cancel and ⌘W retire to welcome's row; restored opens
never see the surface at all (OpenOrigin rides the PendingOpen carrier).

Skips are per-open consent that rides the session — the store retains
the skip set and every reload passes it — and the opened board posts a
warning-tone notice naming what was left out, each item's Reveal riding
the banner strip's new reveal control. On Pro boards the repair bracket
binds its own EchoLedger, heal-marks everything, and the store adopts it
before the committer starts, so repairs land as one separate commit
authored Lanework Integrity — pinned end to end. Also fixed en route: a
retired loading window left its close interception installed and
returned false from windowShouldClose forever, blocking quit.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-08-01 10:52:02 -04:00

227 lines
12 KiB
Swift

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
}
}