mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-15 15:13:48 -06:00
b080dd1e76
* refactor: consolidate coercion contracts Centralize exact string, record, numeric, date, Boolean, argument, and structured-error coercions while preserving call-site semantics. Migrate canonical-name collisions and deprecated internal SDK bypasses, deleting 55 net production/tooling lines. Expand declaration ownership enforcement to 101 allowed helpers and add a narrow export-completeness audit. * fix: preserve standalone script coercions Keep copied Control UI tooling self-contained and retain the trusted release harness module-relative source seam when the harness runs against an old target cwd.
384 lines
14 KiB
TypeScript
384 lines
14 KiB
TypeScript
import { execFileSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import {
|
|
auditCanonicalCoercionExports,
|
|
auditCoercionHelperDeclarations,
|
|
findBannedCoercionHelperDeclarations,
|
|
findExportedCallableNames,
|
|
isGovernedCoercionHelperPath,
|
|
runCoercionHelperDeclarationGuard,
|
|
type CoercionHelperCarveOut,
|
|
type CoercionHelperDeclaration,
|
|
} from "../../scripts/check-coercion-helper-declarations.mts";
|
|
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
|
|
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|
|
|
describe("coercion helper declaration AST guard", () => {
|
|
it("finds functions, callable variables, methods, fields, and object properties", () => {
|
|
const source = [
|
|
"export async function readString() {}",
|
|
"if (true) {",
|
|
" function asRecord() {}",
|
|
"}",
|
|
"const isRecord = (value: unknown) => Boolean(value);",
|
|
"let toError = function (value: unknown) { return value; };",
|
|
"var optionalString = async (value: unknown) => value;",
|
|
"const readString = (((value: unknown) => String(value)) satisfies ((value: unknown) => string));",
|
|
"function readNumber(record: Record<string, unknown>, key: string) { return record[key]; }",
|
|
"const timestampMs = (value: unknown) => Number(value);",
|
|
"function readBoolean() {}",
|
|
"function readOptionalString() {}",
|
|
"function normalizeString() {}",
|
|
"const asString = (value: unknown) => String(value);",
|
|
"function asObject() {}",
|
|
"const readOptionalString = normalizeOptionalString;",
|
|
"const optionalString = helpers.readStringValue;",
|
|
"const asObject = helpers.asOptionalRecord;",
|
|
"class Example {",
|
|
" readString() {}",
|
|
" toError = () => new Error();",
|
|
" asRecord = function () { return {}; };",
|
|
"}",
|
|
"const object = {",
|
|
" optionalString() {},",
|
|
" readBoolean: () => true,",
|
|
" readNumber: function () { return 1; },",
|
|
"};",
|
|
"function normalizeOptionalString() {}",
|
|
"const parseDateFirstTimestampMs = () => 0;",
|
|
"function safeParseJsonRecord() {}",
|
|
"function resolveIntegerOption() {}",
|
|
].join("\n");
|
|
|
|
expect(findBannedCoercionHelperDeclarations(source, "src/example.ts")).toEqual([
|
|
{ file: "src/example.ts", kind: "function", line: 1, name: "readString" },
|
|
{ file: "src/example.ts", kind: "function", line: 3, name: "asRecord" },
|
|
{ file: "src/example.ts", kind: "variable", line: 5, name: "isRecord" },
|
|
{ file: "src/example.ts", kind: "variable", line: 6, name: "toError" },
|
|
{ file: "src/example.ts", kind: "variable", line: 7, name: "optionalString" },
|
|
{ file: "src/example.ts", kind: "variable", line: 8, name: "readString" },
|
|
{ file: "src/example.ts", kind: "function", line: 9, name: "readNumber" },
|
|
{ file: "src/example.ts", kind: "variable", line: 10, name: "timestampMs" },
|
|
{ file: "src/example.ts", kind: "function", line: 11, name: "readBoolean" },
|
|
{ file: "src/example.ts", kind: "function", line: 12, name: "readOptionalString" },
|
|
{ file: "src/example.ts", kind: "function", line: 13, name: "normalizeString" },
|
|
{ file: "src/example.ts", kind: "variable", line: 14, name: "asString" },
|
|
{ file: "src/example.ts", kind: "function", line: 15, name: "asObject" },
|
|
{
|
|
file: "src/example.ts",
|
|
kind: "variable",
|
|
line: 16,
|
|
name: "readOptionalString",
|
|
},
|
|
{ file: "src/example.ts", kind: "variable", line: 17, name: "optionalString" },
|
|
{ file: "src/example.ts", kind: "variable", line: 18, name: "asObject" },
|
|
{ file: "src/example.ts", kind: "method", line: 20, name: "readString" },
|
|
{ file: "src/example.ts", kind: "field", line: 21, name: "toError" },
|
|
{ file: "src/example.ts", kind: "field", line: 22, name: "asRecord" },
|
|
{ file: "src/example.ts", kind: "method", line: 25, name: "optionalString" },
|
|
{ file: "src/example.ts", kind: "property", line: 26, name: "readBoolean" },
|
|
{ file: "src/example.ts", kind: "property", line: 27, name: "readNumber" },
|
|
{
|
|
file: "src/example.ts",
|
|
kind: "function",
|
|
line: 29,
|
|
name: "normalizeOptionalString",
|
|
},
|
|
{
|
|
file: "src/example.ts",
|
|
kind: "variable",
|
|
line: 30,
|
|
name: "parseDateFirstTimestampMs",
|
|
},
|
|
{
|
|
file: "src/example.ts",
|
|
kind: "function",
|
|
line: 31,
|
|
name: "safeParseJsonRecord",
|
|
},
|
|
{
|
|
file: "src/example.ts",
|
|
kind: "function",
|
|
line: 32,
|
|
name: "resolveIntegerOption",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("ignores imports, non-callable properties, shorthand aliases, callback names, and inert text", () => {
|
|
const source = [
|
|
'import { isRecord, readString as importedReadString } from "./helpers.js";',
|
|
"const alias = isRecord;",
|
|
"const { asRecord } = helpers;",
|
|
"const object = { isRecord, readString: 42, toError: importedToError };",
|
|
"const shorthand = { optionalString };",
|
|
"values.map(function readString(value) { return value; });",
|
|
"const aliasWithInternalName = function isRecord(value) { return value; };",
|
|
"const asRecord = raw as Record<string, unknown>;",
|
|
"const optionalString = value as string;",
|
|
"// function asRecord() {}",
|
|
'const fixture = "function toError() {}";',
|
|
].join("\n");
|
|
|
|
expect(findBannedCoercionHelperDeclarations(source, "src/example.ts")).toEqual([]);
|
|
});
|
|
|
|
it("allows an exact function-kind declaration and reports ordinary excess/stale counts", () => {
|
|
const declarations: CoercionHelperDeclaration[] = [
|
|
{ file: "src/allowed.ts", kind: "function", line: 2, name: "isRecord" },
|
|
{ file: "src/new.ts", kind: "function", line: 4, name: "readString" },
|
|
];
|
|
const carveOuts: CoercionHelperCarveOut[] = [
|
|
{
|
|
file: "src/allowed.ts",
|
|
name: "isRecord",
|
|
kind: "function",
|
|
count: 1,
|
|
reason: "Dependency-free protocol boundary.",
|
|
},
|
|
{
|
|
file: "src/removed.ts",
|
|
name: "toError",
|
|
kind: "function",
|
|
count: 1,
|
|
reason: "Hostile object trap semantics.",
|
|
},
|
|
];
|
|
|
|
expect(auditCoercionHelperDeclarations(declarations, carveOuts)).toEqual({
|
|
excessDeclarations: [{ file: "src/new.ts", kind: "function", line: 4, name: "readString" }],
|
|
invalidCarveOuts: [],
|
|
staleCarveOuts: [
|
|
{
|
|
file: "src/removed.ts",
|
|
name: "toError",
|
|
kind: "function",
|
|
count: 1,
|
|
reason: "Hostile object trap semantics.",
|
|
actualCount: 0,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it.each(["method", "field", "property"] as const)(
|
|
"treats %s drift as both excess and stale function ownership",
|
|
(kind) => {
|
|
const declaration: CoercionHelperDeclaration = {
|
|
file: "src/owner.ts",
|
|
kind,
|
|
line: 3,
|
|
name: "isRecord",
|
|
};
|
|
const carveOut: CoercionHelperCarveOut = {
|
|
file: "src/owner.ts",
|
|
name: "isRecord",
|
|
kind: "function",
|
|
count: 1,
|
|
reason: "Exact function owner.",
|
|
};
|
|
|
|
expect(auditCoercionHelperDeclarations([declaration], [carveOut])).toEqual({
|
|
excessDeclarations: [declaration],
|
|
invalidCarveOuts: [],
|
|
staleCarveOuts: [{ ...carveOut, actualCount: 0 }],
|
|
});
|
|
},
|
|
);
|
|
|
|
it("rejects duplicate, non-banned, and malformed carve-outs", () => {
|
|
const valid: CoercionHelperCarveOut = {
|
|
file: "src/owner.ts",
|
|
name: "isRecord",
|
|
kind: "function",
|
|
count: 1,
|
|
reason: "Exact function owner.",
|
|
};
|
|
const invalid = [
|
|
valid,
|
|
valid,
|
|
{
|
|
...valid,
|
|
file: "src/not-banned.ts",
|
|
name: "domainParser",
|
|
} as unknown as CoercionHelperCarveOut,
|
|
{ ...valid, file: "src/blank.ts", count: 0, reason: "" },
|
|
{
|
|
...valid,
|
|
file: "src/kind.ts",
|
|
kind: "getter",
|
|
} as unknown as CoercionHelperCarveOut,
|
|
];
|
|
|
|
expect(auditCoercionHelperDeclarations([], invalid).invalidCarveOuts).toEqual([
|
|
"src/owner.ts [isRecord] is listed more than once",
|
|
"src/not-banned.ts [domainParser] is not a banned helper name",
|
|
"src/blank.ts [isRecord] must have a positive count",
|
|
"src/blank.ts [isRecord] needs a non-empty reason",
|
|
"src/kind.ts [isRecord] has invalid kind getter",
|
|
]);
|
|
});
|
|
|
|
it("excludes structural fixtures and generated sources without hiding authored fixture-named files", () => {
|
|
expect(isGovernedCoercionHelperPath("src/runtime.ts")).toBe(true);
|
|
expect(isGovernedCoercionHelperPath("extensions/demo/runtime.jsx")).toBe(true);
|
|
expect(isGovernedCoercionHelperPath("src/runtime.d.ts")).toBe(false);
|
|
expect(isGovernedCoercionHelperPath("scripts/runtime.d.mts")).toBe(false);
|
|
expect(isGovernedCoercionHelperPath("test/fixtures/example.ts")).toBe(false);
|
|
expect(isGovernedCoercionHelperPath("extensions/demo/dist/index.js")).toBe(false);
|
|
expect(isGovernedCoercionHelperPath("src/example.test-fixtures.ts")).toBe(true);
|
|
expect(isGovernedCoercionHelperPath("extensions/demo/runtime-tool-fixture.ts")).toBe(true);
|
|
expect(isGovernedCoercionHelperPath("src/schema.generated.ts")).toBe(false);
|
|
expect(isGovernedCoercionHelperPath("ui/src/vendor.bundle.js")).toBe(false);
|
|
expect(
|
|
isGovernedCoercionHelperPath(
|
|
"extensions/browser/chrome-extension/modules/copilot-runtime.js",
|
|
),
|
|
).toBe(true);
|
|
expect(isGovernedCoercionHelperPath("root.config.ts")).toBe(true);
|
|
expect(isGovernedCoercionHelperPath(".github/actions/example/index.ts")).toBe(true);
|
|
});
|
|
|
|
it("finds directly exported callable declarations and export aliases", () => {
|
|
const source = [
|
|
"export function canonical() {}",
|
|
"function local() {}",
|
|
"export { local as alias };",
|
|
"export const VALUE = 1;",
|
|
].join("\n");
|
|
|
|
expect(findExportedCallableNames(source, "src/owner.ts")).toEqual(["alias", "canonical"]);
|
|
});
|
|
|
|
it.each([
|
|
["classified", ["kept"], [{ file: "src/owner.ts", name: "kept", status: "enforced" }]],
|
|
[
|
|
"deferred",
|
|
["format"],
|
|
[
|
|
{
|
|
file: "src/owner.ts",
|
|
name: "format",
|
|
status: "deferred",
|
|
reason: "Meaningful public collision.",
|
|
},
|
|
],
|
|
],
|
|
] as const)("accepts a %s canonical export", (_label, names, classifications) => {
|
|
expect(
|
|
auditCanonicalCoercionExports(new Map([["src/owner.ts", names]]), classifications),
|
|
).toEqual({
|
|
invalidClassifications: [],
|
|
staleClassifications: [],
|
|
unclassifiedExports: [],
|
|
});
|
|
});
|
|
|
|
it("reports unclassified exports and stale, duplicate, or blank deferred entries", () => {
|
|
const kept = { file: "src/owner.ts", name: "kept", status: "enforced" } as const;
|
|
const removed = {
|
|
file: "src/owner.ts",
|
|
name: "removed",
|
|
status: "deferred",
|
|
reason: "Removed owner.",
|
|
} as const;
|
|
const blank = {
|
|
file: "src/other.ts",
|
|
name: "unknown",
|
|
status: "deferred",
|
|
reason: "",
|
|
} as const;
|
|
const audit = auditCanonicalCoercionExports(
|
|
new Map([["src/owner.ts", ["kept", "newHelper"]]]),
|
|
[kept, kept, removed, blank],
|
|
);
|
|
|
|
expect(audit.invalidClassifications).toEqual([
|
|
"src/owner.ts [kept] is classified more than once",
|
|
"src/other.ts [unknown] needs a non-empty deferred reason",
|
|
]);
|
|
expect(audit.unclassifiedExports).toEqual([{ file: "src/owner.ts", name: "newHelper" }]);
|
|
expect(audit.staleClassifications).toEqual([removed, blank]);
|
|
});
|
|
|
|
it("scans a temporary repository and reports sorted, owner-specific diagnostics", () => {
|
|
const repoRoot = tempDirs.make("coercion-helper-guard-");
|
|
fs.mkdirSync(path.join(repoRoot, "src"), { recursive: true });
|
|
fs.mkdirSync(path.join(repoRoot, "extensions", "demo"), { recursive: true });
|
|
fs.mkdirSync(path.join(repoRoot, "config"), { recursive: true });
|
|
fs.writeFileSync(path.join(repoRoot, "src", "z.ts"), "class Owner { readString() {} }\n");
|
|
fs.writeFileSync(
|
|
path.join(repoRoot, "extensions", "demo", "a.ts"),
|
|
"const asRecord = () => ({});\n",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(repoRoot, "config", "root.ts"),
|
|
"class Config { normalizeOptionalString() {} }\n",
|
|
);
|
|
const stdout: string[] = [];
|
|
const stderr: string[] = [];
|
|
expect(
|
|
runCoercionHelperDeclarationGuard({
|
|
carveOuts: [
|
|
{
|
|
file: "src/z.ts",
|
|
name: "readString",
|
|
kind: "function",
|
|
count: 1,
|
|
reason: "Exact function owner.",
|
|
},
|
|
],
|
|
repoRoot,
|
|
io: {
|
|
stdout: { write: (value) => stdout.push(value) },
|
|
stderr: { write: (value) => stderr.push(value) },
|
|
},
|
|
}),
|
|
).toBe(1);
|
|
|
|
expect(stdout).toEqual([]);
|
|
const output = stderr.join("");
|
|
expect(output.indexOf("config/root.ts:1")).toBeLessThan(
|
|
output.indexOf("extensions/demo/a.ts:1"),
|
|
);
|
|
expect(output.indexOf("extensions/demo/a.ts:1")).toBeLessThan(output.indexOf("src/z.ts:1"));
|
|
expect(output).toContain("Banned local coercion-helper declarations:");
|
|
expect(output).toContain("readString (method declaration)");
|
|
expect(output).toContain("Stale coercion-helper carve-outs:");
|
|
expect(output).toContain("expected 1 function declaration(s), found 0");
|
|
expect(output).toContain("Core/package/UI/workspace-script code");
|
|
expect(output).toContain("Plugin production code");
|
|
expect(output).toContain("number-runtime");
|
|
expect(output).toContain("Dependency-free, copied, generated, or serialized code");
|
|
});
|
|
|
|
it("scans only tracked files when the repository has a Git index", () => {
|
|
const repoRoot = tempDirs.make("coercion-helper-tracked-guard-");
|
|
fs.mkdirSync(path.join(repoRoot, "src"), { recursive: true });
|
|
fs.writeFileSync(path.join(repoRoot, "src", "tracked.ts"), "function readString() {}\n");
|
|
fs.writeFileSync(path.join(repoRoot, "src", "untracked.ts"), "function readNumber() {}\n");
|
|
execFileSync("git", ["init", "-q"], { cwd: repoRoot });
|
|
execFileSync("git", ["add", "src/tracked.ts"], { cwd: repoRoot });
|
|
const stderr: string[] = [];
|
|
|
|
expect(
|
|
runCoercionHelperDeclarationGuard({
|
|
carveOuts: [],
|
|
repoRoot,
|
|
io: {
|
|
stdout: { write: () => undefined },
|
|
stderr: { write: (value) => stderr.push(value) },
|
|
},
|
|
}),
|
|
).toBe(1);
|
|
|
|
const output = stderr.join("");
|
|
expect(output).toContain("src/tracked.ts:1 readString");
|
|
expect(output).not.toContain("src/untracked.ts");
|
|
expect(output).not.toContain("readNumber");
|
|
});
|
|
});
|