mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-14 20:40:44 +00:00
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
223 lines
9.6 KiB
Swift
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)
|
|
}
|
|
}
|