


















@@ -2,6 +2,7 @@ import { spawn, spawnSync } from "node:child_process";
22import { createHash } from "node:crypto";
33import { EventEmitter } from "node:events";
44import fs from "node:fs";
5+import { Module } from "node:module";
56import os from "node:os";
67import path from "node:path";
78import { afterEach, describe, expect, it, vi } from "vitest";
@@ -2929,3 +2930,224 @@ describe("ensureBundledPluginRuntimeDeps", () => {
29292930expect(fs.existsSync(path.join(pluginRoot, "node_modules", "zod", "package.json"))).toBe(true);
29302931});
29312932});
2933+2934+describe("MIRRORED_CORE_RUNTIME_DEP_NAMES drift guard", () => {
2935+// Intentionally not mirrored at runtime: build-only / type-only / TUI-only
2936+// tooling and packages that resolve transitively through other mirrored deps.
2937+// If you change this set, document why in the comment beside the entry.
2938+const KNOWN_UNMIRRORED_BARE_IMPORTS = new Set<string>([
2939+"@mariozechner/pi-tui", // TUI mode runs from npm-global, not the gateway runtime mirror
2940+"chalk", // available transitively via mirrored deps
2941+"file-type", // available transitively via mirrored deps
2942+"global-agent", // proxy bootstrap, only loaded when HTTP_PROXY is set
2943+"ipaddr.js", // available transitively via mirrored deps
2944+"proxy-agent", // available transitively via mirrored deps
2945+"qrcode", // type-only import in src/media/qr-runtime.ts
2946+"typescript", // CLI/dev only (api-baseline, jiti-runtime-api)
2947+]);
2948+2949+function locateRepoRoot(): string {
2950+let dir = path.resolve(import.meta.dirname);
2951+for (let depth = 0; depth < 10; depth += 1) {
2952+const candidate = path.join(dir, "package.json");
2953+if (fs.existsSync(candidate)) {
2954+try {
2955+const data = JSON.parse(fs.readFileSync(candidate, "utf8")) as { name?: string };
2956+if (data.name === "openclaw") {
2957+return dir;
2958+}
2959+} catch {
2960+// fall through
2961+}
2962+}
2963+const parent = path.dirname(dir);
2964+if (parent === dir) {
2965+break;
2966+}
2967+dir = parent;
2968+}
2969+throw new Error("could not locate openclaw repo root from test file");
2970+}
2971+2972+function readPackageJsonDeps(packageJsonPath: string): Set<string> {
2973+const out = new Set<string>();
2974+if (!fs.existsSync(packageJsonPath)) {
2975+return out;
2976+}
2977+let parsed: {
2978+dependencies?: Record<string, string>;
2979+optionalDependencies?: Record<string, string>;
2980+};
2981+try {
2982+parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
2983+} catch {
2984+return out;
2985+}
2986+for (const name of Object.keys(parsed.dependencies ?? {})) {
2987+out.add(name);
2988+}
2989+for (const name of Object.keys(parsed.optionalDependencies ?? {})) {
2990+out.add(name);
2991+}
2992+return out;
2993+}
2994+2995+function collectExtensionOwnedDeps(repoRoot: string): Set<string> {
2996+const out = new Set<string>();
2997+const extensionsDir = path.join(repoRoot, "extensions");
2998+if (!fs.existsSync(extensionsDir)) {
2999+return out;
3000+}
3001+for (const entry of fs.readdirSync(extensionsDir, { withFileTypes: true })) {
3002+if (!entry.isDirectory()) {
3003+continue;
3004+}
3005+for (const name of readPackageJsonDeps(
3006+path.join(extensionsDir, entry.name, "package.json"),
3007+)) {
3008+out.add(name);
3009+}
3010+}
3011+return out;
3012+}
3013+3014+function walkCoreSourceFiles(repoRoot: string): string[] {
3015+const srcDir = path.join(repoRoot, "src");
3016+const files: string[] = [];
3017+const queue: string[] = [srcDir];
3018+while (queue.length > 0) {
3019+const current = queue.shift();
3020+if (!current) {
3021+continue;
3022+}
3023+for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
3024+const full = path.join(current, entry.name);
3025+if (entry.isDirectory()) {
3026+if (entry.name === "node_modules" || entry.name.startsWith(".")) {
3027+continue;
3028+}
3029+queue.push(full);
3030+continue;
3031+}
3032+if (!entry.isFile()) {
3033+continue;
3034+}
3035+if (
3036+/\.test\.tsx?$/u.test(entry.name) ||
3037+/\.e2e\.test\.tsx?$/u.test(entry.name) ||
3038+/\.test-helpers?\.tsx?$/u.test(entry.name) ||
3039+/\.test-fixture\.tsx?$/u.test(entry.name) ||
3040+entry.name.endsWith(".d.ts") ||
3041+!/\.(?:ts|tsx|cjs|mjs|js)$/u.test(entry.name)
3042+) {
3043+continue;
3044+}
3045+files.push(full);
3046+}
3047+}
3048+return files;
3049+}
3050+3051+function packageNameFromBareSpecifier(specifier: string): string | null {
3052+if (
3053+specifier.startsWith(".") ||
3054+specifier.startsWith("/") ||
3055+specifier.startsWith("node:") ||
3056+specifier.startsWith("#")
3057+) {
3058+return null;
3059+}
3060+const [first, second] = specifier.split("/");
3061+if (!first) {
3062+return null;
3063+}
3064+return first.startsWith("@") && second ? `${first}/${second}` : first;
3065+}
3066+3067+// Match value imports (`import x from 'y'`, `import 'y'`, `require('y')`,
3068+// `import('y')`) but skip `import type` to avoid noise from type-only imports.
3069+const VALUE_IMPORT_PATTERNS = [
3070+/(?:^|[;\n])\s*import\s+(?!type\b)(?:[^'"()]+?\s+from\s+)?["']([^"']+)["']/g,
3071+/\brequire\s*\(\s*["']([^"']+)["']\s*\)/g,
3072+/\bimport\s*\(\s*["']([^"']+)["']\s*\)/g,
3073+] as const;
3074+3075+it("every value-imported root-package dep in src/ is mirrored or owned by an extension", () => {
3076+const repoRoot = locateRepoRoot();
3077+const rootDeps = readPackageJsonDeps(path.join(repoRoot, "package.json"));
3078+const extensionDeps = collectExtensionOwnedDeps(repoRoot);
3079+const mirroredCore = new Set<string>([
3080+"@agentclientprotocol/sdk",
3081+"@lydell/node-pty",
3082+"croner",
3083+"dotenv",
3084+"jiti",
3085+"json5",
3086+"jszip",
3087+"markdown-it",
3088+"semver",
3089+"tar",
3090+"tslog",
3091+"web-push",
3092+]);
3093+const nodeBuiltins = new Set<string>(Module.builtinModules);
3094+3095+const violations = new Map<string, string>();
3096+for (const file of walkCoreSourceFiles(repoRoot)) {
3097+const source = fs.readFileSync(file, "utf8");
3098+const specifiers = new Set<string>();
3099+for (const pattern of VALUE_IMPORT_PATTERNS) {
3100+for (const match of source.matchAll(pattern)) {
3101+if (match[1]) {
3102+specifiers.add(match[1]);
3103+}
3104+}
3105+}
3106+for (const specifier of specifiers) {
3107+const packageName = packageNameFromBareSpecifier(specifier);
3108+if (!packageName) {
3109+continue;
3110+}
3111+if (nodeBuiltins.has(packageName)) {
3112+continue;
3113+}
3114+if (packageName === "openclaw" || packageName.startsWith("@openclaw/")) {
3115+continue;
3116+}
3117+if (mirroredCore.has(packageName) || extensionDeps.has(packageName)) {
3118+continue;
3119+}
3120+if (KNOWN_UNMIRRORED_BARE_IMPORTS.has(packageName)) {
3121+continue;
3122+}
3123+if (!rootDeps.has(packageName)) {
3124+// Not a root runtime dep; not our concern (could be a peer/dev import
3125+// that resolves through some other path; the mirror does not own it).
3126+continue;
3127+}
3128+if (!violations.has(packageName)) {
3129+violations.set(packageName, path.relative(repoRoot, file).replaceAll(path.sep, "/"));
3130+}
3131+}
3132+}
3133+3134+if (violations.size > 0) {
3135+const summary = [...violations.entries()]
3136+.toSorted(([left], [right]) => left.localeCompare(right))
3137+.map(([packageName, filePath]) => ` - ${packageName} (e.g. ${filePath})`)
3138+.join("\n");
3139+throw new Error(
3140+[
3141+"Bare imports found in src/ that are root-package runtime deps but are neither",
3142+"in MIRRORED_CORE_RUNTIME_DEP_NAMES nor declared by any extension's package.json.",
3143+"These will be missing from the runtime-deps mirror at gateway start and Node",
3144+"will fail to resolve them. Either add the package to MIRRORED_CORE_RUNTIME_DEP_NAMES,",
3145+"declare it under an owning extension's dependencies, or add it to",
3146+"KNOWN_UNMIRRORED_BARE_IMPORTS in this test with a comment explaining why.",
3147+"",
3148+summary,
3149+].join("\n"),
3150+);
3151+}
3152+});
3153+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。