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>
144 lines
5.9 KiB
Swift
144 lines
5.9 KiB
Swift
import Testing
|
|
import Foundation
|
|
@testable import ClipboardProvider
|
|
|
|
@Suite("Zwischenablage: die Regeln")
|
|
struct ClipboardRulesTests {
|
|
|
|
private func item(_ fingerprint: String, at seconds: TimeInterval = 0,
|
|
preview: String = "x") -> ClipboardItem {
|
|
ClipboardItem(kind: .text, preview: preview,
|
|
addedAt: Date(timeIntervalSince1970: seconds),
|
|
sourceBundleID: nil, fingerprint: fingerprint)
|
|
}
|
|
|
|
// MARK: - Was nie gemerkt werden darf
|
|
|
|
@Test("Ein als verborgen markierter Inhalt wird nicht gemerkt")
|
|
func concealedIsIgnored() {
|
|
// Passwortmanager setzen diese Marke. Sie zu missachten hieße, fremde
|
|
// Passwörter einzusammeln.
|
|
#expect(!ClipboardRules.shouldRecord(
|
|
types: ["public.utf8-plain-text", "org.nspasteboard.ConcealedType"],
|
|
sourceBundleID: nil, excludedApps: []))
|
|
}
|
|
|
|
@Test("Durchreichen und Selbsterzeugtes ebenso wenig")
|
|
func transientAndGeneratedAreIgnored() {
|
|
#expect(!ClipboardRules.shouldRecord(types: ["org.nspasteboard.TransientType"],
|
|
sourceBundleID: nil, excludedApps: []))
|
|
#expect(!ClipboardRules.shouldRecord(types: ["org.nspasteboard.AutoGeneratedType"],
|
|
sourceBundleID: nil, excludedApps: []))
|
|
}
|
|
|
|
@Test("Gewöhnlicher Text schon")
|
|
func plainTextIsRecorded() {
|
|
#expect(ClipboardRules.shouldRecord(types: ["public.utf8-plain-text"],
|
|
sourceBundleID: "com.apple.TextEdit",
|
|
excludedApps: []))
|
|
}
|
|
|
|
@Test("Aus einem ausgeschlossenen Programm nicht")
|
|
func excludedAppIsIgnored() {
|
|
#expect(!ClipboardRules.shouldRecord(types: ["public.utf8-plain-text"],
|
|
sourceBundleID: "com.agilebits.onepassword7",
|
|
excludedApps: ["com.agilebits.onepassword7"]))
|
|
}
|
|
|
|
@Test("Ohne bekanntes Herkunftsprogramm wird gemerkt")
|
|
func unknownSourceIsRecorded() {
|
|
// Sonst ginge alles verloren, was aus einem Hintergrunddienst kommt.
|
|
#expect(ClipboardRules.shouldRecord(types: ["public.utf8-plain-text"],
|
|
sourceBundleID: nil,
|
|
excludedApps: ["com.agilebits.onepassword7"]))
|
|
}
|
|
|
|
// MARK: - Vorschau
|
|
|
|
@Test("Zeilenumbrüche werden zu Leerzeichen")
|
|
func previewIsOneLine() {
|
|
#expect(ClipboardRules.preview(for: "erste\nzweite") == "erste zweite")
|
|
#expect(ClipboardRules.preview(for: "a\t\tb") == "a b")
|
|
}
|
|
|
|
@Test("Zu langer Text wird gekürzt und bekommt Auslassungspunkte")
|
|
func previewIsTruncated() {
|
|
let long = String(repeating: "a", count: 500)
|
|
let preview = ClipboardRules.preview(for: long)
|
|
#expect(preview.count == ClipboardRules.previewLength + 1)
|
|
#expect(preview.hasSuffix("…"))
|
|
}
|
|
|
|
@Test("Kurzer Text bleibt unangetastet")
|
|
func shortPreviewIsUnchanged() {
|
|
#expect(ClipboardRules.preview(for: "kurz") == "kurz")
|
|
}
|
|
|
|
// MARK: - Aufnehmen
|
|
|
|
@Test("Dasselbe noch einmal kopiert rutscht nach oben, statt sich anzureihen")
|
|
func duplicateMovesToFront() {
|
|
let existing = [item("a", at: 10), item("b", at: 20)]
|
|
let again = item("a", at: 30)
|
|
let result = ClipboardRules.merge(existing, adding: again)
|
|
|
|
#expect(result.items.map(\.fingerprint) == ["a", "b"])
|
|
#expect(result.items.first?.addedAt == Date(timeIntervalSince1970: 30))
|
|
// Der alte Eintrag ist verdrängt — seine Datei muss weg.
|
|
#expect(result.dropped.count == 1)
|
|
}
|
|
|
|
@Test("Über der Grenze fällt das Älteste heraus")
|
|
func limitDropsOldest() {
|
|
let existing = (0..<5).map { item("f\($0)", at: Double(5 - $0)) }
|
|
let result = ClipboardRules.merge(existing, adding: item("neu", at: 99), limit: 3)
|
|
|
|
#expect(result.items.count == 3)
|
|
#expect(result.items.first?.fingerprint == "neu")
|
|
// Verdrängtes wird gemeldet, damit die Dateien nicht liegen bleiben.
|
|
#expect(result.dropped.count == 3)
|
|
}
|
|
|
|
@Test("Verdrängtes wird vollständig gemeldet, auch die Wiederholung")
|
|
func droppedIncludesDuplicate() {
|
|
let existing = [item("a", at: 1), item("b", at: 2), item("c", at: 3)]
|
|
let result = ClipboardRules.merge(existing, adding: item("a", at: 4), limit: 2)
|
|
// „a" einmal als Wiederholung, „c" als das Älteste über der Grenze.
|
|
#expect(Set(result.dropped.map(\.fingerprint)) == ["a", "c"])
|
|
}
|
|
|
|
// MARK: - Verfall
|
|
|
|
@Test("Ohne Frist verfällt nichts")
|
|
func noExpiryKeepsEverything() {
|
|
let old = [item("a", at: 0)]
|
|
#expect(ClipboardRules.expired(old, olderThanDays: nil,
|
|
now: Date(timeIntervalSince1970: 9_000_000)).isEmpty)
|
|
#expect(ClipboardRules.expired(old, olderThanDays: 0,
|
|
now: Date(timeIntervalSince1970: 9_000_000)).isEmpty)
|
|
}
|
|
|
|
@Test("Was älter ist als die Frist, verfällt")
|
|
func oldItemsExpire() {
|
|
let now = Date(timeIntervalSince1970: 10 * 86_400)
|
|
let items = [item("alt", at: 0), item("neu", at: 9 * 86_400)]
|
|
let stale = ClipboardRules.expired(items, olderThanDays: 7, now: now)
|
|
#expect(stale.map(\.fingerprint) == ["alt"])
|
|
}
|
|
|
|
// MARK: - Reihenfolge
|
|
|
|
@Test("Neuestes zuerst, und gleich alte in fester Reihenfolge")
|
|
func sortingIsStable() {
|
|
let a = item("a", at: 5)
|
|
let b = item("b", at: 5)
|
|
let c = item("c", at: 9)
|
|
let once = ClipboardRules.sorted([a, b, c])
|
|
let twice = ClipboardRules.sorted([c, b, a])
|
|
#expect(once.first?.fingerprint == "c")
|
|
// Ohne festen Zweitschlüssel tauschten gleich alte Einträge bei jeder
|
|
// Anzeige die Plätze.
|
|
#expect(once.map(\.id) == twice.map(\.id))
|
|
}
|
|
}
|