Pendant zur Web-Route /rueckblick. Synthetisiert, was man WIRKLICH gelesen hat (status=finished oder readingProgress>=0.5 im Fenster). - Core/Domain/Digest.swift: reine, getestete Logik — Swift-Spiegel von pageta-domain/digest.ts (Auswahl nach Zeitfenster + Lese-Signal, Theme-Gruppierung nach Tags, Prompt-Builder). 8 XCTest-Fälle, grün. - Features/RueckblickTabView.swift: deterministisches Gerüst steht IMMER (gratis, offline) — Kennzahlen, Themencluster, eigene Markierungen wörtlich. Darüber optionale Synthese via ManaLLM.generate (lokal-first; liefert sie nichts, bleibt das Gerüst nutzbar). Tag/Woche/Gesamt-Picker, Markdown-ShareLink, Tap → ArticleDetailView. - RootView: neuer Tab „Rückblick" (iOS TabView + macOS Sidebar), Icon clock.arrow.circlepath, nach Markierungen. - Motivations-Leitplanke (Wert 8): ruhiger Spiegel, keine Streaks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
123 lines
4.1 KiB
Swift
123 lines
4.1 KiB
Swift
import XCTest
|
|
@testable import PagetaNative
|
|
|
|
final class DigestTests: XCTestCase {
|
|
// Fester Bezugspunkt, damit die rollenden Fenster deterministisch sind.
|
|
private let now = ISO8601DateFormatter().date(from: "2026-06-08T12:00:00Z")!
|
|
|
|
private func iso(_ offsetDays: Double) -> String {
|
|
let d = now.addingTimeInterval(-offsetDays * 86_400)
|
|
let f = ISO8601DateFormatter()
|
|
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
return f.string(from: d)
|
|
}
|
|
|
|
private func makeArticle(
|
|
id: String = "a1",
|
|
status: ArticleStatus = .finished,
|
|
readingProgress: Double = 1,
|
|
readAtDaysAgo: Double = 0,
|
|
tags: [String] = []
|
|
) -> Article {
|
|
Article(
|
|
id: id,
|
|
userId: "u1",
|
|
originalUrl: "https://example.com/\(id)",
|
|
title: "Titel \(id)",
|
|
excerpt: nil,
|
|
content: "",
|
|
htmlContent: nil,
|
|
author: nil,
|
|
siteName: "Example",
|
|
imageUrl: nil,
|
|
wordCount: nil,
|
|
readingTimeMinutes: nil,
|
|
publishedAt: nil,
|
|
status: status,
|
|
readingProgress: readingProgress,
|
|
isFavorite: false,
|
|
savedAt: iso(readAtDaysAgo),
|
|
readAt: iso(readAtDaysAgo),
|
|
userNote: nil,
|
|
extractedVersion: 1,
|
|
createdAt: iso(readAtDaysAgo),
|
|
updatedAt: iso(readAtDaysAgo),
|
|
warning: nil,
|
|
tags: tags
|
|
)
|
|
}
|
|
|
|
private func makeBrief(id: String = "a1", tags: [String] = []) -> DigestBrief {
|
|
DigestBrief(id: id, title: "Titel \(id)", source: "Example", url: id, brief: "Kurztext.", tags: tags)
|
|
}
|
|
|
|
func testWindowStart() {
|
|
XCTAssertEqual(Digest.windowStart(scope: .day, now: now), now.addingTimeInterval(-86_400))
|
|
XCTAssertEqual(Digest.windowStart(scope: .week, now: now), now.addingTimeInterval(-7 * 86_400))
|
|
XCTAssertNil(Digest.windowStart(scope: .all, now: now))
|
|
}
|
|
|
|
func testSelectFinishedInWindow() {
|
|
let recent = makeArticle(id: "recent", readAtDaysAgo: 2)
|
|
let old = makeArticle(id: "old", readAtDaysAgo: 30)
|
|
let picked = Digest.selectArticles([recent, old], scope: .week, now: now)
|
|
XCTAssertEqual(picked.map(\.id), ["recent"])
|
|
}
|
|
|
|
func testSelectSubstantiallyReadButNotBarely() {
|
|
let half = makeArticle(id: "half", status: .reading, readingProgress: 0.6)
|
|
let barely = makeArticle(id: "barely", status: .reading, readingProgress: 0.1)
|
|
let picked = Digest.selectArticles([half, barely], scope: .week, now: now)
|
|
XCTAssertEqual(picked.map(\.id), ["half"])
|
|
}
|
|
|
|
func testSelectExcludesArchivedAndSortsNewestFirst() {
|
|
let archived = makeArticle(id: "arch", status: .archived)
|
|
let older = makeArticle(id: "older", readAtDaysAgo: 3)
|
|
let newer = makeArticle(id: "newer", readAtDaysAgo: 1)
|
|
let picked = Digest.selectArticles([archived, older, newer], scope: .week, now: now)
|
|
XCTAssertEqual(picked.map(\.id), ["newer", "older"])
|
|
}
|
|
|
|
func testAllScopeIgnoresWindow() {
|
|
let ancient = makeArticle(id: "ancient", readAtDaysAgo: 400)
|
|
let picked = Digest.selectArticles([ancient], scope: .all, now: now)
|
|
XCTAssertEqual(picked.map(\.id), ["ancient"])
|
|
}
|
|
|
|
func testGroupByThemePrimaryTagUntaggedLast() {
|
|
let groups = Digest.groupByTheme([
|
|
makeBrief(id: "1", tags: ["klima"]),
|
|
makeBrief(id: "2", tags: ["klima", "politik"]),
|
|
makeBrief(id: "3", tags: []),
|
|
makeBrief(id: "4", tags: ["politik"]),
|
|
])
|
|
XCTAssertEqual(groups.first?.theme, "klima")
|
|
XCTAssertEqual(groups.first?.articles.map(\.id), ["1", "2"])
|
|
XCTAssertEqual(groups.last?.theme, Digest.untaggedTheme)
|
|
}
|
|
|
|
func testBuildSynthesisPromptContainsTitlesAndHighlights() {
|
|
let out = Digest.buildSynthesisPrompt(
|
|
scope: .week,
|
|
briefs: [makeBrief(id: "ki", tags: ["ki"])],
|
|
highlights: [
|
|
DigestHighlightLine(
|
|
articleId: "ki", source: "Example", title: "Titel ki",
|
|
text: "wichtiger Satz", note: "merken"
|
|
),
|
|
]
|
|
)
|
|
XCTAssertTrue(out.prompt.contains("Titel ki"))
|
|
XCTAssertTrue(out.prompt.contains("wichtiger Satz"))
|
|
XCTAssertTrue(out.prompt.contains("## Überblick"))
|
|
XCTAssertEqual(out.usedArticles, 1)
|
|
XCTAssertEqual(out.usedHighlights, 1)
|
|
}
|
|
|
|
func testBuildSynthesisPromptTruncates() {
|
|
let briefs = (0..<100).map { makeBrief(id: "b\($0)") }
|
|
let out = Digest.buildSynthesisPrompt(scope: .all, briefs: briefs, highlights: [], maxArticles: 40)
|
|
XCTAssertEqual(out.usedArticles, 40)
|
|
}
|
|
}
|