fix(ci): catch script declaration drift before merge (#110248)

* fix(ci): verify script declaration contracts

* fix(ci): harden declaration export analysis

* fix(ci): cover opaque script module exports

* test(ci): cover cyclic script declaration barrels
This commit is contained in:
Peter Steinberger
2026-07-18 02:54:26 +01:00
committed by GitHub
parent 75e4103c57
commit 3d1b771035
25 changed files with 1293 additions and 141 deletions
+1
View File
@@ -2050,6 +2050,7 @@ jobs:
case "$TASK" in
guards)
pnpm check:no-conflict-markers
pnpm check:script-declarations
pnpm tool-display:check
pnpm check:host-env-policy:swift
pnpm dup:check:coverage
+1
View File
@@ -1585,6 +1585,7 @@
"check:opengrep-rule-metadata": "node security/opengrep/check-rule-metadata.mjs",
"check:protocol-coverage": "node scripts/check-protocol-event-coverage.mjs",
"check:runtime-sidecar-loaders": "node --import tsx scripts/check-runtime-sidecar-loaders.mjs",
"check:script-declarations": "node scripts/check-script-declarations.mjs",
"check:static-import-sccs": "pnpm check:madge-import-cycles",
"check:temp-path-guardrails": "node --import tsx scripts/check-temp-path-guardrails.ts",
"check:test-types": "pnpm tsgo:test",
+3 -1
View File
@@ -506,6 +506,9 @@ export function createChangedCheckPlan(result, options = {}) {
const lanes = result.lanes;
const runAll = lanes.all;
const shouldRunAndroidVersionSync = hasAndroidVersionSyncPath(result.paths);
if (lanes.scripts || lanes.tooling || lanes.testRoot) {
add("script declaration contracts", ["check:script-declarations"]);
}
if (lanes.releaseMetadata) {
add("release metadata guard", [
@@ -545,7 +548,6 @@ export function createChangedCheckPlan(result, options = {}) {
if (shouldRunControlUiI18nVerify(result.paths)) {
addLint("Control UI i18n catalog", ["lint:ui:i18n"]);
}
if (lanes.core) {
addTypecheck("typecheck core", ["tsgo:core"]);
}
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="node" />
export function generateValueExportContract(filePath: string): Buffer;
export function verifyScriptDeclarationContracts(options?: { root?: string; files?: string[] }): {
checked: number;
issues: string[];
};
+697
View File
@@ -0,0 +1,697 @@
#!/usr/bin/env node
// Verifies that typed script declarations expose every runtime value export.
import { execFileSync } from "node:child_process";
import { existsSync, readFileSync, readdirSync } from "node:fs";
import path from "node:path";
import ts from "typescript";
import { isDirectRunUrl } from "./lib/direct-run.mjs";
const SCRIPT_SOURCE_RE = /^scripts\/.+\.mjs$/u;
const SCRIPT_DECLARATION_RE = /^scripts\/.+\.d\.mts$/u;
const TYPED_SOURCE_RE = /\.(?:[cm]?ts|tsx)$/u;
const SKIPPED_DIRS = new Set([".artifacts", ".git", ".worktrees", "dist", "node_modules"]);
function normalizePath(value) {
return value.split(path.sep).join("/").replace(/^\.\//u, "");
}
function listFilesFromGit(root) {
try {
return execFileSync("git", ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], {
cwd: root,
encoding: "utf8",
maxBuffer: 64 * 1024 * 1024,
})
.split("\0")
.map(normalizePath)
.filter(Boolean);
} catch {
return null;
}
}
function listUntrackedFilesFromGit(root) {
try {
return execFileSync("git", ["ls-files", "--others", "--exclude-standard", "-z"], {
cwd: root,
encoding: "utf8",
maxBuffer: 64 * 1024 * 1024,
})
.split("\0")
.map(normalizePath)
.filter(Boolean);
} catch {
return [];
}
}
function listFilesFromDirectory(root, fsImpl) {
const files = [];
const visit = (relativeDir) => {
const absoluteDir = path.join(root, relativeDir);
for (const entry of fsImpl.readdirSync(absoluteDir, { withFileTypes: true })) {
if (entry.isDirectory() && SKIPPED_DIRS.has(entry.name)) {
continue;
}
const relativePath = normalizePath(path.join(relativeDir, entry.name));
if (entry.isDirectory()) {
visit(relativePath);
} else if (entry.isFile()) {
files.push(relativePath);
}
}
};
visit("");
return files;
}
function listRepositoryFiles(root, options) {
if (options.files) {
return [...new Set(options.files.map(normalizePath))].toSorted((left, right) =>
left.localeCompare(right),
);
}
const gitFiles = listFilesFromGit(root);
return (gitFiles ?? listFilesFromDirectory(root, options.fsImpl))
.filter((relativePath) => options.fsImpl.existsSync(path.join(root, relativePath)))
.toSorted((left, right) => left.localeCompare(right));
}
function listTypedMjsImporters(root, files, fsImpl, explicitFiles) {
if (explicitFiles) {
return files.filter((relativePath) => TYPED_SOURCE_RE.test(relativePath));
}
try {
return execFileSync(
"git",
[
"grep",
"-l",
"-z",
"-F",
".mjs",
"--",
":(glob)**/*.ts",
":(glob)**/*.tsx",
":(glob)**/*.mts",
":(glob)**/*.cts",
],
{ cwd: root, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 },
)
.split("\0")
.map(normalizePath)
.filter(Boolean)
.concat(
listUntrackedFilesFromGit(root).filter((relativePath) => {
if (!TYPED_SOURCE_RE.test(relativePath)) {
return false;
}
return fsImpl.readFileSync(path.join(root, relativePath), "utf8").includes(".mjs");
}),
);
} catch (error) {
if (error?.status === 1) {
return [];
}
return files.filter((relativePath) => TYPED_SOURCE_RE.test(relativePath));
}
}
function resolveTypedScriptImports(root, files, fsImpl, explicitFiles) {
const requiredSources = new Set();
for (const relativePath of listTypedMjsImporters(root, files, fsImpl, explicitFiles)) {
const importerPath = path.join(root, relativePath);
if (!fsImpl.existsSync(importerPath)) {
continue;
}
const sourceText = fsImpl.readFileSync(importerPath, "utf8");
const imports = ts.preProcessFile(sourceText, true, true).importedFiles;
for (const imported of imports) {
if (!imported.fileName.startsWith(".") || !imported.fileName.endsWith(".mjs")) {
continue;
}
const absoluteTarget = path.resolve(path.dirname(importerPath), imported.fileName);
const relativeTarget = normalizePath(path.relative(root, absoluteTarget));
if (SCRIPT_SOURCE_RE.test(relativeTarget)) {
requiredSources.add(relativeTarget);
}
}
}
return requiredSources;
}
function hasModifier(node, kind) {
return node.modifiers?.some((modifier) => modifier.kind === kind) === true;
}
function collectBindingNames(name, names) {
if (ts.isIdentifier(name)) {
names.add(name.text);
return;
}
for (const element of name.elements) {
if (ts.isBindingElement(element)) {
collectBindingNames(element.name, names);
}
}
}
function collectLocalBindings(sourceFile, filePath) {
const bindings = new Map();
const setValueBinding = (name) => {
bindings.set(name, { kind: "value", origins: new Set([createBindingOrigin(filePath, name)]) });
};
for (const statement of sourceFile.statements) {
if (ts.isVariableStatement(statement)) {
const names = new Set();
for (const declaration of statement.declarationList.declarations) {
collectBindingNames(declaration.name, names);
}
for (const name of names) {
setValueBinding(name);
}
continue;
}
if (
(ts.isFunctionDeclaration(statement) ||
ts.isClassDeclaration(statement) ||
ts.isEnumDeclaration(statement) ||
ts.isModuleDeclaration(statement)) &&
statement.name
) {
setValueBinding(statement.name.getText(sourceFile));
continue;
}
if (
(ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) &&
statement.name
) {
if (!bindings.has(statement.name.text)) {
bindings.set(statement.name.text, { kind: "type" });
}
continue;
}
if (!ts.isImportDeclaration(statement) || !statement.importClause) {
continue;
}
const specifier = ts.isStringLiteral(statement.moduleSpecifier)
? statement.moduleSpecifier.text
: null;
if (!specifier) {
continue;
}
const clause = statement.importClause;
if (clause.name) {
bindings.set(clause.name.text, {
kind: clause.isTypeOnly ? "type" : "import",
importedName: "default",
specifier,
});
}
if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) {
bindings.set(clause.namedBindings.name.text, {
kind: clause.isTypeOnly ? "type" : "namespace-import",
specifier,
});
} else if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
for (const element of clause.namedBindings.elements) {
bindings.set(element.name.text, {
kind: clause.isTypeOnly || element.isTypeOnly ? "type" : "import",
importedName: element.propertyName?.text ?? element.name.text,
specifier,
});
}
}
}
return bindings;
}
function resolveReexport(importerPath, specifier, fsImpl) {
if (!specifier.startsWith(".") && !path.isAbsolute(specifier)) {
return null;
}
const base = path.resolve(path.dirname(importerPath), specifier);
const declarationCandidates = [];
if (specifier.endsWith(".mjs")) {
declarationCandidates.push(`${base.slice(0, -".mjs".length)}.d.mts`);
} else if (specifier.endsWith(".cjs")) {
declarationCandidates.push(`${base.slice(0, -".cjs".length)}.d.cts`);
} else if (specifier.endsWith(".js")) {
declarationCandidates.push(`${base.slice(0, -".js".length)}.d.ts`);
}
declarationCandidates.push(`${base}.d.ts`, `${base}.d.mts`);
const candidates = /\.d\.[cm]?ts$/u.test(importerPath)
? [...declarationCandidates, base]
: [base, ...declarationCandidates];
return candidates.find((candidate) => fsImpl.existsSync(candidate)) ?? null;
}
function mergeOrigins(target, origins) {
for (const origin of origins) {
target.add(origin);
}
}
function createBindingOrigin(moduleId, name) {
return JSON.stringify(["binding", moduleId, name]);
}
function createNamespaceOrigin(moduleId) {
return JSON.stringify(["namespace", moduleId]);
}
function isExternalModuleSpecifier(specifier) {
return !specifier.startsWith(".") && !path.isAbsolute(specifier);
}
function canUseOpaqueModuleBinding(target, importedName) {
return (
target.endsWith(".cjs") ||
target.endsWith(".node") ||
(target.endsWith(".json") && importedName === "default")
);
}
function createOpaqueExternalOrigin(importerPath, specifier, kind, name = null) {
return JSON.stringify(["opaque-external", importerPath, specifier, kind, name]);
}
function isOpaqueExternalOrigin(origin) {
return origin.startsWith('["opaque-external",');
}
function resolveLocalBindingOrigins(binding, importerPath, fsImpl, state, issues) {
if (!binding || binding.kind === "type") {
return null;
}
if (binding.kind === "value") {
return binding.origins;
}
const target = resolveReexport(importerPath, binding.specifier, fsImpl);
if (!target) {
if (isExternalModuleSpecifier(binding.specifier)) {
if (/\.d\.[cm]?ts$/u.test(importerPath) && binding.kind !== "namespace-import") {
issues.push({
filePath: importerPath,
specifier: binding.specifier,
reason: "unresolved external declaration import",
});
return null;
}
return new Set([
createOpaqueExternalOrigin(
importerPath,
binding.specifier,
binding.kind === "namespace-import" ? "namespace" : "binding",
binding.kind === "namespace-import" ? null : binding.importedName,
),
]);
}
issues.push({
filePath: importerPath,
specifier: binding.specifier,
reason: "unresolved exported import",
});
return null;
}
if (binding.kind === "namespace-import") {
return new Set([createNamespaceOrigin(target)]);
}
const targetResult = collectValueExports(target, fsImpl, state);
issues.push(...targetResult.issues);
if (targetResult.ambiguous.has(binding.importedName)) {
issues.push({
filePath: importerPath,
specifier: `${binding.specifier}:${binding.importedName}`,
reason: "ambiguous exported import",
});
}
const origins = targetResult.exports.get(binding.importedName);
if (origins) {
return origins;
}
if (
!/\.d\.[cm]?ts$/u.test(importerPath) &&
canUseOpaqueModuleBinding(target, binding.importedName)
) {
return new Set([createBindingOrigin(target, binding.importedName)]);
}
issues.push({
filePath: importerPath,
specifier: `${binding.specifier}:${binding.importedName}`,
reason: "unresolved imported value",
});
return null;
}
function collectValueExports(filePath, fsImpl, state) {
const normalizedFilePath = path.resolve(filePath);
const cached = state.cache.get(normalizedFilePath);
if (cached) {
return cached;
}
if (state.visiting.has(normalizedFilePath)) {
// Full cyclic star resolution requires a fixed point. Fail closed instead of
// caching a partial map; script barrels can break the cycle with explicit exports.
return {
ambiguous: new Set(),
exports: new Map(),
issues: [
{
filePath: normalizedFilePath,
specifier: normalizedFilePath,
reason: "cyclic star re-export",
},
],
};
}
state.visiting.add(normalizedFilePath);
const sourceText = fsImpl.readFileSync(normalizedFilePath, "utf8");
const sourceFile = ts.createSourceFile(
normalizedFilePath,
sourceText,
ts.ScriptTarget.Latest,
true,
);
const localBindings = collectLocalBindings(sourceFile, normalizedFilePath);
const isDeclaration = /\.d\.[cm]?ts$/u.test(normalizedFilePath);
const explicitExports = new Map();
const explicitAmbiguous = new Set();
const starResults = [];
const issues = [];
const setExplicitExport = (name, origins) => {
explicitExports.set(name, origins ?? new Set([createBindingOrigin(normalizedFilePath, name)]));
};
for (const statement of sourceFile.statements) {
if (ts.isExportAssignment(statement)) {
const binding = ts.isIdentifier(statement.expression)
? localBindings.get(statement.expression.text)
: null;
setExplicitExport("default", binding?.kind === "value" ? binding.origins : undefined);
continue;
}
if (ts.isExportDeclaration(statement)) {
if (statement.isTypeOnly) {
continue;
}
if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
for (const element of statement.exportClause.elements) {
if (element.isTypeOnly) {
continue;
}
const exportedName = element.name.text;
const sourceName = element.propertyName?.text ?? exportedName;
const specifier = statement.moduleSpecifier;
if (specifier && ts.isStringLiteral(specifier)) {
const target = resolveReexport(normalizedFilePath, specifier.text, fsImpl);
const targetResult = target ? collectValueExports(target, fsImpl, state) : null;
if (targetResult) {
issues.push(...targetResult.issues);
}
const origins = targetResult?.exports.get(sourceName);
if (origins) {
explicitExports.set(exportedName, origins);
} else if (targetResult?.ambiguous.has(sourceName)) {
explicitAmbiguous.add(exportedName);
issues.push({
filePath: normalizedFilePath,
specifier: `${specifier.text}:${sourceName}`,
reason: "ambiguous named re-export",
});
} else if (!target && isExternalModuleSpecifier(specifier.text)) {
if (isDeclaration) {
issues.push({
filePath: normalizedFilePath,
specifier: specifier.text,
reason: "unresolved external declaration re-export",
});
} else {
explicitExports.set(
exportedName,
new Set([
createOpaqueExternalOrigin(
normalizedFilePath,
specifier.text,
"binding",
sourceName,
),
]),
);
}
} else if (!target) {
if (!isDeclaration) {
explicitExports.set(
exportedName,
new Set([createBindingOrigin(specifier.text, sourceName)]),
);
} else {
issues.push({
filePath: normalizedFilePath,
specifier: specifier.text,
reason: "unresolved named re-export",
});
}
} else if (!isDeclaration && canUseOpaqueModuleBinding(target, sourceName)) {
explicitExports.set(exportedName, new Set([createBindingOrigin(target, sourceName)]));
} else {
issues.push({
filePath: normalizedFilePath,
specifier: `${specifier.text}:${sourceName}`,
reason: "unresolved named re-export",
});
}
} else {
const origins = resolveLocalBindingOrigins(
localBindings.get(sourceName),
normalizedFilePath,
fsImpl,
state,
issues,
);
if (origins) {
explicitExports.set(exportedName, origins);
}
}
}
continue;
}
if (statement.exportClause && ts.isNamespaceExport(statement.exportClause)) {
const specifier = statement.moduleSpecifier;
const target =
specifier && ts.isStringLiteral(specifier)
? resolveReexport(normalizedFilePath, specifier.text, fsImpl)
: null;
const externalSpecifier =
specifier && ts.isStringLiteral(specifier) && isExternalModuleSpecifier(specifier.text)
? specifier.text
: null;
setExplicitExport(
statement.exportClause.name.text,
new Set([
target
? createNamespaceOrigin(target)
: externalSpecifier
? createOpaqueExternalOrigin(normalizedFilePath, externalSpecifier, "namespace")
: createNamespaceOrigin(specifier?.getText(sourceFile) ?? normalizedFilePath),
]),
);
continue;
}
const specifier = statement.moduleSpecifier;
if (specifier && ts.isStringLiteral(specifier)) {
const target = resolveReexport(normalizedFilePath, specifier.text, fsImpl);
if (target) {
const targetResult = collectValueExports(target, fsImpl, state);
starResults.push(targetResult);
issues.push(...targetResult.issues);
} else {
issues.push({
filePath: normalizedFilePath,
specifier: specifier.text,
reason: "unresolved star re-export",
});
}
}
continue;
}
if (!hasModifier(statement, ts.SyntaxKind.ExportKeyword)) {
continue;
}
if (
hasModifier(statement, ts.SyntaxKind.DefaultKeyword) &&
(ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement))
) {
const binding = statement.name ? localBindings.get(statement.name.text) : null;
setExplicitExport("default", binding?.kind === "value" ? binding.origins : undefined);
continue;
}
if (ts.isVariableStatement(statement)) {
const names = new Set();
for (const declaration of statement.declarationList.declarations) {
collectBindingNames(declaration.name, names);
}
for (const name of names) {
setExplicitExport(name);
}
} else if (
(ts.isFunctionDeclaration(statement) ||
ts.isClassDeclaration(statement) ||
ts.isEnumDeclaration(statement) ||
ts.isModuleDeclaration(statement)) &&
statement.name
) {
setExplicitExport(statement.name.getText(sourceFile));
}
}
const exports = new Map(explicitExports);
const starOrigins = new Map();
const ambiguous = new Set(explicitAmbiguous);
for (const targetResult of starResults) {
for (const name of targetResult.ambiguous) {
if (name !== "default" && !explicitExports.has(name)) {
ambiguous.add(name);
}
}
for (const [name, origins] of targetResult.exports) {
if (name === "default" || explicitExports.has(name) || explicitAmbiguous.has(name)) {
continue;
}
const merged = starOrigins.get(name) ?? new Set();
mergeOrigins(merged, origins);
starOrigins.set(name, merged);
}
}
for (const [name, origins] of starOrigins) {
// ESM omits a name when multiple star exports resolve to different bindings.
if (origins.size > 1) {
ambiguous.add(name);
if ([...origins].some(isOpaqueExternalOrigin)) {
issues.push({
filePath: normalizedFilePath,
specifier: name,
reason: "opaque external star collision",
});
}
} else if (!ambiguous.has(name)) {
exports.set(name, origins);
}
}
const result = { ambiguous, exports, issues };
state.visiting.delete(normalizedFilePath);
state.cache.set(normalizedFilePath, result);
return result;
}
function analyzeValueExportContract(filePath, fsImpl) {
const result = collectValueExports(filePath, fsImpl, {
cache: new Map(),
visiting: new Set(),
});
const names = [...result.exports.keys()].toSorted((left, right) => left.localeCompare(right));
return {
contract: Buffer.from(names.map((name) => `${name}\n`).join(""), "utf8"),
issues: result.issues,
};
}
/** Generates the canonical byte representation of a module's runtime value exports. */
export function generateValueExportContract(filePath, options = {}) {
const fsImpl = options.fsImpl ?? { existsSync, readFileSync };
const result = analyzeValueExportContract(filePath, fsImpl);
if (result.issues.length > 0) {
throw new Error(result.issues.map((issue) => issue.reason).join(", "));
}
return result.contract;
}
function formatContract(contract) {
return contract.toString("utf8").trim().split("\n").filter(Boolean);
}
/** Regenerates and byte-compares every script runtime/declaration value-export contract. */
export function verifyScriptDeclarationContracts(options = {}) {
const root = path.resolve(options.root ?? process.cwd());
const fsImpl = options.fsImpl ?? {
existsSync,
readFileSync,
readdirSync,
};
const files = listRepositoryFiles(root, { ...options, fsImpl });
const sourcePaths = new Set(
files
.filter((relativePath) => SCRIPT_DECLARATION_RE.test(relativePath))
.map((declarationPath) => declarationPath.slice(0, -".d.mts".length) + ".mjs"),
);
for (const sourcePath of resolveTypedScriptImports(root, files, fsImpl, Boolean(options.files))) {
sourcePaths.add(sourcePath);
}
const issues = [];
const pairs = [];
for (const sourcePath of [...sourcePaths].toSorted((left, right) => left.localeCompare(right))) {
const declarationPath = sourcePath.slice(0, -".mjs".length) + ".d.mts";
if (!fsImpl.existsSync(path.join(root, sourcePath))) {
issues.push(`${sourcePath}: missing runtime source`);
continue;
}
if (!fsImpl.existsSync(path.join(root, declarationPath))) {
issues.push(`${sourcePath}: missing ${declarationPath}`);
continue;
}
pairs.push({ sourcePath, declarationPath });
}
for (const { sourcePath, declarationPath } of pairs) {
const runtimeAnalysis = analyzeValueExportContract(path.join(root, sourcePath), fsImpl);
const declarationAnalysis = analyzeValueExportContract(
path.join(root, declarationPath),
fsImpl,
);
const analysisIssues = [...runtimeAnalysis.issues, ...declarationAnalysis.issues];
if (analysisIssues.length > 0) {
for (const issue of analysisIssues) {
const issuePath = normalizePath(path.relative(root, issue.filePath));
issues.push(`${issuePath}: ${issue.reason} ${JSON.stringify(issue.specifier)}`);
}
continue;
}
const runtimeContract = runtimeAnalysis.contract;
const declarationContract = declarationAnalysis.contract;
if (runtimeContract.equals(declarationContract)) {
continue;
}
const runtimeExports = new Set(formatContract(runtimeContract));
const declarationExports = new Set(formatContract(declarationContract));
const missing = [...runtimeExports].filter((name) => !declarationExports.has(name));
const extra = [...declarationExports].filter((name) => !runtimeExports.has(name));
issues.push(
`${declarationPath}: value-export contract drift` +
(missing.length > 0 ? `; missing ${missing.join(", ")}` : "") +
(extra.length > 0 ? `; extra ${extra.join(", ")}` : ""),
);
}
return { checked: pairs.length, issues };
}
function main() {
const result = verifyScriptDeclarationContracts();
if (result.issues.length > 0) {
for (const issue of result.issues) {
console.error(`[script-declarations] ${issue}`);
}
console.error(
"[script-declarations] update the adjacent .d.mts contract before merging script exports",
);
process.exitCode = 1;
return;
}
console.log(`[script-declarations] ${result.checked} declaration contracts match`);
}
if (isDirectRunUrl(process.argv[1], import.meta.url)) {
main();
}
+1
View File
@@ -110,6 +110,7 @@ export async function main(argv = process.argv.slice(2)) {
{ name: "duplicate scan target coverage", args: ["dup:check:coverage"] },
{ name: "npm shrinkwrap guard", args: ["deps:shrinkwrap:check"] },
{ name: "package patch guard", args: ["deps:patches:check"] },
{ name: "script declaration contracts", args: ["check:script-declarations"] },
],
},
{
@@ -28,7 +28,6 @@ export function runGatewaySuspensionPostRestartClient(
options: { statePath: string; token: string; url: string; timeoutMs?: number },
deps?: { fetchImpl?: typeof fetch },
): Promise<void>;
export function responseError(method: string, response: GatewayFrame): Error;
export function runGatewayNetworkClient(
options: { token: string; url: string; timeoutMs?: number },
deps?: {
@@ -23,12 +23,3 @@ export function resolveRemoteTargetRefSha(
targetRef: string,
executeGit?: (args: string[]) => string,
): string;
export type WorkflowRunCheckSuite = {
status?: string;
conclusion?: string | null;
workflowRun?: { url?: string } | null;
};
export function selectWorkflowRunCheckSuite(
nodes: WorkflowRunCheckSuite[],
parentRunId: unknown,
): WorkflowRunCheckSuite | undefined;
@@ -1,8 +1,6 @@
export const PROOF_OVERRIDE_LABEL: "proof: override";
export const PROOF_SUFFICIENT_LABEL: "proof: sufficient";
export const NEEDS_PR_CONTEXT_LABEL: "triage: needs-pr-context";
export const DEFAULT_GITHUB_API_TIMEOUT_MS: 30000;
type PullRequest = Record<string, unknown>;
type Comment = Record<string, unknown>;
type Evaluation = {
@@ -1,7 +1,3 @@
export const ACTIONS_ARTIFACT_API_VERSION: "2026-03-10";
export const DEFAULT_MAX_ACTIONS_ARTIFACT_BYTES: number;
export const DEFAULT_MAX_ACTIONS_ARTIFACT_EXPANDED_BYTES: number;
export type ArtifactArchivePolicy = {
expectedEntries?: readonly string[];
minEntries?: number;
+2
View File
@@ -14,6 +14,8 @@ export type BundledPluginBuildEntryParams = {
export const NON_PACKAGED_BUNDLED_PLUGIN_DIRS: Set<string>;
export const DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV: string;
export function collectPluginSourceEntries(packageJson: unknown): string[];
export function collectTopLevelPublicSurfaceEntries(pluginDir: string): string[];
export function collectRootPackageExcludedExtensionDirs(
params?: BundledPluginBuildEntryParams,
): Set<string>;
+16
View File
@@ -2,6 +2,10 @@ import type {
ExecFileSyncOptions,
ExecFileSyncOptionsWithBufferEncoding,
ExecFileSyncOptionsWithStringEncoding,
SpawnSyncOptions,
SpawnSyncOptionsWithBufferEncoding,
SpawnSyncOptionsWithStringEncoding,
SpawnSyncReturns,
} from "node:child_process";
export function plainGhEnv(env?: NodeJS.ProcessEnv): {
@@ -32,4 +36,16 @@ export function execGhApiRead(
endpoint: string,
options?: ExecFileSyncOptions,
): string | Uint8Array<ArrayBuffer>;
export function spawnPlainGh(
args: readonly string[],
options: SpawnSyncOptionsWithStringEncoding,
): SpawnSyncReturns<string>;
export function spawnPlainGh(
args: readonly string[],
options?: SpawnSyncOptionsWithBufferEncoding,
): SpawnSyncReturns<Buffer>;
export function spawnPlainGh(
args: readonly string[],
options?: SpawnSyncOptions,
): SpawnSyncReturns<string | Buffer>;
export const PLAIN_GH_SYSTEM_CANDIDATES: string[];
@@ -1,92 +1,4 @@
#!/usr/bin/env node
/**
* Builds the manifest for paired baseline/candidate Telegram Desktop proof artifacts.
*/
export function buildTelegramDesktopProofManifest({
baseline,
baselineRef,
baselineSha,
candidate,
candidateRef,
candidateSha,
scenarioLabel,
}: {
baseline: unknown;
baselineRef: unknown;
baselineSha: unknown;
candidate: unknown;
candidateRef: unknown;
candidateSha: unknown;
scenarioLabel: unknown;
}): {
schemaVersion: number;
id: string;
title: string;
summary: string;
scenario: unknown;
comparison: {
baseline: {
expected: string;
status: string;
ref?: unknown;
sha?: unknown;
};
candidate: {
expected: string;
status: string;
fixed: boolean;
ref?: unknown;
sha?: unknown;
};
pass: boolean;
};
artifacts: (
| {
alt: string;
inline: boolean;
kind: string;
label: string;
lane: string;
path: string;
targetPath: string;
width: number;
required?: undefined;
}
| {
kind: string;
label: string;
lane: string;
path: string;
required: boolean;
targetPath: string;
alt?: undefined;
inline?: undefined;
width?: undefined;
}
| {
alt: string;
inline: boolean;
kind: string;
label: string;
lane: string;
path: string;
required: boolean;
targetPath: string;
width?: undefined;
}
| {
kind: string;
label: string;
lane: string;
path: string;
targetPath: string;
alt?: undefined;
inline?: undefined;
width?: undefined;
required?: undefined;
}
)[];
};
export function writeTelegramDesktopProofEvidence(rawArgs?: string[]): {
manifest: {
schemaVersion: number;
-2
View File
@@ -150,8 +150,6 @@ export function createGatewayClient({
request: (method: unknown, params: unknown, timeoutMs?: number) => Promise<unknown>;
waitOpen: () => Promise<unknown>;
};
/** Maximum time to wait for a spawned gateway to become reachable. */
export const READY_TIMEOUT_MS: 120000;
declare function defaultKillProcess(pid: unknown, signal: unknown): boolean;
declare function defaultRunTaskkill(
command: string,
-2
View File
@@ -48,5 +48,3 @@ export function resolveOpenClawNpmResumeRun(options: {
runId: string;
runGh?: (args: string[]) => string;
}): OpenClawNpmResumeIdentity;
export function main(argv?: string[]): void;
-2
View File
@@ -1,5 +1,4 @@
#!/usr/bin/env node
export function usage(): string;
export function shouldPrintHelp(argv: unknown): boolean;
/**
* Parses CPU profile file paths and --limit.
@@ -8,4 +7,3 @@ export function parseArgs(argv: unknown): {
files: unknown[];
limit: number;
};
export function summarizeProfile(file: unknown, limit: unknown): void;
@@ -1,9 +1,4 @@
#!/usr/bin/env node
export function githubReleaseBodySize(body: unknown): {
characters: number;
bytes: number;
};
export function fitsGithubReleaseBody(body: unknown): boolean;
export function extractChangelogReleaseSections(changelog: string): {
version: string;
source: string;
@@ -12,7 +7,6 @@ export function extractChangelogSection(changelog: unknown, version: unknown): u
export function releaseNotesVersionForTag(tag: unknown): unknown;
export function formatShippedBaselineExclusions(baselines: ShippedBaselineExclusion[]): string;
export function parseShippedBaselineExclusions(section: string): ShippedBaselineExclusion[];
export function tagPinnedContributionRecordUrl(repository: unknown, tag: unknown): string;
export function dedicatedSectionVersionForTag(tag: unknown): unknown;
export function releaseNotesSectionForTag(
changelog: unknown,
-1
View File
@@ -17,4 +17,3 @@ export function collectTempCreationFindingsFromDiff(
source: unknown;
}
)[];
export function main(argv: unknown, io: unknown): Promise<1 | 0>;
+1
View File
@@ -1,6 +1,7 @@
export const runNodeWatchedPaths: string[];
export function isBuildRelevantRunNodePath(repoPath: string): boolean;
export function isRestartRelevantRunNodePath(repoPath: string): boolean;
export function listRequiredRuntimePostBuildOutputs(deps: Record<string, unknown>): string[];
export function resolveBuildRequirement(deps: {
cwd: string;
env: NodeJS.ProcessEnv;
-8
View File
@@ -70,14 +70,6 @@ export function buildVitestRunPlans(
export function buildFullSuiteVitestRunPlans(args: string[], cwd?: string): VitestRunPlan[];
export function shouldUseLocalFullSuiteParallelByDefault(
env?: Record<string, string | undefined>,
): boolean;
export function shouldExpandLocalFullSuiteShardsByDefault(
env?: Record<string, string | undefined>,
): boolean;
export function resolveParallelFullSuiteConcurrency(
specCount: number,
env?: Record<string, string | undefined>,
-14
View File
@@ -1,17 +1,3 @@
export function buildTestboxLeaseFingerprint(
repoRoot: unknown,
args: unknown,
): {
version: number;
baseSha: string;
headSha: string;
workingTreeClean: boolean;
dependencyDigest: string;
environmentDigest: string;
workflow: unknown;
job: unknown;
ref: unknown;
};
export function testboxLeaseStaleReasons(saved: unknown, current: unknown): string[];
export function prepareTestboxLeaseFreshness({
args,
-1
View File
@@ -69,6 +69,5 @@ export function workflowRunQueryPaths(
},
page?: number,
): string[];
export function main(argv?: string[]): void;
export const SCHEDULED_HOSTED_WORKFLOWS: string[];
export const HOSTED_GATE_MAX_AGE_HOURS: 24;
+9
View File
@@ -883,6 +883,15 @@ describe("scripts/changed-lanes", () => {
expect(result.lanes.scripts).toBe(true);
expect(plan.commands.map((command) => command.args[0])).toContain("tsgo:scripts");
expect(plan.commands.map((command) => command.args[0])).toContain("check:script-declarations");
});
it("keeps the declaration guard when another change selects the full lane", () => {
const result = detectChangedLanes(["package.json", "scripts/example.mjs"]);
const plan = createChangedCheckPlan(result);
expect(result.lanes.all).toBe(true);
expect(plan.commands.map((command) => command.args[0])).toContain("check:script-declarations");
});
it("routes Control UI i18n tooling changes through keyless catalog verification", () => {
@@ -0,0 +1,555 @@
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { verifyScriptDeclarationContracts } from "../../scripts/check-script-declarations.mjs";
describe("script declaration contracts", () => {
const tempDirs: string[] = [];
afterEach(() => {
for (const tempDir of tempDirs.splice(0)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it("fails deliberate export drift and passes after regenerating the declaration", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
fs.mkdirSync(path.join(root, "test"), { recursive: true });
fs.writeFileSync(
path.join(root, "scripts", "example.mjs"),
"export function stable() {}\nexport function added() {}\n",
);
fs.writeFileSync(
path.join(root, "scripts", "example.d.mts"),
"export function stable(): void;\n",
);
fs.writeFileSync(
path.join(root, "test", "consumer.ts"),
'import { added } from "../scripts/example.mjs";\nvoid added;\n',
);
const files = ["scripts/example.d.mts", "scripts/example.mjs", "test/consumer.ts"];
expect(verifyScriptDeclarationContracts({ root, files })).toEqual({
checked: 1,
issues: ["scripts/example.d.mts: value-export contract drift; missing added"],
});
fs.writeFileSync(
path.join(root, "scripts", "example.d.mts"),
"export function added(): void;\nexport function stable(): void;\n",
);
expect(verifyScriptDeclarationContracts({ root, files })).toEqual({ checked: 1, issues: [] });
});
it("requires declarations for scripts imported by typed sources", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
fs.mkdirSync(path.join(root, "test"), { recursive: true });
fs.writeFileSync(path.join(root, "scripts", "missing.mjs"), "export const value = 1;\n");
fs.writeFileSync(
path.join(root, "test", "consumer.ts"),
'import { value } from "../scripts/missing.mjs";\nvoid value;\n',
);
expect(
verifyScriptDeclarationContracts({
root,
files: ["scripts/missing.mjs", "test/consumer.ts"],
}),
).toEqual({
checked: 0,
issues: ["scripts/missing.mjs: missing scripts/missing.d.mts"],
});
});
it("rejects declaration sidecars whose runtime script is missing", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
fs.mkdirSync(path.join(root, "test"), { recursive: true });
fs.writeFileSync(path.join(root, "scripts", "orphan.d.mts"), "export const value: 1;\n");
fs.writeFileSync(
path.join(root, "test", "consumer.ts"),
'import { value } from "../scripts/orphan.mjs";\nvoid value;\n',
);
expect(
verifyScriptDeclarationContracts({
root,
files: ["scripts/orphan.d.mts", "test/consumer.ts"],
}),
).toEqual({
checked: 0,
issues: ["scripts/orphan.mjs: missing runtime source"],
});
});
it("ignores tracked declaration pairs deleted from the working tree", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
execFileSync("git", ["init", "-q"], { cwd: root });
const runtimePath = path.join(root, "scripts", "removed.mjs");
const declarationPath = path.join(root, "scripts", "removed.d.mts");
fs.writeFileSync(runtimePath, "export const value = 1;\n");
fs.writeFileSync(declarationPath, "export const value: 1;\n");
execFileSync("git", ["add", "scripts/removed.mjs", "scripts/removed.d.mts"], { cwd: root });
fs.rmSync(runtimePath);
fs.rmSync(declarationPath);
expect(verifyScriptDeclarationContracts({ root })).toEqual({ checked: 0, issues: [] });
});
it("applies ESM default and ambiguity rules to star re-exports", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
fs.writeFileSync(
path.join(root, "scripts", "left.mjs"),
"export default 1;\nexport const shared = 1;\nexport const left = 1;\n",
);
fs.writeFileSync(
path.join(root, "scripts", "right.mjs"),
"export const shared = 2;\nexport const right = 1;\n",
);
fs.writeFileSync(path.join(root, "scripts", "left-alias.mjs"), 'export * from "./left.mjs";\n');
fs.writeFileSync(
path.join(root, "scripts", "barrel.mjs"),
'export * from "./left.mjs";\nexport * from "./left-alias.mjs";\nexport * from "./right.mjs";\n',
);
fs.writeFileSync(
path.join(root, "scripts", "barrel.d.mts"),
"export const left: 1;\nexport const right: 1;\n",
);
expect(
verifyScriptDeclarationContracts({
root,
files: [
"scripts/barrel.d.mts",
"scripts/barrel.mjs",
"scripts/left-alias.mjs",
"scripts/left.mjs",
"scripts/right.mjs",
],
}),
).toEqual({ checked: 1, issues: [] });
});
it("propagates ambiguity through nested star re-exports", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
fs.writeFileSync(path.join(root, "scripts", "left.mjs"), "export const shared = 1;\n");
fs.writeFileSync(path.join(root, "scripts", "right.mjs"), "export const shared = 2;\n");
fs.writeFileSync(
path.join(root, "scripts", "inner.mjs"),
'export * from "./left.mjs";\nexport * from "./right.mjs";\n',
);
fs.writeFileSync(
path.join(root, "scripts", "outer.mjs"),
'export * from "./inner.mjs";\nexport * from "./left.mjs";\n',
);
fs.writeFileSync(path.join(root, "scripts", "outer.d.mts"), "export {};\n");
expect(
verifyScriptDeclarationContracts({
root,
files: [
"scripts/inner.mjs",
"scripts/left.mjs",
"scripts/outer.d.mts",
"scripts/outer.mjs",
"scripts/right.mjs",
],
}),
).toEqual({ checked: 1, issues: [] });
});
it("fails closed on cyclic star graphs", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
fs.writeFileSync(path.join(root, "scripts", "a.mjs"), 'export * from "./b.mjs";\n');
fs.writeFileSync(
path.join(root, "scripts", "b.mjs"),
'export const value = 1;\nexport * from "./a.mjs";\n',
);
fs.writeFileSync(path.join(root, "scripts", "a.d.mts"), "export const value: 1;\n");
fs.writeFileSync(path.join(root, "scripts", "b.d.mts"), "export const value: 1;\n");
const result = verifyScriptDeclarationContracts({
root,
files: ["scripts/a.d.mts", "scripts/a.mjs", "scripts/b.d.mts", "scripts/b.mjs"],
});
expect(result.checked).toBe(2);
expect(result.issues).toHaveLength(2);
expect(result.issues.every((issue) => issue.includes("cyclic star re-export"))).toBe(true);
});
it("fails closed on explicit re-exports of ambiguous bindings", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
fs.writeFileSync(path.join(root, "scripts", "left.mjs"), "export const shared = 1;\n");
fs.writeFileSync(path.join(root, "scripts", "right.mjs"), "export const shared = 2;\n");
fs.writeFileSync(
path.join(root, "scripts", "inner.mjs"),
'export * from "./left.mjs";\nexport * from "./right.mjs";\n',
);
fs.writeFileSync(
path.join(root, "scripts", "barrel.mjs"),
'export { shared } from "./inner.mjs";\n',
);
fs.writeFileSync(path.join(root, "scripts", "barrel.d.mts"), "export {};\n");
expect(
verifyScriptDeclarationContracts({
root,
files: [
"scripts/barrel.d.mts",
"scripts/barrel.mjs",
"scripts/inner.mjs",
"scripts/left.mjs",
"scripts/right.mjs",
],
}),
).toEqual({
checked: 1,
issues: ['scripts/barrel.mjs: ambiguous named re-export "./inner.mjs:shared"'],
});
});
it("fails closed when a star re-export cannot be resolved", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
fs.writeFileSync(path.join(root, "scripts", "barrel.mjs"), 'export * from "package";\n');
fs.writeFileSync(path.join(root, "scripts", "barrel.d.mts"), "export {};\n");
expect(
verifyScriptDeclarationContracts({
root,
files: ["scripts/barrel.d.mts", "scripts/barrel.mjs"],
}),
).toEqual({
checked: 1,
issues: ['scripts/barrel.mjs: unresolved star re-export "package"'],
});
});
it("resolves declaration stars to declarations before runtime modules", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
fs.mkdirSync(path.join(root, "lib"), { recursive: true });
fs.writeFileSync(
path.join(root, "lib", "helper.mjs"),
"export const one = 1;\nexport const two = 2;\n",
);
fs.writeFileSync(path.join(root, "lib", "helper.d.mts"), "export const one: 1;\n");
fs.writeFileSync(
path.join(root, "scripts", "barrel.mjs"),
'export * from "../lib/helper.mjs";\n',
);
fs.writeFileSync(
path.join(root, "scripts", "barrel.d.mts"),
'export * from "../lib/helper.mjs";\n',
);
expect(
verifyScriptDeclarationContracts({
root,
files: ["lib/helper.d.mts", "lib/helper.mjs", "scripts/barrel.d.mts", "scripts/barrel.mjs"],
}),
).toEqual({
checked: 1,
issues: ["scripts/barrel.d.mts: value-export contract drift; missing two"],
});
});
it("does not count type-only named or default declaration exports", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
fs.writeFileSync(
path.join(root, "scripts", "types.mjs"),
"export const Foo = 1;\nexport default 1;\n",
);
fs.writeFileSync(
path.join(root, "scripts", "types.d.mts"),
"interface Foo {}\nexport { Foo };\nexport default interface Default {}\n",
);
expect(
verifyScriptDeclarationContracts({
root,
files: ["scripts/types.d.mts", "scripts/types.mjs"],
}),
).toEqual({
checked: 1,
issues: ["scripts/types.d.mts: value-export contract drift; missing default, Foo"],
});
});
it("preserves local binding identity through renamed star paths", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
fs.writeFileSync(
path.join(root, "scripts", "origin.mjs"),
"const value = 1;\nexport { value as left, value as right };\n",
);
fs.writeFileSync(
path.join(root, "scripts", "left.mjs"),
'export { left as shared } from "./origin.mjs";\n',
);
fs.writeFileSync(
path.join(root, "scripts", "right.mjs"),
'export { right as shared } from "./origin.mjs";\n',
);
fs.writeFileSync(
path.join(root, "scripts", "barrel.mjs"),
'export * from "./left.mjs";\nexport * from "./right.mjs";\n',
);
fs.writeFileSync(path.join(root, "scripts", "barrel.d.mts"), "export const shared: 1;\n");
expect(
verifyScriptDeclarationContracts({
root,
files: [
"scripts/barrel.d.mts",
"scripts/barrel.mjs",
"scripts/left.mjs",
"scripts/origin.mjs",
"scripts/right.mjs",
],
}),
).toEqual({ checked: 1, issues: [] });
});
it("preserves statically named exports from external modules", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
fs.writeFileSync(
path.join(root, "scripts", "external.mjs"),
'import { readFile } from "fs/promises";\nexport { readFile };\n',
);
fs.writeFileSync(
path.join(root, "scripts", "external.d.mts"),
"export declare const readFile: unknown;\n",
);
expect(
verifyScriptDeclarationContracts({
root,
files: ["scripts/external.d.mts", "scripts/external.mjs"],
}),
).toEqual({ checked: 1, issues: [] });
});
it("fails closed on external declaration re-exports with unknown value provenance", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
fs.writeFileSync(path.join(root, "scripts", "external.mjs"), "export const Foo = 1;\n");
fs.writeFileSync(
path.join(root, "scripts", "external.d.mts"),
'export { Foo } from "types-only-package";\n',
);
expect(
verifyScriptDeclarationContracts({
root,
files: ["scripts/external.d.mts", "scripts/external.mjs"],
}),
).toEqual({
checked: 1,
issues: [
'scripts/external.d.mts: unresolved external declaration re-export "types-only-package"',
],
});
});
it("fails closed on re-exported external declaration imports", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
fs.writeFileSync(path.join(root, "scripts", "external.mjs"), "export const Foo = 1;\n");
fs.writeFileSync(
path.join(root, "scripts", "external.d.mts"),
'import { Foo } from "types-only-package";\nexport { Foo };\n',
);
expect(
verifyScriptDeclarationContracts({
root,
files: ["scripts/external.d.mts", "scripts/external.mjs"],
}),
).toEqual({
checked: 1,
issues: [
'scripts/external.d.mts: unresolved external declaration import "types-only-package"',
],
});
});
it("accepts statically value-bearing external namespace declaration imports", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
fs.writeFileSync(
path.join(root, "scripts", "external.mjs"),
'import * as pkg from "pkg";\nexport { pkg };\n',
);
fs.writeFileSync(
path.join(root, "scripts", "external.d.mts"),
'import * as pkg from "pkg";\nexport { pkg };\n',
);
expect(
verifyScriptDeclarationContracts({
root,
files: ["scripts/external.d.mts", "scripts/external.mjs"],
}),
).toEqual({ checked: 1, issues: [] });
});
it("retains runtime exports imported from opaque local modules", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
fs.writeFileSync(path.join(root, "scripts", "config.json"), '{"enabled":true}\n');
fs.writeFileSync(
path.join(root, "scripts", "config.mjs"),
'import config from "./config.json" with { type: "json" };\nexport { config };\n',
);
fs.writeFileSync(
path.join(root, "scripts", "config.d.mts"),
"export declare const config: { enabled: boolean };\n",
);
expect(
verifyScriptDeclarationContracts({
root,
files: ["scripts/config.d.mts", "scripts/config.json", "scripts/config.mjs"],
}),
).toEqual({ checked: 1, issues: [] });
});
it("fails closed on missing exports from analyzable local modules", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
fs.writeFileSync(path.join(root, "scripts", "origin.mjs"), "export const value = 1;\n");
fs.writeFileSync(
path.join(root, "scripts", "barrel.mjs"),
'import { typo } from "./origin.mjs";\nexport { typo };\n',
);
fs.writeFileSync(path.join(root, "scripts", "barrel.d.mts"), "export const typo: 1;\n");
expect(
verifyScriptDeclarationContracts({
root,
files: ["scripts/barrel.d.mts", "scripts/barrel.mjs", "scripts/origin.mjs"],
}),
).toEqual({
checked: 1,
issues: ['scripts/barrel.mjs: unresolved imported value "./origin.mjs:typo"'],
});
});
it("fails closed on named JSON imports", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
fs.writeFileSync(path.join(root, "scripts", "config.json"), '{"enabled":true}\n');
fs.writeFileSync(
path.join(root, "scripts", "config.mjs"),
'import { enabled } from "./config.json" with { type: "json" };\nexport { enabled };\n',
);
fs.writeFileSync(
path.join(root, "scripts", "config.d.mts"),
"export declare const enabled: boolean;\n",
);
expect(
verifyScriptDeclarationContracts({
root,
files: ["scripts/config.d.mts", "scripts/config.json", "scripts/config.mjs"],
}),
).toEqual({
checked: 1,
issues: ['scripts/config.mjs: unresolved imported value "./config.json:enabled"'],
});
});
it("resolves CommonJS imports to adjacent declaration sidecars", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
fs.writeFileSync(path.join(root, "scripts", "helper.cjs"), "exports.value = 1;\n");
fs.writeFileSync(path.join(root, "scripts", "helper.d.cts"), "export const value: 1;\n");
fs.writeFileSync(
path.join(root, "scripts", "barrel.mjs"),
'export { value } from "./helper.cjs";\n',
);
fs.writeFileSync(
path.join(root, "scripts", "barrel.d.mts"),
'export { value } from "./helper.cjs";\n',
);
expect(
verifyScriptDeclarationContracts({
root,
files: [
"scripts/barrel.d.mts",
"scripts/barrel.mjs",
"scripts/helper.cjs",
"scripts/helper.d.cts",
],
}),
).toEqual({ checked: 1, issues: [] });
});
it("distinguishes namespace objects from named external bindings", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-script-declarations-"));
tempDirs.push(root);
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
fs.writeFileSync(
path.join(root, "scripts", "namespace.mjs"),
'export * as shared from "pkg";\n',
);
fs.writeFileSync(
path.join(root, "scripts", "named.mjs"),
'export { namespace as shared } from "pkg";\n',
);
fs.writeFileSync(
path.join(root, "scripts", "barrel.mjs"),
'export * from "./namespace.mjs";\nexport * from "./named.mjs";\n',
);
fs.writeFileSync(path.join(root, "scripts", "barrel.d.mts"), "export {};\n");
expect(
verifyScriptDeclarationContracts({
root,
files: [
"scripts/barrel.d.mts",
"scripts/barrel.mjs",
"scripts/named.mjs",
"scripts/namespace.mjs",
],
}),
).toEqual({
checked: 1,
issues: ['scripts/barrel.mjs: opaque external star collision "shared"'],
});
});
});
+1
View File
@@ -3196,6 +3196,7 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
expect(workflow).toContain("check-guards");
expect(workflow).toContain("check-shrinkwrap");
expect(preflightGuards).toContain("pnpm check:script-declarations");
expect(shrinkwrapGuards).toContain("pnpm deps:shrinkwrap:check");
expect(preflightGuards).toContain("pnpm deps:patches:check");
expect(parsedWorkflow.jobs.preflight.outputs.diff_base_revision).toBe(