245 lines
7.9 KiB
Bash
Executable File
245 lines
7.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
. "$ROOT_DIR/script/validation_helpers.sh"
|
|
apc_require_host_ui_opt_in "overlay runtime validation"
|
|
APP_NAME="${APC_OVERLAY_APP_NAME:-AgentPetCompanion}"
|
|
PETCORE_CLI="${1:-$ROOT_DIR/target/debug/petcore-cli}"
|
|
RESTORE_BEHAVIOR=0
|
|
ORIGINAL_BEHAVIOR_JSON=""
|
|
|
|
restore_overlay_behavior() {
|
|
if [[ "$RESTORE_BEHAVIOR" == "1" && -n "$ORIGINAL_BEHAVIOR_JSON" ]]; then
|
|
"$PETCORE_CLI" behavior set-json --value-json "$ORIGINAL_BEHAVIOR_JSON" >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
trap restore_overlay_behavior EXIT
|
|
|
|
if [[ ! -x "$PETCORE_CLI" ]]; then
|
|
echo "overlay runtime validation skipped: petcore-cli is unavailable at $PETCORE_CLI"
|
|
exit 0
|
|
fi
|
|
|
|
SNAPSHOT=""
|
|
for _ in {1..40}; do
|
|
SNAPSHOT="$("$PETCORE_CLI" snapshot 2>/dev/null || true)"
|
|
OVERLAY_STATE="$(SNAPSHOT="$SNAPSHOT" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
|
|
try:
|
|
data = json.loads(os.environ.get("SNAPSHOT", "{}"))
|
|
except json.JSONDecodeError:
|
|
print("invalid")
|
|
raise SystemExit
|
|
|
|
behavior = data.get("behavior")
|
|
if not isinstance(behavior, dict):
|
|
print("invalid")
|
|
elif behavior.get("enabled", False):
|
|
print("enabled")
|
|
else:
|
|
print("disabled")
|
|
PY
|
|
)"
|
|
if [[ "$OVERLAY_STATE" != "invalid" ]]; then
|
|
break
|
|
fi
|
|
sleep 0.25
|
|
done
|
|
|
|
if [[ "$OVERLAY_STATE" == "invalid" ]]; then
|
|
echo "overlay runtime validation failed: petcore snapshot is unavailable" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$OVERLAY_STATE" == "disabled" ]]; then
|
|
ORIGINAL_BEHAVIOR_JSON="$(SNAPSHOT="$SNAPSHOT" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
|
|
data = json.loads(os.environ["SNAPSHOT"])
|
|
print(json.dumps(data["behavior"], ensure_ascii=False))
|
|
PY
|
|
)"
|
|
ENABLED_BEHAVIOR_JSON="$(SNAPSHOT="$SNAPSHOT" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
|
|
data = json.loads(os.environ["SNAPSHOT"])
|
|
behavior = dict(data["behavior"])
|
|
behavior["enabled"] = True
|
|
behavior["status_bubble"] = True
|
|
behavior["auto_hide"] = False
|
|
print(json.dumps(behavior, ensure_ascii=False))
|
|
PY
|
|
)"
|
|
"$PETCORE_CLI" behavior set-json --value-json "$ENABLED_BEHAVIOR_JSON" >/dev/null
|
|
RESTORE_BEHAVIOR=1
|
|
|
|
for _ in {1..40}; do
|
|
SNAPSHOT="$("$PETCORE_CLI" snapshot 2>/dev/null || true)"
|
|
OVERLAY_STATE="$(SNAPSHOT="$SNAPSHOT" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
|
|
try:
|
|
data = json.loads(os.environ.get("SNAPSHOT", "{}"))
|
|
except json.JSONDecodeError:
|
|
print("invalid")
|
|
raise SystemExit
|
|
|
|
behavior = data.get("behavior")
|
|
if not isinstance(behavior, dict):
|
|
print("invalid")
|
|
elif behavior.get("enabled", False):
|
|
print("enabled")
|
|
else:
|
|
print("disabled")
|
|
PY
|
|
)"
|
|
[[ "$OVERLAY_STATE" == "enabled" ]] && break
|
|
sleep 0.25
|
|
done
|
|
|
|
if [[ "$OVERLAY_STATE" != "enabled" ]]; then
|
|
echo "overlay runtime validation failed: desktop pet could not be enabled for validation" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
SNAPSHOT="$SNAPSHOT" swift - "$APP_NAME" <<'SWIFT'
|
|
import CoreGraphics
|
|
import Foundation
|
|
|
|
let appName = CommandLine.arguments.dropFirst().first ?? "AgentPetCompanion"
|
|
let snapshotObject = (try? JSONSerialization.jsonObject(
|
|
with: Data((ProcessInfo.processInfo.environment["SNAPSHOT"] ?? "{}").utf8)
|
|
)) as? [String: Any] ?? [:]
|
|
let placement = snapshotObject["overlay_placement"] as? [String: Any] ?? [:]
|
|
let behavior = snapshotObject["behavior"] as? [String: Any] ?? [:]
|
|
let activeSessions = ((snapshotObject["active_agent_sessions"] as? [Any])?.isEmpty == false)
|
|
let hasCanonicalState = snapshotObject["active_agent_state"] is [String: Any]
|
|
let persistedDisplayWidthPt = (placement["display_width_pt"] as? NSNumber)?.doubleValue ?? 112
|
|
let displayWidthPt = persistedDisplayWidthPt.isFinite
|
|
? max(100, min(300, persistedDisplayWidthPt))
|
|
: 112
|
|
let expectsBubble = (behavior["status_bubble"] as? Bool ?? true)
|
|
&& (activeSessions || (!hasCanonicalState && !(behavior["auto_hide"] as? Bool ?? false)))
|
|
|
|
struct WindowInfo {
|
|
let id: Int
|
|
let layer: Int
|
|
let x: Double
|
|
let y: Double
|
|
let width: Double
|
|
let height: Double
|
|
}
|
|
|
|
func agentWindows() -> [WindowInfo] {
|
|
guard let list = CGWindowListCopyWindowInfo([.optionOnScreenOnly], kCGNullWindowID) as? [[String: Any]] else {
|
|
return []
|
|
}
|
|
|
|
return list.compactMap { info in
|
|
guard (info[kCGWindowOwnerName as String] as? String) == appName else { return nil }
|
|
guard let bounds = info[kCGWindowBounds as String] as? [String: Any] else { return nil }
|
|
let alpha = (info[kCGWindowAlpha as String] as? NSNumber)?.doubleValue ?? 1
|
|
guard alpha > 0 else { return nil }
|
|
return WindowInfo(
|
|
id: (info[kCGWindowNumber as String] as? NSNumber)?.intValue ?? -1,
|
|
layer: (info[kCGWindowLayer as String] as? NSNumber)?.intValue ?? 0,
|
|
x: (bounds["X"] as? NSNumber)?.doubleValue ?? 0,
|
|
y: (bounds["Y"] as? NSNumber)?.doubleValue ?? 0,
|
|
width: (bounds["Width"] as? NSNumber)?.doubleValue ?? 0,
|
|
height: (bounds["Height"] as? NSNumber)?.doubleValue ?? 0
|
|
)
|
|
}
|
|
}
|
|
|
|
var windows: [WindowInfo] = []
|
|
// A genuinely empty home validates and seeds all three bundled petpacks before
|
|
// the first overlay can exist. Keep this bound above that measured cold path;
|
|
// later assertions still fail immediately when the App process exits.
|
|
for _ in 0..<120 {
|
|
windows = agentWindows()
|
|
let floatingWindows = windows.filter { $0.layer != 0 }
|
|
if expectsBubble {
|
|
if floatingWindows.contains(where: isBubblePanel) {
|
|
break
|
|
}
|
|
} else if !floatingWindows.isEmpty {
|
|
break
|
|
}
|
|
Thread.sleep(forTimeInterval: 0.25)
|
|
}
|
|
|
|
let floating = windows.filter { $0.layer != 0 }
|
|
if floating.isEmpty {
|
|
fputs("overlay runtime validation failed: no floating pet windows found\n", stderr)
|
|
exit(1)
|
|
}
|
|
|
|
let petVisibleWidth = displayWidthPt
|
|
let petVisibleHeight = displayWidthPt * 208 / 192
|
|
let petMaxWidth = max(150, petVisibleWidth + 96)
|
|
let petMaxHeight = max(170, petVisibleHeight + 96)
|
|
|
|
func isPetPanel(_ window: WindowInfo) -> Bool {
|
|
// The panel can briefly be smaller than the persisted display width
|
|
// immediately after placement restoration. Large panels are what block input.
|
|
window.width >= 28
|
|
&& window.height >= 40
|
|
&& window.width <= petMaxWidth
|
|
&& window.height <= petMaxHeight
|
|
}
|
|
|
|
func isBubblePanel(_ window: WindowInfo) -> Bool {
|
|
window.width >= 140
|
|
&& window.width <= 430
|
|
&& window.height >= 48
|
|
// One bounded Agent group may contain up to eight session rows.
|
|
&& window.height <= 720
|
|
}
|
|
|
|
func isControlPanel(_ window: WindowInfo) -> Bool {
|
|
// The pet-side disclosure control is an independently attached 38 pt panel.
|
|
// Keep the tolerance narrow so a larger unexpected input-owning window still
|
|
// fails the desktop-blocking assertion below.
|
|
abs(window.width - 38) <= 1
|
|
&& abs(window.height - 38) <= 1
|
|
}
|
|
|
|
let petPanels = floating.filter(isPetPanel)
|
|
if petPanels.isEmpty {
|
|
fputs("overlay runtime validation failed: no compact pet panel found\n", stderr)
|
|
for window in floating {
|
|
fputs(" id=\(window.id) layer=\(window.layer) frame=(\(window.x), \(window.y), \(window.width), \(window.height))\n", stderr)
|
|
}
|
|
exit(1)
|
|
}
|
|
|
|
let bubblePanels = floating.filter(isBubblePanel)
|
|
if expectsBubble && bubblePanels.isEmpty {
|
|
fputs("overlay runtime validation failed: status bubble is enabled but no compact bubble panel was found\n", stderr)
|
|
for window in floating {
|
|
fputs(" id=\(window.id) layer=\(window.layer) frame=(\(window.x), \(window.y), \(window.width), \(window.height))\n", stderr)
|
|
}
|
|
exit(1)
|
|
}
|
|
|
|
let unexpected = floating.filter {
|
|
!isPetPanel($0) && !isBubblePanel($0) && !isControlPanel($0)
|
|
}
|
|
if !unexpected.isEmpty {
|
|
fputs("overlay runtime validation failed: unexpected floating window size may block desktop input:\n", stderr)
|
|
for window in unexpected {
|
|
fputs(" id=\(window.id) layer=\(window.layer) frame=(\(window.x), \(window.y), \(window.width), \(window.height)) displayWidthPt=\(displayWidthPt)\n", stderr)
|
|
}
|
|
exit(1)
|
|
}
|
|
|
|
print("Overlay runtime validation ok")
|
|
SWIFT
|