diff --git a/docs/concepts/mantis.md b/docs/concepts/mantis.md index f4014f2333f8..4ca71500d43c 100644 --- a/docs/concepts/mantis.md +++ b/docs/concepts/mantis.md @@ -241,10 +241,12 @@ bash scripts/mantis/build-telegram-desktop-image.sh `--provider aws` targets a Crabbox catalog-only Telegram variant image (`--image-sdk telegram-desktop=7.0.9`) so the generic desktop image never -carries the client. Publishing that variant needs Crabbox coordinator admin -(`crabbox image create` / `image promote`) and is not part of this repository -yet; until it is published, use the local Docker image above. Either image must -provide an +carries the client. `scripts/mantis/bake-telegram-desktop-image.sh` bakes and +publishes it; `crabbox image create` and `image promote` are admin-only, so a +Crabbox coordinator admin runs it with `--run`. Anyone can validate the recipe +first with `--prep-only --run`, which warms one lease, installs the pinned +client, runs the recorder's own contract check, and stops without creating an +image. Either image must provide an executable Telegram Desktop at `/opt/Telegram/Telegram`, a readable desktop version marker, `wmctrl`, `xdotool`, `scrot`, `ffmpeg`, `zbarimg`, and `xdpyinfo`, plus a reachable `DISPLAY=:99`. Crabbox refuses the lease when no diff --git a/scripts/e2e/telegram-desktop-recorder.ts b/scripts/e2e/telegram-desktop-recorder.ts index b50e08f4b6f4..f4a3391089ce 100644 --- a/scripts/e2e/telegram-desktop-recorder.ts +++ b/scripts/e2e/telegram-desktop-recorder.ts @@ -93,7 +93,9 @@ export function renderGoldenImagePreflight(): string { contract="Telegram Desktop recorder golden image contract" fail() { echo "$contract failed: $1" >&2; exit 1; } test -x ${TELEGRAM_BINARY} || fail "${TELEGRAM_BINARY} is not executable" -test "$(cat /var/lib/crabbox/telegram-desktop-version 2>/dev/null)" = "${TELEGRAM_DESKTOP_VERSION}" || fail "/var/lib/crabbox/telegram-desktop-version is not ${TELEGRAM_DESKTOP_VERSION}" +# The marker sits beside the app, not in Crabbox's state dir: that directory is +# not traversable by the lease user on AWS desktop images. +test "$(cat /opt/Telegram/openclaw-image-version 2>/dev/null)" = "${TELEGRAM_DESKTOP_VERSION}" || fail "/opt/Telegram/openclaw-image-version is not ${TELEGRAM_DESKTOP_VERSION}" for command in wmctrl xdotool scrot ffmpeg zbarimg xdpyinfo; do command -v "$command" >/dev/null 2>&1 || fail "$command is not on PATH" done @@ -112,8 +114,11 @@ rm -rf ${shellQuote(TELEGRAM_WORKDIR)} # setsid plus closed stdin detaches Telegram from this SSH session: container sshd # tears down the session process group on exit, which kills a plain background child. setsid ${TELEGRAM_BINARY} -noupdate -workdir ${shellQuote(TELEGRAM_WORKDIR)} ${shellQuote(remotePaths.desktopLog)} 2>&1 & +# setsid execs in place, so this pid becomes Telegram; matching by name would miss +# the window between fork and exec on a cold, large binary. +telegram_pid=$! for _ in $(seq 1 30); do - pgrep -x Telegram >/dev/null 2>&1 || { tail -c 262144 ${shellQuote(remotePaths.desktopLog)} >&2 || true; echo "Telegram Desktop exited before opening a window." >&2; exit 1; } + kill -0 "$telegram_pid" 2>/dev/null || { tail -c 262144 ${shellQuote(remotePaths.desktopLog)} >&2 || true; echo "Telegram Desktop exited before opening a window." >&2; exit 1; } wmctrl -lx | awk 'tolower($0) ~ /telegramdesktop/ {found=1} END {exit !found}' && exit 0 sleep 1 done diff --git a/scripts/mantis/bake-telegram-desktop-image.sh b/scripts/mantis/bake-telegram-desktop-image.sh new file mode 100755 index 000000000000..4063c433e213 --- /dev/null +++ b/scripts/mantis/bake-telegram-desktop-image.sh @@ -0,0 +1,507 @@ +#!/usr/bin/env bash +set -euo pipefail + +telegram_version="7.0.9" +telegram_url="https://github.com/telegramdesktop/tdesktop/releases/download/v7.0.9/tsetup.7.0.9.tar.xz" +telegram_sha256="d3c05df0259ab116d11d8c1cdc1403019d2a3be303ad3b46d16a84e19df6615f" +variant_sdk="telegram-desktop=${telegram_version}" + +region="eu-west-1" +server_class="standard" +server_type="" +image_name="" +output_file="" +crabbox_bin="crabbox" +run=0 +promote=1 +keep_lease=0 +prep_only=0 + +usage() { + cat <<'USAGE' +Usage: scripts/mantis/bake-telegram-desktop-image.sh [flags] + +Bake and optionally publish the catalog-only Telegram Desktop Crabbox image. +By default this prints the plan and exits. Add --run to create paid AWS +leases and an AMI. + +Flags: + --region REGION AWS region. Default: eu-west-1. + --class CLASS Crabbox class. Default: standard. + --type TYPE Optional AWS instance type. + --name NAME Image name. Default: openclaw-telegram-desktop-. + --output FILE JSON summary path. Default: .artifacts/mantis/telegram-desktop-image/.json. + --run Create leases and the AMI. + --no-promote Smoke the candidate without publishing it. + --prep-only Prove the recipe on one lease (prep + smoke), then stop. + Creates no image and needs no coordinator admin. + --keep-lease Keep every lease created by this run. + --crabbox-bin PATH Crabbox executable. Default: crabbox from PATH. + -h, --help Show this help. + +Required with --run: + Crabbox coordinator admin: run from a crabbox login listed in the + coordinator's CRABBOX_GITHUB_ADMIN_OWNERS, or set CRABBOX_COORDINATOR plus + CRABBOX_COORDINATOR_ADMIN_TOKEN for headless use. Image create/promote are + admin-only; the script probes for admin before paying for any lease. + +Optional coordinator access: + CRABBOX_ACCESS_CLIENT_ID + CRABBOX_ACCESS_CLIENT_SECRET +USAGE +} + +require_value() { + local value="${2:-}" + if [[ -z "$value" || "$value" == --* ]]; then + printf '%s requires a value\n' "$1" >&2 + exit 2 + fi +} + +while [[ "$#" -gt 0 ]]; do + case "$1" in + --region) + require_value "$@" + region="$2" + shift 2 + ;; + --class) + require_value "$@" + server_class="$2" + shift 2 + ;; + --type) + require_value "$@" + server_type="$2" + shift 2 + ;; + --name) + require_value "$@" + image_name="$2" + shift 2 + ;; + --output) + require_value "$@" + output_file="$2" + shift 2 + ;; + --run) + run=1 + shift + ;; + --prep-only) + prep_only=1 + shift + ;; + --no-promote) + promote=0 + shift + ;; + --keep-lease) + keep_lease=1 + shift + ;; + --crabbox-bin) + require_value "$@" + crabbox_bin="$2" + shift 2 + ;; + -h | --help) + usage + exit 0 + ;; + *) + printf 'unknown argument: %s\n' "$1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if [[ -z "$image_name" ]]; then + image_name="openclaw-telegram-desktop-$(date -u +%Y%m%d-%H%M)" +fi +if [[ -z "$output_file" ]]; then + output_file=".artifacts/mantis/telegram-desktop-image/${image_name}.json" +fi + +cat <&2 + exit 2 +fi +# Admin probe before any paid lease. image create/promote are admin-only, so a +# prep-only run — which creates no image — skips it. The +# probe asks the coordinator about a non-existent AMI: only an admin login gets +# through to the coordinator, which answers "not found"; a non-admin login is +# refused by the CLI before any request. Anything else (outage, auth, contract +# drift) fails closed rather than paying for leases first. +if [[ "$prep_only" == "1" ]]; then + admin_probe="not found" +else + admin_probe="$("$crabbox_resolved" image fsr-status ami-00000000000000000 --provider aws 2>&1 || true)" +fi +if [[ "$admin_probe" == *"not found"* ]]; then + : +elif [[ "$admin_probe" == *"admin command requires"* ]]; then + printf 'Crabbox coordinator admin is required with --run (CRABBOX_GITHUB_ADMIN_OWNERS or CRABBOX_COORDINATOR_ADMIN_TOKEN).\n' >&2 + exit 2 +else + printf 'Could not verify Crabbox coordinator admin access; refusing to start paid leases. Probe output:\n%s\n' "$admin_probe" >&2 + exit 2 +fi +if ! command -v jq >/dev/null 2>&1; then + printf 'jq is required with --run.\n' >&2 + exit 2 +fi + +artifact_dir="$(dirname "$output_file")" +log_dir="$artifact_dir/logs" +mkdir -p "$log_dir" +prep_script="$(mktemp "${TMPDIR:-/tmp}/openclaw-telegram-image-prep.XXXXXX.sh")" +source_lease="" +candidate_lease="" +variant_lease="" +default_lease="" + +cleanup() { + local status=$? + trap - EXIT + rm -f "$prep_script" + if [[ "$keep_lease" != "1" ]]; then + local lease + for lease in "$default_lease" "$variant_lease" "$candidate_lease" "$source_lease"; do + [[ -n "$lease" ]] || continue + "$crabbox_resolved" stop --provider aws --target linux "$lease" || true + done + fi + exit "$status" +} +trap cleanup EXIT + +cat >"$prep_script" </dev/null 2>&1; then + printf 'sudo is required to prepare the Telegram Desktop image.\\n' >&2 + exit 2 + fi + exec sudo -H env DEBIAN_FRONTEND=noninteractive bash "\$0" +fi + +export DEBIAN_FRONTEND=noninteractive + +retry() { + local attempt=1 + until "\$@"; do + if [[ "\$attempt" -ge 5 ]]; then + return 1 + fi + sleep "\$((attempt * 3))" + attempt="\$((attempt + 1))" + done +} + +if [[ "\$(dpkg --print-architecture)" != "amd64" ]]; then + printf 'Telegram Desktop image requires amd64.\\n' >&2 + exit 2 +fi + +retry apt-get update +retry apt-get install -y --no-install-recommends \ + x11-utils \ + zbar-tools \ + libopengl0 \ + libxcb-cursor0 \ + libxcb-icccm4 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-randr0 \ + libxcb-render-util0 \ + libxcb-shape0 \ + libxcb-xfixes0 \ + libxcb-xinerama0 \ + libxkbcommon-x11-0 + +marker=/opt/Telegram/openclaw-image-version +if [[ -x /opt/Telegram/Telegram && -f "\$marker" && "\$(<"\$marker")" == "\$telegram_version" ]]; then + rm -f /opt/Telegram/Updater + test -f /var/lib/crabbox/image-ready + exit 0 +fi + +tmp_dir="\$(mktemp -d /tmp/telegram-desktop-install.XXXXXX)" +trap 'rm -rf "\$tmp_dir"' EXIT +archive="\$tmp_dir/telegram.tar.xz" +retry curl -fL --retry 5 --retry-all-errors -o "\$archive" "\$telegram_url" +printf '%s %s\\n' "\$telegram_sha256" "\$archive" | sha256sum --check --status +tar -xJf "\$archive" -C "\$tmp_dir" +test -x "\$tmp_dir/Telegram/Telegram" + +rm -rf /opt/Telegram +install -d -o root -g root -m 0755 /opt/Telegram +cp -a "\$tmp_dir/Telegram/." /opt/Telegram/ +chown -R root:root /opt/Telegram +chmod 0755 /opt/Telegram /opt/Telegram/Telegram +rm -f /opt/Telegram/Updater +printf '%s\\n' "\$telegram_version" >"\$marker" +chmod 0644 "\$marker" +test -f /var/lib/crabbox/image-ready +PREP +chmod 0755 "$prep_script" + +run_cmd() { + printf '+' + printf ' %q' "$@" + printf '\n' + "$@" +} + +warmup() { + local label="$1" + local image_override="${2:-}" + local image_selector="${3:-}" + local log="$log_dir/${label}.log" + local -a args=( + warmup + --provider aws + --target linux + --desktop + --class "$server_class" + --market on-demand + --ttl 1h + --idle-timeout 20m + --timing-json + ) + if [[ -n "$server_type" ]]; then + args+=(--type "$server_type") + fi + if [[ -n "$image_selector" ]]; then + args+=(--image-sdk "$image_selector") + fi + + printf 'warming %s lease; log=%s\n' "$label" "$log" >&2 + local status=0 + if [[ -n "$image_override" ]]; then + run_cmd env CRABBOX_AWS_REGION="$region" CRABBOX_AWS_AMI="$image_override" \ + "$crabbox_resolved" "${args[@]}" 2>&1 | tee "$log" >&2 || status=$? + else + run_cmd env CRABBOX_AWS_REGION="$region" \ + "$crabbox_resolved" "${args[@]}" 2>&1 | tee "$log" >&2 || status=$? + fi + local lease + lease="$(grep -oE 'cbx_[A-Za-z0-9_-]+' "$log" | tail -n 1 || true)" + if [[ "$status" -ne 0 ]]; then + if [[ -n "$lease" && "$keep_lease" != "1" ]]; then + "$crabbox_resolved" stop --provider aws --target linux "$lease" || true + fi + return "$status" + fi + if [[ -z "$lease" ]]; then + printf 'warmup did not return a cbx lease id; log=%s\n' "$log" >&2 + return 1 + fi + printf '%s\n' "$lease" +} + +selected_image_id() { + local log="$1" + sed -nE 's/.*image selected id=([^[:space:]]+) source=[^[:space:]]+.*/\1/p' "$log" | tail -n 1 +} + +assert_selected_image() { + local log="$1" + local image_id="$2" + local source="$3" + if ! grep -Fq "image selected id=${image_id} source=${source}" "$log"; then + printf 'warmup did not prove image selected id=%s source=%s; log=%s\n' \ + "$image_id" "$source" "$log" >&2 + return 1 + fi +} + +smoke() { + local lease="$1" + local smoke_script + read -r -d '' smoke_script <<'SMOKE' || true +set -euo pipefail +test -x /opt/Telegram/Telegram +test ! -e /opt/Telegram/Updater +test "$(/dev/null +done +DISPLAY=:99 xdpyinfo >/dev/null +rm -rf /tmp/tg-smoke +# Same launch contract the recorder uses: setsid with closed stdin survives the +# SSH session, and process-name matching never matches this script's own shell. +pkill -x Telegram >/dev/null 2>&1 || true +DISPLAY=:99 setsid /opt/Telegram/Telegram -noupdate -workdir /tmp/tg-smoke /tmp/tg-smoke.log 2>&1 & +# setsid execs in place, so this pid becomes Telegram; name matching would miss +# the window between fork and exec on a cold, large binary. +telegram_pid=$! +for _ in $(seq 1 30); do + kill -0 "$telegram_pid" 2>/dev/null || { tail -c 4096 /tmp/tg-smoke.log >&2 || true; echo "Telegram Desktop exited before opening a window." >&2; exit 1; } + DISPLAY=:99 wmctrl -lx | awk 'tolower($0) ~ /telegramdesktop/ {found=1} END {exit !found}' && { pkill -x Telegram >/dev/null 2>&1 || true; exit 0; } + sleep 1 +done +tail -c 4096 /tmp/tg-smoke.log >&2 || true +echo "Telegram Desktop window did not open." >&2 +exit 1 +SMOKE + # --script, not --shell: a login shell runs ~/.bash_logout, whose clear_console + # fails without a console and replaces our exit status with 1. + local smoke_file + smoke_file="$(mktemp "${TMPDIR:-/tmp}/openclaw-telegram-image-smoke.XXXXXX.sh")" + printf '%s\n' "$smoke_script" >"$smoke_file" + chmod 0755 "$smoke_file" + run_cmd "$crabbox_resolved" run --provider aws --target linux --id "$lease" \ + --no-sync --script "$smoke_file" + rm -f "$smoke_file" +} + +stop_lease() { + local lease="$1" + if [[ "$keep_lease" != "1" ]]; then + run_cmd "$crabbox_resolved" stop --provider aws --target linux "$lease" + fi +} + +source_lease="$(warmup source)" +run_cmd "$crabbox_resolved" run --provider aws --target linux --id "$source_lease" \ + --no-sync --script "$prep_script" +smoke "$source_lease" + +if [[ "$prep_only" == "1" ]]; then + printf 'prep-only run passed: the recipe boots Telegram Desktop %s on a real lease.\n' "$telegram_version" + jq -n \ + --arg name "$image_name" \ + --arg region "$region" \ + --arg version "$telegram_version" \ + --arg lease "$source_lease" \ + '{name: $name, region: $region, telegramDesktopVersion: $version, lease: $lease, mode: "prep-only", proofs: {prep: "passed", smoke: "passed", imageCreated: false, promoted: false}}' \ + | tee "$output_file" + exit 0 +fi + +image_json="$log_dir/image-create.json" +env CRABBOX_AWS_REGION="$region" "$crabbox_resolved" image create \ + --id "$source_lease" --name "$image_name" --wait --json | tee "$image_json" +ami_id="$(jq -er '.id | select(type == "string" and startswith("ami-"))' "$image_json")" + +if [[ "$keep_lease" != "1" ]]; then + stop_lease "$source_lease" + source_lease="" +fi + +candidate_lease="$(warmup candidate "$ami_id")" +assert_selected_image "$log_dir/candidate.log" "$ami_id" explicit +smoke "$candidate_lease" +if [[ "$keep_lease" != "1" ]]; then + stop_lease "$candidate_lease" + candidate_lease="" +fi + +promotion_status="skipped" +variant_selection="skipped" +variant_smoke="skipped" +default_unchanged="skipped" +if [[ "$promote" == "1" ]]; then + promote_json="$log_dir/image-promote.json" + env CRABBOX_AWS_REGION="$region" "$crabbox_resolved" image promote "$ami_id" \ + --provider aws --target linux --region "$region" --desktop \ + --catalog-only --variant-sdk "$variant_sdk" --json | tee "$promote_json" + jq -e --arg ami "$ami_id" '.id == $ami and .catalogOnly == true' "$promote_json" >/dev/null + promotion_status="catalog-only" + + variant_lease="$(warmup variant "" "$variant_sdk")" + assert_selected_image "$log_dir/variant.log" "$ami_id" promoted + variant_selection="promoted" + smoke "$variant_lease" + variant_smoke="passed" + if [[ "$keep_lease" != "1" ]]; then + stop_lease "$variant_lease" + variant_lease="" + fi + + default_lease="$(warmup default)" + default_image="$(selected_image_id "$log_dir/default.log")" + if [[ -z "$default_image" ]]; then + printf 'default warmup did not report its selected image; log=%s\n' "$log_dir/default.log" >&2 + exit 1 + fi + if [[ "$default_image" == "$ami_id" ]]; then + printf 'generic desktop selection changed to catalog-only AMI %s.\n' "$ami_id" >&2 + exit 1 + fi + default_unchanged="passed:${default_image}" + if [[ "$keep_lease" != "1" ]]; then + stop_lease "$default_lease" + default_lease="" + fi +fi + +jq -n \ + --arg amiId "$ami_id" \ + --arg name "$image_name" \ + --arg region "$region" \ + --arg version "$telegram_version" \ + --arg promotion "$promotion_status" \ + --arg variantSelection "$variant_selection" \ + --arg variantSmoke "$variant_smoke" \ + --arg defaultUnchanged "$default_unchanged" \ + '{ + amiId: $amiId, + name: $name, + region: $region, + telegramDesktopVersion: $version, + proofs: { + sourceSmoke: "passed", + candidateSelection: "explicit", + candidateSmoke: "passed", + promotion: $promotion, + variantSelection: $variantSelection, + variantSmoke: $variantSmoke, + defaultUnchanged: $defaultUnchanged + } + }' >"$output_file" + +cat </var/lib/crabbox/telegram-desktop-version; \ - chmod 0644 /var/lib/crabbox/telegram-desktop-version; \ + printf '%s\n' "$telegram_desktop_version" >/opt/Telegram/openclaw-image-version; \ + chmod 0644 /opt/Telegram/openclaw-image-version; \ rm -rf "$install_root" /var/lib/apt/lists/* # Fail the build if the base ever drops the account this image defaults to. diff --git a/test/scripts/mantis-telegram-desktop-image.test.ts b/test/scripts/mantis-telegram-desktop-image.test.ts new file mode 100644 index 000000000000..32094337c4cb --- /dev/null +++ b/test/scripts/mantis-telegram-desktop-image.test.ts @@ -0,0 +1,79 @@ +import { spawnSync } from "node:child_process"; +import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +const SCRIPT = "scripts/mantis/bake-telegram-desktop-image.sh"; + +describe("Mantis Telegram Desktop image bake", () => { + it("pins Telegram and proves explicit, promoted, and unchanged-default selection", () => { + const script = readFileSync(SCRIPT, "utf8"); + + expect(script).toContain('telegram_version="7.0.9"'); + expect(script).toContain( + 'telegram_url="https://github.com/telegramdesktop/tdesktop/releases/download/v7.0.9/tsetup.7.0.9.tar.xz"', + ); + expect(script).toContain( + 'telegram_sha256="d3c05df0259ab116d11d8c1cdc1403019d2a3be303ad3b46d16a84e19df6615f"', + ); + expect(script).toContain('--catalog-only --variant-sdk "$variant_sdk"'); + expect(script).toContain('args+=(--image-sdk "$image_selector")'); + expect(script).toContain('assert_selected_image "$log_dir/variant.log" "$ami_id" promoted'); + expect(script).toContain('if [[ "$default_image" == "$ami_id" ]]'); + expect(script).toContain("Crabbox coordinator admin is required with --run"); + expect(script).toContain( + "Could not verify Crabbox coordinator admin access; refusing to start paid leases", + ); + expect(script).not.toContain("ADMIN_TOKEN is required"); + }); + + it("proves the image contract the way the recorder asserts it", () => { + const script = readFileSync(SCRIPT, "utf8"); + + // The marker lives with the app: Crabbox's state dir is not traversable by + // the lease user on AWS desktop images. + expect(script).toContain("marker=/opt/Telegram/openclaw-image-version"); + expect(script).not.toContain("/var/lib/crabbox/telegram-desktop-version"); + // setsid execs in place, so the launcher tracks its pid instead of matching + // by process name, which misses the fork-to-exec window on a cold binary. + expect(script).toContain("setsid /opt/Telegram/Telegram"); + expect(script).toContain('kill -0 "$telegram_pid"'); + // A login shell runs ~/.bash_logout, whose clear_console failure replaces the + // script's exit status, so remote payloads are uploaded, never inlined. + expect(script).toContain("--no-sync --script"); + expect(script).not.toContain("--no-sync --shell"); + }); + + it("runs prep-only without an image, a promotion, or coordinator admin", () => { + const script = readFileSync(SCRIPT, "utf8"); + + expect(script).toContain("--prep-only"); + expect(script).toContain('if [[ "$prep_only" == "1" ]]; then\n admin_probe="not found"'); + expect(script).toContain("prep-only run passed"); + expect(script).toContain("imageCreated: false"); + }); + + it("parses as Bash and performs no Crabbox work without --run", () => { + const syntax = spawnSync("bash", ["-n", SCRIPT], { encoding: "utf8" }); + expect(syntax.stderr).toBe(""); + expect(syntax.status).toBe(0); + + const tempRoot = mkdtempSync(path.join(tmpdir(), "telegram-image-dry-run-")); + try { + const crabbox = path.join(tempRoot, "crabbox"); + const marker = path.join(tempRoot, "called"); + writeFileSync(crabbox, `#!/usr/bin/env bash\nprintf called >${JSON.stringify(marker)}\n`); + chmodSync(crabbox, 0o755); + const dryRun = spawnSync("bash", [SCRIPT, "--crabbox-bin", crabbox], { + encoding: "utf8", + }); + + expect(dryRun.status).toBe(0); + expect(dryRun.stdout).toContain("dry plan only"); + expect(existsSync(marker)).toBe(false); + } finally { + rmSync(tempRoot, { recursive: true }); + } + }); +}); diff --git a/test/scripts/telegram-desktop-recorder.test.ts b/test/scripts/telegram-desktop-recorder.test.ts index 65ee64cbd933..4b487a4f0455 100644 --- a/test/scripts/telegram-desktop-recorder.test.ts +++ b/test/scripts/telegram-desktop-recorder.test.ts @@ -323,7 +323,7 @@ describe("Telegram Desktop recorder remote contract", () => { expect(scripts).toContain("Telegram Desktop recorder golden image contract"); expect(scripts).toContain("/opt/Telegram/Telegram"); expect(scripts).toContain( - 'test "$(cat /var/lib/crabbox/telegram-desktop-version 2>/dev/null)" = "7.0.9"', + 'test "$(cat /opt/Telegram/openclaw-image-version 2>/dev/null)" = "7.0.9"', ); expect(scripts).toContain("DISPLAY=:99 xdpyinfo"); expect(scripts).toContain("wmctrl xdotool scrot ffmpeg zbarimg xdpyinfo"); @@ -331,7 +331,9 @@ describe("Telegram Desktop recorder remote contract", () => { // -f patterns also match this script's own shell (its command line contains the // binary path), so a -f pkill kills the launcher instead of a stale Telegram. expect(scripts).toContain("pkill -x Telegram"); - expect(scripts).toContain("pgrep -x Telegram"); + // setsid execs in place, so liveness follows the pid; name matching would miss + // the fork-to-exec window on a cold binary. + expect(scripts).toContain('kill -0 "$telegram_pid"'); expect(scripts).not.toMatch(/p(kill|grep) -f [^\n]*Telegram/u); // Container sshd tears down the session process group; the client must detach. expect(scripts).toContain("setsid /opt/Telegram/Telegram");