Drei neue Module nach dem Bauplan von `ShelfProvider`: reine Regeln getrennt vom Zustand, versionierte Indexdatei, Inhalte in Dateien statt in `UserDefaults` — was dort steht, liest jedes Programm mit `defaults read` mit. Die Zwischenablage achtet `ConcealedType`, `TransientType` und `AutoGeneratedType`. Diese Marken sind eine Verabredung zwischen Programmen und kein Systemdienst; wer sie missachtet, sammelt fremde Passwörter ein. Der Gesichtsvergleich stand im Plan als der eine echte Unsicherheitsfaktor. Ein Spike hat ihn erledigt: `VNGenerateFaceprintRequest` gibt es nicht, aber `VNGenerateImageFeaturePrintRequest` ist öffentlich, liefert 768 Elemente und ein Abstandsmaß — Faktor 3,5 Trennschärfe. Kein eigenes CoreML-Modell nötig. `FaceMatcher` entscheidet über mehrere Posen, mehrere Einzelbilder und eine Sperre nach Fehlversuchen. Die Schwelle liegt bewusst nicht mittig: ein falsches Ja ist ein Fremder am entsperrten Rechner, ein falsches Nein nur ein Passwort, das man ohnehin kennt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
84 lines
3.0 KiB
Swift
84 lines
3.0 KiB
Swift
import Testing
|
|
import Foundation
|
|
@testable import NotesProvider
|
|
|
|
@Suite("Notizen: die Regeln")
|
|
struct NoteRulesTests {
|
|
|
|
@Test("Die Überschrift ist die erste nichtleere Zeile")
|
|
func titleIsFirstLine() {
|
|
let note = Note(text: "\n\n Einkaufen \nMilch\nBrot")
|
|
#expect(NoteRules.title(of: note) == "Einkaufen")
|
|
}
|
|
|
|
@Test("Eine zu lange erste Zeile wird gekürzt")
|
|
func longTitleIsTruncated() {
|
|
let note = Note(text: String(repeating: "a", count: 200))
|
|
let title = NoteRules.title(of: note)
|
|
#expect(title.count == NoteRules.titleLength + 1)
|
|
#expect(title.hasSuffix("…"))
|
|
}
|
|
|
|
@Test("Eine leere Notiz hat keine Überschrift")
|
|
func emptyHasNoTitle() {
|
|
#expect(NoteRules.title(of: Note(text: " \n\n ")) == "")
|
|
}
|
|
|
|
@Test("Eine Notiz aus lauter Leerzeichen zählt als leer")
|
|
func whitespaceIsEmpty() {
|
|
// Sie entsteht, wenn man auf „Neu" drückt und es sich anders überlegt.
|
|
#expect(NoteRules.isEmpty(Note(text: " \n\t ")))
|
|
#expect(!NoteRules.isEmpty(Note(text: "x")))
|
|
}
|
|
|
|
@Test("Eine leer gewordene Notiz verschwindet beim Speichern")
|
|
func emptyNoteIsDropped() {
|
|
let note = Note(text: "war mal etwas")
|
|
let kept = NoteRules.merge([], updating: note)
|
|
#expect(kept.count == 1)
|
|
|
|
var emptied = note
|
|
emptied.text = " "
|
|
#expect(NoteRules.merge(kept, updating: emptied).isEmpty)
|
|
}
|
|
|
|
@Test("Zuletzt geändert steht oben")
|
|
func newestFirst() {
|
|
let old = Note(text: "alt", updatedAt: Date(timeIntervalSince1970: 1))
|
|
let new = Note(text: "neu", updatedAt: Date(timeIntervalSince1970: 2))
|
|
#expect(NoteRules.sorted([old, new]).first?.text == "neu")
|
|
}
|
|
|
|
@Test("Gleich alte Notizen behalten ihre Reihenfolge")
|
|
func sortingIsStable() {
|
|
let when = Date(timeIntervalSince1970: 5)
|
|
let a = Note(text: "a", updatedAt: when)
|
|
let b = Note(text: "b", updatedAt: when)
|
|
#expect(NoteRules.sorted([a, b]).map(\.id) == NoteRules.sorted([b, a]).map(\.id))
|
|
}
|
|
|
|
@Test("Eine bestehende Notiz wird ersetzt, nicht verdoppelt")
|
|
func updateReplaces() {
|
|
let note = Note(text: "erst")
|
|
var changed = note
|
|
changed.text = "dann"
|
|
let result = NoteRules.merge([note], updating: changed)
|
|
#expect(result.count == 1)
|
|
#expect(result.first?.text == "dann")
|
|
}
|
|
|
|
@Test("Über der Grenze fällt die älteste heraus")
|
|
func limitDropsOldest() {
|
|
let existing = (0..<3).map {
|
|
Note(text: "n\($0)", updatedAt: Date(timeIntervalSince1970: Double($0)))
|
|
}
|
|
let result = NoteRules.merge(existing,
|
|
updating: Note(text: "neu",
|
|
updatedAt: Date(timeIntervalSince1970: 99)),
|
|
limit: 3)
|
|
#expect(result.count == 3)
|
|
#expect(result.first?.text == "neu")
|
|
#expect(!result.contains { $0.text == "n0" })
|
|
}
|
|
}
|