Files
lanework/KanbanTests/BoardTemplateTests.swift
T
rzen e7b48d2d53 Author the ten bundled template boards
The pathfinder's catalog transcribed verbatim as real board folders —
lane titles, icons, palette colors, board icons, and blurbs read
straight from its BoardTemplate.all, every color already in the
palette, no invented fields, no starter cards it never had. Basic had
drifted from the source in the engine card's version: it carried
icon/iconColor keys the pathfinder's key-less Basic never wrote
(visually identical via the fallback, byte-level wrong) and a
rewritten blurb — both restored. Orders run 100 to 1000 in inventory
order, Basic first; fresh lane UUIDs, gapped ranks, one consistent
timestamp; the discovery test now pins all ten slugs, lane counts,
Basic's primacy, and distinct ascending orders through the real
loader.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-28 19:00:45 -04:00

200 lines
8.7 KiB
Swift

import Foundation
import Testing
@testable import Kanban
/// Template **discovery** — the bundled store, and what a `BoardTemplate` reads off a real board
/// folder (09-templates.md ▸ Definition format).
///
/// The instantiation half lives in `TemplateEngineTests.swift`. What this file pins is 09's
/// "testable for free" clause: "template validity is enforced by the loader's own fail-fast rules at
/// test time (walk `Templates/`, load each)".
// MARK: - Tests
@Suite("BoardTemplate")
struct BoardTemplateTests {
@Test("Every bundled template folder loads through the ordinary loader")
func everyBundledTemplateLoads() throws {
let store = try #require(TemplateEngine.bundledStore)
let folders = TemplateEngine.templateFolders(in: store)
#expect(!folders.isEmpty, "the app ships at least one template")
for folder in folders {
let result = TemplateEngine.load(templateAt: folder, origin: .bundled)
if case let .failure(error) = result {
Issue.record("bundled template \(folder.lastPathComponent) failed to load: \(error.description)")
}
}
let templates = TemplateEngine.bundledTemplates()
#expect(templates.count == folders.count)
// 09-templates.md ▸ Inventory: all ten pathfinder templates, each carrying over its own lane
// count — the definitive check that the bundle is the full set, not just "some folders load".
let expectedLaneCounts: [String: Int] = [
"basic": 2,
"classic-kanban": 3,
"software-project": 5,
"content-pipeline": 5,
"job-hunt": 5,
"sales-pipeline": 5,
"weekly-planner": 4,
"roadmap": 4,
"event-planning": 5,
"bug-tracker": 5,
]
#expect(Set(templates.map(\.slug)) == Set(expectedLaneCounts.keys), "all ten bundled slugs are present")
for template in templates {
#expect(template.lanes.count == expectedLaneCounts[template.slug], "\(template.slug) lane count")
}
#expect(templates.first?.slug == "basic", "Basic carries the lowest template.order")
let orders = templates.compactMap(\.order)
#expect(orders.count == templates.count, "every bundled template carries a template.order")
#expect(Set(orders).count == orders.count, "bundled template.order values are distinct")
#expect(orders == orders.sorted(), "bundledTemplates() lists templates in ascending template.order")
}
@Test("Basic is bundled, first, and the plain To Do / Done scaffold")
func basicIsTheFirstBundledTemplate() throws {
let basic = try #require(TemplateEngine.bundledTemplates().first)
#expect(basic.slug == "basic", "09 calls the bundle folder name the template's stable slug")
#expect(basic.origin == .bundled)
#expect(basic.name == "Basic")
#expect(basic.lanes.map { $0.title.value } == ["To Do", "Done"])
#expect(basic.lanes.allSatisfy { $0.cards.isEmpty }, "the Basic scaffold seeds no cards")
}
@Test("Name, blurb, icon and chooser order all come off the template board's own index.md")
func pickerMetadataIsReadFromTheBoard() throws {
let basic = try #require(TemplateEngine.bundledTemplates().first)
#expect(basic.name == basic.model.title.value)
#expect(!basic.blurb.isEmpty, "the body is the picker blurb and the new board's description")
#expect(basic.blurb == basic.model.document.body.trimmingCharacters(in: .whitespacesAndNewlines))
#expect(basic.iconColor == basic.model.iconColor.value)
#expect(basic.order == 100, "template: {order: 100} is the chooser position")
// Basic carries no icon key at all — the pathfinder's plain scaffold, inherited verbatim —
// so the pass-through half of this check needs a template that actually sets one.
let classicKanban = try #require(TemplateEngine.bundledTemplates().first { $0.slug == "classic-kanban" })
#expect(classicKanban.icon == classicKanban.model.icon.value)
}
@Test("An icon the running system cannot draw falls back to the board default")
func unknownIconFallsBackToTheDefault() throws {
let fixture = try TemplateFixture(named: "Odd Icon")
defer { fixture.tearDown() }
try fixture.board(icon: "not.a.real.symbol.name")
let template = try fixture.template()
#expect(template.icon == ItemSymbol.board)
}
// MARK: template.order
@Test("A keyless board dropped in the store is a template with no order")
func keylessTemplateHasNoOrder() throws {
let fixture = try TemplateFixture(named: "Hand Dropped")
defer { fixture.tearDown() }
try fixture.board()
#expect(try fixture.template().order == nil, "no template: key is required of a user template")
}
@Test("A malformed template: key reads as keyless rather than as a failure")
func malformedTemplateKeyIsKeyless() throws {
let fixture = try TemplateFixture(named: "Odd Key")
defer { fixture.tearDown() }
try fixture.board(extraKeys: "template: [1, 2]")
#expect(try fixture.template().order == nil)
}
@Test("A float order is as good as an integer one")
func floatOrderReads() throws {
let fixture = try TemplateFixture(named: "Float")
defer { fixture.tearDown() }
try fixture.board(extraKeys: "template: {order: 12.5}")
#expect(try fixture.template().order == 12.5)
}
// MARK: Chooser order
@Test("Chooser order is template.order, then display name for the keyless tier")
func chooserOrderIsKeyedThenNamed() throws {
let fixture = try TemplateFixture(named: "Sorting")
defer { fixture.tearDown() }
let second = try fixture.freeStandingBoard(named: "b", title: "Second", extraKeys: "template: {order: 200}")
let first = try fixture.freeStandingBoard(named: "a", title: "First", extraKeys: "template: {order: 100}")
let zebra = try fixture.freeStandingBoard(named: "z", title: "Zebra")
let apple = try fixture.freeStandingBoard(named: "y", title: "Apple")
let sorted = TemplateEngine.sortedForChooser([zebra, second, apple, first])
#expect(sorted.map(\.name) == ["First", "Second", "Apple", "Zebra"])
}
}
// MARK: - Fixture
/// A temp store holding hand-written template board folders — the user store's shape, minus
/// Application Support (which no test may touch).
struct TemplateFixture {
let store: URL
let boardURL: URL
init(named name: String) throws {
store = FileManager.default.temporaryDirectory
.appendingPathComponent("TemplateTests-\(UUID().uuidString)", isDirectory: true)
boardURL = store.appendingPathComponent("\(name).kanban", isDirectory: true)
try FileManager.default.createDirectory(at: store, withIntermediateDirectories: true)
}
func tearDown() {
try? FileManager.default.removeItem(at: store)
}
/// The fixture's own board, with one lane so it is a board worth previewing.
func board(title: String = "Fixture", icon: String? = nil, extraKeys: String = "") throws {
try write(board: boardURL, title: title, icon: icon, extraKeys: extraKeys)
}
/// A second board in the same store — the sorting tests need several.
func freeStandingBoard(named name: String, title: String, extraKeys: String = "") throws -> BoardTemplate {
let url = store.appendingPathComponent("\(name).kanban", isDirectory: true)
try write(board: url, title: title, icon: nil, extraKeys: extraKeys)
return try loadTemplate(at: url)
}
func template() throws -> BoardTemplate {
try loadTemplate(at: boardURL)
}
private func loadTemplate(at url: URL) throws -> BoardTemplate {
switch TemplateEngine.load(templateAt: url, origin: .user) {
case let .success(template): return template
case let .failure(error): throw error
}
}
private func write(board url: URL, title: String, icon: String?, extraKeys: String) throws {
let lane = url.appendingPathComponent("11111111-1111-4111-8111-111111111111", isDirectory: true)
try FileManager.default.createDirectory(at: lane, withIntermediateDirectories: true)
var keys = ["schema: 1", "title: \(title)"]
if let icon { keys.append("icon: \(icon)") }
if !extraKeys.isEmpty { keys.append(extraKeys) }
try Data("---\n\(keys.joined(separator: "\n"))\n---\nBlurb.\n".utf8)
.write(to: url.appendingPathComponent("index.md"))
try Data("---\nschema: 1\ntitle: Lane\norder: 1024\n---\n".utf8)
.write(to: lane.appendingPathComponent("index.md"))
}
}