mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 05:11:25 +00:00
test(e2e): stabilize suite
This commit is contained in:
@@ -56,6 +56,23 @@ describe("models-config", () => {
|
|||||||
const previousSynthetic = process.env.SYNTHETIC_API_KEY;
|
const previousSynthetic = process.env.SYNTHETIC_API_KEY;
|
||||||
const previousVenice = process.env.VENICE_API_KEY;
|
const previousVenice = process.env.VENICE_API_KEY;
|
||||||
const previousXiaomi = process.env.XIAOMI_API_KEY;
|
const previousXiaomi = process.env.XIAOMI_API_KEY;
|
||||||
|
const previousOllama = process.env.OLLAMA_API_KEY;
|
||||||
|
const previousVllm = process.env.VLLM_API_KEY;
|
||||||
|
const previousTogether = process.env.TOGETHER_API_KEY;
|
||||||
|
const previousHuggingfaceHub = process.env.HUGGINGFACE_HUB_TOKEN;
|
||||||
|
const previousHuggingfaceHf = process.env.HF_TOKEN;
|
||||||
|
const previousQianfan = process.env.QIANFAN_API_KEY;
|
||||||
|
const previousNvidia = process.env.NVIDIA_API_KEY;
|
||||||
|
const previousAwsAccessKeyId = process.env.AWS_ACCESS_KEY_ID;
|
||||||
|
const previousAwsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
|
||||||
|
const previousAwsSessionToken = process.env.AWS_SESSION_TOKEN;
|
||||||
|
const previousAwsProfile = process.env.AWS_PROFILE;
|
||||||
|
const previousAwsRegion = process.env.AWS_REGION;
|
||||||
|
const previousAwsDefaultRegion = process.env.AWS_DEFAULT_REGION;
|
||||||
|
const previousAwsSharedCredentials = process.env.AWS_SHARED_CREDENTIALS_FILE;
|
||||||
|
const previousAwsConfigFile = process.env.AWS_CONFIG_FILE;
|
||||||
|
const previousAgentDir = process.env.OPENCLAW_AGENT_DIR;
|
||||||
|
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||||
delete process.env.COPILOT_GITHUB_TOKEN;
|
delete process.env.COPILOT_GITHUB_TOKEN;
|
||||||
delete process.env.GH_TOKEN;
|
delete process.env.GH_TOKEN;
|
||||||
delete process.env.GITHUB_TOKEN;
|
delete process.env.GITHUB_TOKEN;
|
||||||
@@ -65,9 +82,29 @@ describe("models-config", () => {
|
|||||||
delete process.env.SYNTHETIC_API_KEY;
|
delete process.env.SYNTHETIC_API_KEY;
|
||||||
delete process.env.VENICE_API_KEY;
|
delete process.env.VENICE_API_KEY;
|
||||||
delete process.env.XIAOMI_API_KEY;
|
delete process.env.XIAOMI_API_KEY;
|
||||||
|
delete process.env.OLLAMA_API_KEY;
|
||||||
|
delete process.env.VLLM_API_KEY;
|
||||||
|
delete process.env.TOGETHER_API_KEY;
|
||||||
|
delete process.env.HUGGINGFACE_HUB_TOKEN;
|
||||||
|
delete process.env.HF_TOKEN;
|
||||||
|
delete process.env.QIANFAN_API_KEY;
|
||||||
|
delete process.env.NVIDIA_API_KEY;
|
||||||
|
delete process.env.AWS_ACCESS_KEY_ID;
|
||||||
|
delete process.env.AWS_SECRET_ACCESS_KEY;
|
||||||
|
delete process.env.AWS_SESSION_TOKEN;
|
||||||
|
delete process.env.AWS_PROFILE;
|
||||||
|
delete process.env.AWS_REGION;
|
||||||
|
delete process.env.AWS_DEFAULT_REGION;
|
||||||
|
delete process.env.AWS_SHARED_CREDENTIALS_FILE;
|
||||||
|
delete process.env.AWS_CONFIG_FILE;
|
||||||
|
delete process.env.OPENCLAW_AGENT_DIR;
|
||||||
|
delete process.env.PI_CODING_AGENT_DIR;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const agentDir = path.join(home, "agent-empty");
|
const agentDir = path.join(home, "agent-empty");
|
||||||
|
// Avoid merging in the user's real main auth store via OPENCLAW_AGENT_DIR.
|
||||||
|
process.env.OPENCLAW_AGENT_DIR = agentDir;
|
||||||
|
process.env.PI_CODING_AGENT_DIR = agentDir;
|
||||||
const result = await ensureOpenClawModelsJson(
|
const result = await ensureOpenClawModelsJson(
|
||||||
{
|
{
|
||||||
models: { providers: {} },
|
models: { providers: {} },
|
||||||
@@ -123,6 +160,91 @@ describe("models-config", () => {
|
|||||||
} else {
|
} else {
|
||||||
process.env.XIAOMI_API_KEY = previousXiaomi;
|
process.env.XIAOMI_API_KEY = previousXiaomi;
|
||||||
}
|
}
|
||||||
|
if (previousOllama === undefined) {
|
||||||
|
delete process.env.OLLAMA_API_KEY;
|
||||||
|
} else {
|
||||||
|
process.env.OLLAMA_API_KEY = previousOllama;
|
||||||
|
}
|
||||||
|
if (previousVllm === undefined) {
|
||||||
|
delete process.env.VLLM_API_KEY;
|
||||||
|
} else {
|
||||||
|
process.env.VLLM_API_KEY = previousVllm;
|
||||||
|
}
|
||||||
|
if (previousTogether === undefined) {
|
||||||
|
delete process.env.TOGETHER_API_KEY;
|
||||||
|
} else {
|
||||||
|
process.env.TOGETHER_API_KEY = previousTogether;
|
||||||
|
}
|
||||||
|
if (previousHuggingfaceHub === undefined) {
|
||||||
|
delete process.env.HUGGINGFACE_HUB_TOKEN;
|
||||||
|
} else {
|
||||||
|
process.env.HUGGINGFACE_HUB_TOKEN = previousHuggingfaceHub;
|
||||||
|
}
|
||||||
|
if (previousHuggingfaceHf === undefined) {
|
||||||
|
delete process.env.HF_TOKEN;
|
||||||
|
} else {
|
||||||
|
process.env.HF_TOKEN = previousHuggingfaceHf;
|
||||||
|
}
|
||||||
|
if (previousQianfan === undefined) {
|
||||||
|
delete process.env.QIANFAN_API_KEY;
|
||||||
|
} else {
|
||||||
|
process.env.QIANFAN_API_KEY = previousQianfan;
|
||||||
|
}
|
||||||
|
if (previousNvidia === undefined) {
|
||||||
|
delete process.env.NVIDIA_API_KEY;
|
||||||
|
} else {
|
||||||
|
process.env.NVIDIA_API_KEY = previousNvidia;
|
||||||
|
}
|
||||||
|
if (previousAwsAccessKeyId === undefined) {
|
||||||
|
delete process.env.AWS_ACCESS_KEY_ID;
|
||||||
|
} else {
|
||||||
|
process.env.AWS_ACCESS_KEY_ID = previousAwsAccessKeyId;
|
||||||
|
}
|
||||||
|
if (previousAwsSecretAccessKey === undefined) {
|
||||||
|
delete process.env.AWS_SECRET_ACCESS_KEY;
|
||||||
|
} else {
|
||||||
|
process.env.AWS_SECRET_ACCESS_KEY = previousAwsSecretAccessKey;
|
||||||
|
}
|
||||||
|
if (previousAwsSessionToken === undefined) {
|
||||||
|
delete process.env.AWS_SESSION_TOKEN;
|
||||||
|
} else {
|
||||||
|
process.env.AWS_SESSION_TOKEN = previousAwsSessionToken;
|
||||||
|
}
|
||||||
|
if (previousAwsProfile === undefined) {
|
||||||
|
delete process.env.AWS_PROFILE;
|
||||||
|
} else {
|
||||||
|
process.env.AWS_PROFILE = previousAwsProfile;
|
||||||
|
}
|
||||||
|
if (previousAwsRegion === undefined) {
|
||||||
|
delete process.env.AWS_REGION;
|
||||||
|
} else {
|
||||||
|
process.env.AWS_REGION = previousAwsRegion;
|
||||||
|
}
|
||||||
|
if (previousAwsDefaultRegion === undefined) {
|
||||||
|
delete process.env.AWS_DEFAULT_REGION;
|
||||||
|
} else {
|
||||||
|
process.env.AWS_DEFAULT_REGION = previousAwsDefaultRegion;
|
||||||
|
}
|
||||||
|
if (previousAwsSharedCredentials === undefined) {
|
||||||
|
delete process.env.AWS_SHARED_CREDENTIALS_FILE;
|
||||||
|
} else {
|
||||||
|
process.env.AWS_SHARED_CREDENTIALS_FILE = previousAwsSharedCredentials;
|
||||||
|
}
|
||||||
|
if (previousAwsConfigFile === undefined) {
|
||||||
|
delete process.env.AWS_CONFIG_FILE;
|
||||||
|
} else {
|
||||||
|
process.env.AWS_CONFIG_FILE = previousAwsConfigFile;
|
||||||
|
}
|
||||||
|
if (previousAgentDir === undefined) {
|
||||||
|
delete process.env.OPENCLAW_AGENT_DIR;
|
||||||
|
} else {
|
||||||
|
process.env.OPENCLAW_AGENT_DIR = previousAgentDir;
|
||||||
|
}
|
||||||
|
if (previousPiAgentDir === undefined) {
|
||||||
|
delete process.env.PI_CODING_AGENT_DIR;
|
||||||
|
} else {
|
||||||
|
process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -158,7 +280,7 @@ describe("models-config", () => {
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
expect(parsed.providers.minimax?.baseUrl).toBe("https://api.minimax.chat/v1");
|
expect(parsed.providers.minimax?.baseUrl).toBe("https://api.minimax.io/anthropic");
|
||||||
expect(parsed.providers.minimax?.apiKey).toBe("MINIMAX_API_KEY");
|
expect(parsed.providers.minimax?.apiKey).toBe("MINIMAX_API_KEY");
|
||||||
const ids = parsed.providers.minimax?.models?.map((model) => model.id);
|
const ids = parsed.providers.minimax?.models?.map((model) => model.id);
|
||||||
expect(ids).toContain("MiniMax-M2.1");
|
expect(ids).toContain("MiniMax-M2.1");
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ vi.mock("./logger.js", () => ({
|
|||||||
info: vi.fn(),
|
info: vi.fn(),
|
||||||
warn: vi.fn(),
|
warn: vi.fn(),
|
||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
|
isEnabled: vi.fn(() => false),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -586,7 +586,7 @@ describe("Agent-specific tool filtering", () => {
|
|||||||
const helperResult = await helperExecTool!.execute("call-helper", {
|
const helperResult = await helperExecTool!.execute("call-helper", {
|
||||||
command: "echo done",
|
command: "echo done",
|
||||||
host: "sandbox",
|
host: "sandbox",
|
||||||
yieldMs: 10,
|
yieldMs: 1000,
|
||||||
});
|
});
|
||||||
expect(helperResult?.details.status).toBe("completed");
|
expect(helperResult?.details.status).toBe("completed");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ describe("loadWorkspaceSkillEntries", () => {
|
|||||||
),
|
),
|
||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
|
await fs.writeFile(path.join(pluginRoot, "index.ts"), "export {};\n", "utf-8");
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(pluginRoot, "skills", "prose", "SKILL.md"),
|
path.join(pluginRoot, "skills", "prose", "SKILL.md"),
|
||||||
`---\nname: prose\ndescription: test\n---\n`,
|
`---\nname: prose\ndescription: test\n---\n`,
|
||||||
@@ -99,6 +100,7 @@ describe("loadWorkspaceSkillEntries", () => {
|
|||||||
),
|
),
|
||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
|
await fs.writeFile(path.join(pluginRoot, "index.ts"), "export {};\n", "utf-8");
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(pluginRoot, "skills", "prose", "SKILL.md"),
|
path.join(pluginRoot, "skills", "prose", "SKILL.md"),
|
||||||
`---\nname: prose\ndescription: test\n---\n`,
|
`---\nname: prose\ndescription: test\n---\n`,
|
||||||
|
|||||||
@@ -44,6 +44,18 @@ type AnnounceQueueState = {
|
|||||||
|
|
||||||
const ANNOUNCE_QUEUES = new Map<string, AnnounceQueueState>();
|
const ANNOUNCE_QUEUES = new Map<string, AnnounceQueueState>();
|
||||||
|
|
||||||
|
export function resetAnnounceQueuesForTests() {
|
||||||
|
// Test isolation: other suites may leave a draining queue behind in the worker.
|
||||||
|
// Clearing the map alone isn't enough because drain loops capture `queue` by reference.
|
||||||
|
for (const queue of ANNOUNCE_QUEUES.values()) {
|
||||||
|
queue.items.length = 0;
|
||||||
|
queue.summaryLines.length = 0;
|
||||||
|
queue.droppedCount = 0;
|
||||||
|
queue.lastEnqueuedAt = 0;
|
||||||
|
}
|
||||||
|
ANNOUNCE_QUEUES.clear();
|
||||||
|
}
|
||||||
|
|
||||||
function getAnnounceQueue(
|
function getAnnounceQueue(
|
||||||
key: string,
|
key: string,
|
||||||
settings: AnnounceQueueSettings,
|
settings: AnnounceQueueSettings,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { loadConfig } from "../config/config.js";
|
|||||||
import { callGateway } from "../gateway/call.js";
|
import { callGateway } from "../gateway/call.js";
|
||||||
import { onAgentEvent } from "../infra/agent-events.js";
|
import { onAgentEvent } from "../infra/agent-events.js";
|
||||||
import { type DeliveryContext, normalizeDeliveryContext } from "../utils/delivery-context.js";
|
import { type DeliveryContext, normalizeDeliveryContext } from "../utils/delivery-context.js";
|
||||||
|
import { resetAnnounceQueuesForTests } from "./subagent-announce-queue.js";
|
||||||
import { runSubagentAnnounceFlow, type SubagentRunOutcome } from "./subagent-announce.js";
|
import { runSubagentAnnounceFlow, type SubagentRunOutcome } from "./subagent-announce.js";
|
||||||
import {
|
import {
|
||||||
loadSubagentRegistryFromDisk,
|
loadSubagentRegistryFromDisk,
|
||||||
@@ -398,6 +399,7 @@ async function waitForSubagentCompletion(runId: string, waitTimeoutMs: number) {
|
|||||||
export function resetSubagentRegistryForTests(opts?: { persist?: boolean }) {
|
export function resetSubagentRegistryForTests(opts?: { persist?: boolean }) {
|
||||||
subagentRuns.clear();
|
subagentRuns.clear();
|
||||||
resumedRuns.clear();
|
resumedRuns.clear();
|
||||||
|
resetAnnounceQueuesForTests();
|
||||||
stopSweeper();
|
stopSweeper();
|
||||||
restoreAttempted = false;
|
restoreAttempted = false;
|
||||||
if (listenerStop) {
|
if (listenerStop) {
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ describe("image tool MiniMax VLM routing", () => {
|
|||||||
|
|
||||||
expect(fetch).toHaveBeenCalledTimes(1);
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
const [url, init] = fetch.mock.calls[0];
|
const [url, init] = fetch.mock.calls[0];
|
||||||
expect(String(url)).toBe("https://api.minimax.chat/v1/coding_plan/vlm");
|
expect(String(url)).toBe("https://api.minimax.io/v1/coding_plan/vlm");
|
||||||
expect(init?.method).toBe("POST");
|
expect(init?.method).toBe("POST");
|
||||||
expect(String((init?.headers as Record<string, string>)?.Authorization)).toBe(
|
expect(String((init?.headers as Record<string, string>)?.Authorization)).toBe(
|
||||||
"Bearer minimax-test",
|
"Bearer minimax-test",
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ describe("gateway-cli coverage", () => {
|
|||||||
expect(out).toContain("- Studio openclaw.internal.");
|
expect(out).toContain("- Studio openclaw.internal.");
|
||||||
expect(out).toContain(" tailnet: studio.tailnet.ts.net");
|
expect(out).toContain(" tailnet: studio.tailnet.ts.net");
|
||||||
expect(out).toContain(" host: studio.openclaw.internal");
|
expect(out).toContain(" host: studio.openclaw.internal");
|
||||||
expect(out).toContain(" ws: ws://studio.tailnet.ts.net:18789");
|
expect(out).toContain(" ws: ws://studio.openclaw.internal:18789");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("validates gateway discover timeout", async () => {
|
it("validates gateway discover timeout", async () => {
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ describe("applyMinimaxApiConfig", () => {
|
|||||||
expect(cfg.models?.providers?.minimax?.apiKey).toBe("old-key");
|
expect(cfg.models?.providers?.minimax?.apiKey).toBe("old-key");
|
||||||
expect(cfg.models?.providers?.minimax?.models.map((m) => m.id)).toEqual([
|
expect(cfg.models?.providers?.minimax?.models.map((m) => m.id)).toEqual([
|
||||||
"old-model",
|
"old-model",
|
||||||
"MiniMax-M2.1",
|
"MiniMax-M2.5",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -448,7 +448,7 @@ describe("gateway server agent", () => {
|
|||||||
|
|
||||||
const spy = vi.mocked(agentCommand);
|
const spy = vi.mocked(agentCommand);
|
||||||
const call = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
const call = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||||
expect(call.sessionKey).toBe("main");
|
expect(call.sessionKey).toBe("agent:main:main");
|
||||||
expectChannels(call, "webchat");
|
expectChannels(call, "webchat");
|
||||||
expect(typeof call.message).toBe("string");
|
expect(typeof call.message).toBe("string");
|
||||||
expect(call.message).toContain("what is in the image?");
|
expect(call.message).toContain("what is in the image?");
|
||||||
|
|||||||
@@ -117,6 +117,18 @@ describe("gateway server auth/connect", () => {
|
|||||||
ws.close();
|
ws.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("ignores requested scopes when device identity is omitted", async () => {
|
||||||
|
const ws = await openWs(port);
|
||||||
|
const res = await connectReq(ws, { device: null });
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
|
||||||
|
const health = await rpcReq(ws, "health");
|
||||||
|
expect(health.ok).toBe(false);
|
||||||
|
expect(health.error?.message).toContain("missing scope");
|
||||||
|
|
||||||
|
ws.close();
|
||||||
|
});
|
||||||
|
|
||||||
test("does not grant admin when scopes are omitted", async () => {
|
test("does not grant admin when scopes are omitted", async () => {
|
||||||
const ws = await openWs(port);
|
const ws = await openWs(port);
|
||||||
const token =
|
const token =
|
||||||
@@ -144,18 +156,6 @@ describe("gateway server auth/connect", () => {
|
|||||||
signedAtMs,
|
signedAtMs,
|
||||||
token: token ?? null,
|
token: token ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ignores requested scopes when device identity is omitted", async () => {
|
|
||||||
const ws = await openWs(port);
|
|
||||||
const res = await connectReq(ws, { device: null });
|
|
||||||
expect(res.ok).toBe(true);
|
|
||||||
|
|
||||||
const health = await rpcReq(ws, "health");
|
|
||||||
expect(health.ok).toBe(false);
|
|
||||||
expect(health.error?.message).toContain("missing scope");
|
|
||||||
|
|
||||||
ws.close();
|
|
||||||
});
|
|
||||||
const device = {
|
const device = {
|
||||||
id: identity.deviceId,
|
id: identity.deviceId,
|
||||||
publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem),
|
publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem),
|
||||||
|
|||||||
@@ -403,8 +403,7 @@ describe("gateway server misc", () => {
|
|||||||
const plugins = updated.plugins as Record<string, unknown> | undefined;
|
const plugins = updated.plugins as Record<string, unknown> | undefined;
|
||||||
const entries = plugins?.entries as Record<string, unknown> | undefined;
|
const entries = plugins?.entries as Record<string, unknown> | undefined;
|
||||||
const discord = entries?.discord as Record<string, unknown> | undefined;
|
const discord = entries?.discord as Record<string, unknown> | undefined;
|
||||||
// Auto-enable registers the plugin entry but keeps it disabled for explicit opt-in.
|
expect(discord?.enabled).toBe(true);
|
||||||
expect(discord?.enabled).toBe(false);
|
|
||||||
expect((updated.channels as Record<string, unknown> | undefined)?.discord).toMatchObject({
|
expect((updated.channels as Record<string, unknown> | undefined)?.discord).toMatchObject({
|
||||||
token: "token-123",
|
token: "token-123",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -170,12 +170,15 @@ installGatewayTestHooks({ scope: "suite" });
|
|||||||
describe("gateway hot reload", () => {
|
describe("gateway hot reload", () => {
|
||||||
let prevSkipChannels: string | undefined;
|
let prevSkipChannels: string | undefined;
|
||||||
let prevSkipGmail: string | undefined;
|
let prevSkipGmail: string | undefined;
|
||||||
|
let prevSkipProviders: string | undefined;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
prevSkipChannels = process.env.OPENCLAW_SKIP_CHANNELS;
|
prevSkipChannels = process.env.OPENCLAW_SKIP_CHANNELS;
|
||||||
prevSkipGmail = process.env.OPENCLAW_SKIP_GMAIL_WATCHER;
|
prevSkipGmail = process.env.OPENCLAW_SKIP_GMAIL_WATCHER;
|
||||||
|
prevSkipProviders = process.env.OPENCLAW_SKIP_PROVIDERS;
|
||||||
process.env.OPENCLAW_SKIP_CHANNELS = "0";
|
process.env.OPENCLAW_SKIP_CHANNELS = "0";
|
||||||
delete process.env.OPENCLAW_SKIP_GMAIL_WATCHER;
|
delete process.env.OPENCLAW_SKIP_GMAIL_WATCHER;
|
||||||
|
delete process.env.OPENCLAW_SKIP_PROVIDERS;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -189,6 +192,11 @@ describe("gateway hot reload", () => {
|
|||||||
} else {
|
} else {
|
||||||
process.env.OPENCLAW_SKIP_GMAIL_WATCHER = prevSkipGmail;
|
process.env.OPENCLAW_SKIP_GMAIL_WATCHER = prevSkipGmail;
|
||||||
}
|
}
|
||||||
|
if (prevSkipProviders === undefined) {
|
||||||
|
delete process.env.OPENCLAW_SKIP_PROVIDERS;
|
||||||
|
} else {
|
||||||
|
process.env.OPENCLAW_SKIP_PROVIDERS = prevSkipProviders;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies hot reload actions and emits restart signal", async () => {
|
it("applies hot reload actions and emits restart signal", async () => {
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ describe("gateway update.run", () => {
|
|||||||
process.on("SIGUSR1", sigusr1);
|
process.on("SIGUSR1", sigusr1);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true });
|
||||||
await fs.writeFile(CONFIG_PATH, JSON.stringify({ update: { channel: "beta" } }, null, 2));
|
await fs.writeFile(CONFIG_PATH, JSON.stringify({ update: { channel: "beta" } }, null, 2));
|
||||||
const updateMock = vi.mocked(runGatewayUpdate);
|
const updateMock = vi.mocked(runGatewayUpdate);
|
||||||
updateMock.mockClear();
|
updateMock.mockClear();
|
||||||
|
|||||||
@@ -70,75 +70,82 @@ describe("web auto-reply", () => {
|
|||||||
});
|
});
|
||||||
it("forces reconnect when watchdog closes without onClose", async () => {
|
it("forces reconnect when watchdog closes without onClose", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const sleep = vi.fn(async () => {});
|
try {
|
||||||
const closeResolvers: Array<(reason: unknown) => void> = [];
|
const sleep = vi.fn(async () => {});
|
||||||
let capturedOnMessage:
|
const closeResolvers: Array<(reason: unknown) => void> = [];
|
||||||
| ((msg: import("./inbound.js").WebInboundMessage) => Promise<void>)
|
let capturedOnMessage:
|
||||||
| undefined;
|
| ((msg: import("./inbound.js").WebInboundMessage) => Promise<void>)
|
||||||
const listenerFactory = vi.fn(
|
| undefined;
|
||||||
async (opts: {
|
const listenerFactory = vi.fn(
|
||||||
onMessage: (msg: import("./inbound.js").WebInboundMessage) => Promise<void>;
|
async (opts: {
|
||||||
}) => {
|
onMessage: (msg: import("./inbound.js").WebInboundMessage) => Promise<void>;
|
||||||
capturedOnMessage = opts.onMessage;
|
}) => {
|
||||||
let resolveClose: (reason: unknown) => void = () => {};
|
capturedOnMessage = opts.onMessage;
|
||||||
const onClose = new Promise<unknown>((res) => {
|
let resolveClose: (reason: unknown) => void = () => {};
|
||||||
resolveClose = res;
|
const onClose = new Promise<unknown>((res) => {
|
||||||
closeResolvers.push(res);
|
resolveClose = res;
|
||||||
});
|
closeResolvers.push(res);
|
||||||
return {
|
});
|
||||||
close: vi.fn(),
|
return {
|
||||||
onClose,
|
close: vi.fn(),
|
||||||
signalClose: (reason?: unknown) => resolveClose(reason),
|
onClose,
|
||||||
};
|
signalClose: (reason?: unknown) => resolveClose(reason),
|
||||||
},
|
};
|
||||||
);
|
},
|
||||||
const runtime = {
|
);
|
||||||
log: vi.fn(),
|
const runtime = {
|
||||||
error: vi.fn(),
|
log: vi.fn(),
|
||||||
exit: vi.fn(),
|
error: vi.fn(),
|
||||||
};
|
exit: vi.fn(),
|
||||||
const controller = new AbortController();
|
};
|
||||||
const run = monitorWebChannel(
|
const controller = new AbortController();
|
||||||
false,
|
const run = monitorWebChannel(
|
||||||
listenerFactory,
|
false,
|
||||||
true,
|
listenerFactory,
|
||||||
async () => ({ text: "ok" }),
|
true,
|
||||||
runtime as never,
|
async () => ({ text: "ok" }),
|
||||||
controller.signal,
|
runtime as never,
|
||||||
{
|
controller.signal,
|
||||||
heartbeatSeconds: 1,
|
{
|
||||||
reconnect: { initialMs: 10, maxMs: 10, maxAttempts: 3, factor: 1.1 },
|
heartbeatSeconds: 1,
|
||||||
sleep,
|
reconnect: { initialMs: 10, maxMs: 10, maxAttempts: 3, factor: 1.1 },
|
||||||
},
|
sleep,
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
expect(listenerFactory).toHaveBeenCalledTimes(1);
|
expect(listenerFactory).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
const reply = vi.fn().mockResolvedValue(undefined);
|
const reply = vi.fn().mockResolvedValue(undefined);
|
||||||
const sendComposing = vi.fn();
|
const sendComposing = vi.fn();
|
||||||
const sendMedia = vi.fn();
|
const sendMedia = vi.fn();
|
||||||
await capturedOnMessage?.({
|
|
||||||
body: "hi",
|
|
||||||
from: "+1",
|
|
||||||
to: "+2",
|
|
||||||
id: "m1",
|
|
||||||
sendComposing,
|
|
||||||
reply,
|
|
||||||
sendMedia,
|
|
||||||
});
|
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(31 * 60 * 1000);
|
// The watchdog only needs `lastMessageAt` to be set. Don't await full message
|
||||||
await Promise.resolve();
|
// processing here since it can schedule timers and become flaky under load.
|
||||||
|
void capturedOnMessage?.({
|
||||||
|
body: "hi",
|
||||||
|
from: "+1",
|
||||||
|
to: "+2",
|
||||||
|
id: "m1",
|
||||||
|
sendComposing,
|
||||||
|
reply,
|
||||||
|
sendMedia,
|
||||||
|
});
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(1);
|
await vi.advanceTimersByTimeAsync(31 * 60 * 1000);
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
expect(listenerFactory).toHaveBeenCalledTimes(2);
|
|
||||||
|
|
||||||
controller.abort();
|
await vi.advanceTimersByTimeAsync(1);
|
||||||
closeResolvers[1]?.({ status: 499, isLoggedOut: false });
|
await Promise.resolve();
|
||||||
await Promise.resolve();
|
expect(listenerFactory).toHaveBeenCalledTimes(2);
|
||||||
await run;
|
|
||||||
|
controller.abort();
|
||||||
|
closeResolvers[1]?.({ status: 499, isLoggedOut: false });
|
||||||
|
await Promise.resolve();
|
||||||
|
await run;
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
}, 15_000);
|
}, 15_000);
|
||||||
|
|
||||||
it("stops after hitting max reconnect attempts", { timeout: 60_000 }, async () => {
|
it("stops after hitting max reconnect attempts", { timeout: 60_000 }, async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user