























@@ -0,0 +1,224 @@
1+import fs from "node:fs";
2+import { Module } from "node:module";
3+import path from "node:path";
4+import { describe, it } from "vitest";
5+6+describe("mirrored root runtime dependency drift guard", () => {
7+// Intentionally not mirrored at runtime: build-only / type-only / TUI-only
8+// tooling and packages that resolve transitively through other mirrored deps.
9+// If you change this set, document why in the comment beside the entry.
10+const KNOWN_UNMIRRORED_BARE_IMPORTS = new Set<string>([
11+"@mariozechner/pi-tui", // TUI mode runs from npm-global, not the gateway runtime mirror
12+"chalk", // available transitively via mirrored deps
13+"file-type", // available transitively via mirrored deps
14+"global-agent", // proxy bootstrap, only loaded when HTTP_PROXY is set
15+"ipaddr.js", // available transitively via mirrored deps
16+"proxy-agent", // available transitively via mirrored deps
17+"qrcode", // type-only import in src/media/qr-runtime.ts
18+"typescript", // CLI/dev only (api-baseline, jiti-runtime-api)
19+]);
20+21+function locateRepoRoot(): string {
22+let dir = path.resolve(import.meta.dirname);
23+for (let depth = 0; depth < 10; depth += 1) {
24+const candidate = path.join(dir, "package.json");
25+if (fs.existsSync(candidate)) {
26+try {
27+const data = JSON.parse(fs.readFileSync(candidate, "utf8")) as { name?: string };
28+if (data.name === "openclaw") {
29+return dir;
30+}
31+} catch {
32+// fall through
33+}
34+}
35+const parent = path.dirname(dir);
36+if (parent === dir) {
37+break;
38+}
39+dir = parent;
40+}
41+throw new Error("could not locate openclaw repo root from test file");
42+}
43+44+function readPackageJsonDeps(packageJsonPath: string): Set<string> {
45+const out = new Set<string>();
46+if (!fs.existsSync(packageJsonPath)) {
47+return out;
48+}
49+let parsed: {
50+dependencies?: Record<string, string>;
51+optionalDependencies?: Record<string, string>;
52+};
53+try {
54+parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
55+} catch {
56+return out;
57+}
58+for (const name of Object.keys(parsed.dependencies ?? {})) {
59+out.add(name);
60+}
61+for (const name of Object.keys(parsed.optionalDependencies ?? {})) {
62+out.add(name);
63+}
64+return out;
65+}
66+67+function readMirroredRootRuntimeDeps(repoRoot: string): Set<string> {
68+const parsed = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8")) as {
69+openclaw?: {
70+bundle?: {
71+mirroredRootRuntimeDependencies?: unknown;
72+};
73+};
74+};
75+const deps = parsed.openclaw?.bundle?.mirroredRootRuntimeDependencies;
76+return new Set(Array.isArray(deps) ? deps.filter((dep) => typeof dep === "string") : []);
77+}
78+79+function collectExtensionOwnedDeps(repoRoot: string): Set<string> {
80+const out = new Set<string>();
81+const extensionsDir = path.join(repoRoot, "extensions");
82+if (!fs.existsSync(extensionsDir)) {
83+return out;
84+}
85+for (const entry of fs.readdirSync(extensionsDir, { withFileTypes: true })) {
86+if (!entry.isDirectory()) {
87+continue;
88+}
89+for (const name of readPackageJsonDeps(
90+path.join(extensionsDir, entry.name, "package.json"),
91+)) {
92+out.add(name);
93+}
94+}
95+return out;
96+}
97+98+function walkCoreSourceFiles(repoRoot: string): string[] {
99+const srcDir = path.join(repoRoot, "src");
100+const files: string[] = [];
101+const queue: string[] = [srcDir];
102+while (queue.length > 0) {
103+const current = queue.shift();
104+if (!current) {
105+continue;
106+}
107+for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
108+const full = path.join(current, entry.name);
109+if (entry.isDirectory()) {
110+if (entry.name === "node_modules" || entry.name.startsWith(".")) {
111+continue;
112+}
113+queue.push(full);
114+continue;
115+}
116+if (!entry.isFile()) {
117+continue;
118+}
119+if (
120+/\.test\.tsx?$/u.test(entry.name) ||
121+/\.e2e\.test\.tsx?$/u.test(entry.name) ||
122+/\.test-helpers?\.tsx?$/u.test(entry.name) ||
123+/\.test-fixture\.tsx?$/u.test(entry.name) ||
124+entry.name.endsWith(".d.ts") ||
125+!/\.(?:ts|tsx|cjs|mjs|js)$/u.test(entry.name)
126+) {
127+continue;
128+}
129+files.push(full);
130+}
131+}
132+return files;
133+}
134+135+function packageNameFromBareSpecifier(specifier: string): string | null {
136+if (
137+specifier.startsWith(".") ||
138+specifier.startsWith("/") ||
139+specifier.startsWith("node:") ||
140+specifier.startsWith("#")
141+) {
142+return null;
143+}
144+const [first, second] = specifier.split("/");
145+if (!first) {
146+return null;
147+}
148+return first.startsWith("@") && second ? `${first}/${second}` : first;
149+}
150+151+// Match value imports (`import x from 'y'`, `import 'y'`, `require('y')`,
152+// `import('y')`) but skip `import type` to avoid noise from type-only imports.
153+const VALUE_IMPORT_PATTERNS = [
154+/(?:^|[;\n])\s*import\s+(?!type\b)(?:[^'"()]+?\s+from\s+)?["']([^"']+)["']/g,
155+/\brequire\s*\(\s*["']([^"']+)["']\s*\)/g,
156+/\bimport\s*\(\s*["']([^"']+)["']\s*\)/g,
157+] as const;
158+159+it("every value-imported root-package dep in src/ is mirrored or owned by an extension", () => {
160+const repoRoot = locateRepoRoot();
161+const rootDeps = readPackageJsonDeps(path.join(repoRoot, "package.json"));
162+const extensionDeps = collectExtensionOwnedDeps(repoRoot);
163+const mirroredCore = readMirroredRootRuntimeDeps(repoRoot);
164+const nodeBuiltins = new Set<string>(Module.builtinModules);
165+166+const violations = new Map<string, string>();
167+for (const file of walkCoreSourceFiles(repoRoot)) {
168+const source = fs.readFileSync(file, "utf8");
169+const specifiers = new Set<string>();
170+for (const pattern of VALUE_IMPORT_PATTERNS) {
171+for (const match of source.matchAll(pattern)) {
172+if (match[1]) {
173+specifiers.add(match[1]);
174+}
175+}
176+}
177+for (const specifier of specifiers) {
178+const packageName = packageNameFromBareSpecifier(specifier);
179+if (!packageName) {
180+continue;
181+}
182+if (nodeBuiltins.has(packageName)) {
183+continue;
184+}
185+if (packageName === "openclaw" || packageName.startsWith("@openclaw/")) {
186+continue;
187+}
188+if (mirroredCore.has(packageName) || extensionDeps.has(packageName)) {
189+continue;
190+}
191+if (KNOWN_UNMIRRORED_BARE_IMPORTS.has(packageName)) {
192+continue;
193+}
194+if (!rootDeps.has(packageName)) {
195+// Not a root runtime dep; not our concern (could be a peer/dev import
196+// that resolves through some other path; the mirror does not own it).
197+continue;
198+}
199+if (!violations.has(packageName)) {
200+violations.set(packageName, path.relative(repoRoot, file).replaceAll(path.sep, "/"));
201+}
202+}
203+}
204+205+if (violations.size > 0) {
206+const summary = [...violations.entries()]
207+.toSorted(([left], [right]) => left.localeCompare(right))
208+.map(([packageName, filePath]) => ` - ${packageName} (e.g. ${filePath})`)
209+.join("\n");
210+throw new Error(
211+[
212+"Bare imports found in src/ that are root-package runtime deps but are neither",
213+"in package.json openclaw.bundle.mirroredRootRuntimeDependencies nor declared by any extension's package.json.",
214+"These will be missing from the runtime-deps mirror at gateway start and Node",
215+"will fail to resolve them. Either add the package to openclaw.bundle.mirroredRootRuntimeDependencies,",
216+"declare it under an owning extension's dependencies, or add it to",
217+"KNOWN_UNMIRRORED_BARE_IMPORTS in this test with a comment explaining why.",
218+"",
219+summary,
220+].join("\n"),
221+);
222+}
223+});
224+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。