From bcea5e75ebc7485d51655f43acdea9e5d368080f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 24 Apr 2026 07:10:54 +0100 Subject: [PATCH] test: harden Docker E2E readiness --- docs/help/testing.md | 2 +- scripts/e2e/cron-mcp-cleanup-docker-client.ts | 6 +- scripts/e2e/mcp-channels-docker-client.ts | 61 ++++++++++++------- scripts/e2e/mcp-channels-harness.ts | 4 +- .../e2e/npm-onboard-channel-agent-docker.sh | 6 +- 5 files changed, 47 insertions(+), 32 deletions(-) diff --git a/docs/help/testing.md b/docs/help/testing.md index 7a8dda01b1b..f86373d78c0 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -516,7 +516,7 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or - Gateway + dev agent: `pnpm test:docker:live-gateway` (script: `scripts/test-live-gateway-models-docker.sh`) - Open WebUI live smoke: `pnpm test:docker:openwebui` (script: `scripts/e2e/openwebui-docker.sh`) - Onboarding wizard (TTY, full scaffolding): `pnpm test:docker:onboard` (script: `scripts/e2e/onboard-docker.sh`) -- Npm tarball onboarding/channel/agent smoke: `pnpm test:docker:npm-onboard-channel-agent` installs the packed OpenClaw tarball globally in Docker, configures OpenAI via env-ref onboarding plus Telegram by default, verifies enabling the plugin installs its runtime deps on demand, runs doctor, and runs one mocked OpenAI agent turn. Reuse a prebuilt tarball with `OPENCLAW_NPM_ONBOARD_PACKAGE_TGZ=/path/to/openclaw-*.tgz`, skip the host rebuild with `OPENCLAW_NPM_ONBOARD_HOST_BUILD=0`, or switch channel with `OPENCLAW_NPM_ONBOARD_CHANNEL=discord`. +- Npm tarball onboarding/channel/agent smoke: `pnpm test:docker:npm-onboard-channel-agent` installs the packed OpenClaw tarball globally in Docker, configures OpenAI via env-ref onboarding plus Telegram by default, verifies doctor repairs activated plugin runtime deps, and runs one mocked OpenAI agent turn. Reuse a prebuilt tarball with `OPENCLAW_NPM_ONBOARD_PACKAGE_TGZ=/path/to/openclaw-*.tgz`, skip the host rebuild with `OPENCLAW_NPM_ONBOARD_HOST_BUILD=0`, or switch channel with `OPENCLAW_NPM_ONBOARD_CHANNEL=discord`. - Bun global install smoke: `bash scripts/e2e/bun-global-install-smoke.sh` packs the current tree, installs it with `bun install -g` in an isolated home, and verifies `openclaw infer image providers --json` returns bundled image providers instead of hanging. Reuse a prebuilt tarball with `OPENCLAW_BUN_GLOBAL_SMOKE_PACKAGE_TGZ=/path/to/openclaw-*.tgz`, skip the host build with `OPENCLAW_BUN_GLOBAL_SMOKE_HOST_BUILD=0`, or copy `dist/` from a built Docker image with `OPENCLAW_BUN_GLOBAL_SMOKE_DIST_IMAGE=openclaw-dockerfile-smoke:local`. - Installer Docker smoke: `bash scripts/test-install-sh-docker.sh` shares one npm cache across its root, update, and direct-npm containers. Update smoke defaults to npm `latest` as the stable baseline before upgrading to the candidate tarball. Non-root installer checks keep an isolated npm cache so root-owned cache entries do not mask user-local install behavior. Set `OPENCLAW_INSTALL_SMOKE_NPM_CACHE_DIR=/path/to/cache` to reuse the root/update/direct-npm cache across local reruns. - Install Smoke CI skips the duplicate direct-npm global update with `OPENCLAW_INSTALL_SMOKE_SKIP_NPM_GLOBAL=1`; run the script locally without that env when direct `npm install -g` coverage is needed. diff --git a/scripts/e2e/cron-mcp-cleanup-docker-client.ts b/scripts/e2e/cron-mcp-cleanup-docker-client.ts index f4ed3e49fb2..b67cc531d41 100644 --- a/scripts/e2e/cron-mcp-cleanup-docker-client.ts +++ b/scripts/e2e/cron-mcp-cleanup-docker-client.ts @@ -131,7 +131,7 @@ async function runCronCleanupScenario(params: { payload: { kind: "agentTurn", message: "Use available context and then stop.", - timeoutSeconds: 12, + timeoutSeconds: 90, lightContext: true, }, delivery: { mode: "none" }, @@ -182,7 +182,7 @@ async function runCronCleanupScenario(params: { entry.payload.jobId === job.id && entry.payload.action === "finished", )?.payload, - 90_000, + 150_000, ); assert(finished, "missing cron finished event"); @@ -212,7 +212,7 @@ async function runSubagentCleanupScenario(params: { cleanupBundleMcpOnRunEnd: true, idempotencyKey: randomUUID(), deliver: false, - timeout: 20, + timeout: 90, bestEffortDeliver: true, }); assert( diff --git a/scripts/e2e/mcp-channels-docker-client.ts b/scripts/e2e/mcp-channels-docker-client.ts index 82581de5326..05a29cd426d 100644 --- a/scripts/e2e/mcp-channels-docker-client.ts +++ b/scripts/e2e/mcp-channels-docker-client.ts @@ -34,16 +34,21 @@ async function main() { mcp = mcpHandle.client; } - const listed = (await mcp.callTool({ - name: "conversations_list", - arguments: {}, - })) as { - structuredContent?: { conversations?: Array> }; - }; - const conversation = listed.structuredContent?.conversations?.find( - (entry) => entry.sessionKey === "agent:main:main", + const conversation = await waitFor( + "seeded conversation in conversations_list", + async () => { + const listed = (await mcp.callTool({ + name: "conversations_list", + arguments: {}, + })) as { + structuredContent?: { conversations?: Array> }; + }; + return listed.structuredContent?.conversations?.find( + (entry) => entry.sessionKey === "agent:main:main", + ); + }, + 240_000, ); - assert(conversation, "expected seeded conversation in conversations_list"); assert(conversation.channel === "imessage", "expected seeded channel"); assert(conversation.to === "+15551234567", "expected seeded target"); @@ -60,19 +65,31 @@ async function main() { "conversation_get returned wrong session", ); - const history = (await mcp.callTool({ - name: "messages_read", - arguments: { session_key: "agent:main:main", limit: 10 }, - })) as { - structuredContent?: { messages?: Array> }; - }; - const messages = history.structuredContent?.messages ?? []; - assert(messages.length >= 2, "expected seeded transcript messages"); - const attachmentMessage = messages.find((entry) => { - const raw = entry.__openclaw; - return raw && typeof raw === "object" && (raw as { id?: unknown }).id === "msg-attachment"; - }); - assert(attachmentMessage, "expected seeded attachment message"); + const messages = await waitFor( + "seeded transcript messages", + async () => { + const history = (await mcp.callTool({ + name: "messages_read", + arguments: { session_key: "agent:main:main", limit: 10 }, + })) as { + structuredContent?: { messages?: Array> }; + }; + const currentMessages = history.structuredContent?.messages ?? []; + return currentMessages.length >= 2 ? currentMessages : undefined; + }, + 240_000, + ); + await waitFor( + "seeded attachment message", + () => + messages.find((entry) => { + const raw = entry.__openclaw; + return ( + raw && typeof raw === "object" && (raw as { id?: unknown }).id === "msg-attachment" + ); + }), + 240_000, + ); const attachments = (await mcp.callTool({ name: "attachments_fetch", diff --git a/scripts/e2e/mcp-channels-harness.ts b/scripts/e2e/mcp-channels-harness.ts index ed2df2f460d..43973601e75 100644 --- a/scripts/e2e/mcp-channels-harness.ts +++ b/scripts/e2e/mcp-channels-harness.ts @@ -74,12 +74,12 @@ export function extractTextFromGatewayPayload( export async function waitFor( label: string, - predicate: () => T | undefined, + predicate: () => Promise | T | undefined, timeoutMs = 10_000, ): Promise { const started = Date.now(); while (Date.now() - started < timeoutMs) { - const value = predicate(); + const value = await predicate(); if (value !== undefined) { return value; } diff --git a/scripts/e2e/npm-onboard-channel-agent-docker.sh b/scripts/e2e/npm-onboard-channel-agent-docker.sh index 22d4e8635c7..ce1f811ace0 100644 --- a/scripts/e2e/npm-onboard-channel-agent-docker.sh +++ b/scripts/e2e/npm-onboard-channel-agent-docker.sh @@ -432,10 +432,8 @@ if (!serialized.includes(token)) { } NODE -assert_dep_present "$DEP_SENTINEL" - -echo "Running doctor after activated plugin dep install..." -openclaw doctor --non-interactive >/tmp/openclaw-doctor.log 2>&1 +echo "Running doctor after channel activation..." +openclaw doctor --repair --non-interactive >/tmp/openclaw-doctor.log 2>&1 assert_dep_present "$DEP_SENTINEL" echo "Running local agent turn against mocked OpenAI..."