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
This commit is contained in:
@@ -3,29 +3,35 @@ 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 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. **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.
|
||||
/// 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 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
|
||||
/// 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
|
||||
@@ -37,47 +43,107 @@ final class FailFastLaunchTests: XCTestCase {
|
||||
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.
|
||||
/// Claims 1 and 2: the surface appears in the board's own window, names the file, and shows no
|
||||
/// board.
|
||||
@MainActor
|
||||
func testMalformedBoardFailsLoudlyAndOpensNoWindow() throws {
|
||||
func testMalformedBoardShowsTheDecisionSurface() 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).
|
||||
// The surface, by the identifier it carries for exactly this
|
||||
// (`BoardDecisionSurface.accessibilityIdentifier`).
|
||||
let surface = app.descendants(matching: .any)["decision-surface"]
|
||||
XCTAssertTrue(
|
||||
app.windows["Welcome to Lanework"].waitForExistence(timeout: XCUIApplication.uiTimeout),
|
||||
"the welcome window did not appear after a failed open"
|
||||
surface.waitForExistence(timeout: XCUIApplication.uiTimeout),
|
||||
"an attended open of a refusing board did not transform into the decision surface"
|
||||
)
|
||||
|
||||
// Claim 1. Checked *after* welcome has appeared, so this is "the board window never came",
|
||||
// not "the board window has not come yet".
|
||||
XCTAssertFalse(
|
||||
// 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,
|
||||
"a board window opened for a board the loader rejected"
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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.
|
||||
// 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.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"
|
||||
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 welcome row did not say why the board was refused"
|
||||
"the row did not say what is wrong with the file"
|
||||
)
|
||||
// And it is the malformed board's own row that says it.
|
||||
|
||||
// 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(
|
||||
app.element(labelContaining: FixtureBoard.malformed.windowTitle).exists,
|
||||
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"
|
||||
)
|
||||
}
|
||||
|
||||
/// Claim 3, twice over: the malformed bytes survive the refusal, and a second launch is refused
|
||||
/// 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
|
||||
@@ -86,8 +152,8 @@ final class FailFastLaunchTests: XCTestCase {
|
||||
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"
|
||||
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
|
||||
@@ -101,7 +167,7 @@ final class FailFastLaunchTests: XCTestCase {
|
||||
)
|
||||
XCTAssertTrue(
|
||||
malformed.contains("order: [1024"),
|
||||
"the malformed frontmatter was repaired — fail-fast must not write"
|
||||
"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.
|
||||
@@ -120,16 +186,12 @@ final class FailFastLaunchTests: XCTestCase {
|
||||
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,
|
||||
second.descendants(matching: .any)["decision-surface"].waitForExistence(timeout: XCUIApplication.uiTimeout),
|
||||
"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"
|
||||
second.element(labelContaining: "unparseable YAML").exists,
|
||||
"the second launch did not name the same defect"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,9 @@ enum FixtureBoard {
|
||||
/// lot of it.
|
||||
case large
|
||||
|
||||
/// A well-formed board with exactly one unparseable card `index.md`, for the fail-fast pass. It
|
||||
/// is the only variant whose board window is *expected* never to appear.
|
||||
/// A well-formed board with exactly one unparseable card `index.md`, for the fail-fast pass. Its
|
||||
/// window is the only one that never holds a board: the open is attended, so the refusal
|
||||
/// transforms the loading content into the decision surface (`FailFastLaunchTests`).
|
||||
case malformed
|
||||
|
||||
/// `UITestLaunch.fixtureFlag` plus this variant's own flag. Both, always: the first is what "this
|
||||
@@ -149,8 +150,10 @@ extension XCUIApplication {
|
||||
/// Launches the app on `board` — the audit fixture unless told otherwise — and waits for its
|
||||
/// window.
|
||||
///
|
||||
/// **Not for `.malformed`**, which has no window to wait for: that variant is launched with
|
||||
/// `launched(with:)` and the caller waits for welcome instead.
|
||||
/// **Not for `.malformed`**, whose window never holds a board: since the decision surface (01
|
||||
/// § Malformed input, settled 2026-07-31) that variant's window does appear — carrying the
|
||||
/// surface rather than lanes — so the wait below would pass while saying nothing. That variant is
|
||||
/// launched with `launched(with:)` and the caller waits for the surface instead.
|
||||
@MainActor
|
||||
static func launchedWithFixtureBoard(_ board: FixtureBoard = .standard) -> XCUIApplication {
|
||||
let app = launched(with: board)
|
||||
|
||||
Reference in New Issue
Block a user