From e7db987ce6083dc34578b13da2899b5676f94327 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 10 Apr 2026 19:12:19 +0100 Subject: [PATCH] test: trim heavy imports and harden ci checks --- .../check-extension-package-tsc-boundary.mjs | 78 +++++++++++++++---- ...e-extension-package-boundary-artifacts.mjs | 2 + .../agent-command.live-model-switch.test.ts | 2 +- ...-lastused-no-explicit-order-exists.test.ts | 4 +- src/agents/model-auth-env.ts | 29 ++++++- src/agents/model-auth.profiles.test.ts | 28 +++++++ src/gateway/openresponses-http.test.ts | 18 +++-- .../server.auth.default-token.suite.ts | 2 +- .../server.chat.gateway-server-chat.test.ts | 2 +- ...server.node-invoke-approval-bypass.test.ts | 5 +- src/infra/install-package-dir.ts | 2 +- src/plugins/install-security-scan.runtime.ts | 75 ++++++++++-------- test/setup.shared.ts | 11 --- 13 files changed, 184 insertions(+), 74 deletions(-) diff --git a/scripts/check-extension-package-tsc-boundary.mjs b/scripts/check-extension-package-tsc-boundary.mjs index 293c3536a0..309cdd6e1c 100644 --- a/scripts/check-extension-package-tsc-boundary.mjs +++ b/scripts/check-extension-package-tsc-boundary.mjs @@ -532,6 +532,39 @@ export function resolveBoundaryCheckLockPath(rootDir = repoRoot) { return resolve(rootDir, "dist", ".extension-package-boundary.lock"); } +function resolveBoundaryCheckLockOwnerPath(lockPath) { + return join(lockPath, "owner.json"); +} + +function isProcessAlive(pid) { + if (!Number.isInteger(pid) || pid <= 0) { + return false; + } + try { + process.kill(pid, 0); + return true; + } catch (error) { + return Boolean(error && typeof error === "object" && "code" in error && error.code === "EPERM"); + } +} + +function removeStaleBoundaryCheckLock(lockPath) { + const ownerPath = resolveBoundaryCheckLockOwnerPath(lockPath); + let owner; + try { + owner = JSON.parse(readFileSync(ownerPath, "utf8")); + } catch { + rmSync(lockPath, { force: true, recursive: true }); + return true; + } + + if (owner && typeof owner === "object" && isProcessAlive(owner.pid)) { + return false; + } + rmSync(lockPath, { force: true, recursive: true }); + return true; +} + export function acquireBoundaryCheckLock(params = {}) { const rootDir = params.rootDir ?? repoRoot; const processObject = params.processObject ?? process; @@ -541,26 +574,37 @@ export function acquireBoundaryCheckLock(params = {}) { mkdirSync(lockPath); } catch (error) { if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") { - throw attachStepFailureMetadata( - new Error( - [ - "extension package boundary check", - "kind: lock-contention", - `lock: ${lockPath}`, - "another extension package boundary check is already running in this checkout", - ].join("\n\n"), - { cause: error }, - ), - "extension package boundary check", - { - kind: "lock-contention", - note: `lock: ${lockPath}\nanother extension package boundary check is already running in this checkout`, - }, - ); + if (removeStaleBoundaryCheckLock(lockPath)) { + mkdirSync(lockPath); + } else { + throw attachStepFailureMetadata( + new Error( + [ + "extension package boundary check", + "kind: lock-contention", + `lock: ${lockPath}`, + "another extension package boundary check is already running in this checkout", + ].join("\n\n"), + { cause: error }, + ), + "extension package boundary check", + { + kind: "lock-contention", + note: `lock: ${lockPath}\nanother extension package boundary check is already running in this checkout`, + }, + ); + } + } else { + throw error; } - throw error; } + writeFileSync( + resolveBoundaryCheckLockOwnerPath(lockPath), + `${JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }, null, 2)}\n`, + "utf8", + ); + const release = () => { rmSync(lockPath, { force: true, recursive: true }); }; diff --git a/scripts/prepare-extension-package-boundary-artifacts.mjs b/scripts/prepare-extension-package-boundary-artifacts.mjs index 50d77f4721..2b31d55daf 100644 --- a/scripts/prepare-extension-package-boundary-artifacts.mjs +++ b/scripts/prepare-extension-package-boundary-artifacts.mjs @@ -12,6 +12,7 @@ const VALID_MODES = new Set(["all", "package-boundary"]); const ROOT_DTS_INPUTS = [ "tsconfig.json", "tsconfig.plugin-sdk.dts.json", + "src/channels/plugins", "src/plugin-sdk", "src/video-generation/dashscope-compatible.ts", "src/video-generation/types.ts", @@ -20,6 +21,7 @@ const ROOT_DTS_INPUTS = [ const PACKAGE_DTS_INPUTS = [ "tsconfig.json", "packages/plugin-sdk/tsconfig.json", + "src/channels/plugins", "src/plugin-sdk", "src/video-generation/dashscope-compatible.ts", "src/video-generation/types.ts", diff --git a/src/agents/agent-command.live-model-switch.test.ts b/src/agents/agent-command.live-model-switch.test.ts index c20354dc22..68fe7d7560 100644 --- a/src/agents/agent-command.live-model-switch.test.ts +++ b/src/agents/agent-command.live-model-switch.test.ts @@ -168,7 +168,7 @@ vi.mock("../logging/subsystem.js", () => ({ vi.mock("../routing/session-key.js", () => ({ normalizeAgentId: (id: string) => id, - normalizeMainKey: (key?: string) => key ?? "main", + normalizeMainKey: (key?: string | null) => key?.trim() || "main", })); vi.mock("../runtime.js", () => ({ diff --git a/src/agents/auth-profiles.resolve-auth-profile-order.orders-by-lastused-no-explicit-order-exists.test.ts b/src/agents/auth-profiles.resolve-auth-profile-order.orders-by-lastused-no-explicit-order-exists.test.ts index 2842fb48e1..c9af0a6ac0 100644 --- a/src/agents/auth-profiles.resolve-auth-profile-order.orders-by-lastused-no-explicit-order-exists.test.ts +++ b/src/agents/auth-profiles.resolve-auth-profile-order.orders-by-lastused-no-explicit-order-exists.test.ts @@ -61,8 +61,8 @@ describe("resolveAuthProfileOrder", () => { }, usageStats: { "anthropic:ready": { lastUsed: 50 }, - "anthropic:cool1": { cooldownUntil: now + 5_000 }, - "anthropic:cool2": { cooldownUntil: now + 1_000 }, + "anthropic:cool1": { cooldownUntil: now + 120_000 }, + "anthropic:cool2": { cooldownUntil: now + 60_000 }, }, }, provider: "anthropic", diff --git a/src/agents/model-auth-env.ts b/src/agents/model-auth-env.ts index 192c391533..7f8c3c6b0d 100644 --- a/src/agents/model-auth-env.ts +++ b/src/agents/model-auth-env.ts @@ -1,4 +1,6 @@ -import { getEnvApiKey } from "@mariozechner/pi-ai"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { getShellEnvAppliedKeys } from "../infra/shell-env.js"; import { resolvePluginSetupProvider } from "../plugins/setup-registry.js"; import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js"; @@ -11,6 +13,29 @@ export type EnvApiKeyResult = { source: string; }; +function hasGoogleVertexAdcCredentials(env: NodeJS.ProcessEnv): boolean { + const explicitCredentialsPath = normalizeOptionalSecretInput(env.GOOGLE_APPLICATION_CREDENTIALS); + if (explicitCredentialsPath) { + return fs.existsSync(explicitCredentialsPath); + } + const homeDir = normalizeOptionalSecretInput(env.HOME) ?? os.homedir(); + return fs.existsSync( + path.join(homeDir, ".config", "gcloud", "application_default_credentials.json"), + ); +} + +function resolveGoogleVertexEnvApiKey(env: NodeJS.ProcessEnv): string | undefined { + const explicitApiKey = normalizeOptionalSecretInput(env.GOOGLE_CLOUD_API_KEY); + if (explicitApiKey) { + return explicitApiKey; + } + const hasProject = Boolean(env.GOOGLE_CLOUD_PROJECT || env.GCLOUD_PROJECT); + const hasLocation = Boolean(env.GOOGLE_CLOUD_LOCATION); + return hasProject && hasLocation && hasGoogleVertexAdcCredentials(env) + ? GCP_VERTEX_CREDENTIALS_MARKER + : undefined; +} + export function resolveEnvApiKey( provider: string, env: NodeJS.ProcessEnv = process.env, @@ -39,7 +64,7 @@ export function resolveEnvApiKey( } if (normalized === "google-vertex") { - const envKey = getEnvApiKey(normalized); + const envKey = resolveGoogleVertexEnvApiKey(env); if (!envKey) { return null; } diff --git a/src/agents/model-auth.profiles.test.ts b/src/agents/model-auth.profiles.test.ts index 8da301a5f6..6ce255a649 100644 --- a/src/agents/model-auth.profiles.test.ts +++ b/src/agents/model-auth.profiles.test.ts @@ -821,6 +821,34 @@ describe("getApiKeyForModel", () => { expect(resolved).toBeNull(); }); + it("resolveEnvApiKey('google-vertex') uses the provided env snapshot", async () => { + const resolved = resolveEnvApiKey("google-vertex", { + GOOGLE_CLOUD_API_KEY: "google-cloud-api-key", + } as NodeJS.ProcessEnv); + + expect(resolved?.apiKey).toBe("google-cloud-api-key"); + expect(resolved?.source).toBe("gcloud adc"); + }); + + it("resolveEnvApiKey('google-vertex') accepts ADC credentials from the provided env snapshot", async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-google-adc-")); + const credentialsPath = path.join(tempDir, "adc.json"); + await fs.writeFile(credentialsPath, "{}", "utf8"); + + try { + const resolved = resolveEnvApiKey("google-vertex", { + GOOGLE_APPLICATION_CREDENTIALS: credentialsPath, + GOOGLE_CLOUD_LOCATION: "us-central1", + GOOGLE_CLOUD_PROJECT: "vertex-project", + } as NodeJS.ProcessEnv); + + expect(resolved?.apiKey).toBe("gcp-vertex-credentials"); + expect(resolved?.source).toBe("gcloud adc"); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + it("resolveEnvApiKey('anthropic-vertex') accepts GOOGLE_APPLICATION_CREDENTIALS with project_id", async () => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-adc-")); const credentialsPath = path.join(tempDir, "adc.json"); diff --git a/src/gateway/openresponses-http.test.ts b/src/gateway/openresponses-http.test.ts index 93fec96b4a..80477e1e03 100644 --- a/src/gateway/openresponses-http.test.ts +++ b/src/gateway/openresponses-http.test.ts @@ -1192,9 +1192,12 @@ describe("OpenResponses HTTP API (e2e)", () => { }), ); - await vi.waitFor(() => { - expect(agentCommand).toHaveBeenCalledTimes(1); - }); + await vi.waitFor( + () => { + expect(agentCommand).toHaveBeenCalledTimes(1); + }, + { timeout: 5_000, interval: 50 }, + ); clientReq.destroy(); @@ -1245,9 +1248,12 @@ describe("OpenResponses HTTP API (e2e)", () => { }), ); - await vi.waitFor(() => { - expect(agentCommand).toHaveBeenCalledTimes(1); - }); + await vi.waitFor( + () => { + expect(agentCommand).toHaveBeenCalledTimes(1); + }, + { timeout: 5_000, interval: 50 }, + ); clientReq.destroy(); diff --git a/src/gateway/server.auth.default-token.suite.ts b/src/gateway/server.auth.default-token.suite.ts index 89578f3a66..34337a7011 100644 --- a/src/gateway/server.auth.default-token.suite.ts +++ b/src/gateway/server.auth.default-token.suite.ts @@ -92,7 +92,7 @@ export function registerDefaultAuthTokenSuite(): void { await withGatewayServer(async ({ port: isolatedPort }) => { const ws = await openWs(isolatedPort); const handshakeTimeoutMs = getPreauthHandshakeTimeoutMsFromEnv(); - const closed = await waitForWsClose(ws, handshakeTimeoutMs + 2500); + const closed = await waitForWsClose(ws, handshakeTimeoutMs + 10_000); expect(closed).toBe(true); }); } finally { diff --git a/src/gateway/server.chat.gateway-server-chat.test.ts b/src/gateway/server.chat.gateway-server-chat.test.ts index 4aec79e4d9..4b35cfa7c0 100644 --- a/src/gateway/server.chat.gateway-server-chat.test.ts +++ b/src/gateway/server.chat.gateway-server-chat.test.ts @@ -22,7 +22,7 @@ import { agentCommand } from "./test-helpers.runtime-state.js"; import { installConnectedControlUiServerSuite } from "./test-with-server.js"; installGatewayTestHooks({ scope: "suite" }); -const CHAT_RESPONSE_TIMEOUT_MS = 4_000; +const CHAT_RESPONSE_TIMEOUT_MS = 10_000; let ws: WebSocket; let port: number; diff --git a/src/gateway/server.node-invoke-approval-bypass.test.ts b/src/gateway/server.node-invoke-approval-bypass.test.ts index d7a572774f..444349b378 100644 --- a/src/gateway/server.node-invoke-approval-bypass.test.ts +++ b/src/gateway/server.node-invoke-approval-bypass.test.ts @@ -21,7 +21,7 @@ import { } from "./test-helpers.js"; installGatewayTestHooks({ scope: "suite" }); -const NODE_CONNECT_TIMEOUT_MS = 3_000; +const NODE_CONNECT_TIMEOUT_MS = 10_000; const CONNECT_REQ_TIMEOUT_MS = 2_000; function createDeviceIdentity(): DeviceIdentity { @@ -393,6 +393,9 @@ describe("node.invoke approval bypass", () => { idempotencyKey: crypto.randomUUID(), }); expect(invoke.ok).toBe(true); + for (let i = 0; i < 100 && !lastInvokeParams; i += 1) { + await sleep(50); + } expect(lastInvokeParams).toBeTruthy(); expect(lastInvokeParams?.["approved"]).toBe(true); expect(lastInvokeParams?.["approvalDecision"]).toBe("allow-once"); diff --git a/src/infra/install-package-dir.ts b/src/infra/install-package-dir.ts index 23c1bd90e0..ebbb291d8f 100644 --- a/src/infra/install-package-dir.ts +++ b/src/infra/install-package-dir.ts @@ -218,7 +218,7 @@ export async function installPackageDir(params: { candidatePaths: [canonicalTargetDir], }); stageDir = await fs.mkdtemp(path.join(installBaseRealPath, ".openclaw-install-stage-")); - await fs.cp(params.sourceDir, stageDir, { recursive: true }); + await fs.cp(params.sourceDir, stageDir, { recursive: true, verbatimSymlinks: true }); } catch (err) { return await fail(`${params.copyErrorPrefix}: ${String(err)}`, err); } diff --git a/src/plugins/install-security-scan.runtime.ts b/src/plugins/install-security-scan.runtime.ts index a0fcb05ced..f02a2d79b1 100644 --- a/src/plugins/install-security-scan.runtime.ts +++ b/src/plugins/install-security-scan.runtime.ts @@ -184,13 +184,13 @@ async function inspectNodeModulesSymlinkTarget(params: { ); } + const resolvedTargetStats = await fs.stat(resolvedTargetPath); const resolvedTargetRelativePath = path.relative(params.rootRealPath, resolvedTargetPath); - const resolvedTargetStat = await fs.lstat(resolvedTargetPath); return { blockedDirectoryFinding: findBlockedPackageDirectoryInPath({ pathRelativeToRoot: resolvedTargetRelativePath, }), - blockedFileFinding: resolvedTargetStat.isFile() + blockedFileFinding: resolvedTargetStats.isFile() ? findBlockedPackageFileAliasInPath({ pathRelativeToRoot: resolvedTargetRelativePath, }) @@ -262,6 +262,16 @@ function resolvePackageManifestTraversalLimits(): PackageManifestTraversalLimits }; } +async function resolvePackageManifestPath(dir: string): Promise { + const manifestPath = path.join(dir, "package.json"); + try { + const stats = await fs.stat(manifestPath); + return stats.isFile() ? manifestPath : undefined; + } catch { + return undefined; + } +} + async function collectPackageManifestPaths( rootDir: string, ): Promise { @@ -361,6 +371,10 @@ async function collectPackageManifestPaths( directoryRelativePath: relativeNextPath, }); if (blockedDirectoryFinding) { + const manifestPath = await resolvePackageManifestPath(nextPath); + if (manifestPath) { + packageManifestPaths.push(manifestPath); + } return { blockedDirectoryFinding, packageManifestPaths, @@ -400,35 +414,6 @@ async function scanManifestDependencyDenylist(params: { targetLabel: string; }): Promise { const traversalResult = await collectPackageManifestPaths(params.packageDir); - if (traversalResult.blockedDirectoryFinding) { - const reason = buildBlockedDependencyDirectoryReason({ - dependencyName: traversalResult.blockedDirectoryFinding.dependencyName, - directoryRelativePath: traversalResult.blockedDirectoryFinding.directoryRelativePath, - targetLabel: params.targetLabel, - }); - params.logger.warn?.(`WARNING: ${reason}`); - return { - blocked: { - code: "security_scan_blocked", - reason, - }, - }; - } - if (traversalResult.blockedFileFinding) { - const reason = buildBlockedDependencyFileReason({ - dependencyName: traversalResult.blockedFileFinding.dependencyName, - fileRelativePath: traversalResult.blockedFileFinding.fileRelativePath, - targetLabel: params.targetLabel, - }); - params.logger.warn?.(`WARNING: ${reason}`); - return { - blocked: { - code: "security_scan_blocked", - reason, - }, - }; - } - const packageManifestPaths = traversalResult.packageManifestPaths; for (const manifestPath of packageManifestPaths) { let manifest: PackageManifest; @@ -458,6 +443,34 @@ async function scanManifestDependencyDenylist(params: { }, }; } + if (traversalResult.blockedDirectoryFinding) { + const reason = buildBlockedDependencyDirectoryReason({ + dependencyName: traversalResult.blockedDirectoryFinding.dependencyName, + directoryRelativePath: traversalResult.blockedDirectoryFinding.directoryRelativePath, + targetLabel: params.targetLabel, + }); + params.logger.warn?.(`WARNING: ${reason}`); + return { + blocked: { + code: "security_scan_blocked", + reason, + }, + }; + } + if (traversalResult.blockedFileFinding) { + const reason = buildBlockedDependencyFileReason({ + dependencyName: traversalResult.blockedFileFinding.dependencyName, + fileRelativePath: traversalResult.blockedFileFinding.fileRelativePath, + targetLabel: params.targetLabel, + }); + params.logger.warn?.(`WARNING: ${reason}`); + return { + blocked: { + code: "security_scan_blocked", + reason, + }, + }; + } return undefined; } diff --git a/test/setup.shared.ts b/test/setup.shared.ts index fd644b35d6..96c294e22c 100644 --- a/test/setup.shared.ts +++ b/test/setup.shared.ts @@ -1,16 +1,5 @@ import { vi } from "vitest"; -vi.mock("@mariozechner/pi-ai", async () => { - const original = - await vi.importActual("@mariozechner/pi-ai"); - return { - ...original, - getOAuthApiKey: () => undefined, - getOAuthProviders: () => [], - loginOpenAICodex: vi.fn(), - }; -}); - vi.mock("@mariozechner/pi-ai/oauth", () => ({ getOAuthApiKey: () => undefined, getOAuthProviders: () => [],