mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 09:11:34 +00:00
fix(release): align beta 6 e2e fixtures
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -55,7 +55,6 @@ describe("browser client fetch attachOnly diagnostics", () => {
|
||||
hung: {
|
||||
cdpUrl: `http://127.0.0.1:${port}`,
|
||||
attachOnly: true,
|
||||
color: "#00AA00",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 } } } },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -12,6 +12,9 @@ scenario:
|
||||
gatewayConfigPatch:
|
||||
channels:
|
||||
telegram:
|
||||
groups:
|
||||
"*":
|
||||
requireMention: false
|
||||
streaming:
|
||||
mode: partial
|
||||
successCriteria:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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"] }),
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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: [] }),
|
||||
}));
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user