Files
openclaw/apps/ios/Tests/CommandSessionGroupingTests.swift
Peter Steinberger 88f1ec38d4 feat(sessions): grouping, unread state, and full session controls on web, iOS, and Android (#100814)
Gateway (additive, no protocol version bump): SessionEntry gains
lastReadAt/markedUnreadAt/lastActivityAt; session rows expose a derived
unread flag (explicit mark, or last read before latest activity; never-read
sessions stay read so upgrades do not light up). lastActivityAt is stamped
in the canonical post-run store update - user, channel, and cron runs count
as activity; heartbeat, internal-event, and preserved-state runs do not.
sessions.patch gains unread; sessions.create gains fork (transcript fork
from parentSessionKey under the parent lifecycle lock, refusing active,
concurrently-changed, and oversized parents, cross-agent aware).

Web sidebar: Pinned/custom-group/Ungrouped sections, unread dots, kebab and
right-click context menu (pin, mark unread/read, rename, fork, move to group,
archive, delete guarded for agent main sessions and active runs), mark-read
on view with loop-safe re-acknowledgement and failure retry; sessions page
gets unread + fork actions and shared custom-group helpers.

iOS Command Center: grouped sections, unread/pin indicators, Show Archived
gated on per-entry state, full context menu with rename/new-group alerts and
delete confirmation, current-session preview guarantee, read-episode
re-acknowledgement; new patch/delete/fork transport calls; Swift protocol
models regenerated.

Android SessionsScreen: grouped headers, unread/pin indicators, Archived
filter gated on per-entry state, long-press menu with the full control set,
agent-scoped forks, explicit label/category clears from session events,
main-session fallback when archiving/deleting the open chat, read-episode
re-acknowledgement with failure retry.

Closes #100739
2026-07-06 14:30:55 +01:00

95 lines
3.3 KiB
Swift

import OpenClawChatUI
import Testing
@testable import OpenClaw
struct CommandSessionGroupingTests {
@Test func `groups pinned categories and ungrouped in display order`() {
let sections = CommandSessionGrouping.sections(from: [
self.entry("ungrouped", activity: 2),
self.entry("beta", category: "Beta", activity: 3),
self.entry("alpha-old", category: "Alpha", activity: 1),
self.entry("pinned", category: "Beta", pinned: true, activity: 4),
self.entry("alpha-new", category: "Alpha", activity: 5),
])
#expect(sections.map(\.id) == [
.pinned,
.category("Alpha"),
.category("Beta"),
.ungrouped,
])
#expect(sections[0].entries.map(\.key) == ["pinned"])
#expect(sections[1].entries.map(\.key) == ["alpha-new", "alpha-old"])
#expect(sections[3].showsHeader)
}
@Test func `hides ungrouped header without category sections`() {
let sections = CommandSessionGrouping.sections(from: [self.entry("plain", activity: 1)])
#expect(sections.count == 1)
#expect(sections[0].id == .ungrouped)
#expect(!sections[0].showsHeader)
}
@Test func `preview puts pinned sessions before recent activity`() {
let entries = CommandSessionGrouping.previewOrder([
self.entry("recent", activity: 20),
self.entry("pinned-old", pinned: true, activity: 1),
self.entry("older", activity: 10),
])
#expect(entries.map(\.key) == ["pinned-old", "recent", "older"])
}
@Test func `preview selection keeps the open chat visible past the cap`() {
let entries = [
self.entry("a", activity: 40),
self.entry("b", activity: 30),
self.entry("c", activity: 20),
self.entry("current", activity: 10),
]
let selection = CommandSessionGrouping.previewSelection(entries, currentKey: "current")
#expect(selection.map(\.key) == ["current", "a", "b"])
// Natural order wins when the current session already fits the cap.
let natural = CommandSessionGrouping.previewSelection(entries, currentKey: "a")
#expect(natural.map(\.key) == ["a", "b", "c"])
// Unknown or empty keys fall back to the plain capped ordering.
let fallback = CommandSessionGrouping.previewSelection(entries, currentKey: "")
#expect(fallback.map(\.key) == ["a", "b", "c"])
}
private func entry(
_ key: String,
category: String? = nil,
pinned: Bool = false,
activity: Double) -> OpenClawChatSessionEntry
{
OpenClawChatSessionEntry(
key: key,
kind: nil,
displayName: nil,
surface: nil,
subject: nil,
room: nil,
space: nil,
updatedAt: nil,
sessionId: nil,
systemSent: nil,
abortedLastRun: nil,
thinkingLevel: nil,
verboseLevel: nil,
inputTokens: nil,
outputTokens: nil,
totalTokens: nil,
modelProvider: nil,
model: nil,
contextTokens: nil,
category: category,
pinned: pinned,
lastActivityAt: activity)
}
}