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
47 changes: 47 additions & 0 deletions src/lib/actions/sandbox/rebuild-messaging-stage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@ const { stageMessagingManifestPlanForRebuild } = D("actions/sandbox/rebuild.js")
) => Promise<unknown>;
};

const emptyStoredMessagingPlan = {
schemaVersion: 1,
sandboxName: "openclaw-sandbox",
agent: "openclaw",
workflow: "remove-channel",
channels: [],
disabledChannels: [],
credentialBindings: [],
networkPolicy: { presets: [], entries: [] },
agentRender: [],
buildSteps: [],
stateUpdates: [],
healthChecks: [],
};

describe("stageMessagingManifestPlanForRebuild non-messaging agent guard", () => {
afterEach(() => {
vi.restoreAllMocks();
Expand Down Expand Up @@ -61,6 +76,38 @@ describe("stageMessagingManifestPlanForRebuild non-messaging agent guard", () =>
expect(result).toBeNull();
});

it("stages an explicit empty rebuild plan so token-backed channels are not rediscovered", async () => {
vi.spyOn(defs, "loadAgent").mockReturnValue({
name: "openclaw",
messagingPlatforms: ["telegram"],
});
const clearPlanEnvSpy = vi.spyOn(messaging.MessagingSetupApplier, "clearPlanEnv");
const writePlanEnvSpy = vi
.spyOn(messaging.MessagingSetupApplier, "writePlanToEnv")
.mockImplementation(() => undefined);

const messages: string[] = [];
const result = await stageMessagingManifestPlanForRebuild(
"openclaw-sandbox",
{
name: "openclaw-sandbox",
messaging: { schemaVersion: 1, plan: emptyStoredMessagingPlan },
},
"openclaw",
(msg) => messages.push(msg),
);

expect(clearPlanEnvSpy).not.toHaveBeenCalled();
expect(writePlanEnvSpy).toHaveBeenCalledWith(
expect.objectContaining({
workflow: "rebuild",
channels: [],
}),
);
expect(messages).toContain("Messaging manifest rebuild plan staged: no configured channels");
expect(result).toMatchObject({ workflow: "rebuild", channels: [] });
});

it("emits the empty-allowlist skip message for a known agent whose messagingPlatforms is an explicit empty allowlist", async () => {
vi.spyOn(defs, "loadAgent").mockReturnValue({
name: "openclaw",
Expand Down
43 changes: 42 additions & 1 deletion src/lib/messaging/compiler/workflow-planner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
createBuiltInRenderTemplateResolver,
} from "../channels";
import { createBuiltInMessagingHookRegistry, MessagingHookRegistry } from "../hooks";
import { createChannelManifestRegistry, type ChannelManifest } from "../manifest";
import { type ChannelManifest, createChannelManifestRegistry } from "../manifest";
import { MessagingWorkflowPlanner } from "./workflow-planner";

const TEST_CREDENTIALS: Readonly<Record<string, string>> = {
Expand Down Expand Up @@ -785,6 +785,47 @@ describe("MessagingWorkflowPlanner", () => {
expect(removed?.agentRender.some((entry) => entry.channelId === "telegram")).toBe(false);
});

it("preserves an explicit empty plan on rebuild after the final channel is removed", async () => {
const existingPlan = await planner().buildPlan({
sandboxName: "demo",
agent: "openclaw",
workflow: "onboard",
isInteractive: false,
configuredChannels: ["telegram"],
credentialAvailability: { TELEGRAM_BOT_TOKEN: true },
});

const removed = await planner().buildChannelRemovePlanFromSandboxEntry({
sandboxName: "demo",
agent: "openclaw",
sandboxEntry: {
name: "demo",
messaging: { schemaVersion: 1, plan: existingPlan },
},
channelId: "telegram",
});

expect(removed?.channels).toEqual([]);

const rebuilt = await planner().buildRebuildPlanFromSandboxEntry({
sandboxName: "demo",
agent: "openclaw",
sandboxEntry: {
name: "demo",
messaging: { schemaVersion: 1, plan: removed! },
},
supportedChannelIds: ["telegram"],
});

expect(rebuilt).toMatchObject({
workflow: "rebuild",
channels: [],
disabledChannels: [],
credentialBindings: [],
networkPolicy: { presets: [], entries: [] },
});
});

it("rebuilds from stored plan input values when config env is unavailable", async () => {
const existingPlan = await withEnv(
{
Expand Down
8 changes: 4 additions & 4 deletions src/lib/messaging/compiler/workflow-planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
// SPDX-License-Identifier: Apache-2.0

import { MessagingHookRegistry } from "../hooks";
import { hydrateDerivedSandboxMessagingPlanFields } from "../persistence";
import { parseSandboxMessagingPlan } from "../plan-validation";
import type {
ChannelManifestRegistry,
MessagingAgentId,
Expand All @@ -13,6 +11,8 @@ import type {
SandboxMessagingPlan,
SandboxMessagingRuntimeSetupPlan,
} from "../manifest";
import { hydrateDerivedSandboxMessagingPlanFields } from "../persistence";
import { parseSandboxMessagingPlan } from "../plan-validation";
import { planHostForward } from "./engines/host-forward-engine";
import { planRuntimeSetup } from "./engines/runtime-setup-engine";
import type { RenderTemplateReferenceResolver } from "./engines/template";
Expand Down Expand Up @@ -120,7 +120,7 @@ export class MessagingWorkflowPlanner {
if (!existingPlan) return null;

const filteredPlan = this.filterPlanChannelsToSupportedAllowlist(existingPlan, context);
if (!filteredPlan || filteredPlan.channels.length === 0) return null;
if (existingPlan.channels.length > 0 && filteredPlan.channels.length === 0) return null;

return refreshDerivedPlanFields(
setPlanDisabledChannels(
Expand All @@ -136,7 +136,7 @@ export class MessagingWorkflowPlanner {
private filterPlanChannelsToSupportedAllowlist(
plan: SandboxMessagingPlan,
context: Pick<MessagingWorkflowPlannerBuildContext, "agent" | "supportedChannelIds">,
): SandboxMessagingPlan | null {
): SandboxMessagingPlan {
if (!Array.isArray(context.supportedChannelIds)) return plan;
const allowlist = new Set(context.supportedChannelIds);
let filtered = plan;
Expand Down
12 changes: 8 additions & 4 deletions test/e2e-scenario/live/agent-turn-latency.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import fs from "node:fs";

import { containsInteger42Answer } from "../../helpers/e2e-answer-assertions.ts";
import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts";
import { resultText } from "../fixtures/clients/index.ts";
import { trustedSandboxShellScript } from "../fixtures/clients/sandbox.ts";
Expand Down Expand Up @@ -88,9 +89,10 @@ test.skipIf(!shouldRunLiveE2EScenarios())(
const openclaw = await openclawTurn(sandbox, apiKey);
expect(openclaw.result.exitCode, resultText(openclaw.result)).toBe(0);
assertNoOpenClawTransportErrors(resultText(openclaw.result));
expect(extractOpenClawAgentText(openclaw.result.stdout), resultText(openclaw.result)).toMatch(
/(^|[^0-9])42([^0-9]|$)/,
);
expect(
containsInteger42Answer(extractOpenClawAgentText(openclaw.result.stdout)),
resultText(openclaw.result),
).toBe(true);
expect(openclaw.elapsedMs).toBeLessThanOrEqual(MAX_TURN_SECONDS * 1000);
results.openclaw = { elapsedMs: openclaw.elapsedMs };

Expand Down Expand Up @@ -152,7 +154,9 @@ test.skipIf(!shouldRunLiveE2EScenarios())(
expect(hermesTurn.exitCode, resultText(hermesTurn)).toBe(0);
const hermesResponse = responseBodyAndStatus(hermesTurn.stdout);
expect(hermesResponse.status, resultText(hermesTurn)).toBe("200");
expect(chatContent(hermesResponse.body)).toMatch(/(^|[^0-9])42([^0-9]|$)/);
expect(containsInteger42Answer(chatContent(hermesResponse.body)), resultText(hermesTurn)).toBe(
true,
);
expect(hermesMs).toBeLessThanOrEqual(MAX_TURN_SECONDS * 1000);
results.hermes = { elapsedMs: hermesMs };
await artifacts.writeJson("turn-latency-results.json", results);
Expand Down
5 changes: 4 additions & 1 deletion test/e2e-scenario/live/full-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";

import { containsInteger42Answer } from "../../helpers/e2e-answer-assertions.ts";
import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts";
import { type HostCliClient } from "../fixtures/clients/host.ts";
import { type SandboxClient, validateSandboxName } from "../fixtures/clients/sandbox.ts";
Expand Down Expand Up @@ -204,7 +205,9 @@ liveTest(
},
);
expect(sandboxInference.exitCode, resultText(sandboxInference)).toBe(0);
expect(sandboxInference.stdout).toMatch(/(^|[^0-9])42([^0-9]|$)/);
expect(containsInteger42Answer(sandboxInference.stdout), resultText(sandboxInference)).toBe(
true,
);

const logs = await repoNemoclaw(
host,
Expand Down
3 changes: 2 additions & 1 deletion test/e2e-scenario/live/gpu-double-onboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";

import { containsInteger42Answer } from "../../helpers/e2e-answer-assertions.ts";
import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts";
import { type HostCliClient } from "../fixtures/clients/host.ts";
import { type SandboxClient, validateSandboxName } from "../fixtures/clients/sandbox.ts";
Expand Down Expand Up @@ -145,7 +146,7 @@ async function expectSandboxInference42(
},
);
expect(response.exitCode, resultText(response)).toBe(0);
expect(response.stdout).toMatch(/(^|[^0-9])42([^0-9]|$)/);
expect(containsInteger42Answer(response.stdout), resultText(response)).toBe(true);
}

liveTest(
Expand Down
11 changes: 9 additions & 2 deletions test/e2e-scenario/live/issue-4462-scope-upgrade-approval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ raise SystemExit(1)
PY
}

contains_integer_42() {
local raw compact
raw="$(cat)"
compact="$(printf '%s' "$raw" | tr -d '[:space:]')"
grep -Eq '(^|[^0-9])42([^0-9]|$)' <<<"$compact"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

assert_agent_scopes_without_admin() {
python3 - <<'PY'
import json, sys
Expand Down Expand Up @@ -159,7 +166,7 @@ if [ -z "$request_id" ]; then
if printf '%s' "$state" | assert_agent_scopes_without_admin >/tmp/issue4462-approved-device.txt 2>/tmp/issue4462-approved-device.err; then
echo "SCOPE_ALREADY_APPROVED=$(cat /tmp/issue4462-approved-device.txt)"
elif [ "$trigger_rc" -eq 0 ] && ! grep -Eiq 'EMBEDDED FALLBACK|scope upgrade pending approval|pairing required|fallbackFrom[": ]+gateway|transport[": ]+embedded' /tmp/issue4462-trigger-agent.log \
&& grep -Eq '(^|[^0-9])42([^0-9]|$)' /tmp/issue4462-trigger-agent.log; then
&& contains_integer_42 </tmp/issue4462-trigger-agent.log; then
echo "TRIGGER_COMPLETED_WITHOUT_PENDING_SCOPE_UPGRADE"
echo "ISSUE_4462_SCOPE_UPGRADE_OK device=trigger-completed request=not-reproduced"
exit 0
Expand Down Expand Up @@ -209,7 +216,7 @@ if grep -Eiq 'EMBEDDED FALLBACK|scope upgrade pending approval|pairing required|
cat /tmp/issue4462-final-agent.log >&2
exit 7
fi
if ! grep -Eq '(^|[^0-9])42([^0-9]|$)' /tmp/issue4462-final-agent.log; then
if ! contains_integer_42 </tmp/issue4462-final-agent.log; then
echo "FINAL_AGENT_MISSING_42" >&2
cat /tmp/issue4462-final-agent.log >&2
exit 8
Expand Down
5 changes: 3 additions & 2 deletions test/e2e-scenario/live/launchable-smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";

import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts";
import { containsInteger42Answer } from "../../helpers/e2e-answer-assertions.ts";
import type { ArtifactSink } from "../fixtures/artifacts.ts";
import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts";
import type { HostCliClient } from "../fixtures/clients/host.ts";
import { validateSandboxName } from "../fixtures/clients/sandbox.ts";
import { expect, test } from "../fixtures/e2e-test.ts";
Expand Down Expand Up @@ -463,7 +464,7 @@ runLaunchableSmokeTest(
).toBe(0);
const agentReply = parseAgentText(agent.stdout);
expect(
/(^|[^0-9])42([^0-9]|$)/.test(agentReply),
containsInteger42Answer(agentReply),
`expected agent reply to contain 42; rc=${agent.exitCode}; reply='${agentReply.slice(0, 200)}'; stdout='${agent.stdout.slice(0, 300)}'; stderr='${agent.stderr.slice(0, 300)}'`,
).toBe(true);

Expand Down
11 changes: 8 additions & 3 deletions test/e2e-scenario/live/openclaw-tui-chat-correlation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { containsReplyTokenAllowingWhitespace } from "../../helpers/e2e-answer-assertions.ts";
import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts";
import { type SandboxClient, trustedSandboxShellScript } from "../fixtures/clients/sandbox.ts";
import { expect, test } from "../fixtures/e2e-test.ts";
Expand Down Expand Up @@ -160,7 +161,7 @@ function analyzeIssue2603Trace({
const finalReplyCounts = new Map<string, number>();
for (const [replyToken, expectedRunId] of expectedRunByReplyToken) {
for (const event of chatEvents) {
if (!event.text.includes(replyToken)) continue;
if (!containsReplyTokenAllowingWhitespace(event.text, replyToken)) continue;
visibleReplyCounts.set(replyToken, (visibleReplyCounts.get(replyToken) ?? 0) + 1);
if (event.state === "final") {
finalReplyCounts.set(replyToken, (finalReplyCounts.get(replyToken) ?? 0) + 1);
Expand Down Expand Up @@ -188,7 +189,7 @@ function analyzeIssue2603Trace({
.filter((event) => event.state === "final")
.flatMap((event) =>
sentRuns
.filter((entry) => event.text.includes(entry.replyToken))
.filter((entry) => containsReplyTokenAllowingWhitespace(event.text, entry.replyToken))
.map((entry) => entry.replyToken),
);

Expand Down Expand Up @@ -288,8 +289,12 @@ function textFromMessage(message) {
return content.map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join("\n");
}

function compactReplyTokenText(value) {
return String(value || "").replace(/\s+/g, "");
}

function sawAllReplies(replyTokens) {
return replyTokens.every((token) => events.some((event) => event.event === "chat" && textFromMessage(event.payload?.message).includes(token)));
return replyTokens.every((token) => events.some((event) => event.event === "chat" && compactReplyTokenText(textFromMessage(event.payload?.message)).includes(compactReplyTokenText(token))));
}

ws.on("message", (data) => {
Expand Down
3 changes: 2 additions & 1 deletion test/e2e-scenario/live/sandbox-operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";

import { containsInteger42Answer } from "../../helpers/e2e-answer-assertions.ts";
import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts";
import type { HostCliClient } from "../fixtures/clients/host.ts";
import { type SandboxClient, trustedSandboxShellScript } from "../fixtures/clients/sandbox.ts";
Expand Down Expand Up @@ -266,7 +267,7 @@ async function assertAgentCanAnswer(host: HostCliClient, sandboxName: string): P
);
const reply = parseOpenClawAgentText(result.stdout);
expectExitZero(result, "openclaw agent --json");
expect(reply, resultText(result)).toMatch(/(^|[^0-9])42([^0-9]|$)/);
expect(containsInteger42Answer(reply), resultText(result)).toBe(true);
}

async function assertStatusFields(host: HostCliClient, sandboxName: string): Promise<void> {
Expand Down
6 changes: 6 additions & 0 deletions test/e2e/lib/openclaw-json.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
# exact envelope shape. This also tolerates wrapper output before the JSON blob
# but intentionally ignores metadata fields so IDs, durations, session names,
# and model/provider details cannot satisfy reply assertions.
e2e_text_contains_integer_42() {
local compact
compact="$(printf '%s' "${1:-}" | tr -d '[:space:]')"
grep -qE '(^|[^0-9])42([^0-9]|$)' <<<"$compact"
}

parse_openclaw_agent_text() {
python3 -c '
import json
Expand Down
4 changes: 2 additions & 2 deletions test/e2e/test-agent-turn-latency-e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ run_openclaw_turn() {
return
fi

if grep -qE '(^|[^0-9])42([^0-9]|$)' <<<"$reply"; then
if e2e_text_contains_integer_42 "$reply"; then
pass "OpenClaw: real agent turn returned 42 in $(duration_s "$OPENCLAW_TURN_MS")"
assert_latency_under_cap "OpenClaw" "$OPENCLAW_TURN_MS"
else
Expand Down Expand Up @@ -463,7 +463,7 @@ print(json.dumps({
return
fi

if grep -qE '(^|[^0-9])42([^0-9]|$)' <<<"$content"; then
if e2e_text_contains_integer_42 "$content"; then
pass "Hermes: real daemon turn returned 42 in $(duration_s "$HERMES_TURN_MS")"
assert_latency_under_cap "Hermes" "$HERMES_TURN_MS"
else
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/test-full-e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ rm -f "$ssh_config"

agent_reply=$(printf '%s' "$agent_response" | parse_openclaw_agent_text 2>/dev/null) || true

if grep -qE "(^|[^0-9])42([^0-9]|$)" <<<"$agent_reply"; then
if e2e_text_contains_integer_42 "$agent_reply"; then
pass "[LIVE] openclaw agent: model answered 6×7=42 through openclaw → inference.local"
else
fail "[LIVE] openclaw agent: expected '42' in agent reply, got: ${agent_reply:0:200}"
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/test-issue-4462-scope-upgrade-approval.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1074,7 +1074,7 @@ openclaw agent --agent main --json --session-id "$session_id" \
last_agent_detail="agent exited ${final_rc}: ${final_output:0:500}"
elif ! grep -q '^__URL_FOR_FINAL_AGENT__=ws://' <<<"$final_output"; then
last_agent_detail="agent command did not preserve OPENCLAW_GATEWAY_URL: ${final_output:0:500}"
elif grep -qE '(^|[^0-9])42([^0-9]|$)' <<<"$reply"; then
elif e2e_text_contains_integer_42 "$reply"; then
agent_ok=1
pass "approved openclaw agent turn answered through gateway mode"
break
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/test-launchable-smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,7 @@ rm -f "$ssh_config" "$agent_stderr_file"

agent_reply=$(printf '%s' "$agent_response" | parse_openclaw_agent_text 2>/dev/null) || true

if grep -qE "(^|[^0-9])42([^0-9]|$)" <<<"$agent_reply"; then
if e2e_text_contains_integer_42 "$agent_reply"; then
pass "[LIVE] openclaw agent: model answered 6×7=42 through openclaw → inference.local"
else
fail "[LIVE] openclaw agent: expected '42' in agent reply; rc=${agent_rc}; reply='${agent_reply:0:200}'; stdout='${agent_response:0:300}'; stderr='${agent_stderr:0:300}'"
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/test-sandbox-operations.sh
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ test_sbx_02_connect_chat() {
local reply
reply=$(printf '%s' "$raw" | parse_openclaw_agent_text 2>/dev/null) || true

if [[ $rc -eq 0 && -n "$reply" ]] && echo "$reply" | grep -qE "(^|[^0-9])42([^0-9]|$)"; then
if [[ $rc -eq 0 && -n "$reply" ]] && e2e_text_contains_integer_42 "$reply"; then
pass "TC-SBX-02: Agent computed 6×7=42 through openclaw → inference.local"
else
fail "TC-SBX-02: Connect & Chat" "Expected '42' in agent reply (rc=$rc); reply='${reply:0:200}'; raw output='${raw:0:200}'"
Expand Down
Loading
Loading