Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/gateway/sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
16 changes: 5 additions & 11 deletions src/gateway/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,15 @@ export async function syncToR2(sandbox: Sandbox, env: MoltbotEnv): Promise<SyncR

// Determine which config directory exists
// Check new path first, fall back to legacy
// Use exit code (0 = exists) rather than stdout parsing to avoid log-flush races
let configDir = '/root/.openclaw';
try {
const checkNew = await sandbox.startProcess(
'test -f /root/.openclaw/openclaw.json && echo "ok"',
);
const checkNew = await sandbox.startProcess('test -f /root/.openclaw/openclaw.json');
await waitForProcess(checkNew, 5000);
const newLogs = await checkNew.getLogs();
if (!newLogs.stdout?.includes('ok')) {
// Try legacy path
const checkLegacy = await sandbox.startProcess(
'test -f /root/.clawdbot/clawdbot.json && echo "ok"',
);
if (checkNew.exitCode !== 0) {
const checkLegacy = await sandbox.startProcess('test -f /root/.clawdbot/clawdbot.json');
await waitForProcess(checkLegacy, 5000);
const legacyLogs = await checkLegacy.getLogs();
if (legacyLogs.stdout?.includes('ok')) {
if (checkLegacy.exitCode === 0) {
configDir = '/root/.clawdbot';
} else {
return {
Expand Down
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
11 changes: 9 additions & 2 deletions test/e2e/_setup.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ start playwright browser
===
./start-browser
---
ready
{{ output }}
---
where
* strip(output) endswith "ready"

===
start video recording
Expand All @@ -31,7 +34,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 });
Expand Down
14 changes: 4 additions & 10 deletions test/e2e/_teardown.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 8 additions & 10 deletions test/e2e/fixture/start-browser
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,38 @@
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")

if [ "${PLAYWRIGHT_HEADED:-}" = "1" ] || [ "${PLAYWRIGHT_HEADED:-}" = "true" ]; then
GLOBAL_ARGS+=("--headed")
fi

# Open the browser to a blank page first
playwright-cli "${GLOBAL_ARGS[@]}" open "about:blank" >/dev/null 2>&1 &
sleep 2
# 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
CF_ACCESS_CLIENT_ID=$(cat "$CCTR_FIXTURE_DIR/cf-access-client-id.txt" 2>/dev/null || echo "")
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 => {
await page.context().setExtraHTTPHeaders({
'CF-Access-Client-Id': '$CF_ACCESS_CLIENT_ID',
'CF-Access-Client-Secret': '$CF_ACCESS_CLIENT_SECRET'
});
}" >/dev/null 2>&1
}" >&2
fi

sleep 1 # Let stderr flush before stdout
echo "ready"
20 changes: 18 additions & 2 deletions test/e2e/pairing_and_conversation.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}"
---

===
Expand Down Expand Up @@ -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');
}"
---

===
Expand All @@ -53,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
Expand Down