fix(release): align beta 6 e2e fixtures

This commit is contained in:
Vincent Koc
2026-07-31 23:44:40 +08:00
parent aa6e31d8e6
commit b3c1ba93ef
27 changed files with 337 additions and 299 deletions

View File

@@ -886,6 +886,8 @@ jobs:
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: ${{ inputs.release_test_profile == 'full' && 90 || 60 }}
env:
OPENCLAW_BUILD_PRIVATE_QA: "1"
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
OPENCLAW_VITEST_MAX_WORKERS: "2"
steps:
- name: Checkout selected ref

View File

@@ -55,7 +55,6 @@ describe("browser client fetch attachOnly diagnostics", () => {
hung: {
cdpUrl: `http://127.0.0.1:${port}`,
attachOnly: true,
color: "#00AA00",
},
},
},

View File

@@ -26,11 +26,16 @@ describe("qa scenario catalog channel contracts", () => {
const config = readQaScenarioExecutionConfig("native-command-session-target") as
| {
requiredProviderMode?: string;
sessionKey?: string;
}
| undefined;
expect(scenario.execution.channel).toBe("telegram");
expect(config?.requiredProviderMode).toBe("mock-openai");
expect(config?.sessionKey).toBe("agent:main:telegram:direct:qa-native-operator");
expect(JSON.stringify(requireFlowScenario(scenario).execution.flow)).toContain(
"session.key === config.sessionKey && session.hasActiveRun === true",
);
});
it("keeps channel-owned scenarios independent from the driver implementation", () => {
@@ -110,7 +115,20 @@ describe("qa scenario catalog channel contracts", () => {
expect(scenario.coverage?.primary).toEqual(["channels.streaming-final-reply"]);
expect(scenario.coverage?.secondary).toEqual([`${agentRuntime}.streaming-replies-delivery`]);
expect(scenario.gatewayConfigPatch).toMatchObject({
channels: { telegram: { streaming: { mode: "partial" } } },
channels: {
telegram: {
groups: { "*": { requireMention: false } },
streaming: { mode: "partial" },
},
},
});
});
it("disables Telegram mention gating for deterministic group delivery proofs", () => {
const scenario = readQaScenarioById("telegram-assistant-transcript-role-boundary");
expect(scenario.gatewayConfigPatch).toMatchObject({
channels: { telegram: { groups: { "*": { requireMention: false } } } },
});
});

View File

@@ -5,8 +5,7 @@ import fs from "node:fs/promises";
import path from "node:path";
import { escapeRegExp, formatEnvelopeTimestamp } from "openclaw/plugin-sdk/channel-test-helpers";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { setLoggerOverride } from "openclaw/plugin-sdk/runtime-env";
import { withEnvAsync } from "openclaw/plugin-sdk/test-env";
import { getChildLogger, setLoggerOverride } from "openclaw/plugin-sdk/runtime-env";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { getActiveWebListener } from "./active-listener.js";
import { WhatsAppAuthUnstableError, resolveWebCredsPath } from "./auth-store.js";
@@ -19,12 +18,16 @@ import {
getLastWebAutoReplySessionSocket,
installWebAutoReplyTestHomeHooks,
installWebAutoReplyUnitTestHooks,
makeSessionStore,
resetLoadConfigMock,
sendWebDirectInboundMessage,
setLoadConfigMock,
startWebAutoReplyMonitor,
} from "./auto-reply.test-harness.js";
import {
createWhatsAppReplyTransportContext,
deliverWebReply,
} from "./auto-reply/deliver-reply.js";
import { buildInboundLine } from "./auto-reply/monitor/message-line.js";
import {
createTestLegacyFlatWebInboundMessage,
createTestWebInboundMessage,
@@ -91,17 +94,23 @@ async function startWatchdogScenario(params: {
{ timeout: 250, interval: 2 },
);
const spies = createWebInboundDeliverySpies();
await sendWebDirectInboundMessage({
onMessage: scripted.getOnMessage()!,
body: "hi",
from: "+1",
to: "+2",
id: "m1",
spies,
});
await requireOnMessage(scripted.getOnMessage())(
createTestWebInboundMessage({
event: { id: "m1" },
payload: { body: "ignored" },
admission: {
conversation: { kind: "direct", id: "+1" },
ingress: {
admission: "drop",
decision: "block",
decisiveGateId: "sender",
reasonCode: "no_policy_match",
},
},
}),
);
return { scripted, sleep, spies, ...started };
return { scripted, sleep, ...started };
}
function expectErrorContaining(errorFn: unknown, text: string): void {
@@ -231,14 +240,23 @@ describe("web auto-reply connection", () => {
).appendReplyWindow,
).toBeUndefined();
await sendWebDirectInboundMessage({
onMessage: requireOnMessage(scripted.getOnMessage()),
body: "hi before reconnect",
from: "+1",
to: "+2",
id: "active-before-reconnect",
spies: createWebInboundDeliverySpies(),
});
// The controller records inbound activity before admission; this test only needs that
// lifecycle fact and must not start unrelated delivery/session work.
await requireOnMessage(scripted.getOnMessage())(
createTestWebInboundMessage({
event: { id: "active-before-reconnect" },
payload: { body: "ignored" },
admission: {
conversation: { kind: "direct", id: "+1" },
ingress: {
admission: "drop",
decision: "block",
decisiveGateId: "sender",
reasonCode: "no_policy_match",
},
},
}),
);
const reconnectStartedAt = Date.now();
scripted.resolveClose(0, { status: 408, isLoggedOut: false, error: new Error("timeout") });
@@ -995,16 +1013,16 @@ describe("web auto-reply connection", () => {
it("normalizes legacy flat listener messages and rejects partial nested input", async () => {
const capture = createWebListenerFactoryCapture();
const { sendMedia, sendComposing, reply } = createWebInboundDeliverySpies();
const resolver = vi.fn().mockResolvedValue(undefined);
await monitorWebChannel(false, capture.listenerFactory as never, false, async () => ({
text: "ok",
}));
await monitorWebChannel(false, capture.listenerFactory as never, false, resolver);
const onMessage = requireOnMessage(capture.getOnMessage());
const msg = createTestLegacyFlatWebInboundMessage({
from: "+1",
conversationId: "+1",
chatId: "+1",
to: "+2",
accessControlPassed: false,
reply,
});
@@ -1027,7 +1045,8 @@ describe("web auto-reply connection", () => {
).toBe(false);
await onMessage(msg);
expect(reply).toHaveBeenCalledWith("ok", undefined);
expect(resolver).not.toHaveBeenCalled();
expect(reply).not.toHaveBeenCalled();
await expect(
onMessage({
event: { id: "canonical-no-admission" },
@@ -1046,8 +1065,7 @@ describe("web auto-reply connection", () => {
}),
).rejects.toThrow(/missing admission facts/);
expect(reply).toHaveBeenCalledWith("ok", undefined);
expect(reply).toHaveBeenCalledTimes(1);
expect(reply).not.toHaveBeenCalled();
await expect(
onMessage({
...msg,
@@ -1057,82 +1075,58 @@ describe("web auto-reply connection", () => {
).rejects.toThrow(/legacy flat or canonical nested/);
});
it("processes inbound messages without batching and preserves timestamps", async () => {
await withEnvAsync({ TZ: "Europe/Vienna" }, async () => {
const originalMax = process.getMaxListeners();
process.setMaxListeners?.(1);
it("raises the process listener budget before opening the web listener", async () => {
const originalMax = process.getMaxListeners();
process.setMaxListeners?.(1);
try {
const capture = createWebListenerFactoryCapture();
await monitorWebChannel(
false,
capture.listenerFactory as never,
false,
async () => undefined,
);
expect(process.getMaxListeners?.()).toBeGreaterThanOrEqual(50);
} finally {
process.setMaxListeners?.(originalMax);
}
});
const store = await makeSessionStore({
main: { sessionId: "sid", updatedAt: Date.now() },
it("builds separate timestamped inbound envelopes without batching", () => {
const cfg = {} as OpenClawConfig;
const buildLine = (body: string, id: string, timestamp: number) =>
buildInboundLine({
cfg,
agentId: "main",
envelope: { timezone: "utc" },
msg: createTestWebInboundMessage({
event: { id, timestamp },
payload: { body },
platform: { recipientJid: "+2" },
admission: {
conversation: { kind: "direct", id: "+1" },
sender: { id: "+1" },
},
}),
});
try {
const { sendMedia, reply, sendComposing } = createWebInboundDeliverySpies();
const resolver = vi.fn().mockResolvedValue({ text: "ok" });
const firstBody = buildLine("first", "m1", 1735689600000);
const secondBody = buildLine("second", "m2", 1735693200000);
const firstTimestamp = formatEnvelopeTimestamp(new Date("2025-01-01T00:00:00Z"), "utc");
const secondTimestamp = formatEnvelopeTimestamp(new Date("2025-01-01T01:00:00Z"), "utc");
const capture = createWebListenerFactoryCapture();
setLoadConfigMock(() => ({
agents: {
defaults: {
envelopeTimezone: "utc",
},
},
session: { store: store.storePath },
}));
await monitorWebChannel(false, capture.listenerFactory as never, false, resolver);
const capturedOnMessage = requireOnMessage(capture.getOnMessage());
const spies = { sendMedia, reply, sendComposing };
await sendWebDirectInboundMessage({
onMessage: capturedOnMessage,
body: "first",
from: "+1",
to: "+2",
id: "m1",
timestamp: 1735689600000,
spies,
});
if (!capturedOnMessage) {
throw new Error("Expected WhatsApp web runtime to register onMessage.");
}
await sendWebDirectInboundMessage({
onMessage: capturedOnMessage,
body: "second",
from: "+1",
to: "+2",
id: "m2",
timestamp: 1735693200000,
spies,
});
expect(resolver).toHaveBeenCalledTimes(2);
const firstArgs = resolver.mock.calls.at(0)?.[0];
const secondArgs = resolver.mock.calls.at(1)?.[0];
const firstTimestamp = formatEnvelopeTimestamp(new Date("2025-01-01T00:00:00Z"));
const secondTimestamp = formatEnvelopeTimestamp(new Date("2025-01-01T01:00:00Z"));
const firstPattern = escapeRegExp(firstTimestamp);
const secondPattern = escapeRegExp(secondTimestamp);
expect(firstArgs.Body).toMatch(
new RegExp(
`\\[WhatsApp \\+1 (\\+\\d+[smhd] )?${firstPattern}\\] \\+1: \\[openclaw\\] first`,
),
);
expect(firstArgs.Body).not.toContain("second");
expect(secondArgs.Body).toMatch(
new RegExp(
`\\[WhatsApp \\+1 (\\+\\d+[smhd] )?${secondPattern}\\] \\+1: \\[openclaw\\] second`,
),
);
expect(secondArgs.Body).not.toContain("first");
expect(process.getMaxListeners?.()).toBeGreaterThanOrEqual(50);
} finally {
process.setMaxListeners?.(originalMax);
await store.cleanup();
resetLoadConfigMock();
}
});
expect(firstBody).toMatch(
new RegExp(
`\\[WhatsApp \\+1 (\\+\\d+[smhd] )?${escapeRegExp(firstTimestamp)}\\] \\+1: \\[openclaw\\] first`,
),
);
expect(firstBody).not.toContain("second");
expect(secondBody).toMatch(
new RegExp(
`\\[WhatsApp \\+1 (\\+\\d+[smhd] )?${escapeRegExp(secondTimestamp)}\\] \\+1: \\[openclaw\\] second`,
),
);
expect(secondBody).not.toContain("first");
});
it("emits heartbeat logs with connection metadata", async () => {
@@ -1167,12 +1161,18 @@ describe("web auto-reply connection", () => {
},
);
await vi.waitFor(() => expect(listenerFactory).toHaveBeenCalledOnce());
await vi.advanceTimersByTimeAsync(1_000);
controller.abort();
await vi.runAllTimersAsync();
await run.catch(() => {});
vi.useRealTimers();
const content = await fs.readFile(logPath, "utf-8");
let content = "";
await vi.waitFor(async () => {
content = await fs.readFile(logPath, "utf-8").catch(() => "");
expect(content).toMatch(/web-heartbeat/);
});
expect(content).toMatch(/web-heartbeat/);
expect(content).toMatch(/connectionId/);
expect(content).toMatch(/messagesHandled/);
@@ -1181,106 +1181,38 @@ describe("web auto-reply connection", () => {
it("logs outbound replies to file", async () => {
const logPath = `/tmp/openclaw-log-test-${crypto.randomUUID()}.log`;
setLoggerOverride({ level: "trace", file: logPath });
const capture = createWebListenerFactoryCapture();
const resolver = vi.fn().mockResolvedValue({ text: "auto" });
await monitorWebChannel(false, capture.listenerFactory as never, false, resolver as never);
const capturedOnMessage = requireOnMessage(capture.getOnMessage());
await capturedOnMessage(
createTestWebInboundMessage({
event: {
id: "msg1",
},
payload: {
body: "hello",
},
platform: {
chatJid: "+1",
recipientJid: "+2",
sendComposing: vi.fn(),
reply: vi.fn(),
sendMedia: vi.fn(),
},
admission: {
accountId: "default",
conversation: {
kind: "direct",
id: "+1",
},
},
}),
);
const content = await fs.readFile(logPath, "utf-8");
expect(content).toMatch(/web-auto-reply/);
expect(content).toMatch(/auto/);
});
it("marks dispatch idle after replies flush", async () => {
const markDispatchIdle = vi.fn();
const typingMock = {
onReplyStart: vi.fn(async () => {}),
startTypingLoop: vi.fn(async () => {}),
startTypingOnText: vi.fn(async () => {}),
refreshTypingTtl: vi.fn(),
isActive: vi.fn(() => false),
markRunComplete: vi.fn(),
markDispatchIdle,
cleanup: vi.fn(),
};
const { reply, sendComposing, sendMedia } = createWebInboundDeliverySpies();
const replyResolver = vi.fn().mockImplementation(async (ctx, opts) => {
void ctx;
opts?.onTypingController?.(typingMock);
return { text: "final reply" };
const spies = createWebInboundDeliverySpies();
const msg = createTestWebInboundMessage({
event: { id: "msg1" },
payload: { body: "hello" },
platform: {
chatJid: "+1",
recipientJid: "+2",
reply: spies.reply,
sendMedia: spies.sendMedia,
},
admission: {
accountId: "default",
conversation: { kind: "direct", id: "+1" },
},
});
const mockConfig: OpenClawConfig = {
channels: { whatsapp: { allowFrom: ["*"] } },
};
await deliverWebReply({
replyResult: { text: "auto" },
transport: createWhatsAppReplyTransportContext(msg),
maxMediaBytes: 5 * 1024 * 1024,
textLimit: 64_000,
replyLogger: getChildLogger({ module: "web-auto-reply", runId: "file-log-test" }),
connectionId: "conn-file-log",
});
setLoadConfigMock(mockConfig);
await monitorWebChannel(
false,
async ({ onMessage }) => {
await onMessage(
createTestWebInboundMessage({
event: {
id: "m1",
timestamp: Date.now(),
},
payload: {
body: "hello",
},
platform: {
chatJid: "direct:+1000",
recipientJid: "+2000",
sendComposing,
reply,
sendMedia,
},
admission: {
accountId: "default",
conversation: {
kind: "direct",
id: "+1000",
},
},
}),
);
return createMockWebListener();
},
false,
replyResolver,
);
resetLoadConfigMock();
expect(markDispatchIdle).toHaveBeenCalled();
let content = "";
await vi.waitFor(async () => {
content = await fs.readFile(logPath, "utf-8").catch(() => "");
expect(content).toMatch(/web-auto-reply/);
});
expect(content).toMatch(/web-auto-reply/);
expect(content).toMatch(/auto/);
});
});

View File

@@ -23,6 +23,7 @@ type PackageManifest = {
name: string;
version: string;
dependencies?: Record<string, string>;
scripts?: Record<string, string>;
[key: string]: unknown;
};
@@ -184,16 +185,23 @@ function runNpmCommand(
});
}
function normalizeWorkspaceDependencies(
async function normalizeWorkspaceDependencies(
repoRoot: string,
dependencies: Record<string, string> | undefined,
): Record<string, string> | undefined {
): Promise<Record<string, string> | undefined> {
if (!dependencies) {
return undefined;
}
const normalized: Record<string, string> = {};
for (const [name, spec] of Object.entries(dependencies)) {
normalized[name] =
name.startsWith("@openclaw/") && spec.startsWith("workspace:") ? "0.0.0-private" : spec;
if (name.startsWith("@openclaw/") && spec.startsWith("workspace:")) {
const dependencyManifest = await readRawPackageManifest(
resolveWorkspacePackageRoot(repoRoot, name),
);
normalized[name] = dependencyManifest.version;
continue;
}
normalized[name] = spec;
}
return normalized;
}
@@ -205,9 +213,10 @@ async function readRawPackageManifest(packageRoot: string): Promise<PackageManif
async function readPackageManifest(packageRoot: string): Promise<PackageManifest> {
const manifest = await readRawPackageManifest(packageRoot);
const repoRoot = path.resolve(packageRoot, "..", "..");
return {
...manifest,
dependencies: normalizeWorkspaceDependencies(manifest.dependencies),
dependencies: await normalizeWorkspaceDependencies(repoRoot, manifest.dependencies),
};
}
@@ -392,7 +401,8 @@ describe("OpenClaw SDK package e2e", () => {
for (const packageRoot of packageRoots) {
const manifest = await readRawPackageManifest(packageRoot);
await runPnpmCommand(["--filter", manifest.name, "build"], {
const buildScript = manifest.scripts?.prepack ? "prepack" : "build";
await runPnpmCommand(["--filter", manifest.name, buildScript], {
cwd: repoRoot,
timeoutMs: 180_000,
});

View File

@@ -12,6 +12,9 @@ scenario:
gatewayConfigPatch:
channels:
telegram:
groups:
"*":
requireMention: false
streaming:
mode: partial
successCriteria:

View File

@@ -34,6 +34,7 @@ scenario:
requiredProviderMode: mock-openai
conversationId: native-stop-target
senderId: qa-native-operator
sessionKey: agent:main:telegram:direct:qa-native-operator
delayedPrompt: "Subagent recovery worker native command target proof. Wait until stopped."
abortReplyNeedle: Agent was aborted
recoveryMarker: QA-NATIVE-STOP-RECOVERY-OK
@@ -69,7 +70,7 @@ flow:
args:
- lambda:
async: true
expr: "env.gateway.call('sessions.list', {}).then((result) => result.sessions?.find((session) => session.hasActiveRun === true))"
expr: "env.gateway.call('sessions.list', {}).then((result) => result.sessions?.find((session) => session.key === config.sessionKey && session.hasActiveRun === true))"
- expr: liveTurnTimeoutMs(env, 30000)
- 100
- set: startIndex

View File

@@ -7,6 +7,12 @@ scenario:
primary:
- channels.automatic-final-reply
objective: Verify Telegram renders transcript-role-looking assistant text as inert authorship-marked content.
gatewayConfigPatch:
channels:
telegram:
groups:
"*":
requireMention: false
successCriteria:
- The controlled model reply reaches the real Telegram plugin through Crabline.
- Telegram HTML wraps only the transcript-role header in a code element.

View File

@@ -7,6 +7,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { GATEWAY_CLIENT_CAPS } from "../../packages/gateway-protocol/src/client-info.js";
import { clearConfigCache, clearRuntimeConfigSnapshot } from "../config/config.js";
import { clearSessionStoreCacheForTest } from "../config/sessions/store-writer-state.js";
import { ADMIN_SCOPE } from "../gateway/method-scopes.js";
@@ -123,6 +124,7 @@ describe("gateway-hosted exec approvals", () => {
clientDisplayName: "approval operator",
mode: GATEWAY_CLIENT_MODES.TEST,
scopes: [ADMIN_SCOPE],
caps: [GATEWAY_CLIENT_CAPS.EXEC_APPROVALS],
requestTimeoutMs: GATEWAY_CONNECT_TIMEOUT_MS,
timeoutMs: GATEWAY_CONNECT_TIMEOUT_MS,
});

View File

@@ -156,6 +156,7 @@ const makeConfig = (opts?: { fallbacks?: string[]; apiKey?: string }): OpenClawC
fallbacks: opts?.fallbacks ?? [],
},
},
list: [{ id: "test" }],
},
models: {
providers: {
@@ -222,6 +223,9 @@ const copilotModelId = "gpt-4o";
const makeCopilotConfig = (): OpenClawConfig =>
({
agents: {
list: [{ id: "test" }],
},
models: {
providers: {
"github-copilot": {
@@ -377,7 +381,6 @@ async function runAutoPinnedOpenAiTurn(params: {
await runEmbeddedAgentInline({
sessionId: "session:test",
sessionKey: params.sessionKey,
sessionFile: path.join(params.workspaceDir, "session.jsonl"),
workspaceDir: params.workspaceDir,
agentDir: params.agentDir,
config: params.config ?? makeConfig(),
@@ -591,7 +594,6 @@ async function runTurnWithCooldownSeed(params: {
await runEmbeddedAgentInline({
sessionId: "session:test",
sessionKey: params.sessionKey,
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeConfig(),
@@ -625,7 +627,6 @@ describe("runEmbeddedAgent auth profile rotation", () => {
await runEmbeddedAgentInline({
sessionId: "session:test",
sessionKey: "agent:test:read-only-auth-profile-state",
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeConfig(),
@@ -693,7 +694,6 @@ describe("runEmbeddedAgent auth profile rotation", () => {
await runEmbeddedAgentInline({
sessionId: "session:test",
sessionKey: "agent:test:copilot-auth-error",
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeCopilotConfig(),
@@ -782,7 +782,6 @@ describe("runEmbeddedAgent auth profile rotation", () => {
await runEmbeddedAgentInline({
sessionId: "session:test",
sessionKey: "agent:test:copilot-auth-repeat",
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeCopilotConfig(),
@@ -830,7 +829,6 @@ describe("runEmbeddedAgent auth profile rotation", () => {
const runPromise = runEmbeddedAgentInline({
sessionId: "session:test",
sessionKey: "agent:test:copilot-shutdown",
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeCopilotConfig(),
@@ -1024,7 +1022,6 @@ describe("runEmbeddedAgent auth profile rotation", () => {
const result = await runEmbeddedAgentInline({
sessionId: "session:test",
sessionKey: "agent:test:compaction-timeout",
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeConfig(),
@@ -1066,7 +1063,6 @@ describe("runEmbeddedAgent auth profile rotation", () => {
const result = await runEmbeddedAgentInline({
sessionId: "session:test",
sessionKey: "agent:test:compaction-wait-abort",
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeConfig(),
@@ -1095,7 +1091,6 @@ describe("runEmbeddedAgent auth profile rotation", () => {
runEmbeddedAgentInline({
sessionId: "session:test",
sessionKey: "agent:test:user",
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeConfig(),
@@ -1145,7 +1140,6 @@ describe("runEmbeddedAgent auth profile rotation", () => {
await runEmbeddedAgentInline({
sessionId: "session:test",
sessionKey: "agent:test:user-order-excluded",
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeConfig(),
@@ -1174,7 +1168,6 @@ describe("runEmbeddedAgent auth profile rotation", () => {
await runEmbeddedAgentInline({
sessionId: "session:test",
sessionKey: "agent:test:user-auth-alias",
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeConfig(),
@@ -1215,7 +1208,6 @@ describe("runEmbeddedAgent auth profile rotation", () => {
await runEmbeddedAgentInline({
sessionId: "session:test",
sessionKey: "agent:test:mismatch",
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeConfig(),
@@ -1257,7 +1249,6 @@ describe("runEmbeddedAgent auth profile rotation", () => {
runEmbeddedAgentInline({
sessionId: "session:test",
sessionKey: "agent:test:cooldown-failover",
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeConfig({ fallbacks: ["openai/mock-2"] }),
@@ -1301,7 +1292,6 @@ describe("runEmbeddedAgent auth profile rotation", () => {
const result = await runEmbeddedAgentInline({
sessionId: "session:test",
sessionKey: "agent:test:cooldown-probe",
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeConfig({ fallbacks: ["openai/mock-2"] }),
@@ -1349,7 +1339,6 @@ describe("runEmbeddedAgent auth profile rotation", () => {
const result = await runEmbeddedAgentInline({
sessionId: "session:test",
sessionKey: "agent:test:overloaded-cooldown-probe",
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeConfig({ fallbacks: ["openai/mock-2"] }),
@@ -1388,7 +1377,6 @@ describe("runEmbeddedAgent auth profile rotation", () => {
runEmbeddedAgentInline({
sessionId: "session:test",
sessionKey: "agent:test:billing-cooldown-probe-no-fallbacks",
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeConfig(),
@@ -1419,7 +1407,6 @@ describe("runEmbeddedAgent auth profile rotation", () => {
runEmbeddedAgentInline({
sessionId: "session:test",
sessionKey: "agent:support:cooldown-failover",
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeAgentOverrideOnlyFallbackConfig("support"),
@@ -1464,7 +1451,6 @@ describe("runEmbeddedAgent auth profile rotation", () => {
runEmbeddedAgentInline({
sessionId: "session:test",
sessionKey: "agent:test:disabled-failover",
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeConfig({ fallbacks: ["openai/mock-2"] }),
@@ -1500,7 +1486,6 @@ describe("runEmbeddedAgent auth profile rotation", () => {
runEmbeddedAgentInline({
sessionId: "session:test",
sessionKey: "agent:test:auth-unavailable",
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeConfig({ fallbacks: ["openai/mock-2"], apiKey: "" }),
@@ -1539,7 +1524,6 @@ describe("runEmbeddedAgent auth profile rotation", () => {
await runEmbeddedAgentInline({
sessionId: "session:test",
sessionKey: "agent:test:billing-failover-active-model",
sessionFile: path.join(workspaceDir, "session.jsonl"),
workspaceDir,
agentDir,
config: makeConfig({ fallbacks: ["openai/mock-2"] }),

View File

@@ -101,6 +101,7 @@ function makeConfig(primaryProvider = "openai"): OpenClawConfig {
fallbacks: ["groq/mock-2"],
},
},
list: [{ id: "test" }],
},
models: {
providers: {
@@ -265,7 +266,6 @@ async function runEmbeddedFallback(params: {
runEmbeddedAgent({
sessionId,
sessionKey: params.sessionKey,
sessionFile: path.join(params.workspaceDir, `${params.runId}.jsonl`),
workspaceDir: params.workspaceDir,
agentDir: params.agentDir,
config: cfg,
@@ -443,7 +443,6 @@ describe("runWithModelFallback + runEmbeddedAgent failover behavior", () => {
const result = await runEmbeddedAgent({
sessionId: "session:tool-side-effect-terminal",
sessionKey: "agent:test:tool-side-effect-terminal",
sessionFile: path.join(workspaceDir, "tool-side-effect-terminal.jsonl"),
workspaceDir,
agentDir,
config: makeConfig(),
@@ -630,7 +629,6 @@ describe("runWithModelFallback + runEmbeddedAgent failover behavior", () => {
runEmbeddedAgent({
sessionId,
sessionKey: "agent:test:direct-embedded-suspension",
sessionFile: path.join(workspaceDir, "direct-embedded-suspension.jsonl"),
workspaceDir,
agentDir,
config: {

View File

@@ -246,27 +246,21 @@ describe("Agent-specific sandbox config", () => {
{
scope: "agent" as const,
expectedSetup: "echo work",
expectedContainerFragment: "agent-work",
},
{
scope: "shared" as const,
expectedSetup: "echo global",
expectedContainerFragment: "shared",
},
])(
"should resolve $scope setupCommand overrides",
async ({ scope, expectedSetup, expectedContainerFragment }) => {
const cfg = createWorkSetupCommandConfig(scope);
const context = await resolveContext(cfg, "agent:work:main", "/tmp/test-work");
])("should resolve $scope setupCommand overrides", async ({ scope, expectedSetup }) => {
const cfg = createWorkSetupCommandConfig(scope);
const context = await resolveContext(cfg, "agent:work:main", "/tmp/test-work");
if (!context) {
throw new Error(`Expected sandbox context for ${scope} scoped setup`);
}
expect(context.docker?.setupCommand).toBe(expectedSetup);
expect(context.containerName).toContain(expectedContainerFragment);
expectDockerSetupCommand(expectedSetup);
},
);
if (!context) {
throw new Error(`Expected sandbox context for ${scope} scoped setup`);
}
expect(context.docker?.setupCommand).toBe(expectedSetup);
expectDockerSetupCommand(expectedSetup);
});
it("should allow agent-specific docker settings beyond setupCommand", () => {
const cfg: OpenClawConfig = {

View File

@@ -82,8 +82,11 @@ vi.mock("../plugins/hook-runner-global.js", () => ({
describe("subagent registry archive behavior", () => {
let mod: typeof import("./subagent-registry.test-helpers.js");
let createCanonicalSubagentRunFixture: typeof import("./subagent-registry.persistence.test-support.js").createCanonicalSubagentRunFixture;
beforeAll(async () => {
({ createCanonicalSubagentRunFixture } =
await import("./subagent-registry.persistence.test-support.js"));
mod = await import("./subagent-registry.test-helpers.js");
});
@@ -94,10 +97,20 @@ describe("subagent registry archive behavior", () => {
callGateway,
getRuntimeConfig: loadConfigMock as typeof import("../config/config.js").getRuntimeConfig,
ensureRuntimePluginsLoaded: vi.fn(),
maybeWakeRequesterAfterAllChildrenSettled: vi.fn(async (params) => {
params.completeBatch([params.settledEntry.runId]);
return false;
}),
...overrides,
});
};
const addCanonicalSubagentRunForTests = (
entry: Parameters<typeof mod.addSubagentRunForTests>[0],
) => {
mod.addSubagentRunForTests(createCanonicalSubagentRunFixture(entry));
};
const waitForNoRequesterRuns = async () => {
await vi.waitFor(() => {
expect(mod.listSubagentRunsForRequester("agent:main:main")).toHaveLength(0);
@@ -219,7 +232,7 @@ describe("subagent registry archive behavior", () => {
resolveContextEngine: vi.fn(async () => ({ onSubagentEnded }) as never),
});
mod.addSubagentRunForTests({
addCanonicalSubagentRunForTests({
runId: "run-delete-retry",
childSessionKey: "agent:main:subagent:delete-retry",
requesterSessionKey: "agent:main:main",
@@ -250,7 +263,7 @@ describe("subagent registry archive behavior", () => {
it("stabilizes provisional killed tasks before deleting expired tombstones", async () => {
const now = Date.now();
mod.addSubagentRunForTests({
addCanonicalSubagentRunForTests({
runId: "run-killed-tombstone-expired",
childSessionKey: "agent:main:subagent:killed-tombstone-expired",
requesterSessionKey: "agent:main:main",
@@ -288,7 +301,7 @@ describe("subagent registry archive behavior", () => {
it("retains expired tombstones when provisional task finalization is rejected", async () => {
const now = Date.now();
taskRuntimeMocks.finalizeTaskRunByRunId.mockReturnValueOnce([]);
mod.addSubagentRunForTests({
addCanonicalSubagentRunForTests({
runId: "run-killed-tombstone-retry",
childSessionKey: "agent:main:subagent:killed-tombstone-retry",
requesterSessionKey: "agent:main:main",
@@ -321,7 +334,7 @@ describe("subagent registry archive behavior", () => {
it("retires expired tombstones when their task row is already gone", async () => {
const now = Date.now();
taskStatusMocks.findTaskByRunIdForStatus.mockReturnValue(undefined);
mod.addSubagentRunForTests({
addCanonicalSubagentRunForTests({
runId: "run-killed-task-missing",
childSessionKey: "agent:main:subagent:killed-task-missing",
requesterSessionKey: "agent:main:main",
@@ -357,7 +370,7 @@ describe("subagent registry archive behavior", () => {
status: "cancelled",
error: "Cancelled by operator.",
} as never);
mod.addSubagentRunForTests({
addCanonicalSubagentRunForTests({
runId: "run-killed-operator-cancelled",
childSessionKey: "agent:main:subagent:killed-operator-cancelled",
requesterSessionKey: "agent:main:main",
@@ -393,7 +406,7 @@ describe("subagent registry archive behavior", () => {
status: "cancelled",
error: "Cancelled by operator.",
} as never);
mod.addSubagentRunForTests({
addCanonicalSubagentRunForTests({
runId: "run-killed-grace",
childSessionKey: "agent:main:subagent:killed-grace",
requesterSessionKey: "agent:main:main",
@@ -425,7 +438,7 @@ describe("subagent registry archive behavior", () => {
taskStatusMocks.listTasksForSessionKeyForStatus.mockReturnValue([]);
taskRuntimeMocks.finalizeTaskRunByRunId.mockReturnValueOnce([]);
const now = Date.now();
mod.addSubagentRunForTests({
addCanonicalSubagentRunForTests({
runId: "run-killed-opaque-runtime",
childSessionKey: "agent:main:subagent:killed-opaque-runtime",
requesterSessionKey: "agent:main:main",
@@ -463,7 +476,7 @@ describe("subagent registry archive behavior", () => {
createdAt: now - 11 * 60_000,
},
] as never);
mod.addSubagentRunForTests({
addCanonicalSubagentRunForTests({
runId: "run-after-replacement",
childSessionKey: "agent:main:subagent:replacement",
requesterSessionKey: "agent:main:main",
@@ -508,7 +521,7 @@ describe("subagent registry archive behavior", () => {
createdAt: now - 11 * 60_000,
},
] as never);
mod.addSubagentRunForTests({
addCanonicalSubagentRunForTests({
runId: "run-after-replacement-direct-kill",
childSessionKey,
requesterSessionKey: "agent:main:main",
@@ -548,7 +561,7 @@ describe("subagent registry archive behavior", () => {
createdAt: now - 60_000,
},
] as never);
mod.addSubagentRunForTests({
addCanonicalSubagentRunForTests({
runId: "run-old-generation",
childSessionKey: "agent:main:subagent:reused-session",
requesterSessionKey: "agent:main:main",
@@ -578,7 +591,7 @@ describe("subagent registry archive behavior", () => {
it("retires expired keep-mode reconciliation rows without deleting their sessions", async () => {
const now = Date.now();
mod.addSubagentRunForTests({
addCanonicalSubagentRunForTests({
runId: "run-killed-keep-expired",
childSessionKey: "agent:main:subagent:killed-keep-expired",
requesterSessionKey: "agent:main:main",
@@ -613,7 +626,7 @@ describe("subagent registry archive behavior", () => {
it("stabilizes killed tasks before their configured session archive deadline", async () => {
const now = Date.now();
const archiveAtMs = now + 55 * 60_000;
mod.addSubagentRunForTests({
addCanonicalSubagentRunForTests({
runId: "run-killed-retained-session",
childSessionKey: "agent:main:subagent:killed-retained-session",
requesterSessionKey: "agent:main:main",
@@ -657,7 +670,7 @@ describe("subagent registry archive behavior", () => {
throw new Error("plugin load failed");
}),
});
mod.addSubagentRunForTests({
addCanonicalSubagentRunForTests({
runId: "run-killed-hook-load-failure",
childSessionKey: "agent:main:subagent:killed-hook-load-failure",
requesterSessionKey: "agent:main:main",
@@ -699,7 +712,7 @@ describe("subagent registry archive behavior", () => {
return {};
});
mod.addSubagentRunForTests({
addCanonicalSubagentRunForTests({
runId: "run-delete-inflight",
childSessionKey: "agent:main:subagent:delete-inflight",
requesterSessionKey: "agent:main:main",

View File

@@ -575,10 +575,8 @@ describe("subagent registry lifecycle error grace", () => {
await waitForAgentCallCount(1);
expect(readFirstAnnounceOutcome()?.status).toBe("ok");
// Advance past the original grace window; no timeout completion should
// re-announce. The exhausted completion announce suspends its delivery,
// which fires the one-shot requester settle wake for the undelivered
// required completion — that wake is not a completion event.
// Advance past the original grace window; no timeout completion or
// requester-settle wake should be emitted after successful delivery.
await vi.advanceTimersByTimeAsync(30_000);
await flushAsync();
const readIdempotencyKey = (request: GatewayRequest) => {
@@ -592,7 +590,7 @@ describe("subagent registry lifecycle error grace", () => {
getAgentCalls()
.map(readIdempotencyKey)
.filter((key) => key.startsWith("announce:requester-settle:")),
).toEqual(["announce:requester-settle:agent:main:main:run-timeout-cancel"]);
).toHaveLength(0);
});
it("keeps parallel child completion results frozen even when late traffic arrives", async () => {

View File

@@ -17,7 +17,6 @@ import {
} from "../../test/helpers/auto-reply/trigger-handling-test-harness.js";
import { saveAuthProfileStore } from "../agents/auth-profiles/store.js";
import { resolveSessionKey } from "../config/sessions.js";
import { parseSqliteSessionFileMarker } from "../config/sessions/legacy-sqlite-marker.js";
import {
loadExactSessionEntry,
loadSessionEntry,
@@ -564,12 +563,12 @@ describe("trigger handling", () => {
const text = maybeReplyText(res);
expect(text?.startsWith("⚙️ Compacted")).toBe(true);
expect(getCompactEmbeddedAgentSessionMock()).toHaveBeenCalledOnce();
const sessionKey = resolveSessionKey("per-sender", request);
const sessionKey = resolveSessionKey("per-sender", request, undefined, "main");
expect(loadSessionEntry({ storePath, sessionKey })?.compactionCount).toBe(1);
});
});
it("compacts worker sessions via the agent session file", async () => {
it("compacts worker sessions via the explicit session target", async () => {
await withTempHome(async (home) => {
getCompactEmbeddedAgentSessionMock().mockReset();
mockSuccessfulCompaction();
@@ -590,17 +589,16 @@ describe("trigger handling", () => {
const text = maybeReplyText(res);
expect(text?.startsWith("⚙️ Compacted")).toBe(true);
expect(getCompactEmbeddedAgentSessionMock()).toHaveBeenCalledOnce();
const sessionFile = firstMockCallArg(
const call = firstMockCallArg(
getCompactEmbeddedAgentSessionMock(),
"embedded OpenClaw compaction",
).sessionFile;
if (typeof sessionFile !== "string") {
throw new Error("expected embedded OpenClaw compaction sessionFile");
}
expect(parseSqliteSessionFileMarker(sessionFile)).toMatchObject({
);
expect(call.sessionTarget).toMatchObject({
agentId: "worker1",
sessionKey: "agent:worker1:telegram:12345",
storePath: cfg.session.store,
});
expect(call.sessionFile).toBe("agent:worker1:telegram:12345");
});
});

View File

@@ -845,7 +845,7 @@ describe("runReplyAgent heartbeat followup guard", () => {
const sessionStore = {
main: {
sessionId: "pre-compact-session",
sessionFile: "/tmp/pre-compact.jsonl",
sessionFile: "main",
updatedAt: Date.now(),
},
};
@@ -871,7 +871,7 @@ describe("runReplyAgent heartbeat followup guard", () => {
active.updateSessionId("post-compact-session");
sessionStore.main = {
sessionId: "post-compact-session",
sessionFile: "/tmp/post-compact.jsonl",
sessionFile: "main",
updatedAt: Date.now(),
};
active.complete();
@@ -880,7 +880,7 @@ describe("runReplyAgent heartbeat followup guard", () => {
expect(state.runEmbeddedAgentMock).toHaveBeenCalledTimes(1);
const [call] = mockCallArgs(state.runEmbeddedAgentMock, "run embedded agent");
expect((call as AgentRunParams).sessionId).toBe("post-compact-session");
expect((call as AgentRunParams).sessionFile).toBe("/tmp/post-compact.jsonl");
expect((call as AgentRunParams).sessionFile).toBe("main");
});
it("drops runs when reply-lane admission sees an already-aborted caller", async () => {
@@ -1941,7 +1941,13 @@ describe("runReplyAgent pending final delivery capture", () => {
expect(stored.restartRecoveryDeliveryRequestFingerprint).toBeUndefined();
expect(stored.restartRecoveryDeliveryRunId).toBeUndefined();
expect(stored.restartRecoveryDeliverySourceRunId).toBeUndefined();
expect(stored.pendingFinalDelivery).toBe(true);
expect(stored.pendingFinalDelivery).toMatchObject({
kind: "replayable",
text: "visible final",
context: { channel: "webchat" },
intentId: expect.any(String),
createdAt: expect.any(Number),
});
expect(stored.restartRecoverySourceIngress).toBe("control-ui");
expect(stored.restartRecoveryTerminalRunIds).toEqual(["control-ui-run"]);
});
@@ -2602,7 +2608,13 @@ describe("runReplyAgent pending final delivery capture", () => {
expect(state.beforeAgentReplyRunMock).toHaveBeenCalledOnce();
expect(state.runEmbeddedAgentMock).toHaveBeenCalledOnce();
expect(await readStoredMainSession(storePath)).toMatchObject({
pendingFinalDelivery: { kind: "transport-only" },
pendingFinalDelivery: {
kind: "replayable",
text: "model reply",
context: { channel: "discord" },
intentId: expect.any(String),
createdAt: expect.any(Number),
},
restartRecoveryBeforeAgentReplyState: "continue",
restartRecoverySourceIngress: "channel",
});

View File

@@ -301,7 +301,6 @@ describe("claws lifecycle cli e2e", () => {
agent: { id: "workspace-agent" },
workspace: {
bootstrapFiles: {
"SOUL.md": { source: "workspace/SOUL.md" },
"HEARTBEAT.md": { source: "workspace/HEARTBEAT.md" },
},
files: [
@@ -320,6 +319,9 @@ describe("claws lifecycle cli e2e", () => {
type: "module",
},
);
await expect(readFile(join(outputDirectory, "CLAW.md"), "utf8")).resolves.toContain(
"Incident Response",
);
const inspected = await runOpenClaw(["claws", "inspect", outputDirectory, "--json"]);
expect(parseJson(inspected.stdout)).toMatchObject({
valid: true,

View File

@@ -4,6 +4,7 @@ import os from "node:os";
import path from "node:path";
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import { afterEach, beforeEach, vi } from "vitest";
import type { ConfigFileSnapshot, OpenClawConfig } from "../config/types.js";
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
import type { MockFn } from "../test-utils/vitest-mock-fn.js";
import {
@@ -372,14 +373,19 @@ const runLegacyStateMigrations = vi.fn().mockResolvedValue({
const DEFAULT_CONFIG_SNAPSHOT = {
path: "/tmp/openclaw.json",
includedPaths: [],
exists: true,
raw: "{}",
parsed: {},
sourceConfig: {},
resolved: {},
valid: true,
runtimeConfig: {},
config: {},
issues: [],
warnings: [],
legacyIssues: [],
} as const;
} as unknown as ConfigFileSnapshot;
vi.mock("@clack/prompts", () => ({
confirm,
@@ -624,14 +630,18 @@ export function mockDoctorConfigSnapshot(
legacyIssues?: Array<{ path: string; message: string }>;
} = {},
) {
const config = (params.config ?? DEFAULT_CONFIG_SNAPSHOT.config) as OpenClawConfig;
readConfigFileSnapshot.mockResolvedValue({
...DEFAULT_CONFIG_SNAPSHOT,
config: params.config ?? DEFAULT_CONFIG_SNAPSHOT.config,
parsed: params.parsed ?? DEFAULT_CONFIG_SNAPSHOT.parsed,
sourceConfig: config,
resolved: config,
runtimeConfig: config,
config,
valid: params.valid ?? DEFAULT_CONFIG_SNAPSHOT.valid,
issues: params.issues ?? DEFAULT_CONFIG_SNAPSHOT.issues,
legacyIssues: params.legacyIssues ?? DEFAULT_CONFIG_SNAPSHOT.legacyIssues,
});
} as ConfigFileSnapshot);
}
/** Creates a runtime mock that captures doctor command output and exits. */

View File

@@ -59,6 +59,9 @@ vi.mock("./doctor/cron/index.js", () => ({
}));
vi.mock("./doctor/cron/legacy-repair.js", () => ({
collectCronCodexRuntimePolicyTargetsReadOnly: vi
.fn()
.mockResolvedValue({ targets: [], warnings: [] }),
repairLegacyCronStoreWithoutPrompt: vi.fn().mockResolvedValue({ changes: [], warnings: [] }),
}));

View File

@@ -24,6 +24,10 @@ import {
import { resolveAuthProfileOrderWithMetadata } from "../../agents/auth-profiles/order.js";
import { resolveAuthProfileDatabasePath } from "../../agents/auth-profiles/sqlite.js";
import { describeFailoverError } from "../../agents/failover-error.js";
import {
prepareInternalSessionEffectsSession,
removeInternalSessionEffectsSession,
} from "../../agents/internal-session-effects.js";
import {
hasUsableCustomProviderApiKey,
resolveEnvApiKey,
@@ -35,10 +39,7 @@ import { findNormalizedProviderValue, normalizeProviderId } from "../../agents/m
import { loadPreparedModelCatalog } from "../../agents/prepared-model-catalog.js";
import { resolveProviderIdForAuth } from "../../agents/provider-auth-aliases.js";
import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js";
import {
resolveSessionTranscriptPath,
resolveSessionTranscriptsDirForAgent,
} from "../../config/sessions/paths.js";
import { resolveStorePath } from "../../config/sessions/paths.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import {
coerceSecretRef,
@@ -718,12 +719,12 @@ async function probeTarget(params: {
agentId: string;
agentDir: string;
workspaceDir: string;
sessionDir: string;
storePath: string;
target: AuthProbeTarget;
timeoutMs: number;
maxTokens: number;
}): Promise<AuthProbeResult> {
const { cfg, agentId, agentDir, workspaceDir, sessionDir, target, timeoutMs, maxTokens } = params;
const { cfg, agentId, agentDir, workspaceDir, storePath, target, timeoutMs, maxTokens } = params;
// Marker credentials must be resolved by the runtime from config, but the
// "config" probe must reflect only that credential — empty the provider auth
// order and isolate the agent dir so stored profiles cannot satisfy it via
@@ -748,11 +749,10 @@ async function probeTarget(params: {
}
const model = target.model;
const sessionId = `probe-${target.provider}-${crypto.randomUUID()}`;
const sessionFile = resolveSessionTranscriptPath(sessionId, agentId);
await fs.mkdir(sessionDir, { recursive: true });
const runId = `probe-${target.provider}-${crypto.randomUUID()}`;
let isolatedAgentDir: string | null = null;
let isolatedProfileId: string | undefined;
let sessionTarget: Awaited<ReturnType<typeof prepareInternalSessionEffectsSession>> | undefined;
const start = Date.now();
const buildResult = (status: AuthProbeResult["status"], error?: string): AuthProbeResult => ({
@@ -767,6 +767,12 @@ async function probeTarget(params: {
latencyMs: Date.now() - start,
});
try {
sessionTarget = await prepareInternalSessionEffectsSession({
agentId,
cwd: workspaceDir,
runId,
storePath,
});
// Any bound-value target runs in an empty agent dir so stored profiles are
// absent and cannot satisfy the probe via failover. Direct values pin a
// synthetic profile; marker values are resolved by the runtime from the
@@ -806,8 +812,9 @@ async function probeTarget(params: {
}
const { runEmbeddedAgent } = await loadEmbeddedRunnerModule();
await runEmbeddedAgent({
sessionId,
sessionFile,
sessionId: sessionTarget.sessionId,
sessionKey: sessionTarget.sessionKey,
sessionTarget,
agentId,
workspaceDir,
agentDir: isolatedAgentDir ?? agentDir,
@@ -818,7 +825,7 @@ async function probeTarget(params: {
authProfileId: isolatedProfileId ?? target.profileId,
authProfileIdSource: isolatedProfileId || target.profileId ? "user" : undefined,
timeoutMs,
runId: `probe-${crypto.randomUUID()}`,
runId,
lane: `auth-probe:${target.provider}:${target.profileId ?? target.source}`,
thinkLevel: "off",
reasoningLevel: "off",
@@ -836,6 +843,7 @@ async function probeTarget(params: {
redactAuthProbeError(described.message),
);
} finally {
await removeInternalSessionEffectsSession(sessionTarget);
if (isolatedAgentDir) {
clearRuntimeAuthProfileStoreSnapshot(isolatedAgentDir);
disposeOpenClawAgentDatabaseByPath(resolveAuthProfileDatabasePath(isolatedAgentDir));
@@ -864,7 +872,7 @@ async function runTargetsWithConcurrency(params: {
params.workspaceDir ??
resolveAgentWorkspaceDir(cfg, agentId) ??
resolveDefaultAgentWorkspaceDir();
const sessionDir = resolveSessionTranscriptsDirForAgent(agentId);
const storePath = resolveStorePath(cfg.session?.store, { agentId });
await fs.mkdir(workspaceDir, { recursive: true });
@@ -882,7 +890,7 @@ async function runTargetsWithConcurrency(params: {
agentId,
agentDir,
workspaceDir,
sessionDir,
storePath,
target,
timeoutMs,
maxTokens,

View File

@@ -491,6 +491,12 @@ function createCronPromptExecutor(params: {
const result = await runEmbeddedAgent({
sessionId: params.cronSession.sessionEntry.sessionId,
sessionKey: params.runSessionKey,
sessionTarget: {
agentId: params.agentId,
sessionId: params.cronSession.sessionEntry.sessionId,
sessionKey: params.runSessionKey,
storePath: params.cronSession.storePath,
},
promptCacheKey,
agentId: params.agentId,
trigger: "cron",
@@ -502,7 +508,6 @@ function createCronPromptExecutor(params: {
messageTo: params.resolvedDelivery.to,
messageThreadId: params.resolvedDelivery.threadId,
currentChannelId,
sessionFile,
agentDir: params.agentDir,
workspaceDir: params.workspaceDir,
config: params.cfgWithAgentDefaults,

View File

@@ -65,12 +65,26 @@ describe("runCronIsolatedAgentTurn isolated session identity", () => {
const runRequest = requireFirstMockArg(runEmbeddedAgentMock, "runEmbeddedAgentMock") as {
sessionId?: string;
sessionKey?: string;
sessionFile?: string;
sessionTarget?: {
agentId?: string;
sessionId?: string;
sessionKey?: string;
storePath?: string;
};
promptCacheKey?: string;
bootstrapContextMode?: string;
bootstrapContextRunKind?: string;
};
expect(runRequest.sessionId).toBe("isolated-run-1");
expect(runRequest.sessionKey).toBe("agent:default:cron:daily-monitor:run:isolated-run-1");
expect(runRequest.sessionFile).toBeUndefined();
expect(runRequest.sessionTarget).toEqual({
agentId: "default",
sessionId: "isolated-run-1",
sessionKey: "agent:default:cron:daily-monitor:run:isolated-run-1",
storePath: expect.any(String),
});
expect(runRequest.sessionKey).not.toBe("agent:default:cron:daily-monitor");
expect(runRequest.promptCacheKey).toMatch(/^openclaw-cron-[a-f0-9]{32}$/u);
expect(runRequest.promptCacheKey).not.toContain("isolated-run-1");

View File

@@ -162,6 +162,10 @@ export async function createDockerSetupSandbox(): Promise<DockerSetupSandbox> {
join(repoRoot, "scripts", "lib", "docker-e2e-container.sh"),
join(rootDir, "scripts", "lib", "docker-e2e-container.sh"),
);
await copyFile(
join(repoRoot, "scripts", "lib", "docker-e2e-resource-diagnostics.sh"),
join(rootDir, "scripts", "lib", "docker-e2e-resource-diagnostics.sh"),
);
await copyFile(
join(repoRoot, "scripts", "lib", "host-timeout.sh"),
join(rootDir, "scripts", "lib", "host-timeout.sh"),

View File

@@ -500,8 +500,10 @@ describe("SSRF external proxy routing", () => {
expect(child.stdout).toContain('"body":"from loopback target"');
expect(seenConnectTargets).toContain(`127.0.0.1:${wsTargetPort}`);
expect(seenConnectTargets).toContain(`127.0.0.1:${httpsLikeTargetPort}`);
expect(seenConnectTargets).toContain(`127.0.0.1:${targetPort}`);
expect(seenConnectTargets).toContain(`127.0.0.1:${globalFetchTargetPort}`);
expect(seenConnectTargets).toContain(`http://127.0.0.1:${targetPort}/private-metadata`);
expect(seenConnectTargets).toContain(
`http://127.0.0.1:${globalFetchTargetPort}/global-fetch-metadata`,
);
expect(seenConnectTargets).toContain(`http://127.0.0.1:${targetPort}/node-http-metadata`);
expect(seenConnectTargets).toContain(`http://127.0.0.1:${targetPort}/explicit-agent`);
expect(seenConnectTargets).not.toContain(`127.0.0.1:${gatewayBypassWsTargetPort}`);

View File

@@ -205,7 +205,12 @@ describe("Gateway queued session rotation", () => {
enabled: true,
allow: ["queued-rotation-tracer"],
load: { paths: [pluginDir] },
entries: { "queued-rotation-tracer": { enabled: true } },
entries: {
"queued-rotation-tracer": {
enabled: true,
hooks: { allowConversationAccess: true },
},
},
slots: { memory: "none" },
},
agents: {

View File

@@ -305,12 +305,19 @@ export function makeCfg(home: string): OpenClawConfig {
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-7" },
models: {
"anthropic/claude-haiku-4-5-20251001": {},
"anthropic/claude-opus-4-7": {},
"openai/gpt-4.1-mini": {},
"openai/gpt-5.4": {},
},
workspace: join(home, "openclaw"),
// Test harness: avoid 1s coalescer idle sleeps that dominate trigger suites.
blockStreamingCoalesce: { idleMs: 1 },
// Trigger tests assert routing/authorization behavior, not delivery pacing.
humanDelay: { mode: "off" },
},
list: [{ id: "main", default: true }],
},
channels: {
whatsapp: {

View File

@@ -2409,6 +2409,14 @@ describe("ci workflow guards", () => {
const source = readFileSync(workflowPath, "utf8");
expect(source, workflowPath).not.toContain("build-all-cache-scope:");
}
const releaseChecks = parse(
readFileSync(".github/workflows/openclaw-live-and-e2e-checks-reusable.yml", "utf8"),
);
expect(releaseChecks.jobs.validate_repo_e2e.env).toMatchObject({
OPENCLAW_BUILD_PRIVATE_QA: "1",
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1",
});
});
it("persists Node 22 declarations through trusted bounded artifacts", () => {