























@@ -1,6 +1,7 @@
1+import { spawnSync } from "node:child_process";
12import fs from "node:fs";
23import path from "node:path";
3-import { describe, expect, it } from "vitest";
4+import { describe, expect, it, vi } from "vitest";
4556type PluginManifestFile = {
67id?: unknown;
@@ -10,6 +11,11 @@ type PluginManifestFile = {
1011};
11121213function walkFiles(dir: string): string[] {
14+const gitFiles = listGitFiles(dir);
15+if (gitFiles) {
16+return gitFiles;
17+}
18+1319const files: string[] = [];
1420for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1521if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) {
@@ -25,6 +31,71 @@ function walkFiles(dir: string): string[] {
2531return files;
2632}
273334+function repoRelativePath(filePath: string): string {
35+return path.relative(process.cwd(), filePath).split(path.sep).join("/");
36+}
37+38+function isSkippedRepoPath(relativePath: string): boolean {
39+return relativePath
40+.split("/")
41+.some((part) => part === "node_modules" || part === "dist" || part.startsWith("."));
42+}
43+44+function listGitFiles(dir: string): string[] | null {
45+const relativeDir = repoRelativePath(dir);
46+if (!relativeDir || relativeDir.startsWith("..") || path.isAbsolute(relativeDir)) {
47+return null;
48+}
49+const result = spawnSync("git", ["ls-files", "--", relativeDir], {
50+cwd: process.cwd(),
51+encoding: "utf8",
52+stdio: ["ignore", "pipe", "ignore"],
53+});
54+if (result.status !== 0) {
55+return null;
56+}
57+return result.stdout
58+.split("\n")
59+.map((line) => line.trim().replaceAll("\\", "/"))
60+.filter((line) => line.length > 0 && !isSkippedRepoPath(line))
61+.map((line) => path.join(process.cwd(), ...line.split("/")))
62+.toSorted();
63+}
64+65+function listGitPluginManifestPaths(extensionsDir: string): string[] | null {
66+const relativeDir = repoRelativePath(extensionsDir);
67+if (!relativeDir || relativeDir.startsWith("..") || path.isAbsolute(relativeDir)) {
68+return null;
69+}
70+const result = spawnSync("git", ["ls-files", "--", relativeDir], {
71+cwd: process.cwd(),
72+encoding: "utf8",
73+stdio: ["ignore", "pipe", "ignore"],
74+});
75+if (result.status !== 0) {
76+return null;
77+}
78+return result.stdout
79+.split("\n")
80+.map((line) => line.trim().replaceAll("\\", "/"))
81+.filter((line) => /^extensions\/[^/]+\/openclaw\.plugin\.json$/u.test(line))
82+.map((line) => path.join(process.cwd(), ...line.split("/")))
83+.toSorted();
84+}
85+86+function listPluginManifestPaths(extensionsDir: string): string[] {
87+const gitPaths = listGitPluginManifestPaths(extensionsDir);
88+if (gitPaths) {
89+return gitPaths;
90+}
91+92+return fs
93+.readdirSync(extensionsDir, { withFileTypes: true })
94+.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
95+.map((entry) => path.join(extensionsDir, entry.name, "openclaw.plugin.json"))
96+.filter((manifestPath) => fs.existsSync(manifestPath));
97+}
98+2899function isProductionSource(filePath: string): boolean {
29100if (!/\.(?:cjs|mjs|js|ts|tsx)$/.test(filePath)) {
30101return false;
@@ -202,22 +273,31 @@ function normalizeManifestTools(value: unknown): string[] {
202273}
203274204275describe("bundled plugin tool manifest contracts", () => {
276+it("lists plugin tool contract inputs from git without walking extension roots", () => {
277+const extensionsDir = path.join(process.cwd(), "extensions");
278+const readDir = vi.spyOn(fs, "readdirSync");
279+try {
280+const manifestPaths = listPluginManifestPaths(extensionsDir);
281+const sourceFiles = manifestPaths.flatMap((manifestPath) =>
282+walkFiles(path.dirname(manifestPath)).filter(isProductionSource),
283+);
284+285+expect(manifestPaths.length).toBeGreaterThan(0);
286+expect(sourceFiles.length).toBeGreaterThan(0);
287+expect(readDir).not.toHaveBeenCalled();
288+} finally {
289+readDir.mockRestore();
290+}
291+});
292+205293it("declares every production registerTool owner in contracts.tools", () => {
206294const extensionsDir = path.join(process.cwd(), "extensions");
207295const failures: string[] = [];
208296209-for (const entry of fs.readdirSync(extensionsDir, { withFileTypes: true })) {
210-if (!entry.isDirectory() || entry.name.startsWith(".")) {
211-continue;
212-}
213-const pluginDir = path.join(extensionsDir, entry.name);
214-const manifestPath = path.join(pluginDir, "openclaw.plugin.json");
215-if (!fs.existsSync(manifestPath)) {
216-continue;
217-}
218-297+for (const manifestPath of listPluginManifestPaths(extensionsDir)) {
298+const pluginDir = path.dirname(manifestPath);
219299const manifest = readManifest(manifestPath);
220-const pluginId = typeof manifest.id === "string" ? manifest.id : entry.name;
300+const pluginId = typeof manifest.id === "string" ? manifest.id : path.basename(pluginDir);
221301const declaredTools = new Set(normalizeManifestTools(manifest.contracts?.tools));
222302const registeredNames = new Set<string>();
223303let registerCallCount = 0;
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。