Calendarr liefert Termine über den Gruppencontainer. Streng lesend: keine eigene Anmeldung, kein Netz, kein Schreiben zurück. Die Abdeckung ist der Kern und kein Detail. Der Schnappschuss reicht 49 Tage nach vorne, und das steht nirgends in der Datei — es ist zugesagtes Verhalten des Schreibers. Außerhalb liegt keine Information vor, nicht keine Termine; das Monatsraster graut solche Tage aus, statt „frei" zu behaupten. Verlockend wäre, die Abdeckung aus dem frühesten Termin abzuleiten. Das ist falsch, und ein Test hält es fest: mehrtägige Termine ragen ins Fenster hinein und werden mitgeschrieben, obwohl ihr Anfang nicht abgedeckt ist. Bei „beide Quellen" ist die Abdeckung der Schnitt, nicht die Vereinigung. Ein Tag gilt nur als bekannt, wenn ihn jede Quelle abdeckt. Umgekehrt darf ein Ausfall einer Quelle die andere nicht mitreißen: Calendarr nie geöffnet zu haben knipst den Apple-Kalender nicht aus. Die Ablage kopiert, statt zu verweisen. Anhänge aus Mail oder WhatsApp liegen in temporären Verzeichnissen — ein Verweis wäre in Stunden tot, und die Ablage wäre leer, wenn man sie braucht. Aus demselben Grund nimmt die Fläche UTType.item statt public.file-url an: solche Anhänge kommen als Dateiversprechen, und loadFileRepresentation löst beides ein. Die übergebene Datei existiert nur innerhalb des Rückrufs, also wird sie dort sofort gesichert statt später auf dem MainActor. Gelöscht wird in den Papierkorb. Automatisches Aufräumen ist standardmäßig aus — eine Ablage, die ungefragt löscht, ist ein Papierkorb mit Zeitschaltuhr. Nebenbei: „mixer.on" und „network.vpn.active" standen als rohe Schlüssel in der Oberfläche. Label(_:systemImage:) kennt kein Bundle und sucht immer im Hauptbundle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
113 lines
4.2 KiB
Swift
113 lines
4.2 KiB
Swift
import Testing
|
|
import Foundation
|
|
@testable import ShelfProvider
|
|
|
|
private let day: TimeInterval = 86_400
|
|
private let now = Date(timeIntervalSince1970: 1_770_000_000)
|
|
|
|
private func item(_ name: String, age: TimeInterval = 0) -> ShelfItem {
|
|
ShelfItem(url: URL(fileURLWithPath: "/tmp/\(name)"), name: name,
|
|
size: 1, addedAt: now - age)
|
|
}
|
|
|
|
@Suite("Namensvergabe in der Ablage")
|
|
struct ShelfNamingTests {
|
|
|
|
@Test("Ein freier Name bleibt, wie er ist")
|
|
func freeNameIsKept() {
|
|
#expect(ShelfNaming.uniqueName(for: "Vertrag.pdf", taken: []) == "Vertrag.pdf")
|
|
}
|
|
|
|
@Test("Ein belegter Name wird durchnummeriert")
|
|
func takenNameIsNumbered() {
|
|
// Zwei Dateien gleichen Namens sind der Normalfall, nicht die Ausnahme:
|
|
// „IMG_0001.jpg" aus zwei Chats. Überschreiben wäre Datenverlust.
|
|
#expect(ShelfNaming.uniqueName(for: "Vertrag.pdf", taken: ["Vertrag.pdf"])
|
|
== "Vertrag 2.pdf")
|
|
}
|
|
|
|
@Test("Die Nummer zählt weiter, bis etwas frei ist")
|
|
func numberingContinues() {
|
|
let taken: Set<String> = ["Vertrag.pdf", "Vertrag 2.pdf", "Vertrag 3.pdf"]
|
|
#expect(ShelfNaming.uniqueName(for: "Vertrag.pdf", taken: taken) == "Vertrag 4.pdf")
|
|
}
|
|
|
|
@Test("Ohne Endung wird trotzdem nummeriert")
|
|
func noExtension() {
|
|
#expect(ShelfNaming.uniqueName(for: "README", taken: ["README"]) == "README 2")
|
|
}
|
|
|
|
@Test("Ein führender Punkt ist keine Endung")
|
|
func leadingDotIsNotAnExtension() {
|
|
// Sonst würde aus „.gitignore" ein „ 2.gitignore" mit führendem
|
|
// Leerzeichen — ein Name, der nirgends gut aussieht.
|
|
#expect(ShelfNaming.uniqueName(for: ".gitignore", taken: [".gitignore"])
|
|
== ".gitignore 2")
|
|
}
|
|
|
|
@Test("Mehrfache Endungen bleiben zusammen")
|
|
func doubleExtension() {
|
|
// `.tar.gz` als `archiv.tar 2.gz` zu zerlegen wäre technisch möglich
|
|
// und praktisch verwirrend. Nur die letzte Endung zählt.
|
|
#expect(ShelfNaming.uniqueName(for: "archiv.tar.gz", taken: ["archiv.tar.gz"])
|
|
== "archiv.tar 2.gz")
|
|
}
|
|
|
|
@Test("Ein leerer Name bekommt einen Ersatz")
|
|
func emptyNameGetsFallback() {
|
|
#expect(!ShelfNaming.uniqueName(for: "", taken: []).isEmpty)
|
|
}
|
|
}
|
|
|
|
@Suite("Automatisches Aufräumen")
|
|
struct ShelfCleanupTests {
|
|
|
|
@Test("Nichts läuft ab, wenn Aufräumen aus ist")
|
|
func disabledKeepsEverything() {
|
|
// Standard ist aus. Eine Ablage, die ungefragt löscht, ist keine
|
|
// Ablage, sondern ein Papierkorb mit Zeitschaltuhr.
|
|
let items = [item("alt", age: 400 * day)]
|
|
#expect(ShelfCleanup.expired(items, now: now, afterDays: nil).isEmpty)
|
|
}
|
|
|
|
@Test("Was älter als die Frist ist, läuft ab")
|
|
func olderThanLimitExpires() {
|
|
let items = [item("alt", age: 31 * day), item("neu", age: 2 * day)]
|
|
let expired = ShelfCleanup.expired(items, now: now, afterDays: 30)
|
|
#expect(expired.map(\.name) == ["alt"])
|
|
}
|
|
|
|
@Test("Genau auf der Frist bleibt es liegen")
|
|
func exactlyAtLimitStays() {
|
|
// Aufgerundet zugunsten des Nutzers: löschen ist unwiderruflich,
|
|
// einen Tag länger liegen zu lassen ist es nicht.
|
|
#expect(ShelfCleanup.expired([item("grenze", age: 30 * day)],
|
|
now: now, afterDays: 30).isEmpty)
|
|
}
|
|
|
|
@Test("Eine Frist von null löscht nicht sofort alles")
|
|
func zeroDaysIsTreatedAsOff() {
|
|
// Sonst wäre ein Fehlgriff im Einstellungsfeld der Verlust der Ablage.
|
|
#expect(ShelfCleanup.expired([item("neu")], now: now, afterDays: 0).isEmpty)
|
|
}
|
|
}
|
|
|
|
@Suite("Reihenfolge in der Ablage")
|
|
struct ShelfOrderTests {
|
|
|
|
@Test("Das Neueste liegt oben")
|
|
func newestFirst() {
|
|
let items = [item("alt", age: 3 * day), item("neu", age: 1 * day),
|
|
item("mittel", age: 2 * day)]
|
|
#expect(ShelfItem.sorted(items).map(\.name) == ["neu", "mittel", "alt"])
|
|
}
|
|
|
|
@Test("Gleich alte Dateien stehen stabil nach Namen")
|
|
func tiesBreakByName() {
|
|
// Ohne festen Zweitschlüssel springt die Liste bei jeder Aktualisierung
|
|
// umher, weil das Dateisystem keine Reihenfolge zusagt.
|
|
let items = [item("b"), item("a")]
|
|
#expect(ShelfItem.sorted(items).map(\.name) == ["a", "b"])
|
|
}
|
|
}
|