test(core): trim redundant setup and tighten waits
This commit is contained in:
@@ -123,7 +123,7 @@ describe("daemon-cli coverage", () => {
|
||||
expect(callGateway).toHaveBeenCalledWith(expect.objectContaining({ method: "status" }));
|
||||
expect(findExtraGatewayServices).toHaveBeenCalled();
|
||||
expect(inspectPortUsage).toHaveBeenCalled();
|
||||
}, 20_000);
|
||||
});
|
||||
|
||||
it("derives probe URL from service args + env (json)", async () => {
|
||||
resetRuntimeCapture();
|
||||
@@ -162,7 +162,7 @@ describe("daemon-cli coverage", () => {
|
||||
expect(parsed.config?.mismatch).toBe(true);
|
||||
expect(parsed.rpc?.url).toBe("ws://127.0.0.1:19001");
|
||||
expect(parsed.rpc?.ok).toBe(true);
|
||||
}, 20_000);
|
||||
});
|
||||
|
||||
it("passes deep scan flag for daemon status", async () => {
|
||||
findExtraGatewayServices.mockClear();
|
||||
|
||||
@@ -143,9 +143,8 @@ describe("gateway-cli coverage", () => {
|
||||
|
||||
expect(discoverGatewayBeacons).toHaveBeenCalledTimes(1);
|
||||
const out = runtimeLogs.join("\n");
|
||||
for (const text of ['"beacons"', '"wsUrl"', "ws://"]) {
|
||||
expect(out).toContain(text);
|
||||
}
|
||||
expect(out).toContain('"beacons"');
|
||||
expect(out).toContain("ws://");
|
||||
});
|
||||
|
||||
it("validates gateway discover timeout", async () => {
|
||||
|
||||
@@ -30,7 +30,7 @@ describe("doctor command", () => {
|
||||
const stateNote = note.mock.calls.find((call) => call[1] === "State integrity");
|
||||
expect(stateNote).toBeTruthy();
|
||||
expect(String(stateNote?.[0])).toContain("CRITICAL");
|
||||
}, 30_000);
|
||||
});
|
||||
|
||||
it("warns about opencode provider overrides", async () => {
|
||||
mockDoctorConfigSnapshot({
|
||||
|
||||
@@ -66,7 +66,7 @@ async function removeDirWithRetry(dir: string): Promise<void> {
|
||||
if (!isTransient || attempt === 4) {
|
||||
throw error;
|
||||
}
|
||||
await delay(25 * (attempt + 1));
|
||||
await delay(10 * (attempt + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,7 +189,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
key: "sk-minimax-test",
|
||||
});
|
||||
});
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("supports MiniMax CN API endpoint auth choice", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-minimax-cn-", async (env) => {
|
||||
@@ -208,7 +208,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
key: "sk-minimax-test",
|
||||
});
|
||||
});
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("stores Z.AI API key and uses global baseUrl by default", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-zai-", async (env) => {
|
||||
@@ -223,7 +223,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
expect(cfg.agents?.defaults?.model?.primary).toBe("zai/glm-5");
|
||||
await expectApiKeyProfile({ profileId: "zai:default", provider: "zai", key: "zai-test-key" });
|
||||
});
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("supports Z.AI CN coding endpoint auth choice", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-zai-cn-", async (env) => {
|
||||
@@ -236,7 +236,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
"https://open.bigmodel.cn/api/coding/paas/v4",
|
||||
);
|
||||
});
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("stores xAI API key and sets default model", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-xai-", async (env) => {
|
||||
@@ -251,7 +251,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
expect(cfg.agents?.defaults?.model?.primary).toBe("xai/grok-4");
|
||||
await expectApiKeyProfile({ profileId: "xai:default", provider: "xai", key: "xai-test-key" });
|
||||
});
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("infers Mistral auth choice from --mistral-api-key and sets default model", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-mistral-infer-", async (env) => {
|
||||
@@ -268,7 +268,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
key: "mistral-test-key",
|
||||
});
|
||||
});
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("stores Volcano Engine API key and sets default model", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-volcengine-", async (env) => {
|
||||
@@ -279,7 +279,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
|
||||
expect(cfg.agents?.defaults?.model?.primary).toBe("volcengine-plan/ark-code-latest");
|
||||
});
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("infers BytePlus auth choice from --byteplus-api-key and sets default model", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-byteplus-infer-", async (env) => {
|
||||
@@ -289,7 +289,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
|
||||
expect(cfg.agents?.defaults?.model?.primary).toBe("byteplus-plan/ark-code-latest");
|
||||
});
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("stores Vercel AI Gateway API key and sets default model", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-ai-gateway-", async (env) => {
|
||||
@@ -309,7 +309,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
key: "gateway-test-key",
|
||||
});
|
||||
});
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("stores token auth profile", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-token-", async ({ configPath, runtime }) => {
|
||||
@@ -336,7 +336,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
expect(profile.token).toBe(cleanToken);
|
||||
}
|
||||
});
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("stores OpenAI API key and sets OpenAI default model", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-openai-", async (env) => {
|
||||
@@ -347,7 +347,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
|
||||
expect(cfg.agents?.defaults?.model?.primary).toBe(OPENAI_DEFAULT_MODEL);
|
||||
});
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("rejects vLLM auth choice in non-interactive mode", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-vllm-non-interactive-", async ({ runtime }) => {
|
||||
@@ -358,7 +358,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
}),
|
||||
).rejects.toThrow('Auth choice "vllm" requires interactive mode.');
|
||||
});
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("stores LiteLLM API key and sets default model", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-litellm-", async (env) => {
|
||||
@@ -376,7 +376,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
key: "litellm-test-key",
|
||||
});
|
||||
});
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
@@ -391,37 +391,31 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
prefix: "openclaw-onboard-cf-gateway-infer-",
|
||||
options: {},
|
||||
},
|
||||
])(
|
||||
"$name",
|
||||
async ({ prefix, options }) => {
|
||||
await withOnboardEnv(prefix, async ({ configPath, runtime }) => {
|
||||
await runNonInteractiveOnboardingWithDefaults(runtime, {
|
||||
cloudflareAiGatewayAccountId: "cf-account-id",
|
||||
cloudflareAiGatewayGatewayId: "cf-gateway-id",
|
||||
cloudflareAiGatewayApiKey: "cf-gateway-test-key",
|
||||
skipSkills: true,
|
||||
...options,
|
||||
});
|
||||
|
||||
const cfg = await readJsonFile<ProviderAuthConfigSnapshot>(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" },
|
||||
});
|
||||
])("$name", async ({ prefix, options }) => {
|
||||
await withOnboardEnv(prefix, async ({ configPath, runtime }) => {
|
||||
await runNonInteractiveOnboardingWithDefaults(runtime, {
|
||||
cloudflareAiGatewayAccountId: "cf-account-id",
|
||||
cloudflareAiGatewayGatewayId: "cf-gateway-id",
|
||||
cloudflareAiGatewayApiKey: "cf-gateway-test-key",
|
||||
skipSkills: true,
|
||||
...options,
|
||||
});
|
||||
},
|
||||
60_000,
|
||||
);
|
||||
|
||||
const cfg = await readJsonFile<ProviderAuthConfigSnapshot>(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" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("infers Together auth choice from --together-api-key and sets default model", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-together-infer-", async (env) => {
|
||||
@@ -438,7 +432,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
key: "together-test-key",
|
||||
});
|
||||
});
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("infers QIANFAN auth choice from --qianfan-api-key and sets default model", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-qianfan-infer-", async (env) => {
|
||||
@@ -455,7 +449,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
key: "qianfan-test-key",
|
||||
});
|
||||
});
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("configures a custom provider from non-interactive flags", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-custom-provider-", async ({ configPath, runtime }) => {
|
||||
@@ -477,7 +471,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
expect(provider?.models?.some((model) => model.id === "foo-large")).toBe(true);
|
||||
expect(cfg.agents?.defaults?.model?.primary).toBe("custom-llm-example-com/foo-large");
|
||||
});
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("infers custom provider auth choice from custom flags", async () => {
|
||||
await withOnboardEnv(
|
||||
@@ -501,7 +495,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
expect(cfg.agents?.defaults?.model?.primary).toBe("custom-models-custom-local/local-large");
|
||||
},
|
||||
);
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("uses CUSTOM_API_KEY env fallback for non-interactive custom provider auth", async () => {
|
||||
await withOnboardEnv(
|
||||
@@ -512,7 +506,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
expect(await readCustomLocalProviderApiKey(configPath)).toBe("custom-env-key");
|
||||
},
|
||||
);
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("uses matching profile fallback for non-interactive custom provider auth", async () => {
|
||||
await withOnboardEnv(
|
||||
@@ -530,7 +524,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
expect(await readCustomLocalProviderApiKey(configPath)).toBe("custom-profile-key");
|
||||
},
|
||||
);
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("fails custom provider auth when compatibility is invalid", async () => {
|
||||
await withOnboardEnv(
|
||||
@@ -547,7 +541,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
).rejects.toThrow('Invalid --custom-compatibility (use "openai" or "anthropic").');
|
||||
},
|
||||
);
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("fails custom provider auth when explicit provider id is invalid", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-custom-provider-invalid-id-", async ({ runtime }) => {
|
||||
@@ -563,7 +557,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
"Invalid custom provider config: Custom provider ID must include letters, numbers, or hyphens.",
|
||||
);
|
||||
});
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("fails inferred custom auth when required flags are incomplete", async () => {
|
||||
await withOnboardEnv(
|
||||
@@ -577,5 +571,5 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
).rejects.toThrow('Auth choice "custom-api-key" requires a base URL and model ID.');
|
||||
},
|
||||
);
|
||||
}, 60_000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ async function waitForPersistedSecret(configPath: string, expectedSecret: string
|
||||
if (parsed.commands?.ownerDisplaySecret === expectedSecret) {
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
}
|
||||
throw new Error("timed out waiting for ownerDisplaySecret persistence");
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ function expectNoFinding(res: SecurityAuditReport, checkId: string): void {
|
||||
describe("security audit", () => {
|
||||
let fixtureRoot = "";
|
||||
let caseId = 0;
|
||||
let channelSecurityStateDir = "";
|
||||
|
||||
const makeTmpDir = async (label: string) => {
|
||||
const dir = path.join(fixtureRoot, `case-${caseId++}-${label}`);
|
||||
@@ -110,14 +111,23 @@ describe("security audit", () => {
|
||||
return dir;
|
||||
};
|
||||
|
||||
const withStateDir = async (label: string, fn: (tmp: string) => Promise<void>) => {
|
||||
const tmp = await makeTmpDir(label);
|
||||
await fs.mkdir(path.join(tmp, "credentials"), { recursive: true, mode: 0o700 });
|
||||
await withEnvAsync({ OPENCLAW_STATE_DIR: tmp }, async () => await fn(tmp));
|
||||
const withChannelSecurityStateDir = async (fn: (tmp: string) => Promise<void>) => {
|
||||
const credentialsDir = path.join(channelSecurityStateDir, "credentials");
|
||||
await fs.rm(credentialsDir, { recursive: true, force: true });
|
||||
await fs.mkdir(credentialsDir, { recursive: true, mode: 0o700 });
|
||||
await withEnvAsync(
|
||||
{ OPENCLAW_STATE_DIR: channelSecurityStateDir },
|
||||
async () => await fn(channelSecurityStateDir),
|
||||
);
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-security-audit-"));
|
||||
channelSecurityStateDir = path.join(fixtureRoot, "channel-security");
|
||||
await fs.mkdir(path.join(channelSecurityStateDir, "credentials"), {
|
||||
recursive: true,
|
||||
mode: 0o700,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -1367,7 +1377,7 @@ describe("security audit", () => {
|
||||
});
|
||||
|
||||
it("flags Discord native commands without a guild user allowlist", async () => {
|
||||
await withStateDir("discord", async () => {
|
||||
await withChannelSecurityStateDir(async () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
discord: {
|
||||
@@ -1404,7 +1414,7 @@ describe("security audit", () => {
|
||||
});
|
||||
|
||||
it("does not flag Discord slash commands when dm.allowFrom includes a Discord snowflake id", async () => {
|
||||
await withStateDir("discord-allowfrom-snowflake", async () => {
|
||||
await withChannelSecurityStateDir(async () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
discord: {
|
||||
@@ -1441,7 +1451,7 @@ describe("security audit", () => {
|
||||
});
|
||||
|
||||
it("warns when Discord allowlists contain name-based entries", async () => {
|
||||
await withStateDir("discord-name-based-allowlist", async (tmp) => {
|
||||
await withChannelSecurityStateDir(async (tmp) => {
|
||||
await fs.writeFile(
|
||||
path.join(tmp, "credentials", "discord-allowFrom.json"),
|
||||
JSON.stringify({ version: 1, allowFrom: ["team.owner"] }),
|
||||
@@ -1491,7 +1501,7 @@ describe("security audit", () => {
|
||||
});
|
||||
|
||||
it("does not warn when Discord allowlists use ID-style entries only", async () => {
|
||||
await withStateDir("discord-id-only-allowlist", async () => {
|
||||
await withChannelSecurityStateDir(async () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
discord: {
|
||||
@@ -1534,7 +1544,7 @@ describe("security audit", () => {
|
||||
});
|
||||
|
||||
it("flags Discord slash commands when access-group enforcement is disabled and no users allowlist exists", async () => {
|
||||
await withStateDir("discord-open", async () => {
|
||||
await withChannelSecurityStateDir(async () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
commands: { useAccessGroups: false },
|
||||
channels: {
|
||||
@@ -1572,7 +1582,7 @@ describe("security audit", () => {
|
||||
});
|
||||
|
||||
it("flags Slack slash commands without a channel users allowlist", async () => {
|
||||
await withStateDir("slack", async () => {
|
||||
await withChannelSecurityStateDir(async () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
slack: {
|
||||
@@ -1604,7 +1614,7 @@ describe("security audit", () => {
|
||||
});
|
||||
|
||||
it("flags Slack slash commands when access-group enforcement is disabled", async () => {
|
||||
await withStateDir("slack-open", async () => {
|
||||
await withChannelSecurityStateDir(async () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
commands: { useAccessGroups: false },
|
||||
channels: {
|
||||
@@ -1637,7 +1647,7 @@ describe("security audit", () => {
|
||||
});
|
||||
|
||||
it("flags Telegram group commands without a sender allowlist", async () => {
|
||||
await withStateDir("telegram", async () => {
|
||||
await withChannelSecurityStateDir(async () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
telegram: {
|
||||
@@ -1668,7 +1678,7 @@ describe("security audit", () => {
|
||||
});
|
||||
|
||||
it("warns when Telegram allowFrom entries are non-numeric (legacy @username configs)", async () => {
|
||||
await withStateDir("telegram-invalid-allowfrom", async () => {
|
||||
await withChannelSecurityStateDir(async () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
telegram: {
|
||||
|
||||
Reference in New Issue
Block a user