test: harden Docker E2E readiness

This commit is contained in:
Peter Steinberger
2026-04-24 07:10:54 +01:00
parent 7ac35b4f69
commit bcea5e75eb
5 changed files with 47 additions and 32 deletions

View File

@@ -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.

View File

@@ -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(

View File

@@ -34,16 +34,21 @@ async function main() {
mcp = mcpHandle.client;
}
const listed = (await mcp.callTool({
name: "conversations_list",
arguments: {},
})) as {
structuredContent?: { conversations?: Array<Record<string, unknown>> };
};
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<Record<string, unknown>> };
};
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<Record<string, unknown>> };
};
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<Record<string, unknown>> };
};
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",

View File

@@ -74,12 +74,12 @@ export function extractTextFromGatewayPayload(
export async function waitFor<T>(
label: string,
predicate: () => T | undefined,
predicate: () => Promise<T | undefined> | T | undefined,
timeoutMs = 10_000,
): Promise<T> {
const started = Date.now();
while (Date.now() - started < timeoutMs) {
const value = predicate();
const value = await predicate();
if (value !== undefined) {
return value;
}

View File

@@ -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..."