import Foundation import Testing @testable import OrgSocialKit private let relay = URL(string: "https://relay.org-social.org")! // Known post from shom with 2+ direct replies and nested children (verified live) private let postWithReplies = "https://shom.dev/social.org#2025-09-06T22:36:20-0500" private let postWithNested = "https://shom.dev/social.org#2025-09-06T22:36:20-0500" // Root resolution test fixtures (verified live against relay): // shom is root → adsan replied to shom → andros replied to adsan (2-deep chain) private let rootPost = "https://shom.dev/social.org#2025-09-06T22:36:20-0500" // adsan's reply to shom (1-deep, uses +02:00 timezone in URL) private let reply1Deep = "https://adsan.dev/social.org#2025-09-07T15:45:33+02:00" // andros's reply to adsan (2-deep cross-domain) private let reply2Deep = "https://host.org-social.org/andros/social.org#2025-09-07T16:27:11+0200" // adsan post that is both a reply (to andros.dev) and has its own reply (middle of chain) private let replyMiddle = "https://adsan.dev/social.org#2025-10-12T20:24:12+02:00" private let replyMiddleRoot = "https://andros.dev/static/social.org#2025-10-12T11:30:47+0200" @Suite("ThreadClient – interactions", .serialized) struct ThreadClientInteractionTests { let client = ThreadClient() @Test("returns interactions for post with known replies") func fetchInteractions() async throws { let interactions = try await client.fetchInteractions(for: postWithReplies, from: relay) #expect(interactions.replyCount >= 2) } @Test("hasActivity is true when there are replies") func hasActivity() async throws { let interactions = try await client.fetchInteractions(for: postWithReplies, from: relay) #expect(interactions.hasActivity) } @Test("replyURLs are non-empty strings") func replyURLsValid() async throws { let interactions = try await client.fetchInteractions(for: postWithReplies, from: relay) #expect(!interactions.replyURLs.isEmpty) for url in interactions.replyURLs { #expect(!url.isEmpty) } } @Test("fetchInteractions returns valid struct for any known post") func interactionsStructureValid() async throws { let interactions = try await client.fetchInteractions(for: postWithReplies, from: relay) #expect(interactions.replyCount >= 0) #expect(interactions.reactionCount >= 0) #expect(interactions.boostCount >= 0) } } @Suite("ThreadClient – raw thread", .serialized) struct ThreadClientRawTests { let client = ThreadClient() @Test("raw thread parentURL matches requested post") func parentURLMatches() async throws { let raw = try await client.fetchRawThread(for: postWithReplies, from: relay) #expect(raw.parentURL == postWithReplies) } @Test("raw thread has expected reply count") func replyCount() async throws { let raw = try await client.fetchRawThread(for: postWithReplies, from: relay) #expect(raw.replies.count >= 2) } @Test("nested thread has children") func nestedChildren() async throws { let raw = try await client.fetchRawThread(for: postWithNested, from: relay) let hasChildren = raw.replies.contains { !$0.children.isEmpty } #expect(hasChildren) } @Test("all reply post URLs contain # separator") func replyURLsHaveHash() async throws { let raw = try await client.fetchRawThread(for: postWithReplies, from: relay) for node in raw.replies { #expect(node.postURL.contains("#")) } } } @Suite("ThreadFetcher – full resolution", .serialized) struct ThreadFetcherTests { let fetcher = ThreadFetcher() @Test("resolved focal post timestamp matches requested post") func focalPostTimestamp() async throws { let thread = try await fetcher.fetchThread(for: postWithReplies, from: relay) let expectedTimestamp = postWithReplies.components(separatedBy: "#").last ?? "" #expect(thread.focalPost.timestamp == expectedTimestamp) } @Test("thread has at least two resolved replies") func repliesCount() async throws { let thread = try await fetcher.fetchThread(for: postWithReplies, from: relay) #expect(thread.replyCount >= 2) } @Test("all resolved reply posts have non-empty timestamps") func replyTimestampsNonEmpty() async throws { let thread = try await fetcher.fetchThread(for: postWithReplies, from: relay) func check(_ nodes: [OrgSocialThreadNode]) { for node in nodes { #expect(!node.post.timestamp.isEmpty) check(node.children) } } check(thread.replies) } @Test("nested thread resolves children") func nestedResolution() async throws { let thread = try await fetcher.fetchThread(for: postWithNested, from: relay) let hasChildren = thread.replies.contains { !$0.children.isEmpty } #expect(hasChildren) } @Test("replyCount helper counts all depths") func replyCountHelper() async throws { let thread = try await fetcher.fetchThread(for: postWithNested, from: relay) #expect(thread.replyCount >= 1) } @Test("parent chain is ordered oldest-first (root at index 0)") func parentChainOrderedOldestFirst() async throws { // Opening a 2-deep reply: parentChain should be [shom, adsan] (root first) let thread = try await fetcher.fetchThread(for: reply2Deep, from: relay) #expect(thread.parentChain.count >= 2) // shom (Sept 6) is older than adsan (Sept 7) → shom must come first let firstAncestorTimestamp = thread.parentChain.first?.timestamp ?? "" let lastAncestorTimestamp = thread.parentChain.last?.timestamp ?? "" #expect(firstAncestorTimestamp < lastAncestorTimestamp) } } @Suite("ThreadClient – root resolution", .serialized) struct ThreadClientRootTests { let client = ThreadClient() @Test("root post returns its own URL") func rootPostReturnsItself() async throws { let root = try await client.fetchRootPostURL(for: rootPost, from: relay) #expect(root == rootPost) } @Test("1-deep reply returns the root URL") func reply1DeepReturnsRoot() async throws { let root = try await client.fetchRootPostURL(for: reply1Deep, from: relay) #expect(root == rootPost) } @Test("2-deep reply returns the root URL (cross-domain)") func reply2DeepReturnsRoot() async throws { let root = try await client.fetchRootPostURL(for: reply2Deep, from: relay) #expect(root == rootPost) } @Test("middle-of-chain post returns its root (different domain, +02:00 timezone in URL)") func replyMiddleReturnsRoot() async throws { let root = try await client.fetchRootPostURL(for: replyMiddle, from: relay) #expect(root == replyMiddleRoot) } @Test("root URL always contains # separator") func rootURLContainsHash() async throws { for postURL in [rootPost, reply1Deep, reply2Deep, replyMiddle] { let root = try await client.fetchRootPostURL(for: postURL, from: relay) #expect(root.contains("#"), "Root URL missing #: \(root)") } } }