134 lines
5.2 KiB
Swift
134 lines
5.2 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import Foundation
|
|
import Logging
|
|
import Testing
|
|
|
|
@testable import TermindRemoteInfrastructure
|
|
|
|
@Suite
|
|
struct ShellIntegrationLoggingTests {
|
|
@Test
|
|
func lifecycleLogsAreCorrelatedAndExcludeRemoteValues() throws {
|
|
let capture = ShellIntegrationLogCapture()
|
|
let logger = Logger(label: "termind.shell-integration-test") { _ in
|
|
ShellIntegrationCaptureHandler(capture: capture)
|
|
}
|
|
let hostID = UUID()
|
|
var lifecycle = RemoteShellIntegrationLifecycleLogger(logger: logger)
|
|
lifecycle.begin(hostID: hostID, enabled: true)
|
|
|
|
var bootstrap = RemoteShellIntegrationBootstrap(token: "LOGGING")
|
|
_ = bootstrap.receive(
|
|
Data(
|
|
("__TRMD_LOGGING_SYSTEM__sensitive-system||1|SensitiveKernel|1|arm64__\r\n"
|
|
+ "__TRMD_LOGGING_SHELL__/sensitive/shell/bash__\r\n").utf8
|
|
)
|
|
)
|
|
let completed = bootstrap.receive(
|
|
Data(
|
|
"__TRMD_LOGGING_HOME__%2Fsensitive%2Fhome\r\n__TRMD_LOGGING_DONE__\r\n".utf8
|
|
)
|
|
)
|
|
lifecycle.record(try #require(completed.diagnostic), hostID: hostID)
|
|
lifecycle.sessionEnded(hostID: hostID)
|
|
|
|
let entries = capture.entries
|
|
try #require(entries.map(\.message) == ["ssh.shell_integration.started", "ssh.shell_integration.completed"])
|
|
#expect(entries.map(\.level) == [.info, .info])
|
|
#expect(entries[1].metadata["outcome"] == "success")
|
|
#expect(entries[1].metadata["home_resolution"] == "resolved")
|
|
#expect(entries[1].metadata["system_info"] == "available")
|
|
#expect(entries[0].metadata["operation_id"] == entries[1].metadata["operation_id"])
|
|
#expect(entries.allSatisfy { $0.metadata["host_id"] == hostID.uuidString.lowercased() })
|
|
let serialized = entries.map { "\($0.message) \($0.metadata)" }.joined(separator: "\n")
|
|
#expect(!serialized.localizedCaseInsensitiveContains("sensitive"))
|
|
#expect(!serialized.contains("LOGGING"))
|
|
}
|
|
|
|
@Test
|
|
func delayedTimeoutDisabledAndSessionEndUseStableReasonsWithoutDuplicates() {
|
|
let capture = ShellIntegrationLogCapture()
|
|
let logger = Logger(label: "termind.shell-integration-test") { _ in
|
|
ShellIntegrationCaptureHandler(capture: capture)
|
|
}
|
|
let hostID = UUID()
|
|
var lifecycle = RemoteShellIntegrationLifecycleLogger(logger: logger)
|
|
|
|
lifecycle.begin(hostID: hostID, enabled: false)
|
|
lifecycle.begin(hostID: hostID, enabled: true)
|
|
lifecycle.record(.probeDelayed, hostID: hostID)
|
|
lifecycle.record(.timedOut(stage: .installation, late: true), hostID: hostID)
|
|
lifecycle.sessionEnded(hostID: hostID)
|
|
lifecycle.begin(hostID: hostID, enabled: true)
|
|
lifecycle.sessionEnded(hostID: hostID)
|
|
lifecycle.sessionEnded(hostID: hostID)
|
|
|
|
let entries = capture.entries
|
|
#expect(
|
|
entries.map(\.message) == [
|
|
"ssh.shell_integration.skipped",
|
|
"ssh.shell_integration.started",
|
|
"ssh.shell_integration.delayed",
|
|
"ssh.shell_integration.timed_out",
|
|
"ssh.shell_integration.started",
|
|
"ssh.shell_integration.skipped",
|
|
])
|
|
#expect(entries.map(\.level) == [.info, .info, .warning, .warning, .info, .info])
|
|
#expect(entries[0].metadata["reason"] == "disabled")
|
|
#expect(entries[2].metadata["reason"] == "probe_timeout")
|
|
#expect(entries[3].metadata["reason"] == "timeout")
|
|
#expect(entries[3].metadata["stage"] == "installation")
|
|
#expect(entries[5].metadata["reason"] == "session_ended")
|
|
}
|
|
}
|
|
|
|
private nonisolated struct ShellIntegrationLogEntry: Sendable {
|
|
let level: Logger.Level
|
|
let message: String
|
|
let metadata: [String: String]
|
|
}
|
|
|
|
private nonisolated final class ShellIntegrationLogCapture: @unchecked Sendable {
|
|
private let lock = NSLock()
|
|
private var storage: [ShellIntegrationLogEntry] = []
|
|
|
|
var entries: [ShellIntegrationLogEntry] { lock.withLock { storage } }
|
|
|
|
func append(_ event: LogEvent, metadata: Logger.Metadata) {
|
|
lock.withLock {
|
|
storage.append(
|
|
ShellIntegrationLogEntry(
|
|
level: event.level,
|
|
message: event.message.description,
|
|
metadata: metadata.mapValues(\.description)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private nonisolated struct ShellIntegrationCaptureHandler: LogHandler {
|
|
let capture: ShellIntegrationLogCapture
|
|
var metadataProvider: Logger.MetadataProvider?
|
|
var metadata: Logger.Metadata = [:]
|
|
var logLevel: Logger.Level = .trace
|
|
|
|
subscript(metadataKey key: String) -> Logger.Metadata.Value? {
|
|
get { metadata[key] }
|
|
set { metadata[key] = newValue }
|
|
}
|
|
|
|
func log(event: LogEvent) {
|
|
var combined = metadata
|
|
if let provided = metadataProvider?.get() {
|
|
combined.merge(provided) { _, provided in provided }
|
|
}
|
|
if let explicit = event.metadata {
|
|
combined.merge(explicit) { _, explicit in explicit }
|
|
}
|
|
capture.append(event, metadata: combined)
|
|
}
|
|
}
|