diff --git a/CHANGELOG.md b/CHANGELOG.md index 63fb58c..05fc0de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,27 @@ All notable DeckDoc changes are recorded here. The project follows ## [Unreleased] -No unreleased changes. +### Added + +- External-display blackout coverage for desktop mode (#29): `display_blackout.sh` now inspects + connected non-eDP connectors in a KWin session, matches each display's EDID against the stored + KWin output policy, and emits `EXTERNAL_BLACKOUT_SIGNATURE: FULLSCREEN_SYNC_POLICY_GAP`, + `SYNC_POLICY_UNDETERMINED` (policy unreadable — treated as unpinned, never as fixed), or + `SYNC_POLICY_ALREADY_PINNED` when a physical-black symptom is declared with a healthy deck-side + link — the discriminator for fullscreen adaptive-sync blanking on a monitor or dock chain. The + internal and external assessments are independent, so an extended desktop reports both. +- A guarded `rem_external_display.sh` remediation behind the new `--fix-external-display-blackout` + flag: pins adaptive sync off (`kscreen-doctor vrrpolicy.never`) for connected external outputs + after desktop-session and symptom prechecks, with config backup and verification scoped to each + targeted output (live state and per-EDID persistence). Shares the internal-panel remediation's + no-power/brightness/clock-write contract; the pinned policy itself persists per-display by KWin + design and is reversible via `vrrpolicy.automatic` or the saved backup. + +### Fixed + +- A connected-but-disabled internal panel (docked, external-only setups) no longer claims the + internal `LIVE_RENDER_TO_PHYSICAL_SCANOUT_GAP` signature ahead of an active external display; + the panel must be enabled in the output configuration for that classification. ## [3.4.0] - 2026-07-21 diff --git a/deckdoc.sh b/deckdoc.sh index 72044c0..6c1f255 100755 --- a/deckdoc.sh +++ b/deckdoc.sh @@ -5,6 +5,7 @@ umask 077 FIX_MODE=false DISPLAY_BLACK_REPORTED=false DISPLAY_FIX_MODE=false +EXTERNAL_DISPLAY_FIX_MODE=false PERSIST_DISPLAY_STABILITY=false for arg in "$@"; do case "$arg" in @@ -14,6 +15,10 @@ for arg in "$@"; do DISPLAY_BLACK_REPORTED=true DISPLAY_FIX_MODE=true ;; + --fix-external-display-blackout) + DISPLAY_BLACK_REPORTED=true + EXTERNAL_DISPLAY_FIX_MODE=true + ;; --persist-display-stability) DISPLAY_BLACK_REPORTED=true DISPLAY_FIX_MODE=true @@ -145,5 +150,15 @@ if [ "$DISPLAY_FIX_MODE" = true ]; then sync fi +# External-display remediation targets the KWin desktop path, not gamescope; it +# shares the explicit physical-black symptom gate with the internal-panel fix. +if [ "$EXTERNAL_DISPLAY_FIX_MODE" = true ]; then + echo "" >> "${REPORT_FILE}" + echo "=== EXTERNAL DISPLAY REMEDIATION PHASE ===" >> "${REPORT_FILE}" + "${MODULES_DIR}/rem_external_display.sh" 2>&1 | "$REDACTOR" >> "${REPORT_FILE}" + echo "" >> "${REPORT_FILE}" + sync +fi + sync exit 0 diff --git a/modules/display_blackout.sh b/modules/display_blackout.sh index e57568a..99c54ea 100755 --- a/modules/display_blackout.sh +++ b/modules/display_blackout.sh @@ -29,6 +29,7 @@ sync echo "--- DRM connector status ---" EDP_DIR="" +EDP_ENABLED="not-exposed" for conn in "${SYS_ROOT}"/class/drm/*/status; do [ -f "$conn" ] || continue connector_dir=$(dirname "$conn") @@ -38,6 +39,7 @@ for conn in "${SYS_ROOT}"/class/drm/*/status; do echo " ${name}: status=${status}, enabled=${enabled}" if [ "$status" = "connected" ] && echo "$name" | grep -qi 'edp'; then EDP_DIR="$connector_dir" + EDP_ENABLED="$enabled" fi done @@ -109,6 +111,54 @@ fi echo " Active hardware planes with framebuffers: ${ACTIVE_PLANES}" sync +echo "--- External display / desktop session (read-only) ---" +SESSION_HOME=$(getent passwd "$SESSION_USER" 2>/dev/null | cut -d: -f6) +KWIN_OUTPUT_CONFIG="${DECKDOC_KWIN_OUTPUT_CONFIG:-${SESSION_HOME}/.config/kwinoutputconfig.json}" +KWIN_ACTIVE=false +if [ "${DECKDOC_KWIN_ACTIVE:-auto}" = "auto" ]; then + if pgrep -x kwin_wayland >/dev/null 2>&1; then KWIN_ACTIVE=true; fi +else + KWIN_ACTIVE="${DECKDOC_KWIN_ACTIVE}" +fi +echo " KWin desktop session active: ${KWIN_ACTIVE}" +EXTERNAL_CONNECTED=false +ANY_VRR_UNPINNED=false +ANY_VRR_UNKNOWN=false +for conn in "${SYS_ROOT}"/class/drm/*/status; do + [ -f "$conn" ] || continue + connector_dir=$(dirname "$conn") + name=$(basename "$connector_dir") + case "$name" in *eDP*|*Writeback*) continue ;; esac + [ "$(read_value "$conn")" = "connected" ] || continue + EXTERNAL_CONNECTED=true + ext_edid_bytes=$(wc -c < "${connector_dir}/edid" 2>/dev/null || echo 0) + echo " ${name}: connected, EDID=${ext_edid_bytes} bytes" + if [ -r "${connector_dir}/modes" ]; then head -3 "${connector_dir}/modes" | sed 's/^/ mode: /'; fi + # KWin persists per-display policy keyed by the EDID md5; match this + # connector against the session's stored output configuration. Track + # unpinned and unreadable states across every connector rather than a + # single last-writer flag so one pinned display cannot mask another. + vrr_policy="" + if [ "$ext_edid_bytes" -gt 0 ] && [ -r "$KWIN_OUTPUT_CONFIG" ] && command -v md5sum >/dev/null 2>&1; then + edid_hash=$(md5sum "${connector_dir}/edid" 2>/dev/null | cut -d' ' -f1) + vrr_policy=$(awk -v hash="$edid_hash" ' + /"edidHash"/ { entry_hash = ($0 ~ hash) } + entry_hash && /"vrrPolicy"/ { + gsub(/[",]/, ""); print $2; exit + } + ' "$KWIN_OUTPUT_CONFIG" 2>/dev/null) + fi + if [ -n "$vrr_policy" ]; then + echo " ${name}: stored vrrPolicy=${vrr_policy}" + if [ "$vrr_policy" != "Never" ]; then ANY_VRR_UNPINNED=true; fi + else + echo " ${name}: stored sync policy could not be determined for this display." + ANY_VRR_UNKNOWN=true + fi +done +if [ "$EXTERNAL_CONNECTED" = "false" ]; then echo " No external display connector is connected."; fi +sync + echo "--- Gamescope composition state ---" if [ "${DECKDOC_SKIP_GAMESCOPE:-0}" = "1" ]; then echo " Skipped by test environment." @@ -154,18 +204,50 @@ fi sync echo "--- Display-path assessment ---" -if [ "$DISPLAY_BLACK_REPORTED" = "true" ] && [ -n "$EDP_DIR" ] && [ "$EDP_EDID_BYTES" -gt 0 ] && [ "$BACKLIGHT_LIT" = "true" ] && [ "$CRTC_ACTIVE" = "true" ]; then - echo " BLACKOUT_SIGNATURE: LIVE_RENDER_TO_PHYSICAL_SCANOUT_GAP" - echo " eDP link/EDID, backlight, and CRTC remain live while the physical panel is reported black." - if [ "$ACTIVE_PLANES" -gt 1 ]; then - echo " Multi-plane scanout is active (${ACTIVE_PLANES} planes); test forced composition with --fix-display-blackout." - else - echo " Single-plane composition is already active; continue panel-link/kernel investigation." - fi -elif [ "$DISPLAY_BLACK_REPORTED" = "true" ]; then - echo " BLACKOUT_SIGNATURE: PANEL_OR_MODESET_STATE_INCOMPLETE" - echo " A panel-link, EDID, backlight, or CRTC check failed; forced composition is not automatically indicated." -else +if [ "$DISPLAY_BLACK_REPORTED" != "true" ]; then echo " No physical-black symptom was declared. Use --display-black while it is present." +else + # The internal and external assessments are independent facts: an extended + # desktop can have a live panel and an unpinned external display at once, + # and neither signature may shadow the other. + EXTERNAL_ASSESSED=false + if [ "$EXTERNAL_CONNECTED" = "true" ] && [ "$KWIN_ACTIVE" = "true" ]; then + # A user-visible external blank with a healthy deck-side connector state + # is the discriminator for a presentation-path or downstream fault: the + # kernel keeps scanning out while the monitor loses the picture. KWin + # engages adaptive sync and direct scanout only for fullscreen surfaces, + # which is why the desktop is stable and games are not. + EXTERNAL_ASSESSED=true + if [ "$ANY_VRR_UNPINNED" = "true" ]; then + echo " EXTERNAL_BLACKOUT_SIGNATURE: FULLSCREEN_SYNC_POLICY_GAP" + echo " External connector is live with adaptive sync not pinned off; the monitor or dock" + echo " chain may blank on fullscreen VRR engagement. Test --fix-external-display-blackout." + elif [ "$ANY_VRR_UNKNOWN" = "true" ]; then + # An unreadable policy must never be reported as a confirmed fix; + # the safe direction is to recommend the reversible remediation. + echo " EXTERNAL_BLACKOUT_SIGNATURE: SYNC_POLICY_UNDETERMINED" + echo " The stored sync policy could not be read for at least one external display; treat" + echo " it as unpinned and test --fix-external-display-blackout before deeper investigation." + else + echo " EXTERNAL_BLACKOUT_SIGNATURE: SYNC_POLICY_ALREADY_PINNED" + echo " Adaptive sync is already pinned off for every external display; continue dock-segment," + echo " cable, and monitor investigation (the deck-side link shows no fault)." + fi + fi + # A connected panel that is disabled in the output configuration (docked, + # lid use, external-only setups) still reports a lit backlight; it must not + # claim the blackout signature while inactive. + if [ -n "$EDP_DIR" ] && [ "$EDP_ENABLED" != "disabled" ] && [ "$EDP_EDID_BYTES" -gt 0 ] && [ "$BACKLIGHT_LIT" = "true" ] && [ "$CRTC_ACTIVE" = "true" ]; then + echo " BLACKOUT_SIGNATURE: LIVE_RENDER_TO_PHYSICAL_SCANOUT_GAP" + echo " eDP link/EDID, backlight, and CRTC remain live while the physical panel is reported black." + if [ "$ACTIVE_PLANES" -gt 1 ]; then + echo " Multi-plane scanout is active (${ACTIVE_PLANES} planes); test forced composition with --fix-display-blackout." + else + echo " Single-plane composition is already active; continue panel-link/kernel investigation." + fi + elif [ "$EXTERNAL_ASSESSED" != "true" ]; then + echo " BLACKOUT_SIGNATURE: PANEL_OR_MODESET_STATE_INCOMPLETE" + echo " A panel-link, EDID, backlight, or CRTC check failed; forced composition is not automatically indicated." + fi fi sync diff --git a/modules/rem_external_display.sh b/modules/rem_external_display.sh new file mode 100755 index 0000000..3648107 --- /dev/null +++ b/modules/rem_external_display.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +set -uo pipefail + +echo "[REMEDIATION: External Display Blackout / Adaptive-Sync Pinning]" +SYS_ROOT="${DECKDOC_SYS_ROOT:-/sys}" +DECKDOC_DIR="${DECKDOC_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +REPORTED="${DECKDOC_DISPLAY_BLACK_REPORTED:-false}" + +SESSION_USER="${DECKDOC_SESSION_USER:-${SUDO_USER:-$(id -un)}}" +if [ "$SESSION_USER" = "root" ]; then + SESSION_USER=$(loginctl list-users --no-legend 2>/dev/null | awk '$2 != "root" { print $2; exit }') +fi +SESSION_UID=$(id -u "$SESSION_USER" 2>/dev/null || echo "") +SESSION_HOME=$(getent passwd "$SESSION_USER" 2>/dev/null | cut -d: -f6) +KWIN_OUTPUT_CONFIG="${DECKDOC_KWIN_OUTPUT_CONFIG:-${SESSION_HOME}/.config/kwinoutputconfig.json}" + +run_session() { + if [ "$(id -un)" = "$SESSION_USER" ]; then + XDG_RUNTIME_DIR="/run/user/${SESSION_UID}" WAYLAND_DISPLAY="${WAYLAND_SOCKET}" "$@" + else + runuser -u "$SESSION_USER" -- env XDG_RUNTIME_DIR="/run/user/${SESSION_UID}" WAYLAND_DISPLAY="${WAYLAND_SOCKET}" "$@" + fi +} + +echo "--- PRE_CHECK ---" +if [ "$REPORTED" != "true" ]; then + echo "SKIPPED: Physical-black symptom was not explicitly reported." + echo "REMEDIATION_OUTCOME: SKIPPED (symptom not declared)" + exit 0 +fi +if [ -z "$SESSION_USER" ] || [ -z "$SESSION_UID" ] || [ -z "$SESSION_HOME" ]; then + echo "FAILED: Could not resolve the active non-root desktop user." + echo "REMEDIATION_OUTCOME: FAILED (session user unresolved)" + exit 1 +fi +if ! pgrep -x kwin_wayland >/dev/null 2>&1; then + echo "SKIPPED: No KWin desktop session is active. Use --fix-display-blackout for Game Mode." + echo "REMEDIATION_OUTCOME: SKIPPED (KWin inactive)" + exit 0 +fi +if ! command -v kscreen-doctor >/dev/null 2>&1; then + echo "FAILED: kscreen-doctor is not available." + echo "REMEDIATION_OUTCOME: FAILED (kscreen-doctor missing)" + exit 1 +fi +WAYLAND_SOCKET="" +for sock in "/run/user/${SESSION_UID}"/wayland-*; do + [ -S "$sock" ] || continue + case "$sock" in *.lock) continue ;; esac + WAYLAND_SOCKET=$(basename "$sock") + break +done +if [ -z "$WAYLAND_SOCKET" ]; then + echo "FAILED: No Wayland socket found for the desktop session." + echo "REMEDIATION_OUTCOME: FAILED (no Wayland socket)" + exit 1 +fi +EXTERNAL_OUTPUTS="" +for conn in "${SYS_ROOT}"/class/drm/*/status; do + [ -f "$conn" ] || continue + connector_dir=$(dirname "$conn") + name=$(basename "$connector_dir") + case "$name" in *eDP*|*Writeback*) continue ;; esac + [ "$(cat "$conn" 2>/dev/null)" = "connected" ] || continue + # KWin names outputs without the DRM card prefix (card0-DP-1 -> DP-1). + EXTERNAL_OUTPUTS="${EXTERNAL_OUTPUTS} ${name#card*-}" +done +EXTERNAL_OUTPUTS="${EXTERNAL_OUTPUTS# }" +if [ -z "$EXTERNAL_OUTPUTS" ]; then + echo "SKIPPED: No external display connector is connected." + echo "REMEDIATION_OUTCOME: SKIPPED (no external display)" + exit 0 +fi +echo "PASS: KWin active, Wayland socket ${WAYLAND_SOCKET}, external output(s): ${EXTERNAL_OUTPUTS}" + +echo "--- BACKUP ---" +BACKUP_DIR="${DECKDOC_DIR}/remediation_backups" +mkdir -p "$BACKUP_DIR" +STAMP=$(date +%s) +if [ -r "$KWIN_OUTPUT_CONFIG" ]; then + cp -a "$KWIN_OUTPUT_CONFIG" "${BACKUP_DIR}/kwinoutputconfig.json.${STAMP}.bak" + echo "Saved pre-change output policy: ${BACKUP_DIR}/kwinoutputconfig.json.${STAMP}.bak" +else + echo "NOTE: No existing KWin output configuration file to back up." +fi +run_session kscreen-doctor -o > "${BACKUP_DIR}/kscreen_pre_${STAMP}.txt" 2>&1 || true + +echo "--- EXECUTE ---" +# Pinning adaptive sync off changes fullscreen VRR engagement only. It does not +# change resolution, refresh rate, panel power, brightness, TDP, or GPU clocks. +APPLIED="" +for output in $EXTERNAL_OUTPUTS; do + if run_session kscreen-doctor "output.${output}.vrrpolicy.never" >/dev/null 2>&1; then + echo "Applied vrrpolicy=never on ${output}." + APPLIED="${APPLIED} ${output}" + else + echo "FAILED: kscreen-doctor rejected vrrpolicy change on ${output}." + fi +done +if [ -z "${APPLIED# }" ]; then + echo "REMEDIATION_OUTCOME: FAILED (no output accepted the policy)" + exit 1 +fi + +echo "--- VERIFY ---" +sleep 1 +# Verification is scoped to each output this run actually changed; a policy +# already pinned on some other display (or a stale per-EDID entry from an +# earlier fix) must never satisfy the check for this one. +LIVE_STATE=$(run_session kscreen-doctor -o 2>/dev/null | sed 's/\x1b\[[0-9;]*m//g') +VERIFY_FAILED=false +for output in $APPLIED; do + live_vrr=$(printf '%s\n' "$LIVE_STATE" | awk -v out="$output" ' + /^Output:/ { in_block = ($3 == out) } + in_block && /^[[:space:]]*Vrr:/ { print $2; exit }') + if [ "$live_vrr" = "Never" ]; then + echo "PASS: ${output} reports adaptive sync pinned off in the live output state." + else + echo "FAILED: ${output} live state reports \"${live_vrr:-no policy}\" instead of a pinned sync policy." + VERIFY_FAILED=true + fi +done +if [ "$VERIFY_FAILED" = "true" ]; then + echo "REMEDIATION_OUTCOME: FAILED (live verification)" + exit 1 +fi +for output in $APPLIED; do + edid_file="" + for candidate in "${SYS_ROOT}"/class/drm/card*-"${output}"/edid; do + [ -r "$candidate" ] && { edid_file="$candidate"; break; } + done + stored="" + if [ -n "$edid_file" ] && [ -r "$KWIN_OUTPUT_CONFIG" ] && command -v md5sum >/dev/null 2>&1; then + edid_hash=$(md5sum "$edid_file" 2>/dev/null | cut -d' ' -f1) + stored=$(awk -v hash="$edid_hash" ' + /"edidHash"/ { entry_hash = ($0 ~ hash) } + entry_hash && /"vrrPolicy"/ { gsub(/[",]/, ""); print $2; exit } + ' "$KWIN_OUTPUT_CONFIG" 2>/dev/null) + fi + if [ "$stored" = "Never" ]; then + echo "PASS: ${output} policy persisted for this display's EDID (survives reboot, keyed to this display)." + else + echo "NOTE: ${output}: persistence not yet visible for this display's EDID; KWin may write it on session exit." + fi +done +echo "NOTE: Software cannot observe the monitor's panel; confirm blanking stops during fullscreen use." +echo "REMEDIATION_OUTCOME: PARTIAL (sync policy pinned; physical confirmation required)" diff --git a/tests/test_runner.sh b/tests/test_runner.sh index 893029c..d650dee 100755 --- a/tests/test_runner.sh +++ b/tests/test_runner.sh @@ -75,10 +75,10 @@ echo "" echo "--- Test 5: Module count ---" MODULE_COUNT=$(ls -1 "${DECKDOC_DIR}"/modules/*.sh | wc -l) echo " Total modules: ${MODULE_COUNT}" -if [ "$MODULE_COUNT" -eq 21 ]; then - echo " PASS: Expected 21 modules present (19 diagnostic + 2 remediation)." +if [ "$MODULE_COUNT" -eq 22 ]; then + echo " PASS: Expected 22 modules present (19 diagnostic + 3 remediation)." else - echo " FAIL: Expected 21 modules, found ${MODULE_COUNT}." + echo " FAIL: Expected 22 modules, found ${MODULE_COUNT}." exit 1 fi @@ -642,6 +642,116 @@ echo "--- Test 31: RyuDeck application diagnostics ---" "${DECKDOC_DIR}/tests/validate_ryudeck.sh" echo " PASS: RyuDeck startup stalls, cache suspicion, renderer failure, rendering, and privacy remain distinct." +# === Test 32: External fullscreen sync-policy signature === +echo "" +echo "--- Test 32: External display sync-policy signature ---" +EXT_ENV="${TEST_ENV}/external" +mkdir -p "${EXT_ENV}/sys/class/drm/card0-eDP-1" \ + "${EXT_ENV}/sys/class/drm/card0-DP-1" \ + "${EXT_ENV}/sys/class/backlight/amdgpu_bl1" \ + "${EXT_ENV}/debug/dri/0" +printf 'connected\n' > "${EXT_ENV}/sys/class/drm/card0-eDP-1/status" +printf 'disabled\n' > "${EXT_ENV}/sys/class/drm/card0-eDP-1/enabled" +head -c 128 /dev/zero > "${EXT_ENV}/sys/class/drm/card0-eDP-1/edid" +printf 'connected\n' > "${EXT_ENV}/sys/class/drm/card0-DP-1/status" +printf 'enabled\n' > "${EXT_ENV}/sys/class/drm/card0-DP-1/enabled" +printf '1920x1080\n' > "${EXT_ENV}/sys/class/drm/card0-DP-1/modes" +printf 'external-display-edid-fixture' > "${EXT_ENV}/sys/class/drm/card0-DP-1/edid" +printf '65535\n' > "${EXT_ENV}/sys/class/backlight/amdgpu_bl1/max_brightness" +printf '9996\n' > "${EXT_ENV}/sys/class/backlight/amdgpu_bl1/brightness" +printf '9996\n' > "${EXT_ENV}/sys/class/backlight/amdgpu_bl1/actual_brightness" +printf '0\n' > "${EXT_ENV}/sys/class/backlight/amdgpu_bl1/bl_power" +printf 'crtc[67]: crtc-0\n active=1\n' > "${EXT_ENV}/debug/dri/0/state" +EXT_EDID_HASH=$(md5sum "${EXT_ENV}/sys/class/drm/card0-DP-1/edid" | cut -d' ' -f1) +printf '%s\n' \ + '[ { "outputs": [ {' \ + ' "connectorName": "DP-1",' \ + " \"edidHash\": \"${EXT_EDID_HASH}\"," \ + ' "vrrPolicy": "Automatic"' \ + '} ] } ]' > "${EXT_ENV}/kwinoutputconfig.json" +EXT_REPORT="${EXT_ENV}/report.txt" +DECKDOC_SYS_ROOT="${EXT_ENV}/sys" \ +DECKDOC_DEBUGFS_ROOT="${EXT_ENV}/debug" \ +DECKDOC_DISPLAY_BLACK_REPORTED=true \ +DECKDOC_KWIN_ACTIVE=true \ +DECKDOC_KWIN_OUTPUT_CONFIG="${EXT_ENV}/kwinoutputconfig.json" \ +DECKDOC_SKIP_JOURNAL=1 \ +DECKDOC_SKIP_GAMESCOPE=1 \ + "${DECKDOC_DIR}/modules/display_blackout.sh" > "$EXT_REPORT" +if grep -q 'EXTERNAL_BLACKOUT_SIGNATURE: FULLSCREEN_SYNC_POLICY_GAP' "$EXT_REPORT" && \ + grep -q 'stored vrrPolicy=Automatic' "$EXT_REPORT"; then + echo " PASS: Un-pinned external sync policy with a disabled panel is classified as a fullscreen sync gap." +else + echo " FAIL: External sync-policy gap was not classified correctly." + cat "$EXT_REPORT" + exit 1 +fi +if grep -q 'BLACKOUT_SIGNATURE: LIVE_RENDER_TO_PHYSICAL_SCANOUT_GAP' "$EXT_REPORT"; then + echo " FAIL: Disabled internal panel wrongly claimed the internal blackout signature." + exit 1 +else + echo " PASS: Disabled internal panel does not shadow the external diagnosis." +fi +sed -i 's/"vrrPolicy": "Automatic"/"vrrPolicy": "Never"/' "${EXT_ENV}/kwinoutputconfig.json" +DECKDOC_SYS_ROOT="${EXT_ENV}/sys" \ +DECKDOC_DEBUGFS_ROOT="${EXT_ENV}/debug" \ +DECKDOC_DISPLAY_BLACK_REPORTED=true \ +DECKDOC_KWIN_ACTIVE=true \ +DECKDOC_KWIN_OUTPUT_CONFIG="${EXT_ENV}/kwinoutputconfig.json" \ +DECKDOC_SKIP_JOURNAL=1 \ +DECKDOC_SKIP_GAMESCOPE=1 \ + "${DECKDOC_DIR}/modules/display_blackout.sh" > "$EXT_REPORT" +if grep -q 'EXTERNAL_BLACKOUT_SIGNATURE: SYNC_POLICY_ALREADY_PINNED' "$EXT_REPORT"; then + echo " PASS: Pinned sync policy routes investigation to the downstream display chain." +else + echo " FAIL: Pinned sync policy was not recognized." + cat "$EXT_REPORT" + exit 1 +fi +DECKDOC_SYS_ROOT="${EXT_ENV}/sys" \ +DECKDOC_DEBUGFS_ROOT="${EXT_ENV}/debug" \ +DECKDOC_DISPLAY_BLACK_REPORTED=true \ +DECKDOC_KWIN_ACTIVE=true \ +DECKDOC_KWIN_OUTPUT_CONFIG="${EXT_ENV}/does-not-exist.json" \ +DECKDOC_SKIP_JOURNAL=1 \ +DECKDOC_SKIP_GAMESCOPE=1 \ + "${DECKDOC_DIR}/modules/display_blackout.sh" > "$EXT_REPORT" +if grep -q 'EXTERNAL_BLACKOUT_SIGNATURE: SYNC_POLICY_UNDETERMINED' "$EXT_REPORT" && \ + ! grep -q 'SYNC_POLICY_ALREADY_PINNED' "$EXT_REPORT"; then + echo " PASS: An unreadable sync policy is reported as undetermined, never as already pinned." +else + echo " FAIL: Missing output policy was not classified as undetermined." + cat "$EXT_REPORT" + exit 1 +fi + +# === Test 33: External display remediation safety contract === +echo "" +echo "--- Test 33: External display remediation safety contract ---" +if grep -q 'kscreen-doctor "output.${output}.vrrpolicy.never"' "${DECKDOC_DIR}/modules/rem_external_display.sh" && \ + grep -q 'REMEDIATION_OUTCOME: SKIPPED (symptom not declared)' "${DECKDOC_DIR}/modules/rem_external_display.sh" && \ + grep -q 'REMEDIATION_OUTCOME: SKIPPED (KWin inactive)' "${DECKDOC_DIR}/modules/rem_external_display.sh" && \ + grep -q 'rem_external_display.sh' "${DECKDOC_DIR}/deckdoc.sh" && \ + grep -q -- '--fix-external-display-blackout' "${DECKDOC_DIR}/deckdoc.sh"; then + echo " PASS: External sync-pinning remediation is present, gated, and wired to its own flag." +else + echo " FAIL: External display remediation is incomplete or unwired." + exit 1 +fi +if grep -q 'in_block = ($3 == out)' "${DECKDOC_DIR}/modules/rem_external_display.sh" && \ + grep -q 'entry_hash = ($0 ~ hash)' "${DECKDOC_DIR}/modules/rem_external_display.sh"; then + echo " PASS: VERIFY scopes live and persisted checks to each targeted output, not the whole state." +else + echo " FAIL: VERIFY is not scoped per targeted output (PR #12-class unscoped verification)." + exit 1 +fi +if grep -Eq '(tee|echo).*(brightness|bl_power|power_control|power_dpm|pp_od_clk_voltage)' "${DECKDOC_DIR}/modules/rem_external_display.sh"; then + echo " FAIL: External display remediation contains a forbidden power/brightness/clock write." + exit 1 +else + echo " PASS: External display remediation contains no panel-power, brightness, or clock writes." +fi + echo "" echo "=========================================" echo "All DeckDoc tests completed successfully."