fix(test): avoid walking channel import guardrails

This commit is contained in:
Vincent Koc
2026-05-16 10:46:36 +08:00
parent 445c7d050a
commit aa6ea4510c

View File

@@ -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<string, readonly string[] | null>();
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);