Tests: harden flake hotspots and consolidate provider-auth suites (#11598)
* Tests: harden flake hotspots and consolidate provider-auth suites * Tests: restore env vars by deleting missing snapshot values * Tests: use real newline in memory summary filter case * Tests(memory): use fake timers for qmd timeout coverage * Changelog: add tests hardening entry for #11598
This commit is contained in:
committed by
GitHub
parent
a30c4f45c3
commit
e2dea2684f
@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Memory: set Voyage embeddings `input_type` for improved retrieval. (#10818) Thanks @mcinteerj.
|
- 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)
|
- 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.
|
- 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
|
## 2026.2.6
|
||||||
|
|
||||||
|
|||||||
@@ -92,70 +92,28 @@ describe("memory plugin e2e", () => {
|
|||||||
}).toThrow("embedding.apiKey is required");
|
}).toThrow("embedding.apiKey is required");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("shouldCapture filters correctly", async () => {
|
test("shouldCapture applies real capture rules", async () => {
|
||||||
// Test the capture filtering logic by checking the rules
|
const { shouldCapture } = await import("./index.js");
|
||||||
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: "<relevant-memories>injected</relevant-memories>", shouldMatch: false }, // Skip injected
|
|
||||||
];
|
|
||||||
|
|
||||||
// The shouldCapture function is internal, but we can test via the capture behavior
|
expect(shouldCapture("I prefer dark mode")).toBe(true);
|
||||||
// For now, just verify the patterns we expect to match
|
expect(shouldCapture("Remember that my name is John")).toBe(true);
|
||||||
for (const { text, shouldMatch } of triggers) {
|
expect(shouldCapture("My email is test@example.com")).toBe(true);
|
||||||
const hasPreference = /prefer|radši|like|love|hate|want/i.test(text);
|
expect(shouldCapture("Call me at +1234567890123")).toBe(true);
|
||||||
const hasRemember = /zapamatuj|pamatuj|remember/i.test(text);
|
expect(shouldCapture("I always want verbose output")).toBe(true);
|
||||||
const hasEmail = /[\w.-]+@[\w.-]+\.\w+/.test(text);
|
expect(shouldCapture("x")).toBe(false);
|
||||||
const hasPhone = /\+\d{10,}/.test(text);
|
expect(shouldCapture("<relevant-memories>injected</relevant-memories>")).toBe(false);
|
||||||
const hasDecision = /rozhodli|decided|will use|budeme/i.test(text);
|
expect(shouldCapture("<system>status</system>")).toBe(false);
|
||||||
const hasAlways = /always|never|important/i.test(text);
|
expect(shouldCapture("Here is a short **summary**\n- bullet")).toBe(false);
|
||||||
const isInjected = text.includes("<relevant-memories>");
|
|
||||||
const isTooShort = text.length < 10;
|
|
||||||
|
|
||||||
const wouldCapture =
|
|
||||||
!isTooShort &&
|
|
||||||
!isInjected &&
|
|
||||||
(hasPreference || hasRemember || hasEmail || hasPhone || hasDecision || hasAlways);
|
|
||||||
|
|
||||||
if (shouldMatch) {
|
|
||||||
expect(wouldCapture).toBe(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("detectCategory classifies correctly", async () => {
|
test("detectCategory classifies using production logic", async () => {
|
||||||
// Test category detection patterns
|
const { detectCategory } = await import("./index.js");
|
||||||
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" },
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const { text, expected } of cases) {
|
expect(detectCategory("I prefer dark mode")).toBe("preference");
|
||||||
const lower = text.toLowerCase();
|
expect(detectCategory("We decided to use React")).toBe("decision");
|
||||||
let category: string;
|
expect(detectCategory("My email is test@example.com")).toBe("entity");
|
||||||
|
expect(detectCategory("The server is running on port 3000")).toBe("fact");
|
||||||
if (/prefer|radši|like|love|hate|want/i.test(lower)) {
|
expect(detectCategory("Random note")).toBe("other");
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ const MEMORY_TRIGGERS = [
|
|||||||
/always|never|important/i,
|
/always|never|important/i,
|
||||||
];
|
];
|
||||||
|
|
||||||
function shouldCapture(text: string): boolean {
|
export function shouldCapture(text: string): boolean {
|
||||||
if (text.length < 10 || text.length > 500) {
|
if (text.length < 10 || text.length > 500) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -219,7 +219,7 @@ function shouldCapture(text: string): boolean {
|
|||||||
return MEMORY_TRIGGERS.some((r) => r.test(text));
|
return MEMORY_TRIGGERS.some((r) => r.test(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectCategory(text: string): MemoryCategory {
|
export function detectCategory(text: string): MemoryCategory {
|
||||||
const lower = text.toLowerCase();
|
const lower = text.toLowerCase();
|
||||||
if (/prefer|radši|like|love|hate|want/i.test(lower)) {
|
if (/prefer|radši|like|love|hate|want/i.test(lower)) {
|
||||||
return "preference";
|
return "preference";
|
||||||
|
|||||||
@@ -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<string, { provider?: string; mode?: string }>;
|
|
||||||
};
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
@@ -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<string, { provider?: string; mode?: string }>;
|
|
||||||
};
|
|
||||||
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<string, { provider?: string; mode?: string }>;
|
|
||||||
};
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
327
src/commands/onboard-non-interactive.provider-auth.test.ts
Normal file
327
src/commands/onboard-non-interactive.provider-auth.test.ts
Normal file
@@ -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<void>,
|
||||||
|
): Promise<void> {
|
||||||
|
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<string, unknown>,
|
||||||
|
runtime: RuntimeMock,
|
||||||
|
): Promise<void> {
|
||||||
|
const { runNonInteractiveOnboarding } = await import("./onboard-non-interactive.js");
|
||||||
|
await runNonInteractiveOnboarding(options, runtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readJsonFile<T>(filePath: string): Promise<T> {
|
||||||
|
return JSON.parse(await fs.readFile(filePath, "utf8")) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectApiKeyProfile(params: {
|
||||||
|
profileId: string;
|
||||||
|
provider: string;
|
||||||
|
key: string;
|
||||||
|
metadata?: Record<string, string>;
|
||||||
|
}): Promise<void> {
|
||||||
|
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<string, { provider?: string; mode?: string }> };
|
||||||
|
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<string, { provider?: string; mode?: string }> };
|
||||||
|
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<string, { provider?: string; mode?: string }> };
|
||||||
|
}>(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<string, { provider?: string; mode?: string }> };
|
||||||
|
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<string, { provider?: string; mode?: string }> };
|
||||||
|
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);
|
||||||
|
});
|
||||||
@@ -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<string, { provider?: string; mode?: string }>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
@@ -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<string, { provider?: string; mode?: string }>;
|
|
||||||
};
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
@@ -25,9 +25,15 @@ function createMockChild(params?: { autoClose?: boolean; closeDelayMs?: number }
|
|||||||
};
|
};
|
||||||
if (params?.autoClose !== false) {
|
if (params?.autoClose !== false) {
|
||||||
const delayMs = params?.closeDelayMs ?? 0;
|
const delayMs = params?.closeDelayMs ?? 0;
|
||||||
setTimeout(() => {
|
if (delayMs <= 0) {
|
||||||
child.emit("close", 0);
|
queueMicrotask(() => {
|
||||||
}, delayMs);
|
child.emit("close", 0);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
child.emit("close", 0);
|
||||||
|
}, delayMs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
@@ -73,6 +79,7 @@ describe("QmdMemoryManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
vi.useRealTimers();
|
||||||
delete process.env.OPENCLAW_STATE_DIR;
|
delete process.env.OPENCLAW_STATE_DIR;
|
||||||
await fs.rm(tmpRoot, { recursive: true, force: true });
|
await fs.rm(tmpRoot, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
@@ -218,6 +225,7 @@ describe("QmdMemoryManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("times out qmd update during sync when configured", async () => {
|
it("times out qmd update during sync when configured", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
cfg = {
|
cfg = {
|
||||||
...cfg,
|
...cfg,
|
||||||
memory: {
|
memory: {
|
||||||
@@ -242,14 +250,17 @@ describe("QmdMemoryManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
|
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();
|
expect(manager).toBeTruthy();
|
||||||
if (!manager) {
|
if (!manager) {
|
||||||
throw new Error("manager missing");
|
throw new Error("manager missing");
|
||||||
}
|
}
|
||||||
await expect(manager.sync({ reason: "manual" })).rejects.toThrow(
|
const syncPromise = manager.sync({ reason: "manual" });
|
||||||
"qmd update timed out after 20ms",
|
const rejected = expect(syncPromise).rejects.toThrow("qmd update timed out after 20ms");
|
||||||
);
|
await vi.advanceTimersByTimeAsync(20);
|
||||||
|
await rejected;
|
||||||
await manager.close();
|
await manager.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -378,6 +389,7 @@ describe("QmdMemoryManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("logs and continues when qmd embed times out", async () => {
|
it("logs and continues when qmd embed times out", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
cfg = {
|
cfg = {
|
||||||
...cfg,
|
...cfg,
|
||||||
memory: {
|
memory: {
|
||||||
@@ -402,12 +414,17 @@ describe("QmdMemoryManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
|
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();
|
expect(manager).toBeTruthy();
|
||||||
if (!manager) {
|
if (!manager) {
|
||||||
throw new Error("manager missing");
|
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();
|
await manager.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,20 @@ import { runSecurityAudit } from "./audit.js";
|
|||||||
|
|
||||||
const isWindows = process.platform === "win32";
|
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", () => {
|
describe("security audit", () => {
|
||||||
it("includes an attack surface summary (info)", async () => {
|
it("includes an attack surface summary (info)", async () => {
|
||||||
const cfg: OpenClawConfig = {
|
const cfg: OpenClawConfig = {
|
||||||
@@ -1021,6 +1035,7 @@ describe("security audit", () => {
|
|||||||
includeChannelSecurity: false,
|
includeChannelSecurity: false,
|
||||||
deep: true,
|
deep: true,
|
||||||
stateDir: tmpDir,
|
stateDir: tmpDir,
|
||||||
|
probeGatewayFn: async (opts) => successfulProbeResult(opts.url),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@@ -1075,6 +1090,7 @@ description: test skill
|
|||||||
includeChannelSecurity: false,
|
includeChannelSecurity: false,
|
||||||
deep: true,
|
deep: true,
|
||||||
stateDir: tmpDir,
|
stateDir: tmpDir,
|
||||||
|
probeGatewayFn: async (opts) => successfulProbeResult(opts.url),
|
||||||
});
|
});
|
||||||
|
|
||||||
const pluginFinding = deepRes.findings.find(
|
const pluginFinding = deepRes.findings.find(
|
||||||
@@ -1113,6 +1129,7 @@ description: test skill
|
|||||||
includeChannelSecurity: false,
|
includeChannelSecurity: false,
|
||||||
deep: true,
|
deep: true,
|
||||||
stateDir: tmpDir,
|
stateDir: tmpDir,
|
||||||
|
probeGatewayFn: async (opts) => successfulProbeResult(opts.url),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.findings.some((f) => f.checkId === "plugins.code_safety.entry_escape")).toBe(true);
|
expect(res.findings.some((f) => f.checkId === "plugins.code_safety.entry_escape")).toBe(true);
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ const { createWaSocket } = await import("./session.js");
|
|||||||
|
|
||||||
describe("web login", () => {
|
describe("web login", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
resetLogger();
|
resetLogger();
|
||||||
setLoggerOverride(null);
|
setLoggerOverride(null);
|
||||||
});
|
});
|
||||||
@@ -36,7 +38,12 @@ describe("web login", () => {
|
|||||||
const close = vi.spyOn(sock.ws, "close");
|
const close = vi.spyOn(sock.ws, "close");
|
||||||
const waiter: typeof waitForWaConnection = vi.fn().mockResolvedValue(undefined);
|
const waiter: typeof waitForWaConnection = vi.fn().mockResolvedValue(undefined);
|
||||||
await loginWeb(false, waiter);
|
await loginWeb(false, waiter);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 550));
|
expect(close).not.toHaveBeenCalled();
|
||||||
expect(close).toHaveBeenCalled();
|
|
||||||
|
await vi.advanceTimersByTimeAsync(499);
|
||||||
|
expect(close).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(1);
|
||||||
|
expect(close).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user