Download src/plugins/contracts/plugin-tool-contracts.test.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 9.78 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/plugins/contracts/plugin-tool-contracts.test.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/plugins/contracts/plugin-tool-contracts.test.ts
-
curl -L -o plugin-tool-contracts.test.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/plugins/contracts/plugin-tool-contracts.test.ts
9.78 kB
| // Plugin tool contract tests cover bundled plugin tool schemas and invocation contracts. | |
| import fs from "node:fs"; | |
| import path from "node:path"; | |
| import { beforeAll, describe, expect, it } from "vitest"; | |
| import { expectNoReaddirSyncDuring } from "../../test-utils/fs-scan-assertions.js"; | |
| import { | |
| listGitTrackedFiles, | |
| toRepoPath, | |
| toRepoRelativePath, | |
| } from "../../test-utils/repo-files.js"; | |
| type PluginManifestFile = { | |
| id?: unknown; | |
| contracts?: { | |
| tools?: unknown; | |
| }; | |
| }; | |
| function walkFiles(dir: string): string[] { | |
| const gitFiles = listGitFiles(dir); | |
| if (gitFiles) { | |
| return gitFiles; | |
| } | |
| const files: string[] = []; | |
| for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { | |
| if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) { | |
| continue; | |
| } | |
| const entryPath = path.join(dir, entry.name); | |
| if (entry.isDirectory()) { | |
| files.push(...walkFiles(entryPath)); | |
| continue; | |
| } | |
| files.push(entryPath); | |
| } | |
| return files; | |
| } | |
| function repoRelativePath(filePath: string): string { | |
| return toRepoRelativePath(process.cwd(), filePath); | |
| } | |
| function isSkippedRepoPath(relativePath: string): boolean { | |
| return relativePath | |
| .split("/") | |
| .some((part) => part === "node_modules" || part === "dist" || part.startsWith(".")); | |
| } | |
| function listGitFiles(dir: string): string[] | null { | |
| const relativeDir = repoRelativePath(dir); | |
| if (!relativeDir || relativeDir.startsWith("..") || path.isAbsolute(relativeDir)) { | |
| return null; | |
| } | |
| const files = listGitTrackedFiles({ pathspecs: relativeDir }); | |
| if (!files) { | |
| return null; | |
| } | |
| return files | |
| .filter((line) => !isSkippedRepoPath(line)) | |
| .map((line) => path.join(process.cwd(), ...line.split("/"))) | |
| .filter((filePath) => fs.existsSync(filePath)) | |
| .toSorted(); | |
| } | |
| function listGitPluginManifestPaths(extensionsDir: string): string[] | null { | |
| const relativeDir = repoRelativePath(extensionsDir); | |
| if (!relativeDir || relativeDir.startsWith("..") || path.isAbsolute(relativeDir)) { | |
| return null; | |
| } | |
| const files = listGitTrackedFiles({ pathspecs: relativeDir }); | |
| if (!files) { | |
| return null; | |
| } | |
| return files | |
| .filter((line) => /^extensions\/[^/]+\/openclaw\.plugin\.json$/u.test(line)) | |
| .map((line) => path.join(process.cwd(), ...line.split("/"))) | |
| .filter((filePath) => fs.existsSync(filePath)) | |
| .toSorted(); | |
| } | |
| function listPluginManifestPaths(extensionsDir: string): string[] { | |
| const gitPaths = listGitPluginManifestPaths(extensionsDir); | |
| if (gitPaths) { | |
| return gitPaths; | |
| } | |
| return fs | |
| .readdirSync(extensionsDir, { withFileTypes: true }) | |
| .filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")) | |
| .map((entry) => path.join(extensionsDir, entry.name, "openclaw.plugin.json")) | |
| .filter((manifestPath) => fs.existsSync(manifestPath)); | |
| } | |
| function isProductionSource(filePath: string): boolean { | |
| if (!/\.(?:cjs|mjs|js|ts|tsx)$/.test(filePath)) { | |
| return false; | |
| } | |
| const normalized = toRepoPath(filePath); | |
| return !/(\.test\.|\.spec\.|\/__tests__\/|\/test-support\/)/.test(normalized); | |
| } | |
| function readBalancedCallArguments(source: string, openParenIndex: number): string | undefined { | |
| let depth = 0; | |
| let quote: '"' | "'" | "`" | undefined; | |
| let escaped = false; | |
| for (let index = openParenIndex; index < source.length; index += 1) { | |
| const char = source[index]; | |
| if (!char) { | |
| continue; | |
| } | |
| if (quote) { | |
| if (escaped) { | |
| escaped = false; | |
| continue; | |
| } | |
| if (char === "\\") { | |
| escaped = true; | |
| continue; | |
| } | |
| if (char === quote) { | |
| quote = undefined; | |
| } | |
| continue; | |
| } | |
| if (char === '"' || char === "'" || char === "`") { | |
| quote = char; | |
| continue; | |
| } | |
| if (char === "(" || char === "{" || char === "[") { | |
| depth += 1; | |
| continue; | |
| } | |
| if (char === ")" || char === "}" || char === "]") { | |
| depth -= 1; | |
| if (depth === 0 && char === ")") { | |
| return source.slice(openParenIndex + 1, index); | |
| } | |
| } | |
| } | |
| return undefined; | |
| } | |
| function listRegisterToolCalls(source: string): string[] { | |
| const calls: string[] = []; | |
| const pattern = /\bregisterTool\s*\(/g; | |
| let match: RegExpExecArray | null; | |
| while ((match = pattern.exec(source))) { | |
| const openParenIndex = source.indexOf("(", match.index); | |
| const args = readBalancedCallArguments(source, openParenIndex); | |
| if (args !== undefined) { | |
| calls.push(args); | |
| } | |
| } | |
| return calls; | |
| } | |
| function splitTopLevelArgs(args: string): string[] { | |
| const parts: string[] = []; | |
| let depth = 0; | |
| let quote: '"' | "'" | "`" | undefined; | |
| let escaped = false; | |
| let start = 0; | |
| for (let index = 0; index < args.length; index += 1) { | |
| const char = args[index]; | |
| if (!char) { | |
| continue; | |
| } | |
| if (quote) { | |
| if (escaped) { | |
| escaped = false; | |
| continue; | |
| } | |
| if (char === "\\") { | |
| escaped = true; | |
| continue; | |
| } | |
| if (char === quote) { | |
| quote = undefined; | |
| } | |
| continue; | |
| } | |
| if (char === '"' || char === "'" || char === "`") { | |
| quote = char; | |
| continue; | |
| } | |
| if (char === "(" || char === "{" || char === "[") { | |
| depth += 1; | |
| continue; | |
| } | |
| if (char === ")" || char === "}" || char === "]") { | |
| depth -= 1; | |
| continue; | |
| } | |
| if (char === "," && depth === 0) { | |
| const part = args.slice(start, index).trim(); | |
| if (part.length > 0) { | |
| parts.push(part); | |
| } | |
| start = index + 1; | |
| } | |
| } | |
| const part = args.slice(start).trim(); | |
| if (part.length > 0) { | |
| parts.push(part); | |
| } | |
| return parts; | |
| } | |
| function extractStringLiterals(source: string): string[] { | |
| const names: string[] = []; | |
| const pattern = /["']([^"']+)["']/g; | |
| let match: RegExpExecArray | null; | |
| while ((match = pattern.exec(source))) { | |
| if (match[1]) { | |
| names.push(match[1]); | |
| } | |
| } | |
| return names; | |
| } | |
| function extractStaticRegisteredToolNamesFromObject(source: string): string[] { | |
| const names = new Set<string>(); | |
| const namesPattern = /\bnames\s*:\s*\[([\s\S]*?)\]/g; | |
| let namesMatch: RegExpExecArray | null; | |
| while ((namesMatch = namesPattern.exec(source))) { | |
| for (const name of extractStringLiterals(namesMatch[1] ?? "")) { | |
| names.add(name); | |
| } | |
| } | |
| const namePattern = /\bname\s*:\s*["']([^"']+)["']/g; | |
| let nameMatch: RegExpExecArray | null; | |
| while ((nameMatch = namePattern.exec(source))) { | |
| if (nameMatch[1]) { | |
| names.add(nameMatch[1]); | |
| } | |
| } | |
| return [...names]; | |
| } | |
| function extractStaticRegisteredToolNames(callArgs: string): string[] { | |
| const args = splitTopLevelArgs(callArgs); | |
| const names = new Set<string>(); | |
| const firstArg = args[0]?.trim() ?? ""; | |
| const optionsArg = args[1]?.trim() ?? ""; | |
| if (firstArg.startsWith("{")) { | |
| for (const name of extractStaticRegisteredToolNamesFromObject(firstArg)) { | |
| names.add(name); | |
| } | |
| } | |
| if (optionsArg.startsWith("{")) { | |
| for (const name of extractStaticRegisteredToolNamesFromObject(optionsArg)) { | |
| names.add(name); | |
| } | |
| } | |
| return [...names]; | |
| } | |
| function readManifest(manifestPath: string): PluginManifestFile { | |
| return JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as PluginManifestFile; | |
| } | |
| function normalizeManifestTools(value: unknown): string[] { | |
| if (!Array.isArray(value)) { | |
| return []; | |
| } | |
| return value.filter((item): item is string => typeof item === "string" && item.trim() !== ""); | |
| } | |
| describe("bundled plugin tool manifest contracts", () => { | |
| let toolContractFailures: string[] = []; | |
| beforeAll(() => { | |
| listGitTrackedFiles({ pathspecs: "extensions" }); | |
| toolContractFailures = collectToolContractFailures(path.join(process.cwd(), "extensions")); | |
| }); | |
| it("lists plugin tool contract inputs from git without walking extension roots", () => { | |
| const extensionsDir = path.join(process.cwd(), "extensions"); | |
| expectNoReaddirSyncDuring(() => { | |
| const manifestPaths = listPluginManifestPaths(extensionsDir); | |
| const sourceFiles = manifestPaths[0] | |
| ? walkFiles(path.dirname(manifestPaths[0])).filter(isProductionSource) | |
| : []; | |
| expect(manifestPaths.length).toBeGreaterThan(0); | |
| expect(sourceFiles.length).toBeGreaterThan(0); | |
| }); | |
| }); | |
| it("declares every production registerTool owner in contracts.tools", () => { | |
| expect(toolContractFailures).toStrictEqual([]); | |
| }); | |
| }); | |
| function collectToolContractFailures(extensionsDir: string): string[] { | |
| const failures: string[] = []; | |
| for (const manifestPath of listPluginManifestPaths(extensionsDir)) { | |
| const pluginDir = path.dirname(manifestPath); | |
| const manifest = readManifest(manifestPath); | |
| const pluginId = typeof manifest.id === "string" ? manifest.id : path.basename(pluginDir); | |
| const declaredTools = new Set(normalizeManifestTools(manifest.contracts?.tools)); | |
| const registeredNames = new Set<string>(); | |
| let registerCallCount = 0; | |
| for (const filePath of walkFiles(pluginDir).filter(isProductionSource)) { | |
| const source = fs.readFileSync(filePath, "utf-8"); | |
| for (const call of listRegisterToolCalls(source)) { | |
| registerCallCount += 1; | |
| for (const name of extractStaticRegisteredToolNames(call)) { | |
| registeredNames.add(name); | |
| } | |
| } | |
| } | |
| if (registerCallCount === 0) { | |
| continue; | |
| } | |
| if (declaredTools.size === 0) { | |
| failures.push(`${pluginId}: registers agent tools but has no contracts.tools`); | |
| continue; | |
| } | |
| const missing = [...registeredNames].filter((name) => !declaredTools.has(name)).toSorted(); | |
| if (missing.length > 0) { | |
| failures.push(`${pluginId}: missing contracts.tools for ${missing.join(", ")}`); | |
| } | |
| } | |
| return failures; | |
| } | |