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>
This commit is contained in:
xjxandgd-ops authored and GitHub committed 2026-08-17 14:43:39 +08:00
1 parent e216a4fd13
commit 80f7912026
25 files changed
+1359 -652

No files matched your search

@@ -61,11 +61,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
guard AppSingleInstanceCoordinator.shared.claim == .primary else { return }
AppDiagnostics.shared.log(.notice, category: "lifecycle", event: "app_did_finish_launching")
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
// Defer one turn so SwiftUI installs the primary activation handler
// and AppKit completes its launch-time scene ordering before the
// shared presenter fronts the registered Control Center window.
DispatchQueue.main.async {
// The presentation coordinator owns activation and key-window state.
// A bounded host-validation-only delay exercises both SwiftUI setup
// orderings without changing production launch timing.
ControlCenterColdLaunchTiming.schedule {
AppSingleInstanceCoordinator.shared.activatePrimaryInstance()
}
}
@@ -208,7 +207,7 @@ struct AgentPetCompanionApp: App {
.apcInterfaceLanguage(store)
.background {
if AppLaunchMode.manualInstallationRequest == nil {
InitialAppearanceWindowGateView(
InitialAppearanceWindowView(
readiness: store.initialAppearanceReadiness,
theme: store.behavior.appearanceTheme
)
@@ -412,7 +411,6 @@ private struct AppStatusItemLabel: View {
.onAppear {
store.setMainWindowPresenter {
openWindow(id: "main")
NSApp.activate(ignoringOtherApps: true)
}
}
}
@@ -508,7 +506,7 @@ private struct MainWindowContent: View {
.background {
ZStack {
if AppLaunchMode.manualInstallationRequest == nil {
InitialAppearanceWindowGateView(
InitialAppearanceWindowView(
readiness: store.initialAppearanceReadiness,
theme: store.behavior.appearanceTheme
)
@@ -9,20 +9,6 @@ enum OverlayBubbleDisclosureDirection: Equatable {
case collapsing
}
enum InitialOverlayFocusPolicy {
static func shouldRestoreControlCenter(
controlCenterWasKeyBeforePresentation: Bool,
appIsActive: Bool,
controlCenterIsOpen: Bool,
windowIsVisible: Bool
) -> Bool {
controlCenterWasKeyBeforePresentation
&& appIsActive
&& controlCenterIsOpen
&& windowIsVisible
}
}
enum OverlayBubbleDisclosureAction: Equatable {
case revealBubble
case revealCollapsedStandaloneStack
@@ -790,6 +776,10 @@ final class AppStore: ObservableObject {
private let overlayPlacementJournalStore: OverlayPlacementJournalStore
private let agentSessionRouteOpener: AgentSessionRouteOpener
private let runtimeHandoffIfNeeded: RuntimeHandoffCheck
private let controlCenterWindowProvider:
ControlCenterPresentationCoordinator.WindowProvider
private let controlCenterApplicationActivator:
ControlCenterPresentationCoordinator.ApplicationActivator
private let applicationAppearanceApplier: ApplicationAppearanceApplier
private let applicationLanguageApplier: ApplicationLanguageApplier
private let overlayPresenter: OverlayPresenter
@@ -827,14 +817,34 @@ final class AppStore: ObservableObject {
private var behaviorMutationTask: Task<Void, Never>?
private var behaviorMutationSequence: UInt64 = 0
private var pendingBehaviorMutationCount = 0
private var mainWindowPresenter: (() -> Void)?
private weak var controlCenterWindow: NSWindow?
private var controlCenterCloseObserver: AnyCancellable?
private var connectionsModelObservation: AnyCancellable?
private(set) var controlCenterIsOpen = false
private var pendingMainWindowPresentation = false
private var pendingMainWindowPresentationChecksRuntimeHandoff = true
private var pendingControlCenterFronting = false
private lazy var controlCenterPresentationCoordinator =
ControlCenterPresentationCoordinator(
identifier: Self.controlCenterWindowIdentifier,
windowProvider: controlCenterWindowProvider,
activateApplication: controlCenterApplicationActivator,
runtimeHandoffIfNeeded: { [weak self] in
self?.runtimeHandoffIfNeeded() ?? false
},
onWindowOpened: { [weak self] _ in
guard let self else { return }
self.overlayController.controlCenterDidOpen()
self.diagnostics.log(
.debug,
category: "lifecycle",
event: "control_center_opened"
)
},
onWindowClosed: { [weak self] in
guard let self else { return }
self.overlayController.controlCenterDidClose()
self.diagnostics.log(
.debug,
category: "lifecycle",
event: "control_center_closed"
)
}
)
private var generationMessagesTask: Task<Void, Never>?
private var latestGenerationRestoreAttemptState = LatestGenerationRestoreAttemptState.notAttempted
private var latestGenerationRestoreAttemptSequence: UInt64 = 0
@@ -885,6 +895,14 @@ final class AppStore: ObservableObject {
static let controlCenterWindowIdentifier = NSUserInterfaceItemIdentifier(
"dev.agentpet.companion.control-center"
)
var controlCenterIsOpen: Bool {
controlCenterPresentationCoordinator.isOpen
}
var controlCenterPresentationPhase: ControlCenterPresentationCoordinator.Phase {
controlCenterPresentationCoordinator.phase
}
private static let defaultOverlayKeyboardFocusHandler: OverlayKeyboardFocusHandler = { controller, action in
switch action {
case .bubbleSessions:
@@ -915,6 +933,10 @@ final class AppStore: ObservableObject {
runtimeHandoffIfNeeded = {
AppUpdateHandoffCoordinator.shared.restartIfInstalledBuildChanged()
}
controlCenterWindowProvider = { NSApp?.windows ?? [] }
controlCenterApplicationActivator = {
NSApp?.activate(ignoringOtherApps: true)
}
applicationAppearanceApplier = { theme in
APCApplicationAppearance.apply(theme)
}
@@ -969,6 +991,10 @@ final class AppStore: ObservableObject {
.failed(.applicationUnavailable)
},
runtimeHandoffIfNeeded: @escaping RuntimeHandoffCheck = { false },
controlCenterWindowProvider: @escaping
ControlCenterPresentationCoordinator.WindowProvider = { [] },
controlCenterApplicationActivator: @escaping
ControlCenterPresentationCoordinator.ApplicationActivator = {},
applicationAppearanceApplier: @escaping ApplicationAppearanceApplier = { theme in
APCApplicationAppearance.apply(theme)
},
@@ -1007,6 +1033,8 @@ final class AppStore: ObservableObject {
self.overlayPlacementJournalStore = overlayPlacementJournalStore
self.agentSessionRouteOpener = agentSessionRouteOpener
self.runtimeHandoffIfNeeded = runtimeHandoffIfNeeded
self.controlCenterWindowProvider = controlCenterWindowProvider
self.controlCenterApplicationActivator = controlCenterApplicationActivator
self.applicationAppearanceApplier = applicationAppearanceApplier
self.applicationLanguageApplier = applicationLanguageApplier
self.overlayPresenter = overlayPresenter
@@ -1511,44 +1539,11 @@ final class AppStore: ObservableObject {
}
func setMainWindowPresenter(_ presenter: @escaping () -> Void) {
mainWindowPresenter = presenter
guard pendingMainWindowPresentation else { return }
let checksRuntimeHandoff = pendingMainWindowPresentationChecksRuntimeHandoff
pendingMainWindowPresentation = false
pendingMainWindowPresentationChecksRuntimeHandoff = true
guard !checksRuntimeHandoff || !runtimeHandoffIfNeeded() else {
pendingControlCenterFronting = false
return
}
presenter()
NSApp?.activate(ignoringOtherApps: true)
controlCenterPresentationCoordinator.installPresenter(presenter)
}
func registerControlCenterWindow(_ window: NSWindow) {
guard controlCenterWindow !== window else { return }
controlCenterCloseObserver?.cancel()
window.identifier = Self.controlCenterWindowIdentifier
controlCenterWindow = window
controlCenterIsOpen = true
overlayController.controlCenterDidOpen(window: window)
controlCenterCloseObserver = NotificationCenter.default.publisher(
for: NSWindow.willCloseNotification,
object: window
)
.sink { [weak self, weak window] _ in
Task { @MainActor in
guard let self, self.controlCenterWindow === window else {
return
}
self.controlCenterIsOpen = false
self.overlayController.controlCenterDidClose()
}
}
if pendingControlCenterFronting {
pendingControlCenterFronting = false
window.makeKeyAndOrderFront(nil)
NSApp?.activate(ignoringOtherApps: true)
}
controlCenterPresentationCoordinator.register(window)
}
func presentManualAppInstallation(_ request: AppManualInstallationRequest) {
@@ -1636,28 +1631,20 @@ final class AppStore: ObservableObject {
}
private func presentMainWindow(checkRuntimeHandoff: Bool) {
guard !checkRuntimeHandoff || !runtimeHandoffIfNeeded() else { return }
pendingControlCenterFronting = true
if frontExistingMainWindow() {
pendingControlCenterFronting = false
pendingMainWindowPresentation = false
pendingMainWindowPresentationChecksRuntimeHandoff = true
NSApp?.activate(ignoringOtherApps: true)
return
}
if let mainWindowPresenter {
pendingMainWindowPresentation = false
pendingMainWindowPresentationChecksRuntimeHandoff = true
mainWindowPresenter()
} else {
// A secondary launch or Dock reopen can arrive before either scene
// has installed its openWindow presenter. Replay exactly once when
// the first presenter becomes available.
pendingMainWindowPresentation = true
pendingMainWindowPresentationChecksRuntimeHandoff = checkRuntimeHandoff
}
NSApp?.activate(ignoringOtherApps: true)
controlCenterPresentationCoordinator.requestPresentation(
checkRuntimeHandoff: checkRuntimeHandoff
)
diagnostics.log(
.debug,
category: "lifecycle",
event: "control_center_presentation_requested",
metadata: [
"phase": .string(
controlCenterPresentationCoordinator.phase.diagnosticValue
),
"runtime_handoff_checked": .bool(checkRuntimeHandoff),
]
)
}
func presentAgentSession(
@@ -1898,31 +1885,11 @@ final class AppStore: ObservableObject {
}
}
private func frontExistingMainWindow() -> Bool {
guard let application = NSApp else {
return false
}
let window = if let controlCenterWindow,
Self.isMainWindowCandidate(controlCenterWindow) {
controlCenterWindow
} else {
application.windows.first(where: Self.isMainWindowCandidate)
}
guard let window else { return false }
if window.isMiniaturized {
window.deminiaturize(nil)
}
window.makeKeyAndOrderFront(nil)
controlCenterIsOpen = true
overlayController.controlCenterDidOpen(window: window)
return true
}
static func isMainWindowCandidate(_ window: NSWindow) -> Bool {
window.identifier == controlCenterWindowIdentifier
&& !(window is NSPanel)
&& window.level == .normal
&& window.styleMask.contains(.titled)
ControlCenterPresentationCoordinator.isCandidate(
window,
identifier: controlCenterWindowIdentifier
)
}
private enum RuntimeBootstrapStartMode {
@@ -1969,9 +1936,9 @@ final class AppStore: ObservableObject {
appUpdateConvergenceState = .updating
}
setServiceChecking()
// Bound the invisible window gate from bootstrap start. Runtime
// replacement and rollback can take much longer than appearance
// hydration, and must not make the App look dead while it is checking.
// Bound appearance hydration from bootstrap start. Runtime replacement
// and rollback can take much longer, so the already-visible system
// appearance must remain the explicit fallback while they run.
scheduleInitialAppearanceFallback()
let startResult = switch startMode {
case .ensureRunning:
@@ -2105,9 +2072,9 @@ final class AppStore: ObservableObject {
// focused behavior request is suspended. Once that snapshot is
// authoritative, never publish this older focused result.
guard initialAppearanceReadiness != .authoritative else { return }
// The publication order is deliberate: every window gate observing
// readiness must see behavior, revision, and AppKit appearance as
// one fully prepared initial presentation state.
// The publication order is deliberate: every window appearance
// observer sees behavior, revision, and AppKit appearance as one
// fully prepared initial presentation state.
authoritativeBehavior = versioned.behavior
behavior = versioned.behavior
behaviorRevision = versioned.revision
@@ -3017,33 +2984,8 @@ final class AppStore: ObservableObject {
hasLoadedStateSnapshot,
!hasPresentedOverlay
else { return }
let keyControlCenterWindow: NSWindow? = if controlCenterIsOpen,
controlCenterWindow?.isKeyWindow == true {
controlCenterWindow
} else {
nil
}
hasPresentedOverlay = true
overlayPresenter(overlayController, self)
guard let keyControlCenterWindow else { return }
// The resident panels can become key for explicit keyboard navigation,
// but their first ordering must not take the cold-launch focus away
// from an already-key Control Center. Defer until AppKit has completed
// the overlay ordering turn, and never reactivate an App the user has
// already left.
DispatchQueue.main.async { [weak self, weak keyControlCenterWindow] in
guard let self, let keyControlCenterWindow,
self.controlCenterWindow === keyControlCenterWindow,
InitialOverlayFocusPolicy.shouldRestoreControlCenter(
controlCenterWasKeyBeforePresentation: true,
appIsActive: NSApp?.isActive == true,
controlCenterIsOpen: self.controlCenterIsOpen,
windowIsVisible: keyControlCenterWindow.isVisible
)
else { return }
keyControlCenterWindow.makeKeyAndOrderFront(nil)
}
}
private func reconcileActiveGeneration(_ snapshot: ActiveGenerationSnapshot) {
@@ -0,0 +1,261 @@
import AppKit
enum ControlCenterColdLaunchTiming {
static let maximumValidationJitterMilliseconds = 1_500
static func validationJitterMilliseconds(
environment: [String: String] = ProcessInfo.processInfo.environment
) -> Int {
guard environment["APC_VALIDATE_HOST_UI"] == "1",
let rawValue = environment["APC_CONTROL_CENTER_LAUNCH_JITTER_MS"],
let value = Int(rawValue),
(0...maximumValidationJitterMilliseconds).contains(value)
else {
return 0
}
return value
}
@MainActor
static func schedule(_ action: @escaping @MainActor () -> Void) {
let delay = validationJitterMilliseconds()
if delay == 0 {
DispatchQueue.main.async(execute: action)
} else {
DispatchQueue.main.asyncAfter(
deadline: .now() + .milliseconds(delay),
execute: action
)
}
}
}
/// The single presentation owner for the product's Control Center window.
///
/// SwiftUI may install its `openWindow` presenter and attach the concrete
/// `NSWindow` in either order. Every launch, reopen, menu, overlay, and update
/// request is reduced to one desired presentation here, then fulfilled only
/// when the exact singleton window can be made visible and key.
@MainActor
final class ControlCenterPresentationCoordinator: NSObject {
enum Phase: Equatable {
case closed
case waitingForPresenter
case waitingForWindow
case presented
var diagnosticValue: String {
switch self {
case .closed: "closed"
case .waitingForPresenter: "waiting_for_presenter"
case .waitingForWindow: "waiting_for_window"
case .presented: "presented"
}
}
}
typealias WindowProvider = @MainActor () -> [NSWindow]
typealias ApplicationActivator = @MainActor () -> Void
typealias RuntimeHandoffCheck = @MainActor () -> Bool
typealias WindowHandler = @MainActor (NSWindow) -> Void
typealias CloseHandler = @MainActor () -> Void
private let identifier: NSUserInterfaceItemIdentifier
private let windowProvider: WindowProvider
private let activateApplication: ApplicationActivator
private let runtimeHandoffIfNeeded: RuntimeHandoffCheck
private let onWindowOpened: WindowHandler
private let onWindowClosed: CloseHandler
private var presenter: (() -> Void)?
private weak var window: NSWindow?
private weak var observedWindow: NSWindow?
private var wantsPresentation = false
private var presenterInvokedForOutstandingRequest = false
private var runtimeHandoffCheckRequired = true
private(set) var phase = Phase.closed
private(set) var isOpen = false
var registeredWindow: NSWindow? {
guard let window, Self.isCandidate(window, identifier: identifier) else {
return nil
}
return window
}
init(
identifier: NSUserInterfaceItemIdentifier,
windowProvider: @escaping WindowProvider,
activateApplication: @escaping ApplicationActivator,
runtimeHandoffIfNeeded: @escaping RuntimeHandoffCheck,
onWindowOpened: @escaping WindowHandler,
onWindowClosed: @escaping CloseHandler
) {
self.identifier = identifier
self.windowProvider = windowProvider
self.activateApplication = activateApplication
self.runtimeHandoffIfNeeded = runtimeHandoffIfNeeded
self.onWindowOpened = onWindowOpened
self.onWindowClosed = onWindowClosed
super.init()
}
func installPresenter(_ presenter: @escaping () -> Void) {
self.presenter = presenter
fulfillOutstandingRequestIfPossible()
}
func register(_ window: NSWindow) {
window.identifier = identifier
guard Self.isCandidate(window, identifier: identifier) else { return }
let publishesOpenTransition = self.window !== window || !isOpen
if self.window !== window {
if let observedWindow {
NotificationCenter.default.removeObserver(
self,
name: NSWindow.willCloseNotification,
object: observedWindow
)
}
self.window = window
observedWindow = window
NotificationCenter.default.addObserver(
self,
selector: #selector(controlCenterWindowWillClose(_:)),
name: NSWindow.willCloseNotification,
object: window
)
}
isOpen = true
phase = .presented
if publishesOpenTransition {
onWindowOpened(window)
}
if wantsPresentation {
front(window)
}
}
@objc
private func controlCenterWindowWillClose(_ notification: Notification) {
guard let closingWindow = notification.object as? NSWindow,
window === closingWindow
else {
return
}
window = nil
observedWindow = nil
wantsPresentation = false
presenterInvokedForOutstandingRequest = false
runtimeHandoffCheckRequired = true
isOpen = false
phase = .closed
onWindowClosed()
}
func requestPresentation(checkRuntimeHandoff: Bool) {
if wantsPresentation {
// A forced recovery or installation guide must not be weakened by
// an earlier ordinary request that was still waiting on SwiftUI.
runtimeHandoffCheckRequired =
runtimeHandoffCheckRequired && checkRuntimeHandoff
} else {
runtimeHandoffCheckRequired = checkRuntimeHandoff
}
wantsPresentation = true
fulfillOutstandingRequestIfPossible()
}
static func isCandidate(
_ window: NSWindow,
identifier: NSUserInterfaceItemIdentifier
) -> Bool {
window.identifier == identifier
&& !(window is NSPanel)
&& window.level == .normal
&& window.styleMask.contains(.titled)
}
private func fulfillOutstandingRequestIfPossible() {
guard wantsPresentation else { return }
if let window = resolveWindow() {
if wantsPresentation {
front(window)
}
return
}
guard let presenter else {
phase = .waitingForPresenter
return
}
guard !presenterInvokedForOutstandingRequest else {
phase = .waitingForWindow
return
}
guard authorizePresentationIfNeeded() else { return }
presenterInvokedForOutstandingRequest = true
phase = .waitingForWindow
presenter()
// `openWindow(id:)` normally attaches the AppKit window later, but a
// test presenter or future scene implementation may do so inline.
if let window = resolveWindow(), wantsPresentation {
front(window)
}
}
private func resolveWindow() -> NSWindow? {
if let registeredWindow {
return registeredWindow
}
guard let candidate = windowProvider().first(where: {
Self.isCandidate($0, identifier: identifier)
}) else {
return nil
}
register(candidate)
return candidate
}
private func authorizePresentationIfNeeded() -> Bool {
guard runtimeHandoffCheckRequired else { return true }
guard !runtimeHandoffIfNeeded() else {
wantsPresentation = false
presenterInvokedForOutstandingRequest = false
runtimeHandoffCheckRequired = true
phase = isOpen ? .presented : .closed
return false
}
runtimeHandoffCheckRequired = false
return true
}
private func front(_ window: NSWindow) {
guard authorizePresentationIfNeeded() else { return }
if window.isMiniaturized {
window.deminiaturize(nil)
}
// Presentation invariants belong here. No appearance hydration or
// overlay update may leave the Control Center transparent, unable to
// receive input, behind another app, or represented by another window.
window.alphaValue = 1
window.ignoresMouseEvents = false
activateApplication()
window.makeKeyAndOrderFront(nil)
self.window = window
wantsPresentation = false
presenterInvokedForOutstandingRequest = false
runtimeHandoffCheckRequired = true
isOpen = true
phase = .presented
}
}
@@ -0,0 +1,84 @@
import AppKit
import AgentPetCompanionCore
import SwiftUI
enum InitialAppearanceReadiness: Equatable {
case pending
case authoritative
case unavailable
}
enum InitialAppearanceWindowPolicy {
static func appearanceName(
for readiness: InitialAppearanceReadiness,
theme: AppearanceTheme
) -> NSAppearance.Name? {
switch readiness {
case .pending, .unavailable:
nil
case .authoritative:
APCApplicationAppearance.appearanceName(for: theme)
}
}
}
/// Applies the persisted appearance to AppKit window chrome without ever
/// gating the window's visibility or input. While PetCore is still hydrating,
/// the window is immediately usable with the system appearance; an
/// authoritative setting is an ordinary idempotent appearance update.
struct InitialAppearanceWindowView: NSViewRepresentable {
let readiness: InitialAppearanceReadiness
let theme: AppearanceTheme
func makeNSView(context: Context) -> InitialAppearanceWindowHostView {
InitialAppearanceWindowHostView(readiness: readiness, theme: theme)
}
func updateNSView(_ nsView: InitialAppearanceWindowHostView, context: Context) {
nsView.update(readiness: readiness, theme: theme)
}
}
@MainActor
final class InitialAppearanceWindowHostView: NSView {
private(set) var readiness: InitialAppearanceReadiness
private(set) var theme: AppearanceTheme
private weak var configuredWindow: NSWindow?
init(readiness: InitialAppearanceReadiness, theme: AppearanceTheme) {
self.readiness = readiness
self.theme = theme
super.init(frame: .zero)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
nil
}
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
applyAppearance()
}
func update(readiness: InitialAppearanceReadiness, theme: AppearanceTheme) {
self.readiness = readiness
self.theme = theme
applyAppearance()
}
private func applyAppearance() {
guard let window else { return }
let appearanceName = InitialAppearanceWindowPolicy.appearanceName(
for: readiness,
theme: theme
)
let nextAppearance = appearanceName.flatMap(NSAppearance.init(named:))
guard configuredWindow !== window
|| window.appearance?.name != nextAppearance?.name
else { return }
configuredWindow = window
window.appearance = nextAppearance
}
}
@@ -1,247 +0,0 @@
import AppKit
import AgentPetCompanionCore
import SwiftUI
enum InitialAppearanceReadiness: Equatable {
case pending
case authoritative
case unavailable
}
enum InitialAppearanceWindowGate {
static func shouldRevealWindow(for readiness: InitialAppearanceReadiness) -> Bool {
readiness != .pending
}
static func action(
for readiness: InitialAppearanceReadiness,
theme: AppearanceTheme,
hasRevealed: Bool
) -> InitialAppearanceWindowGateAction {
switch readiness {
case .pending:
hasRevealed ? .noChange : .conceal
case .authoritative:
.reveal(appearanceName: APCApplicationAppearance.appearanceName(for: theme))
case .unavailable:
.reveal(appearanceName: nil)
}
}
}
enum InitialAppearanceWindowGateAction: Equatable {
case conceal
case reveal(appearanceName: NSAppearance.Name?)
case noChange
}
/// Hides AppKit chrome as well as SwiftUI content until the persisted theme is
/// known. A SwiftUI opacity modifier cannot cover a title-bar flash, so the
/// gate controls the owning NSWindow without ordering it on screen or taking
/// input focus.
struct InitialAppearanceWindowGateView: NSViewRepresentable {
let readiness: InitialAppearanceReadiness
let theme: AppearanceTheme
func makeNSView(context: Context) -> InitialAppearanceWindowGateHostView {
InitialAppearanceWindowGateHostView(readiness: readiness, theme: theme)
}
func updateNSView(_ nsView: InitialAppearanceWindowGateHostView, context: Context) {
nsView.update(readiness: readiness, theme: theme)
}
static func dismantleNSView(
_ nsView: InitialAppearanceWindowGateHostView,
coordinator: Void
) {
nsView.detachFromWindow()
}
}
@MainActor
private enum InitialAppearanceWindowGateCoordinatorStore {
static let coordinators =
NSMapTable<NSWindow, InitialAppearanceWindowGateWindowCoordinator>
.weakToStrongObjects()
static func coordinator(
for window: NSWindow
) -> InitialAppearanceWindowGateWindowCoordinator {
if let coordinator = coordinators.object(forKey: window) {
return coordinator
}
let coordinator = InitialAppearanceWindowGateWindowCoordinator(window: window)
coordinators.setObject(coordinator, forKey: window)
return coordinator
}
}
/// One coordinator owns the shared NSWindow state even when SwiftUI replaces
/// or temporarily duplicates its representable host views. The window must
/// never depend on a transient host surviving until the deferred reveal turn.
@MainActor
private final class InitialAppearanceWindowGateWindowCoordinator {
private weak var window: NSWindow?
private var owners: Set<ObjectIdentifier> = []
private var resolvedReadiness = InitialAppearanceReadiness.pending
private var theme = AppearanceTheme.system
private var originalAlphaValue: CGFloat?
private var originalIgnoresMouseEvents: Bool?
private var isConcealed = false
private var revealScheduled = false
private(set) var hasRevealed = false
init(window: NSWindow) {
self.window = window
}
func attach(
owner: InitialAppearanceWindowGateHostView,
readiness: InitialAppearanceReadiness,
theme: AppearanceTheme
) {
owners.insert(ObjectIdentifier(owner))
update(readiness: readiness, theme: theme)
}
func update(readiness: InitialAppearanceReadiness, theme: AppearanceTheme) {
self.theme = theme
if readiness != .pending {
resolvedReadiness = readiness
}
applyGate()
}
func detach(owner: InitialAppearanceWindowGateHostView) {
owners.remove(ObjectIdentifier(owner))
guard owners.isEmpty, isConcealed else { return }
// A representable can disappear between updateNSView and the next
// main-queue turn. Fail open with the system appearance instead of
// leaving the whole App permanently transparent and non-interactive.
if resolvedReadiness == .pending {
resolvedReadiness = .unavailable
}
scheduleReveal()
}
private func applyGate() {
guard let window else { return }
switch InitialAppearanceWindowGate.action(
for: resolvedReadiness,
theme: theme,
hasRevealed: hasRevealed
) {
case .conceal:
conceal(window)
case .noChange:
return
case let .reveal(appearanceName):
window.appearance = appearanceName.flatMap(NSAppearance.init(named:))
scheduleReveal()
}
}
private func conceal(_ window: NSWindow) {
guard !isConcealed, !hasRevealed else { return }
// A second representable may attach after the first already concealed
// this window. Window-scoped ownership prevents it from recording zero
// alpha or ignored mouse events as the state that should be restored.
originalAlphaValue = window.alphaValue > 0 ? window.alphaValue : 1
originalIgnoresMouseEvents = window.alphaValue > 0
? window.ignoresMouseEvents
: false
window.alphaValue = 0
window.ignoresMouseEvents = true
isConcealed = true
}
private func scheduleReveal() {
guard let window else { return }
guard isConcealed else {
hasRevealed = true
return
}
guard !revealScheduled else { return }
revealScheduled = true
// `updateNSView` runs while SwiftUI may still be rendering its
// NSHostingView. Defer the AppKit mutation, but keep its ownership in
// this window-retained coordinator rather than the transient host.
DispatchQueue.main.async { [self, weak window] in
revealScheduled = false
guard let window, self.window === window else { return }
guard resolvedReadiness != .pending || owners.isEmpty else { return }
NSAnimationContext.runAnimationGroup { context in
context.duration = 0
context.allowsImplicitAnimation = false
window.alphaValue = originalAlphaValue ?? 1
}
window.ignoresMouseEvents = originalIgnoresMouseEvents ?? false
originalAlphaValue = nil
originalIgnoresMouseEvents = nil
isConcealed = false
hasRevealed = true
}
}
}
@MainActor
final class InitialAppearanceWindowGateHostView: NSView {
private(set) var readiness: InitialAppearanceReadiness
private(set) var theme: AppearanceTheme
var hasRevealed: Bool {
windowCoordinator?.hasRevealed ?? false
}
private weak var controlledWindow: NSWindow?
private var windowCoordinator: InitialAppearanceWindowGateWindowCoordinator?
init(readiness: InitialAppearanceReadiness, theme: AppearanceTheme) {
self.readiness = readiness
self.theme = theme
super.init(frame: .zero)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
nil
}
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
attachToWindowIfNeeded()
}
func update(readiness: InitialAppearanceReadiness, theme: AppearanceTheme) {
self.readiness = readiness
self.theme = theme
attachToWindowIfNeeded()
windowCoordinator?.update(readiness: readiness, theme: theme)
}
func detachFromWindow() {
if let windowCoordinator {
windowCoordinator.detach(owner: self)
}
windowCoordinator = nil
controlledWindow = nil
}
private func attachToWindowIfNeeded() {
guard controlledWindow !== window else { return }
detachFromWindow()
guard let window else { return }
let coordinator = InitialAppearanceWindowGateCoordinatorStore.coordinator(
for: window
)
controlledWindow = window
windowCoordinator = coordinator
coordinator.attach(owner: self, readiness: readiness, theme: theme)
}
}
@@ -18,7 +18,6 @@ struct OverlayDesktopVisibilityPolicy {
func shouldHide(
rendererValidationActive: Bool,
controlCenterWindowIsConcealed: Bool,
frontmostApplicationIsFinder: Bool,
finderHasRegularWindow: Bool,
hasVisibleRegularApplicationWindow: Bool
@@ -30,10 +29,6 @@ struct OverlayDesktopVisibilityPolicy {
guard controlCenterIsOpen, !rendererValidationActive else {
return false
}
// The initial appearance gate may briefly conceal the real control
// center while applying persisted presentation. That transition is
// not the system Show Desktop gesture and must never hide the pet.
guard !controlCenterWindowIsConcealed else { return false }
if frontmostApplicationIsFinder {
return !finderHasRegularWindow
}
@@ -115,7 +110,6 @@ final class PetOverlayController {
private var desktopVisibilityTimer: Timer?
private var hiddenForDesktop = false
private var desktopVisibilityPolicy = OverlayDesktopVisibilityPolicy()
private weak var controlCenterWindow: NSWindow?
private let controlPresentation = OverlayControlPresentationState()
private let interactionPresentation = OverlayInteractionPresentationState()
private var controlPanelFadeOutTask: Task<Void, Never>?
@@ -219,7 +213,7 @@ final class PetOverlayController {
bubblePanel.ignoresMouseEvents = false
bubblePanel.acceptsMouseMovedEvents = true
bubblePanel.isMovableByWindowBackground = false
bubblePanel.becomesKeyOnlyIfNeeded = false
bubblePanel.becomesKeyOnlyIfNeeded = true
bubblePanel.collectionBehavior = Self.companionCollectionBehavior
bubblePanel.canHide = true
bubblePanel.hidesOnDeactivate = false
@@ -411,13 +405,11 @@ final class PetOverlayController {
}
}
func controlCenterDidOpen(window: NSWindow) {
controlCenterWindow = window
func controlCenterDidOpen() {
desktopVisibilityPolicy.controlCenterDidOpen()
}
func controlCenterDidClose() {
controlCenterWindow = nil
desktopVisibilityPolicy.controlCenterDidClose()
hiddenForDesktop = false
guard store?.behavior.enabled == true else { return }
@@ -1000,10 +992,6 @@ final class PetOverlayController {
}
return desktopVisibilityPolicy.shouldHide(
rendererValidationActive: false,
controlCenterWindowIsConcealed:
controlCenterWindow.map {
$0.alphaValue <= 0 || $0.ignoresMouseEvents
} ?? false,
frontmostApplicationIsFinder: frontmostApplicationIsFinder,
finderHasRegularWindow: finderHasRegularWindow,
hasVisibleRegularApplicationWindow:
@@ -1088,12 +1076,13 @@ final class BubbleOverlayPanel: NSPanel {
private let pointerMonitor = OverlayPointerEventMonitor()
private var clickMenuTarget: BubbleClickMenuTarget?
private var pendingBubbleClick: OverlayBubblePressGesture?
private(set) var keyboardNavigationLeaseIsActive = false
override func constrainFrameRect(_ frameRect: NSRect, to screen: NSScreen?) -> NSRect {
frameRect
}
override var canBecomeKey: Bool { true }
override var canBecomeKey: Bool { keyboardNavigationLeaseIsActive }
override var canBecomeMain: Bool { false }
override func performDrag(with event: NSEvent) {}
@@ -1113,6 +1102,7 @@ final class BubbleOverlayPanel: NSPanel {
func stopPointerTracking() {
pointerMonitor.stop()
pendingBubbleClick = nil
endKeyboardNavigation()
setIgnoresMouseEventsIfNeeded(false)
}
@@ -1131,12 +1121,28 @@ final class BubbleOverlayPanel: NSPanel {
}
func beginKeyboardNavigation() {
beginKeyboardNavigationLease()
setIgnoresMouseEventsIfNeeded(false)
makeKeyAndOrderFront(nil)
_ = makeFirstResponder(contentView)
selectNextKeyView(nil)
}
func beginKeyboardNavigationLease() {
keyboardNavigationLeaseIsActive = true
}
func endKeyboardNavigation() {
guard keyboardNavigationLeaseIsActive else { return }
if isKeyWindow {
resignKey()
} else {
keyboardNavigationLeaseIsActive = false
pendingBubbleClick = nil
onKeyboardNavigationChanged?(false)
}
}
override func becomeKey() {
super.becomeKey()
setIgnoresMouseEventsIfNeeded(false)
@@ -1144,9 +1150,13 @@ final class BubbleOverlayPanel: NSPanel {
}
override func resignKey() {
let hadKeyboardNavigationLease = keyboardNavigationLeaseIsActive
super.resignKey()
keyboardNavigationLeaseIsActive = false
pendingBubbleClick = nil
onKeyboardNavigationChanged?(false)
if hadKeyboardNavigationLease {
onKeyboardNavigationChanged?(false)
}
refreshPointerPassthrough()
}
@@ -1457,7 +1467,7 @@ final class ControlOverlayPanel: NSPanel {
frameRect
}
override var canBecomeKey: Bool { true }
override var canBecomeKey: Bool { false }
override var canBecomeMain: Bool { false }
override func becomeKey() {
@@ -1522,7 +1532,7 @@ private final class OverlayPanel: NSPanel {
frameRect
}
override var canBecomeKey: Bool { true }
override var canBecomeKey: Bool { false }
override var canBecomeMain: Bool { false }
override func performDrag(with event: NSEvent) {}
@@ -4,7 +4,7 @@ import SwiftUI
import Testing
@testable import AgentPetCompanion
@Suite
@Suite(.serialized)
struct AppLifecycleContractTests {
@MainActor
@Test
@@ -25,13 +25,12 @@ struct AppLifecycleContractTests {
onReady: { _ in }
)
)
let window = NSPanel(
let window = NSWindow(
contentRect: .zero,
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
defer { window.close() }
store.registerControlCenterWindow(window)
#expect(store.controlCenterIsOpen)
@@ -40,7 +39,6 @@ struct AppLifecycleContractTests {
name: NSWindow.willCloseNotification,
object: window
)
await Task.yield()
#expect(!store.controlCenterIsOpen)
}
@@ -52,7 +50,6 @@ struct AppLifecycleContractTests {
#expect(policy.shouldHide(
rendererValidationActive: false,
controlCenterWindowIsConcealed: false,
frontmostApplicationIsFinder: true,
finderHasRegularWindow: false,
hasVisibleRegularApplicationWindow: false
@@ -62,14 +59,12 @@ struct AppLifecycleContractTests {
#expect(!policy.shouldHide(
rendererValidationActive: false,
controlCenterWindowIsConcealed: false,
frontmostApplicationIsFinder: true,
finderHasRegularWindow: false,
hasVisibleRegularApplicationWindow: false
))
#expect(!policy.shouldHide(
rendererValidationActive: false,
controlCenterWindowIsConcealed: false,
frontmostApplicationIsFinder: false,
finderHasRegularWindow: false,
hasVisibleRegularApplicationWindow: false
@@ -84,32 +79,22 @@ struct AppLifecycleContractTests {
#expect(policy.shouldHide(
rendererValidationActive: false,
controlCenterWindowIsConcealed: false,
frontmostApplicationIsFinder: false,
finderHasRegularWindow: false,
hasVisibleRegularApplicationWindow: false
))
#expect(!policy.shouldHide(
rendererValidationActive: false,
controlCenterWindowIsConcealed: false,
frontmostApplicationIsFinder: false,
finderHasRegularWindow: false,
hasVisibleRegularApplicationWindow: true
))
#expect(!policy.shouldHide(
rendererValidationActive: true,
controlCenterWindowIsConcealed: false,
frontmostApplicationIsFinder: true,
finderHasRegularWindow: false,
hasVisibleRegularApplicationWindow: false
))
#expect(!policy.shouldHide(
rendererValidationActive: false,
controlCenterWindowIsConcealed: true,
frontmostApplicationIsFinder: false,
finderHasRegularWindow: false,
hasVisibleRegularApplicationWindow: false
))
}
@MainActor
@@ -127,10 +112,12 @@ struct AppLifecycleContractTests {
store.presentMainWindow()
store.presentMainWindow()
#expect(store.controlCenterPresentationPhase == .waitingForPresenter)
store.setMainWindowPresenter {
presentations += 1
}
#expect(presentations == 1)
#expect(store.controlCenterPresentationPhase == .waitingForWindow)
store.setMainWindowPresenter {
presentations += 1
@@ -138,6 +125,42 @@ struct AppLifecycleContractTests {
#expect(presentations == 1)
}
@MainActor
@Test
func presenterAndWindowRegistrationOrderConvergeOnOneKeyPresentation() {
let store = AppStore(
bootstrapHooks: AppStoreBootstrapHooks(
ensureRunning: { .alreadyHealthy },
recover: { .alreadyHealthy },
refreshSnapshot: { _ in },
onReady: { _ in }
),
runtimeHandoffIfNeeded: { false }
)
let window = ControlCenterFrontRecordingWindow(
contentRect: NSRect(x: 0, y: 0, width: 640, height: 480),
styleMask: [.titled, .closable, .resizable],
backing: .buffered,
defer: false
)
var presentations = 0
store.setMainWindowPresenter {
presentations += 1
}
store.presentMainWindow()
store.presentMainWindow()
#expect(presentations == 1)
#expect(store.controlCenterPresentationPhase == .waitingForWindow)
store.registerControlCenterWindow(window)
#expect(window.frontRequestCount == 1)
#expect(store.controlCenterPresentationPhase == .presented)
#expect(store.controlCenterIsOpen)
}
@MainActor
@Test
func coldLaunchPresentationFrontsTheWindowWhenRegistrationWinsTheRace() {
@@ -160,12 +183,42 @@ struct AppLifecycleContractTests {
store.presentMainWindow()
#expect(window.frontRequestCount == 0)
window.alphaValue = 0
window.ignoresMouseEvents = true
store.registerControlCenterWindow(window)
#expect(window.frontRequestCount == 1)
#expect(window.alphaValue == 1)
#expect(!window.ignoresMouseEvents)
#expect(store.controlCenterIsOpen)
}
@Test
func coldLaunchJitterIsBoundedAndHostValidationOnly() {
#expect(ControlCenterColdLaunchTiming.validationJitterMilliseconds(
environment: [:]
) == 0)
#expect(ControlCenterColdLaunchTiming.validationJitterMilliseconds(
environment: [
"APC_VALIDATE_HOST_UI": "0",
"APC_CONTROL_CENTER_LAUNCH_JITTER_MS": "750",
]
) == 0)
#expect(ControlCenterColdLaunchTiming.validationJitterMilliseconds(
environment: [
"APC_VALIDATE_HOST_UI": "1",
"APC_CONTROL_CENTER_LAUNCH_JITTER_MS": "750",
]
) == 750)
#expect(ControlCenterColdLaunchTiming.validationJitterMilliseconds(
environment: [
"APC_VALIDATE_HOST_UI": "1",
"APC_CONTROL_CENTER_LAUNCH_JITTER_MS": "1501",
]
) == 0)
}
@MainActor
@Test
func aboutWindowFirstDoesNotInterceptAControlCenterReopen() {
@@ -271,7 +324,9 @@ struct AppLifecycleContractTests {
#expect(store.appUpdateConvergenceState == .idle)
#expect(store.manualAppInstallationRequest == request)
#expect(presentations == 2)
// Both forced recovery requests target the same outstanding singleton
// scene presentation; the visible guide state updates independently.
#expect(presentations == 1)
}
@Test
@@ -307,7 +362,7 @@ struct AppLifecycleContractTests {
in: appSource
))
#expect(LifecycleSource.matches(
#"applicationDidFinishLaunching[\s\S]*?DispatchQueue\.main\.async\s*\{\s*AppSingleInstanceCoordinator\.shared\.activatePrimaryInstance\(\)"#,
#"applicationDidFinishLaunching[\s\S]*?ControlCenterColdLaunchTiming\.schedule\s*\{\s*AppSingleInstanceCoordinator\.shared\.activatePrimaryInstance\(\)"#,
in: appSource
))
#expect(LifecycleSource.matches(
@@ -337,10 +392,7 @@ struct AppLifecycleContractTests {
))
#expect(overlaySource.contains("onOpenMainWindow: { store.presentMainWindow() }"))
#expect(appStoreSource.contains(
"private func presentMainWindow(checkRuntimeHandoff: Bool)"
))
#expect(appStoreSource.contains(
"guard !checkRuntimeHandoff || !runtimeHandoffIfNeeded() else { return }"
"controlCenterPresentationCoordinator.requestPresentation("
))
#expect(appSource.components(separatedBy: ".sheet(").count - 1 == 1)
#expect(appSource.contains(
@@ -21,26 +21,6 @@ struct ControlCenterShellTests {
#expect(window.titlebarSeparatorStyle == .none)
}
@Test
func initialOverlayOnlyRestoresAnActiveVisibleKeyControlCenter() {
let scenarios = [
(true, true, true, true, true),
(false, true, true, true, false),
(true, false, true, true, false),
(true, true, false, true, false),
(true, true, true, false, false),
]
for scenario in scenarios {
#expect(InitialOverlayFocusPolicy.shouldRestoreControlCenter(
controlCenterWasKeyBeforePresentation: scenario.0,
appIsActive: scenario.1,
controlCenterIsOpen: scenario.2,
windowIsVisible: scenario.3
) == scenario.4)
}
}
@Test
func navigationUsesTheFixedProductOrderAndTypedSelection() {
let items = ControlCenterNavigationPresentation.items(
@@ -40,7 +40,7 @@ struct ControlOverlayPanelTests {
}
@Test
func bubblePanelSupportsExplicitFullKeyboardNavigation() {
func bubblePanelRequiresAnExplicitKeyboardNavigationLease() {
let panel = BubbleOverlayPanel(
contentRect: NSRect(x: 0, y: 0, width: 320, height: 180),
styleMask: [.borderless, .nonactivatingPanel],
@@ -48,8 +48,18 @@ struct ControlOverlayPanelTests {
defer: false
)
#expect(panel.canBecomeKey)
#expect(!panel.canBecomeKey)
#expect(!panel.canBecomeMain)
panel.beginKeyboardNavigationLease()
#expect(panel.keyboardNavigationLeaseIsActive)
#expect(panel.canBecomeKey)
panel.endKeyboardNavigation()
#expect(!panel.keyboardNavigationLeaseIsActive)
#expect(!panel.canBecomeKey)
}
@Test
@@ -65,10 +75,20 @@ struct ControlOverlayPanelTests {
focusTransitions.append($0)
}
panel.beginKeyboardNavigationLease()
panel.becomeKey()
panel.resignKey()
#expect(focusTransitions == [true, false])
#expect(!panel.canBecomeKey)
}
@Test
func ordinaryControlPanelCanNeverBecomeKey() {
let panel = makePanel()
#expect(!panel.canBecomeKey)
#expect(!panel.canBecomeMain)
}
@Test
@@ -7,74 +7,28 @@ import Testing
@Suite(.serialized)
struct InitialAppearanceBootstrapTests {
@Test
func windowGateOnlyHoldsWhileAuthoritativeAppearanceIsPending() {
#expect(!InitialAppearanceWindowGate.shouldRevealWindow(for: .pending))
#expect(InitialAppearanceWindowGate.shouldRevealWindow(for: .authoritative))
#expect(InitialAppearanceWindowGate.shouldRevealWindow(for: .unavailable))
#expect(InitialAppearanceWindowGate.action(
func windowAppearanceUsesSystemFallbackUntilAuthoritativeThemeArrives() {
#expect(InitialAppearanceWindowPolicy.appearanceName(
for: .pending,
theme: .system,
hasRevealed: false
) == .conceal)
#expect(InitialAppearanceWindowGate.action(
theme: .dark
) == nil)
#expect(InitialAppearanceWindowPolicy.appearanceName(
for: .authoritative,
theme: .dark,
hasRevealed: false
) == .reveal(appearanceName: .darkAqua))
#expect(InitialAppearanceWindowGate.action(
theme: .dark
) == .darkAqua)
#expect(InitialAppearanceWindowPolicy.appearanceName(
for: .authoritative,
theme: .light,
hasRevealed: false
) == .reveal(appearanceName: .aqua))
#expect(InitialAppearanceWindowGate.action(
theme: .light
) == .aqua)
#expect(InitialAppearanceWindowPolicy.appearanceName(
for: .unavailable,
theme: .dark,
hasRevealed: false
) == .reveal(appearanceName: nil))
#expect(InitialAppearanceWindowGate.action(
for: .pending,
theme: .dark,
hasRevealed: true
) == .noChange)
theme: .dark
) == nil)
}
@MainActor
@Test
func windowGateRevealDefersPresentationWithoutForcingContentLayout() async {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 640, height: 480),
styleMask: [.titled],
backing: .buffered,
defer: false
)
let contentView = InitialAppearanceLayoutProbe(frame: window.contentLayoutRect)
let gate = InitialAppearanceWindowGateHostView(
readiness: .pending,
theme: .system
)
window.contentView = contentView
contentView.addSubview(gate)
#expect(window.alphaValue == 0)
#expect(window.ignoresMouseEvents)
contentView.reset()
gate.update(readiness: .authoritative, theme: .dark)
#expect(contentView.layoutPasses == 0)
#expect(contentView.displayPasses == 0)
#expect(window.alphaValue == 0)
await waitForNextMainQueueTurn()
#expect(window.alphaValue == 1)
#expect(!window.ignoresMouseEvents)
#expect(gate.hasRevealed)
}
@MainActor
@Test
func windowGateRevealSurvivesHostRemovalBeforeDeferredTurn() async {
func pendingAppearanceNeverConcealsOrDisablesTheWindow() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 640, height: 480),
styleMask: [.titled],
@@ -82,28 +36,27 @@ struct InitialAppearanceBootstrapTests {
defer: false
)
let contentView = NSView(frame: window.contentLayoutRect)
var gate: InitialAppearanceWindowGateHostView? =
InitialAppearanceWindowGateHostView(
readiness: .pending,
theme: .system
)
let appearanceView = InitialAppearanceWindowHostView(
readiness: .pending,
theme: .system
)
window.contentView = contentView
contentView.addSubview(gate!)
contentView.addSubview(appearanceView)
#expect(window.alphaValue == 0)
gate?.update(readiness: .authoritative, theme: .dark)
gate?.removeFromSuperview()
gate = nil
#expect(window.alphaValue == 1)
#expect(!window.ignoresMouseEvents)
#expect(window.appearance == nil)
await waitForNextMainQueueTurn()
appearanceView.update(readiness: .authoritative, theme: .dark)
#expect(window.appearance?.name == .darkAqua)
#expect(window.alphaValue == 1)
#expect(!window.ignoresMouseEvents)
}
@MainActor
@Test
func windowGateReplacementCannotRecordConcealedStateAsOriginal() async {
func appearanceHostReplacementCannotChangeVisibilityOrInputState() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 640, height: 480),
styleMask: [.titled],
@@ -111,63 +64,22 @@ struct InitialAppearanceBootstrapTests {
defer: false
)
let contentView = NSView(frame: window.contentLayoutRect)
let firstGate = InitialAppearanceWindowGateHostView(
readiness: .pending,
theme: .system
)
let replacementGate = InitialAppearanceWindowGateHostView(
readiness: .pending,
theme: .system
)
window.contentView = contentView
contentView.addSubview(firstGate)
contentView.addSubview(replacementGate)
#expect(window.alphaValue == 0)
#expect(window.ignoresMouseEvents)
replacementGate.update(readiness: .authoritative, theme: .light)
await waitForNextMainQueueTurn()
let firstHost = InitialAppearanceWindowHostView(
readiness: .pending,
theme: .system
)
let replacementHost = InitialAppearanceWindowHostView(
readiness: .authoritative,
theme: .light
)
contentView.addSubview(firstHost)
firstHost.removeFromSuperview()
contentView.addSubview(replacementHost)
#expect(window.alphaValue == 1)
#expect(!window.ignoresMouseEvents)
#expect(firstGate.hasRevealed)
#expect(replacementGate.hasRevealed)
}
@MainActor
@Test
func pendingWindowGateFailsOpenWhenItsLastHostIsDismantled() async {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 640, height: 480),
styleMask: [.titled],
backing: .buffered,
defer: false
)
let contentView = NSView(frame: window.contentLayoutRect)
let gate = InitialAppearanceWindowGateHostView(
readiness: .pending,
theme: .system
)
window.contentView = contentView
contentView.addSubview(gate)
#expect(window.alphaValue == 0)
gate.detachFromWindow()
await waitForNextMainQueueTurn()
#expect(window.alphaValue == 1)
#expect(!window.ignoresMouseEvents)
}
@MainActor
private func waitForNextMainQueueTurn() async {
await withCheckedContinuation { continuation in
DispatchQueue.main.async {
continuation.resume()
}
}
#expect(window.appearance?.name == .aqua)
}
@MainActor
@@ -219,7 +131,7 @@ struct InitialAppearanceBootstrapTests {
@MainActor
@Test
func behaviorFetchFailureKeepsTheGateThroughSnapshotAttemptThenFallsBack() async {
func behaviorFetchFailureKeepsAppearancePendingThroughSnapshotThenFallsBack() async {
let probe = InitialAppearanceProbe()
let store = AppStore(
bootstrapHooks: AppStoreBootstrapHooks(
@@ -245,7 +157,7 @@ struct InitialAppearanceBootstrapTests {
@MainActor
@Test
func behaviorFetchFailureReleasesTheWindowBeforeAStalledReadyPipelineCompletes() async {
func behaviorFetchFailurePublishesFallbackBeforeAStalledReadyPipelineCompletes() async {
let gate = InitialAppearanceReadyGate()
let fallback = InitialAppearanceFallbackProbe()
let store = AppStore(
@@ -598,7 +510,7 @@ struct InitialAppearanceBootstrapTests {
@MainActor
@Test
func petCoreStartupFailureAlsoReleasesTheWindowGateToFallbackAppearance() async {
func petCoreStartupFailureAlsoPublishesTheSystemAppearanceFallback() async {
let probe = InitialAppearanceProbe()
let store = AppStore(
bootstrapHooks: AppStoreBootstrapHooks(
@@ -919,27 +831,6 @@ private actor InitialAppearanceFallbackProbe {
}
}
@MainActor
private final class InitialAppearanceLayoutProbe: NSView {
private(set) var layoutPasses = 0
private(set) var displayPasses = 0
override func layoutSubtreeIfNeeded() {
layoutPasses += 1
super.layoutSubtreeIfNeeded()
}
override func displayIfNeeded() {
displayPasses += 1
super.displayIfNeeded()
}
func reset() {
layoutPasses = 0
displayPasses = 0
}
}
private actor InitialBootstrapRaceProbe {
private var ensureAttempts = 0
private var recoveryAttempts = 0
@@ -0,0 +1,7 @@
{
"schema_version": "apc.changelog-fragment.v1",
"kind": "Fixed",
"scope": "macos",
"summary_en": "Control Center presentation now has one focus-safe window owner, nonblocking appearance hydration, and a repeated cold-launch release gate.",
"summary_zh": "控制中心现由单一且焦点安全的窗口协调器展示,主题加载不再阻塞窗口,并新增重复冷启动发布门禁。"
}
+35 -15
View File
@@ -1497,6 +1497,16 @@ fn sidecar_path(path: &Path, suffix: &str) -> PathBuf {
mod tests {
use super::*;
use serde_json::json;
use std::sync::OnceLock;
use time::{format_description::well_known::Rfc3339, Duration as TimeDuration, OffsetDateTime};
fn retained_event_timestamp(offset_seconds: i64) -> String {
static BASE: OnceLock<OffsetDateTime> = OnceLock::new();
(*BASE.get_or_init(|| OffsetDateTime::now_utc() - TimeDuration::minutes(1))
+ TimeDuration::seconds(offset_seconds))
.format(&Rfc3339)
.expect("format retained event fixture timestamp")
}
fn sqlite_contention_error(code: ErrorCode, extended_code: i32) -> PetCoreError {
PetCoreError::Sqlite(rusqlite::Error::SqliteFailure(
@@ -2553,7 +2563,7 @@ mod tests {
"diagnostic": diagnostic,
"affects_activity": affects_activity
}),
created_at: "2026-07-20T00:00:00Z".to_string(),
created_at: retained_event_timestamp(0),
}
}
@@ -2630,7 +2640,7 @@ mod tests {
"terminal_app": fixture.terminal_app,
"session_open_url": fixture.session_open_url
}),
created_at: "2026-07-20T00:00:00Z".to_string(),
created_at: retained_event_timestamp(0),
}
}
@@ -2712,7 +2722,7 @@ mod tests {
Some("warp://session/0123456789abcdef0123456789abcdef"),
),
);
current_cli_old.created_at = "2026-07-20T00:00:10Z".to_string();
current_cli_old.created_at = retained_event_timestamp(10);
let mut current_app_new = codex_runtime_marker(CodexRuntimeMarkerFixture::new(
"current-app-new",
"current-session",
@@ -2720,7 +2730,7 @@ mod tests {
"PostToolUse",
"chatgpt_app",
));
current_app_new.created_at = "2026-07-20T00:00:01Z".to_string();
current_app_new.created_at = retained_event_timestamp(1);
for marker in [
current_cli_old,
current_app_new,
@@ -2883,7 +2893,7 @@ mod tests {
event.payload_json["outcome"] = json!("session_closed");
event.payload_json["session_active"] = json!(false);
event.payload_json["session_open"] = json!(false);
event.created_at = "2026-07-20T00:01:00Z".to_string();
event.created_at = retained_event_timestamp(60);
event
};
let listed_closure = closure(listed_thread);
@@ -2994,30 +3004,40 @@ mod tests {
let database = Database::new(temp.path().join("events.sqlite"));
database.init().unwrap();
for event in [
message_event("user-first", "user", "first prompt", "2026-07-20T00:00:00Z"),
message_event(
"user-first",
"user",
"first prompt",
&retained_event_timestamp(0),
),
message_event(
"assistant-first",
"assistant",
"first answer",
"2026-07-20T00:00:01Z",
&retained_event_timestamp(1),
),
message_event(
"user-latest",
"user",
"next prompt",
&retained_event_timestamp(2),
),
message_event("user-latest", "user", "next prompt", "2026-07-20T00:00:02Z"),
message_event(
"assistant-empty",
"assistant",
" ",
"2026-07-20T00:00:03Z",
&retained_event_timestamp(3),
),
message_event(
"assistant-latest",
"assistant",
"latest answer",
"2026-07-20T00:00:04Z",
&retained_event_timestamp(4),
),
session_title_event(
"title-latest",
"Generated session title",
"2026-07-20T00:00:05Z",
&retained_event_timestamp(5),
),
] {
assert_eq!(
@@ -3072,7 +3092,7 @@ mod tests {
"before",
"user",
"before",
"2026-07-20T00:00:00Z",
&retained_event_timestamp(0),
))
.unwrap();
let old_revision = database.state_revision().unwrap();
@@ -3095,7 +3115,7 @@ mod tests {
enum_name(AgentEventType::Start),
AgentEventType::Start.zh_label(),
"{not-json",
"2026-07-20T00:00:01Z",
retained_event_timestamp(1),
],
)
.unwrap();
@@ -3140,7 +3160,7 @@ mod tests {
"before",
"user",
"before",
"2026-07-20T00:00:00Z",
&retained_event_timestamp(0),
))
.unwrap();
@@ -3157,7 +3177,7 @@ mod tests {
"concurrent",
"assistant",
"concurrent answer",
"2026-07-20T00:00:01Z",
&retained_event_timestamp(1),
))
.unwrap();
})
@@ -5,7 +5,16 @@ use petcore_types::{
AgentVerification, BehaviorSettings, ConnectionCheckMode,
};
use serde_json::json;
use std::sync::{Arc, Barrier};
use std::sync::{Arc, Barrier, OnceLock};
use time::{format_description::well_known::Rfc3339, Duration, OffsetDateTime};
fn fixture_timestamp(offset_seconds: i64) -> String {
static BASE: OnceLock<OffsetDateTime> = OnceLock::new();
(*BASE.get_or_init(|| OffsetDateTime::now_utc() - Duration::minutes(1))
+ Duration::seconds(offset_seconds))
.format(&Rfc3339)
.expect("format retained event fixture timestamp")
}
fn event(id: &str, kind: AgentEventType, source_event: &str, turn_id: &str) -> AgentEvent {
AgentEvent {
@@ -23,7 +32,7 @@ fn event(id: &str, kind: AgentEventType, source_event: &str, turn_id: &str) -> A
"affects_activity": true,
"session_active": !matches!(kind, AgentEventType::Done | AgentEventType::Failed)
}),
created_at: "2026-07-17T10:00:00Z".to_string(),
created_at: fixture_timestamp(0),
}
}
@@ -47,11 +56,7 @@ fn metadata_only_connector_events_never_drive_the_pet_state() {
preferred_app_navigation_payload: None,
tool_activity_run_marker: None,
}];
let now = time::OffsetDateTime::parse(
"2026-07-17T10:00:01Z",
&time::format_description::well_known::Rfc3339,
)
.unwrap();
let now = OffsetDateTime::parse(&fixture_timestamp(1), &Rfc3339).unwrap();
assert!(
agent_state::select_active_agent_state(&BehaviorSettings::default(), &candidates, now)
.is_none()
@@ -114,7 +119,7 @@ fn newer_metadata_does_not_hide_the_latest_activity_for_a_session() {
assert_eq!(candidates[0].event.event_type, AgentEventType::Tool);
let mut done = event("evt-done", AgentEventType::Done, "Stop", "prompt-one");
done.created_at = "2026-07-17T10:00:01Z".to_string();
done.created_at = fixture_timestamp(1);
database.insert_event(&done).unwrap();
let mut config = event(
"evt-config",
@@ -122,7 +127,7 @@ fn newer_metadata_does_not_hide_the_latest_activity_for_a_session() {
"ConfigChange",
"prompt-one",
);
config.created_at = "2026-07-17T10:00:02Z".to_string();
config.created_at = fixture_timestamp(2);
config.payload_json["affects_activity"] = json!(false);
database.insert_event(&config).unwrap();
@@ -156,7 +161,7 @@ fn concurrent_single_source_status_writes_preserve_all_four_agents() {
verification: AgentVerification::default(),
capabilities: AgentConnectorCapabilities::default(),
check_mode: ConnectionCheckMode::Runtime,
checked_at: "2026-07-17T10:00:00Z".to_string(),
checked_at: fixture_timestamp(0),
};
barrier.wait();
database.upsert_connection_status(&status).unwrap();
@@ -8,7 +8,7 @@ use serde_json::{json, Value};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
use time::{format_description::well_known::Rfc3339, Duration, OffsetDateTime};
fn received_at() -> &'static str {
// Ordinary fixtures stay inside the retention window without embedding a
@@ -22,6 +22,13 @@ fn received_at() -> &'static str {
})
.as_str()
}
fn received_at_offset(seconds: i64) -> String {
(OffsetDateTime::parse(received_at(), &Rfc3339).expect("parse retained fixture timestamp")
+ Duration::seconds(seconds))
.format(&Rfc3339)
.expect("format retained fixture timestamp")
}
const FORBIDDEN_VALUES: &[&str] = &[
"FORBIDDEN_PROMPT_VALUE_7f16",
"FORBIDDEN_COMMAND_VALUE_b8c2",
@@ -918,17 +925,18 @@ fn event_retention_prunes_oldest_rows() {
let temp = tempfile::tempdir().unwrap();
let database = Database::new(temp.path().join("events.sqlite"));
database.init().unwrap();
let oldest_created_at = received_at_offset(0);
for (id, created_at) in [
("oldest", "2099-07-01T00:00:00Z"),
("middle", "2099-07-02T00:00:00Z"),
("newest", "2099-07-03T00:00:00Z"),
("oldest", oldest_created_at.clone()),
("middle", received_at_offset(1)),
("newest", received_at_offset(2)),
] {
database
.insert_event(&strict_event(
id,
AgentSource::ClaudeCode,
Some("session"),
created_at,
&created_at,
))
.unwrap();
}
@@ -947,17 +955,18 @@ fn event_retention_prunes_oldest_rows() {
let recent = database.recent_events(10).unwrap();
assert_eq!(recent.len(), 2);
assert!(recent.iter().all(|event| event.id != "oldest"));
let summarized_day = &oldest_created_at[..10];
let summarized: i64 = Connection::open(database.path())
.unwrap()
.query_row(
r#"
SELECT event_count
FROM agent_event_daily_counts
WHERE event_day = '2099-07-01'
WHERE event_day = ?1
AND source = 'claude_code'
AND event_type = 'tool'
"#,
[],
params![summarized_day],
|row| row.get(0),
)
.unwrap();
@@ -971,15 +980,15 @@ fn session_alias_authority_survives_restart_migration_and_tracks_event_retention
let database = Database::new(path.clone());
database.init().unwrap();
for (id, session_id, created_at) in [
("alias-a-first", "raw-session-a", "2026-07-20T00:00:00Z"),
("alias-b-first", "raw-session-b", "2026-07-20T00:00:01Z"),
("alias-a-first", "raw-session-a", received_at_offset(0)),
("alias-b-first", "raw-session-b", received_at_offset(1)),
] {
database
.insert_event(&strict_event(
id,
AgentSource::ClaudeCode,
Some(session_id),
created_at,
&created_at,
))
.unwrap();
}
@@ -1037,7 +1046,7 @@ fn session_alias_authority_survives_restart_migration_and_tracks_event_retention
"alias-a-newest",
AgentSource::ClaudeCode,
Some("raw-session-a"),
"2026-07-20T00:00:02Z",
&received_at_offset(2),
))
.unwrap();
assert_eq!(read_aliases(), initial);
@@ -1054,7 +1063,7 @@ fn session_alias_authority_survives_restart_migration_and_tracks_event_retention
"alias-c-after-retention",
AgentSource::ClaudeCode,
Some("raw-session-c"),
"2026-07-20T00:00:03Z",
&received_at_offset(3),
))
.unwrap();
assert_eq!(
+8 -2
View File
@@ -108,7 +108,7 @@
"apps/macos/Sources/AgentPetCompanion/App/AppRuntimeLifecycle.swift",
"apps/macos/Sources/AgentPetCompanion/App/BrandAssets.swift",
"apps/macos/Sources/AgentPetCompanion/App/BundledPetInventory.swift",
"apps/macos/Sources/AgentPetCompanion/App/InitialAppearanceWindowGate.swift",
"apps/macos/Sources/AgentPetCompanion/App/InitialAppearanceWindowAppearance.swift",
"apps/macos/Sources/AgentPetCompanion/App/Localization.swift",
"apps/macos/Sources/AgentPetCompanion/App/LocalizationKeys/**",
"apps/macos/Sources/AgentPetCompanion/App/PackagedResourceBundle.swift",
@@ -177,7 +177,7 @@
"apps/macos/Sources/AgentPetCompanion/App/AppRuntimeLifecycle.swift",
"apps/macos/Sources/AgentPetCompanion/App/BrandAssets.swift",
"apps/macos/Sources/AgentPetCompanion/App/BundledPetInventory.swift",
"apps/macos/Sources/AgentPetCompanion/App/InitialAppearanceWindowGate.swift",
"apps/macos/Sources/AgentPetCompanion/App/InitialAppearanceWindowAppearance.swift",
"apps/macos/Sources/AgentPetCompanion/App/Localization.swift",
"apps/macos/Sources/AgentPetCompanion/App/LocalizationKeys/**",
"apps/macos/Sources/AgentPetCompanion/App/PackagedResourceBundle.swift",
@@ -522,12 +522,14 @@
"script/development_flow.py",
"script/engineering_metrics.py",
"script/interaction-contract-files.txt",
"script/fixtures/MainWindowColdLaunchProbe.swift",
"script/resolve_release_source_proof.py",
"script/release_source_proof.py",
"script/swift_build_artifact.py",
"script/validate_build_scripts_safety.sh",
"script/validate_local_tests.sh",
"script/validate_local_tests.py",
"script/validate_main_window_ui.sh",
"script/validate_overlay_offline.sh",
"script/validate_portable_pet_maker.sh",
"script/validate_rust_test_shards.py",
@@ -535,6 +537,7 @@
"script/validate_security_boundaries.sh",
"script/validate_swift_tests.sh",
"script/validate_test_isolation.sh",
"script/validate_time_independent_tests.py",
"script/validation_scope.py"
],
"dependencies": [
@@ -673,12 +676,14 @@
"script/development_flow.py",
"script/engineering_metrics.py",
"script/interaction-contract-files.txt",
"script/fixtures/MainWindowColdLaunchProbe.swift",
"script/resolve_release_source_proof.py",
"script/release_source_proof.py",
"script/swift_build_artifact.py",
"script/validate_build_scripts_safety.sh",
"script/validate_local_tests.py",
"script/validate_local_tests.sh",
"script/validate_main_window_ui.sh",
"script/validate_overlay_offline.sh",
"script/validate_portable_pet_maker.sh",
"script/validate_rust_test_shards.py",
@@ -686,6 +691,7 @@
"script/validate_security_boundaries.sh",
"script/validate_swift_tests.sh",
"script/validate_test_isolation.sh",
"script/validate_time_independent_tests.py",
"script/validation_scope.py"
]
},
+1 -1
View File
@@ -58,7 +58,7 @@ The App composition root retains cross-feature orchestration in `AppStore`, whil
4. After the launch-critical snapshot path and any release connector convergence complete, the App runs one full check of all four Agent connections and projects actionable failures into a control-center-wide notice that opens Agent Connections. An Agent App or CLI that is not installed remains informational on Agent Connections and never enters this global notice.
5. Subsequent state arrives through revision-based `state.wait`; the App does not read SQLite or poll bundle files.
Cold launch and reopen use the registered control-center window as their single fronting authority. Initial desktop-pet presentation preserves an already-key control center, but never reactivates the App after the user has switched away; keyboard focus moves to the session bubble only through its explicit command. Closing the control center leaves the menu bar and enabled pet running. Standard Quit closes the UI host and overlay; the LaunchAgent-hosted PetCore may remain available.
Cold launch, Dock reopen, secondary-instance activation, menu/overlay actions, and update recovery all enter one control-center presentation coordinator. It owns the desired visibility, application activation, exact registered window, and key-window transition regardless of whether SwiftUI installs its presenter or registers the `NSWindow` first. Overlay panels are passive and cannot take keyboard focus during ordering or state updates; only the session bubble's explicit keyboard command grants a focus lease, which ends when that panel resigns key. Closing the control center leaves the menu bar and enabled pet running. Standard Quit closes the UI host and overlay; the LaunchAgent-hosted PetCore may remain available.
The pet, bubble, and menu form one AppKit composition. A drag has one presentation owner, uses absolute screen coordinates from a captured anchor, and commits one final hard-clamped position without momentum. Pointer ownership is exact and non-configurable: opaque pet pixels and rendered bubble/menu surfaces are interactive, while transparent pet and panel regions pass through to lower apps; missing or stale masks retain a geometric pet fallback. Display size is a separate Pet Configuration setting; the overlay exposes no resize interaction. See [Runtime and IPC](runtime-and-ipc.md) for lifecycle and persistence.
+3 -3
View File
@@ -15,7 +15,7 @@ The App and PetCore each hold a private instance lock. A second App instance req
Closing the control-center window leaves the menu bar and enabled overlay active. Reopen targets the registered control-center scene; About cannot intercept it. Standard Quit closes the App and overlay. A LaunchAgent-hosted PetCore remains available, while a direct-child fallback is tied to the App process.
A primary cold launch queues one activation through that same registered control-center presenter on the main run-loop turn after AppKit finishes launching. If SwiftUI has not installed the presenter or registered its NSWindow yet, the request remains pending until both steps complete; AppKit activation then fronts that exact window. Cold launch, Dock reopen, and secondary-instance activation therefore converge on the same single window.
A primary cold launch queues one request through the control-center presentation coordinator after AppKit finishes launching. The coordinator is the only owner of desired visibility, App activation, exact-window registration, and the key-window transition. If SwiftUI has not installed the presenter or registered its `NSWindow` yet, the request remains pending and is fulfilled once in either ordering. Cold launch, Dock reopen, secondary-instance activation, product actions, and update recovery therefore converge on the same singleton window without activating the App before that window can be fronted.
## Startup and managed runtime
@@ -40,7 +40,7 @@ Every current managed runtime also carries the build-bound `interaction-attestat
After the healthy commit boundary, the runtime store retains the exact current build, the compatible last-known-good build, and any source/candidate builds named by a live rollback checkpoint. It prunes only unreferenced version directories whose private ownership, closed file inventory, regular-file identity, and manifest build ID prove that they are App-managed runtimes. Unknown, malformed, linked, or foreign entries remain untouched; pruning failure never reverses a healthy runtime commit. This retention does not affect SQLite, pets, generation workspaces, connectors, logs, or settings.
At bootstrap, the App applies persisted interface language and appearance before showing ordinary windows. A short system-default fallback prevents a stalled PetCore read from leaving them invisible. The appearance gate is owned once per NSWindow: transient SwiftUI host removal, replacement, or duplication cannot recapture a concealed state or cancel the deferred reveal, and dismantling the final host fails open. Initial launch and recovery coalesce onto one behavior → seed → snapshot → overlay pipeline. PetCore's first complete snapshot is the final authority for settings, onboarding, pets, placement, connections, and active sessions.
At bootstrap, ordinary windows are visible and interactive immediately with the system appearance. Persisted appearance hydration is an idempotent `NSAppearance` update only: it never changes window alpha, input handling, ordering, layout, activation, or key state. A stalled PetCore read therefore cannot leave a window blank or transparent. Initial launch and recovery coalesce onto one behavior → seed → snapshot → overlay pipeline. PetCore's first complete snapshot is the final authority for settings, onboarding, pets, placement, connections, and active sessions.
## Overlay and state delivery
@@ -50,7 +50,7 @@ The App reads a consistent `state.snapshot`, then waits with `state.wait(after_r
Overlay pointer ownership is permission-free: a main-run-loop 120 Hz sampler reads each overlay panel's window-local cursor position and updates whole-window passthrough, while App-local mouse monitors immediately reconcile events already delivered to the App. The implementation never reads global cursor, button, or modifier-key state; creates a system event tap; registers a cross-application event monitor; or installs a keyboard event monitor, so desktop-pet interaction does not require Input Monitoring access. The current frame's Alpha mask owns only opaque pet pixels; the bubble owns only its rendered rounded cards, and all surrounding transparent panel area remains available to lower applications. A finite playback substate has its own render identity but not a second semantic owner: when `burst_then_idle` presents its settled idle frame, that frame publishes its real visible envelope and Alpha mask under the originating Agent event's entry ID. The App therefore accepts the current settled geometry while continuing to reject callbacks owned by an earlier semantic state. Missing or stale pet masks deliberately fall back to the geometric pet region so launch and frame transitions never disable dragging. The same policy is reapplied in AppKit hit testing as a defensive boundary. No enlarged proximity region may take ownership on behalf of pet or bubble content.
Display width is stored separately and changed only through Pet Configuration. Bubble and menu geometry stays App-local and does not cross RPC. A temporarily concealed Control Center is never classified as the system Show Desktop gesture, so appearance hydration cannot hide the desktop pet as a secondary effect. Interaction telemetry is aggregate-only and excludes coordinates, display/session identifiers, paths, payloads, and user text.
Display width is stored separately and changed only through Pet Configuration. Bubble and menu geometry stays App-local and does not cross RPC. Explicitly closing the Control Center is not classified as the system Show Desktop gesture. Interaction telemetry is aggregate-only and excludes coordinates, display/session identifiers, paths, payloads, and user text.
The App exposes typed service states (`checking`, `recovering`, `online`, `offline`, `runtimeMismatch`, `error`). Human-readable copy never grants recovery authority. Service & Diagnostics projects one aggregate state while preserving typed component and renderer detail.
+5
View File
@@ -26,6 +26,8 @@ Build and validation are separate profiles. `build_and_run.sh --run` and the env
The local default is deliberately bounded: it checks diff and source syntax, localization and lightweight Skill/workflow contracts when touched, Rust formatting plus `cargo check`, and strict Swift compilation. `validate_local_tests.sh` is an optional domain-manifest-driven focused entrypoint; it uses explicit changed paths, rejects stale or unknown domains, and falls back conservatively when no exact focused suite exists. Neither local path runs complete Rust or Swift tests, simulated connector/producer roundtrips, App assembly, stress, or release artifact validation. Those duplicate proofs belong to required GitHub CI. The local gate never launches the GUI, mutates user LaunchAgents, invokes real Agents, or reads credentials. / 本地默认门禁刻意保持轻量;可选的领域聚焦入口根据类型化领域清单和明确改动路径生成计划,拒绝未知领域,并在无法精确命中时保守回退。两条本地路径都不重复远端全量测试、组装、压力或发布验证,也不会启动 GUI、修改 LaunchAgent、调用真实 Agent 或读取凭据。
Source syntax validation also rejects absolute RFC 3339 dates in Rust fixtures that call event-retention write APIs, including dates reached through same-file fixture helpers. Such tests must capture one current UTC base and express ordering or expiry with relative offsets; fixed dates remain valid in isolated schema, serialization, and migration fixtures that do not cross a real wall clock. / 源码语法门禁还会拒绝 Rust 测试中调用事件保留写入 API(包括经同文件 fixture helper 间接调用)时使用固定 RFC 3339 绝对日期。这类测试必须捕获一次当前 UTC 基准并用相对偏移表达顺序或过期;不接触真实时钟的独立 schema、序列化与迁移 fixture 仍可保留固定日期。
`validation_scope.py` is the single path classifier used by local pre-push and CI. The routing contract is: / 本地预推送与 CI 共用 `validation_scope.py`,范围合同如下:
| Change / 变更 | Automatic proof / 自动证明 | Not automatic / 不自动执行 |
@@ -70,6 +72,7 @@ Branch/worktree ownership and direct/train PR coordination are defined by [Paral
## Environment-dependent gates / 环境门禁
- `APC_VALIDATE_HOST_UI=1` permits repository validators that affect a packaged App runtime. It does not itself invoke Computer Use.
- `APC_MAIN_UI_COLD_LAUNCH_ITERATIONS=1..100` controls the real foreground Control Center stress loop inside `validate_main_window_ui.sh` (default `3`; Release acceptance requires at least `20`). Each iteration uses an isolated owned runtime and varied launch-request timing, then proves the exact window identifier, layer, alpha, frame, nonblank ScreenCaptureKit pixels, overlay presentation, and retained key-window identity. Screen Recording and Accessibility access must be available to the executing host; an unavailable observation fails this host gate rather than being inferred as passed. / 该变量控制真实前台控制中心的冷启动压力循环(默认 `3`,Release 验收至少 `20`);每轮使用隔离运行时与不同的展示请求时序,并验证精确窗口标识、层级、透明度、尺寸、非空白像素、悬浮层展示及持续的 key-window 身份。主机缺少录屏或辅助功能权限时门禁直接失败,不得推断为通过。
- `APC_VALIDATE_REAL_AGENT_CONNECTORS=1` permits checks against installed Agent CLIs and managed connector files. It never permits reading credential stores.
- `APC_VALIDATE_REAL_APP_SERVER=1` permits the real App Server lifecycle validation. It deliberately requests native input, completes one pet, interrupts and restarts its owned PetCore, resumes a second task, then strictly cancels and verifies the exact Studio thread is archived or absent from the ordinary list. `APC_REQUIRE_EXTERNAL_SKILL_SOURCE=1` keeps the strict release proof; lowering it weakens the result.
- `APC_RUN_SIX_HOUR_MAKER_SOAK=1` runs the non-default six-hour gate. It holds a real task in durable waiting-for-user state for just over six hours, verifies the backend-authoritative duration includes that interval, and then runs the same resume/cancel lifecycle checks.
@@ -86,6 +89,8 @@ APC_VALIDATE_REAL_AGENT_CONNECTORS=1 ./script/validate_real_agent_connectors.sh
APC_VALIDATE_REAL_APP_SERVER=1 ./script/validate_real_app_server.sh
APC_RUN_SIX_HOUR_MAKER_SOAK=1 ./script/soak_ai_pet_maker_six_hours.sh
APC_EVENT_STORM_COUNT=1000 ./script/validate_event_storm.sh
APC_VALIDATE_HOST_UI=1 APC_MAIN_UI_COLD_LAUNCH_ITERATIONS=20 \
./script/validate_main_window_ui.sh
./script/validate_overlay_performance_summary.sh /absolute/summary.json 60
```
+7 -2
View File
@@ -109,9 +109,14 @@ git switch main
git pull --ff-only
test -z "$(git status --porcelain)"
git rev-parse HEAD
APC_VALIDATE_HOST_UI=1 \
APC_MAIN_UI_COLD_LAUNCH_ITERATIONS=20 \
./script/validate_main_window_ui.sh
./script/build_and_run.sh --run
```
The stress command is a fail-closed Release prerequisite, not a substitute for Computer Use. It runs twenty real foreground cold launches with deterministic timing jitter and requires the exact singleton Control Center to be visible, opaque, on layer 0, pixel-nonblank, and still key after the overlay is ordered. A ScreenCaptureKit or Accessibility observation that cannot be completed is a failed gate. / 冷启动压力命令是 fail-closed 的发布前置条件,不能替代 Computer Use。它以确定性时序扰动执行二十次真实前台冷启动,并要求精确的单例控制中心可见、不透明、位于 layer 0、像素非空,且桌宠浮层完成排序后仍保持 key window;ScreenCaptureKit 或辅助功能观察无法完成时按失败处理。
Use Computer Use—not `--run-ui-validation`, a hidden `NSHostingView`, AX-only validation, or source inspection—to observe and operate the running App. The acceptance is one bounded basic-function pass:
必须使用 Computer Use 直接观察并操作正在运行的 App;`--run-ui-validation`、隐藏的 `NSHostingView`、仅 AX 验证或源码检查都不能替代。验收只需完成一次有界的基础功能检查:
@@ -122,9 +127,9 @@ Use Computer Use—not `--run-ui-validation`, a hidden `NSHostingView`, AX-only
4. The desktop pet can be hidden and shown from the Control Center. / 可从控制中心隐藏并重新显示桌宠。
5. Closing and reopening the Control Center, then quitting and relaunching the App twice consecutively, restores a visible usable window after each cold launch. / 关闭并重新打开控制中心后,连续两次退出并重启 App;每次冷启动都必须恢复可见且可用的窗口。
Record the full tested commit and the result. A pass authorizes one dispatch for that exact commit. If any item fails or cannot be directly observed, do not dispatch, do not enter `passed`, and do not autonomously waive the failure; preserve the evidence and stop so the next action is 交由用户决定. After any source change, even a release-only fix, repeat the acceptance against the new commit.
Record the full tested commit and the combined stress plus Computer Use result. Enter `host_ui_result=passed` only when both gates passed on the same exact clean commit; that pass authorizes one dispatch for that commit. If any item fails or cannot be directly observed, do not dispatch, do not enter `passed`, and do not autonomously waive the failure; preserve the evidence and stop so the next action is 交由用户决定. After any source change, even a release-only fix, repeat both gates against the new commit.
记录完整的被测 commit 与结果。通过结果只授权发布这个精确 commit。若任一检查失败或无法直接观察,不得触发发布、不得填写 `passed`、也不得由 Agent 自行豁免;应保留证据并停止,后续动作交由用户决定。任何源码变化(包括仅用于发布的修复)都会使旧结果失效,必须针对新 commit 重新验收。
记录完整的被测 commit,以及压力门禁与 Computer Use 的组合结果。只有两项门禁都在同一个精确且干净的 commit 上通过,才能填写 `host_ui_result=passed`,且该结果只授权发布这个 commit。若任一检查失败或无法直接观察,不得触发发布、不得填写 `passed`、也不得由 Agent 自行豁免;应保留证据并停止,后续动作交由用户决定。任何源码变化(包括仅用于发布的修复)都会使旧结果失效,必须针对新 commit 重新执行两项门禁。
## GitHub automation / GitHub 自动化
@@ -0,0 +1,264 @@
import AppKit
import ApplicationServices
import CoreGraphics
import Foundation
import ScreenCaptureKit
private let controlCenterIdentifier = "dev.agentpet.companion.control-center"
private func fail(_ message: String) -> Never {
fputs("main window cold-launch probe failed: \(message)\n", stderr)
exit(1)
}
private func copyAttribute(
_ element: AXUIElement,
_ attribute: String
) -> AnyObject? {
var value: AnyObject?
guard AXUIElementCopyAttributeValue(
element,
attribute as CFString,
&value
) == .success else {
return nil
}
return value
}
private func stringAttribute(
_ element: AXUIElement,
_ attribute: String
) -> String {
copyAttribute(element, attribute) as? String ?? ""
}
private func pointAttribute(
_ element: AXUIElement,
_ attribute: String
) -> CGPoint? {
guard let value = copyAttribute(element, attribute) else { return nil }
var result = CGPoint.zero
guard AXValueGetValue(value as! AXValue, .cgPoint, &result) else {
return nil
}
return result
}
private func sizeAttribute(
_ element: AXUIElement,
_ attribute: String
) -> CGSize? {
guard let value = copyAttribute(element, attribute) else { return nil }
var result = CGSize.zero
guard AXValueGetValue(value as! AXValue, .cgSize, &result) else {
return nil
}
return result
}
private struct VisibleWindow {
let id: CGWindowID
let layer: Int
let alpha: Double
let bounds: CGRect
}
private func visibleWindows(ownerPID: pid_t) -> [VisibleWindow] {
let options = CGWindowListOption(
arrayLiteral: .optionOnScreenOnly,
.excludeDesktopElements
)
let rows = CGWindowListCopyWindowInfo(options, kCGNullWindowID)
as? [[String: Any]] ?? []
return rows.compactMap { row in
guard (row[kCGWindowOwnerPID as String] as? NSNumber)?.int32Value
== ownerPID,
let id = (row[kCGWindowNumber as String] as? NSNumber)?.uint32Value,
let layer = (row[kCGWindowLayer as String] as? NSNumber)?.intValue,
let boundsDictionary = row[kCGWindowBounds as String]
as? [String: Any],
let bounds = CGRect(
dictionaryRepresentation: boundsDictionary as CFDictionary
)
else {
return nil
}
return VisibleWindow(
id: id,
layer: layer,
alpha: (row[kCGWindowAlpha as String] as? NSNumber)?.doubleValue ?? 0,
bounds: bounds
)
}
}
private func framesMatch(_ lhs: CGRect, _ rhs: CGRect) -> Bool {
abs(lhs.minX - rhs.minX) <= 3
&& abs(lhs.minY - rhs.minY) <= 3
&& abs(lhs.width - rhs.width) <= 3
&& abs(lhs.height - rhs.height) <= 3
}
private func pixelRange(_ image: CGImage) -> (opaqueFraction: Double, range: Int)? {
let width = 160
let height = max(1, Int(Double(width) * Double(image.height) / Double(image.width)))
var pixels = [UInt8](repeating: 0, count: width * height * 4)
guard let context = CGContext(
data: &pixels,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: width * 4,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
) else {
return nil
}
context.interpolationQuality = .low
context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
var opaquePixels = 0
var minimumLuminance = 255
var maximumLuminance = 0
for offset in stride(from: 0, to: pixels.count, by: 4) {
let alpha = Int(pixels[offset + 3])
guard alpha > 16 else { continue }
opaquePixels += 1
let red = Int(pixels[offset])
let green = Int(pixels[offset + 1])
let blue = Int(pixels[offset + 2])
let luminance = (54 * red + 183 * green + 19 * blue) / 256
minimumLuminance = min(minimumLuminance, luminance)
maximumLuminance = max(maximumLuminance, luminance)
}
return (
Double(opaquePixels) / Double(width * height),
maximumLuminance - minimumLuminance
)
}
@main
private struct MainWindowColdLaunchProbe {
static func main() async {
guard let pidValue = ProcessInfo.processInfo.environment["APP_PID"],
let appPID = Int32(pidValue),
let app = NSRunningApplication(processIdentifier: appPID),
!app.isTerminated
else {
fail("owned App PID is unavailable")
}
let axApp = AXUIElementCreateApplication(appPID)
var controlCenter: AXUIElement?
var controlCenterFrame: CGRect?
var visibleControlCenter: VisibleWindow?
var overlayWasPresented = false
for _ in 0..<160 {
let windows = copyAttribute(axApp, kAXWindowsAttribute)
as? [AXUIElement] ?? []
let exactWindows = windows.filter {
stringAttribute($0, kAXIdentifierAttribute)
== controlCenterIdentifier
}
if exactWindows.count > 1 {
fail("more than one exact Control Center window is registered")
}
if let exactWindow = exactWindows.first,
let position = pointAttribute(exactWindow, kAXPositionAttribute),
let size = sizeAttribute(exactWindow, kAXSizeAttribute) {
let frame = CGRect(origin: position, size: size)
let appWindows = visibleWindows(ownerPID: appPID)
let layerZero = appWindows.first {
$0.layer == 0
&& $0.alpha > 0
&& $0.bounds.width >= 760
&& $0.bounds.height >= 520
&& framesMatch($0.bounds, frame)
}
let hasOverlay = appWindows.contains {
$0.layer > 0
&& $0.alpha > 0
&& $0.bounds.width > 20
&& $0.bounds.height > 20
}
if let layerZero {
controlCenter = exactWindow
controlCenterFrame = frame
visibleControlCenter = layerZero
}
overlayWasPresented = overlayWasPresented || hasOverlay
}
if controlCenter != nil, visibleControlCenter != nil,
overlayWasPresented {
break
}
try? await Task.sleep(for: .milliseconds(100))
}
guard let controlCenter, let controlCenterFrame,
let visibleControlCenter
else {
fail("exact layer-0 Control Center never became visible")
}
guard visibleControlCenter.alpha > 0,
visibleControlCenter.layer == 0,
controlCenterFrame.width >= 760,
controlCenterFrame.height >= 520
else {
fail("Control Center layer, alpha, or frame invariant failed")
}
guard overlayWasPresented else {
fail("overlay never became visible before focus verification")
}
// Give the first overlay ordering turn time to settle. Passive panels
// must not change the exact focused window after this point.
try? await Task.sleep(for: .milliseconds(250))
guard let focusedWindow = copyAttribute(
axApp,
kAXFocusedWindowAttribute
) as! AXUIElement?,
stringAttribute(focusedWindow, kAXIdentifierAttribute)
== controlCenterIdentifier,
CFEqual(focusedWindow, controlCenter)
else {
fail("overlay presentation displaced Control Center keyboard focus")
}
do {
let shareableContent = try await SCShareableContent.excludingDesktopWindows(
false,
onScreenWindowsOnly: true
)
guard let captureWindow = shareableContent.windows.first(where: {
$0.windowID == visibleControlCenter.id
}) else {
fail("ScreenCaptureKit could not resolve the exact Control Center")
}
let filter = SCContentFilter(desktopIndependentWindow: captureWindow)
let configuration = SCStreamConfiguration()
configuration.width = min(1_280, max(1, Int(controlCenterFrame.width * 2)))
configuration.height = min(1_024, max(1, Int(controlCenterFrame.height * 2)))
configuration.showsCursor = false
let image = try await SCScreenshotManager.captureImage(
contentFilter: filter,
configuration: configuration
)
guard let result = pixelRange(image),
result.opaqueFraction >= 0.80,
result.range >= 12
else {
fail("captured Control Center pixels are blank or transparent")
}
} catch {
fail("ScreenCaptureKit pixel proof failed: \(error.localizedDescription)")
}
print(
"Control Center cold-launch probe ok: pid=\(appPID) "
+ "window=\(visibleControlCenter.id)"
)
}
}
+66
View File
@@ -31,6 +31,7 @@ CHANGELOG_FRAGMENTS_PATH = ROOT / "script/changelog_fragments.py"
LOCAL_TESTS_PATH = ROOT / "script/validate_local_tests.py"
ENGINEERING_METRICS_PATH = ROOT / "script/engineering_metrics.py"
SWIFT_BUILD_ARTIFACT_PATH = ROOT / "script/swift_build_artifact.py"
TIME_INDEPENDENT_TESTS_PATH = ROOT / "script/validate_time_independent_tests.py"
INTERACTION_SUITES = [
"OverlayPlacementAuthorityTests",
"AppStoreOverlaySnapshotTests",
@@ -63,6 +64,71 @@ engineering_metrics = load_module("apc_engineering_metrics", ENGINEERING_METRICS
swift_build_artifact = load_module(
"apc_swift_build_artifact", SWIFT_BUILD_ARTIFACT_PATH
)
time_independent_tests = load_module(
"apc_time_independent_tests", TIME_INDEPENDENT_TESTS_PATH
)
class TimeIndependentRustFixtureTests(unittest.TestCase):
def violations(self, source: str) -> list[object]:
return time_independent_tests.violations_in_source(
pathlib.Path("crates/example/tests/retention.rs"), source
)
def test_rejects_fixed_timestamp_reached_through_fixture_helper(self) -> None:
violations = self.violations(
r'''
fn event() -> AgentEvent {
AgentEvent { created_at: "2026-07-17T10:00:00Z".to_string() }
}
#[test]
fn retention_projection() {
let database = database();
database.insert_event(&event()).unwrap();
}
'''
)
self.assertEqual(len(violations), 1)
self.assertEqual(violations[0].function, "event")
self.assertEqual(violations[0].timestamp, "2026-07-17T10:00:00Z")
def test_rejects_fixed_timestamp_in_raw_sql_retention_fixture(self) -> None:
violations = self.violations(
r'''
#[test]
fn retention_projection() {
database.insert_event(&event()).unwrap();
connection.execute(r#"INSERT INTO agent_events VALUES ('2099-01-01T00:00:00Z')"#);
}
'''
)
self.assertEqual(len(violations), 1)
self.assertEqual(violations[0].function, "retention_projection")
def test_allows_relative_current_time_and_unrelated_schema_fixture(self) -> None:
violations = self.violations(
r'''
fn event() -> AgentEvent {
AgentEvent { created_at: fixture_timestamp(2) }
}
#[test]
fn retention_projection() {
database.upsert_codex_activity_event(&event()).unwrap();
}
#[test]
fn schema_round_trip() {
let json = r#"{"created_at":"2026-07-17T10:00:00Z"}"#;
assert!(json.contains("created_at"));
}
'''
)
self.assertEqual(violations, [])
class EngineeringMetricsTests(unittest.TestCase):
+45 -5
View File
@@ -11,8 +11,20 @@ import sys
root = pathlib.Path(sys.argv[1])
app = (root / "apps/macos/Sources/AgentPetCompanion/App/AgentPetCompanionApp.swift").read_text()
app_store = (root / "apps/macos/Sources/AgentPetCompanion/App/AppStore.swift").read_text()
presentation = (
root
/ "apps/macos/Sources/AgentPetCompanion/App/ControlCenterPresentationCoordinator.swift"
).read_text()
appearance = (
root
/ "apps/macos/Sources/AgentPetCompanion/App/InitialAppearanceWindowAppearance.swift"
).read_text()
app_runtime = (root / "apps/macos/Sources/AgentPetCompanion/App/AppRuntimeLifecycle.swift").read_text()
overlay = (root / "apps/macos/Sources/AgentPetCompanion/Overlay/OverlayRootView.swift").read_text()
overlay_controller = (
root / "apps/macos/Sources/AgentPetCompanion/Overlay/PetOverlayController.swift"
).read_text()
main_window_validator = (root / "script/validate_main_window_ui.sh").read_text()
petcore = (root / "apps/macos/Sources/AgentPetCompanion/App/PetCoreProcessManager.swift").read_text()
zh_hans = "\n".join(
path.read_text()
@@ -58,7 +70,7 @@ checks = {
),
"primary cold launch is routed through the registered control center presenter": (
re.search(
r'applicationDidFinishLaunching.*?DispatchQueue\.main\.async\s*\{\s*'
r'applicationDidFinishLaunching.*?ControlCenterColdLaunchTiming\.schedule\s*\{\s*'
r'AppSingleInstanceCoordinator\.shared\.activatePrimaryInstance\(\)',
app,
re.S,
@@ -93,14 +105,42 @@ checks = {
app_store,
)
is not None
and "controlCenterPresentationCoordinator.requestPresentation(" in app_store
and "runtimeHandoffIfNeeded" in presentation
),
"one coordinator owns control-center visibility activation and key state": (
"final class ControlCenterPresentationCoordinator" in presentation
and "private var wantsPresentation = false" in presentation
and "window.alphaValue = 1" in presentation
and "window.ignoresMouseEvents = false" in presentation
and "activateApplication()" in presentation
and "window.makeKeyAndOrderFront(nil)" in presentation
and "ControlCenterPresentationCoordinator(" in app_store
and re.search(
r'private func presentMainWindow\(checkRuntimeHandoff: Bool\)\s*\{\s*'
r'guard !checkRuntimeHandoff \|\| !runtimeHandoffIfNeeded\(\) '
r'else \{ return \}',
app_store,
r'setMainWindowPresenter\s*\{\s*openWindow\(id:\s*"main"\)\s*\}',
app,
)
is not None
),
"appearance hydration never hides or disables a window": (
"InitialAppearanceWindowView" in appearance
and "window.appearance = nextAppearance" in appearance
and "alphaValue" not in appearance
and "ignoresMouseEvents" not in appearance
and "InitialAppearanceWindowGate" not in app
),
"overlay panels are passive unless the bubble holds an explicit keyboard lease": (
"bubblePanel.becomesKeyOnlyIfNeeded = true" in overlay_controller
and "override var canBecomeKey: Bool { keyboardNavigationLeaseIsActive }"
in overlay_controller
and overlay_controller.count("override var canBecomeKey: Bool { false }") >= 2
and "beginKeyboardNavigationLease()" in overlay_controller
),
"cold-launch acceptance rebuilds the exact worktree App": (
'"$ROOT_DIR/script/build_app_bundle.sh" >/dev/null'
in main_window_validator
and 'if [[ ! -x "$APP_BINARY"' not in main_window_validator
),
"resident UI host has no periodic installed-build polling": (
"AppInstalledBuildMonitor" not in app_runtime
and "installedBuildMonitor" not in app
+44 -2
View File
@@ -9,10 +9,13 @@ 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"
@@ -20,10 +23,24 @@ cleanup() {
}
trap cleanup EXIT
if [[ ! -x "$APP_BINARY" || ! -x "$PETCORE_BINARY" || ! -x "$PETCORE_CLI" ]]; then
"$ROOT_DIR/script/build_app_bundle.sh" >/dev/null
# 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" \
@@ -890,3 +907,28 @@ guard waitForVisibleControlCenter() else {
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"
+2
View File
@@ -55,4 +55,6 @@ if broken_links:
raise SystemExit("broken local Markdown links:\n" + "\n".join(broken_links))
PY
python3 "$ROOT_DIR/script/validate_time_independent_tests.py" --root "$ROOT_DIR"
echo 'Shell, Python, JSON and local Markdown link syntax ok'
+245
View File
@@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""Reject calendar-dependent Rust fixtures that cross real retention clocks."""
from __future__ import annotations
import argparse
import dataclasses
import pathlib
import re
from collections.abc import Iterable
RETENTION_API_RE = re.compile(
r"\b(?:insert_event|upsert_codex_activity_event|prune_events)\s*\("
)
LOCAL_CALL_RE = re.compile(r"(?<![.:])\b([A-Za-z_][A-Za-z0-9_]*)\s*!?\s*\(")
FUNCTION_RE = re.compile(r"\bfn\s+([A-Za-z_][A-Za-z0-9_]*)\b")
RFC3339_RE = re.compile(
r"\b[12][0-9]{3}-[01][0-9]-[0-3][0-9][Tt]"
r"[0-2][0-9]:[0-5][0-9]:[0-6][0-9](?:\.[0-9]+)?"
r"(?:[Zz]|[+-][0-2][0-9]:[0-5][0-9])\b"
)
@dataclasses.dataclass(frozen=True)
class RustFunction:
name: str
start: int
body_start: int
end: int
@dataclasses.dataclass(frozen=True)
class StringLiteral:
start: int
end: int
@dataclasses.dataclass(frozen=True)
class Violation:
path: pathlib.Path
line: int
function: str
timestamp: str
def _blank(mask: list[str], source: str, start: int, end: int) -> None:
for index in range(start, end):
if source[index] != "\n":
mask[index] = " "
def _rust_code_and_strings(source: str) -> tuple[str, list[StringLiteral]]:
"""Return a position-preserving code mask and Rust string spans.
Comments, strings, and character literals are blanked in the code mask so
braces and call names inside fixture data cannot affect the function walk.
"""
mask = list(source)
strings: list[StringLiteral] = []
index = 0
length = len(source)
while index < length:
if source.startswith("//", index):
end = source.find("\n", index + 2)
if end < 0:
end = length
_blank(mask, source, index, end)
index = end
continue
if source.startswith("/*", index):
depth = 1
end = index + 2
while end < length and depth:
if source.startswith("/*", end):
depth += 1
end += 2
elif source.startswith("*/", end):
depth -= 1
end += 2
else:
end += 1
_blank(mask, source, index, end)
index = end
continue
raw_prefix = None
if source.startswith("br", index):
raw_prefix = index + 1
elif source[index] == "r":
raw_prefix = index
if raw_prefix is not None and (
index == 0 or not (source[index - 1].isalnum() or source[index - 1] == "_")
):
cursor = raw_prefix + 1
while cursor < length and source[cursor] == "#":
cursor += 1
if cursor < length and source[cursor] == '"':
hashes = source[raw_prefix + 1 : cursor]
terminator = '"' + hashes
end_marker = source.find(terminator, cursor + 1)
end = length if end_marker < 0 else end_marker + len(terminator)
strings.append(StringLiteral(index, end))
_blank(mask, source, index, end)
index = end
continue
if source[index] == '"':
end = index + 1
escaped = False
while end < length:
character = source[end]
if escaped:
escaped = False
elif character == "\\":
escaped = True
elif character == '"':
end += 1
break
end += 1
strings.append(StringLiteral(index, end))
_blank(mask, source, index, end)
index = end
continue
if source[index] == "'":
# Mask character literals but leave lifetimes such as `'a` intact.
char_match = re.match(r"'(?:\\.|[^\\'\n])'", source[index:])
if char_match:
end = index + char_match.end()
_blank(mask, source, index, end)
index = end
continue
index += 1
return "".join(mask), strings
def _matching_brace(code: str, opening: int) -> int | None:
depth = 0
for index in range(opening, len(code)):
if code[index] == "{":
depth += 1
elif code[index] == "}":
depth -= 1
if depth == 0:
return index + 1
return None
def _rust_functions(code: str) -> list[RustFunction]:
functions: list[RustFunction] = []
for match in FUNCTION_RE.finditer(code):
opening = code.find("{", match.end())
terminator = code.find(";", match.end())
if opening < 0 or (terminator >= 0 and terminator < opening):
continue
end = _matching_brace(code, opening)
if end is None:
continue
functions.append(RustFunction(match.group(1), match.start(), opening + 1, end))
return functions
def violations_in_source(path: pathlib.Path, source: str) -> list[Violation]:
code, strings = _rust_code_and_strings(source)
functions = _rust_functions(code)
by_name: dict[str, list[int]] = {}
for index, function in enumerate(functions):
by_name.setdefault(function.name, []).append(index)
marked = {
index
for index, function in enumerate(functions)
if RETENTION_API_RE.search(code[function.body_start : function.end])
}
pending = list(marked)
while pending:
function_index = pending.pop()
function = functions[function_index]
body = code[function.body_start : function.end]
for call in LOCAL_CALL_RE.finditer(body):
for callee_index in by_name.get(call.group(1), []):
if callee_index not in marked:
marked.add(callee_index)
pending.append(callee_index)
violations: dict[tuple[int, str], Violation] = {}
for literal in strings:
literal_source = source[literal.start : literal.end]
timestamp_match = RFC3339_RE.search(literal_source)
if timestamp_match is None:
continue
owners = [
functions[index]
for index in marked
if functions[index].start <= literal.start < functions[index].end
]
if not owners:
continue
owner = min(owners, key=lambda function: function.end - function.start)
timestamp_start = literal.start + timestamp_match.start()
line = source.count("\n", 0, timestamp_start) + 1
violation = Violation(
path=path,
line=line,
function=owner.name,
timestamp=timestamp_match.group(0),
)
violations[(line, violation.timestamp)] = violation
return sorted(violations.values(), key=lambda item: (item.line, item.timestamp))
def scan(root: pathlib.Path, paths: Iterable[pathlib.Path] | None = None) -> list[Violation]:
candidates = paths
if candidates is None:
candidates = sorted((root / "crates").rglob("*.rs"))
violations: list[Violation] = []
for path in candidates:
source = path.read_text(encoding="utf-8")
violations.extend(violations_in_source(path.relative_to(root), source))
return violations
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--root", type=pathlib.Path, default=pathlib.Path.cwd())
args = parser.parse_args()
root = args.root.resolve()
violations = scan(root)
if violations:
details = "\n".join(
f"{item.path}:{item.line}: {item.function} embeds {item.timestamp}"
for item in violations
)
raise SystemExit(
"retention-sensitive Rust fixtures must derive one current-time base and use "
"relative offsets; fixed RFC3339 dates become calendar time bombs:\n" + details
)
print("Retention-sensitive Rust test fixtures are time-independent")
return 0
if __name__ == "__main__":
raise SystemExit(main())