diff --git a/CHANGELOG.md b/CHANGELOG.md
index 95f9331b4b..4f10d31e09 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
- Memory: set Voyage embeddings `input_type` for improved retrieval. (#10818) Thanks @mcinteerj.
- Memory/QMD: run boot refresh in background by default, add configurable QMD maintenance timeouts, and retry QMD after fallback failures. (#9690, #9705)
- Media understanding: recognize `.caf` audio attachments for transcription. (#10982) Thanks @succ985.
+- Tests: harden flaky hotspots by removing timer sleeps, consolidating onboarding provider-auth coverage, and improving memory test realism. (#11598) Thanks @gumadeiras.
## 2026.2.6
diff --git a/extensions/memory-lancedb/index.test.ts b/extensions/memory-lancedb/index.test.ts
index 5d10d9bbac..d51eb66ad7 100644
--- a/extensions/memory-lancedb/index.test.ts
+++ b/extensions/memory-lancedb/index.test.ts
@@ -92,70 +92,28 @@ describe("memory plugin e2e", () => {
}).toThrow("embedding.apiKey is required");
});
- test("shouldCapture filters correctly", async () => {
- // Test the capture filtering logic by checking the rules
- const triggers = [
- { text: "I prefer dark mode", shouldMatch: true },
- { text: "Remember that my name is John", shouldMatch: true },
- { text: "My email is test@example.com", shouldMatch: true },
- { text: "Call me at +1234567890123", shouldMatch: true },
- { text: "We decided to use TypeScript", shouldMatch: true },
- { text: "I always want verbose output", shouldMatch: true },
- { text: "Just a random short message", shouldMatch: false },
- { text: "x", shouldMatch: false }, // Too short
- { text: "injected", shouldMatch: false }, // Skip injected
- ];
+ test("shouldCapture applies real capture rules", async () => {
+ const { shouldCapture } = await import("./index.js");
- // The shouldCapture function is internal, but we can test via the capture behavior
- // For now, just verify the patterns we expect to match
- for (const { text, shouldMatch } of triggers) {
- const hasPreference = /prefer|radši|like|love|hate|want/i.test(text);
- const hasRemember = /zapamatuj|pamatuj|remember/i.test(text);
- const hasEmail = /[\w.-]+@[\w.-]+\.\w+/.test(text);
- const hasPhone = /\+\d{10,}/.test(text);
- const hasDecision = /rozhodli|decided|will use|budeme/i.test(text);
- const hasAlways = /always|never|important/i.test(text);
- const isInjected = text.includes("");
- const isTooShort = text.length < 10;
-
- const wouldCapture =
- !isTooShort &&
- !isInjected &&
- (hasPreference || hasRemember || hasEmail || hasPhone || hasDecision || hasAlways);
-
- if (shouldMatch) {
- expect(wouldCapture).toBe(true);
- }
- }
+ expect(shouldCapture("I prefer dark mode")).toBe(true);
+ expect(shouldCapture("Remember that my name is John")).toBe(true);
+ expect(shouldCapture("My email is test@example.com")).toBe(true);
+ expect(shouldCapture("Call me at +1234567890123")).toBe(true);
+ expect(shouldCapture("I always want verbose output")).toBe(true);
+ expect(shouldCapture("x")).toBe(false);
+ expect(shouldCapture("injected")).toBe(false);
+ expect(shouldCapture("status")).toBe(false);
+ expect(shouldCapture("Here is a short **summary**\n- bullet")).toBe(false);
});
- test("detectCategory classifies correctly", async () => {
- // Test category detection patterns
- const cases = [
- { text: "I prefer dark mode", expected: "preference" },
- { text: "We decided to use React", expected: "decision" },
- { text: "My email is test@example.com", expected: "entity" },
- { text: "The server is running on port 3000", expected: "fact" },
- ];
+ test("detectCategory classifies using production logic", async () => {
+ const { detectCategory } = await import("./index.js");
- for (const { text, expected } of cases) {
- const lower = text.toLowerCase();
- let category: string;
-
- if (/prefer|radši|like|love|hate|want/i.test(lower)) {
- category = "preference";
- } else if (/rozhodli|decided|will use|budeme/i.test(lower)) {
- category = "decision";
- } else if (/\+\d{10,}|@[\w.-]+\.\w+|is called|jmenuje se/i.test(lower)) {
- category = "entity";
- } else if (/is|are|has|have|je|má|jsou/i.test(lower)) {
- category = "fact";
- } else {
- category = "other";
- }
-
- expect(category).toBe(expected);
- }
+ expect(detectCategory("I prefer dark mode")).toBe("preference");
+ expect(detectCategory("We decided to use React")).toBe("decision");
+ expect(detectCategory("My email is test@example.com")).toBe("entity");
+ expect(detectCategory("The server is running on port 3000")).toBe("fact");
+ expect(detectCategory("Random note")).toBe("other");
});
});
diff --git a/extensions/memory-lancedb/index.ts b/extensions/memory-lancedb/index.ts
index ce26e33e91..9ee2e39077 100644
--- a/extensions/memory-lancedb/index.ts
+++ b/extensions/memory-lancedb/index.ts
@@ -195,7 +195,7 @@ const MEMORY_TRIGGERS = [
/always|never|important/i,
];
-function shouldCapture(text: string): boolean {
+export function shouldCapture(text: string): boolean {
if (text.length < 10 || text.length > 500) {
return false;
}
@@ -219,7 +219,7 @@ function shouldCapture(text: string): boolean {
return MEMORY_TRIGGERS.some((r) => r.test(text));
}
-function detectCategory(text: string): MemoryCategory {
+export function detectCategory(text: string): MemoryCategory {
const lower = text.toLowerCase();
if (/prefer|radši|like|love|hate|want/i.test(lower)) {
return "preference";
diff --git a/src/commands/onboard-non-interactive.ai-gateway.test.ts b/src/commands/onboard-non-interactive.ai-gateway.test.ts
deleted file mode 100644
index 0b02632a51..0000000000
--- a/src/commands/onboard-non-interactive.ai-gateway.test.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import fs from "node:fs/promises";
-import os from "node:os";
-import path from "node:path";
-import { describe, expect, it, vi } from "vitest";
-
-describe("onboard (non-interactive): Vercel AI Gateway", () => {
- it("stores the API key and configures the default model", async () => {
- const prev = {
- home: process.env.HOME,
- stateDir: process.env.OPENCLAW_STATE_DIR,
- configPath: process.env.OPENCLAW_CONFIG_PATH,
- skipChannels: process.env.OPENCLAW_SKIP_CHANNELS,
- skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER,
- skipCron: process.env.OPENCLAW_SKIP_CRON,
- skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST,
- token: process.env.OPENCLAW_GATEWAY_TOKEN,
- password: process.env.OPENCLAW_GATEWAY_PASSWORD,
- };
-
- process.env.OPENCLAW_SKIP_CHANNELS = "1";
- process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1";
- process.env.OPENCLAW_SKIP_CRON = "1";
- process.env.OPENCLAW_SKIP_CANVAS_HOST = "1";
- delete process.env.OPENCLAW_GATEWAY_TOKEN;
- delete process.env.OPENCLAW_GATEWAY_PASSWORD;
-
- const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-onboard-gateway-"));
- process.env.HOME = tempHome;
- process.env.OPENCLAW_STATE_DIR = tempHome;
- process.env.OPENCLAW_CONFIG_PATH = path.join(tempHome, "openclaw.json");
- vi.resetModules();
-
- const runtime = {
- log: () => {},
- error: (msg: string) => {
- throw new Error(msg);
- },
- exit: (code: number) => {
- throw new Error(`exit:${code}`);
- },
- };
-
- try {
- const { runNonInteractiveOnboarding } = await import("./onboard-non-interactive.js");
- await runNonInteractiveOnboarding(
- {
- nonInteractive: true,
- authChoice: "ai-gateway-api-key",
- aiGatewayApiKey: "gateway-test-key",
- skipHealth: true,
- skipChannels: true,
- skipSkills: true,
- json: true,
- },
- runtime,
- );
-
- const { CONFIG_PATH } = await import("../config/config.js");
- const cfg = JSON.parse(await fs.readFile(CONFIG_PATH, "utf8")) as {
- auth?: {
- profiles?: Record;
- };
- agents?: { defaults?: { model?: { primary?: string } } };
- };
-
- expect(cfg.auth?.profiles?.["vercel-ai-gateway:default"]?.provider).toBe("vercel-ai-gateway");
- expect(cfg.auth?.profiles?.["vercel-ai-gateway:default"]?.mode).toBe("api_key");
- expect(cfg.agents?.defaults?.model?.primary).toBe(
- "vercel-ai-gateway/anthropic/claude-opus-4.6",
- );
-
- const { ensureAuthProfileStore } = await import("../agents/auth-profiles.js");
- const store = ensureAuthProfileStore();
- const profile = store.profiles["vercel-ai-gateway:default"];
- expect(profile?.type).toBe("api_key");
- if (profile?.type === "api_key") {
- expect(profile.provider).toBe("vercel-ai-gateway");
- expect(profile.key).toBe("gateway-test-key");
- }
- } finally {
- await fs.rm(tempHome, { recursive: true, force: true });
- process.env.HOME = prev.home;
- process.env.OPENCLAW_STATE_DIR = prev.stateDir;
- process.env.OPENCLAW_CONFIG_PATH = prev.configPath;
- process.env.OPENCLAW_SKIP_CHANNELS = prev.skipChannels;
- process.env.OPENCLAW_SKIP_GMAIL_WATCHER = prev.skipGmail;
- process.env.OPENCLAW_SKIP_CRON = prev.skipCron;
- process.env.OPENCLAW_SKIP_CANVAS_HOST = prev.skipCanvas;
- process.env.OPENCLAW_GATEWAY_TOKEN = prev.token;
- process.env.OPENCLAW_GATEWAY_PASSWORD = prev.password;
- }
- }, 60_000);
-});
diff --git a/src/commands/onboard-non-interactive.cloudflare-ai-gateway.test.ts b/src/commands/onboard-non-interactive.cloudflare-ai-gateway.test.ts
deleted file mode 100644
index c6196317cf..0000000000
--- a/src/commands/onboard-non-interactive.cloudflare-ai-gateway.test.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-import fs from "node:fs/promises";
-import os from "node:os";
-import path from "node:path";
-import { describe, expect, it, vi } from "vitest";
-
-describe("onboard (non-interactive): Cloudflare AI Gateway", () => {
- it("stores the API key and configures the default model", async () => {
- const prev = {
- home: process.env.HOME,
- stateDir: process.env.OPENCLAW_STATE_DIR,
- configPath: process.env.OPENCLAW_CONFIG_PATH,
- skipChannels: process.env.OPENCLAW_SKIP_CHANNELS,
- skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER,
- skipCron: process.env.OPENCLAW_SKIP_CRON,
- skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST,
- token: process.env.OPENCLAW_GATEWAY_TOKEN,
- password: process.env.OPENCLAW_GATEWAY_PASSWORD,
- };
-
- process.env.OPENCLAW_SKIP_CHANNELS = "1";
- process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1";
- process.env.OPENCLAW_SKIP_CRON = "1";
- process.env.OPENCLAW_SKIP_CANVAS_HOST = "1";
- delete process.env.OPENCLAW_GATEWAY_TOKEN;
- delete process.env.OPENCLAW_GATEWAY_PASSWORD;
-
- const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-onboard-cf-gateway-"));
- process.env.HOME = tempHome;
- process.env.OPENCLAW_STATE_DIR = tempHome;
- process.env.OPENCLAW_CONFIG_PATH = path.join(tempHome, "openclaw.json");
- vi.resetModules();
-
- const runtime = {
- log: () => {},
- error: (msg: string) => {
- throw new Error(msg);
- },
- exit: (code: number) => {
- throw new Error(`exit:${code}`);
- },
- };
-
- try {
- const { runNonInteractiveOnboarding } = await import("./onboard-non-interactive.js");
- await runNonInteractiveOnboarding(
- {
- nonInteractive: true,
- authChoice: "cloudflare-ai-gateway-api-key",
- cloudflareAiGatewayAccountId: "cf-account-id",
- cloudflareAiGatewayGatewayId: "cf-gateway-id",
- cloudflareAiGatewayApiKey: "cf-gateway-test-key",
- skipHealth: true,
- skipChannels: true,
- skipSkills: true,
- json: true,
- },
- runtime,
- );
-
- const { CONFIG_PATH } = await import("../config/config.js");
- const cfg = JSON.parse(await fs.readFile(CONFIG_PATH, "utf8")) as {
- auth?: {
- profiles?: Record;
- };
- agents?: { defaults?: { model?: { primary?: string } } };
- };
-
- expect(cfg.auth?.profiles?.["cloudflare-ai-gateway:default"]?.provider).toBe(
- "cloudflare-ai-gateway",
- );
- expect(cfg.auth?.profiles?.["cloudflare-ai-gateway:default"]?.mode).toBe("api_key");
- expect(cfg.agents?.defaults?.model?.primary).toBe("cloudflare-ai-gateway/claude-sonnet-4-5");
-
- const { ensureAuthProfileStore } = await import("../agents/auth-profiles.js");
- const store = ensureAuthProfileStore();
- const profile = store.profiles["cloudflare-ai-gateway:default"];
- expect(profile?.type).toBe("api_key");
- if (profile?.type === "api_key") {
- expect(profile.provider).toBe("cloudflare-ai-gateway");
- expect(profile.key).toBe("cf-gateway-test-key");
- expect(profile.metadata).toEqual({
- accountId: "cf-account-id",
- gatewayId: "cf-gateway-id",
- });
- }
- } finally {
- await fs.rm(tempHome, { recursive: true, force: true });
- process.env.HOME = prev.home;
- process.env.OPENCLAW_STATE_DIR = prev.stateDir;
- process.env.OPENCLAW_CONFIG_PATH = prev.configPath;
- process.env.OPENCLAW_SKIP_CHANNELS = prev.skipChannels;
- process.env.OPENCLAW_SKIP_GMAIL_WATCHER = prev.skipGmail;
- process.env.OPENCLAW_SKIP_CRON = prev.skipCron;
- process.env.OPENCLAW_SKIP_CANVAS_HOST = prev.skipCanvas;
- process.env.OPENCLAW_GATEWAY_TOKEN = prev.token;
- process.env.OPENCLAW_GATEWAY_PASSWORD = prev.password;
- }
- }, 60_000);
-
- it("infers auth choice from API key flags", async () => {
- const prev = {
- home: process.env.HOME,
- stateDir: process.env.OPENCLAW_STATE_DIR,
- configPath: process.env.OPENCLAW_CONFIG_PATH,
- skipChannels: process.env.OPENCLAW_SKIP_CHANNELS,
- skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER,
- skipCron: process.env.OPENCLAW_SKIP_CRON,
- skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST,
- token: process.env.OPENCLAW_GATEWAY_TOKEN,
- password: process.env.OPENCLAW_GATEWAY_PASSWORD,
- };
-
- process.env.OPENCLAW_SKIP_CHANNELS = "1";
- process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1";
- process.env.OPENCLAW_SKIP_CRON = "1";
- process.env.OPENCLAW_SKIP_CANVAS_HOST = "1";
- delete process.env.OPENCLAW_GATEWAY_TOKEN;
- delete process.env.OPENCLAW_GATEWAY_PASSWORD;
-
- const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-onboard-cf-gateway-infer-"));
- process.env.HOME = tempHome;
- process.env.OPENCLAW_STATE_DIR = tempHome;
- process.env.OPENCLAW_CONFIG_PATH = path.join(tempHome, "openclaw.json");
- vi.resetModules();
-
- const runtime = {
- log: () => {},
- error: (msg: string) => {
- throw new Error(msg);
- },
- exit: (code: number) => {
- throw new Error(`exit:${code}`);
- },
- };
-
- try {
- const { runNonInteractiveOnboarding } = await import("./onboard-non-interactive.js");
- await runNonInteractiveOnboarding(
- {
- nonInteractive: true,
- cloudflareAiGatewayAccountId: "cf-account-id",
- cloudflareAiGatewayGatewayId: "cf-gateway-id",
- cloudflareAiGatewayApiKey: "cf-gateway-test-key",
- skipHealth: true,
- skipChannels: true,
- skipSkills: true,
- json: true,
- },
- runtime,
- );
-
- const { CONFIG_PATH } = await import("../config/config.js");
- const cfg = JSON.parse(await fs.readFile(CONFIG_PATH, "utf8")) as {
- auth?: {
- profiles?: Record;
- };
- agents?: { defaults?: { model?: { primary?: string } } };
- };
-
- expect(cfg.auth?.profiles?.["cloudflare-ai-gateway:default"]?.provider).toBe(
- "cloudflare-ai-gateway",
- );
- expect(cfg.auth?.profiles?.["cloudflare-ai-gateway:default"]?.mode).toBe("api_key");
- expect(cfg.agents?.defaults?.model?.primary).toBe("cloudflare-ai-gateway/claude-sonnet-4-5");
-
- const { ensureAuthProfileStore } = await import("../agents/auth-profiles.js");
- const store = ensureAuthProfileStore();
- const profile = store.profiles["cloudflare-ai-gateway:default"];
- expect(profile?.type).toBe("api_key");
- if (profile?.type === "api_key") {
- expect(profile.provider).toBe("cloudflare-ai-gateway");
- expect(profile.key).toBe("cf-gateway-test-key");
- expect(profile.metadata).toEqual({
- accountId: "cf-account-id",
- gatewayId: "cf-gateway-id",
- });
- }
- } finally {
- await fs.rm(tempHome, { recursive: true, force: true });
- process.env.HOME = prev.home;
- process.env.OPENCLAW_STATE_DIR = prev.stateDir;
- process.env.OPENCLAW_CONFIG_PATH = prev.configPath;
- process.env.OPENCLAW_SKIP_CHANNELS = prev.skipChannels;
- process.env.OPENCLAW_SKIP_GMAIL_WATCHER = prev.skipGmail;
- process.env.OPENCLAW_SKIP_CRON = prev.skipCron;
- process.env.OPENCLAW_SKIP_CANVAS_HOST = prev.skipCanvas;
- process.env.OPENCLAW_GATEWAY_TOKEN = prev.token;
- process.env.OPENCLAW_GATEWAY_PASSWORD = prev.password;
- }
- }, 60_000);
-});
diff --git a/src/commands/onboard-non-interactive.openai-api-key.test.ts b/src/commands/onboard-non-interactive.openai-api-key.test.ts
deleted file mode 100644
index 1a9d5989e9..0000000000
--- a/src/commands/onboard-non-interactive.openai-api-key.test.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import fs from "node:fs/promises";
-import os from "node:os";
-import path from "node:path";
-import { describe, expect, it, vi } from "vitest";
-import { OPENAI_DEFAULT_MODEL } from "./openai-model-default.js";
-
-describe("onboard (non-interactive): OpenAI API key", () => {
- it("stores OPENAI_API_KEY and configures the OpenAI default model", async () => {
- const prev = {
- home: process.env.HOME,
- stateDir: process.env.OPENCLAW_STATE_DIR,
- configPath: process.env.OPENCLAW_CONFIG_PATH,
- skipChannels: process.env.OPENCLAW_SKIP_CHANNELS,
- skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER,
- skipCron: process.env.OPENCLAW_SKIP_CRON,
- skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST,
- token: process.env.OPENCLAW_GATEWAY_TOKEN,
- password: process.env.OPENCLAW_GATEWAY_PASSWORD,
- };
-
- process.env.OPENCLAW_SKIP_CHANNELS = "1";
- process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1";
- process.env.OPENCLAW_SKIP_CRON = "1";
- process.env.OPENCLAW_SKIP_CANVAS_HOST = "1";
- delete process.env.OPENCLAW_GATEWAY_TOKEN;
- delete process.env.OPENCLAW_GATEWAY_PASSWORD;
-
- const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-onboard-openai-"));
- process.env.HOME = tempHome;
- process.env.OPENCLAW_STATE_DIR = tempHome;
- process.env.OPENCLAW_CONFIG_PATH = path.join(tempHome, "openclaw.json");
- vi.resetModules();
-
- const runtime = {
- log: () => {},
- error: (msg: string) => {
- throw new Error(msg);
- },
- exit: (code: number) => {
- throw new Error(`exit:${code}`);
- },
- };
-
- try {
- const { runNonInteractiveOnboarding } = await import("./onboard-non-interactive.js");
- await runNonInteractiveOnboarding(
- {
- nonInteractive: true,
- authChoice: "openai-api-key",
- openaiApiKey: "sk-openai-test",
- skipHealth: true,
- skipChannels: true,
- skipSkills: true,
- json: true,
- },
- runtime,
- );
-
- const { CONFIG_PATH } = await import("../config/config.js");
- const cfg = JSON.parse(await fs.readFile(CONFIG_PATH, "utf8")) as {
- agents?: { defaults?: { model?: { primary?: string } } };
- };
- expect(cfg.agents?.defaults?.model?.primary).toBe(OPENAI_DEFAULT_MODEL);
- } finally {
- await fs.rm(tempHome, { recursive: true, force: true });
- process.env.HOME = prev.home;
- process.env.OPENCLAW_STATE_DIR = prev.stateDir;
- process.env.OPENCLAW_CONFIG_PATH = prev.configPath;
- process.env.OPENCLAW_SKIP_CHANNELS = prev.skipChannels;
- process.env.OPENCLAW_SKIP_GMAIL_WATCHER = prev.skipGmail;
- process.env.OPENCLAW_SKIP_CRON = prev.skipCron;
- process.env.OPENCLAW_SKIP_CANVAS_HOST = prev.skipCanvas;
- process.env.OPENCLAW_GATEWAY_TOKEN = prev.token;
- process.env.OPENCLAW_GATEWAY_PASSWORD = prev.password;
- }
- }, 60_000);
-});
diff --git a/src/commands/onboard-non-interactive.provider-auth.test.ts b/src/commands/onboard-non-interactive.provider-auth.test.ts
new file mode 100644
index 0000000000..d3edb1891d
--- /dev/null
+++ b/src/commands/onboard-non-interactive.provider-auth.test.ts
@@ -0,0 +1,327 @@
+import fs from "node:fs/promises";
+import os from "node:os";
+import path from "node:path";
+import { describe, expect, it, vi } from "vitest";
+import { OPENAI_DEFAULT_MODEL } from "./openai-model-default.js";
+
+type RuntimeMock = {
+ log: () => void;
+ error: (msg: string) => never;
+ exit: (code: number) => never;
+};
+
+type EnvSnapshot = {
+ home: string | undefined;
+ stateDir: string | undefined;
+ configPath: string | undefined;
+ skipChannels: string | undefined;
+ skipGmail: string | undefined;
+ skipCron: string | undefined;
+ skipCanvas: string | undefined;
+ token: string | undefined;
+ password: string | undefined;
+ disableConfigCache: string | undefined;
+};
+
+type OnboardEnv = {
+ configPath: string;
+ runtime: RuntimeMock;
+};
+
+function captureEnv(): EnvSnapshot {
+ return {
+ home: process.env.HOME,
+ stateDir: process.env.OPENCLAW_STATE_DIR,
+ configPath: process.env.OPENCLAW_CONFIG_PATH,
+ skipChannels: process.env.OPENCLAW_SKIP_CHANNELS,
+ skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER,
+ skipCron: process.env.OPENCLAW_SKIP_CRON,
+ skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST,
+ token: process.env.OPENCLAW_GATEWAY_TOKEN,
+ password: process.env.OPENCLAW_GATEWAY_PASSWORD,
+ disableConfigCache: process.env.OPENCLAW_DISABLE_CONFIG_CACHE,
+ };
+}
+
+function restoreEnvVar(key: keyof NodeJS.ProcessEnv, value: string | undefined): void {
+ if (value == null) {
+ delete process.env[key];
+ return;
+ }
+ process.env[key] = value;
+}
+
+function restoreEnv(prev: EnvSnapshot): void {
+ restoreEnvVar("HOME", prev.home);
+ restoreEnvVar("OPENCLAW_STATE_DIR", prev.stateDir);
+ restoreEnvVar("OPENCLAW_CONFIG_PATH", prev.configPath);
+ restoreEnvVar("OPENCLAW_SKIP_CHANNELS", prev.skipChannels);
+ restoreEnvVar("OPENCLAW_SKIP_GMAIL_WATCHER", prev.skipGmail);
+ restoreEnvVar("OPENCLAW_SKIP_CRON", prev.skipCron);
+ restoreEnvVar("OPENCLAW_SKIP_CANVAS_HOST", prev.skipCanvas);
+ restoreEnvVar("OPENCLAW_GATEWAY_TOKEN", prev.token);
+ restoreEnvVar("OPENCLAW_GATEWAY_PASSWORD", prev.password);
+ restoreEnvVar("OPENCLAW_DISABLE_CONFIG_CACHE", prev.disableConfigCache);
+}
+
+async function withOnboardEnv(
+ prefix: string,
+ run: (ctx: OnboardEnv) => Promise,
+): Promise {
+ const prev = captureEnv();
+
+ process.env.OPENCLAW_SKIP_CHANNELS = "1";
+ process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1";
+ process.env.OPENCLAW_SKIP_CRON = "1";
+ process.env.OPENCLAW_SKIP_CANVAS_HOST = "1";
+ process.env.OPENCLAW_DISABLE_CONFIG_CACHE = "1";
+ delete process.env.OPENCLAW_GATEWAY_TOKEN;
+ delete process.env.OPENCLAW_GATEWAY_PASSWORD;
+
+ const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
+ const configPath = path.join(tempHome, "openclaw.json");
+ process.env.HOME = tempHome;
+ process.env.OPENCLAW_STATE_DIR = tempHome;
+ process.env.OPENCLAW_CONFIG_PATH = configPath;
+ vi.resetModules();
+
+ const runtime: RuntimeMock = {
+ log: () => {},
+ error: (msg: string) => {
+ throw new Error(msg);
+ },
+ exit: (code: number) => {
+ throw new Error(`exit:${code}`);
+ },
+ };
+
+ try {
+ await run({ configPath, runtime });
+ } finally {
+ await fs.rm(tempHome, { recursive: true, force: true });
+ restoreEnv(prev);
+ }
+}
+
+async function runNonInteractive(
+ options: Record,
+ runtime: RuntimeMock,
+): Promise {
+ const { runNonInteractiveOnboarding } = await import("./onboard-non-interactive.js");
+ await runNonInteractiveOnboarding(options, runtime);
+}
+
+async function readJsonFile(filePath: string): Promise {
+ return JSON.parse(await fs.readFile(filePath, "utf8")) as T;
+}
+
+async function expectApiKeyProfile(params: {
+ profileId: string;
+ provider: string;
+ key: string;
+ metadata?: Record;
+}): Promise {
+ const { ensureAuthProfileStore } = await import("../agents/auth-profiles.js");
+ const store = ensureAuthProfileStore();
+ const profile = store.profiles[params.profileId];
+ expect(profile?.type).toBe("api_key");
+ if (profile?.type === "api_key") {
+ expect(profile.provider).toBe(params.provider);
+ expect(profile.key).toBe(params.key);
+ if (params.metadata) {
+ expect(profile.metadata).toEqual(params.metadata);
+ }
+ }
+}
+
+describe("onboard (non-interactive): provider auth", () => {
+ it("stores xAI API key and sets default model", async () => {
+ await withOnboardEnv("openclaw-onboard-xai-", async ({ configPath, runtime }) => {
+ await runNonInteractive(
+ {
+ nonInteractive: true,
+ authChoice: "xai-api-key",
+ xaiApiKey: "xai-test-key",
+ skipHealth: true,
+ skipChannels: true,
+ skipSkills: true,
+ json: true,
+ },
+ runtime,
+ );
+
+ const cfg = await readJsonFile<{
+ auth?: { profiles?: Record };
+ agents?: { defaults?: { model?: { primary?: string } } };
+ }>(configPath);
+
+ expect(cfg.auth?.profiles?.["xai:default"]?.provider).toBe("xai");
+ expect(cfg.auth?.profiles?.["xai:default"]?.mode).toBe("api_key");
+ expect(cfg.agents?.defaults?.model?.primary).toBe("xai/grok-4");
+ await expectApiKeyProfile({ profileId: "xai:default", provider: "xai", key: "xai-test-key" });
+ });
+ }, 60_000);
+
+ it("stores Vercel AI Gateway API key and sets default model", async () => {
+ await withOnboardEnv("openclaw-onboard-ai-gateway-", async ({ configPath, runtime }) => {
+ await runNonInteractive(
+ {
+ nonInteractive: true,
+ authChoice: "ai-gateway-api-key",
+ aiGatewayApiKey: "gateway-test-key",
+ skipHealth: true,
+ skipChannels: true,
+ skipSkills: true,
+ json: true,
+ },
+ runtime,
+ );
+
+ const cfg = await readJsonFile<{
+ auth?: { profiles?: Record };
+ agents?: { defaults?: { model?: { primary?: string } } };
+ }>(configPath);
+
+ expect(cfg.auth?.profiles?.["vercel-ai-gateway:default"]?.provider).toBe("vercel-ai-gateway");
+ expect(cfg.auth?.profiles?.["vercel-ai-gateway:default"]?.mode).toBe("api_key");
+ expect(cfg.agents?.defaults?.model?.primary).toBe(
+ "vercel-ai-gateway/anthropic/claude-opus-4.6",
+ );
+ await expectApiKeyProfile({
+ profileId: "vercel-ai-gateway:default",
+ provider: "vercel-ai-gateway",
+ key: "gateway-test-key",
+ });
+ });
+ }, 60_000);
+
+ it("stores token auth profile", async () => {
+ await withOnboardEnv("openclaw-onboard-token-", async ({ configPath, runtime }) => {
+ const token = `sk-ant-oat01-${"a".repeat(80)}`;
+
+ await runNonInteractive(
+ {
+ nonInteractive: true,
+ authChoice: "token",
+ tokenProvider: "anthropic",
+ token,
+ tokenProfileId: "anthropic:default",
+ skipHealth: true,
+ skipChannels: true,
+ json: true,
+ },
+ runtime,
+ );
+
+ const cfg = await readJsonFile<{
+ auth?: { profiles?: Record };
+ }>(configPath);
+
+ expect(cfg.auth?.profiles?.["anthropic:default"]?.provider).toBe("anthropic");
+ expect(cfg.auth?.profiles?.["anthropic:default"]?.mode).toBe("token");
+
+ const { ensureAuthProfileStore } = await import("../agents/auth-profiles.js");
+ const store = ensureAuthProfileStore();
+ const profile = store.profiles["anthropic:default"];
+ expect(profile?.type).toBe("token");
+ if (profile?.type === "token") {
+ expect(profile.provider).toBe("anthropic");
+ expect(profile.token).toBe(token);
+ }
+ });
+ }, 60_000);
+
+ it("stores OpenAI API key and sets OpenAI default model", async () => {
+ await withOnboardEnv("openclaw-onboard-openai-", async ({ configPath, runtime }) => {
+ await runNonInteractive(
+ {
+ nonInteractive: true,
+ authChoice: "openai-api-key",
+ openaiApiKey: "sk-openai-test",
+ skipHealth: true,
+ skipChannels: true,
+ skipSkills: true,
+ json: true,
+ },
+ runtime,
+ );
+
+ const cfg = await readJsonFile<{
+ agents?: { defaults?: { model?: { primary?: string } } };
+ }>(configPath);
+
+ expect(cfg.agents?.defaults?.model?.primary).toBe(OPENAI_DEFAULT_MODEL);
+ });
+ }, 60_000);
+
+ it("stores Cloudflare AI Gateway API key and metadata", async () => {
+ await withOnboardEnv("openclaw-onboard-cf-gateway-", async ({ configPath, runtime }) => {
+ await runNonInteractive(
+ {
+ nonInteractive: true,
+ authChoice: "cloudflare-ai-gateway-api-key",
+ cloudflareAiGatewayAccountId: "cf-account-id",
+ cloudflareAiGatewayGatewayId: "cf-gateway-id",
+ cloudflareAiGatewayApiKey: "cf-gateway-test-key",
+ skipHealth: true,
+ skipChannels: true,
+ skipSkills: true,
+ json: true,
+ },
+ runtime,
+ );
+
+ const cfg = await readJsonFile<{
+ auth?: { profiles?: Record };
+ agents?: { defaults?: { model?: { primary?: string } } };
+ }>(configPath);
+
+ expect(cfg.auth?.profiles?.["cloudflare-ai-gateway:default"]?.provider).toBe(
+ "cloudflare-ai-gateway",
+ );
+ expect(cfg.auth?.profiles?.["cloudflare-ai-gateway:default"]?.mode).toBe("api_key");
+ expect(cfg.agents?.defaults?.model?.primary).toBe("cloudflare-ai-gateway/claude-sonnet-4-5");
+ await expectApiKeyProfile({
+ profileId: "cloudflare-ai-gateway:default",
+ provider: "cloudflare-ai-gateway",
+ key: "cf-gateway-test-key",
+ metadata: { accountId: "cf-account-id", gatewayId: "cf-gateway-id" },
+ });
+ });
+ }, 60_000);
+
+ it("infers Cloudflare auth choice from API key flags", async () => {
+ await withOnboardEnv("openclaw-onboard-cf-gateway-infer-", async ({ configPath, runtime }) => {
+ await runNonInteractive(
+ {
+ nonInteractive: true,
+ cloudflareAiGatewayAccountId: "cf-account-id",
+ cloudflareAiGatewayGatewayId: "cf-gateway-id",
+ cloudflareAiGatewayApiKey: "cf-gateway-test-key",
+ skipHealth: true,
+ skipChannels: true,
+ skipSkills: true,
+ json: true,
+ },
+ runtime,
+ );
+
+ const cfg = await readJsonFile<{
+ auth?: { profiles?: Record };
+ agents?: { defaults?: { model?: { primary?: string } } };
+ }>(configPath);
+
+ expect(cfg.auth?.profiles?.["cloudflare-ai-gateway:default"]?.provider).toBe(
+ "cloudflare-ai-gateway",
+ );
+ expect(cfg.auth?.profiles?.["cloudflare-ai-gateway:default"]?.mode).toBe("api_key");
+ expect(cfg.agents?.defaults?.model?.primary).toBe("cloudflare-ai-gateway/claude-sonnet-4-5");
+ await expectApiKeyProfile({
+ profileId: "cloudflare-ai-gateway:default",
+ provider: "cloudflare-ai-gateway",
+ key: "cf-gateway-test-key",
+ metadata: { accountId: "cf-account-id", gatewayId: "cf-gateway-id" },
+ });
+ });
+ }, 60_000);
+});
diff --git a/src/commands/onboard-non-interactive.token.test.ts b/src/commands/onboard-non-interactive.token.test.ts
deleted file mode 100644
index bcbe2c221b..0000000000
--- a/src/commands/onboard-non-interactive.token.test.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import fs from "node:fs/promises";
-import os from "node:os";
-import path from "node:path";
-import { describe, expect, it, vi } from "vitest";
-
-describe("onboard (non-interactive): token auth", () => {
- it("writes token profile config and stores the token", async () => {
- const prev = {
- home: process.env.HOME,
- stateDir: process.env.OPENCLAW_STATE_DIR,
- configPath: process.env.OPENCLAW_CONFIG_PATH,
- skipChannels: process.env.OPENCLAW_SKIP_CHANNELS,
- skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER,
- skipCron: process.env.OPENCLAW_SKIP_CRON,
- skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST,
- token: process.env.OPENCLAW_GATEWAY_TOKEN,
- password: process.env.OPENCLAW_GATEWAY_PASSWORD,
- };
-
- process.env.OPENCLAW_SKIP_CHANNELS = "1";
- process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1";
- process.env.OPENCLAW_SKIP_CRON = "1";
- process.env.OPENCLAW_SKIP_CANVAS_HOST = "1";
- delete process.env.OPENCLAW_GATEWAY_TOKEN;
- delete process.env.OPENCLAW_GATEWAY_PASSWORD;
-
- const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-onboard-token-"));
- process.env.HOME = tempHome;
- process.env.OPENCLAW_STATE_DIR = tempHome;
- process.env.OPENCLAW_CONFIG_PATH = path.join(tempHome, "openclaw.json");
- vi.resetModules();
-
- const token = `sk-ant-oat01-${"a".repeat(80)}`;
-
- const runtime = {
- log: () => {},
- error: (msg: string) => {
- throw new Error(msg);
- },
- exit: (code: number) => {
- throw new Error(`exit:${code}`);
- },
- };
-
- try {
- const { runNonInteractiveOnboarding } = await import("./onboard-non-interactive.js");
- await runNonInteractiveOnboarding(
- {
- nonInteractive: true,
- authChoice: "token",
- tokenProvider: "anthropic",
- token,
- tokenProfileId: "anthropic:default",
- skipHealth: true,
- skipChannels: true,
- json: true,
- },
- runtime,
- );
-
- const { CONFIG_PATH } = await import("../config/config.js");
- const cfg = JSON.parse(await fs.readFile(CONFIG_PATH, "utf8")) as {
- auth?: {
- profiles?: Record;
- };
- };
-
- expect(cfg.auth?.profiles?.["anthropic:default"]?.provider).toBe("anthropic");
- expect(cfg.auth?.profiles?.["anthropic:default"]?.mode).toBe("token");
-
- const { ensureAuthProfileStore } = await import("../agents/auth-profiles.js");
- const store = ensureAuthProfileStore();
- const profile = store.profiles["anthropic:default"];
- expect(profile?.type).toBe("token");
- if (profile?.type === "token") {
- expect(profile.provider).toBe("anthropic");
- expect(profile.token).toBe(token);
- }
- } finally {
- await fs.rm(tempHome, { recursive: true, force: true });
- process.env.HOME = prev.home;
- process.env.OPENCLAW_STATE_DIR = prev.stateDir;
- process.env.OPENCLAW_CONFIG_PATH = prev.configPath;
- process.env.OPENCLAW_SKIP_CHANNELS = prev.skipChannels;
- process.env.OPENCLAW_SKIP_GMAIL_WATCHER = prev.skipGmail;
- process.env.OPENCLAW_SKIP_CRON = prev.skipCron;
- process.env.OPENCLAW_SKIP_CANVAS_HOST = prev.skipCanvas;
- process.env.OPENCLAW_GATEWAY_TOKEN = prev.token;
- process.env.OPENCLAW_GATEWAY_PASSWORD = prev.password;
- }
- }, 60_000);
-});
diff --git a/src/commands/onboard-non-interactive.xai.test.ts b/src/commands/onboard-non-interactive.xai.test.ts
deleted file mode 100644
index 1c4d2dda7f..0000000000
--- a/src/commands/onboard-non-interactive.xai.test.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import fs from "node:fs/promises";
-import os from "node:os";
-import path from "node:path";
-import { describe, expect, it, vi } from "vitest";
-
-describe("onboard (non-interactive): xAI", () => {
- it("stores the API key and configures the default model", async () => {
- const prev = {
- home: process.env.HOME,
- stateDir: process.env.OPENCLAW_STATE_DIR,
- configPath: process.env.OPENCLAW_CONFIG_PATH,
- skipChannels: process.env.OPENCLAW_SKIP_CHANNELS,
- skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER,
- skipCron: process.env.OPENCLAW_SKIP_CRON,
- skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST,
- token: process.env.OPENCLAW_GATEWAY_TOKEN,
- password: process.env.OPENCLAW_GATEWAY_PASSWORD,
- };
-
- process.env.OPENCLAW_SKIP_CHANNELS = "1";
- process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1";
- process.env.OPENCLAW_SKIP_CRON = "1";
- process.env.OPENCLAW_SKIP_CANVAS_HOST = "1";
- delete process.env.OPENCLAW_GATEWAY_TOKEN;
- delete process.env.OPENCLAW_GATEWAY_PASSWORD;
-
- const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-onboard-xai-"));
- process.env.HOME = tempHome;
- process.env.OPENCLAW_STATE_DIR = tempHome;
- process.env.OPENCLAW_CONFIG_PATH = path.join(tempHome, "openclaw.json");
- vi.resetModules();
-
- const runtime = {
- log: () => {},
- error: (msg: string) => {
- throw new Error(msg);
- },
- exit: (code: number) => {
- throw new Error(`exit:${code}`);
- },
- };
-
- try {
- const { runNonInteractiveOnboarding } = await import("./onboard-non-interactive.js");
- await runNonInteractiveOnboarding(
- {
- nonInteractive: true,
- authChoice: "xai-api-key",
- xaiApiKey: "xai-test-key",
- skipHealth: true,
- skipChannels: true,
- skipSkills: true,
- json: true,
- },
- runtime,
- );
-
- const { CONFIG_PATH } = await import("../config/config.js");
- const cfg = JSON.parse(await fs.readFile(CONFIG_PATH, "utf8")) as {
- auth?: {
- profiles?: Record;
- };
- agents?: { defaults?: { model?: { primary?: string } } };
- };
-
- expect(cfg.auth?.profiles?.["xai:default"]?.provider).toBe("xai");
- expect(cfg.auth?.profiles?.["xai:default"]?.mode).toBe("api_key");
- expect(cfg.agents?.defaults?.model?.primary).toBe("xai/grok-4");
-
- const { ensureAuthProfileStore } = await import("../agents/auth-profiles.js");
- const store = ensureAuthProfileStore();
- const profile = store.profiles["xai:default"];
- expect(profile?.type).toBe("api_key");
- if (profile?.type === "api_key") {
- expect(profile.provider).toBe("xai");
- expect(profile.key).toBe("xai-test-key");
- }
- } finally {
- await fs.rm(tempHome, { recursive: true, force: true });
- process.env.HOME = prev.home;
- process.env.OPENCLAW_STATE_DIR = prev.stateDir;
- process.env.OPENCLAW_CONFIG_PATH = prev.configPath;
- process.env.OPENCLAW_SKIP_CHANNELS = prev.skipChannels;
- process.env.OPENCLAW_SKIP_GMAIL_WATCHER = prev.skipGmail;
- process.env.OPENCLAW_SKIP_CRON = prev.skipCron;
- process.env.OPENCLAW_SKIP_CANVAS_HOST = prev.skipCanvas;
- process.env.OPENCLAW_GATEWAY_TOKEN = prev.token;
- process.env.OPENCLAW_GATEWAY_PASSWORD = prev.password;
- }
- }, 60_000);
-});
diff --git a/src/memory/qmd-manager.test.ts b/src/memory/qmd-manager.test.ts
index 0475aec502..54fb5bd174 100644
--- a/src/memory/qmd-manager.test.ts
+++ b/src/memory/qmd-manager.test.ts
@@ -25,9 +25,15 @@ function createMockChild(params?: { autoClose?: boolean; closeDelayMs?: number }
};
if (params?.autoClose !== false) {
const delayMs = params?.closeDelayMs ?? 0;
- setTimeout(() => {
- child.emit("close", 0);
- }, delayMs);
+ if (delayMs <= 0) {
+ queueMicrotask(() => {
+ child.emit("close", 0);
+ });
+ } else {
+ setTimeout(() => {
+ child.emit("close", 0);
+ }, delayMs);
+ }
}
return child;
}
@@ -73,6 +79,7 @@ describe("QmdMemoryManager", () => {
});
afterEach(async () => {
+ vi.useRealTimers();
delete process.env.OPENCLAW_STATE_DIR;
await fs.rm(tmpRoot, { recursive: true, force: true });
});
@@ -218,6 +225,7 @@ describe("QmdMemoryManager", () => {
});
it("times out qmd update during sync when configured", async () => {
+ vi.useFakeTimers();
cfg = {
...cfg,
memory: {
@@ -242,14 +250,17 @@ describe("QmdMemoryManager", () => {
});
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
- const manager = await QmdMemoryManager.create({ cfg, agentId, resolved });
+ const createPromise = QmdMemoryManager.create({ cfg, agentId, resolved });
+ await vi.advanceTimersByTimeAsync(0);
+ const manager = await createPromise;
expect(manager).toBeTruthy();
if (!manager) {
throw new Error("manager missing");
}
- await expect(manager.sync({ reason: "manual" })).rejects.toThrow(
- "qmd update timed out after 20ms",
- );
+ const syncPromise = manager.sync({ reason: "manual" });
+ const rejected = expect(syncPromise).rejects.toThrow("qmd update timed out after 20ms");
+ await vi.advanceTimersByTimeAsync(20);
+ await rejected;
await manager.close();
});
@@ -378,6 +389,7 @@ describe("QmdMemoryManager", () => {
});
it("logs and continues when qmd embed times out", async () => {
+ vi.useFakeTimers();
cfg = {
...cfg,
memory: {
@@ -402,12 +414,17 @@ describe("QmdMemoryManager", () => {
});
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
- const manager = await QmdMemoryManager.create({ cfg, agentId, resolved });
+ const createPromise = QmdMemoryManager.create({ cfg, agentId, resolved });
+ await vi.advanceTimersByTimeAsync(0);
+ const manager = await createPromise;
expect(manager).toBeTruthy();
if (!manager) {
throw new Error("manager missing");
}
- await expect(manager.sync({ reason: "manual" })).resolves.toBeUndefined();
+ const syncPromise = manager.sync({ reason: "manual" });
+ const resolvedSync = expect(syncPromise).resolves.toBeUndefined();
+ await vi.advanceTimersByTimeAsync(20);
+ await resolvedSync;
await manager.close();
});
diff --git a/src/security/audit.test.ts b/src/security/audit.test.ts
index 27b55e4823..7fc5b8843c 100644
--- a/src/security/audit.test.ts
+++ b/src/security/audit.test.ts
@@ -11,6 +11,20 @@ import { runSecurityAudit } from "./audit.js";
const isWindows = process.platform === "win32";
+function successfulProbeResult(url: string) {
+ return {
+ ok: true,
+ url,
+ connectLatencyMs: 1,
+ error: null,
+ close: null,
+ health: null,
+ status: null,
+ presence: null,
+ configSnapshot: null,
+ };
+}
+
describe("security audit", () => {
it("includes an attack surface summary (info)", async () => {
const cfg: OpenClawConfig = {
@@ -1021,6 +1035,7 @@ describe("security audit", () => {
includeChannelSecurity: false,
deep: true,
stateDir: tmpDir,
+ probeGatewayFn: async (opts) => successfulProbeResult(opts.url),
});
expect(
@@ -1075,6 +1090,7 @@ description: test skill
includeChannelSecurity: false,
deep: true,
stateDir: tmpDir,
+ probeGatewayFn: async (opts) => successfulProbeResult(opts.url),
});
const pluginFinding = deepRes.findings.find(
@@ -1113,6 +1129,7 @@ description: test skill
includeChannelSecurity: false,
deep: true,
stateDir: tmpDir,
+ probeGatewayFn: async (opts) => successfulProbeResult(opts.url),
});
expect(res.findings.some((f) => f.checkId === "plugins.code_safety.entry_escape")).toBe(true);
diff --git a/src/web/login.test.ts b/src/web/login.test.ts
index c3ed32e521..5ef6f889bd 100644
--- a/src/web/login.test.ts
+++ b/src/web/login.test.ts
@@ -23,10 +23,12 @@ const { createWaSocket } = await import("./session.js");
describe("web login", () => {
beforeEach(() => {
+ vi.useFakeTimers();
vi.clearAllMocks();
});
afterEach(() => {
+ vi.useRealTimers();
resetLogger();
setLoggerOverride(null);
});
@@ -36,7 +38,12 @@ describe("web login", () => {
const close = vi.spyOn(sock.ws, "close");
const waiter: typeof waitForWaConnection = vi.fn().mockResolvedValue(undefined);
await loginWeb(false, waiter);
- await new Promise((resolve) => setTimeout(resolve, 550));
- expect(close).toHaveBeenCalled();
+ expect(close).not.toHaveBeenCalled();
+
+ await vi.advanceTimersByTimeAsync(499);
+ expect(close).not.toHaveBeenCalled();
+
+ await vi.advanceTimersByTimeAsync(1);
+ expect(close).toHaveBeenCalledTimes(1);
});
});