






















@@ -3,6 +3,7 @@ import os from "node:os";
33import path from "node:path";
44import { bundledDistPluginFile } from "openclaw/plugin-sdk/test-fixtures";
55import { afterEach, describe, expect, it, vi } from "vitest";
6+import type { PluginInstallRecord } from "../config/types.plugins.js";
67import { discoverOpenClawPlugins } from "./discovery.js";
78import { listBuiltRuntimeEntryCandidates } from "./package-entrypoints.js";
89import {
@@ -401,7 +402,7 @@ function expectBundleCandidateMatch(params: {
401402async function expectRejectedPackageExtensionEntry(params: {
402403stateDir: string;
403404setup: (stateDir: string) => boolean | void;
404-expectedDiagnostic?: "escapes" | "none" | "runtime";
405+expectedDiagnostic?: "escapes" | "none" | "not_found" | "runtime";
405406expectedId?: string;
406407expectedDiagnosticPluginId?: string;
407408}) {
@@ -434,6 +435,14 @@ async function expectRejectedPackageExtensionEntry(params: {
434435).toBe(true);
435436return;
436437}
438+if (params.expectedDiagnostic === "not_found") {
439+expect(
440+result.diagnostics.some(
441+(entry) => entry.level === "error" && entry.message.includes("extension entry not found"),
442+),
443+).toBe(true);
444+return;
445+}
437446expect(result.diagnostics).toStrictEqual([]);
438447}
439448@@ -838,7 +847,34 @@ describe("discoverOpenClawPlugins", () => {
838847expectCandidateIds(candidates, { includes: ["pack/one", "pack/two"] });
839848});
840849841-it("skips source-only TypeScript entries for installed package plugins", async () => {
850+it("discovers untracked global package plugins that point at TypeScript source", async () => {
851+const stateDir = makeTempDir();
852+const pluginDir = path.join(stateDir, "extensions", "local-source-pack");
853+mkdirSafe(pluginDir);
854+855+writePluginPackageManifest({
856+packageDir: pluginDir,
857+packageName: "@openclaw/local-source-pack",
858+extensions: ["./index.ts"],
859+});
860+writePluginManifest({ pluginDir, id: "local-source-pack" });
861+writePluginEntry(path.join(pluginDir, "index.ts"));
862+863+const result = await discoverWithStateDir(stateDir, {});
864+865+expectCandidateSource(
866+result.candidates,
867+"local-source-pack",
868+fs.realpathSync(path.join(pluginDir, "index.ts")),
869+);
870+expectNoDiagnostic({
871+diagnostics: result.diagnostics,
872+pluginId: "local-source-pack",
873+messageIncludes: "requires compiled runtime output",
874+});
875+});
876+877+it("still requires compiled runtime output for tracked installed package plugins", async () => {
842878const stateDir = makeTempDir();
843879const pluginDir = path.join(stateDir, "extensions", "source-only-pack");
844880mkdirSafe(path.join(pluginDir, "src"));
@@ -850,7 +886,13 @@ describe("discoverOpenClawPlugins", () => {
850886});
851887writePluginEntry(path.join(pluginDir, "src", "index.ts"));
852888853-const result = await discoverWithStateDir(stateDir, {});
889+const installRecords = {
890+"source-only-pack": {
891+source: "path",
892+installPath: pluginDir,
893+},
894+} satisfies Record<string, PluginInstallRecord>;
895+const result = await discoverWithStateDir(stateDir, { installRecords });
854896855897expectCandidateIds(result.candidates, { excludes: ["source-only-pack"] });
856898expect(
@@ -864,6 +906,128 @@ describe("discoverOpenClawPlugins", () => {
864906entry.message.includes("disable/uninstall the plugin"),
865907),
866908).toBe(true);
909+expect(result.diagnostics).toHaveLength(1);
910+});
911+912+it("treats install record sourcePath dirs as managed during global scans", async () => {
913+const stateDir = makeTempDir();
914+const sourceDir = path.join(stateDir, "extensions", "source-path-pack");
915+const installDir = path.join(stateDir, "installed", "source-path-pack");
916+mkdirSafe(path.join(sourceDir, "src"));
917+mkdirSafe(installDir);
918+919+writePluginPackageManifest({
920+packageDir: sourceDir,
921+packageName: "@openclaw/source-path-pack",
922+extensions: ["./src/index.ts"],
923+});
924+writePluginEntry(path.join(sourceDir, "src", "index.ts"));
925+926+const installRecords = {
927+"source-path-pack": {
928+source: "path",
929+installPath: installDir,
930+sourcePath: sourceDir,
931+},
932+} satisfies Record<string, PluginInstallRecord>;
933+const result = await discoverWithStateDir(stateDir, { installRecords });
934+935+expectCandidateIds(result.candidates, { excludes: ["source-path-pack"] });
936+expectDiagnostic({
937+diagnostics: result.diagnostics,
938+level: "warn",
939+pluginId: "source-path-pack",
940+messageIncludes: "requires compiled runtime output",
941+source: sourceDir,
942+});
943+expect(result.diagnostics).toHaveLength(1);
944+});
945+946+it.skipIf(!canCreateDirectorySymlinks)(
947+"treats symlinked install record sourcePath dirs as managed during global scans",
948+async () => {
949+const stateDir = makeTempDir();
950+const globalExt = path.join(stateDir, "extensions");
951+const actualSourceDir = path.join(stateDir, "source-checkouts", "source-path-symlink-pack");
952+const linkedSourceDir = path.join(globalExt, "source-path-symlink-pack");
953+const installDir = path.join(stateDir, "installed", "source-path-symlink-pack");
954+mkdirSafe(globalExt);
955+mkdirSafe(path.join(actualSourceDir, "src"));
956+mkdirSafe(installDir);
957+958+writePluginPackageManifest({
959+packageDir: actualSourceDir,
960+packageName: "@openclaw/source-path-symlink-pack",
961+extensions: ["./src/index.ts"],
962+});
963+writePluginEntry(path.join(actualSourceDir, "src", "index.ts"));
964+symlinkDirectory(actualSourceDir, linkedSourceDir);
965+966+const installRecords = {
967+"source-path-symlink-pack": {
968+source: "path",
969+installPath: installDir,
970+sourcePath: linkedSourceDir,
971+},
972+} satisfies Record<string, PluginInstallRecord>;
973+const result = await discoverWithStateDir(stateDir, { installRecords });
974+975+expectCandidateIds(result.candidates, { excludes: ["source-path-symlink-pack"] });
976+expectDiagnostic({
977+diagnostics: result.diagnostics,
978+level: "warn",
979+pluginId: "source-path-symlink-pack",
980+messageIncludes: "requires compiled runtime output",
981+source: linkedSourceDir,
982+});
983+expect(result.diagnostics).toHaveLength(1);
984+},
985+);
986+987+it("discovers global plugin directories with package metadata but no package entries", async () => {
988+const stateDir = makeTempDir();
989+const pluginDir = path.join(stateDir, "extensions", "metadata-only-pack");
990+mkdirSafe(pluginDir);
991+fs.writeFileSync(
992+path.join(pluginDir, "package.json"),
993+JSON.stringify({ name: "@openclaw/metadata-only-pack", version: "0.0.1" }),
994+"utf-8",
995+);
996+writePluginManifest({ pluginDir, id: "metadata-only-pack" });
997+writePluginEntry(path.join(pluginDir, "index.ts"));
998+999+const result = await discoverWithStateDir(stateDir, {});
1000+1001+expectCandidateSource(
1002+result.candidates,
1003+"metadata-only-pack",
1004+fs.realpathSync(path.join(pluginDir, "index.ts")),
1005+);
1006+});
1007+1008+it("keeps explicit runtime extension entries strict for untracked global packages", async () => {
1009+const stateDir = makeTempDir();
1010+const pluginDir = path.join(stateDir, "extensions", "missing-runtime-pack");
1011+mkdirSafe(pluginDir);
1012+1013+writePluginPackageManifest({
1014+packageDir: pluginDir,
1015+packageName: "@openclaw/missing-runtime-pack",
1016+extensions: ["./index.ts"],
1017+runtimeExtensions: ["./dist/index.js"],
1018+});
1019+writePluginManifest({ pluginDir, id: "missing-runtime-pack" });
1020+writePluginEntry(path.join(pluginDir, "index.ts"));
1021+1022+const result = await discoverWithStateDir(stateDir, {});
1023+1024+expectCandidateIds(result.candidates, { excludes: ["missing-runtime-pack"] });
1025+expectDiagnostic({
1026+diagnostics: result.diagnostics,
1027+level: "error",
1028+source: pluginDir,
1029+messageIncludes: "runtime extension entry not found: ./dist/index.js",
1030+});
8671031});
86810328691033it("lets a valid bundled plugin win when a managed package is source-only TypeScript", () => {
@@ -894,6 +1058,12 @@ describe("discoverOpenClawPlugins", () => {
8941058env: buildDiscoveryEnvWithOverrides(stateDir, {
8951059OPENCLAW_BUNDLED_PLUGINS_DIR: bundledDir,
8961060}),
1061+installRecords: {
1062+discord: {
1063+source: "path",
1064+installPath: installedPluginDir,
1065+},
1066+},
8971067});
89810688991069const discordCandidates = result.candidates.filter(
@@ -1038,6 +1208,31 @@ describe("discoverOpenClawPlugins", () => {
10381208).toBe(true);
10391209});
104012101211+it("reports missing declared setup entries for package plugins", async () => {
1212+const stateDir = makeTempDir();
1213+const pluginDir = path.join(stateDir, "extensions", "missing-setup-pack");
1214+mkdirSafe(path.join(pluginDir, "dist"));
1215+1216+writePluginPackageManifest({
1217+packageDir: pluginDir,
1218+packageName: "@openclaw/missing-setup-pack",
1219+extensions: ["./dist/index.js"],
1220+setupEntry: "./src/setup-entry.ts",
1221+});
1222+writePluginEntry(path.join(pluginDir, "dist", "index.js"));
1223+1224+const result = await discoverWithStateDir(stateDir, {});
1225+const candidate = requireCandidateById(result.candidates, "missing-setup-pack");
1226+1227+expect(candidate.setupSource).toBeUndefined();
1228+expectDiagnostic({
1229+diagnostics: result.diagnostics,
1230+level: "error",
1231+source: pluginDir,
1232+messageIncludes: "setup entry not found: src/setup-entry.ts",
1233+});
1234+});
1235+10411236it("rejects package runtimeExtensions that do not match extension entries", async () => {
10421237const stateDir = makeTempDir();
10431238const pluginDir = path.join(stateDir, "extensions", "runtime-mismatch-pack");
@@ -1196,6 +1391,38 @@ describe("discoverOpenClawPlugins", () => {
11961391expectCandidateIds(candidates, { includes: ["downloadable"] });
11971392});
119813931394+it("keeps trusted bundled package fallback from missing TypeScript metadata to JavaScript", () => {
1395+const stateDir = makeTempDir();
1396+const bundledDir = path.join(stateDir, "bundled");
1397+const pluginDir = path.join(bundledDir, "downloadable");
1398+mkdirSafe(pluginDir);
1399+fs.writeFileSync(
1400+path.join(pluginDir, "package.json"),
1401+JSON.stringify({
1402+name: "@openclaw/downloadable",
1403+openclaw: {
1404+extensions: ["./index.ts"],
1405+},
1406+}),
1407+"utf-8",
1408+);
1409+writePluginManifest({ pluginDir, id: "downloadable" });
1410+writePluginEntry(path.join(pluginDir, "index.js"));
1411+1412+const { candidates, diagnostics } = discoverOpenClawPlugins({
1413+env: buildDiscoveryEnvWithOverrides(stateDir, {
1414+OPENCLAW_BUNDLED_PLUGINS_DIR: bundledDir,
1415+}),
1416+});
1417+1418+expectCandidateSource(
1419+candidates,
1420+"downloadable",
1421+fs.realpathSync(path.join(pluginDir, "index.js")),
1422+);
1423+expect(diagnostics).toStrictEqual([]);
1424+});
1425+11991426it("discovers source-checkout-only bundled plugins alongside built bundled plugins", () => {
12001427const stateDir = makeTempDir();
12011428const packageRoot = path.join(stateDir, "openclaw");
@@ -1545,7 +1772,7 @@ describe("discoverOpenClawPlugins", () => {
15451772},
15461773{
15471774name: "rejects missing TypeScript package runtime entries without escape diagnostics",
1548-expectedDiagnostic: "runtime" as const,
1775+expectedDiagnostic: "not_found" as const,
15491776setup: (stateDir: string) => {
15501777const globalExt = path.join(stateDir, "extensions", "missing-entry-pack");
15511778mkdirSafe(globalExt);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。