
























@@ -1,7 +1,11 @@
1-import { readdirSync, readFileSync } from "node:fs";
2-import { describe, expect, it } from "vitest";
1+import { spawnSync } from "node:child_process";
2+import fs, { readFileSync } from "node:fs";
3+import { fileURLToPath } from "node:url";
4+import { describe, expect, it, vi } from "vitest";
3546const toolsDir = new URL("./", import.meta.url);
7+const toolsDirPath = fileURLToPath(toolsDir);
8+const repoRoot = fileURLToPath(new URL("../..", import.meta.url));
59const moduleReferencePattern =
610/\b(?:import|export)\s+(?:type\s+)?(?:[^"'`]*?\s+from\s+)?["']([^"']+)["']/gu;
711@@ -25,19 +29,92 @@ function collectStaticModuleReferences(
2529return references;
2630}
273132+function listProductionToolModuleFiles(): string[] {
33+const externalFiles = listExternalProductionToolModuleFiles();
34+if (externalFiles) {
35+return externalFiles;
36+}
37+return fs
38+.readdirSync(toolsDir, { withFileTypes: true })
39+.filter((entry) => entry.isFile())
40+.map((entry) => entry.name)
41+.filter((name) => name.endsWith(".ts") && !name.endsWith(".test.ts"))
42+.toSorted();
43+}
44+45+function listExternalProductionToolModuleFiles(): string[] | null {
46+return listGitProductionToolModuleFiles() ?? listFindProductionToolModuleFiles();
47+}
48+49+function listGitProductionToolModuleFiles(): string[] | null {
50+const result = spawnSync("git", ["ls-files", "--", "src/tools/*.ts"], {
51+cwd: repoRoot,
52+encoding: "utf8",
53+maxBuffer: 1024 * 1024,
54+stdio: ["ignore", "pipe", "ignore"],
55+});
56+if (result.status !== 0) {
57+return null;
58+}
59+return result.stdout
60+.split("\n")
61+.map((line) => line.trim())
62+.filter((line) => line.startsWith("src/tools/"))
63+.map((line) => line.slice("src/tools/".length))
64+.filter((name) => name.endsWith(".ts") && !name.endsWith(".test.ts"))
65+.toSorted();
66+}
67+68+function listFindProductionToolModuleFiles(): string[] | null {
69+const result = spawnSync(
70+"find",
71+[toolsDirPath, "-maxdepth", "1", "-type", "f", "-name", "*.ts"],
72+{
73+cwd: repoRoot,
74+encoding: "utf8",
75+maxBuffer: 1024 * 1024,
76+stdio: ["ignore", "pipe", "ignore"],
77+},
78+);
79+if (result.status !== 0) {
80+return null;
81+}
82+return result.stdout
83+.split("\n")
84+.map((line) => line.trim())
85+.filter((line) => line.length > 0)
86+.map((line) =>
87+line.slice(toolsDirPath.endsWith("/") ? toolsDirPath.length : toolsDirPath.length + 1),
88+)
89+.filter((name) => name.endsWith(".ts") && !name.endsWith(".test.ts"))
90+.toSorted();
91+}
92+2893describe("tool system boundary", () => {
94+it("lists production tool modules without scanning the tools directory in-process", () => {
95+const readDir = vi.spyOn(fs, "readdirSync");
96+try {
97+const files = listProductionToolModuleFiles();
98+99+expect(files.length).toBeGreaterThan(0);
100+expect(files.every((file) => file.endsWith(".ts") && !file.endsWith(".test.ts"))).toBe(
101+true,
102+);
103+expect(readDir).not.toHaveBeenCalled();
104+} finally {
105+readDir.mockRestore();
106+}
107+});
108+29109it("keeps production tool modules independent from OpenClaw subsystems", () => {
30-const violations = readdirSync(toolsDir, { withFileTypes: true }).flatMap((entry) => {
31-if (!entry.isFile() || !entry.name.endsWith(".ts") || entry.name.endsWith(".test.ts")) {
32-return [];
33-}
34-const source = readFileSync(new URL(entry.name, toolsDir), "utf8");
110+const violations = listProductionToolModuleFiles().flatMap((fileName) => {
111+const source = readFileSync(new URL(fileName, toolsDir), "utf8");
35112return collectStaticModuleReferences(source)
36113.filter(
37114(reference) =>
38115!reference.specifier.startsWith("./") && !reference.specifier.startsWith("node:"),
39116)
40-.map((reference) => `${entry.name}:${reference.line} ${reference.specifier}`);
117+.map((reference) => `${fileName}:${reference.line} ${reference.specifier}`);
41118});
4211943120expect(violations).toStrictEqual([]);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。