The embedded guide catches up on its own — creation-time parity for both apps, an open-time refresh for the phone
The card's frozen spec recommended Option C (install the agent guide at board creation); the owner's follow-up comment extended that ruling to a second axis — embedded guidelines should update whenever a board opens if the on-disk version is older than Lanework's, which the Mac app already does via BoardStore.refreshAgentGuide()/runScheduledHeals(). This card implements both halves. - BoardWriter.createBoard now calls AgentGuide.install(atBoardRoot:) right after seedGitignoreIfAbsent, so every board — Mac- or phone-created, since KanbanMobile.BoardIndexStore.createBoard calls this same method — is born with a current-version CLAUDE.md, with no dependency on a later open. Routed through AgentGuide.install itself rather than a hand-rolled write, so never-downgrade, the CLAUDE.user.md rescue, squatter displacement, and the EchoLedger heal-attribution exclusion all carry over unchanged. - BoardSession (KanbanMobile) gains a private refreshAgentGuideOnce(), fired once from open() (already idempotent on the .idle phase), fire-and-forget through the same CoordinatedFileAccess.write bracket every phone write uses. Deliberately not a heal scheduler — a one-shot courtesy check at session open, silent on failure (logged, never surfaced to lastError or a banner), matching AgentGuide's own "nothing here is a user-facing event" posture. The type's doc comment now names this one exception while keeping "no heal scheduler" true. - project.yml: lifted the KanbanMobile target's AgentGuide.swift build exclusion (dating to the original mobile MVP, "agents work where the Mac app runs") — both changes above fail to compile on the phone without it, since the type simply wasn't in that module. Verified safe: AgentGuide.swift imports only Foundation, and its one upward dependency touches only EchoLedger's unconditional recording API, never the #if os(macOS)-gated consumer surfaces. Tests: KanbanTests/BoardWriterTests.swift gains createBoardInstallsTheCurrentAgentGuide, calling createBoard directly and asserting the guide lands at AgentGuide.version immediately — the card's own Done-when, and also the phone's creation-time coverage since it's the same call site. KanbanMobileUITests/AgentGuideUITests.swift covers the open-time refresh itself, the one piece only reachable end-to-end from a running KanbanMobile process (no mobile unit-test target exists): the bundle's fixture board already carries no CLAUDE.md, so tapping into it and polling disk proves the wiring with no fixture changes needed. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import XCTest
|
||||
|
||||
/// The one narrow mobile behavior `BoardSession` adds beyond `BoardStore`'s own machinery (this
|
||||
/// card, ruled 2026-08-09): a session opened against a board with no `CLAUDE.md` at all — the
|
||||
/// shape every board predating the "install at creation" fix is left in, and the shape this
|
||||
/// suite's own fixture is deliberately in — gets one written the moment its board screen
|
||||
/// appears, with no dependency on the board ever touching a Mac.
|
||||
///
|
||||
/// This is the one piece of that wiring `KanbanTests` cannot reach: the write itself
|
||||
/// (`AgentGuide.install`) is the Mac's own well-covered code (`AgentGuideTests.swift`), and
|
||||
/// `BoardWriterCreateBoardTests.createBoardInstallsTheCurrentAgentGuide` already proves
|
||||
/// `BoardWriter.createBoard` — the one path `KanbanMobile.BoardIndexStore.createBoard` also
|
||||
/// calls — installs a current guide at birth. What only exists inside the `KanbanMobile` module,
|
||||
/// and so can only be driven end to end from here, is `BoardSession.open()` firing the one-shot
|
||||
/// `refreshAgentGuideOnce()` for a board that predates that guarantee. `KanbanMobileUITests`
|
||||
/// black-boxes the app (plain `XCTest`, no `@testable import KanbanMobile`), so this asserts on
|
||||
/// the marker prefix rather than `AgentGuide.version` — a version bump does not need to touch
|
||||
/// this file, and the KanbanTests suite is what pins the exact version and byte content.
|
||||
final class AgentGuideUITests: XCTestCase {
|
||||
|
||||
/// Boards list → tap the fixture board → its lane screen appears — no need to drill into a
|
||||
/// lane or a card, which is what keeps this the cheapest possible proof of the wiring rather
|
||||
/// than a rerun of `navigateToFirstCard`'s own coverage: `BoardScreen`'s
|
||||
/// `.task { session.open() }` is what fires the refresh, and it fires the moment the lane
|
||||
/// list itself appears.
|
||||
@MainActor
|
||||
func testOpeningABoardWithNoGuideInstallsOne() throws {
|
||||
let (app, root) = XCUIApplication.launchedWithFixtureBoard()
|
||||
|
||||
let boardRow = app.element(labelContaining: RichBoard.title)
|
||||
XCTAssertTrue(
|
||||
boardRow.waitForExistence(timeout: XCUIApplication.uiTimeout),
|
||||
"the \"\(RichBoard.title)\" row never appeared — check the fixture copy or the first scan"
|
||||
)
|
||||
boardRow.tap()
|
||||
|
||||
XCTAssertTrue(
|
||||
waitForAgentGuide(under: root),
|
||||
"no CLAUDE.md carrying the app's marker ever appeared under \(root.path)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// `waitForFile(under:containing:)` only ever looks at files named `index.md` (every other test
|
||||
/// in this bundle is asserting on a card's or a board's own content) — wrong shape for a file
|
||||
/// named `CLAUDE.md`, so this polls the one path a guide install can land on directly instead of
|
||||
/// walking the tree. Same 0.25s-poll shape as its sibling, for the same reason: the write reaches
|
||||
/// disk asynchronously, well after `BoardScreen`'s `.task { session.open() }` returns.
|
||||
private func waitForAgentGuide(under root: URL, timeout: TimeInterval = 15) -> Bool {
|
||||
let guideURL = root
|
||||
.appendingPathComponent(RichBoard.packageName, isDirectory: true)
|
||||
.appendingPathComponent("CLAUDE.md")
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
repeat {
|
||||
if let text = try? String(contentsOf: guideURL, encoding: .utf8), text.contains("lanework-agent-guide v") {
|
||||
return true
|
||||
}
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.25))
|
||||
} while Date() < deadline
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user