diff --git a/packaging/monitor-mode/installer-app/TargetBridgeInstallerEvent.swift b/packaging/monitor-mode/installer-app/TargetBridgeInstallerEvent.swift new file mode 100644 index 0000000..82e85ad --- /dev/null +++ b/packaging/monitor-mode/installer-app/TargetBridgeInstallerEvent.swift @@ -0,0 +1,62 @@ +import Foundation + +enum InstallerCheckState: String, Equatable { + case passed = "pass" + case warning = "warn" + case failed = "fail" + case info +} + +struct InstallerEvent: Equatable { + var phase: String + var code: String + var state: InstallerCheckState + var message: String + + static func parse(line: String) -> InstallerEvent? { + guard line.hasPrefix("TB_EVENT|") else { return nil } + let fields = line.split(separator: "|", maxSplits: 4, omittingEmptySubsequences: false) + guard fields.count == 5, + let state = InstallerCheckState(rawValue: String(fields[3])) else { return nil } + return InstallerEvent( + phase: String(fields[1]), + code: String(fields[2]), + state: state, + message: String(fields[4]) + ) + } + + static func summary( + from events: [InstallerEvent], + priorityCodes: [String], + limit: Int = 5 + ) -> [InstallerEvent] { + guard limit > 0 else { return [] } + var latestByCode: [String: InstallerEvent] = [:] + events.forEach { latestByCode[$0.code] = $0 } + + var selected: [InstallerEvent] = [] + var includedCodes = Set() + func appendOnce(_ event: InstallerEvent) { + guard includedCodes.insert(event.code).inserted else { return } + selected.append(event) + } + + // A failed or warning check must never be hidden by earlier successful + // rows. Show the most recent actionable result first, then fill the + // remaining space with the normal high-level summary. + for state in [InstallerCheckState.failed, .warning] { + for event in events.reversed() + where event.state == state && latestByCode[event.code] == event { + appendOnce(event) + } + } + for code in priorityCodes { + if let event = latestByCode[code] { appendOnce(event) } + } + for event in events.reversed() where latestByCode[event.code] == event { + appendOnce(event) + } + return Array(selected.prefix(limit)) + } +} diff --git a/packaging/monitor-mode/tests/InstallerEventTests.swift b/packaging/monitor-mode/tests/InstallerEventTests.swift new file mode 100644 index 0000000..51a4ed0 --- /dev/null +++ b/packaging/monitor-mode/tests/InstallerEventTests.swift @@ -0,0 +1,54 @@ +import Foundation + +@main +struct InstallerEventTests { + private static var checks = 0 + private static var failures = 0 + + private static func check(_ label: String, _ condition: @autoclosure () -> Bool) { + checks += 1 + if !condition() { + failures += 1 + fputs("FAIL: \(label)\n", stderr) + } + } + + static func main() { + let parsed = InstallerEvent.parse(line: "TB_EVENT|preflight|hardware|pass|iMac18,2") + check("event phase parsed", parsed?.phase == "preflight") + check("event code parsed", parsed?.code == "hardware") + check("event state parsed", parsed?.state == .passed) + check("event message parsed", parsed?.message == "iMac18,2") + check( + "event message preserves separators", + InstallerEvent.parse(line: "TB_EVENT|phase|code|info|one|two")?.message == "one|two" + ) + check("ordinary output ignored", InstallerEvent.parse(line: "ordinary output") == nil) + check("malformed event ignored", InstallerEvent.parse(line: "TB_EVENT|short") == nil) + check("unknown state ignored", InstallerEvent.parse(line: "TB_EVENT|phase|code|maybe|message") == nil) + + let events = [ + InstallerEvent(phase: "preflight", code: "hardware", state: .passed, message: "hardware ok"), + InstallerEvent(phase: "preflight", code: "package", state: .warning, message: "old warning"), + InstallerEvent(phase: "preflight", code: "package", state: .passed, message: "package ok"), + InstallerEvent(phase: "preflight", code: "architecture", state: .failed, message: "wrong architecture"), + InstallerEvent(phase: "readiness", code: "link", state: .warning, message: "using Wi-Fi") + ] + let summary = InstallerEvent.summary( + from: events, + priorityCodes: ["hardware", "package", "link"] + ) + check("failure appears first", summary.first?.code == "architecture") + check("warning remains visible", summary.dropFirst().first?.code == "link") + check("resolved warning is not prioritized", summary.dropFirst(2).first?.code == "hardware") + check("summary has no duplicate codes", Set(summary.map(\.code)).count == summary.count) + check( + "summary limit is enforced", + InstallerEvent.summary(from: events, priorityCodes: ["hardware"], limit: 2).count == 2 + ) + check("zero limit returns no rows", InstallerEvent.summary(from: events, priorityCodes: [], limit: 0).isEmpty) + + print("installer event tests: \(checks) checks, \(failures) failures") + exit(failures == 0 ? 0 : 1) + } +} diff --git a/packaging/monitor-mode/tests/test_installer_events.zsh b/packaging/monitor-mode/tests/test_installer_events.zsh new file mode 100755 index 0000000..20ed218 --- /dev/null +++ b/packaging/monitor-mode/tests/test_installer_events.zsh @@ -0,0 +1,15 @@ +#!/bin/zsh +set -euo pipefail + +TEST_DIR="${0:A:h}" +SOURCE_DIR="${TEST_DIR:h}/installer-app" +BUILD_DIR="$(/usr/bin/mktemp -d /private/tmp/targetbridge-installer-event-tests.XXXXXX)" +trap '/bin/rm -R "${BUILD_DIR}"' EXIT + +/usr/bin/xcrun swiftc \ + -module-cache-path "${BUILD_DIR}/ModuleCache" \ + "${SOURCE_DIR}/TargetBridgeInstallerEvent.swift" \ + "${TEST_DIR}/InstallerEventTests.swift" \ + -o "${BUILD_DIR}/InstallerEventTests" + +"${BUILD_DIR}/InstallerEventTests"