Files
agent-pet-companion/apps/macos/Sources/AgentPetCompanion/Overlay/OverlayValidationContract.swift
T
xjxandgd-ops 1807246b7e Add portable skill management and harden protected delivery (#10)
## What changed

- add App and PetCore portable Agent Pet Maker skill management, with
fd-relative ownership and race-safe install/uninstall
- improve Pi no-model failure projection, session bubble message
selection, child-session filtering, onboarding rendering, and related
App behavior
- delete verified PR source branches with exact leases after merge while
dispatching exact-main CI first
- document direct/train multi-agent delivery, explicit
add/commit/clean/push checks, post-merge worktree audits, and exact-SHA
release proof

## Validation

- full `cargo test -p petcore`
- Swift package suite: 734 tests
- Pi connector privacy tests
- promotion/release Python tests and actionlint
- `./script/validate_pre_push.sh`
- independent specification and safety reviews: no remaining blockers

This PR changes the protected CI control plane, so it is intended for
full CI followed by trusted exact-head squash merge.

---------

Co-authored-by: gd-ops <gd-ops@xmfunny.com>
2026-08-12 01:49:51 +08:00

750 lines
29 KiB
Swift

import AgentPetCompanionCore
import AppKit
import CoreImage
import Foundation
public struct AgentPetCompanionUIValidationFailure: LocalizedError, Sendable {
public let errorDescription: String?
init(_ description: String) {
errorDescription = description
}
}
public enum AgentPetCompanionUIValidationContract {
public static func run() async throws -> [String] {
var passed: [String] = []
try validateGeometry()
passed.append("geometry.complete-interactive-bounds")
try validateMultiDisplaySelection()
passed.append("geometry.current-pointer-display")
try validateDisplayWidthPolicy()
passed.append("geometry.display-width-policy")
try validateActiveSessionBubbleContent()
passed.append("bubble.active-session-content-retention")
try validateLifecyclePresentationMatrix()
passed.append("bubble.lifecycle-idle-through-failed")
try validateNavigationCapabilityMatrix()
passed.append("bubble.truthful-navigation-and-accessibility-order")
try validateBubbleActionRouting()
passed.append("bubble.session-group-close-hit-regions-and-deeplink")
try await validateBubbleDisclosureState()
passed.append("bubble.expand-preserves-consumed-sessions")
try validateFrameTimeline()
passed.append("timeline.authored-durations-and-playback")
try await validateFramePipeline()
passed.append("renderer.actor-lru-eager-ready-handoff")
try await validatePointerMonitor()
passed.append("pointer.permission-free-monitor")
return passed
}
@MainActor
private static func validateBubbleDisclosureState() throws {
let store = AppStore(
bootstrapHooks: AppStoreBootstrapHooks(
ensureRunning: { .alreadyHealthy },
recover: { .alreadyHealthy },
refreshSnapshot: { _ in },
onReady: { _ in }
)
)
store.overlayBubbleDismissed = true
store.overlayDismissedBubbleEventIDs = ["codex-session-event", "pi-session-event"]
store.toggleOverlayBubble()
try require(!store.overlayBubbleDismissed, "expand left the global bubble dismissal active")
try require(
store.overlayDismissedBubbleEventIDs == ["codex-session-event", "pi-session-event"],
"expand restored session rows that had already been consumed"
)
}
private static func validateGeometry() throws {
let visibleFrames = [
CGRect(x: 0, y: 25, width: 1512, height: 934),
CGRect(x: -1280, y: 0, width: 1280, height: 775)
]
for visibleFrame in visibleFrames {
for displayWidthPt: CGFloat in [100, 112, 300] {
let localPetCenter = CGPoint(x: 420, y: 360)
let menuScreenRect = OverlayGeometry.rect(
center: OverlayGeometry.menuScreenCenter(
petScreenCenter: localPetCenter,
displayWidthPt: displayWidthPt
),
size: OverlayGeometry.menuHitSize
)
let bubbleSize = CGSize(width: OverlayGeometry.bubbleWidth, height: 76)
let bubbleRect = OverlayGeometry.rect(
center: OverlayGeometry.bubbleScreenCenter(
bubbleSize: bubbleSize,
displayWidthPt: displayWidthPt,
petScreenCenter: localPetCenter,
screenFrame: visibleFrame
),
size: bubbleSize
)
try require(
!bubbleRect.intersects(menuScreenRect),
"bubble panel overlaps the toggle hit region at \(displayWidthPt)pt"
)
let proposals = [
CGPoint(x: visibleFrame.minX, y: visibleFrame.minY),
CGPoint(x: visibleFrame.maxX, y: visibleFrame.minY),
CGPoint(x: visibleFrame.minX, y: visibleFrame.maxY),
CGPoint(x: visibleFrame.maxX, y: visibleFrame.maxY)
]
for proposal in proposals {
let center = OverlayGeometry.clampedPetScreenCenter(
proposal,
displayWidthPt: displayWidthPt,
visibleFrame: visibleFrame,
clickMenuEnabled: true
)
let bounds = OverlayGeometry.petMovementScreenBounds(
displayWidthPt: displayWidthPt,
petScreenCenter: center,
clickMenuEnabled: true
)
try require(
visibleFrame.insetBy(dx: -0.5, dy: -0.5).contains(bounds),
"movement bounds escaped its frame at \(displayWidthPt)pt: \(bounds)"
)
}
}
}
let fullScreen = CGRect(x: 0, y: 0, width: 1728, height: 1117)
let systemVisibleFrame = CGRect(x: 0, y: 60, width: 1728, height: 1024)
let movementFrame = OverlayGeometry.petMovementFrame(
screenFrame: fullScreen,
visibleFrame: systemVisibleFrame
)
try require(
movementFrame.minY == fullScreen.minY,
"movement frame still excludes the Dock reservation"
)
try require(
movementFrame.maxY == systemVisibleFrame.maxY,
"movement frame no longer protects the menu-bar strip"
)
}
private static func validateMultiDisplaySelection() throws {
let primary = OverlayDisplayGeometry(
frame: CGRect(x: 0, y: 0, width: 1512, height: 982),
visibleFrame: CGRect(x: 0, y: 25, width: 1512, height: 934),
backingScaleFactor: 2
)
let secondary = OverlayDisplayGeometry(
frame: CGRect(x: -1280, y: 0, width: 1280, height: 800),
visibleFrame: CGRect(x: -1280, y: 0, width: 1280, height: 775),
backingScaleFactor: 1
)
let target = OverlayGeometry.dragTargetDisplay(
pointer: CGPoint(x: -640, y: 400),
proposedPetCenter: CGPoint(x: 20, y: 400),
displays: [primary, secondary],
fallback: primary
)
try require(target == secondary, "drag target did not follow the current pointer display")
}
private static func validateDisplayWidthPolicy() throws {
try require(
OverlayGeometry.clampedDisplayWidthPt(1)
== OverlayGeometry.minimumDisplayWidthPt,
"minimum display-width clamp failed"
)
try require(
OverlayGeometry.clampedDisplayWidthPt(999)
== OverlayGeometry.maximumDisplayWidthPt,
"maximum display-width clamp failed"
)
try require(
OverlayGeometry.resolvedInitialDisplayWidthPt(
persistedDisplayWidthPt: 80,
hasPersistedPosition: false
) == 112,
"never-positioned placement did not use the default width"
)
try require(
OverlayGeometry.resolvedInitialDisplayWidthPt(
persistedDisplayWidthPt: 80,
hasPersistedPosition: true
) == 100,
"legacy persisted display width was not clamped to the current minimum"
)
}
private static func validateActiveSessionBubbleContent() throws {
let stateJSON = #"""
{
"state": "tool",
"official_status": "running",
"source": "codex",
"session_id": "session_validation",
"session_active": true,
"source_session_sequence": 3,
"priority": 300,
"lease_seconds": null,
"expires_at": null,
"event": {
"id": "evt_tool",
"source": "codex",
"session_id": "session_validation",
"event_type": "tool",
"title": "Executing tool",
"detail": null,
"payload_json": {
"schema_version": "apc.agent-event.v1",
"source_event": "PreToolUse",
"session_active": true,
"project_label": "agent-pet-companion"
},
"created_at": "2026-07-13T00:00:03Z"
},
"latest_message": {
"id": "evt_prompt",
"source": "codex",
"session_id": "session_validation",
"event_type": "start",
"title": "Started",
"detail": null,
"payload_json": {
"schema_version": "apc.agent-event.v1",
"source_event": "UserPromptSubmit",
"session_active": true,
"message_role": "user",
"message_content": "Keep the current conversation message visible.",
"project_label": "agent-pet-companion"
},
"created_at": "2026-07-13T00:00:01Z"
},
"session_title": "Persistent Codex task title",
"session_message": {
"role": "assistant",
"content": "Latest App Server message"
},
"session_activity": {
"kind": "thinking",
"content": "Verifying live activity synchronization"
},
"overlay_display": {
"summary_kind": "thinking",
"navigation": {
"capability": "agent_host",
"session_open": true,
"surface": "chatgpt_app",
"terminal_app": null,
"open_url": null
}
}
}
"""#
let state = try JSONDecoder().decode(ActiveAgentState.self, from: Data(stateJSON.utf8))
let content = OverlayBubbleContent(state: state)
guard let session = content.sessions.first else {
throw AgentPetCompanionUIValidationFailure("active bubble omitted its session row")
}
try require(
session.messageText == "Latest App Server message",
"active bubble did not render the bounded assistant display message"
)
try require(
session.activityText == "Verifying live activity synchronization",
"active bubble did not render the bounded current activity detail"
)
try require(
session.detailText == session.messageText,
"active bubble rendered more than one body message"
)
var navigationFailure = session
navigationFailure.navigationNotice = .failed
try require(
navigationFailure.detailText
== APCLocalization.text(.overlaySessionNavigationFailed),
"navigation feedback no longer replaced ordinary bubble body copy"
)
try require(
content.agentName == "Codex",
"active bubble omitted its Agent group title"
)
try require(session.sessionID == "session_validation", "active bubble omitted its session id")
try require(
session.sessionTitle == "Persistent Codex task title",
"active bubble did not render the bounded session title"
)
try require(
!session.detailText.contains("Keep the current conversation message visible."),
"active bubble fell back to the user prompt"
)
try require(!session.statusText.isEmpty, "active bubble omitted its run status")
try require(!session.actionLabel.isEmpty, "active bubble omitted its interaction action")
let secondStateJSON = stateJSON
.replacingOccurrences(of: "session_validation", with: "session_validation_2")
.replacingOccurrences(of: "evt_tool", with: "evt_tool_2")
.replacingOccurrences(
of: "Persistent Codex task title",
with: "Second Codex task title"
)
let secondState = try JSONDecoder().decode(
ActiveAgentState.self,
from: Data(secondStateJSON.utf8)
)
let grouped = OverlayBubbleContent(
source: .codex,
states: [state, secondState],
isExpanded: true
)
try require(grouped.sessions.count == 2, "same-Agent sessions were not grouped")
try require(
grouped.sessions.map(\.sessionTitle) == [
"Persistent Codex task title",
"Second Codex task title",
],
"grouped session titles did not preserve their original values"
)
let groupedSize = OverlayGeometry.resolvedBubbleSize(
in: CGSize(width: 1512, height: 934),
content: grouped
)
let groupedRects = OverlayGeometry.bubbleSessionRects(
in: CGRect(origin: .zero, size: groupedSize),
content: grouped
)
try require(
groupedRects.count == 2 && !groupedRects[0].intersects(groupedRects[1]),
"grouped session rows overlap or lack distinct hit regions"
)
let stacked = OverlayBubbleContent(
source: .codex,
states: [state, secondState],
isExpanded: false
)
try require(stacked.visibleSessions.count == 1, "stacked group exposed hidden rows")
try require(stacked.sessionCount == 2, "stacked group lost its full session count")
try require(stacked.stackDecorationDepth > 0, "stacked group lost its depth treatment")
}
private static func validateBubbleActionRouting() throws {
let sessions = (0 ..< 2).map { index in
OverlaySessionContent(
id: "validation-session-\(index)",
source: .codex,
sessionID: "validation-session-\(index)",
eventType: .tool,
sessionTitle: "Validation \(index)",
messageText: "Running",
statusText: "Running",
navigation: AgentSessionNavigation(
capability: .agentHost,
sessionOpen: true,
surface: "chatgpt_app"
)
)
}
let content = OverlayBubbleContent(
id: "validation-agent-codex",
source: .codex,
agentName: "Codex",
sessions: sessions,
isExpanded: false
)
// Every selectable text tier must keep the same routing contract: a
// larger tier grows rows and header controls, so its hit regions are
// re-measured rather than inherited from the standard tier.
for fontScale in BubbleFontScale.allCases {
let size = OverlayGeometry.resolvedBubbleSize(
in: CGSize(width: 1512, height: 934),
content: content,
fontScale: fontScale
)
let bubbleRect = CGRect(origin: .zero, size: size)
let closeRect = OverlayGeometry.bubbleCloseHitRect(
in: bubbleRect,
fontScale: fontScale
)
let groupRect = OverlayGeometry.bubbleGroupToggleHitRect(
in: bubbleRect,
content: content,
fontScale: fontScale
)
let sessionRects = OverlayGeometry.bubbleSessionRects(
in: bubbleRect,
content: content,
fontScale: fontScale
)
try require(sessionRects.count == 1, "bubble session hit regions did not match session rows")
try require(!groupRect.isEmpty, "multi-session bubble omitted its group control hit region")
try require(
!sessionRects.contains(where: { $0.intersects(closeRect) }),
"bubble session and close hit regions overlap"
)
try require(!groupRect.intersects(closeRect), "bubble group and close hit regions overlap")
try require(
!sessionRects.contains(where: { $0.intersects(groupRect) }),
"bubble session and group hit regions overlap"
)
try require(
closeRect.contains(CGPoint(x: bubbleRect.maxX - 12, y: 12)),
"bubble close hit region missed its visible control"
)
try require(
sessionRects[0].contains(CGPoint(x: sessionRects[0].midX, y: sessionRects[0].midY)),
"bubble session hit region missed its visible row"
)
}
try require(
AgentSessionDeepLink.url(source: .codex, sessionID: "019f5b0f-88ff-7413-8953-29de4ed0951c")?.absoluteString
== "codex://threads/019f5b0f-88ff-7413-8953-29de4ed0951c",
"Codex session did not resolve to the ChatGPT task deep link"
)
try require(
AgentSessionDeepLink.url(source: .codex, sessionID: "unsafe/session") == nil,
"unsafe session id was accepted for deep-link routing"
)
try require(
AgentSessionDeepLink.url(source: .codex, sessionID: "thread-confirmed") == nil,
"non-UUID session id was accepted for deep-link routing"
)
try require(
AgentSessionDeepLink.url(source: .claudeCode, sessionID: "session") == nil,
"unsupported agent source produced a Codex deep link"
)
}
private static func validateLifecyclePresentationMatrix() throws {
try require(
OverlayBubbleProjection.contents(
states: [],
omittedCount: 0,
dismissedSessionIDs: [],
isExpanded: { _ in true }
).isEmpty,
"idle state emitted a placeholder session bubble"
)
for eventType in AgentEventKind.allCases {
let content = OverlaySessionContent(event: AgentEvent(
id: "validation-\(eventType.rawValue)",
source: .codex,
sessionID: "validation-session",
eventType: eventType,
title: "arbitrary transport title",
createdAt: "2026-07-23T00:00:00Z"
))
let expectedStatus = APCLocalizedPresentation.eventTitle(eventType)
try require(
content.statusText == expectedStatus,
"bubble status did not preserve the filtered event for \(eventType)"
)
try require(
content.sessionTitle != content.statusText
&& content.activityText.isEmpty
&& content.messageText.isEmpty,
"bubble inserted placeholder detail copy for \(eventType)"
)
}
}
private static func validateNavigationCapabilityMatrix() throws {
func session(
id: String,
navigation: AgentSessionNavigation
) -> OverlaySessionContent {
OverlaySessionContent(
id: id,
source: .codex,
sessionID: id,
eventType: .waiting,
sessionTitle: "Session \(id)",
activityText: APCLocalization.text(.overlayDetailNeedsInput),
messageText: "A response is required",
statusText: APCLocalizedPresentation.lifecycleTitle(.waiting),
navigation: navigation
)
}
let exact = session(
id: "exact",
navigation: AgentSessionNavigation(
capability: .exactSession,
sessionOpen: true,
surface: "cli_terminal",
terminalApp: "warp",
openURL: "warp://session/A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4"
)
)
let host = session(
id: "host",
navigation: AgentSessionNavigation(
capability: .agentHost,
sessionOpen: true,
surface: "chatgpt_app"
)
)
let malformed = session(
id: "malformed",
navigation: AgentSessionNavigation(
capability: .exactSession,
sessionOpen: true,
surface: "chatgpt_app",
routableSessionID: "unsafe/raw/session"
)
)
let closed = session(
id: "closed",
navigation: AgentSessionNavigation(
capability: .agentHost,
sessionOpen: false,
surface: "chatgpt_app"
)
)
try require(exact.navigationCapability == .exactSession, "exact route was not retained")
try require(host.navigationCapability == .agentHost, "host route was not retained")
try require(
malformed.navigationCapability == .unavailable && !malformed.canOpen,
"malformed exact target remained actionable"
)
try require(
closed.navigationCapability == .unavailable && !closed.canOpen,
"closed session remained actionable"
)
try require(
exact.actionLabel != host.actionLabel,
"exact-session and host-level routes used the same action copy"
)
try require(
exact.accessibilityReadingOrder == [
"Codex",
exact.surfaceLabel,
"Session exact",
APCLocalizedPresentation.lifecycleTitle(.waiting),
"A response is required",
exact.actionLabel,
],
"VoiceOver order was not Agent, surface, session, status, visible message, action"
)
}
private static func validateFrameTimeline() throws {
let oneShot = FrameTimeline(
durationsMS: [100, 200, 300],
playback: PlaybackContract(
mode: .burstThenSettle,
entryRepeatCount: 1,
settleFrameIndex: 1
),
reducedMotionFrameIndex: 2
)
try require(oneShot.frameIndex(elapsedMS: 99) == 0, "first authored boundary drifted")
try require(oneShot.frameIndex(elapsedMS: 100) == 1, "second authored frame started late")
try require(oneShot.frameIndex(elapsedMS: 300) == 2, "third authored frame started late")
try require(oneShot.frameIndex(elapsedMS: 600) == 1, "one-shot did not hold its settle frame")
try require(oneShot.hasCompleted(elapsedMS: 600), "one-shot completion was not reported")
let looping = FrameTimeline(
durationsMS: [100, 200],
playback: PlaybackContract(mode: .loop),
reducedMotionFrameIndex: 1
)
try require(
looping.frameIndex(elapsedMS: 300) == 0,
"looping state did not wrap"
)
let periodic = FrameTimeline(
durationsMS: [100, 100],
playback: PlaybackContract(
mode: .periodic,
cooldownMS: [500, 500]
),
reducedMotionFrameIndex: 1,
periodicCooldownMS: 500
)
try require(
periodic.frameIndex(elapsedMS: 250) == 1,
"periodic cooldown did not hold its settle frame"
)
try require(
periodic.frameIndex(elapsedMS: 700) == 0,
"periodic playback did not restart after cooldown"
)
try require(
periodic.frameIndex(elapsedMS: 0, reducedMotion: true) == 1,
"reduced motion did not select the authored static frame"
)
var playback = FramePlaybackState(stateID: "start", enteredAt: 10)
playback.enter(stateID: "start", at: 10.5)
try require(
playback.enteredAt == 10,
"unchanged semantic state restarted playback"
)
playback.enter(stateID: "done", at: 11)
try require(
playback.frameIndex(at: 11, timeline: oneShot) == 0,
"state entry did not reset playback"
)
try require(
oneShot.resolvedFrameAfterStall(elapsedMS: 450) == 2,
"stall recovery did not jump directly to the current authored frame"
)
}
private static func validateFramePipeline() async throws {
let timing = PetAnimationContract.defaultStates.first {
$0.name == "tool"
}!
let urls = timing.frameDurationsMS.indices.map {
URL(fileURLWithPath: "/virtual/frame-\($0).png")
}
let probe = UIValidationDecodeProbe()
let pipeline = PetFramePipeline(
memoryBudgetBytes: 32,
catalog: { _, _ in PetFrameAssetCatalog(frameURLs: urls, coverURL: nil) },
decoder: { probe.decode($0) }
)
let quality = QualityLevel.standard
let pet = PetSummary(
id: "pet_validation",
name: "Validation",
style: "pixel",
quality: quality,
renderSize: quality.renderSize,
petpackPath: "/virtual/validation.petpack",
coverPath: "",
active: true,
createdAt: "2026-07-10T00:00:00Z"
)
let request = PetFrameLoadRequest(
pet: pet,
stateName: "tool",
timing: timing
)
let prepared = try await Task { @MainActor in
try await pipeline.prepare(request)
}.value
try require(prepared.sourceKind == .eager, "V3 action did not use eager frame handoff")
try require(
prepared.sourceFrameCount == timing.frameDurationsMS.count,
"source frame count did not match the timing contract"
)
try require(
prepared.frameCount == timing.frameDurationsMS.count,
"runtime frame count did not preserve every authored frame"
)
try require(
prepared.readyFrameCount == timing.frameDurationsMS.count,
"eager handoff did not contain the complete authored state"
)
try require(
prepared.timeline == FrameTimeline(state: timing),
"prepared timeline diverged from the package timing contract"
)
let readsAfterPrepare = probe.snapshot().count
for index in 0..<100 {
_ = prepared.readyFrame(at: index % max(1, prepared.frameCount))
}
try require(
probe.snapshot().count == readsAfterPrepare,
"ready-frame lookup performed decode or file work"
)
let metrics = await pipeline.cacheMetrics()
try require(metrics.byteCount <= 32, "LRU exceeded byte budget")
try require(metrics.maximumConcurrentDecodes == 1, "decode queue was not bounded")
try require(!probe.snapshot().decodedOnMain, "frame decode executed on the main thread")
}
private static func validatePointerMonitor() async throws {
let result = await MainActor.run {
let monitor = OverlayPointerEventMonitor()
let idleUsesPolling = monitor.usesPolling
let idleIsRunning = monitor.isRunning
monitor.start {}
let activeUsesPolling = monitor.usesPolling
let activeIsRunning = monitor.isRunning
monitor.stop()
return (
idleUsesPolling: idleUsesPolling,
idleIsRunning: idleIsRunning,
activeUsesPolling: activeUsesPolling,
activeIsRunning: activeIsRunning,
stoppedIsRunning: monitor.isRunning,
pollingInterval: OverlayPointerEventMonitor.pollingInterval,
hasMouseMoved: OverlayPointerEventMonitor.eventMask.contains(.mouseMoved),
hasMouseDown: OverlayPointerEventMonitor.eventMask.contains(.leftMouseDown),
hasMouseUp: OverlayPointerEventMonitor.eventMask.contains(.leftMouseUp),
hasDrag: OverlayPointerEventMonitor.eventMask.contains(.leftMouseDragged)
)
}
try require(!result.idleUsesPolling, "idle pointer monitor unexpectedly polls")
try require(!result.idleIsRunning, "pointer monitor starts while not needed")
try require(result.activeUsesPolling, "active pointer monitor does not poll")
try require(result.activeIsRunning, "active pointer monitor is not running")
try require(!result.stoppedIsRunning, "stopped pointer monitor is still running")
try require(
result.pollingInterval == 1.0 / 120.0,
"pointer monitor does not use the authored permission-free sampling interval"
)
try require(
result.hasMouseMoved && result.hasMouseDown && result.hasMouseUp
&& !result.hasDrag,
"local pointer mask must track hover/press/release without duplicating drag delivery"
)
}
private static func require(_ condition: @autoclosure () -> Bool, _ message: String) throws {
guard condition() else { throw AgentPetCompanionUIValidationFailure(message) }
}
}
private final class UIValidationDecodeProbe: @unchecked Sendable {
private let lock = NSLock()
private var count = 0
private var decodedOnMain = false
func decode(_ url: URL) -> PetDecodedFrame {
lock.lock()
count += 1
decodedOnMain = decodedOnMain || Thread.isMainThread
lock.unlock()
return PetDecodedFrame(
image: CIImage(color: .white).cropped(
to: CGRect(x: 0, y: 0, width: 2, height: 2)
),
pixelWidth: 2,
pixelHeight: 2
)
}
func snapshot() -> (count: Int, decodedOnMain: Bool) {
lock.lock()
defer { lock.unlock() }
return (count, decodedOnMain)
}
}