From ba7dccc49df2b691657ab8881ad468d82dc4b00e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Feb 2026 04:30:39 +0000 Subject: [PATCH] test: speed up test suite and trim redundant onboarding tests --- scripts/test-parallel.mjs | 96 +++++++++++++++---- ...oard-non-interactive.provider-auth.test.ts | 8 +- .../onboard-non-interactive.token.test.ts | 93 ------------------ .../onboard-non-interactive.xai.test.ts | 91 ------------------ src/wizard/onboarding.test.ts | 5 + 5 files changed, 90 insertions(+), 203 deletions(-) delete mode 100644 src/commands/onboard-non-interactive.token.test.ts delete mode 100644 src/commands/onboard-non-interactive.xai.test.ts diff --git a/scripts/test-parallel.mjs b/scripts/test-parallel.mjs index 646c57c609a..218196630cc 100644 --- a/scripts/test-parallel.mjs +++ b/scripts/test-parallel.mjs @@ -5,19 +5,24 @@ import path from "node:path"; const pnpm = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; -const runs = [ - { - name: "unit", - args: ["vitest", "run", "--config", "vitest.unit.config.ts"], - }, - { - name: "extensions", - args: ["vitest", "run", "--config", "vitest.extensions.config.ts"], - }, - { - name: "gateway", - args: ["vitest", "run", "--config", "vitest.gateway.config.ts"], - }, +const unitIsolatedFiles = [ + "src/plugins/loader.test.ts", + "src/plugins/tools.optional.test.ts", + "src/agents/session-tool-result-guard.tool-result-persist-hook.test.ts", + "src/security/fix.test.ts", + "src/utils.test.ts", + "src/auto-reply/tool-meta.test.ts", + "src/commands/auth-choice.test.ts", + "src/media/store.header-ext.test.ts", + "src/browser/server.covers-additional-endpoint-branches.test.ts", + "src/browser/server.post-tabs-open-profile-unknown-returns-404.test.ts", + "src/browser/server.agent-contract-snapshot-endpoints.test.ts", + "src/browser/server.agent-contract-form-layout-act-commands.test.ts", + "src/browser/server.serves-status-starts-browser-requested.test.ts", + "src/browser/server.skips-default-maxchars-explicitly-set-zero.test.ts", + "src/browser/server.auth-token-gates-http.test.ts", + "src/browser/server-context.remote-tab-ops.test.ts", + "src/browser/server-context.ensure-tab-available.prefers-last-target.test.ts", ]; const children = new Set(); @@ -25,6 +30,62 @@ const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true"; const isMacOS = process.platform === "darwin" || process.env.RUNNER_OS === "macOS"; const isWindows = process.platform === "win32" || process.env.RUNNER_OS === "Windows"; const isWindowsCi = isCI && isWindows; +const useVmForks = + process.env.OPENCLAW_TEST_VM_FORKS === "1" || + (process.env.OPENCLAW_TEST_VM_FORKS !== "0" && !isWindows); +const runs = [ + ...(useVmForks + ? [ + { + name: "unit-fast", + args: [ + "vitest", + "run", + "--config", + "vitest.unit.config.ts", + "--pool=vmForks", + ...unitIsolatedFiles.flatMap((file) => ["--exclude", file]), + ], + }, + { + name: "unit-isolated", + args: [ + "vitest", + "run", + "--config", + "vitest.unit.config.ts", + "--pool=forks", + ...unitIsolatedFiles, + ], + }, + ] + : [ + { + name: "unit", + args: ["vitest", "run", "--config", "vitest.unit.config.ts"], + }, + ]), + { + name: "extensions", + args: [ + "vitest", + "run", + "--config", + "vitest.extensions.config.ts", + ...(useVmForks ? ["--pool=vmForks"] : []), + ], + }, + { + name: "gateway", + args: [ + "vitest", + "run", + "--config", + "vitest.gateway.config.ts", + ...(useVmForks ? ["--pool=vmForks"] : []), + ], + }, +]; const shardOverride = Number.parseInt(process.env.OPENCLAW_TEST_SHARDS ?? "", 10); const shardCount = isWindowsCi ? Number.isFinite(shardOverride) && shardOverride > 1 @@ -40,12 +101,12 @@ const passthroughArgs = const overrideWorkers = Number.parseInt(process.env.OPENCLAW_TEST_WORKERS ?? "", 10); const resolvedOverride = Number.isFinite(overrideWorkers) && overrideWorkers > 0 ? overrideWorkers : null; -// Keep gateway serial by default to avoid resource contention with unit/extensions. -// Allow explicit opt-in parallel runs on non-Windows CI/local when requested. +// Keep gateway serial on Windows CI and CI by default; run in parallel locally +// for lower wall-clock time. CI can opt in via OPENCLAW_TEST_PARALLEL_GATEWAY=1. const keepGatewaySerial = isWindowsCi || process.env.OPENCLAW_TEST_SERIAL_GATEWAY === "1" || - process.env.OPENCLAW_TEST_PARALLEL_GATEWAY !== "1"; + (isCI && process.env.OPENCLAW_TEST_PARALLEL_GATEWAY !== "1"); const parallelRuns = keepGatewaySerial ? runs.filter((entry) => entry.name !== "gateway") : runs; const serialRuns = keepGatewaySerial ? runs.filter((entry) => entry.name === "gateway") : []; const localWorkers = Math.max(4, Math.min(16, os.cpus().length)); @@ -65,6 +126,9 @@ const maxWorkersForRun = (name) => { if (isCI && isMacOS) { return 1; } + if (name === "unit-isolated") { + return 1; + } if (name === "extensions") { return defaultExtensionsWorkers; } diff --git a/src/commands/onboard-non-interactive.provider-auth.test.ts b/src/commands/onboard-non-interactive.provider-auth.test.ts index 692320eb1fa..6a88c86685f 100644 --- a/src/commands/onboard-non-interactive.provider-auth.test.ts +++ b/src/commands/onboard-non-interactive.provider-auth.test.ts @@ -212,11 +212,12 @@ describe("onboard (non-interactive): provider auth", () => { it("stores xAI API key and sets default model", async () => { await withOnboardEnv("openclaw-onboard-xai-", async ({ configPath, runtime }) => { + const rawKey = "xai-test-\r\nkey"; await runNonInteractive( { nonInteractive: true, authChoice: "xai-api-key", - xaiApiKey: "xai-test-key", + xaiApiKey: rawKey, skipHealth: true, skipChannels: true, skipSkills: true, @@ -272,7 +273,8 @@ describe("onboard (non-interactive): provider auth", () => { it("stores token auth profile", async () => { await withOnboardEnv("openclaw-onboard-token-", async ({ configPath, runtime }) => { - const token = `sk-ant-oat01-${"a".repeat(80)}`; + const cleanToken = `sk-ant-oat01-${"a".repeat(80)}`; + const token = `${cleanToken.slice(0, 30)}\r${cleanToken.slice(30)}`; await runNonInteractive( { @@ -301,7 +303,7 @@ describe("onboard (non-interactive): provider auth", () => { expect(profile?.type).toBe("token"); if (profile?.type === "token") { expect(profile.provider).toBe("anthropic"); - expect(profile.token).toBe(token); + expect(profile.token).toBe(cleanToken); } }); }, 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 9c88b27c9f1..00000000000 --- a/src/commands/onboard-non-interactive.token.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): 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 cleanToken = `sk-ant-oat01-${"a".repeat(80)}`; - const token = `${cleanToken.slice(0, 30)}\r${cleanToken.slice(30)}`; - - 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(cleanToken); - } - } 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 84e70e653c4..00000000000 --- 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-\r\nkey", - 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/wizard/onboarding.test.ts b/src/wizard/onboarding.test.ts index 937f7b33cbf..2ace481d945 100644 --- a/src/wizard/onboarding.test.ts +++ b/src/wizard/onboarding.test.ts @@ -19,6 +19,7 @@ const ensureSystemdUserLingerInteractive = vi.hoisted(() => vi.fn(async () => {} const isSystemdUserServiceAvailable = vi.hoisted(() => vi.fn(async () => true)); const ensureControlUiAssetsBuilt = vi.hoisted(() => vi.fn(async () => ({ ok: true }))); const runTui = vi.hoisted(() => vi.fn(async () => {})); +const setupOnboardingShellCompletion = vi.hoisted(() => vi.fn(async () => {})); vi.mock("../commands/onboard-channels.js", () => ({ setupChannels, @@ -73,6 +74,10 @@ vi.mock("../tui/tui.js", () => ({ runTui, })); +vi.mock("./onboarding.completion.js", () => ({ + setupOnboardingShellCompletion, +})); + describe("runOnboardingWizard", () => { it("exits when config is invalid", async () => { readConfigFileSnapshot.mockResolvedValueOnce({