mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 18:44:45 +00:00
adcc984091
Lifts Scarf's Kanban surface from the v2.6 read-only list to a drag-and-drop board with the complete Hermes v0.12 mutation surface wired up, plus per-project boards bound to a Scarf-minted tenant slug and a read-only board on iOS. Why now: the v2.6 list was a placeholder shipped while upstream Kanban collab was still mid-rework. v0.12 stabilized the 27-verb CLI; this release makes Scarf a real GUI client for it. Driving real tasks end-to-end exposed and closed a connected bug pattern (claim vs dispatch, silent skipped_unassigned, integer-vs-ISO timestamps, parser-leaked "(no" sentinel) that would have shipped as latent UX papercuts otherwise. ScarfCore: KanbanService actor (Sendable, pure I/O) wrapping every verb; KanbanTenantReader cross-platform manifest projection; eight new model types (TaskDetail, Comment, Event, Run, Stats, Assignee, CreateRequest, Filters); KanbanError; pure transition planner that maps drag-drop column changes to verb sequences, tested against canonical Hermes JSON fixtures. Mac: KanbanBoardView orchestrator with five-column drag-drop layout, optimistic-merge state, KanbanInspectorPane side-pane (Comments / Events / Runs / Log tabs, Log streams worker stdout every 2s while running), inline assignee picker, health banner for unassigned and last-failed-run states. New Task sheet defaults to active profile and auto-fires kanban dispatch on submit. Sidebar moved Kanban from Manage to Monitor. Read-only KanbanListView preserved as Board|List toggle for narrow windows / accessibility. Per-project: DashboardTab.kanban tab on every project gated on hasKanban; KanbanTenantResolver mints scarf:<slug> tenants on first interaction and persists to .scarf/manifest.json (immutable across rename); ProjectAgentContextService surfaces the tenant in the AGENTS.md scarf-managed block so agents pass --tenant <slug> on kanban create. New kanban_summary dashboard widget; vocabulary mirrored in tools/widget-schema.json and site/widgets.js. iOS: read-only board on the project tab via paged single-column Picker, modal detail sheet with Comments / Events / Runs. Mutations + drag-drop deferred to v2.8. Tests: 19 new pure-logic tests covering decoding, planner verb mapping, argv assembly, glance string formatting, and parser rejection of the kanban assignees empty-state sentinel. All 348 ScarfCore tests pass. Constraints documented in CLAUDE.md: no within-column reorder (Hermes has no update --priority verb); no live watch streaming yet (5s polling for board, 2s for log); no bulk re-tag for legacy NULL-tenant tasks. Pre-v0.12 Hermes hosts gracefully hide the surface end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
48 lines
1.7 KiB
Swift
48 lines
1.7 KiB
Swift
import Testing
|
|
import Foundation
|
|
import ScarfCore
|
|
@testable import scarf
|
|
|
|
/// Pure slug-generation tests for `KanbanTenantResolver`. The disk
|
|
/// I/O paths (`resolveOrMint`, `persist`) need a real `ServerContext`
|
|
/// + filesystem and are covered by integration tests.
|
|
@Suite struct KanbanTenantResolverSlugTests {
|
|
|
|
@Test func basicNameSlugifiesCleanly() {
|
|
#expect(KanbanTenantResolver.makeSlug(for: "My Project") == "scarf:my-project")
|
|
}
|
|
|
|
@Test func punctuationCollapsesToHyphens() {
|
|
#expect(KanbanTenantResolver.makeSlug(for: "Foo: Bar / Baz!") == "scarf:foo-bar-baz")
|
|
}
|
|
|
|
@Test func consecutiveSeparatorsCollapse() {
|
|
#expect(KanbanTenantResolver.makeSlug(for: "a b___c") == "scarf:a-b-c")
|
|
}
|
|
|
|
@Test func emptyNameFallsBackToProjectLiteral() {
|
|
#expect(KanbanTenantResolver.makeSlug(for: "!@#") == "scarf:project")
|
|
}
|
|
|
|
@Test func slugBoundedTo48CharsAfterPrefix() {
|
|
let huge = String(repeating: "x", count: 200)
|
|
let slug = KanbanTenantResolver.makeSlug(for: huge)
|
|
#expect(slug.hasPrefix("scarf:"))
|
|
// 6 chars for "scarf:" + ≤48 for the slug body
|
|
#expect(slug.count <= 6 + 48)
|
|
}
|
|
|
|
@Test func unicodeNormalizesToAscii() {
|
|
// The slug rule lowercases and replaces non-letter/digit with
|
|
// hyphens; Latin-extended letters survive lowercase but accented
|
|
// chars route through Foundation's lowercasing path.
|
|
let slug = KanbanTenantResolver.makeSlug(for: "Mañana")
|
|
#expect(slug.hasPrefix("scarf:"))
|
|
#expect(!slug.contains(" "))
|
|
}
|
|
|
|
@Test func prefixIsStable() {
|
|
#expect(KanbanTenantResolver.prefix == "scarf:")
|
|
}
|
|
}
|