Files
agent-pet-companion/script/validate_main_window_ui.sh
xjxandgd-ops 80f7912026 Fix Control Center presentation and time-dependent tests (#28)
## Summary / 摘要

- Make one coordinator the sole owner of Control Center presentation,
activation, exact-window registration, and key-window state across cold
launch, reopen, menu, overlay, and update paths.
- Remove the startup alpha/input gate; appearance hydration now applies
only `NSAppearance` while the window stays visible and interactive.
- Keep overlay panels passive by default and grant the session bubble
keyboard focus only through an explicit, revocable lease.
- Add bounded lifecycle diagnostics and a fail-closed cold-launch
release gate that rebuilds the exact worktree App and proves singleton
identity, layer, alpha, nonblank pixels, overlay ordering, and retained
focus.
- Replace every retention-sensitive fixed Rust event timestamp with one
current UTC fixture base plus relative offsets; the audit removed 25
calendar-dependent timestamps, including the failing 2026-07-17 cases,
imminent 2026-07-20 cases, and 2099 escape fixtures.
- Add a CI-enforced Rust source validator that follows same-file fixture
helpers, rejects fixed RFC 3339 timestamps crossing real event-retention
APIs, and permits isolated deterministic schema/serialization/migration
samples.

## Validation / 验证

- `cargo test --workspace --locked` — complete Rust workspace passed
(petcore unit 279; integration A/B/C/D 96/40/66/160; CLI and type suites
passed).
- `cargo clippy -p petcore --all-targets --locked -- -D warnings`
passed.
- `python3 script/validate_time_independent_tests.py --root .` passed;
running the same validator against the unfixed main tree reports all 25
risky timestamps.
- `python3 script/tests/test_validation_tooling.py` — 56 tests passed.
- `./script/validate_swift_tests.sh --scope all` — 749 tests passed.
- `./script/validate_app_lifecycle_contract.sh` — 16/16 passed.
- `APC_VALIDATE_HOST_UI=1 APC_MAIN_UI_COLD_LAUNCH_ITERATIONS=20
./script/validate_main_window_ui.sh` — 20/20 cold launches passed.
- Computer Use acceptance — five destinations, three bundled pets,
action preview, pet hide/show, Control Center close/reopen, and two
consecutive quit/relaunch cycles passed.
- `./script/validate_pre_push.sh` passed after the Rust and source-gate
update.
- `./script/validate_test_isolation.sh` and
`./script/validate_build_scripts_safety.sh --static-only` passed.

This PR updates the release-control-plane inventory, so it must complete
PR CI and then receive a trusted manual squash merge.

## Development claim / 开发声明

- Domain: `release-control-plane`
- Claim: `control-plane`
- Base commit: `e216a4fd13af88902cf2bd77d23939fe606c9383`
- Release preparation: `no`
- Focused validation:
  - `python3 script/tests/test_validation_tooling.py`
  - `./script/validate_build_scripts_safety.sh --static-only`

<!-- apc-engineering-coordination-v1
{"claim_overlap_rejections":1,"merge_tree_conflicts":0,"schema_version":"apc.engineering-coordination.v1","task_created_at":"2026-08-16T12:09:52Z","train_conflict_resolutions":0}
-->

---------

Co-authored-by: gd-ops <gd-ops@xmfunny.com>
2026-08-17 14:43:39 +08:00

935 lines
35 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
. "$ROOT_DIR/script/validation_helpers.sh"
apc_require_host_ui_opt_in "main window UI validation"
APP_NAME="${APC_MAIN_UI_APP_NAME:-AgentPetCompanion}"
APP_BUNDLE="$ROOT_DIR/dist/AgentPetCompanion.app"
APP_BINARY="$APP_BUNDLE/Contents/MacOS/AgentPetCompanion"
PETCORE_BINARY="$APP_BUNDLE/Contents/Resources/bin/petcore"
PETCORE_CLI="$APP_BUNDLE/Contents/Resources/bin/petcore-cli"
COLD_LAUNCH_ITERATIONS="${APC_MAIN_UI_COLD_LAUNCH_ITERATIONS:-3}"
COLD_LAUNCH_PROBE_SOURCE="$ROOT_DIR/script/fixtures/MainWindowColdLaunchProbe.swift"
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/apc-main-window-ui.XXXXXX")"
apc_use_isolated_home "$TMP_DIR"
OWNED_PROTOCOL="$APC_HOME/run/validation-owned-runtime.json"
APP_LOG="$TMP_DIR/app.log"
COLD_LAUNCH_PROBE="$TMP_DIR/main-window-cold-launch-probe"
cleanup() {
apc_stop_owned_runtime "$PETCORE_CLI" "$PETCORE_BINARY" "$OWNED_PROTOCOL"
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
# Host acceptance must exercise the current worktree, never a stale bundle
# left in dist by an earlier branch or commit. The project builder remains
# incremental when its exact source-bound build identity is unchanged.
"$ROOT_DIR/script/build_app_bundle.sh" >/dev/null
if [[ ! "$COLD_LAUNCH_ITERATIONS" =~ ^[0-9]+$ ]] \
|| ((COLD_LAUNCH_ITERATIONS < 1 || COLD_LAUNCH_ITERATIONS > 100)); then
printf 'APC_MAIN_UI_COLD_LAUNCH_ITERATIONS must be an integer from 1 through 100\n' >&2
exit 2
fi
xcrun swiftc \
-parse-as-library \
-strict-concurrency=complete \
-warnings-as-errors \
"$COLD_LAUNCH_PROBE_SOURCE" \
-o "$COLD_LAUNCH_PROBE"
apc_start_owned_runtime \
"$APP_BINARY" \
"$PETCORE_CLI" \
"$PETCORE_BINARY" \
"$APP_LOG" \
"$OWNED_PROTOCOL"
# Exercise the populated Pet Library rather than accidentally depending on a
# developer's existing pet data. All assets stay inside this validation HOME.
PET_SOURCE="$TMP_DIR/library-pet"
"$PETCORE_CLI" petpack sample --output "$PET_SOURCE" --quality standard >/dev/null
"$PETCORE_CLI" petpack import "$PET_SOURCE" >/dev/null
APP_NAME="$APP_NAME" APP_PID="$APC_OWNED_APP_PID" swift - <<'SWIFT'
import AppKit
import ApplicationServices
import CoreGraphics
import Foundation
let appName = ProcessInfo.processInfo.environment["APP_NAME"] ?? "AgentPetCompanion"
let appPID = Int32(ProcessInfo.processInfo.environment["APP_PID"] ?? "") ?? -1
guard let app = NSRunningApplication(processIdentifier: appPID),
app.executableURL?.lastPathComponent == appName || app.localizedName == appName else {
fputs("main window UI validation failed: app is not running\n", stderr)
exit(1)
}
let axApp = AXUIElementCreateApplication(app.processIdentifier)
func copy(_ element: AXUIElement, _ attr: String) -> AnyObject? {
var value: AnyObject?
return AXUIElementCopyAttributeValue(element, attr as CFString, &value) == .success ? value : nil
}
func string(_ element: AXUIElement, _ attr: String) -> String {
(copy(element, attr) as? String) ?? ""
}
func point(_ element: AXUIElement, _ attr: String) -> CGPoint? {
guard let value = copy(element, attr) else { return nil }
var point = CGPoint.zero
guard AXValueGetValue(value as! AXValue, .cgPoint, &point) else { return nil }
return point
}
func size(_ element: AXUIElement, _ attr: String) -> CGSize? {
guard let value = copy(element, attr) else { return nil }
var size = CGSize.zero
guard AXValueGetValue(value as! AXValue, .cgSize, &size) else { return nil }
return size
}
func actions(_ element: AXUIElement) -> [String] {
var value: CFArray?
guard AXUIElementCopyActionNames(element, &value) == .success else { return [] }
return value as? [String] ?? []
}
struct Node {
let element: AXUIElement
let role: String
let identifier: String
let title: String
let value: String
let description: String
let frame: CGRect?
var strings: [String] {
[title, value, description].filter { !$0.isEmpty }
}
}
func collect(_ element: AXUIElement, into nodes: inout [Node]) {
let position = point(element, kAXPositionAttribute)
let nodeSize = size(element, kAXSizeAttribute)
nodes.append(Node(
element: element,
role: string(element, kAXRoleAttribute),
identifier: string(element, kAXIdentifierAttribute),
title: string(element, kAXTitleAttribute),
value: string(element, kAXValueAttribute),
description: string(element, kAXDescriptionAttribute),
frame: position.flatMap { origin in nodeSize.map { CGRect(origin: origin, size: $0) } }
))
if let children = copy(element, kAXChildrenAttribute) as? [AXUIElement] {
for child in children {
collect(child, into: &nodes)
}
}
}
func snapshotNodes(_ root: AXUIElement) -> [Node] {
var nodes: [Node] = []
collect(root, into: &nodes)
return nodes
}
let supportedMainWindowTitles: Set<String> = [
"Agent Pet Companion",
"宠物库", "Pet Library",
"AI宠物制作", "AI 宠物制作", "AI Pet Maker",
"宠物配置", "Pet Configuration",
"Agent 连接", "Agent Connections",
"服务与诊断", "Service & Diagnostics",
]
let controlCenterWindowIdentifier = "dev.agentpet.companion.control-center"
func isControlCenterWindow(_ window: AXUIElement) -> Bool {
string(window, kAXIdentifierAttribute) == controlCenterWindowIdentifier
|| supportedMainWindowTitles.contains(string(window, kAXTitleAttribute))
}
func controlCenterIsVisiblyPresented() -> Bool {
let options = CGWindowListOption(
arrayLiteral: .optionOnScreenOnly,
.excludeDesktopElements
)
let windows = CGWindowListCopyWindowInfo(options, kCGNullWindowID)
as? [[String: Any]] ?? []
return windows.contains { window in
guard
(window[kCGWindowOwnerPID as String] as? NSNumber)?.int32Value == appPID,
(window[kCGWindowLayer as String] as? NSNumber)?.intValue == 0,
(window[kCGWindowAlpha as String] as? NSNumber)?.doubleValue ?? 0 > 0,
let bounds = window[kCGWindowBounds as String] as? [String: Any]
else {
return false
}
let width = (bounds["Width"] as? NSNumber)?.doubleValue ?? 0
let height = (bounds["Height"] as? NSNumber)?.doubleValue ?? 0
return width >= 800 && height >= 500
}
}
func waitForVisibleControlCenter() -> Bool {
for _ in 0..<40 {
if controlCenterIsVisiblyPresented() { return true }
usleep(100_000)
}
return false
}
var resolvedMainWindow: AXUIElement?
for _ in 0..<40 {
let windows = copy(axApp, kAXWindowsAttribute) as? [AXUIElement] ?? []
resolvedMainWindow = windows.first(where: isControlCenterWindow)
if resolvedMainWindow != nil {
break
}
usleep(100_000)
}
guard let mainWindow = resolvedMainWindow else {
fputs("main window UI validation failed: main window was not found\n", stderr)
exit(1)
}
guard waitForVisibleControlCenter() else {
fputs("main window UI validation failed: control center stayed transparent or off screen\n", stderr)
exit(1)
}
// A fresh production home presents onboarding before the five-page control
// center. Finish that isolated flow through its stable semantic Skip action;
// the onboarding-specific scene progression is validated separately.
var onboardingWasPresented = false
var onboardingSkipAction: AXUIElement?
// Clean-home bootstrap validates and seeds the complete bundled inventory
// before onboarding becomes authoritative. Do not mistake that bounded loading
// state for a returning user whose onboarding was already completed.
for _ in 0..<240 {
let currentNodes = snapshotNodes(mainWindow)
if currentNodes.contains(where: {
$0.identifier == "onboarding.root"
}) {
onboardingWasPresented = true
if let skip = currentNodes.first(where: {
$0.identifier == "onboarding.skip"
&& actions($0.element).contains(kAXPressAction as String)
}) {
onboardingSkipAction = skip.element
break
}
}
usleep(100_000)
}
if onboardingWasPresented {
guard let onboardingSkipAction else {
fputs("main window UI validation failed: onboarding has no enabled semantic Skip action\n", stderr)
let onboardingNodes = snapshotNodes(mainWindow)
.filter {
$0.identifier.hasPrefix("onboarding.")
|| $0.strings.contains(where: { $0.localizedCaseInsensitiveContains("skip") })
}
.map {
"\($0.role):\($0.identifier):\($0.strings.joined(separator: "/")):actions=\(actions($0.element).joined(separator: ","))"
}
.joined(separator: " | ")
if !onboardingNodes.isEmpty {
fputs("onboarding nodes: \(onboardingNodes)\n", stderr)
}
exit(1)
}
let result = AXUIElementPerformAction(
onboardingSkipAction,
kAXPressAction as CFString
)
if result != .success {
fputs("main window UI validation failed: onboarding Skip action failed: \(result.rawValue)\n", stderr)
exit(1)
}
for _ in 0..<40 {
if !snapshotNodes(mainWindow).contains(where: {
$0.identifier == "onboarding.root"
}) {
break
}
usleep(100_000)
}
if snapshotNodes(mainWindow).contains(where: {
$0.identifier == "onboarding.root"
}) {
fputs("main window UI validation failed: onboarding remained after Skip\n", stderr)
exit(1)
}
}
guard let mainSize = size(mainWindow, kAXSizeAttribute) else {
fputs("main window UI validation failed: main window has no AX size\n", stderr)
exit(1)
}
let supportedMinimumSize = CGSize(width: 760, height: 520)
if mainSize.width < supportedMinimumSize.width
|| mainSize.height < supportedMinimumSize.height {
fputs("main window UI validation failed: main window is below the supported minimum size: \(mainSize.width)x\(mainSize.height)\n", stderr)
exit(1)
}
func setMainWindowSize(_ requestedSize: CGSize, context: String) {
var validationSize = requestedSize
if let sizeValue = AXValueCreate(.cgSize, &validationSize) {
let result = AXUIElementSetAttributeValue(
mainWindow,
kAXSizeAttribute as CFString,
sizeValue
)
if result != .success {
fputs("main window UI validation failed: could not set \(context) size: \(result.rawValue)\n", stderr)
exit(1)
}
usleep(300_000)
}
}
// Exercise the real supported minimum before expanding to the all-column
// structure used to validate navigation order across all five pages. AXSize
// measures the whole NSWindow, so its height includes titlebar/toolbar chrome
// above the 520-point SwiftUI content minimum.
setMainWindowSize(supportedMinimumSize, context: "supported minimum")
guard let compactSize = size(mainWindow, kAXSizeAttribute),
abs(compactSize.width - supportedMinimumSize.width) <= 1,
compactSize.height >= supportedMinimumSize.height else {
let observed = size(mainWindow, kAXSizeAttribute)
.map { "\($0.width)x\($0.height)" }
?? "missing"
fputs("main window UI validation failed: supported minimum size was not applied; observed \(observed)\n", stderr)
exit(1)
}
var nodes: [Node] = []
for _ in 0..<300 {
nodes = snapshotNodes(mainWindow)
let libraryIsVisible = nodes.contains { $0.identifier == "pet-library.page" }
&& nodes.contains { $0.identifier == "product.pet-library.page-header" }
&& nodes.contains { $0.identifier == "pet-library.hero" }
if libraryIsVisible {
break
}
usleep(100_000)
}
func contains(_ text: String, in nodes: [Node]) -> Bool {
nodes.flatMap(\.strings).contains { $0 == text || $0.contains(text) }
}
func containsAny(_ candidates: [String], in nodes: [Node]) -> Bool {
candidates.contains { contains($0, in: nodes) }
}
func containsIdentifier(_ identifier: String, in nodes: [Node]) -> Bool {
nodes.contains { $0.identifier == identifier }
}
func requireAny(_ candidates: [String], _ context: String) {
if !containsAny(candidates, in: nodes) {
fputs("main window UI validation failed: missing \(context): \(candidates.joined(separator: " / "))\n", stderr)
if context == "bundled pet" {
let visiblePetNodes = nodes
.filter { $0.identifier.hasPrefix("pet-library.card.") }
.map {
"\($0.role):\($0.identifier):\($0.strings.joined(separator: "/"))"
}
.joined(separator: " | ")
if !visiblePetNodes.isEmpty {
fputs("visible pet cards: \(visiblePetNodes)\n", stderr)
}
}
exit(1)
}
}
func requireIdentifier(_ identifier: String, _ context: String) {
if !containsIdentifier(identifier, in: nodes) {
fputs("main window UI validation failed: missing \(context) identifier: \(identifier)\n", stderr)
let relatedNodes = nodes
.filter {
let prefix = identifier.split(separator: ".").first.map(String.init) ?? identifier
return $0.identifier.hasPrefix(prefix)
}
.map {
"\($0.role):\($0.identifier):frame=\($0.frame.map(String.init(describing:)) ?? "missing")"
}
.prefix(40)
.joined(separator: " | ")
if !relatedNodes.isEmpty {
fputs("related nodes: \(relatedNodes)\n", stderr)
}
exit(1)
}
}
func resolveVisibleControlLabel(
_ candidates: [String],
roles: Set<String>,
_ context: String
) -> String {
guard let label = candidates.first(where: { candidate in
nodes.contains { node in
(roles.contains(node.role) || actions(node.element).contains(kAXPressAction as String))
&& (node.description == candidate || node.title == candidate || node.value == candidate)
}
}) else {
let visibleControls = nodes
.filter {
roles.contains($0.role) || actions($0.element).contains(kAXPressAction as String)
}
.flatMap(\.strings)
.filter { !$0.isEmpty }
.prefix(24)
.joined(separator: " | ")
let candidateNodes = nodes
.filter { node in candidates.contains(where: { candidate in node.strings.contains(candidate) }) }
.map {
"\($0.role):\($0.strings.joined(separator: "/")) actions=\(actions($0.element).joined(separator: ","))"
}
.prefix(12)
.joined(separator: " | ")
fputs("main window UI validation failed: missing localized \(context): \(candidates.joined(separator: " / "))\n", stderr)
if !visibleControls.isEmpty {
fputs("available controls: \(visibleControls)\n", stderr)
}
if !candidateNodes.isEmpty {
fputs("candidate nodes: \(candidateNodes)\n", stderr)
}
exit(1)
}
return label
}
requireIdentifier("pet-library.page", "library page")
requireIdentifier("product.pet-library.page-header", "library page header")
requireIdentifier("pet-library.hero", "library primary experience")
requireIdentifier(
"product.pet-library.featured.primary-experience-card",
"library primary experience card"
)
requireIdentifier(
"product.pet-library.featured.pet-preview-stage",
"library pet preview"
)
requireAny(["宠物库", "Pet Library"], "library heading")
guard let compactPage = nodes.first(where: {
$0.identifier == "pet-library.page"
}), let compactPageFrame = compactPage.frame,
compactPageFrame.width > 0,
compactPageFrame.width <= supportedMinimumSize.width + 1 else {
fputs("main window UI validation failed: library did not resolve inside the supported minimum width\n", stderr)
exit(1)
}
setMainWindowSize(
CGSize(width: 1_120, height: max(720, compactSize.height)),
context: "all-column validation"
)
for _ in 0..<80 {
nodes = snapshotNodes(mainWindow)
let libraryInventoryIsVisible =
containsIdentifier("pet-library.grid", in: nodes)
&& containsAny(["全部宠物 · 4", "All Pets · 4"], in: nodes)
if libraryInventoryIsVisible {
break
}
usleep(100_000)
}
requireIdentifier("pet-library.grid", "library grid")
requireIdentifier("sidebar.navigation-list", "sidebar navigation list")
requireIdentifier("sidebar.configuration-live-preview", "sidebar live preview")
requireIdentifier("sidebar.current-pet", "sidebar current-pet row")
requireAny(["全部宠物 · 4", "All Pets · 4"], "seeded and imported pet count")
requireAny(["星雾团子"], "bundled pet")
guard let navigationFrame = nodes.first(where: {
$0.identifier == "sidebar.navigation-list"
})?.frame,
let previewFrame = nodes.first(where: {
$0.identifier == "sidebar.configuration-live-preview"
})?.frame,
let identityFrame = nodes.first(where: {
$0.identifier == "sidebar.current-pet"
})?.frame,
navigationFrame.width > 0,
previewFrame.width > 0,
previewFrame.height > 0,
identityFrame.height > 0,
previewFrame.minX >= navigationFrame.minX - 1,
previewFrame.maxX <= navigationFrame.maxX + 1,
previewFrame.maxY <= identityFrame.minY + 1,
identityFrame.minX >= navigationFrame.minX - 1,
identityFrame.maxX <= navigationFrame.maxX + 1 else {
fputs("main window UI validation failed: sidebar preview is not pinned above its identity row\n", stderr)
exit(1)
}
guard let libraryGrid = nodes.first(where: {
$0.identifier == "pet-library.grid"
}), actions(libraryGrid.element).contains("AXScrollToBottom") else {
fputs("main window UI validation failed: pet grid does not expose scroll-to-bottom\n", stderr)
exit(1)
}
let scrollToBottomResult = AXUIElementPerformAction(
libraryGrid.element,
"AXScrollToBottom" as CFString
)
if scrollToBottomResult != .success {
fputs("main window UI validation failed: pet grid could not scroll to bottom: \(scrollToBottomResult.rawValue)\n", stderr)
exit(1)
}
for _ in 0..<40 {
nodes = snapshotNodes(mainWindow)
let lastBundledRowIsVisible =
containsAny(["Bytebud 字节芽"], in: nodes)
&& containsAny(["桃蕾"], in: nodes)
if lastBundledRowIsVisible {
break
}
usleep(100_000)
}
requireAny(["Bytebud 字节芽"], "bottom pet row")
requireAny(["桃蕾"], "bottom pet row")
let libraryNavigationLabel = resolveVisibleControlLabel(
["宠物库", "Pet Library"],
roles: [kAXButtonRole as String],
"primary navigation"
)
let makerNavigationLabel = resolveVisibleControlLabel(
["AI宠物制作", "AI Pet Maker"],
roles: [kAXButtonRole as String],
"primary navigation"
)
let configurationNavigationLabel = resolveVisibleControlLabel(
["宠物配置", "Pet Configuration"],
roles: [kAXButtonRole as String],
"primary navigation"
)
let connectionsNavigationLabel = resolveVisibleControlLabel(
["Agent 连接", "Agent Connections"],
roles: [kAXButtonRole as String],
"primary navigation"
)
let diagnosticsNavigationLabel = resolveVisibleControlLabel(
["服务与诊断", "Service & Diagnostics"],
roles: [kAXButtonRole as String],
"primary navigation"
)
let scrollAreas = nodes.filter { $0.role == kAXScrollAreaRole as String }
if scrollAreas.count < 2 {
fputs("main window UI validation failed: expected sidebar and content scroll areas, found \(scrollAreas.count)\n", stderr)
exit(1)
}
let actionButtons = nodes.filter { $0.role == kAXButtonRole as String }
for label in [libraryNavigationLabel, makerNavigationLabel, configurationNavigationLabel, connectionsNavigationLabel, diagnosticsNavigationLabel] {
guard actionButtons.contains(where: { $0.description == label || $0.title == label || $0.value == label }) else {
fputs("main window UI validation failed: button not exposed: \(label)\n", stderr)
exit(1)
}
}
let actionableControls = nodes.filter {
$0.role == kAXButtonRole as String || actions($0.element).contains(kAXPressAction as String)
}
let expectedNavigationOrder = [
libraryNavigationLabel,
makerNavigationLabel,
configurationNavigationLabel,
connectionsNavigationLabel,
diagnosticsNavigationLabel
]
var resolvedNavigationNodes: [(label: String, node: Node)] = []
for label in expectedNavigationOrder {
let matches = actionableControls.filter {
$0.description == label || $0.title == label || $0.value == label
}
let semanticMatches = {
let buttons = matches.filter { $0.role == kAXButtonRole as String }
return buttons.isEmpty ? matches : buttons
}()
if semanticMatches.count != 1 {
fputs("main window UI validation failed: expected exactly one primary navigation button \(label), found \(semanticMatches.count)\n", stderr)
for match in semanticMatches {
fputs("matching navigation node: \(match.role) actions=\(actions(match.element).joined(separator: ","))\n", stderr)
}
exit(1)
}
resolvedNavigationNodes.append((label, semanticMatches[0]))
}
guard resolvedNavigationNodes.allSatisfy({ $0.node.frame != nil }) else {
fputs("main window UI validation failed: primary navigation controls have no AX frame\n", stderr)
exit(1)
}
let displayedNavigationOrder = resolvedNavigationNodes
.sorted { $0.node.frame!.midY < $1.node.frame!.midY }
.map { $0.label }
if displayedNavigationOrder != expectedNavigationOrder {
fputs(
"main window UI validation failed: primary navigation order is \(displayedNavigationOrder.joined(separator: " → "))\n",
stderr
)
exit(1)
}
func controlLabelMatches(_ node: Node, _ label: String) -> Bool {
node.description == label || node.title == label || node.value == label
}
func pressControl(_ label: String, roles: Set<String>) {
let currentNodes = snapshotNodes(mainWindow)
let roleMatch = currentNodes.first(where: {
roles.contains($0.role) && controlLabelMatches($0, label)
})
let actionableMatch = currentNodes.first(where: {
actions($0.element).contains(kAXPressAction as String) && controlLabelMatches($0, label)
})
guard let node = roleMatch ?? actionableMatch else {
fputs("main window UI validation failed: control not found for AXPress: \(label)\n", stderr)
exit(1)
}
let result = AXUIElementPerformAction(node.element, kAXPressAction as CFString)
if result != .success {
fputs("main window UI validation failed: AXPress failed for \(label): \(result.rawValue)\n", stderr)
exit(1)
}
}
func pressControl(identifier: String) {
let currentNodes = snapshotNodes(mainWindow)
guard let node = currentNodes.first(where: {
$0.identifier == identifier
&& actions($0.element).contains(kAXPressAction as String)
}) else {
fputs("main window UI validation failed: actionable control identifier not found: \(identifier)\n", stderr)
exit(1)
}
let result = AXUIElementPerformAction(node.element, kAXPressAction as CFString)
if result != .success {
fputs("main window UI validation failed: AXPress failed for \(identifier): \(result.rawValue)\n", stderr)
exit(1)
}
}
func waitFor(_ description: String, _ predicate: ([Node]) -> Bool) {
var lastNodes: [Node] = []
for _ in 0..<40 {
let currentNodes = snapshotNodes(mainWindow)
lastNodes = currentNodes
if predicate(currentNodes) {
return
}
usleep(100_000)
}
fputs("main window UI validation failed: timed out waiting for \(description)\n", stderr)
fputs("current main window title: \(string(mainWindow, kAXTitleAttribute))\n", stderr)
let visibleIdentifiers = Array(Set(lastNodes.map(\.identifier).filter { !$0.isEmpty }))
.sorted()
.prefix(80)
.joined(separator: " | ")
if !visibleIdentifiers.isEmpty {
fputs("visible identifiers: \(visibleIdentifiers)\n", stderr)
}
let visibleStrings = Array(Set(lastNodes.flatMap(\.strings).filter { !$0.isEmpty }))
.sorted()
.prefix(60)
.joined(separator: " | ")
if !visibleStrings.isEmpty {
fputs("visible strings: \(visibleStrings)\n", stderr)
}
exit(1)
}
func subtreeContainsIdentifier(_ element: AXUIElement, _ identifier: String) -> Bool {
if string(element, kAXIdentifierAttribute) == identifier {
return true
}
let children = copy(element, kAXChildrenAttribute) as? [AXUIElement] ?? []
return children.contains { subtreeContainsIdentifier($0, identifier) }
}
func requireScrollToBottom(
identifier: String,
visibleTargetIdentifier: String,
context: String
) {
let currentNodes = snapshotNodes(mainWindow)
let directScrollNode = currentNodes.first(where: {
$0.identifier == identifier
&& actions($0.element).contains("AXScrollToBottom")
})
let pagedScrollNode = currentNodes.first(where: {
$0.role == kAXScrollAreaRole as String
&& actions($0.element).contains("AXScrollDownByPage")
&& subtreeContainsIdentifier($0.element, visibleTargetIdentifier)
})
guard let scrollNode = directScrollNode ?? pagedScrollNode,
let scrollFrame = scrollNode.frame else {
fputs("main window UI validation failed: \(context) does not expose scroll-to-bottom\n", stderr)
let prefix = identifier.split(separator: ".").first.map(String.init) ?? identifier
let relatedNodes = currentNodes
.filter { $0.identifier.hasPrefix(prefix) || $0.role == kAXScrollAreaRole as String }
.map {
"\($0.role):\($0.identifier):frame=\($0.frame.map(String.init(describing:)) ?? "missing"):actions=\(actions($0.element).joined(separator: ","))"
}
.prefix(48)
.joined(separator: " | ")
if !relatedNodes.isEmpty {
fputs("related scroll nodes: \(relatedNodes)\n", stderr)
}
exit(1)
}
let scrollAction = directScrollNode == nil ? "AXScrollDownByPage" : "AXScrollToBottom"
let scrollTargetToVisibleAction = "AXScrollToVisible"
guard let windowFrame = point(mainWindow, kAXPositionAttribute).flatMap({ origin in
size(mainWindow, kAXSizeAttribute).map { CGRect(origin: origin, size: $0) }
}) else {
fputs("main window UI validation failed: control center has no frame during \(context)\n", stderr)
exit(1)
}
for _ in 0..<12 {
let refreshedNodes = snapshotNodes(mainWindow)
let targetNode = refreshedNodes.first(where: {
$0.identifier == visibleTargetIdentifier
})
if let targetFrame = targetNode?.frame,
targetFrame.width > 0,
targetFrame.height > 0,
windowFrame.intersects(targetFrame),
scrollFrame.intersects(targetFrame) {
return
}
if let targetNode,
actions(targetNode.element).contains(scrollTargetToVisibleAction) {
let targetResult = AXUIElementPerformAction(
targetNode.element,
scrollTargetToVisibleAction as CFString
)
if targetResult == .success {
usleep(150_000)
continue
}
}
let pageResult = AXUIElementPerformAction(
scrollNode.element,
scrollAction as CFString
)
if pageResult != .success {
if let scrollBarValue = copy(
scrollNode.element,
kAXVerticalScrollBarAttribute
) {
let scrollBar = scrollBarValue as! AXUIElement
let setResult = AXUIElementSetAttributeValue(
scrollBar,
kAXValueAttribute as CFString,
NSNumber(value: 1)
)
if setResult == .success {
usleep(150_000)
continue
}
}
let targetActions = targetNode
.map { actions($0.element).joined(separator: ",") }
?? "missing"
fputs(
"main window UI validation failed: \(context) could not scroll down: \(pageResult.rawValue); target actions: \(targetActions)\n",
stderr
)
exit(1)
}
usleep(150_000)
}
fputs("main window UI validation failed: \(context) bottom target remained outside the window\n", stderr)
exit(1)
}
let buttonRole = kAXButtonRole as String
pressControl(identifier: "sidebar.navigation.maker")
waitFor("AI Pet Maker page") { nodes in
containsIdentifier("maker.page", in: nodes)
&& containsIdentifier("maker.session-list", in: nodes)
&& containsIdentifier("maker.session-list.new", in: nodes)
&& containsIdentifier("maker.session-list.draft", in: nodes)
&& containsIdentifier("maker.draft", in: nodes)
&& containsIdentifier("maker.draft.discard", in: nodes)
&& containsIdentifier("maker.draft.submit", in: nodes)
&& containsIdentifier("product.maker.draft.page-header", in: nodes)
&& containsIdentifier("maker.brief", in: nodes)
&& containsIdentifier("maker.brief.description", in: nodes)
&& containsAny(["新宠物", "New Pet"], in: nodes)
&& containsAny(["参考图(可选)", "Reference Images (Optional)"], in: nodes)
&& containsAny(["开始制作", "Create Pet"], in: nodes)
&& !containsIdentifier("pet-library.page", in: nodes)
}
requireScrollToBottom(
identifier: "maker.draft",
visibleTargetIdentifier: "maker.brief.references.dropzone",
context: "AI Pet Maker draft"
)
let makerNodes = snapshotNodes(mainWindow)
let removedStudioTabLabels: Set<String> = ["新建", "New", "宠物库", "Pet Library"]
if makerNodes.contains(where: { node in
node.role == kAXRadioButtonRole as String
&& node.strings.contains(where: removedStudioTabLabels.contains)
}) {
fputs("main window UI validation failed: AI Pet Maker still exposes a removed Studio tab\n", stderr)
exit(1)
}
pressControl(identifier: "sidebar.navigation.configuration")
waitFor("Pet Configuration page") { nodes in
containsIdentifier("configuration.root", in: nodes)
&& containsIdentifier("configuration.appearance.enabled", in: nodes)
&& containsIdentifier("configuration.appearance.status-bubble", in: nodes)
&& containsIdentifier("configuration.appearance.theme", in: nodes)
&& containsIdentifier("configuration.appearance.pet-size", in: nodes)
&& containsIdentifier(
"product.configuration.appearance.advanced-details-disclosure",
in: nodes
)
}
pressControl(identifier: "sidebar.navigation.connections")
waitFor("Agent Connections page") { nodes in
containsIdentifier("connections.root", in: nodes)
&& containsIdentifier("product.connections.page-header", in: nodes)
&& containsIdentifier("connections.agent-section.codex", in: nodes)
&& containsIdentifier("connections.primary.check-all", in: nodes)
&& containsIdentifier("connections.secondary.setup-all", in: nodes)
}
requireScrollToBottom(
identifier: "connections.root",
visibleTargetIdentifier: "connections.agent-section.opencode",
context: "Agent Connections"
)
pressControl(identifier: "sidebar.navigation.diagnostics")
waitFor("Service & Diagnostics page") { nodes in
containsIdentifier("diagnostics.page", in: nodes)
&& containsIdentifier("diagnostics.layout.single-column", in: nodes)
&& containsIdentifier("diagnostics.service-details", in: nodes)
&& containsIdentifier(
"product.diagnostics.service.primary-experience-card.primary-action",
in: nodes
)
&& containsAny(["服务状态", "Service Status"], in: nodes)
&& containsAny(["诊断日志包", "Diagnostic Archive"], in: nodes)
&& containsAny(["打包并下载", "Package and Download"], in: nodes)
}
pressControl(identifier: "sidebar.navigation.library")
waitFor("Pet Library page") { nodes in
containsIdentifier("pet-library.page", in: nodes)
&& containsIdentifier("pet-library.hero", in: nodes)
&& containsIdentifier("pet-library.grid", in: nodes)
&& contains("星雾团子", in: nodes)
&& (contains("导入", in: nodes) || contains("Import", in: nodes))
}
requireScrollToBottom(
identifier: "pet-library.detail-scroll",
visibleTargetIdentifier: "pet-library.hero.technical",
context: "Pet Library detail"
)
func currentMainWindows() -> [AXUIElement] {
let windows = copy(axApp, kAXWindowsAttribute) as? [AXUIElement] ?? []
return windows.filter(isControlCenterWindow)
}
guard let closeButtonValue = copy(mainWindow, kAXCloseButtonAttribute as String) else {
fputs("main window UI validation failed: control center close action is unavailable\n", stderr)
exit(1)
}
let closeButton = closeButtonValue as! AXUIElement
guard AXUIElementPerformAction(closeButton, kAXPressAction as CFString) == .success else {
fputs("main window UI validation failed: control center close action failed\n", stderr)
exit(1)
}
for _ in 0..<40 where !currentMainWindows().isEmpty {
usleep(100_000)
}
guard currentMainWindows().isEmpty, !app.isTerminated else {
fputs("main window UI validation failed: closing the control center terminated the UI host or left the window open\n", stderr)
exit(1)
}
let activationHome = ProcessInfo.processInfo.environment["APC_HOME"] ?? ""
guard !activationHome.isEmpty else {
fputs("main window UI validation failed: APC_HOME activation scope is unavailable\n", stderr)
exit(1)
}
let activationScope = URL(
fileURLWithPath: activationHome,
isDirectory: true
).standardizedFileURL.path
DistributedNotificationCenter.default().postNotificationName(
Notification.Name("dev.agentpet.companion.activate-running-instance"),
object: activationScope,
userInfo: nil,
deliverImmediately: true
)
var reopenedWindows: [AXUIElement] = []
for _ in 0..<40 {
reopenedWindows = currentMainWindows()
if reopenedWindows.count == 1 { break }
usleep(100_000)
}
guard reopenedWindows.count == 1, !app.isTerminated else {
fputs("main window UI validation failed: activation did not reopen exactly one control center\n", stderr)
exit(1)
}
guard waitForVisibleControlCenter() else {
fputs("main window UI validation failed: reopened control center stayed transparent or off screen\n", stderr)
exit(1)
}
print("Main window UI and close/reopen lifecycle validation ok")
SWIFT
# The structural acceptance above runs once against the complete product. The
# bounded loop below then repeatedly tears down and cold-starts the same owned
# App/runtime while varying the coordinator request timing. Every iteration
# proves the exact singleton identifier, layer-0 visibility, positive alpha,
# nonblank pixels, overlay presentation, and retained Control Center focus.
apc_stop_owned_runtime "$PETCORE_CLI" "$PETCORE_BINARY" "$OWNED_PROTOCOL"
COLD_LAUNCH_JITTERS=(0 1 16 33 83 167 333 667 1000 1500)
for ((iteration = 1; iteration <= COLD_LAUNCH_ITERATIONS; iteration += 1)); do
jitter_index=$(((iteration - 1) % ${#COLD_LAUNCH_JITTERS[@]}))
export APC_CONTROL_CENTER_LAUNCH_JITTER_MS="${COLD_LAUNCH_JITTERS[$jitter_index]}"
APP_LOG="$TMP_DIR/cold-launch-$iteration.log"
apc_start_owned_runtime \
"$APP_BINARY" \
"$PETCORE_CLI" \
"$PETCORE_BINARY" \
"$APP_LOG" \
"$OWNED_PROTOCOL"
APP_PID="$APC_OWNED_APP_PID" "$COLD_LAUNCH_PROBE"
apc_stop_owned_runtime "$PETCORE_CLI" "$PETCORE_BINARY" "$OWNED_PROTOCOL"
done
unset APC_CONTROL_CENTER_LAUNCH_JITTER_MS
printf 'Main window cold-launch stress validation ok: %s iterations\n' \
"$COLD_LAUNCH_ITERATIONS"