From aa6ea4510ca90789e3768d24f77ab35cdbcec751 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 16 May 2026 10:46:36 +0800 Subject: [PATCH] fix(test): avoid walking channel import guardrails --- .../channel-import-guardrails.test.ts | 85 +++++++++++++++++-- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/src/channels/plugins/contracts/channel-import-guardrails.test.ts b/src/channels/plugins/contracts/channel-import-guardrails.test.ts index 25aeccbb941..9978064bdb5 100644 --- a/src/channels/plugins/contracts/channel-import-guardrails.test.ts +++ b/src/channels/plugins/contracts/channel-import-guardrails.test.ts @@ -1,7 +1,8 @@ -import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; import { basename, dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { classifyBundledExtensionSourcePath } from "../../../../scripts/lib/extension-source-classifier.mjs"; import { GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES } from "../../../plugin-sdk/test-helpers/public-artifacts.js"; import { loadPluginManifestRegistry } from "../../../plugins/manifest-registry.js"; @@ -48,7 +49,7 @@ const GUARDED_CHANNEL_EXTENSIONS = new Set([ function resolveBundledPluginSourceRoot(rootDir: string): string { const sourceRoot = resolve(REPO_ROOT, BUNDLED_PLUGIN_ROOT_DIR, basename(rootDir)); - return existsSync(sourceRoot) ? sourceRoot : rootDir; + return fs.existsSync(sourceRoot) ? sourceRoot : rootDir; } function bundledPluginFile(pluginId: string, relativePath: string): string { @@ -261,6 +262,7 @@ const RE_EXPORT_STAR_RE = const RE_EXPORT_NAMED_RE = /^\s*export\s+(?:type\s+)?\{[^}]*\}\s+from\s*["']([^"']+)["']/gmu; const DYNAMIC_IMPORT_RE = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/gmu; const REQUIRE_RE = /\brequire\s*\(\s*["']([^"']+)["']\s*\)/gmu; +const trackedSourceFilesByRoot = new Map(); type SourceFileCollectorOptions = { rootDir: string; @@ -274,7 +276,7 @@ function readSource(path: string): string { if (cached !== undefined) { return cached; } - const text = readFileSync(fullPath, "utf8"); + const text = fs.readFileSync(fullPath, "utf8"); sourceTextCache.set(fullPath, text); return text; } @@ -283,6 +285,58 @@ function normalizePath(path: string): string { return path.replaceAll("\\", "/"); } +function repoRelativePath(path: string): string { + const normalizedRepoRoot = normalizePath(REPO_ROOT); + const normalizedPath = normalizePath(path); + return normalizedPath.startsWith(normalizedRepoRoot) + ? normalizedPath.slice(normalizedRepoRoot.length + 1) + : normalizedPath; +} + +function listTrackedSourceFiles(options: SourceFileCollectorOptions): string[] | null { + const relativeRoot = repoRelativePath(options.rootDir); + if (!relativeRoot || relativeRoot.startsWith("..")) { + return null; + } + if (trackedSourceFilesByRoot.has(relativeRoot)) { + const files = trackedSourceFilesByRoot.get(relativeRoot); + return files ? [...files] : null; + } + const result = spawnSync("git", ["ls-files", "--", relativeRoot], { + cwd: REPO_ROOT, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + if (result.status !== 0) { + trackedSourceFilesByRoot.set(relativeRoot, null); + return null; + } + const files = result.stdout + .split("\n") + .map((line) => line.trim().replaceAll("\\", "/")) + .filter((line) => { + if (!/\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/u.test(line) || line.endsWith(".d.ts")) { + return false; + } + const parts = line.split("/"); + return !parts.some( + (part) => part === "node_modules" || part === "dist" || part === "coverage", + ); + }) + .map((line) => resolve(REPO_ROOT, line)) + .filter((fullPath) => { + const normalizedFullPath = normalizePath(fullPath); + const entryName = basename(fullPath); + return !( + options.shouldSkipPath?.(normalizedFullPath) || + options.shouldSkipEntry?.({ entryName, normalizedFullPath }) + ); + }) + .toSorted(); + trackedSourceFilesByRoot.set(relativeRoot, files); + return [...files]; +} + function collectSourceFiles( cached: string[] | undefined | null, options: SourceFileCollectorOptions, @@ -290,6 +344,11 @@ function collectSourceFiles( if (cached) { return cached; } + const trackedFiles = listTrackedSourceFiles(options); + if (trackedFiles) { + return trackedFiles; + } + const files: string[] = []; const stack = [options.rootDir]; while (stack.length > 0) { @@ -297,7 +356,7 @@ function collectSourceFiles( if (!current) { continue; } - for (const entry of readdirSync(current, { withFileTypes: true })) { + for (const entry of fs.readdirSync(current, { withFileTypes: true })) { const fullPath = resolve(current, entry.name); const normalizedFullPath = normalizePath(fullPath); if (entry.isDirectory()) { @@ -529,6 +588,22 @@ function expectCoreSourceStaysOffPluginSpecificSdkFacades(file: string, imports: } describe("channel import guardrails", () => { + it("lists channel import guardrail sources from git without walking roots", () => { + const readDir = vi.spyOn(fs, "readdirSync"); + try { + const extensionSources = collectExtensionSourceFiles(); + const coreSources = collectCoreSourceFiles(); + const telegramSources = collectExtensionFiles("telegram"); + + expect(extensionSources.length).toBeGreaterThan(0); + expect(coreSources.length).toBeGreaterThan(0); + expect(telegramSources.length).toBeGreaterThan(0); + expect(readDir).not.toHaveBeenCalled(); + } finally { + readDir.mockRestore(); + } + }); + it("keeps channel helper modules off their own SDK barrels", () => { for (const source of SAME_CHANNEL_SDK_GUARDS) { const text = readSource(source.path);