mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
278 lines
8.9 KiB
TypeScript
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"] } }],
|
|
});
|
|
});
|
|
});
|