mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-05-01 06:36:23 +08:00
refactor: tighten extension test support boundaries
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
6c33fafd66396c2c769b8f8d1f5d0792beb8b4b2d23b5f0323ff2bb2d0a79805 plugin-sdk-api-baseline.json
|
||||
27336f313c029843be409d22d79caf6f45884cf37458d9f52c0a88c4e8b268fd plugin-sdk-api-baseline.jsonl
|
||||
343a555f212dd5ebf26dccbefff1cb4b56a08e4dcc2c801ac7ab5fb98973192a plugin-sdk-api-baseline.json
|
||||
02aaccbe13f261de2d41fcb4270fc9ae70b931966089e56d21a4ebc8e80c8821 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -45,6 +45,9 @@ plugins.
|
||||
|
||||
Prefer the focused subpaths below for new plugin tests. The broad
|
||||
`openclaw/plugin-sdk/testing` barrel is legacy compatibility only.
|
||||
Repo guardrails reject new real imports from `plugin-sdk/testing` and
|
||||
`plugin-sdk/test-utils`; those names remain only as deprecated compatibility
|
||||
surfaces for external plugins and compatibility-record tests.
|
||||
|
||||
```typescript
|
||||
import {
|
||||
|
||||
@@ -1,2 +1,41 @@
|
||||
import { buildDmGroupAccountAllowlistAdapter } from "openclaw/plugin-sdk/allowlist-config-edit";
|
||||
import type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
||||
import { getChatChannelMeta } from "openclaw/plugin-sdk/channel-plugin-common";
|
||||
import { resolveTelegramAccount, type ResolvedTelegramAccount } from "./src/accounts.js";
|
||||
import { telegramApprovalCapability } from "./src/approval-native.js";
|
||||
import { telegramConfigAdapter } from "./src/shared.js";
|
||||
|
||||
export { sendMessageTelegram, sendPollTelegram, type TelegramApiOverride } from "./src/send.js";
|
||||
export { resetTelegramThreadBindingsForTests } from "./src/thread-bindings.js";
|
||||
|
||||
export const telegramCommandTestPlugin = {
|
||||
id: "telegram",
|
||||
meta: getChatChannelMeta("telegram"),
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group", "channel", "thread"],
|
||||
reactions: true,
|
||||
threads: true,
|
||||
media: true,
|
||||
polls: true,
|
||||
nativeCommands: true,
|
||||
blockStreaming: true,
|
||||
},
|
||||
config: telegramConfigAdapter,
|
||||
approvalCapability: telegramApprovalCapability,
|
||||
pairing: {
|
||||
idLabel: "telegramUserId",
|
||||
},
|
||||
allowlist: buildDmGroupAccountAllowlistAdapter<ResolvedTelegramAccount>({
|
||||
channelId: "telegram",
|
||||
resolveAccount: resolveTelegramAccount,
|
||||
normalize: ({ cfg, accountId, values }) =>
|
||||
telegramConfigAdapter.formatAllowFrom!({ cfg, accountId, allowFrom: values }),
|
||||
resolveDmAllowFrom: (account) => account.config.allowFrom,
|
||||
resolveGroupAllowFrom: (account) => account.config.groupAllowFrom,
|
||||
resolveDmPolicy: (account) => account.config.dmPolicy,
|
||||
resolveGroupPolicy: (account) => account.config.groupPolicy,
|
||||
}),
|
||||
} satisfies Pick<
|
||||
ChannelPlugin<ResolvedTelegramAccount>,
|
||||
"id" | "meta" | "capabilities" | "config" | "approvalCapability" | "pairing" | "allowlist"
|
||||
>;
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { buildDmGroupAccountAllowlistAdapter } from "openclaw/plugin-sdk/allowlist-config-edit";
|
||||
import type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
||||
import { getChatChannelMeta } from "openclaw/plugin-sdk/channel-plugin-common";
|
||||
import type { ResolvedTelegramAccount } from "./src/accounts.js";
|
||||
import { resolveTelegramAccount } from "./src/accounts.js";
|
||||
import { telegramApprovalCapability } from "./src/approval-native.js";
|
||||
import { telegramConfigAdapter } from "./src/shared.js";
|
||||
|
||||
export const telegramCommandTestPlugin = {
|
||||
id: "telegram",
|
||||
meta: getChatChannelMeta("telegram"),
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group", "channel", "thread"],
|
||||
reactions: true,
|
||||
threads: true,
|
||||
media: true,
|
||||
polls: true,
|
||||
nativeCommands: true,
|
||||
blockStreaming: true,
|
||||
},
|
||||
config: telegramConfigAdapter,
|
||||
approvalCapability: telegramApprovalCapability,
|
||||
pairing: {
|
||||
idLabel: "telegramUserId",
|
||||
},
|
||||
allowlist: buildDmGroupAccountAllowlistAdapter<ResolvedTelegramAccount>({
|
||||
channelId: "telegram",
|
||||
resolveAccount: resolveTelegramAccount,
|
||||
normalize: ({ cfg, accountId, values }) =>
|
||||
telegramConfigAdapter.formatAllowFrom!({ cfg, accountId, allowFrom: values }),
|
||||
resolveDmAllowFrom: (account) => account.config.allowFrom,
|
||||
resolveGroupAllowFrom: (account) => account.config.groupAllowFrom,
|
||||
resolveDmPolicy: (account) => account.config.dmPolicy,
|
||||
resolveGroupPolicy: (account) => account.config.groupPolicy,
|
||||
}),
|
||||
} satisfies Pick<
|
||||
ChannelPlugin<ResolvedTelegramAccount>,
|
||||
"id" | "meta" | "capabilities" | "config" | "approvalCapability" | "pairing" | "allowlist"
|
||||
>;
|
||||
@@ -6,14 +6,14 @@ import {
|
||||
createLifecycleMonitorSetup,
|
||||
expectImageLifecycleDelivery,
|
||||
settleAsyncWork,
|
||||
} from "../test-support/lifecycle-test-support.js";
|
||||
} from "./test-support/lifecycle-test-support.js";
|
||||
import {
|
||||
getUpdatesMock,
|
||||
getZaloRuntimeMock,
|
||||
loadCachedLifecycleMonitorModule,
|
||||
resetLifecycleTestState,
|
||||
sendMessageMock,
|
||||
} from "../test-support/monitor-mocks-test-support.js";
|
||||
} from "./test-support/monitor-mocks-test-support.js";
|
||||
|
||||
describe("Zalo polling image handling", () => {
|
||||
const {
|
||||
|
||||
@@ -5,13 +5,13 @@ import {
|
||||
createTextUpdate,
|
||||
postWebhookReplay,
|
||||
settleAsyncWork,
|
||||
} from "../test-support/lifecycle-test-support.js";
|
||||
} from "./test-support/lifecycle-test-support.js";
|
||||
import {
|
||||
resetLifecycleTestState,
|
||||
sendMessageMock,
|
||||
setLifecycleRuntimeCore,
|
||||
startWebhookLifecycleMonitor,
|
||||
} from "../test-support/monitor-mocks-test-support.js";
|
||||
} from "./test-support/monitor-mocks-test-support.js";
|
||||
|
||||
describe("Zalo pairing lifecycle", () => {
|
||||
const readAllowFromStoreMock = vi.fn(async () => [] as string[]);
|
||||
|
||||
@@ -9,14 +9,14 @@ import {
|
||||
createLifecycleMonitorSetup,
|
||||
createTextUpdate,
|
||||
settleAsyncWork,
|
||||
} from "../test-support/lifecycle-test-support.js";
|
||||
} from "./test-support/lifecycle-test-support.js";
|
||||
import {
|
||||
getUpdatesMock,
|
||||
loadCachedLifecycleMonitorModule,
|
||||
resetLifecycleTestState,
|
||||
sendPhotoMock,
|
||||
setLifecycleRuntimeCore,
|
||||
} from "../test-support/monitor-mocks-test-support.js";
|
||||
} from "./test-support/monitor-mocks-test-support.js";
|
||||
|
||||
const prepareHostedZaloMediaUrlMock = vi.fn();
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ import {
|
||||
createTextUpdate,
|
||||
postWebhookReplay,
|
||||
settleAsyncWork,
|
||||
} from "../test-support/lifecycle-test-support.js";
|
||||
} from "./test-support/lifecycle-test-support.js";
|
||||
import {
|
||||
resetLifecycleTestState,
|
||||
sendMessageMock,
|
||||
setLifecycleRuntimeCore,
|
||||
startWebhookLifecycleMonitor,
|
||||
} from "../test-support/monitor-mocks-test-support.js";
|
||||
} from "./test-support/monitor-mocks-test-support.js";
|
||||
|
||||
describe("Zalo reply-once lifecycle", () => {
|
||||
const finalizeInboundContextMock = vi.fn((ctx: Record<string, unknown>) => ctx);
|
||||
|
||||
@@ -6,13 +6,6 @@ import {
|
||||
import { withServer } from "openclaw/plugin-sdk/test-env";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig, PluginRuntime } from "../runtime-api.js";
|
||||
import {
|
||||
createImageLifecycleCore,
|
||||
createImageUpdate,
|
||||
createTextUpdate,
|
||||
expectImageLifecycleDelivery,
|
||||
postWebhookReplay,
|
||||
} from "../test-support/lifecycle-test-support.js";
|
||||
import { handleZaloWebhookRequest } from "./monitor.js";
|
||||
import type { ZaloRuntimeEnv } from "./monitor.types.js";
|
||||
import {
|
||||
@@ -24,6 +17,13 @@ import {
|
||||
type ZaloWebhookProcessUpdate,
|
||||
ZaloRetryableWebhookError,
|
||||
} from "./monitor.webhook.js";
|
||||
import {
|
||||
createImageLifecycleCore,
|
||||
createImageUpdate,
|
||||
createTextUpdate,
|
||||
expectImageLifecycleDelivery,
|
||||
postWebhookReplay,
|
||||
} from "./test-support/lifecycle-test-support.js";
|
||||
import type { ResolvedZaloAccount } from "./types.js";
|
||||
const DEFAULT_ACCOUNT: ResolvedZaloAccount = {
|
||||
accountId: "default",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { request as httpRequest } from "node:http";
|
||||
import { expect, vi } from "vitest";
|
||||
import type { OpenClawConfig, PluginRuntime } from "../runtime-api.js";
|
||||
import type { ResolvedZaloAccount } from "../src/types.js";
|
||||
import type { ResolvedZaloAccount } from "../types.js";
|
||||
|
||||
export function createLifecycleConfig(params: {
|
||||
accountId: string;
|
||||
@@ -6,17 +6,17 @@ import {
|
||||
} from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { vi, type Mock } from "vitest";
|
||||
import type { OpenClawConfig } from "../runtime-api.js";
|
||||
import type { ResolvedZaloAccount } from "../src/types.js";
|
||||
import type { ResolvedZaloAccount } from "../types.js";
|
||||
|
||||
type MonitorModule = typeof import("../src/monitor.js");
|
||||
type SecretInputModule = typeof import("../src/secret-input.js");
|
||||
type WebhookModule = typeof import("../src/monitor.webhook.js");
|
||||
type MonitorModule = typeof import("../monitor.js");
|
||||
type SecretInputModule = typeof import("../secret-input.js");
|
||||
type WebhookModule = typeof import("../monitor.webhook.js");
|
||||
|
||||
const monitorModuleUrl = new URL("../src/monitor.ts", import.meta.url).href;
|
||||
const secretInputModuleUrl = new URL("../src/secret-input.ts", import.meta.url).href;
|
||||
const webhookModuleUrl = new URL("../src/monitor.webhook.ts", import.meta.url).href;
|
||||
const apiModuleId = new URL("../src/api.js", import.meta.url).pathname;
|
||||
const runtimeModuleId = new URL("../src/runtime.js", import.meta.url).pathname;
|
||||
const monitorModuleUrl = new URL("../monitor.ts", import.meta.url).href;
|
||||
const secretInputModuleUrl = new URL("../secret-input.ts", import.meta.url).href;
|
||||
const webhookModuleUrl = new URL("../monitor.webhook.ts", import.meta.url).href;
|
||||
const apiModuleId = new URL("../api.js", import.meta.url).pathname;
|
||||
const runtimeModuleId = new URL("../runtime.js", import.meta.url).pathname;
|
||||
|
||||
type UnknownMock = Mock<(...args: unknown[]) => unknown>;
|
||||
type AsyncUnknownMock = Mock<(...args: unknown[]) => Promise<unknown>>;
|
||||
@@ -86,6 +86,8 @@ const MOCK_RELATIVE_MODULE_PATTERN =
|
||||
|
||||
const RELATIVE_CORE_HINT =
|
||||
"Use a focused plugin-sdk test/runtime subpath instead of core internals.";
|
||||
const ROOT_TEST_SUPPORT_LOCAL_SRC_HINT =
|
||||
"Move this helper under the extension's src/test-support tree or expose a narrow test-api/runtime-api surface instead of reaching into private src from package-root test-support.";
|
||||
|
||||
// Tombstones for retired repo-only plugin helper bridge files. Keep this list so
|
||||
// deleted bridges fail loudly if they are recreated instead of using SDK subpaths.
|
||||
@@ -203,6 +205,30 @@ function resolvesToRepoSrc(filePath: string, specifier: string): boolean {
|
||||
return repoRelative === "src" || repoRelative.startsWith("src/");
|
||||
}
|
||||
|
||||
function getExtensionRootForFile(filePath: string): string | undefined {
|
||||
const relativePath = path.relative(process.cwd(), filePath).replaceAll(path.sep, "/");
|
||||
const match = /^extensions\/[^/]+(?:\/|$)/u.exec(relativePath);
|
||||
return match ? path.resolve(process.cwd(), match[0]) : undefined;
|
||||
}
|
||||
|
||||
function isRootExtensionTestSupportFile(filePath: string): boolean {
|
||||
const relativePath = path.relative(process.cwd(), filePath).replaceAll(path.sep, "/");
|
||||
return /^extensions\/[^/]+\/test-support(?:\.[cm]?[jt]sx?|\/)/u.test(relativePath);
|
||||
}
|
||||
|
||||
function resolvesToExtensionLocalSrc(filePath: string, specifier: string): boolean {
|
||||
if (!specifier.startsWith(".")) {
|
||||
return false;
|
||||
}
|
||||
const extensionRoot = getExtensionRootForFile(filePath);
|
||||
if (!extensionRoot) {
|
||||
return false;
|
||||
}
|
||||
const resolved = path.resolve(path.dirname(filePath), specifier);
|
||||
const localSrc = path.join(extensionRoot, "src");
|
||||
return resolved === localSrc || resolved.startsWith(`${localSrc}${path.sep}`);
|
||||
}
|
||||
|
||||
function collectRelativeCoreImportOffenders(
|
||||
filePath: string,
|
||||
content: string,
|
||||
@@ -229,6 +255,34 @@ function collectRelativeCoreImportOffenders(
|
||||
return offenders;
|
||||
}
|
||||
|
||||
function collectRootTestSupportLocalSrcImportOffenders(
|
||||
filePath: string,
|
||||
content: string,
|
||||
): Offender[] {
|
||||
if (!isRootExtensionTestSupportFile(filePath)) {
|
||||
return [];
|
||||
}
|
||||
const offenders: Offender[] = [];
|
||||
const matches = [
|
||||
...content.matchAll(STATIC_RELATIVE_MODULE_PATTERN),
|
||||
...content.matchAll(DYNAMIC_RELATIVE_MODULE_PATTERN),
|
||||
...content.matchAll(MOCK_RELATIVE_MODULE_PATTERN),
|
||||
];
|
||||
for (const match of matches) {
|
||||
const specifier = match[1];
|
||||
if (!specifier || !resolvesToExtensionLocalSrc(filePath, specifier)) {
|
||||
continue;
|
||||
}
|
||||
offenders.push({
|
||||
file: filePath,
|
||||
hint: ROOT_TEST_SUPPORT_LOCAL_SRC_HINT,
|
||||
line: lineNumberForOffset(content, match.index ?? 0),
|
||||
specifier,
|
||||
});
|
||||
}
|
||||
return offenders;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const extensionsDir = path.join(process.cwd(), "extensions");
|
||||
const pluginHelpersDir = path.join(process.cwd(), "test/helpers/plugins");
|
||||
@@ -272,6 +326,7 @@ function main() {
|
||||
includeDynamic: true,
|
||||
}),
|
||||
);
|
||||
offenders.push(...collectRootTestSupportLocalSrcImportOffenders(file, content));
|
||||
}
|
||||
|
||||
for (const file of pluginHelperFiles) {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// Deprecated compatibility alias.
|
||||
// Prefer focused openclaw/plugin-sdk/* test subpaths for public test helpers.
|
||||
/**
|
||||
* @deprecated Compatibility alias for the legacy `plugin-sdk/testing` barrel.
|
||||
*
|
||||
* Prefer focused `openclaw/plugin-sdk/*` test subpaths for public test helpers.
|
||||
*/
|
||||
|
||||
export * from "./testing.js";
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
// Broad legacy compatibility barrel for older plugin tests.
|
||||
// New tests should import focused plugin-sdk/* test subpaths.
|
||||
/**
|
||||
* @deprecated Broad compatibility barrel for older plugin tests.
|
||||
*
|
||||
* New tests should import focused `openclaw/plugin-sdk/*` test subpaths such as
|
||||
* `plugin-test-runtime`, `channel-test-helpers`, `test-env`, or `test-fixtures`.
|
||||
*/
|
||||
|
||||
export {
|
||||
createAckReactionHandle,
|
||||
|
||||
@@ -31,6 +31,17 @@ const DEPRECATED_EXTENSION_SDK_SPECIFIERS = new Set([
|
||||
"openclaw/plugin-sdk/testing",
|
||||
"openclaw/plugin-sdk/test-utils",
|
||||
]);
|
||||
const DEPRECATED_TEST_BARREL_SPECIFIERS = new Set([
|
||||
"openclaw/plugin-sdk/testing",
|
||||
"openclaw/plugin-sdk/test-utils",
|
||||
]);
|
||||
const DEPRECATED_TEST_BARREL_ALLOWED_REFERENCE_FILES = new Set([
|
||||
"src/plugin-sdk/testing.ts",
|
||||
"src/plugin-sdk/test-utils.ts",
|
||||
"src/plugins/compat/registry.ts",
|
||||
"src/plugins/contracts/plugin-entry-guardrails.test.ts",
|
||||
"src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts",
|
||||
]);
|
||||
|
||||
function collectPluginSdkPackageExports(): string[] {
|
||||
const packageJson = JSON.parse(readFileSync(resolve(REPO_ROOT, "package.json"), "utf8")) as {
|
||||
@@ -300,6 +311,56 @@ function collectDeprecatedExtensionSdkImports(): Array<{ file: string; specifier
|
||||
return leaks;
|
||||
}
|
||||
|
||||
function collectCodeFiles(dir: string): string[] {
|
||||
const files: string[] = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.name === "dist" || entry.name === "node_modules" || entry.name === ".git") {
|
||||
continue;
|
||||
}
|
||||
const nextPath = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...collectCodeFiles(nextPath));
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile() || !/\.(?:[cm]?ts|tsx|mts|cts)$/.test(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
files.push(nextPath);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function collectDeprecatedTestBarrelImports(): Array<{ file: string; specifier: string }> {
|
||||
const leaks: Array<{ file: string; specifier: string }> = [];
|
||||
const importPatterns = [
|
||||
/\b(?:import|export)\b[\s\S]*?\bfrom\s*["'](openclaw\/plugin-sdk\/(?:testing|test-utils))["']/g,
|
||||
/\bimport\s*\(\s*["'](openclaw\/plugin-sdk\/(?:testing|test-utils))["']\s*\)/g,
|
||||
/\bvi\.(?:mock|doMock)\s*\(\s*["'](openclaw\/plugin-sdk\/(?:testing|test-utils))["']/g,
|
||||
];
|
||||
for (const root of ["src", "test", "extensions", "packages"]) {
|
||||
for (const file of collectCodeFiles(resolve(REPO_ROOT, root))) {
|
||||
const repoRelativePath = relative(REPO_ROOT, file).replaceAll("\\", "/");
|
||||
if (DEPRECATED_TEST_BARREL_ALLOWED_REFERENCE_FILES.has(repoRelativePath)) {
|
||||
continue;
|
||||
}
|
||||
const source = readFileSync(file, "utf8");
|
||||
for (const importPattern of importPatterns) {
|
||||
for (const match of source.matchAll(importPattern)) {
|
||||
const specifier = match[1];
|
||||
if (!specifier || !DEPRECATED_TEST_BARREL_SPECIFIERS.has(specifier)) {
|
||||
continue;
|
||||
}
|
||||
leaks.push({
|
||||
file: repoRelativePath,
|
||||
specifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return leaks;
|
||||
}
|
||||
|
||||
function collectCrossOwnerReservedSdkImports(): Array<{
|
||||
file: string;
|
||||
specifier: string;
|
||||
@@ -467,6 +528,10 @@ describe("plugin-sdk package contract guardrails", () => {
|
||||
expect(collectDeprecatedExtensionSdkImports()).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps real tests off deprecated plugin-sdk testing barrels", () => {
|
||||
expect(collectDeprecatedTestBarrelImports()).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps reserved SDK compatibility subpaths inside their owning bundled plugins", () => {
|
||||
expect(collectCrossOwnerReservedSdkImports()).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -81,6 +81,14 @@ function findBundledPluginPublicSurfaceImports(source: string): string[] {
|
||||
].map((match) => match[0]);
|
||||
}
|
||||
|
||||
function findRelativeSrcImports(source: string): string[] {
|
||||
return [
|
||||
...source.matchAll(/from\s+["']((?:\.\.?\/)+src\/[^"']+)["']/g),
|
||||
...source.matchAll(/import\(\s*["']((?:\.\.?\/)+src\/[^"']+)["']\s*\)/g),
|
||||
...source.matchAll(/vi\.(?:mock|doMock)\s*\(\s*["']((?:\.\.?\/)+src\/[^"']+)["']/g),
|
||||
].map((match) => match[1]);
|
||||
}
|
||||
|
||||
function getImportBasename(importPath: string): string {
|
||||
return importPath.split("/").at(-1) ?? importPath;
|
||||
}
|
||||
@@ -229,6 +237,21 @@ describe("non-extension test boundaries", () => {
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps extension root test-support helpers from reaching into private src trees", () => {
|
||||
const files = walkCode(path.join(repoRoot, "extensions")).filter((file) =>
|
||||
/^extensions\/[^/]+\/test-support(?:\.ts|\/)/u.test(file),
|
||||
);
|
||||
|
||||
const offenders = files
|
||||
.map((file) => {
|
||||
const imports = findRelativeSrcImports(fs.readFileSync(path.join(repoRoot, file), "utf8"));
|
||||
return imports.length === 0 ? null : { file, imports };
|
||||
})
|
||||
.filter((entry): entry is { file: string; imports: string[] } => entry !== null);
|
||||
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps bundled extension sources off deprecated channel config schema aliases", () => {
|
||||
const files = walkCode(path.join(repoRoot, "extensions"));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user