From 656121a12be79193b8085f03790416b7de1acb8f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 2 Mar 2026 05:30:25 +0000 Subject: [PATCH] test: micro-optimize hot unit test files --- ...rns-sandbox-enabled-without-docker.test.ts | 7 ++ .../config.meta-timestamp-coercion.test.ts | 28 ++++---- src/config/schema.tags.test.ts | 46 ------------- src/config/schema.test.ts | 42 ++++++++++++ src/line/monitor.lifecycle.test.ts | 47 +++++++++++++ src/process/exec.test.ts | 12 ++-- src/secrets/resolve.test.ts | 68 +++++++++---------- 7 files changed, 146 insertions(+), 104 deletions(-) delete mode 100644 src/config/schema.tags.test.ts diff --git a/src/commands/doctor-sandbox.warns-sandbox-enabled-without-docker.test.ts b/src/commands/doctor-sandbox.warns-sandbox-enabled-without-docker.test.ts index 106066c511a..50217c5d8cb 100644 --- a/src/commands/doctor-sandbox.warns-sandbox-enabled-without-docker.test.ts +++ b/src/commands/doctor-sandbox.warns-sandbox-enabled-without-docker.test.ts @@ -11,6 +11,13 @@ vi.mock("../process/exec.js", () => ({ runCommandWithTimeout: vi.fn(), })); +vi.mock("../agents/sandbox.js", () => ({ + DEFAULT_SANDBOX_BROWSER_IMAGE: "browser-image", + DEFAULT_SANDBOX_COMMON_IMAGE: "common-image", + DEFAULT_SANDBOX_IMAGE: "default-image", + resolveSandboxScope: vi.fn(() => "shared"), +})); + vi.mock("../terminal/note.js", () => ({ note, })); diff --git a/src/config/config.meta-timestamp-coercion.test.ts b/src/config/config.meta-timestamp-coercion.test.ts index d87b16b451e..2fc75d1972c 100644 --- a/src/config/config.meta-timestamp-coercion.test.ts +++ b/src/config/config.meta-timestamp-coercion.test.ts @@ -1,9 +1,13 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeAll, describe, expect, it } from "vitest"; + +let validateConfigObject: typeof import("./config.js").validateConfigObject; + +beforeAll(async () => { + ({ validateConfigObject } = await import("./config.js")); +}); describe("meta.lastTouchedAt numeric timestamp coercion", () => { - it("accepts a numeric Unix timestamp and coerces it to an ISO string", async () => { - vi.resetModules(); - const { validateConfigObject } = await import("./config.js"); + it("accepts a numeric Unix timestamp and coerces it to an ISO string", () => { const numericTimestamp = 1770394758161; const res = validateConfigObject({ meta: { @@ -17,9 +21,7 @@ describe("meta.lastTouchedAt numeric timestamp coercion", () => { } }); - it("still accepts a string ISO timestamp unchanged", async () => { - vi.resetModules(); - const { validateConfigObject } = await import("./config.js"); + it("still accepts a string ISO timestamp unchanged", () => { const isoTimestamp = "2026-02-07T01:39:18.161Z"; const res = validateConfigObject({ meta: { @@ -32,9 +34,7 @@ describe("meta.lastTouchedAt numeric timestamp coercion", () => { } }); - it("rejects out-of-range numeric timestamps without throwing", async () => { - vi.resetModules(); - const { validateConfigObject } = await import("./config.js"); + it("rejects out-of-range numeric timestamps without throwing", () => { const res = validateConfigObject({ meta: { lastTouchedAt: 1e20, @@ -43,9 +43,7 @@ describe("meta.lastTouchedAt numeric timestamp coercion", () => { expect(res.ok).toBe(false); }); - it("passes non-date strings through unchanged (backwards-compatible)", async () => { - vi.resetModules(); - const { validateConfigObject } = await import("./config.js"); + it("passes non-date strings through unchanged (backwards-compatible)", () => { const res = validateConfigObject({ meta: { lastTouchedAt: "not-a-date", @@ -57,9 +55,7 @@ describe("meta.lastTouchedAt numeric timestamp coercion", () => { } }); - it("accepts meta with only lastTouchedVersion (no lastTouchedAt)", async () => { - vi.resetModules(); - const { validateConfigObject } = await import("./config.js"); + it("accepts meta with only lastTouchedVersion (no lastTouchedAt)", () => { const res = validateConfigObject({ meta: { lastTouchedVersion: "2026.2.6", diff --git a/src/config/schema.tags.test.ts b/src/config/schema.tags.test.ts deleted file mode 100644 index 5dd0e5d745d..00000000000 --- a/src/config/schema.tags.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { buildConfigSchema } from "./schema.js"; -import { applyDerivedTags, CONFIG_TAGS, deriveTagsForPath } from "./schema.tags.js"; - -describe("config schema tags", () => { - it("derives security/auth tags for credential paths", () => { - const tags = deriveTagsForPath("gateway.auth.token"); - expect(tags).toContain("security"); - expect(tags).toContain("auth"); - }); - - it("derives tools/performance tags for web fetch timeout paths", () => { - const tags = deriveTagsForPath("tools.web.fetch.timeoutSeconds"); - expect(tags).toContain("tools"); - expect(tags).toContain("performance"); - }); - - it("keeps tags in the allowed taxonomy", () => { - const withTags = applyDerivedTags({ - "gateway.auth.token": {}, - "tools.web.fetch.timeoutSeconds": {}, - "channels.slack.accounts.*.token": {}, - }); - const allowed = new Set(CONFIG_TAGS); - for (const hint of Object.values(withTags)) { - for (const tag of hint.tags ?? []) { - expect(allowed.has(tag)).toBe(true); - } - } - }); - - it("covers core/built-in config paths with tags", () => { - const schema = buildConfigSchema(); - const allowed = new Set(CONFIG_TAGS); - for (const [key, hint] of Object.entries(schema.uiHints)) { - if (!key.includes(".")) { - continue; - } - const tags = hint.tags ?? []; - expect(tags.length, `expected tags for ${key}`).toBeGreaterThan(0); - for (const tag of tags) { - expect(allowed.has(tag), `unexpected tag ${tag} on ${key}`).toBe(true); - } - } - }); -}); diff --git a/src/config/schema.test.ts b/src/config/schema.test.ts index 804286219ac..eaabe2841b1 100644 --- a/src/config/schema.test.ts +++ b/src/config/schema.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { buildConfigSchema } from "./schema.js"; +import { applyDerivedTags, CONFIG_TAGS, deriveTagsForPath } from "./schema.tags.js"; describe("config schema", () => { it("exports schema + hints", () => { @@ -119,4 +120,45 @@ describe("config schema", () => { expect(defaultsHint?.help).toContain("last"); expect(listHint?.help).toContain("bluebubbles"); }); + + it("derives security/auth tags for credential paths", () => { + const tags = deriveTagsForPath("gateway.auth.token"); + expect(tags).toContain("security"); + expect(tags).toContain("auth"); + }); + + it("derives tools/performance tags for web fetch timeout paths", () => { + const tags = deriveTagsForPath("tools.web.fetch.timeoutSeconds"); + expect(tags).toContain("tools"); + expect(tags).toContain("performance"); + }); + + it("keeps tags in the allowed taxonomy", () => { + const withTags = applyDerivedTags({ + "gateway.auth.token": {}, + "tools.web.fetch.timeoutSeconds": {}, + "channels.slack.accounts.*.token": {}, + }); + const allowed = new Set(CONFIG_TAGS); + for (const hint of Object.values(withTags)) { + for (const tag of hint.tags ?? []) { + expect(allowed.has(tag)).toBe(true); + } + } + }); + + it("covers core/built-in config paths with tags", () => { + const schema = buildConfigSchema(); + const allowed = new Set(CONFIG_TAGS); + for (const [key, hint] of Object.entries(schema.uiHints)) { + if (!key.includes(".")) { + continue; + } + const tags = hint.tags ?? []; + expect(tags.length, `expected tags for ${key}`).toBeGreaterThan(0); + for (const tag of tags) { + expect(allowed.has(tag), `unexpected tag ${tag} on ${key}`).toBe(true); + } + } + }); }); diff --git a/src/line/monitor.lifecycle.test.ts b/src/line/monitor.lifecycle.test.ts index 635d921e7ad..f1d23a08f99 100644 --- a/src/line/monitor.lifecycle.test.ts +++ b/src/line/monitor.lifecycle.test.ts @@ -15,6 +15,23 @@ vi.mock("./bot.js", () => ({ createLineBot: createLineBotMock, })); +vi.mock("../auto-reply/chunk.js", () => ({ + chunkMarkdownText: vi.fn(), +})); + +vi.mock("../auto-reply/reply/provider-dispatcher.js", () => ({ + dispatchReplyWithBufferedBlockDispatcher: vi.fn(), +})); + +vi.mock("../channels/reply-prefix.js", () => ({ + createReplyPrefixOptions: vi.fn(() => ({})), +})); + +vi.mock("../globals.js", () => ({ + danger: (value: unknown) => String(value), + logVerbose: vi.fn(), +})); + vi.mock("../plugins/http-path.js", () => ({ normalizePluginHttpPath: (_path: string | undefined, fallback: string) => fallback, })); @@ -27,6 +44,36 @@ vi.mock("./webhook-node.js", () => ({ createLineNodeWebhookHandler: vi.fn(() => vi.fn()), })); +vi.mock("./auto-reply-delivery.js", () => ({ + deliverLineAutoReply: vi.fn(), +})); + +vi.mock("./markdown-to-line.js", () => ({ + processLineMessage: vi.fn(), +})); + +vi.mock("./reply-chunks.js", () => ({ + sendLineReplyChunks: vi.fn(), +})); + +vi.mock("./send.js", () => ({ + createFlexMessage: vi.fn(), + createImageMessage: vi.fn(), + createLocationMessage: vi.fn(), + createQuickReplyItems: vi.fn(), + createTextMessageWithQuickReplies: vi.fn(), + getUserDisplayName: vi.fn(), + pushMessageLine: vi.fn(), + pushMessagesLine: vi.fn(), + pushTextMessageWithQuickReplies: vi.fn(), + replyMessageLine: vi.fn(), + showLoadingAnimation: vi.fn(), +})); + +vi.mock("./template-messages.js", () => ({ + buildTemplateMessageFromPayload: vi.fn(), +})); + describe("monitorLineProvider lifecycle", () => { beforeEach(() => { createLineBotMock.mockClear(); diff --git a/src/process/exec.test.ts b/src/process/exec.test.ts index 831cd4925fc..f76d9137f3b 100644 --- a/src/process/exec.test.ts +++ b/src/process/exec.test.ts @@ -101,24 +101,24 @@ describe("runCommandWithTimeout", () => { "let count = 0;", 'const ticker = setInterval(() => { process.stdout.write(".");', "count += 1;", - "if (count === 10) {", + "if (count === 6) {", "clearInterval(ticker);", "process.exit(0);", "}", - "}, 100);", + "}, 25);", ].join(" "), ], { - timeoutMs: 10_000, - // Extra headroom for busy CI workers while still validating timer resets. - noOutputTimeoutMs: 2_500, + timeoutMs: 3_000, + // Keep a healthy margin above the emit interval while avoiding a 1s+ test delay. + noOutputTimeoutMs: 400, }, ); expect(result.code ?? 0).toBe(0); expect(result.termination).toBe("exit"); expect(result.noOutputTimedOut).toBe(false); - expect(result.stdout.length).toBeGreaterThanOrEqual(11); + expect(result.stdout.length).toBeGreaterThanOrEqual(7); }); it("reports global timeout termination when overall timeout elapses", async () => { diff --git a/src/secrets/resolve.test.ts b/src/secrets/resolve.test.ts index 0c9119cb947..62769bcb927 100644 --- a/src/secrets/resolve.test.ts +++ b/src/secrets/resolve.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { resolveSecretRefString, resolveSecretRefValue } from "./resolve.js"; @@ -12,17 +12,28 @@ async function writeSecureFile(filePath: string, content: string, mode = 0o600): } describe("secret ref resolver", () => { - const cleanupRoots: string[] = []; + let fixtureRoot = ""; + let caseId = 0; + + const createCaseDir = async (label: string): Promise => { + const dir = path.join(fixtureRoot, `${label}-${caseId++}`); + await fs.mkdir(dir, { recursive: true }); + return dir; + }; + + beforeAll(async () => { + fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-")); + }); afterEach(async () => { vi.restoreAllMocks(); - while (cleanupRoots.length > 0) { - const root = cleanupRoots.pop(); - if (!root) { - continue; - } - await fs.rm(root, { recursive: true, force: true }); + }); + + afterAll(async () => { + if (!fixtureRoot) { + return; } + await fs.rm(fixtureRoot, { recursive: true, force: true }); }); it("resolves env refs via implicit default env provider", async () => { @@ -41,8 +52,7 @@ describe("secret ref resolver", () => { if (process.platform === "win32") { return; } - const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-file-")); - cleanupRoots.push(root); + const root = await createCaseDir("file"); const filePath = path.join(root, "secrets.json"); await writeSecureFile( filePath, @@ -78,8 +88,7 @@ describe("secret ref resolver", () => { if (process.platform === "win32") { return; } - const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-exec-")); - cleanupRoots.push(root); + const root = await createCaseDir("exec"); const scriptPath = path.join(root, "resolver.mjs"); await writeSecureFile( scriptPath, @@ -116,8 +125,7 @@ describe("secret ref resolver", () => { if (process.platform === "win32") { return; } - const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-exec-plain-")); - cleanupRoots.push(root); + const root = await createCaseDir("exec-plain"); const scriptPath = path.join(root, "resolver-plain.mjs"); await writeSecureFile( scriptPath, @@ -149,8 +157,7 @@ describe("secret ref resolver", () => { if (process.platform === "win32") { return; } - const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-exec-link-")); - cleanupRoots.push(root); + const root = await createCaseDir("exec-link-reject"); const scriptPath = path.join(root, "resolver-target.mjs"); const symlinkPath = path.join(root, "resolver-link.mjs"); await writeSecureFile( @@ -185,8 +192,7 @@ describe("secret ref resolver", () => { if (process.platform === "win32") { return; } - const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-exec-link-")); - cleanupRoots.push(root); + const root = await createCaseDir("exec-link-allow"); const scriptPath = path.join(root, "resolver-target.mjs"); const symlinkPath = path.join(root, "resolver-link.mjs"); await writeSecureFile( @@ -224,8 +230,7 @@ describe("secret ref resolver", () => { return; } - const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-homebrew-")); - cleanupRoots.push(root); + const root = await createCaseDir("homebrew"); const binDir = path.join(root, "opt", "homebrew", "bin"); const cellarDir = path.join(root, "opt", "homebrew", "Cellar", "node", "25.0.0", "bin"); await fs.mkdir(binDir, { recursive: true }); @@ -293,10 +298,8 @@ describe("secret ref resolver", () => { if (process.platform === "win32") { return; } - const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-exec-link-")); - const outside = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-exec-out-")); - cleanupRoots.push(root); - cleanupRoots.push(outside); + const root = await createCaseDir("exec-link-trusted"); + const outside = await createCaseDir("exec-outside"); const scriptPath = path.join(outside, "resolver-target.mjs"); const symlinkPath = path.join(root, "resolver-link.mjs"); await writeSecureFile( @@ -333,10 +336,7 @@ describe("secret ref resolver", () => { if (process.platform === "win32") { return; } - const root = await fs.mkdtemp( - path.join(os.tmpdir(), "openclaw-secrets-resolve-exec-protocol-"), - ); - cleanupRoots.push(root); + const root = await createCaseDir("exec-protocol"); const scriptPath = path.join(root, "resolver-protocol.mjs"); await writeSecureFile( scriptPath, @@ -371,8 +371,7 @@ describe("secret ref resolver", () => { if (process.platform === "win32") { return; } - const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-exec-id-")); - cleanupRoots.push(root); + const root = await createCaseDir("exec-missing-id"); const scriptPath = path.join(root, "resolver-missing-id.mjs"); await writeSecureFile( scriptPath, @@ -407,8 +406,7 @@ describe("secret ref resolver", () => { if (process.platform === "win32") { return; } - const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-exec-json-")); - cleanupRoots.push(root); + const root = await createCaseDir("exec-invalid-json"); const scriptPath = path.join(root, "resolver-invalid-json.mjs"); await writeSecureFile( scriptPath, @@ -441,8 +439,7 @@ describe("secret ref resolver", () => { if (process.platform === "win32") { return; } - const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-single-value-")); - cleanupRoots.push(root); + const root = await createCaseDir("file-single-value"); const filePath = path.join(root, "token.txt"); await writeSecureFile(filePath, "raw-token-value\n"); @@ -469,8 +466,7 @@ describe("secret ref resolver", () => { if (process.platform === "win32") { return; } - const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-timeout-")); - cleanupRoots.push(root); + const root = await createCaseDir("file-timeout"); const filePath = path.join(root, "secrets.json"); await writeSecureFile( filePath,