fix(android): show conversation in screenshot fixture (#111000)

* fix(android): seed screenshot chat history

* fix(android): update screenshot chat readiness

* fix(android): use visible screenshot chat marker

* style(android): format screenshot script test

* fix(android): pin screenshot timezone

* fix(android): restore screenshot timezone
This commit is contained in:
Peter Steinberger
2026-07-18 17:02:29 -07:00
committed by GitHub
parent a68fdad8f5
commit 68ec50d643
4 changed files with 147 additions and 7 deletions

View File

@@ -191,13 +191,44 @@ internal object AndroidScreenshotFixture {
buildJsonObject {
put("sessionId", JsonPrimitive("screenshot-session"))
put("thinkingLevel", JsonPrimitive("low"))
put("messages", buildJsonArray {})
put(
"messages",
buildJsonArray {
add(chatMessage("user", "What is blocking the Android release?", 1_783_555_020_000))
add(
chatMessage(
"assistant",
"Two review threads are still open on the release branch, and the localization sync needs one more pass. " +
"Once those land, the changelog draft is ready for review and the tag can go out.",
1_783_555_080_000,
),
)
add(chatMessage("user", "Summarize the open review feedback for me.", 1_783_555_140_000))
add(
chatMessage(
"assistant",
"The main thread asks for a regression test around session restore, and the second one wants the new " +
"config key documented before merge. Both are small; I can draft patches for each if you want.",
1_783_555_200_000,
),
)
add(chatMessage("user", "Draft a short status update for the team.", 1_783_555_260_000))
add(
chatMessage(
"assistant",
"The Android release is close. Two review follow-ups and one localization pass remain; once those land, " +
"the changelog can be reviewed and the tag can go out.",
1_783_555_320_000,
),
)
},
)
put(
"sessionInfo",
buildJsonObject {
put("key", JsonPrimitive(mainSessionKey))
put("displayName", JsonPrimitive("New chat"))
put("updatedAt", JsonPrimitive(1_783_555_200_000))
put("updatedAt", JsonPrimitive(1_783_555_320_000))
put("unread", JsonPrimitive(false))
put("modelProvider", JsonPrimitive("openai"))
put("model", JsonPrimitive("gpt-5.2"))
@@ -206,6 +237,16 @@ internal object AndroidScreenshotFixture {
)
}.toString()
private fun chatMessage(
role: String,
content: String,
timestamp: Long,
) = buildJsonObject {
put("role", JsonPrimitive(role))
put("content", JsonPrimitive(content))
put("timestamp", JsonPrimitive(timestamp))
}
private fun sessionList(): String =
buildJsonObject {
put(

View File

@@ -67,6 +67,50 @@ class AndroidScreenshotFixtureTest {
assertEquals("Play publish blocked", parsedCronRuns.last().error)
}
@Test
fun providesDeterministicChatHistory() {
val messages =
json
.parseToJsonElement(AndroidScreenshotFixture.request("chat.history", null))
.jsonObject["messages"]
?.jsonArray
.orEmpty()
assertEquals(
listOf(
listOf("user", "What is blocking the Android release?", "1783555020000"),
listOf(
"assistant",
"Two review threads are still open on the release branch, and the localization sync needs one more pass. " +
"Once those land, the changelog draft is ready for review and the tag can go out.",
"1783555080000",
),
listOf("user", "Summarize the open review feedback for me.", "1783555140000"),
listOf(
"assistant",
"The main thread asks for a regression test around session restore, and the second one wants the new " +
"config key documented before merge. Both are small; I can draft patches for each if you want.",
"1783555200000",
),
listOf("user", "Draft a short status update for the team.", "1783555260000"),
listOf(
"assistant",
"The Android release is close. Two review follow-ups and one localization pass remain; once those land, " +
"the changelog can be reviewed and the tag can go out.",
"1783555320000",
),
),
messages.map { message ->
val fields = message.jsonObject
listOf(
fields["role"]?.jsonPrimitive?.content,
fields["content"]?.jsonPrimitive?.content,
fields["timestamp"]?.jsonPrimitive?.content,
)
},
)
}
@Test
fun rejectsUnexpectedGatewayCalls() {
val error =

View File

@@ -47,6 +47,9 @@ DISPLAY_OVERRIDDEN=0
ORIGINAL_WM_SIZE=""
ORIGINAL_WM_DENSITY=""
SCREENSHOT_DENSITY=""
TIMEZONE_OVERRIDDEN=0
ORIGINAL_AUTO_TIME_ZONE=""
ORIGINAL_TIME_ZONE=""
while [[ $# -gt 0 ]]; do
case "$1" in
@@ -151,6 +154,18 @@ restore_device_display() {
fi
}
restore_device_timezone() {
if [[ "$TIMEZONE_OVERRIDDEN" != "1" || -z "${ADB_BIN:-}" || -z "${ADB_SERIAL:-}" ]]; then
return
fi
if [[ -n "$ORIGINAL_TIME_ZONE" ]]; then
"$ADB_BIN" -s "$ADB_SERIAL" shell cmd alarm set-timezone "$ORIGINAL_TIME_ZONE" >/dev/null 2>&1 || true
fi
if [[ "$ORIGINAL_AUTO_TIME_ZONE" == "true" || "$ORIGINAL_AUTO_TIME_ZONE" == "false" ]]; then
"$ADB_BIN" -s "$ADB_SERIAL" shell cmd time_zone_detector set_auto_detection_enabled "$ORIGINAL_AUTO_TIME_ZONE" >/dev/null 2>&1 || true
fi
}
cleanup_emulator_log() {
if [[ -n "$EMULATOR_LOG" && -f "$EMULATOR_LOG" ]]; then
rm -f "$EMULATOR_LOG"
@@ -159,6 +174,7 @@ cleanup_emulator_log() {
cleanup() {
restore_device_display
restore_device_timezone
cleanup_started_emulator
cleanup_emulator_log
}
@@ -333,6 +349,22 @@ wait_for_explicit_device() {
stabilize_device_for_screenshots() {
local adb="$1"
local serial="$2"
ORIGINAL_AUTO_TIME_ZONE="$("$adb" -s "$serial" shell cmd time_zone_detector is_auto_detection_enabled 2>/dev/null | tr -d '\r')"
ORIGINAL_TIME_ZONE="$("$adb" -s "$serial" shell getprop persist.sys.timezone 2>/dev/null | tr -d '\r')"
if [[ "$ORIGINAL_AUTO_TIME_ZONE" != "true" && "$ORIGINAL_AUTO_TIME_ZONE" != "false" ]]; then
echo "Could not determine emulator automatic timezone setting." >&2
return 1
fi
if [[ -z "$ORIGINAL_TIME_ZONE" ]]; then
echo "Could not determine emulator timezone." >&2
return 1
fi
# Arm cleanup before the first mutation; restoring an unchanged snapshot is safe.
TIMEZONE_OVERRIDDEN=1
# Seeded chat timestamps use the device timezone, so disable detection before
# pinning UTC to avoid inheriting the runner's locale.
"$adb" -s "$serial" shell cmd time_zone_detector set_auto_detection_enabled false >/dev/null
"$adb" -s "$serial" shell cmd alarm set-timezone UTC >/dev/null
"$adb" -s "$serial" shell settings put global window_animation_scale 0 >/dev/null 2>&1 || true
"$adb" -s "$serial" shell settings put global transition_animation_scale 0 >/dev/null 2>&1 || true
"$adb" -s "$serial" shell settings put global animator_duration_scale 0 >/dev/null 2>&1 || true
@@ -404,7 +436,6 @@ boot_emulator() {
serial="$(wait_for_single_device "$adb")"
wait_for_boot_completed "$adb" "$serial"
stabilize_device_for_screenshots "$adb" "$serial"
ADB_SERIAL="$serial"
}
@@ -416,7 +447,6 @@ resolve_device() {
if [[ -n "$DEVICE" ]]; then
wait_for_explicit_device "$adb" "$DEVICE"
stabilize_device_for_screenshots "$adb" "$DEVICE"
ADB_SERIAL="$DEVICE"
return
fi
@@ -429,7 +459,6 @@ resolve_device() {
echo "Stop it so the script can boot '${AVD}', or pass --device '${devices}' to override the no-cutout profile." >&2
return 1
fi
stabilize_device_for_screenshots "$adb" "$devices"
ADB_SERIAL="$devices"
return
fi
@@ -452,7 +481,9 @@ latest_play_debug_apk() {
scene_ready_text() {
case "$1" in
home) printf '%s\n' "Overview" ;;
chat) printf '%s\n' "Ready when you are" ;;
# The screenshot fixture seeds chat history and restores at the latest user
# turn, so wait for that visible anchor instead of empty-chat copy.
chat) printf '%s\n' "Draft a short status update for the team." ;;
voice) printf '%s\n' "Ready to talk" ;;
settings) printf '%s\n' "OpenClaw mobile" ;;
voice-wake) printf '%s\n' "Wake listener" ;;
@@ -568,6 +599,7 @@ fi
ADB_BIN="$(adb_bin)"
resolve_device "$ADB_BIN"
require_emulator_device "$ADB_BIN" "$ADB_SERIAL"
stabilize_device_for_screenshots "$ADB_BIN" "$ADB_SERIAL"
configure_screenshot_display "$ADB_BIN" "$ADB_SERIAL"
mkdir -p "$OUTPUT_DIR"
rm -f "$OUTPUT_DIR"/*.png "$OUTPUT_DIR"/*.jpg "$OUTPUT_DIR"/*.jpeg

View File

@@ -3,6 +3,8 @@ import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const SCRIPT = "scripts/android-screenshots.sh";
const SCREENSHOT_FIXTURE =
"apps/android/app/src/main/java/ai/openclaw/app/AndroidScreenshotFixture.kt";
function runAndroidScreenshots(args: string[], env: NodeJS.ProcessEnv = {}) {
return spawnSync("bash", [SCRIPT, ...args], {
@@ -38,9 +40,13 @@ describe("android screenshots script", () => {
expect(result.stdout).not.toContain(`Android screenshot artifacts: ${process.env.HOME}\n`);
});
it("waits for content unique to the settings and gateway screens", () => {
it("waits for fixture content unique to the chat, settings, and gateway screens", () => {
const script = readFileSync(SCRIPT, "utf8");
const fixture = readFileSync(SCREENSHOT_FIXTURE, "utf8");
expect(script).toContain("chat) printf '%s\\n' \"Draft a short status update for the team.\"");
expect(script).not.toContain("chat) printf '%s\\n' \"Ready when you are\"");
expect(fixture).toContain('"Draft a short status update for the team."');
expect(script).toContain("settings) printf '%s\\n' \"OpenClaw mobile\"");
expect(script).not.toContain("settings) printf '%s\\n' \"Settings\"");
expect(script).toContain(
@@ -58,6 +64,23 @@ describe("android screenshots script", () => {
expect(script).toContain("shell wm density reset");
});
it("pins the device timezone before rendering seeded timestamps", () => {
const script = readFileSync(SCRIPT, "utf8");
const requireEmulatorIndex = script.indexOf('require_emulator_device "$ADB_BIN" "$ADB_SERIAL"');
const stabilizeDeviceIndex = script.indexOf(
'stabilize_device_for_screenshots "$ADB_BIN" "$ADB_SERIAL"',
);
expect(script).toContain("shell cmd time_zone_detector set_auto_detection_enabled false");
expect(script).toContain("shell cmd alarm set-timezone UTC");
expect(script).toContain('shell cmd alarm set-timezone "$ORIGINAL_TIME_ZONE"');
expect(script).toContain(
'shell cmd time_zone_detector set_auto_detection_enabled "$ORIGINAL_AUTO_TIME_ZONE"',
);
expect(requireEmulatorIndex).toBeGreaterThan(-1);
expect(stabilizeDeviceIndex).toBeGreaterThan(requireEmulatorIndex);
});
it("provisions a retained no-cutout screenshot emulator by default", () => {
const script = readFileSync(SCRIPT, "utf8");