Files
openclaw/test/scripts/check-export-name-collisions.test.ts
T
2026-08-10 21:36:22 -07:00

278 lines
8.9 KiB
TypeScript

import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
collectModuleExportNames,
collectRepositoryCollisions,
compareExportNameCollisionDebt,
findAliasingReExports,
findExportNameCollisions,
isExcludedExportCollisionSource,
} from "../../scripts/check-export-name-collisions.mts";
import { withTempDir } from "../../src/test-utils/temp-dir.js";
describe("export name collision guard", () => {
it.each([
["src/example.test.ts", true],
["src/example.e2e.test.ts", true],
["src/example.test-support.ts", true],
["src/example.test-helpers.ts", true],
["src/example.test-utils.ts", true],
["src/example.test-harness.ts", true],
["src/example.e2e-harness.ts", true],
["src/example.d.ts", true],
["src/test/example.ts", true],
["src/nested/__fixtures__/example.mts", true],
["src/example.ts", false],
["src/example.mts", false],
])("classifies source exclusion %s", (filePath, expected) => {
expect(isExcludedExportCollisionSource(filePath)).toBe(expected);
});
it("finds exported function and const definitions across modules", () => {
expect(
findExportNameCollisions([
{ path: "src/alpha.ts", content: "export function sharedBehavior() {}" },
{ path: "src/beta.ts", content: "export const sharedBehavior = () => {};" },
{
path: "src/gamma.ts",
content: "async function listedBehavior() {}\nexport { listedBehavior };",
},
{
path: "src/delta.mts",
content: "export async function listedBehavior() {}",
},
]),
).toEqual([
{ name: "listedBehavior", files: ["src/delta.mts", "src/gamma.ts"] },
{ name: "sharedBehavior", files: ["src/alpha.ts", "src/beta.ts"] },
]);
});
it("ignores types, pure re-exports, imports exported locally, and renamed exports", () => {
const result = collectModuleExportNames(`
import { importedValue } from "./other.js";
interface LocalShape {}
type LocalType = string;
export { importedValue };
export { remoteValue } from "./remote.js";
export { remoteValue as renamedValue } from "./remote.js";
export * from "./barrel.js";
export interface ExportedShape {}
export type ExportedType = string;
`);
expect([...result.definitions]).toEqual([]);
expect([...result.exportedNames]).toEqual(["importedValue", "remoteValue"]);
});
it("reports direct aliasing re-exports only outside the Plugin SDK", () => {
expect(
findAliasingReExports([
{
path: "src/alias.ts",
content: `
export { original } from "./source.js";
export type { OriginalType as RenamedType } from "./source.js";
export { original as renamed } from "./source.js";
`,
},
{
path: "src/local-alias.ts",
content: `
import { original } from "./source.js";
export { original as locallyRenamed };
`,
},
{
path: "src/plugin-sdk/alias.ts",
content: 'export { original as sanctioned } from "../source.js";',
},
{
path: "packages/support.ts",
content: 'export { original as packageAlias } from "./source.js";',
includeDefinitions: false,
},
]),
).toEqual([
{
exportedName: "renamed",
importedName: "original",
line: 4,
moduleSpecifier: "./source.js",
path: "src/alias.ts",
},
]);
});
it("exempts exact function and const same-name forwarders", () => {
const forwarders = [
`
import { resolveThing as resolveThingImpl } from "./thing.js";
export function resolveThing(first: string, second?: number) {
return resolveThingImpl(first, second);
}
`,
`
import { resolveThing as resolveThingImpl } from "./thing.js";
export const resolveThing = resolveThingImpl;
`,
`
import { resolveThing as resolveThingImpl } from "./thing.js";
export const resolveThing = (first: string, second?: number) =>
resolveThingImpl(first, second);
`,
`
export const runThing = async (...args: unknown[]) => {
const runtime = await loadRuntime();
return runtime.runThing(...args);
};
`,
`
export async function runThing(...args: unknown[]) {
return (await loadRuntime()).runThing(...args);
}
`,
`
export async function runThing(...args: unknown[]) {
const runtime = await loadRuntime();
return runtime.runThing(...args);
}
`,
];
for (const content of forwarders) {
expect([...collectModuleExportNames(content).definitions]).toEqual([]);
}
});
it.each([
{
name: "extra call",
body: `
prepare();
return resolveThingImpl(...args);
`,
},
{
name: "added argument",
body: "return resolveThingImpl(...args, fallback);",
},
{
name: "changed argument order",
params: "first: string, second: string",
body: "return resolveThingImpl(second, first);",
},
{
name: "layered argument",
params: "params: Record<string, unknown>",
body: "return resolveThingImpl({ ...params, enabled: true });",
},
{
name: "conditional",
body: "return ready ? resolveThingImpl(...args) : fallback;",
},
])("keeps $name wrappers as real definitions", ({ params = "...args: unknown[]", body }) => {
const result = collectModuleExportNames(`
import { resolveThing as resolveThingImpl } from "./thing.js";
export function resolveThing(${params}) {
${body}
}
`);
expect([...result.definitions]).toEqual(["resolveThing"]);
});
it("keeps const arrows that add arguments as real definitions", () => {
const result = collectModuleExportNames(`
import { resolveThing as resolveThingImpl } from "./thing.js";
export const resolveThing = (...args: unknown[]) => resolveThingImpl(...args, fallback);
`);
expect([...result.definitions]).toEqual(["resolveThing"]);
});
it("discovers JavaScript source collisions", async () => {
await withTempDir("openclaw-export-collisions-", async (repoRoot) => {
const sourceRoot = path.join(repoRoot, "src");
await fs.mkdir(sourceRoot);
await Promise.all([
fs.writeFile(path.join(sourceRoot, "alpha.js"), "export const sharedValue = 1;\n"),
fs.writeFile(path.join(sourceRoot, "beta.mjs"), "export const sharedValue = 2;\n"),
]);
expect(await collectRepositoryCollisions(repoRoot)).toEqual([
{ name: "sharedValue", files: ["src/alpha.js", "src/beta.mjs"] },
]);
});
});
it("deduplicates overloads inside one module", () => {
expect(
findExportNameCollisions([
{
path: "src/overloads.ts",
content: `
export function convert(value: string): string;
export function convert(value: number): number;
export function convert(value: string | number) { return value; }
`,
},
]),
).toEqual([]);
});
it("marks collisions exposed by a Plugin SDK module", () => {
expect(
findExportNameCollisions([
{ path: "src/one.ts", content: "export const publicCollision = 1;" },
{ path: "src/two.ts", content: "export function publicCollision() {}" },
{
path: "src/plugin-sdk/public.ts",
content: 'export * from "./public-star.js";',
},
{
path: "src/plugin-sdk/public-star.ts",
content: 'export * from "../../packages/public.js";',
},
{
path: "packages/public.ts",
content: "export const publicCollision = true;",
includeDefinitions: false,
},
]),
).toEqual([
{
name: "publicCollision",
files: ["src/one.ts", "src/two.ts"],
sdk: true,
},
]);
});
});
describe("export name collision debt baseline", () => {
it("separates new debt from baseline improvements", () => {
expect(
compareExportNameCollisionDebt(
[
{ name: "added", files: ["src/a.ts", "src/b.ts"] },
{ name: "expanded", files: ["src/a.ts", "src/b.ts", "src/c.ts"], sdk: true },
],
[
{ name: "expanded", files: ["src/a.ts", "src/b.ts"] },
{ name: "removed", files: ["src/c.ts", "src/d.ts"] },
],
),
).toEqual({
regressions: [
{ current: { name: "added", files: ["src/a.ts", "src/b.ts"] } },
{
baseline: { name: "expanded", files: ["src/a.ts", "src/b.ts"] },
current: {
name: "expanded",
files: ["src/a.ts", "src/b.ts", "src/c.ts"],
sdk: true,
},
},
],
improvements: [{ baseline: { name: "removed", files: ["src/c.ts", "src/d.ts"] } }],
});
});
});