Files
openclaw/apps/macos/Tests/OpenClawIPCTests/MenuSessionsInjectorTests.swift
Nimrod Gutman f42645037f refactor(macos): move sessions into context submenu (#75489)
Merged via squash.

Prepared head SHA: bbf5450572
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
2026-05-01 10:22:29 +03:00

223 lines
9.6 KiB
Swift

import AppKit
import Testing
@testable import OpenClaw
@Suite(.serialized)
@MainActor
struct MenuSessionsInjectorTests {
@Test func `anchors dynamic rows below controls and actions`() throws {
let injector = MenuSessionsInjector()
let menu = NSMenu()
menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: ""))
menu.addItem(.separator())
menu.addItem(NSMenuItem(title: "Send Heartbeats", action: nil, keyEquivalent: ""))
menu.addItem(NSMenuItem(title: "Browser Control", action: nil, keyEquivalent: ""))
menu.addItem(.separator())
menu.addItem(NSMenuItem(title: "Open Dashboard", action: nil, keyEquivalent: ""))
menu.addItem(NSMenuItem(title: "Open Chat", action: nil, keyEquivalent: ""))
menu.addItem(.separator())
menu.addItem(NSMenuItem(title: "Settings…", action: nil, keyEquivalent: ""))
let footerSeparatorIndex = try #require(menu.items.lastIndex(where: { $0.isSeparatorItem }))
#expect(injector.testingFindInsertIndex(in: menu) == footerSeparatorIndex)
#expect(injector.testingFindNodesInsertIndex(in: menu) == footerSeparatorIndex)
}
@Test func `injects disconnected message`() {
let injector = MenuSessionsInjector()
injector.setTestingControlChannelConnected(false)
injector.setTestingSnapshot(nil, errorText: nil)
let menu = NSMenu()
menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: ""))
menu.addItem(.separator())
menu.addItem(NSMenuItem(title: "Send Heartbeats", action: nil, keyEquivalent: ""))
injector.injectForTesting(into: menu)
let contextItem = menu.items.first { $0.tag == 9_415_557 && $0.title == "Context" }
#expect(contextItem != nil)
#expect(contextItem?.submenu != nil)
}
@Test func `injects session rows`() throws {
let injector = MenuSessionsInjector()
injector.setTestingControlChannelConnected(true)
let defaults = SessionDefaults(model: "anthropic/claude-opus-4-6", contextTokens: 200_000)
let rows = [
SessionRow(
id: "main",
key: "main",
kind: .direct,
displayName: nil,
provider: nil,
subject: nil,
room: nil,
space: nil,
updatedAt: Date(),
sessionId: "s1",
thinkingLevel: "low",
verboseLevel: nil,
systemSent: false,
abortedLastRun: false,
tokens: SessionTokenStats(input: 10, output: 20, total: 30, contextTokens: 200_000),
model: "claude-opus-4-6"),
SessionRow(
id: "discord:group:alpha",
key: "discord:group:alpha",
kind: .group,
displayName: nil,
provider: nil,
subject: nil,
room: nil,
space: nil,
updatedAt: Date(timeIntervalSinceNow: -60),
sessionId: "s2",
thinkingLevel: "high",
verboseLevel: "debug",
systemSent: true,
abortedLastRun: true,
tokens: SessionTokenStats(input: 50, output: 50, total: 100, contextTokens: 200_000),
model: "claude-opus-4-6"),
]
let snapshot = SessionStoreSnapshot(
storePath: "/tmp/sessions.json",
defaults: defaults,
rows: rows)
injector.setTestingSnapshot(snapshot, errorText: nil)
let usage = GatewayUsageSummary(
updatedAt: Date().timeIntervalSince1970 * 1000,
providers: [
GatewayUsageProvider(
provider: "anthropic",
displayName: "Claude",
windows: [GatewayUsageWindow(label: "5h", usedPercent: 12, resetAt: nil)],
plan: "Pro",
error: nil),
GatewayUsageProvider(
provider: "openai-codex",
displayName: "Codex",
windows: [GatewayUsageWindow(label: "day", usedPercent: 3, resetAt: nil)],
plan: nil,
error: nil),
])
injector.setTestingUsageSummary(usage, errorText: nil)
let menu = NSMenu()
menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: ""))
menu.addItem(.separator())
menu.addItem(NSMenuItem(title: "Send Heartbeats", action: nil, keyEquivalent: ""))
menu.addItem(NSMenuItem(title: "Browser Control", action: nil, keyEquivalent: ""))
menu.addItem(.separator())
menu.addItem(NSMenuItem(title: "Open Dashboard", action: nil, keyEquivalent: ""))
menu.addItem(.separator())
menu.addItem(NSMenuItem(title: "Settings…", action: nil, keyEquivalent: ""))
injector.injectForTesting(into: menu)
let contextItem = try #require(menu.items.first { $0.tag == 9_415_557 && $0.title == "Context" })
let contextSubmenu = try #require(contextItem.submenu)
#expect(menu.items.filter { $0.tag == 9_415_557 && $0.title == "Context" }.count == 1)
#expect(menu.items.contains { $0.tag == 9_415_557 && $0.isSeparatorItem })
#expect(contextSubmenu.items.compactMap { $0.representedObject as? String }.filter { ["main", "discord:group:alpha"].contains($0) }.count == 2)
#expect(contextSubmenu.items.allSatisfy { $0.title != "Usage cost (30 days)" })
let sendHeartbeatsIndex = try #require(menu.items.firstIndex(where: { $0.title == "Send Heartbeats" }))
let openDashboardIndex = try #require(menu.items.firstIndex(where: { $0.title == "Open Dashboard" }))
let firstInjectedIndex = try #require(menu.items.firstIndex(where: { $0.tag == 9_415_557 }))
let settingsIndex = try #require(menu.items.firstIndex(where: { $0.title == "Settings…" }))
#expect(sendHeartbeatsIndex < firstInjectedIndex)
#expect(openDashboardIndex < firstInjectedIndex)
#expect(firstInjectedIndex < settingsIndex)
}
@Test func `cost usage submenu does not use injector delegate`() {
let injector = MenuSessionsInjector()
injector.setTestingControlChannelConnected(true)
let summary = GatewayCostUsageSummary(
updatedAt: Date().timeIntervalSince1970 * 1000,
days: 1,
daily: [
GatewayCostUsageDay(
date: "2026-02-24",
input: 10,
output: 20,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 30,
totalCost: 0.12,
missingCostEntries: 0),
],
totals: GatewayCostUsageTotals(
input: 10,
output: 20,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 30,
totalCost: 0.12,
missingCostEntries: 0))
injector.setTestingCostUsageSummary(summary, errorText: nil)
let menu = NSMenu()
menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: ""))
menu.addItem(.separator())
menu.addItem(NSMenuItem(title: "Send Heartbeats", action: nil, keyEquivalent: ""))
injector.injectForTesting(into: menu)
let contextItem = menu.items.first { $0.tag == 9_415_557 && $0.title == "Context" }
#expect(contextItem?.submenu?.items.allSatisfy { $0.title != "Usage cost (30 days)" } == true)
let usageCostItem = menu.items.first { $0.title == "Usage cost (30 days)" }
#expect(usageCostItem != nil)
#expect(usageCostItem?.submenu != nil)
#expect(usageCostItem?.submenu?.delegate == nil)
}
@Test func `node status text distinguishes paired disconnected nodes`() {
let pairedDisconnected = Self.node(id: "paired", paired: true, connected: false)
let unpairedDisconnected = Self.node(id: "unpaired", paired: false, connected: false)
let connected = Self.node(id: "connected", paired: true, connected: true)
#expect(NodeMenuEntryFormatter.roleText(pairedDisconnected) == "paired · disconnected")
#expect(NodeMenuEntryFormatter.roleText(unpairedDisconnected) == "unpaired · disconnected")
#expect(NodeMenuEntryFormatter.roleText(connected) == "paired · connected")
}
@Test func `sorted node entries include paired disconnected nodes`() {
let injector = MenuSessionsInjector()
defer { NodesStore.shared.nodes = [] }
NodesStore.shared.nodes = [
Self.node(id: "ignored", paired: false, connected: false, displayName: "Ignored"),
Self.node(id: "paired", paired: true, connected: false, displayName: "MacBook"),
Self.node(id: "connected", paired: true, connected: true, displayName: "iPhone"),
]
let entries = injector.testingSortedNodeEntries()
#expect(entries.map(\.nodeId) == ["connected", "paired"])
}
private static func node(
id: String,
paired: Bool,
connected: Bool,
displayName: String? = nil) -> NodeInfo
{
NodeInfo(
nodeId: id,
displayName: displayName ?? id,
platform: "macOS 26.3.1",
version: nil,
coreVersion: nil,
uiVersion: nil,
deviceFamily: "Mac",
modelIdentifier: nil,
remoteIp: nil,
caps: nil,
commands: nil,
permissions: nil,
paired: paired,
connected: connected)
}
}