diff --git a/src/hooks/install.test.ts b/src/hooks/install.test.ts index 4c692abb878..ca71fdf0bac 100644 --- a/src/hooks/install.test.ts +++ b/src/hooks/install.test.ts @@ -1,8 +1,8 @@ import { createHash, randomUUID } from "node:crypto"; import fs from "node:fs"; -import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runCommandWithTimeout } from "../process/exec.js"; import { expectSingleNpmPackIgnoreScriptsCall } from "../test-utils/exec-assertions.js"; import { expectInstallUsesIgnoreScripts, @@ -11,8 +11,13 @@ import { mockNpmPackMetadataResult, } from "../test-utils/npm-spec-install-test-helpers.js"; import { isAddressInUseError } from "./gmail-watcher-errors.js"; +import { + installHooksFromArchive, + installHooksFromNpmSpec, + installHooksFromPath, +} from "./install.js"; -const fixtureRoot = path.join(os.tmpdir(), `openclaw-hook-install-${randomUUID()}`); +const fixtureRoot = path.join(process.cwd(), ".tmp", `openclaw-hook-install-${randomUUID()}`); const sharedArchiveDir = path.join(fixtureRoot, "_archives"); let tempDirIndex = 0; const sharedArchivePathByName = new Map(); @@ -36,10 +41,6 @@ function makeTempDir() { return dir; } -const { runCommandWithTimeout } = await import("../process/exec.js"); -const { installHooksFromArchive, installHooksFromNpmSpec, installHooksFromPath } = - await import("./install.js"); - afterAll(() => { try { fs.rmSync(fixtureRoot, { recursive: true, force: true }); diff --git a/src/plugin-sdk/index.bundle.test.ts b/src/plugin-sdk/index.bundle.test.ts index 1d3904e2b49..796108cb075 100644 --- a/src/plugin-sdk/index.bundle.test.ts +++ b/src/plugin-sdk/index.bundle.test.ts @@ -1,6 +1,5 @@ import fs from "node:fs/promises"; import { createRequire } from "node:module"; -import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { describe, expect, it } from "vitest"; @@ -23,12 +22,14 @@ function buildBundledCoverageEntrySources() { describe("plugin-sdk bundled exports", () => { it("emits importable bundled subpath entries", { timeout: 120_000 }, async () => { - const outDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-plugin-sdk-build-")); + const bundleTempRoot = path.join(process.cwd(), ".tmp"); + await fs.mkdir(bundleTempRoot, { recursive: true }); + const outDir = await fs.mkdtemp(path.join(bundleTempRoot, "openclaw-plugin-sdk-build-")); try { const { build } = await import(tsdownModuleUrl); await build({ - clean: true, + clean: false, config: false, dts: false, // Full plugin-sdk coverage belongs to `pnpm build`, package contract @@ -41,11 +42,6 @@ describe("plugin-sdk bundled exports", () => { outDir, platform: "node", }); - await fs.symlink( - path.join(process.cwd(), "node_modules"), - path.join(outDir, "node_modules"), - "dir", - ); expect(pluginSdkEntrypoints.length).toBeGreaterThan(bundledRepresentativeEntrypoints.length); await Promise.all( diff --git a/src/plugin-sdk/subpaths.test.ts b/src/plugin-sdk/subpaths.test.ts index 039a0b7b671..89eda599ac6 100644 --- a/src/plugin-sdk/subpaths.test.ts +++ b/src/plugin-sdk/subpaths.test.ts @@ -71,24 +71,45 @@ function readPluginSdkSource(subpath: string): string { return text; } -function escapeRegExp(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +function isIdentifierCode(code: number): boolean { + return ( + (code >= 48 && code <= 57) || + (code >= 65 && code <= 90) || + (code >= 97 && code <= 122) || + code === 36 || + code === 95 + ); +} + +function sourceMentionsIdentifier(source: string, name: string): boolean { + let fromIndex = 0; + while (true) { + const matchIndex = source.indexOf(name, fromIndex); + if (matchIndex === -1) { + return false; + } + const beforeCode = matchIndex === 0 ? -1 : source.charCodeAt(matchIndex - 1); + const afterIndex = matchIndex + name.length; + const afterCode = afterIndex >= source.length ? -1 : source.charCodeAt(afterIndex); + if (!isIdentifierCode(beforeCode) && !isIdentifierCode(afterCode)) { + return true; + } + fromIndex = matchIndex + 1; + } } function expectSourceMentions(subpath: string, names: readonly string[]) { const source = readPluginSdkSource(subpath); for (const name of names) { - expect(source, `${subpath} should mention ${name}`).toMatch( - new RegExp(`\\b${escapeRegExp(name)}\\b`, "u"), - ); + expect(sourceMentionsIdentifier(source, name), `${subpath} should mention ${name}`).toBe(true); } } function expectSourceOmits(subpath: string, names: readonly string[]) { const source = readPluginSdkSource(subpath); for (const name of names) { - expect(source, `${subpath} should not mention ${name}`).not.toMatch( - new RegExp(`\\b${escapeRegExp(name)}\\b`, "u"), + expect(sourceMentionsIdentifier(source, name), `${subpath} should not mention ${name}`).toBe( + false, ); } } @@ -141,11 +162,8 @@ describe("plugin-sdk subpath exports", () => { expect(coreSdk.definePluginEntry).toBe(pluginEntrySdk.definePluginEntry); }); - it("exports routing helpers from the dedicated subpath", () => { + it("keeps generic helper subpaths aligned", () => { expectSourceMentions("routing", ["buildAgentSessionKey", "resolveThreadSessionKeys"]); - }); - - it("exports reply payload helpers from the dedicated subpath", () => { expectSourceMentions("reply-payload", [ "buildMediaPayload", "deliverTextOrMediaReply", @@ -156,16 +174,10 @@ describe("plugin-sdk subpath exports", () => { "sendTextMediaPayload", "sendPayloadWithChunkedTextAndMedia", ]); - }); - - it("exports media runtime helpers from the dedicated subpath", () => { expectSourceMentions("media-runtime", [ "createDirectTextMediaOutbound", "createScopedChannelMediaMaxBytesResolver", ]); - }); - - it("exports reply history helpers from the dedicated subpath", () => { expectSourceMentions("reply-history", [ "buildPendingHistoryContextFromMap", "clearHistoryEntriesIfEnabled", @@ -177,28 +189,16 @@ describe("plugin-sdk subpath exports", () => { "recordPendingHistoryEntryIfEnabled", "DEFAULT_GROUP_HISTORY_LIMIT", ]); - }); - - it("exports account helper builders from the dedicated subpath", () => { expectSourceMentions("account-helpers", ["createAccountListHelpers"]); - }); - - it("exports device bootstrap helpers from the dedicated subpath", () => { expectSourceMentions("device-bootstrap", [ "approveDevicePairing", "issueDeviceBootstrapToken", "listDevicePairing", ]); - }); - - it("exports allowlist edit helpers from the dedicated subpath", () => { expectSourceMentions("allowlist-config-edit", [ "buildDmGroupAccountAllowlistAdapter", "createNestedAllowlistOverrideResolver", ]); - }); - - it("exports allowlist resolution helpers from the dedicated subpath", () => { expectSourceMentions("allow-from", [ "addAllowlistUserEntriesFromConfigEntry", "buildAllowlistResolutionSummary", @@ -208,9 +208,6 @@ describe("plugin-sdk subpath exports", () => { "patchAllowlistUsersInConfigEntries", "summarizeMapping", ]); - }); - - it("exports allow-from matching helpers from the dedicated subpath", () => { expectSourceMentions("allow-from", [ "compileAllowlist", "firstDefined", @@ -219,31 +216,19 @@ describe("plugin-sdk subpath exports", () => { "mergeDmAllowFromSources", "resolveAllowlistMatchSimple", ]); - }); - - it("exports runtime helpers from the dedicated subpath", () => { expectSourceMentions("runtime", ["createLoggerBackedRuntime"]); - }); - - it("exports Discord component helpers from the dedicated subpath", () => { expectSourceMentions("discord", [ "buildDiscordComponentMessage", "editDiscordComponentMessage", "registerBuiltDiscordComponentMessage", "resolveDiscordAccount", ]); - }); - - it("exports channel identity and session helpers from stronger existing homes", () => { expectSourceMentions("routing", ["normalizeMessageChannel", "resolveGatewayMessageChannel"]); expectSourceMentions("conversation-runtime", [ "recordInboundSession", "recordInboundSessionMetaSafe", "resolveConversationLabel", ]); - }); - - it("exports directory runtime helpers from the dedicated subpath", () => { expectSourceMentions("directory-runtime", [ "createChannelDirectoryAdapter", "createRuntimeDirectoryLiveAdapter", @@ -361,7 +346,7 @@ describe("plugin-sdk subpath exports", () => { ]); }); - it("exports inbound channel helpers from the dedicated subpath", () => { + it("keeps channel helper subpaths aligned", () => { expectSourceMentions("channel-inbound", [ "buildMentionRegexes", "createChannelInboundDebouncer", @@ -392,24 +377,15 @@ describe("plugin-sdk subpath exports", () => { "resolveEnvelopeFormatOptions", "resolveInboundDebounceMs", ]); - }); - - it("exports channel setup helpers from the dedicated subpath", () => { expectSourceMentions("channel-setup", [ "createOptionalChannelSetupSurface", "createTopLevelChannelDmPolicy", ]); - }); - - it("exports channel action helpers from the dedicated subpath", () => { expectSourceMentions("channel-actions", [ "createUnionActionGate", "listTokenSourcedAccounts", "resolveReactionMessageId", ]); - }); - - it("exports channel target helpers from the dedicated subpath", () => { expectSourceMentions("channel-targets", [ "applyChannelMatchMeta", "buildChannelKeyCandidates", @@ -421,44 +397,12 @@ describe("plugin-sdk subpath exports", () => { "resolveChannelMatchConfig", "resolveTargetsWithOptionalToken", ]); - }); - - it("exports channel config write helpers from the dedicated subpath", () => { expectSourceMentions("channel-config-helpers", [ "authorizeConfigWrite", "canBypassConfigWritePolicy", "formatConfigWriteDeniedMessage", "resolveChannelConfigWrites", ]); - }); - - it("keeps channel contract types on the dedicated subpath", () => { - expectTypeOf().toMatchTypeOf(); - expectTypeOf().toMatchTypeOf(); - expectTypeOf().toMatchTypeOf(); - expectTypeOf().toMatchTypeOf(); - expectTypeOf().toMatchTypeOf(); - expectTypeOf().toMatchTypeOf(); - expectTypeOf().toMatchTypeOf(); - expectTypeOf().toMatchTypeOf(); - expectTypeOf().toMatchTypeOf(); - expectTypeOf().toMatchTypeOf(); - expectTypeOf().toMatchTypeOf(); - expectTypeOf().toMatchTypeOf(); - }); - - it("exports channel lifecycle helpers from the dedicated subpath", async () => { - const channelLifecycleSdk = await importPluginSdkSubpath( - "openclaw/plugin-sdk/channel-lifecycle", - ); - expect(typeof channelLifecycleSdk.createDraftStreamLoop).toBe("function"); - expect(typeof channelLifecycleSdk.createFinalizableDraftLifecycle).toBe("function"); - expect(typeof channelLifecycleSdk.runPassiveAccountLifecycle).toBe("function"); - expect(typeof channelLifecycleSdk.createRunStateMachine).toBe("function"); - expect(typeof channelLifecycleSdk.createArmableStallWatchdog).toBe("function"); - }); - - it("exports channel feedback helpers from the dedicated subpath", () => { expectSourceMentions("channel-feedback", [ "createStatusReactionController", "logAckFailure", @@ -468,9 +412,6 @@ describe("plugin-sdk subpath exports", () => { "shouldAckReactionForWhatsApp", "DEFAULT_EMOJIS", ]); - }); - - it("exports status helper utilities from the dedicated subpath", () => { expectSourceMentions("status-helpers", [ "appendMatchMetadata", "asString", @@ -478,38 +419,10 @@ describe("plugin-sdk subpath exports", () => { "isRecord", "resolveEnabledConfiguredAccountId", ]); - }); - - it("exports message tool schema helpers from the dedicated subpath", () => { expectSourceMentions("channel-actions", [ "createMessageToolButtonsSchema", "createMessageToolCardSchema", ]); - }); - - it("exports channel pairing helpers from the dedicated subpath", async () => { - const channelPairingSdk = await importPluginSdkSubpath("openclaw/plugin-sdk/channel-pairing"); - expectSourceMentions("channel-pairing", [ - "createChannelPairingController", - "createChannelPairingChallengeIssuer", - "createLoggedPairingApprovalNotifier", - "createPairingPrefixStripper", - "createTextPairingAdapter", - ]); - expect("createScopedPairingAccess" in channelPairingSdk).toBe(false); - }); - - it("exports channel reply pipeline helpers from the dedicated subpath", async () => { - const channelReplyPipelineSdk = await importPluginSdkSubpath( - "openclaw/plugin-sdk/channel-reply-pipeline", - ); - expectSourceMentions("channel-reply-pipeline", ["createChannelReplyPipeline"]); - expect("createTypingCallbacks" in channelReplyPipelineSdk).toBe(false); - expect("createReplyPrefixContext" in channelReplyPipelineSdk).toBe(false); - expect("createReplyPrefixOptions" in channelReplyPipelineSdk).toBe(false); - }); - - it("exports command auth helpers from the dedicated subpath", () => { expectSourceMentions("command-auth", [ "buildCommandTextFromArgs", "buildCommandsPaginationKeyboard", @@ -542,14 +455,60 @@ describe("plugin-sdk subpath exports", () => { ]); }); - it("exports channel send-result helpers from the dedicated subpath", () => { + it("keeps channel contract types on the dedicated subpath", () => { + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + }); + + it("exports channel lifecycle helpers from the dedicated subpath", async () => { + const channelLifecycleSdk = await importPluginSdkSubpath( + "openclaw/plugin-sdk/channel-lifecycle", + ); + expect(typeof channelLifecycleSdk.createDraftStreamLoop).toBe("function"); + expect(typeof channelLifecycleSdk.createFinalizableDraftLifecycle).toBe("function"); + expect(typeof channelLifecycleSdk.runPassiveAccountLifecycle).toBe("function"); + expect(typeof channelLifecycleSdk.createRunStateMachine).toBe("function"); + expect(typeof channelLifecycleSdk.createArmableStallWatchdog).toBe("function"); + }); + + it("exports channel pairing helpers from the dedicated subpath", async () => { + const channelPairingSdk = await importPluginSdkSubpath("openclaw/plugin-sdk/channel-pairing"); + expectSourceMentions("channel-pairing", [ + "createChannelPairingController", + "createChannelPairingChallengeIssuer", + "createLoggedPairingApprovalNotifier", + "createPairingPrefixStripper", + "createTextPairingAdapter", + ]); + expect("createScopedPairingAccess" in channelPairingSdk).toBe(false); + }); + + it("exports channel reply pipeline helpers from the dedicated subpath", async () => { + const channelReplyPipelineSdk = await importPluginSdkSubpath( + "openclaw/plugin-sdk/channel-reply-pipeline", + ); + expectSourceMentions("channel-reply-pipeline", ["createChannelReplyPipeline"]); + expect("createTypingCallbacks" in channelReplyPipelineSdk).toBe(false); + expect("createReplyPrefixContext" in channelReplyPipelineSdk).toBe(false); + expect("createReplyPrefixOptions" in channelReplyPipelineSdk).toBe(false); + }); + + it("keeps source-only helper subpaths aligned", () => { expectSourceMentions("channel-send-result", [ "attachChannelToResult", "buildChannelSendResult", ]); - }); - it("exports binding lifecycle helpers from the conversation-runtime subpath", () => { expectSourceMentions("conversation-runtime", [ "DISCORD_THREAD_BINDING_CHANNEL", "MATRIX_THREAD_BINDING_CHANNEL", @@ -571,17 +530,9 @@ describe("plugin-sdk subpath exports", () => { "createStaticReplyToModeResolver", "createTopLevelChannelReplyToModeResolver", ]); - }); - it("exports narrow binding lifecycle helpers from the dedicated subpath", () => { expectSourceMentions("thread-bindings-runtime", ["resolveThreadBindingLifecycle"]); - }); - - it("exports narrow matrix runtime helpers from the dedicated subpath", () => { expectSourceMentions("matrix-runtime-shared", ["formatZonedTimestamp"]); - }); - - it("exports narrow ssrf helpers from the dedicated subpath", () => { expectSourceMentions("ssrf-runtime", [ "closeDispatcher", "createPinnedDispatcher", @@ -589,25 +540,17 @@ describe("plugin-sdk subpath exports", () => { "assertHttpUrlTargetsPrivateNetwork", "ssrfPolicyFromAllowPrivateNetwork", ]); - }); - it("exports provider setup helpers from the dedicated subpath", () => { expectSourceMentions("provider-setup", [ "buildVllmProvider", "discoverOpenAICompatibleSelfHostedProvider", ]); - }); - - it("exports oauth helpers from provider-auth", () => { expectSourceMentions("provider-auth", [ "buildOauthProviderAuthResult", "generatePkceVerifierChallenge", "toFormUrlEncoded", ]); expectSourceOmits("core", ["buildOauthProviderAuthResult"]); - }); - - it("keeps provider models focused on shared provider primitives", () => { expectSourceMentions("provider-models", [ "applyOpenAIConfig", "buildKilocodeModelDefinition", @@ -619,9 +562,7 @@ describe("plugin-sdk subpath exports", () => { "QIANFAN_BASE_URL", "resolveZaiBaseUrl", ]); - }); - it("exports shared setup helpers from the dedicated subpath", () => { expectSourceMentions("setup", [ "DEFAULT_ACCOUNT_ID", "createAllowFromSection", @@ -629,29 +570,15 @@ describe("plugin-sdk subpath exports", () => { "createTopLevelChannelDmPolicy", "mergeAllowFromEntries", ]); - }); - - it("exports shared lazy runtime helpers from the dedicated subpath", () => { expectSourceMentions("lazy-runtime", ["createLazyRuntimeSurface", "createLazyRuntimeModule"]); - }); - - it("exports narrow self-hosted provider setup helpers", () => { expectSourceMentions("self-hosted-provider-setup", [ "buildVllmProvider", "buildSglangProvider", "configureOpenAICompatibleSelfHostedProviderNonInteractive", ]); - }); - - it("exports narrow Ollama setup helpers", () => { expectSourceMentions("ollama-setup", ["buildOllamaProvider", "configureOllamaNonInteractive"]); - }); - - it("exports sandbox helpers from the dedicated subpath", () => { expectSourceMentions("sandbox", ["registerSandboxBackend", "runPluginCommandWithTimeout"]); - }); - it("exports secret input helpers from the dedicated subpath", () => { expectSourceMentions("secret-input", [ "buildSecretInputSchema", "buildOptionalSecretInputSchema", @@ -662,9 +589,7 @@ describe("plugin-sdk subpath exports", () => { "normalizeResolvedSecretInputString", "normalizeSecretInputString", ]); - }); - it("exports webhook ingress helpers from the dedicated subpath", () => { expectSourceMentions("webhook-ingress", [ "registerPluginHttpRoute", "resolveWebhookPath", @@ -673,6 +598,7 @@ describe("plugin-sdk subpath exports", () => { "requestBodyErrorToText", "withResolvedWebhookRequestPipeline", ]); + expectSourceMentions("testing", ["removeAckReactionAfterReply", "shouldAckReaction"]); }); it("exports shared core types used by bundled extensions", () => { @@ -681,10 +607,6 @@ describe("plugin-sdk subpath exports", () => { expectTypeOf().toMatchTypeOf(); }); - it("exports the public testing surface", () => { - expectSourceMentions("testing", ["removeAckReactionAfterReply", "shouldAckReaction"]); - }); - it("keeps core shared types aligned with the channel prelude", () => { expectTypeOf().toMatchTypeOf(); expectTypeOf().toMatchTypeOf(); diff --git a/src/plugins/install.test.ts b/src/plugins/install.test.ts index afd84c27aba..9550bba7524 100644 --- a/src/plugins/install.test.ts +++ b/src/plugins/install.test.ts @@ -1,5 +1,4 @@ import fs from "node:fs"; -import os from "node:os"; import path from "node:path"; import * as tar from "tar"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -69,7 +68,9 @@ function ensureSuiteTempRoot() { if (suiteTempRoot) { return suiteTempRoot; } - suiteTempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-install-")); + const bundleTempRoot = path.join(process.cwd(), ".tmp"); + fs.mkdirSync(bundleTempRoot, { recursive: true }); + suiteTempRoot = fs.mkdtempSync(path.join(bundleTempRoot, "openclaw-plugin-install-")); return suiteTempRoot; }