From a212938302b5653b2a77b923ba871c82de0766a5 Mon Sep 17 00:00:00 2001 From: Andreas Jansson Date: Fri, 6 Feb 2026 22:03:31 +0100 Subject: [PATCH 1/7] fix(e2e): find start-openclaw.sh process correctly in log dump Use jq to find the process with 'start-openclaw' in its command, instead of grabbing the first process (which was a mount check). --- test/e2e/_teardown.txt | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/test/e2e/_teardown.txt b/test/e2e/_teardown.txt index 6e914b49..ae2952d7 100644 --- a/test/e2e/_teardown.txt +++ b/test/e2e/_teardown.txt @@ -4,19 +4,13 @@ dump gateway logs for debugging WORKER_URL=$(cat "$CCTR_FIXTURE_DIR/worker-url.txt" 2>/dev/null || echo "") if [ -n "$WORKER_URL" ]; then PROCS=$(./curl-auth -s "$WORKER_URL/debug/processes" 2>/dev/null || echo "") - PROC_ID=$(echo "$PROCS" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + PROC_ID=$(echo "$PROCS" | jq -r '[.processes[] | select(.command | contains("start-openclaw"))][0].id // empty' 2>/dev/null) if [ -n "$PROC_ID" ]; then echo "=== Gateway process logs ($PROC_ID) ===" - ./curl-auth -s "$WORKER_URL/debug/logs?id=$PROC_ID" 2>/dev/null | python3 -c " -import sys, json -try: - d = json.load(sys.stdin) - if d.get('stdout'): print('STDOUT:', d['stdout'][-3000:]) - if d.get('stderr'): print('STDERR:', d['stderr'][-3000:]) -except: print('Failed to parse logs') -" || echo "Failed to fetch logs" + LOGS=$(./curl-auth -s "$WORKER_URL/debug/logs?id=$PROC_ID" 2>/dev/null) + echo "$LOGS" | jq -r '"STATUS: \(.process_status)\nSTDOUT: \(.stdout)\nSTDERR: \(.stderr)"' 2>/dev/null || echo "Failed to parse logs" else - echo "No gateway process found" + echo "No start-openclaw.sh process found" echo "Processes: $PROCS" fi else From 0c1b37ded38d90bf8c7c15383e8a1814e1a4b9b7 Mon Sep 17 00:00:00 2001 From: Andreas Jansson Date: Fri, 6 Feb 2026 22:13:45 +0100 Subject: [PATCH 2/7] fix: use exitCode instead of stdout to check config file in sync The cron sync was failing with 'no config file found' because getLogs() sometimes returned empty stdout due to a race between process completion and log flushing. Using exitCode from 'test -f' is more reliable than parsing stdout for 'ok'. --- src/gateway/sync.test.ts | 4 ++-- src/gateway/sync.ts | 16 +++++----------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/gateway/sync.test.ts b/src/gateway/sync.test.ts index 580b0c9b..f062ffed 100644 --- a/src/gateway/sync.test.ts +++ b/src/gateway/sync.test.ts @@ -43,8 +43,8 @@ describe('syncToR2', () => { const { sandbox, startProcessMock } = createMockSandbox(); startProcessMock .mockResolvedValueOnce(createMockProcess('s3fs on /data/moltbot type fuse.s3fs\n')) - .mockResolvedValueOnce(createMockProcess('')) // No openclaw.json - .mockResolvedValueOnce(createMockProcess('')); // No clawdbot.json either + .mockResolvedValueOnce(createMockProcess('', { exitCode: 1 })) // No openclaw.json + .mockResolvedValueOnce(createMockProcess('', { exitCode: 1 })); // No clawdbot.json either const env = createMockEnvWithR2(); diff --git a/src/gateway/sync.ts b/src/gateway/sync.ts index 910f9b37..63808c47 100644 --- a/src/gateway/sync.ts +++ b/src/gateway/sync.ts @@ -43,21 +43,15 @@ export async function syncToR2(sandbox: Sandbox, env: MoltbotEnv): Promise Date: Fri, 6 Feb 2026 22:19:44 +0100 Subject: [PATCH 3/7] fix: skip cron sync if gateway is not running yet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cron job was racing with the startup script — it would check for the config file before openclaw onboard had created it. Now it checks if the gateway process is running first and skips the sync if not. --- src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/index.ts b/src/index.ts index 803c1c30..53b06d3d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -456,6 +456,12 @@ async function scheduled( const options = buildSandboxOptions(env); const sandbox = getSandbox(env.Sandbox, 'moltbot', options); + const gatewayProcess = await findExistingMoltbotProcess(sandbox); + if (!gatewayProcess) { + console.log('[cron] Gateway not running yet, skipping sync'); + return; + } + console.log('[cron] Starting backup sync to R2...'); const result = await syncToR2(sandbox, env); From 07256dbbabc6877b8f8e193d6296a7e750a66b0d Mon Sep 17 00:00:00 2001 From: Andreas Jansson Date: Fri, 6 Feb 2026 22:33:36 +0100 Subject: [PATCH 4/7] fix(e2e): increase browser startup wait from 2s to 20s In CI the browser takes longer to start, causing setExtraHTTPHeaders to fail silently. The browser wasn't ready at 2s so the Access service token headers were never set, causing all requests to be redirected to the Cloudflare Access login page. --- test/e2e/fixture/start-browser | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/fixture/start-browser b/test/e2e/fixture/start-browser index 6338db6c..a18c4637 100755 --- a/test/e2e/fixture/start-browser +++ b/test/e2e/fixture/start-browser @@ -24,7 +24,7 @@ fi # Open the browser to a blank page first playwright-cli "${GLOBAL_ARGS[@]}" open "about:blank" >/dev/null 2>&1 & -sleep 2 +sleep 20 # Read Access credentials CF_ACCESS_CLIENT_ID=$(cat "$CCTR_FIXTURE_DIR/cf-access-client-id.txt" 2>/dev/null || echo "") From 527d35e3106c55d4a257b5e638aec112f05ad2ad Mon Sep 17 00:00:00 2001 From: Andreas Jansson Date: Sat, 7 Feb 2026 09:44:41 +0100 Subject: [PATCH 5/7] fix(e2e): use page.goto() instead of open to preserve Access headers The playwright-cli 'open' command creates a new browser process, which loses the CF-Access-Client-Id/Secret headers set via setExtraHTTPHeaders in start-browser. Replaced all 'open' calls with 'run-code page.goto()' to navigate within the existing browser context. Also added IMPORTANT comment in start-browser explaining this constraint for future maintainers. --- test/e2e/_setup.txt | 6 +++++- test/e2e/fixture/start-browser | 13 +++++-------- test/e2e/pairing_and_conversation.txt | 8 ++++++-- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/test/e2e/_setup.txt b/test/e2e/_setup.txt index 4438a801..25eebfb7 100644 --- a/test/e2e/_setup.txt +++ b/test/e2e/_setup.txt @@ -31,7 +31,11 @@ navigate to main page and wait for worker to be ready === TOKEN=$(cat "$CCTR_FIXTURE_DIR/gateway-token.txt") WORKER_URL=$(cat "$CCTR_FIXTURE_DIR/worker-url.txt") -./pw --session=moltworker-e2e open "$WORKER_URL/?token=$TOKEN" +# Use page.goto() instead of 'open' — 'open' creates a new browser process, +# which loses the CF-Access headers set via setExtraHTTPHeaders in start-browser. +./pw --session=moltworker-e2e run-code "async page => { + await page.goto('$WORKER_URL/?token=$TOKEN'); +}" # Wait for pairing required message (worker shows loading screen first, then UI loads) ./pw --session=moltworker-e2e run-code "async page => { await page.waitForSelector('text=Pairing required', { timeout: 480000 }); diff --git a/test/e2e/fixture/start-browser b/test/e2e/fixture/start-browser index a18c4637..6c83e630 100755 --- a/test/e2e/fixture/start-browser +++ b/test/e2e/fixture/start-browser @@ -4,17 +4,12 @@ set -e SESSION_NAME="moltworker-e2e" -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # Support running directly (not via cctr) if [ -z "$CCTR_FIXTURE_DIR" ]; then CCTR_FIXTURE_DIR="/tmp/e2e-cloud-manual" fi -# Stop and delete any existing session -playwright-cli session-stop "$SESSION_NAME" >/dev/null 2>&1 || true -playwright-cli session-delete "$SESSION_NAME" >/dev/null 2>&1 || true - # Build the args GLOBAL_ARGS=("--session=$SESSION_NAME") @@ -23,7 +18,7 @@ if [ "${PLAYWRIGHT_HEADED:-}" = "1" ] || [ "${PLAYWRIGHT_HEADED:-}" = "true" ]; fi # Open the browser to a blank page first -playwright-cli "${GLOBAL_ARGS[@]}" open "about:blank" >/dev/null 2>&1 & +playwright-cli "${GLOBAL_ARGS[@]}" open "about:blank" & sleep 20 # Read Access credentials @@ -31,13 +26,15 @@ CF_ACCESS_CLIENT_ID=$(cat "$CCTR_FIXTURE_DIR/cf-access-client-id.txt" 2>/dev/nul CF_ACCESS_CLIENT_SECRET=$(cat "$CCTR_FIXTURE_DIR/cf-access-client-secret.txt" 2>/dev/null || echo "") if [ -n "$CF_ACCESS_CLIENT_ID" ] && [ -n "$CF_ACCESS_CLIENT_SECRET" ]; then - # Set extra HTTP headers for Access authentication + # Set extra HTTP headers for Access authentication. + # IMPORTANT: All subsequent navigation MUST use 'run-code page.goto()' instead of 'open', + # because 'open' creates a new browser process which loses these headers. playwright-cli "${GLOBAL_ARGS[@]}" run-code "async page => { await page.context().setExtraHTTPHeaders({ 'CF-Access-Client-Id': '$CF_ACCESS_CLIENT_ID', 'CF-Access-Client-Secret': '$CF_ACCESS_CLIENT_SECRET' }); - }" >/dev/null 2>&1 + }" fi echo "ready" diff --git a/test/e2e/pairing_and_conversation.txt b/test/e2e/pairing_and_conversation.txt index 7ae70dcb..e02a6c7d 100644 --- a/test/e2e/pairing_and_conversation.txt +++ b/test/e2e/pairing_and_conversation.txt @@ -4,7 +4,9 @@ navigate to admin page to approve device === TOKEN=$(cat "$CCTR_FIXTURE_DIR/gateway-token.txt") WORKER_URL=$(cat "$CCTR_FIXTURE_DIR/worker-url.txt") -./pw --session=moltworker-e2e open "$WORKER_URL/_admin/?token=$TOKEN" +./pw --session=moltworker-e2e run-code "async page => { + await page.goto('$WORKER_URL/_admin/?token=$TOKEN'); +}" --- === @@ -41,7 +43,9 @@ navigate back to main chat page === TOKEN=$(cat "$CCTR_FIXTURE_DIR/gateway-token.txt") WORKER_URL=$(cat "$CCTR_FIXTURE_DIR/worker-url.txt") -./pw --session=moltworker-e2e open "$WORKER_URL/?token=$TOKEN" +./pw --session=moltworker-e2e run-code "async page => { + await page.goto('$WORKER_URL/?token=$TOKEN'); +}" --- === From 62fb7c64f3398ae105e3c1e239853dd929dca09d Mon Sep 17 00:00:00 2001 From: Andreas Jansson Date: Sat, 7 Feb 2026 10:27:21 +0100 Subject: [PATCH 6/7] fix(e2e): redirect playwright output to stderr, fix start-browser assertion cctr captures both stdout and stderr. Redirect all playwright output to stderr so it doesn't pollute the test output. Add a 1s sleep before echoing 'ready' to let stderr flush, and use 'endswith ready' assertion. --- test/e2e/_setup.txt | 5 ++++- test/e2e/fixture/start-browser | 9 +++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/test/e2e/_setup.txt b/test/e2e/_setup.txt index 25eebfb7..a1187890 100644 --- a/test/e2e/_setup.txt +++ b/test/e2e/_setup.txt @@ -13,7 +13,10 @@ start playwright browser === ./start-browser --- -ready +{{ output }} +--- +where +* strip(output) endswith "ready" === start video recording diff --git a/test/e2e/fixture/start-browser b/test/e2e/fixture/start-browser index 6c83e630..909a527c 100755 --- a/test/e2e/fixture/start-browser +++ b/test/e2e/fixture/start-browser @@ -17,8 +17,8 @@ if [ "${PLAYWRIGHT_HEADED:-}" = "1" ] || [ "${PLAYWRIGHT_HEADED:-}" = "true" ]; GLOBAL_ARGS+=("--headed") fi -# Open the browser to a blank page first -playwright-cli "${GLOBAL_ARGS[@]}" open "about:blank" & +# Open the browser to a blank page first (output to stderr to keep stdout clean for cctr) +playwright-cli "${GLOBAL_ARGS[@]}" open "about:blank" >&2 & sleep 20 # Read Access credentials @@ -26,7 +26,7 @@ CF_ACCESS_CLIENT_ID=$(cat "$CCTR_FIXTURE_DIR/cf-access-client-id.txt" 2>/dev/nul CF_ACCESS_CLIENT_SECRET=$(cat "$CCTR_FIXTURE_DIR/cf-access-client-secret.txt" 2>/dev/null || echo "") if [ -n "$CF_ACCESS_CLIENT_ID" ] && [ -n "$CF_ACCESS_CLIENT_SECRET" ]; then - # Set extra HTTP headers for Access authentication. + # Set extra HTTP headers for Access authentication (output to stderr). # IMPORTANT: All subsequent navigation MUST use 'run-code page.goto()' instead of 'open', # because 'open' creates a new browser process which loses these headers. playwright-cli "${GLOBAL_ARGS[@]}" run-code "async page => { @@ -34,7 +34,8 @@ if [ -n "$CF_ACCESS_CLIENT_ID" ] && [ -n "$CF_ACCESS_CLIENT_SECRET" ]; then 'CF-Access-Client-Id': '$CF_ACCESS_CLIENT_ID', 'CF-Access-Client-Secret': '$CF_ACCESS_CLIENT_SECRET' }); - }" + }" >&2 fi +sleep 1 # Let stderr flush before stdout echo "ready" From 810dfb719842e387e27a52c5190e529b21f805aa Mon Sep 17 00:00:00 2001 From: Andreas Jansson Date: Sat, 7 Feb 2026 10:44:36 +0100 Subject: [PATCH 7/7] test(e2e): send /models command before math question for video debugging --- test/e2e/pairing_and_conversation.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/e2e/pairing_and_conversation.txt b/test/e2e/pairing_and_conversation.txt index e02a6c7d..fb700a47 100644 --- a/test/e2e/pairing_and_conversation.txt +++ b/test/e2e/pairing_and_conversation.txt @@ -57,6 +57,18 @@ wait for chat interface to load }" --- +=== +send /models command +%require +=== +./pw --session=moltworker-e2e run-code "async page => { + const textarea = await page.waitForSelector('textarea'); + await textarea.fill('/models'); + const btn = await page.waitForSelector('button:has-text(\"Send\")'); + await btn.click(); +}" +--- + === type math question into chat %require