mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
fix(ios): page session rosters and restore approval guidance (#112004)
* fix(ios): page session rosters and restore approval guidance * test(ios): align lifecycle guard and native locale inventory * fix(ios): bound advancing session roster scans
This commit is contained in:
committed by
GitHub
parent
2e50bdf9fb
commit
160e2baf49
Generated
+2
@@ -3591,6 +3591,7 @@
|
||||
{"id":"native.apple.6f167dc8a11d0c47","source":"Show reasoning & tool activity","surface":"apple","sites":[{"kind":"ui-localized-call","path":"apps/ios/Sources/Design/ChatProTab.swift"}]},
|
||||
{"id":"native.apple.c9009c9a9599a439","source":"Show talk transcript","surface":"apple","sites":[{"kind":"ui-localized-call","path":"apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatTalkActivityViews.swift"}]},
|
||||
{"id":"native.apple.7a36d1dbcf3ee77c","source":"Show the Debug page with development utilities.","surface":"apple","sites":[{"kind":"ui-named-argument","path":"apps/macos/Sources/OpenClaw/GeneralSettings.swift"}]},
|
||||
{"id":"native.apple.c0998dff769c3504","source":"Showing %lld of %lld sessions. Refresh to load the rest.","surface":"apple","sites":[{"kind":"ui-localized-call","path":"apps/ios/Sources/RootSidebarModel.swift"}]},
|
||||
{"id":"native.apple.8b7ad79ccffd6b4e","source":"Shows the full gateway status label","surface":"apple","sites":[{"kind":"ui-localized-call","path":"apps/ios/Sources/Design/ChatProTab.swift"}]},
|
||||
{"id":"native.apple.2fb7291439473673","source":"Sidebar Agents","surface":"apple","sites":[{"kind":"ui-call","path":"apps/ios/Sources/Design/SettingsProTabSections.swift"}]},
|
||||
{"id":"native.apple.0ad29d33b340a6f4","source":"Sifting","surface":"apple","sites":[{"kind":"ui-localized-call","path":"apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift"}]},
|
||||
@@ -3621,6 +3622,7 @@
|
||||
{"id":"native.apple.80006987fdb875be","source":"Skipping…","surface":"apple","sites":[{"kind":"ui-call","path":"apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatQuestionCard.swift"}]},
|
||||
{"id":"native.apple.10f82d82056b15dc","source":"Snapping","surface":"apple","sites":[{"kind":"ui-localized-call","path":"apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift"}]},
|
||||
{"id":"native.apple.820504301d33764a","source":"Some approvals need renewal","surface":"apple","sites":[{"kind":"ui-named-argument","path":"apps/macos/Sources/OpenClaw/SystemRunSettingsView.swift"}]},
|
||||
{"id":"native.apple.7e25b2a702a07140","source":"Some sessions could not be loaded. Refresh to try again.","surface":"apple","sites":[{"kind":"ui-localized-call","path":"apps/ios/Sources/RootSidebarModel.swift"}]},
|
||||
{"id":"native.apple.9bd57456b80dbeeb","source":"Sounds","surface":"apple","sites":[{"kind":"ui-call","path":"apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift"}]},
|
||||
{"id":"native.apple.649e6748080b4252","source":"Source","surface":"apple","sites":[{"kind":"ui-named-argument","path":"apps/ios/Sources/Design/AgentProTab+Skills.swift"}]},
|
||||
{"id":"native.apple.39f6d32204cc5d60","source":"Speak failed: %@","surface":"apple","sites":[{"kind":"ui-localized-call","path":"apps/ios/Sources/Voice/TalkModeManager.swift"}]},
|
||||
|
||||
@@ -123,13 +123,13 @@ struct CommandCenterTab: View {
|
||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 4), spacing: 8) {
|
||||
self.threadTile(
|
||||
title: String(localized: "Sessions"),
|
||||
value: self.overviewSessions.count.formatted())
|
||||
value: self.overviewCountText(self.overviewSessions.count))
|
||||
self.threadTile(
|
||||
title: String(localized: "Live"),
|
||||
value: self.overviewLiveCount.formatted())
|
||||
value: self.overviewCountText(self.overviewLiveCount))
|
||||
self.threadTile(
|
||||
title: String(localized: "Unread"),
|
||||
value: self.overviewUnreadCount.formatted())
|
||||
value: self.overviewCountText(self.overviewUnreadCount))
|
||||
self.threadTile(
|
||||
title: String(localized: "Tokens"),
|
||||
value: self.overviewTokenText)
|
||||
@@ -282,8 +282,14 @@ struct CommandCenterTab: View {
|
||||
self.overviewSessions.count { $0.unread == true }
|
||||
}
|
||||
|
||||
private func overviewCountText(_ count: Int) -> String {
|
||||
"\(count.formatted())\(self.dashboardModel?.isSessionRosterComplete == false ? "+" : "")"
|
||||
}
|
||||
|
||||
private var overviewTokenText: String {
|
||||
let summary = RootSidebarModel.tokenUsageSummary(for: self.overviewSessions)
|
||||
let summary = RootSidebarModel.tokenUsageSummary(
|
||||
for: self.overviewSessions,
|
||||
rosterIsComplete: self.dashboardModel?.isSessionRosterComplete ?? true)
|
||||
guard let total = summary.total else { return "n/a" }
|
||||
return "\(summary.isPartial ? "~" : "")\(total.formatted(.number.notation(.compactName)))"
|
||||
}
|
||||
|
||||
@@ -360,8 +360,12 @@ struct SettingsProTab: View {
|
||||
self.onGatewaySetupRequestHandled?(gatewaySetupRequest.id)
|
||||
}
|
||||
|
||||
var canOpenNotificationsRouteFromApprovals: Bool {
|
||||
self.ownsNavigationStack ? self.directRoute == nil : self.navigateToRoute != nil
|
||||
}
|
||||
|
||||
func openNotificationsRouteFromApprovals() {
|
||||
guard self.directRoute == nil else { return }
|
||||
guard self.canOpenNotificationsRouteFromApprovals else { return }
|
||||
if let approvalID = ExecApprovalIdentifier.exact(self.appModel.pendingExecApprovalPrompt?.id) {
|
||||
self.onApprovalNotificationsRoute?(approvalID)
|
||||
}
|
||||
|
||||
@@ -544,7 +544,7 @@ extension SettingsProTab {
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
if self.directRoute == nil {
|
||||
if self.canOpenNotificationsRouteFromApprovals {
|
||||
Button {
|
||||
self.openNotificationsRouteFromApprovals()
|
||||
} label: {
|
||||
|
||||
@@ -5,8 +5,87 @@ import OpenClawKit
|
||||
import OpenClawProtocol
|
||||
|
||||
struct ChatSessionRosterSnapshot: Sendable {
|
||||
private static let maximumPageCount = 50
|
||||
private static let maximumSessionCount = 10000
|
||||
|
||||
let sessions: [OpenClawChatSessionEntry]
|
||||
let isCached: Bool
|
||||
let totalCount: Int?
|
||||
let isComplete: Bool
|
||||
|
||||
init(
|
||||
sessions: [OpenClawChatSessionEntry],
|
||||
isCached: Bool,
|
||||
totalCount: Int? = nil,
|
||||
isComplete: Bool = true)
|
||||
{
|
||||
self.sessions = sessions
|
||||
self.isCached = isCached
|
||||
self.totalCount = totalCount
|
||||
self.isComplete = isComplete
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static func collect(
|
||||
fetchPage: @MainActor (Int) async throws -> OpenClawChatSessionsListResponse) async throws -> Self
|
||||
{
|
||||
var sessions: [OpenClawChatSessionEntry] = []
|
||||
var rowIndices: [String: Int] = [:]
|
||||
var totalCount: Int?
|
||||
var offset = 0
|
||||
var pageCount = 0
|
||||
|
||||
while true {
|
||||
try Task.checkCancellation()
|
||||
// Match sessions.list's bounded scan so a changing Gateway snapshot
|
||||
// cannot keep a sidebar refresh alive forever.
|
||||
guard pageCount < Self.maximumPageCount, sessions.count < Self.maximumSessionCount else {
|
||||
return Self(sessions: sessions, isCached: false, totalCount: totalCount, isComplete: false)
|
||||
}
|
||||
pageCount += 1
|
||||
let response: OpenClawChatSessionsListResponse
|
||||
do {
|
||||
response = try await fetchPage(offset)
|
||||
} catch is CancellationError {
|
||||
throw CancellationError()
|
||||
} catch {
|
||||
guard !sessions.isEmpty else { throw error }
|
||||
return Self(sessions: sessions, isCached: false, totalCount: totalCount, isComplete: false)
|
||||
}
|
||||
try Task.checkCancellation()
|
||||
|
||||
if let count = response.totalCount {
|
||||
totalCount = count
|
||||
}
|
||||
for session in response.sessions {
|
||||
if let index = rowIndices[session.key] {
|
||||
sessions[index] = session
|
||||
} else {
|
||||
guard sessions.count < Self.maximumSessionCount else {
|
||||
return Self(sessions: sessions, isCached: false, totalCount: totalCount, isComplete: false)
|
||||
}
|
||||
rowIndices[session.key] = sessions.count
|
||||
sessions.append(session)
|
||||
}
|
||||
}
|
||||
|
||||
let advancedOffset = offset + response.sessions.count
|
||||
let hasMore = response.hasMore ?? totalCount.map { advancedOffset < $0 } ?? false
|
||||
guard hasMore else {
|
||||
let isComplete = totalCount.map { sessions.count >= $0 } ?? true
|
||||
return Self(sessions: sessions, isCached: false, totalCount: totalCount, isComplete: isComplete)
|
||||
}
|
||||
|
||||
let nextOffset = response.nextOffset ?? advancedOffset
|
||||
guard !response.sessions.isEmpty,
|
||||
nextOffset > offset,
|
||||
totalCount.map({ nextOffset < $0 }) ?? true
|
||||
else {
|
||||
return Self(sessions: sessions, isCached: false, totalCount: totalCount, isComplete: false)
|
||||
}
|
||||
offset = nextOffset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NodeAppModel {
|
||||
@@ -19,21 +98,61 @@ extension NodeAppModel {
|
||||
guard allowCachedFallback else { throw URLError(.notConnectedToInternet) }
|
||||
return await ChatSessionRosterSnapshot(
|
||||
sessions: archived ? [] : self.loadCachedChatSessions(),
|
||||
isCached: true)
|
||||
isCached: true,
|
||||
isComplete: false)
|
||||
}
|
||||
|
||||
do {
|
||||
let response = try await self.makeChatTransport().listSessions(limit: limit, archived: archived)
|
||||
if !archived {
|
||||
self.reconcileChatSessionReadState(response.sessions)
|
||||
await self.storeCachedChatSessions(response.sessions)
|
||||
let sourceGatewayID = self.chatTranscriptCacheGatewayID
|
||||
let snapshot: ChatSessionRosterSnapshot
|
||||
if self.isLocalChatFixtureEnabled {
|
||||
let response = try await self.makeChatTransport().listSessions(limit: limit, archived: archived)
|
||||
snapshot = ChatSessionRosterSnapshot(
|
||||
sessions: response.sessions,
|
||||
isCached: false,
|
||||
totalCount: response.totalCount,
|
||||
isComplete: response.hasMore != true)
|
||||
} else {
|
||||
guard let sourceGatewayID,
|
||||
let route = await self.operatorSession.currentRoute(ifGatewayID: sourceGatewayID)
|
||||
else { throw URLError(.notConnectedToInternet) }
|
||||
|
||||
// Every page belongs to one physical authenticated Gateway route;
|
||||
// reconnects and gateway switches must never splice two rosters.
|
||||
snapshot = try await ChatSessionRosterSnapshot.collect { offset in
|
||||
let request = OpenClawChatGatewayRequests.sessionsList(
|
||||
limit: limit,
|
||||
search: nil,
|
||||
archived: archived,
|
||||
offset: offset)
|
||||
let data = try await self.operatorSession.request(request, ifCurrentRoute: route)
|
||||
return try JSONDecoder().decode(OpenClawChatSessionsListResponse.self, from: data)
|
||||
}
|
||||
guard GatewayStableIdentifier.matches(self.chatTranscriptCacheGatewayID, sourceGatewayID) else {
|
||||
throw CancellationError()
|
||||
}
|
||||
}
|
||||
return ChatSessionRosterSnapshot(sessions: response.sessions, isCached: false)
|
||||
|
||||
if !archived {
|
||||
self.reconcileChatSessionReadState(snapshot.sessions)
|
||||
// An interrupted page must not replace a more complete offline roster.
|
||||
if snapshot.isComplete {
|
||||
await self.storeCachedChatSessions(snapshot.sessions)
|
||||
if let sourceGatewayID,
|
||||
!GatewayStableIdentifier.matches(self.chatTranscriptCacheGatewayID, sourceGatewayID)
|
||||
{
|
||||
throw CancellationError()
|
||||
}
|
||||
}
|
||||
}
|
||||
return snapshot
|
||||
} catch is CancellationError {
|
||||
throw CancellationError()
|
||||
} catch {
|
||||
guard allowCachedFallback, !archived else { throw error }
|
||||
let cached = await self.loadCachedChatSessions()
|
||||
guard !cached.isEmpty else { throw error }
|
||||
return ChatSessionRosterSnapshot(sessions: cached, isCached: true)
|
||||
return ChatSessionRosterSnapshot(sessions: cached, isCached: true, isComplete: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +178,7 @@ final class RootSidebarModel {
|
||||
private(set) var cronJobs: [CronJob] = []
|
||||
private(set) var isRefreshing = false
|
||||
private(set) var sessionErrorText: String?
|
||||
private(set) var isSessionRosterComplete = true
|
||||
private var rosterGeneration = 0
|
||||
private var dashboardGeneration = 0
|
||||
private var sessionObserverVisibility = false
|
||||
@@ -113,8 +233,7 @@ final class RootSidebarModel {
|
||||
if rosterGeneration == self.rosterGeneration {
|
||||
switch loadedRoster {
|
||||
case let .success(loadedRoster):
|
||||
self.sessions = loadedRoster.sessions
|
||||
self.sessionErrorText = nil
|
||||
self.applyRoster(loadedRoster)
|
||||
case let .failure(message):
|
||||
self.sessionErrorText = message
|
||||
case .cancelled:
|
||||
@@ -147,8 +266,7 @@ final class RootSidebarModel {
|
||||
guard !Task.isCancelled, rosterGeneration == self.rosterGeneration else { return }
|
||||
switch loadedRoster {
|
||||
case let .success(roster):
|
||||
self.sessions = roster.sessions
|
||||
self.sessionErrorText = nil
|
||||
self.applyRoster(roster)
|
||||
case let .failure(message):
|
||||
self.sessionErrorText = message
|
||||
case .cancelled:
|
||||
@@ -357,11 +475,32 @@ final class RootSidebarModel {
|
||||
self.sessionErrorText = error.localizedDescription
|
||||
}
|
||||
|
||||
static func tokenUsageSummary(for sessions: [OpenClawChatSessionEntry]) -> TokenUsageSummary {
|
||||
static func tokenUsageSummary(
|
||||
for sessions: [OpenClawChatSessionEntry],
|
||||
rosterIsComplete: Bool = true) -> TokenUsageSummary
|
||||
{
|
||||
let knownTotals = sessions.compactMap(\.totalTokens)
|
||||
return TokenUsageSummary(
|
||||
total: knownTotals.isEmpty ? nil : knownTotals.reduce(0, +),
|
||||
isPartial: knownTotals.count < sessions.count || sessions.contains { $0.totalTokensFresh == false })
|
||||
isPartial: !rosterIsComplete || knownTotals.count < sessions.count ||
|
||||
sessions.contains { $0.totalTokensFresh == false })
|
||||
}
|
||||
|
||||
private func applyRoster(_ roster: ChatSessionRosterSnapshot) {
|
||||
self.sessions = roster.sessions
|
||||
self.isSessionRosterComplete = roster.isComplete
|
||||
guard !roster.isComplete, !roster.isCached else {
|
||||
self.sessionErrorText = nil
|
||||
return
|
||||
}
|
||||
if let totalCount = roster.totalCount {
|
||||
self.sessionErrorText = String(
|
||||
format: String(localized: "Showing %lld of %lld sessions. Refresh to load the rest."),
|
||||
roster.sessions.count,
|
||||
totalCount)
|
||||
} else {
|
||||
self.sessionErrorText = String(localized: "Some sessions could not be loaded. Refresh to try again.")
|
||||
}
|
||||
}
|
||||
|
||||
private func loadRoster(
|
||||
|
||||
@@ -71,6 +71,9 @@ struct RootTabsPresentationTests {
|
||||
Self.sessionEntry(key: "two", totalTokens: 80),
|
||||
])
|
||||
let unknown = RootSidebarModel.tokenUsageSummary(for: [Self.sessionEntry(key: "unknown")])
|
||||
let incompleteRoster = RootSidebarModel.tokenUsageSummary(
|
||||
for: [Self.sessionEntry(key: "known", totalTokens: 45, totalTokensFresh: true)],
|
||||
rosterIsComplete: false)
|
||||
|
||||
#expect(summary.total == 1500)
|
||||
#expect(summary.isPartial)
|
||||
@@ -78,6 +81,122 @@ struct RootTabsPresentationTests {
|
||||
#expect(!complete.isPartial)
|
||||
#expect(unknown.total == nil)
|
||||
#expect(unknown.isPartial)
|
||||
#expect(incompleteRoster.total == 45)
|
||||
#expect(incompleteRoster.isPartial)
|
||||
}
|
||||
|
||||
@Test func `session roster loads every gateway page beyond the former thousand-row ceiling`() async throws {
|
||||
let entries = (0..<1205).map { Self.sessionEntry(key: "session-\($0)") }
|
||||
var offsets: [Int] = []
|
||||
|
||||
let snapshot = try await ChatSessionRosterSnapshot.collect { offset in
|
||||
offsets.append(offset)
|
||||
let page = Array(entries.dropFirst(offset).prefix(200))
|
||||
let nextOffset = offset + page.count
|
||||
return OpenClawChatSessionsListResponse(
|
||||
ts: nil,
|
||||
path: nil,
|
||||
count: page.count,
|
||||
totalCount: entries.count,
|
||||
offset: offset,
|
||||
nextOffset: nextOffset < entries.count ? nextOffset : nil,
|
||||
hasMore: nextOffset < entries.count,
|
||||
defaults: nil,
|
||||
sessions: page)
|
||||
}
|
||||
|
||||
#expect(offsets == [0, 200, 400, 600, 800, 1000, 1200])
|
||||
#expect(snapshot.sessions.count == 1205)
|
||||
#expect(snapshot.sessions.last?.key == "session-1204")
|
||||
#expect(snapshot.totalCount == 1205)
|
||||
#expect(snapshot.isComplete)
|
||||
}
|
||||
|
||||
@Test func `session roster bounds an endlessly advancing gateway snapshot without dropping rows`() async throws {
|
||||
var requestCount = 0
|
||||
|
||||
let snapshot = try await ChatSessionRosterSnapshot.collect { offset in
|
||||
requestCount += 1
|
||||
guard requestCount <= 51 else { throw URLError(.networkConnectionLost) }
|
||||
let sessions = (offset..<(offset + 200)).map { Self.sessionEntry(key: "session-\($0)") }
|
||||
return OpenClawChatSessionsListResponse(
|
||||
ts: nil,
|
||||
path: nil,
|
||||
count: sessions.count,
|
||||
totalCount: offset + 400,
|
||||
offset: offset,
|
||||
nextOffset: offset + sessions.count,
|
||||
hasMore: true,
|
||||
defaults: nil,
|
||||
sessions: sessions)
|
||||
}
|
||||
|
||||
#expect(requestCount == 50)
|
||||
#expect(snapshot.sessions.count == 10000)
|
||||
#expect(snapshot.sessions.last?.key == "session-9999")
|
||||
#expect(!snapshot.isComplete)
|
||||
}
|
||||
|
||||
@Test func `session roster preserves successful pages when a later request fails`() async throws {
|
||||
let firstPage = (0..<200).map { Self.sessionEntry(key: "session-\($0)") }
|
||||
|
||||
let snapshot = try await ChatSessionRosterSnapshot.collect { offset in
|
||||
guard offset == 0 else { throw URLError(.networkConnectionLost) }
|
||||
return OpenClawChatSessionsListResponse(
|
||||
ts: nil,
|
||||
path: nil,
|
||||
count: firstPage.count,
|
||||
totalCount: 350,
|
||||
offset: offset,
|
||||
nextOffset: firstPage.count,
|
||||
hasMore: true,
|
||||
defaults: nil,
|
||||
sessions: firstPage)
|
||||
}
|
||||
|
||||
#expect(snapshot.sessions.count == 200)
|
||||
#expect(snapshot.totalCount == 350)
|
||||
#expect(!snapshot.isComplete)
|
||||
}
|
||||
|
||||
@Test func `session roster rejects a nonadvancing gateway cursor without losing rows`() async throws {
|
||||
var requestCount = 0
|
||||
|
||||
let snapshot = try await ChatSessionRosterSnapshot.collect { offset in
|
||||
requestCount += 1
|
||||
return OpenClawChatSessionsListResponse(
|
||||
ts: nil,
|
||||
path: nil,
|
||||
count: 1,
|
||||
totalCount: 2,
|
||||
offset: offset,
|
||||
nextOffset: offset,
|
||||
hasMore: true,
|
||||
defaults: nil,
|
||||
sessions: [Self.sessionEntry(key: "first")])
|
||||
}
|
||||
|
||||
#expect(requestCount == 1)
|
||||
#expect(snapshot.sessions.map(\.key) == ["first"])
|
||||
#expect(!snapshot.isComplete)
|
||||
}
|
||||
|
||||
@Test func `session roster propagates cancellation after a successful page`() async {
|
||||
await #expect(throws: CancellationError.self) {
|
||||
_ = try await ChatSessionRosterSnapshot.collect { offset in
|
||||
guard offset == 0 else { throw CancellationError() }
|
||||
return OpenClawChatSessionsListResponse(
|
||||
ts: nil,
|
||||
path: nil,
|
||||
count: 1,
|
||||
totalCount: 2,
|
||||
offset: offset,
|
||||
nextOffset: 1,
|
||||
hasMore: true,
|
||||
defaults: nil,
|
||||
sessions: [Self.sessionEntry(key: "first")])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `usage list shows the latest fourteen days newest first`() {
|
||||
@@ -543,6 +662,20 @@ struct RootTabsPresentationTests {
|
||||
#expect(!embedded.ownsNavigationStack)
|
||||
}
|
||||
|
||||
@Test func `direct approvals exposes notification remediation only when a navigation owner exists`() {
|
||||
let routedApprovals = SettingsProTab(
|
||||
directRoute: .approvals,
|
||||
ownsNavigationStack: false,
|
||||
navigateToRoute: { _ in })
|
||||
let isolatedApprovals = SettingsProTab(directRoute: .approvals)
|
||||
let unroutedEmbeddedSettings = SettingsProTab(ownsNavigationStack: false)
|
||||
|
||||
#expect(routedApprovals.canOpenNotificationsRouteFromApprovals)
|
||||
#expect(!isolatedApprovals.canOpenNotificationsRouteFromApprovals)
|
||||
#expect(!unroutedEmbeddedSettings.canOpenNotificationsRouteFromApprovals)
|
||||
#expect(SettingsProTab().canOpenNotificationsRouteFromApprovals)
|
||||
}
|
||||
|
||||
@Test func `localized QR status matcher accepts positional placeholders`() {
|
||||
#expect(SettingsProTab.localizedFormat(
|
||||
"qr loaded. connecting to %1$@:%2$@...",
|
||||
|
||||
@@ -165,9 +165,15 @@ struct RootTabsSourceGuardTests {
|
||||
source,
|
||||
from: "func refreshSessions(appModel: NodeAppModel) async {",
|
||||
to: "func reportSessionError(_ error: any Error) {")
|
||||
let rosterCommit = try #require(refresh.range(of: "self.sessions = loadedRoster.sessions"))
|
||||
let rosterOwner = try Self.extract(
|
||||
source,
|
||||
from: "private func applyRoster(_ roster: ChatSessionRosterSnapshot) {",
|
||||
to: "private func loadRoster(")
|
||||
let rosterCommit = try #require(refresh.range(of: "self.applyRoster(loadedRoster)"))
|
||||
let dashboardWait = try #require(refresh.range(of: "let loadedDashboard = await dashboard"))
|
||||
|
||||
#expect(rosterOwner.contains("self.sessions = roster.sessions"))
|
||||
#expect(rosterOwner.contains("self.isSessionRosterComplete = roster.isComplete"))
|
||||
#expect(source.contains("private var rosterGeneration = 0"))
|
||||
#expect(source.contains("private var dashboardGeneration = 0"))
|
||||
#expect(source.matches(of: /self\.rosterGeneration &\+= 1/).count == 2)
|
||||
|
||||
Reference in New Issue
Block a user