Files
agent-pet-companion/script/validate_overlay_runtime.sh

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