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
227 lines
12 KiB
Swift
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
|
|
}
|
|
}
|