From 64695e502468b15ee8531ad41e2cf9e9b6f06de1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 9 Aug 2026 18:58:28 -0700 Subject: [PATCH] chore: detect export name collisions (#121300) * chore(scripts): add export-name-collision check with debt baseline * chore(scripts): allowlist per-module test-hook export idiom * chore(scripts): recognize const forwarders, harness files, and JS sources in collision check --- .github/workflows/ci.yml | 12 + package.json | 2 + scripts/check-export-name-collisions.mts | 635 ++++++ .../lib/export-name-collision-baseline.json | 1849 +++++++++++++++++ scripts/lib/ts-guard-utils.mts | 2 +- .../check-export-name-collisions.test.ts | 237 +++ test/scripts/ci-workflow-guards.test.ts | 24 + 7 files changed, 2760 insertions(+), 1 deletion(-) create mode 100644 scripts/check-export-name-collisions.mts create mode 100644 scripts/lib/export-name-collision-baseline.json create mode 100644 test/scripts/check-export-name-collisions.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2aba50c0a81f..6d44278b163e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2450,6 +2450,9 @@ jobs: - check_name: check-prompt-snapshots group: prompt-snapshots runner: blacksmith-8vcpu-ubuntu-2404 + - check_name: check-export-name-collisions + group: export-name-collisions + runner: blacksmith-4vcpu-ubuntu-2404 - check_name: check-session-accessor-boundary group: session-accessor-boundary runner: blacksmith-4vcpu-ubuntu-2404 @@ -2649,6 +2652,15 @@ jobs: run_check "prompt:snapshots:check" pnpm prompt:snapshots:check fi ;; + export-name-collisions) + if [ ! -f scripts/check-export-name-collisions.mts ]; then + echo "[skip] export name collision check is not present in this checkout" + elif ! node -e 'const pkg = require("./package.json"); process.exit(pkg.scripts?.["lint:tmp:export-name-collisions"] ? 0 : 1);'; then + echo "[skip] export name collision script is not present in package.json" + else + run_check "lint:tmp:export-name-collisions" pnpm run lint:tmp:export-name-collisions + fi + ;; session-accessor-boundary) if [ ! -f scripts/check-session-accessor-boundary.mts ]; then echo "[skip] session accessor boundary check is not present in this checkout" diff --git a/package.json b/package.json index cba35a0fd4ad..821777a478af 100644 --- a/package.json +++ b/package.json @@ -1672,6 +1672,8 @@ "lint:swift": "./scripts/lint-swift.sh", "lint:tmp:channel-agnostic-boundaries": "node --import tsx scripts/check-channel-agnostic-boundaries.mts", "lint:tmp:dynamic-import-warts": "node --import tsx scripts/check-dynamic-import-warts.mts", + "lint:tmp:export-name-collisions": "node --import tsx scripts/check-export-name-collisions.mts", + "lint:tmp:export-name-collisions:gen": "node --import tsx scripts/check-export-name-collisions.mts --update-debt-baseline", "lint:tmp:no-random-messaging": "node --import tsx scripts/check-no-random-messaging-tmp.mts", "lint:tmp:no-raw-channel-fetch": "node --import tsx scripts/check-no-raw-channel-fetch.mts", "lint:tmp:no-raw-http2-imports": "node --import tsx scripts/check-no-raw-http2-imports.mts", diff --git a/scripts/check-export-name-collisions.mts b/scripts/check-export-name-collisions.mts new file mode 100644 index 000000000000..50d1b474af02 --- /dev/null +++ b/scripts/check-export-name-collisions.mts @@ -0,0 +1,635 @@ +#!/usr/bin/env node + +import fs from "node:fs/promises"; +import path from "node:path"; +import ts from "typescript"; +import { z } from "zod"; +import { resolveRepoRoot } from "./lib/repo-root.mjs"; +import { + collectTypeScriptFilesFromRoots, + isTestLikeTypeScriptFile, + resolveSourceRoots, + runAsScript, + unwrapExpression, +} from "./lib/ts-guard-utils.mts"; + +export type ExportNameCollision = { + name: string; + files: string[]; + sdk?: true; +}; + +type SourceModule = { + content: string; + includeDefinitions?: boolean; + path: string; +}; + +type ModuleExports = { + definitions: Set; + exportedNames: Set; + starExportSpecifiers: string[]; +}; + +const exportNameCollisionSchema = z + .object({ + name: z.string(), + files: z.array(z.string()), + sdk: z.literal(true).optional(), + }) + .strict(); +const exportNameCollisionBaselineSchema = z.array(exportNameCollisionSchema); + +const baselineRelativePath = "scripts/lib/export-name-collision-baseline.json"; +const baselineRegenCommand = "pnpm lint:tmp:export-name-collisions:gen"; +const failurePrefix = "check-export-name-collisions"; +const extraExcludedFileSuffixes = [".test-support.ts", ".test-helpers.ts", ".d.ts"]; + +function normalizeRelativePath(filePath: string) { + return filePath.replaceAll(path.sep, "/"); +} + +export function isExcludedExportCollisionSource(filePath: string) { + const normalized = normalizeRelativePath(filePath); + const segments = normalized.split("/"); + return ( + segments.includes("test") || + segments.includes("__fixtures__") || + isTestLikeTypeScriptFile(normalized, extraExcludedFileSuffixes) + ); +} + +function hasModifier(node: ts.Node, kind: ts.SyntaxKind) { + return ts.canHaveModifiers(node) && ts.getModifiers(node)?.some((item) => item.kind === kind); +} + +function collectBindingNames(name: ts.BindingName, names: Set) { + if (ts.isIdentifier(name)) { + names.add(name.text); + return; + } + for (const element of name.elements) { + if (ts.isBindingElement(element)) { + collectBindingNames(element.name, names); + } + } +} + +function parametersAreForwarded( + parameters: ts.NodeArray, + args: ts.NodeArray, +) { + if (parameters.length !== args.length) { + return false; + } + return parameters.every((parameter, index) => { + if (!ts.isIdentifier(parameter.name)) { + return false; + } + const argument = args[index]; + if (!argument) { + return false; + } + if (parameter.dotDotDotToken) { + if (!ts.isSpreadElement(argument)) { + return false; + } + const forwarded = unwrapExpression(argument.expression); + return ts.isIdentifier(forwarded) && forwarded.text === parameter.name.text; + } + if (ts.isSpreadElement(argument)) { + return false; + } + const forwarded = unwrapExpression(argument); + return ts.isIdentifier(forwarded) && forwarded.text === parameter.name.text; + }); +} + +function isAwaitedZeroArgumentCall(expression: ts.Expression) { + const unwrapped = unwrapExpression(expression); + if (!ts.isAwaitExpression(unwrapped)) { + return false; + } + const awaited = unwrapExpression(unwrapped.expression); + return ts.isCallExpression(awaited) && awaited.arguments.length === 0; +} + +function returnCall(statement: ts.Statement) { + if (!ts.isReturnStatement(statement) || !statement.expression) { + return null; + } + const expression = unwrapExpression(statement.expression); + return ts.isCallExpression(expression) ? expression : null; +} + +function isStaticImportForwarder( + call: ts.CallExpression, + functionName: string, + importedNamesByLocalName: ReadonlyMap, +) { + const callee = unwrapExpression(call.expression); + return ( + ts.isIdentifier(callee) && + callee.text !== functionName && + importedNamesByLocalName.get(callee.text) === functionName + ); +} + +function isLazyModuleForwarderCall( + call: ts.CallExpression, + functionName: string, + moduleObjectName?: string, +) { + const callee = unwrapExpression(call.expression); + if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== functionName) { + return false; + } + const target = unwrapExpression(callee.expression); + if (moduleObjectName) { + return ts.isIdentifier(target) && target.text === moduleObjectName; + } + return isAwaitedZeroArgumentCall(target); +} + +function isForwardingOnlyFunction( + declaration: ts.FunctionDeclaration | ts.ArrowFunction, + functionName: string, + importedNamesByLocalName: ReadonlyMap, +) { + const body = declaration.body; + if (!body) { + return false; + } + + if (!ts.isBlock(body)) { + const expression = unwrapExpression(body); + return ( + ts.isCallExpression(expression) && + parametersAreForwarded(declaration.parameters, expression.arguments) && + (isStaticImportForwarder(expression, functionName, importedNamesByLocalName) || + isLazyModuleForwarderCall(expression, functionName)) + ); + } + + if (body.statements.length === 1) { + const statement = body.statements[0]; + if (!statement) { + return false; + } + const call = returnCall(statement); + return Boolean( + call && + parametersAreForwarded(declaration.parameters, call.arguments) && + (isStaticImportForwarder(call, functionName, importedNamesByLocalName) || + isLazyModuleForwarderCall(call, functionName)), + ); + } + + if (body.statements.length !== 2) { + return false; + } + const loadStatement = body.statements[0]; + const returnStatement = body.statements[1]; + if ( + !loadStatement || + !returnStatement || + !ts.isVariableStatement(loadStatement) || + !(loadStatement.declarationList.flags & ts.NodeFlags.Const) || + loadStatement.declarationList.declarations.length !== 1 + ) { + return false; + } + const declarationItem = loadStatement.declarationList.declarations[0]; + if ( + !declarationItem || + !ts.isIdentifier(declarationItem.name) || + !declarationItem.initializer || + !isAwaitedZeroArgumentCall(declarationItem.initializer) + ) { + return false; + } + const call = returnCall(returnStatement); + return Boolean( + call && + parametersAreForwarded(declaration.parameters, call.arguments) && + isLazyModuleForwarderCall(call, functionName, declarationItem.name.text), + ); +} + +function isForwardingOnlyConst( + declaration: ts.VariableDeclaration, + exportName: string, + importedNamesByLocalName: ReadonlyMap, +) { + if (!ts.isIdentifier(declaration.name) || !declaration.initializer) { + return false; + } + const initializer = unwrapExpression(declaration.initializer); + if (ts.isIdentifier(initializer)) { + return importedNamesByLocalName.get(initializer.text) === exportName; + } + return ( + ts.isArrowFunction(initializer) && + isForwardingOnlyFunction(initializer, exportName, importedNamesByLocalName) + ); +} + +/** Collects value exports and locally defined exported functions/consts from one module. */ +export function collectModuleExportNames(content: string, fileName = "source.ts"): ModuleExports { + const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true); + const importedNamesByLocalName = new Map(); + const localConstDeclarations = new Map(); + const localFunctions = new Map(); + const directlyExportedNames = new Set(); + const locallyExportedNames = new Set(); + const exportedNames = new Set(); + const starExportSpecifiers: string[] = []; + + for (const statement of sourceFile.statements) { + if (ts.isImportDeclaration(statement)) { + const bindings = statement.importClause?.namedBindings; + if (bindings && ts.isNamedImports(bindings)) { + for (const specifier of bindings.elements) { + if (!statement.importClause?.isTypeOnly && !specifier.isTypeOnly) { + importedNamesByLocalName.set( + specifier.name.text, + specifier.propertyName?.text ?? specifier.name.text, + ); + } + } + } + continue; + } + + if (ts.isFunctionDeclaration(statement) && statement.name) { + const name = statement.name.text; + const declarations = localFunctions.get(name) ?? []; + declarations.push(statement); + localFunctions.set(name, declarations); + if ( + hasModifier(statement, ts.SyntaxKind.ExportKeyword) && + !hasModifier(statement, ts.SyntaxKind.DefaultKeyword) + ) { + directlyExportedNames.add(name); + exportedNames.add(name); + } + continue; + } + + if (ts.isVariableStatement(statement)) { + const isConst = Boolean(statement.declarationList.flags & ts.NodeFlags.Const); + if (!isConst) { + continue; + } + const statementNames = new Set(); + for (const declaration of statement.declarationList.declarations) { + const declarationNames = new Set(); + collectBindingNames(declaration.name, declarationNames); + for (const name of declarationNames) { + statementNames.add(name); + const declarations = localConstDeclarations.get(name) ?? []; + declarations.push(declaration); + localConstDeclarations.set(name, declarations); + } + } + for (const name of statementNames) { + if (hasModifier(statement, ts.SyntaxKind.ExportKeyword)) { + directlyExportedNames.add(name); + exportedNames.add(name); + } + } + continue; + } + + const moduleSpecifier = ts.isExportDeclaration(statement) + ? statement.moduleSpecifier + : undefined; + if ( + ts.isExportDeclaration(statement) && + !statement.exportClause && + moduleSpecifier && + ts.isStringLiteral(moduleSpecifier) + ) { + starExportSpecifiers.push(moduleSpecifier.text); + continue; + } + + if ( + ts.isExportDeclaration(statement) && + statement.exportClause && + ts.isNamedExports(statement.exportClause) && + !statement.isTypeOnly + ) { + for (const specifier of statement.exportClause.elements) { + if (specifier.isTypeOnly) { + continue; + } + const localName = specifier.propertyName?.text ?? specifier.name.text; + // Renamed re-exports are deliberately outside this guard's first slice. + if (specifier.name.text !== localName) { + continue; + } + exportedNames.add(specifier.name.text); + if (!statement.moduleSpecifier) { + locallyExportedNames.add(localName); + } + } + } + } + + const definitions = new Set(); + for (const name of new Set([...directlyExportedNames, ...locallyExportedNames])) { + const constDeclarations = localConstDeclarations.get(name); + if (constDeclarations) { + const [constDeclaration] = constDeclarations; + if ( + constDeclarations.length === 1 && + constDeclaration && + isForwardingOnlyConst(constDeclaration, name, importedNamesByLocalName) + ) { + continue; + } + definitions.add(name); + continue; + } + const functionDeclarations = localFunctions.get(name); + if (!functionDeclarations) { + continue; + } + const implementation = functionDeclarations.find((declaration) => declaration.body); + // Lazy runtime facades are mandated by AGENTS.md. Exempt only exact same-name + // argument forwarding so those boundaries do not become duplicate behavior. + if ( + implementation && + isForwardingOnlyFunction(implementation, name, importedNamesByLocalName) + ) { + continue; + } + definitions.add(name); + } + + return { definitions, exportedNames, starExportSpecifiers }; +} + +function resolveStarExportPath( + sourcePath: string, + specifier: string, + modulesByPath: ReadonlyMap, +) { + if (!specifier.startsWith(".")) { + return null; + } + const unresolved = path.posix.normalize( + path.posix.join(path.posix.dirname(sourcePath), specifier), + ); + const extensionless = unresolved.replace(/\.(?:c|m)?(?:j|t)s$/u, ""); + const candidates = [ + `${extensionless}.ts`, + `${extensionless}.mts`, + `${extensionless}.js`, + `${extensionless}.mjs`, + `${extensionless}/index.ts`, + `${extensionless}/index.mts`, + `${extensionless}/index.js`, + `${extensionless}/index.mjs`, + ]; + return candidates.find((candidate) => modulesByPath.has(candidate)) ?? null; +} + +function collectTransitiveExportNames( + modulePath: string, + modulesByPath: ReadonlyMap, + visiting = new Set(), +): Set { + if (visiting.has(modulePath)) { + return new Set(); + } + const moduleExports = modulesByPath.get(modulePath); + if (!moduleExports) { + return new Set(); + } + const nextVisiting = new Set(visiting).add(modulePath); + const names = new Set(moduleExports.exportedNames); + for (const specifier of moduleExports.starExportSpecifiers) { + const targetPath = resolveStarExportPath(modulePath, specifier, modulesByPath); + if (!targetPath) { + continue; + } + for (const name of collectTransitiveExportNames(targetPath, modulesByPath, nextVisiting)) { + names.add(name); + } + } + return names; +} + +// Per-module test-hook namespaces are an intentional same-name family: each module +// exports its own `testing`/`testApi` object and tests import it qualified from that +// exact module. Flagging them would push burn-down work to "fix" a deliberate idiom. +const intentionalSameNameFamilies = new Set(["testing", "testApi"]); + +/** Finds duplicate exported function/const definitions across source modules. */ +export function findExportNameCollisions(modules: SourceModule[]): ExportNameCollision[] { + const filesByName = new Map>(); + const sdkExportNames = new Set(); + const modulesByPath = new Map(); + for (const sourceModule of modules.toSorted((left, right) => + left.path.localeCompare(right.path), + )) { + const relativePath = normalizeRelativePath(sourceModule.path); + const moduleExports = collectModuleExportNames(sourceModule.content, relativePath); + modulesByPath.set(relativePath, moduleExports); + if (sourceModule.includeDefinitions !== false) { + for (const name of moduleExports.definitions) { + const files = filesByName.get(name) ?? new Set(); + files.add(relativePath); + filesByName.set(name, files); + } + } + } + for (const modulePath of modulesByPath.keys()) { + if (!modulePath.startsWith("src/plugin-sdk/")) { + continue; + } + for (const name of collectTransitiveExportNames(modulePath, modulesByPath)) { + sdkExportNames.add(name); + } + } + + const collisions: ExportNameCollision[] = []; + for (const [name, fileSet] of filesByName) { + if (fileSet.size < 2 || intentionalSameNameFamilies.has(name)) { + continue; + } + const collision: ExportNameCollision = { + name, + files: [...fileSet].toSorted(), + }; + if (sdkExportNames.has(name)) { + collision.sdk = true; + } + collisions.push(collision); + } + return collisions.toSorted((left, right) => left.name.localeCompare(right.name)); +} + +type CollisionChange = { + baseline?: ExportNameCollision; + current?: ExportNameCollision; +}; + +/** Compares every collision cluster so additions fail and removals ratchet debt down. */ +export function compareExportNameCollisionDebt( + current: ExportNameCollision[], + baseline: ExportNameCollision[], +) { + const currentByName = new Map(current.map((collision) => [collision.name, collision])); + const baselineByName = new Map(baseline.map((collision) => [collision.name, collision])); + const regressions: CollisionChange[] = []; + const improvements: CollisionChange[] = []; + const names = [...new Set([...currentByName.keys(), ...baselineByName.keys()])].toSorted(); + + for (const name of names) { + const currentCollision = currentByName.get(name); + const baselineCollision = baselineByName.get(name); + if (!baselineCollision) { + regressions.push({ current: currentCollision }); + continue; + } + if (!currentCollision) { + improvements.push({ baseline: baselineCollision }); + continue; + } + const baselineFiles = new Set(baselineCollision.files); + const currentFiles = new Set(currentCollision.files); + const hasAddedFile = currentCollision.files.some((file) => !baselineFiles.has(file)); + const hasRemovedFile = baselineCollision.files.some((file) => !currentFiles.has(file)); + if (hasAddedFile || (currentCollision.sdk === true && baselineCollision.sdk !== true)) { + regressions.push({ baseline: baselineCollision, current: currentCollision }); + } + if (hasRemovedFile || (baselineCollision.sdk === true && currentCollision.sdk !== true)) { + improvements.push({ baseline: baselineCollision, current: currentCollision }); + } + } + return { regressions, improvements }; +} + +function resolveBaselinePath(repoRoot: string) { + return path.join(repoRoot, ...baselineRelativePath.split("/")); +} + +export async function collectRepositoryCollisions(repoRoot: string) { + const sourceCollectOptions = { + fileExtensions: [".ts", ".mts", ".js", ".mjs"], + includeTests: true, + skipDirectories: ["test", "__fixtures__"], + }; + const supportCollectOptions = { + ...sourceCollectOptions, + fileExtensions: [".ts", ".mts"], + }; + const [collectedFiles, collectedSupportFiles] = await Promise.all([ + collectTypeScriptFilesFromRoots(resolveSourceRoots(repoRoot, ["src"]), sourceCollectOptions), + // Package modules are resolution-only: Plugin SDK barrels can export their + // names, but the collision rule itself remains scoped to src/ definitions. + collectTypeScriptFilesFromRoots( + resolveSourceRoots(repoRoot, ["packages"]), + supportCollectOptions, + ), + ]); + const files = collectedFiles.filter((filePath) => !isExcludedExportCollisionSource(filePath)); + const supportFiles = collectedSupportFiles.filter( + (filePath) => !isExcludedExportCollisionSource(filePath), + ); + const modules = await Promise.all( + [ + ...files.map((filePath) => ({ filePath, includeDefinitions: true })), + ...supportFiles.map((filePath) => ({ filePath, includeDefinitions: false })), + ].map(async ({ filePath, includeDefinitions }) => ({ + content: await fs.readFile(filePath, "utf8"), + includeDefinitions, + path: normalizeRelativePath(path.relative(repoRoot, filePath)), + })), + ); + return findExportNameCollisions(modules); +} + +async function readBaseline(repoRoot: string) { + try { + return exportNameCollisionBaselineSchema.parse( + JSON.parse(await fs.readFile(resolveBaselinePath(repoRoot), "utf8")), + ); + } catch (error) { + if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") { + return null; + } + throw error; + } +} + +async function writeBaseline(repoRoot: string) { + const collisions = await collectRepositoryCollisions(repoRoot); + await fs.writeFile(resolveBaselinePath(repoRoot), `${JSON.stringify(collisions, null, 2)}\n`); + return collisions.length; +} + +function formatCollision(collision: ExportNameCollision | undefined) { + return JSON.stringify(collision); +} + +export async function main() { + const repoRoot = resolveRepoRoot(import.meta.url); + if (process.argv.includes("--update-debt-baseline")) { + const count = await writeBaseline(repoRoot); + console.log(`Wrote ${baselineRelativePath} (${count} entries)`); + return 0; + } + + const baseline = await readBaseline(repoRoot); + if (!baseline) { + console.error( + `Missing ${baselineRelativePath}; run \`${baselineRegenCommand}\` and commit it.`, + ); + return 1; + } + const current = await collectRepositoryCollisions(repoRoot); + const debt = compareExportNameCollisionDebt(current, baseline); + if (debt.regressions.length === 0 && debt.improvements.length === 0) { + console.log("export name collision guard passed."); + return 0; + } + + if (debt.regressions.length > 0) { + console.error( + `Found new exported function/const name collisions beyond ${baselineRelativePath}:`, + ); + for (const regression of debt.regressions) { + console.error(`- ${formatCollision(regression.current)}`); + } + console.error( + `Give each behavior one exported spelling. If the debt increase is intentional, run \`${baselineRegenCommand}\` and commit the generated baseline.`, + ); + } + if (debt.improvements.length > 0) { + console.error(`Export name collision debt dropped below ${baselineRelativePath}:`); + for (const improvement of debt.improvements) { + console.error( + `- ${improvement.baseline?.name}: ${formatCollision(improvement.baseline)} -> ${formatCollision(improvement.current)}`, + ); + } + console.error(`Run \`${baselineRegenCommand}\` to ratchet the baseline down and commit it.`); + } + return 1; +} + +runAsScript(import.meta.url, async () => { + let exitCode = 1; + try { + exitCode = await main(); + } catch (error) { + console.error(error); + } + if (exitCode !== 0) { + process.exitCode = exitCode; + console.error(`[${failurePrefix}] FAILED (exit ${exitCode})`); + } +}); diff --git a/scripts/lib/export-name-collision-baseline.json b/scripts/lib/export-name-collision-baseline.json new file mode 100644 index 000000000000..3199291eb3f5 --- /dev/null +++ b/scripts/lib/export-name-collision-baseline.json @@ -0,0 +1,1849 @@ +[ + { + "name": "agentCommand", + "files": [ + "src/agents/agent-command.ts", + "src/gateway/test-helpers.runtime-state.ts" + ] + }, + { + "name": "applyExclusiveSlotSelection", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/slots.ts" + ] + }, + { + "name": "applyFinalEffectiveToolPolicy", + "files": [ + "src/agents/embedded-agent-runner/effective-tool-policy.ts", + "src/gateway/server-methods/__mocks__/tools-effective.runtime.ts" + ] + }, + { + "name": "applyPluginUninstallDirectoryRemoval", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/uninstall.ts" + ] + }, + { + "name": "asString", + "files": [ + "src/channels/plugins/status-issues/shared.ts", + "src/cli/nodes-media-utils.ts", + "src/tui/tui-formatters.ts" + ], + "sdk": true + }, + { + "name": "authorizeConfigWrite", + "files": [ + "src/channels/plugins/config-writes.ts", + "src/plugin-sdk/channel-config-helpers.ts" + ], + "sdk": true + }, + { + "name": "buildApprovalPresentation", + "files": [ + "src/infra/approval-presentation.ts", + "src/infra/exec-approval-reply.ts" + ], + "sdk": true + }, + { + "name": "buildBundleMcpToolsFromCatalog", + "files": [ + "src/agents/agent-bundle-mcp-materialize.ts", + "src/gateway/server-methods/__mocks__/tools-effective.runtime.ts" + ] + }, + { + "name": "buildChannelAccountSnapshot", + "files": [ + "src/channels/account-summary.ts", + "src/channels/plugins/status.ts" + ] + }, + { + "name": "buildPluginDiagnosticsReport", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/status.ts" + ] + }, + { + "name": "buildPluginInspectReport", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/status.ts" + ] + }, + { + "name": "buildPluginRegistrySnapshotReport", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/status-snapshot.ts" + ] + }, + { + "name": "buildPluginSnapshotReport", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/status.ts" + ] + }, + { + "name": "buildQaTarget", + "files": [ + "src/plugin-sdk/qa-channel-protocol.ts", + "src/plugin-sdk/qa-channel.ts" + ], + "sdk": true + }, + { + "name": "bundledPluginFile", + "files": [ + "src/plugin-sdk/test-helpers/bundled-plugin-paths.ts", + "src/plugins/contracts/test-helpers/bundled-plugin-roots.ts" + ], + "sdk": true + }, + { + "name": "callGateway", + "files": [ + "src/cli/program.test-mocks.ts", + "src/gateway/call.ts" + ] + }, + { + "name": "callGatewayCli", + "files": [ + "src/cli/nodes-cli/rpc.ts", + "src/gateway/call.ts" + ] + }, + { + "name": "canBypassConfigWritePolicy", + "files": [ + "src/channels/plugins/config-writes.ts", + "src/plugin-sdk/channel-config-helpers.ts" + ], + "sdk": true + }, + { + "name": "cancelDetachedTaskRunById", + "files": [ + "src/tasks/task-executor-cancel.runtime.ts", + "src/tasks/task-executor.ts" + ] + }, + { + "name": "classifySessionKind", + "files": [ + "src/agents/tools/sessions-helpers.ts", + "src/sessions/classify-session-kind.ts" + ] + }, + { + "name": "clearPluginRegistryLoadCache", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/loader-cache.ts" + ] + }, + { + "name": "clearRuntimeAuthProfileStoreSnapshots", + "files": [ + "src/agents/auth-profiles/runtime-snapshots.ts", + "src/agents/auth-profiles/store.ts" + ], + "sdk": true + }, + { + "name": "clearSecretsRuntimeSnapshot", + "files": [ + "src/secrets/runtime-state.ts", + "src/secrets/runtime.ts" + ] + }, + { + "name": "closeActiveMemorySearchManager", + "files": [ + "src/plugin-sdk/memory-host-search.ts", + "src/plugins/memory-runtime.ts" + ], + "sdk": true + }, + { + "name": "closeActiveMemorySearchManagers", + "files": [ + "src/plugin-sdk/memory-host-search.ts", + "src/plugins/memory-runtime.ts" + ], + "sdk": true + }, + { + "name": "compactEmbeddedAgentSessionDirect", + "files": [ + "src/agents/embedded-agent-runner/compact.runtime.ts", + "src/agents/embedded-agent-runner/compact.ts" + ] + }, + { + "name": "completeTaskRunByRunId", + "files": [ + "src/tasks/detached-task-runtime.ts", + "src/tasks/task-executor.ts" + ] + }, + { + "name": "containsConfigIncludeDirective", + "files": [ + "src/config/io.read-helpers.ts", + "src/plugins/install-persistence.ts" + ] + }, + { + "name": "countActiveDescendantRuns", + "files": [ + "src/agents/subagent-registry-read.ts", + "src/agents/subagent-registry.ts" + ] + }, + { + "name": "countPendingDescendantRuns", + "files": [ + "src/agents/subagent-registry-announce-read.ts", + "src/agents/subagent-registry.ts" + ] + }, + { + "name": "createCompactionDiagId", + "files": [ + "src/agents/embedded-agent-runner/compaction-diagnostics.ts", + "src/agents/embedded-agent-runner/run/helpers.ts" + ] + }, + { + "name": "createFixedWindowRateLimiter", + "files": [ + "src/infra/fixed-window-rate-limit.ts", + "src/plugin-sdk/webhook-memory-guards.ts" + ], + "sdk": true + }, + { + "name": "createGatewayStartupTrace", + "files": [ + "src/cli/startup-trace.ts", + "src/gateway/server-startup-trace.ts" + ] + }, + { + "name": "createPluginLoaderLogger", + "files": [ + "src/plugins/loader-shared.ts", + "src/plugins/logger.ts" + ] + }, + { + "name": "createQueuedTaskRun", + "files": [ + "src/tasks/detached-task-runtime.ts", + "src/tasks/task-executor.ts" + ] + }, + { + "name": "createRunningTaskRun", + "files": [ + "src/tasks/detached-task-runtime.ts", + "src/tasks/task-executor.ts" + ] + }, + { + "name": "createSessionId", + "files": [ + "src/agents/sessions/session-manager-id.ts", + "src/agents/tools/transcripts-tool-runtime.ts" + ] + }, + { + "name": "createSessionSlug", + "files": [ + "src/agents/bash-process-registry.ts", + "src/agents/session-slug.ts" + ] + }, + { + "name": "createSuiteTempRootTracker", + "files": [ + "src/plugins/test-helpers/fs-fixtures.ts", + "src/test-helpers/temp-dir.ts" + ] + }, + { + "name": "createTestRegistry", + "files": [ + "src/gateway/server/__tests__/test-utils.ts", + "src/test-utils/channel-plugins.ts" + ], + "sdk": true + }, + { + "name": "DEFAULT_TIMEOUT_MS", + "files": [ + "src/infra/provider-usage.shared.ts", + "src/infra/update-runner-command.ts" + ] + }, + { + "name": "defaultRuntime", + "files": [ + "src/cli/daemon-cli/test-helpers/lifecycle-core-harness.ts", + "src/runtime.ts" + ], + "sdk": true + }, + { + "name": "deliverInboundReplyWithMessageSendContext", + "files": [ + "src/channels/turn/durable-delivery.ts", + "src/plugin-sdk/channel-outbound.ts" + ], + "sdk": true + }, + { + "name": "describeImagesWithModel", + "files": [ + "src/media-understanding/image-runtime.ts", + "src/media-understanding/image.ts" + ], + "sdk": true + }, + { + "name": "describeImagesWithModelPayloadTransform", + "files": [ + "src/media-understanding/image-runtime.ts", + "src/media-understanding/image.ts" + ], + "sdk": true + }, + { + "name": "describeImageWithModel", + "files": [ + "src/media-understanding/image-runtime.ts", + "src/media-understanding/image.ts" + ], + "sdk": true + }, + { + "name": "describeImageWithModelPayloadTransform", + "files": [ + "src/media-understanding/image-runtime.ts", + "src/media-understanding/image.ts" + ], + "sdk": true + }, + { + "name": "dispatchChannelInboundReply", + "files": [ + "src/channels/message/inbound-reply-dispatch.ts", + "src/channels/turn/kernel.ts" + ], + "sdk": true + }, + { + "name": "dispatchChannelInboundTurn", + "files": [ + "src/channels/message/inbound-reply-dispatch.ts", + "src/channels/turn/kernel.ts" + ], + "sdk": true + }, + { + "name": "dispatchReplyWithBufferedBlockDispatcher", + "files": [ + "src/auto-reply/reply/provider-dispatcher.ts", + "src/plugin-sdk/reply-dispatch-runtime.ts" + ], + "sdk": true + }, + { + "name": "dispatchReplyWithDispatcher", + "files": [ + "src/auto-reply/reply/provider-dispatcher.ts", + "src/plugin-sdk/reply-dispatch-runtime.ts" + ], + "sdk": true + }, + { + "name": "doctorCommand", + "files": [ + "src/commands/doctor.ts", + "src/flows/doctor-health.ts" + ] + }, + { + "name": "drainPendingDeliveries", + "files": [ + "src/infra/outbound/delivery-queue-recovery.ts", + "src/plugin-sdk/delivery-queue-runtime.ts" + ], + "sdk": true + }, + { + "name": "enablePluginInConfig", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugin-sdk/provider-enable-config.ts", + "src/plugins/enable.ts" + ], + "sdk": true + }, + { + "name": "ensureConfigReady", + "files": [ + "src/cli/program.test-mocks.ts", + "src/cli/program/config-guard.ts" + ] + }, + { + "name": "ensureConfiguredAcpBindingReady", + "files": [ + "src/acp/persistent-bindings.lifecycle.ts", + "src/plugin-sdk/core.ts" + ], + "sdk": true + }, + { + "name": "ensureRecord", + "files": [ + "src/commands/doctor/shared/legacy-config-record-shared.ts", + "src/config/legacy.shared.ts" + ] + }, + { + "name": "extractAssistantText", + "files": [ + "src/agents/embedded-agent-utils.ts", + "src/agents/tools/chat-history-text.ts" + ], + "sdk": true + }, + { + "name": "extractAssistantVisibleText", + "files": [ + "src/agents/embedded-agent-utils.ts", + "src/shared/chat-message-content.ts" + ] + }, + { + "name": "extractMessageText", + "files": [ + "src/auto-reply/reply/commands-subagents-text.ts", + "src/gateway/session-transcript-readers.ts" + ] + }, + { + "name": "failTaskRunByRunId", + "files": [ + "src/tasks/detached-task-runtime.ts", + "src/tasks/task-executor.ts" + ] + }, + { + "name": "fileExists", + "files": [ + "src/infra/state-migrations.fs.ts", + "src/media-understanding/fs.ts", + "src/plugin-sdk/security-runtime.ts" + ], + "sdk": true + }, + { + "name": "finalizeTaskRunByRunId", + "files": [ + "src/tasks/detached-task-runtime.ts", + "src/tasks/task-registry-record-api.ts" + ] + }, + { + "name": "formatConfigPath", + "files": [ + "src/commands/doctor-config-analysis.ts", + "src/config/logging.ts" + ] + }, + { + "name": "formatConfigWriteDeniedMessage", + "files": [ + "src/channels/plugins/config-writes.ts", + "src/plugin-sdk/channel-config-helpers.ts" + ], + "sdk": true + }, + { + "name": "formatEnvelopeTimestamp", + "files": [ + "src/auto-reply/envelope.ts", + "src/plugin-sdk/test-helpers/envelope-timestamp.ts" + ], + "sdk": true + }, + { + "name": "formatSkillsForPrompt", + "files": [ + "src/skills/loading/session.ts", + "src/skills/loading/skill-contract.ts" + ], + "sdk": true + }, + { + "name": "generateOAuthState", + "files": [ + "src/plugin-sdk/provider-auth-runtime.ts", + "src/plugin-sdk/provider-oauth-runtime.ts" + ], + "sdk": true + }, + { + "name": "getActiveMemorySearchManager", + "files": [ + "src/plugin-sdk/memory-host-search.ts", + "src/plugins/memory-runtime.ts" + ], + "sdk": true + }, + { + "name": "getActivePluginChannelRegistryVersion", + "files": [ + "src/gateway/server-methods/__mocks__/tools-effective.runtime.ts", + "src/plugins/runtime.ts" + ] + }, + { + "name": "getActivePluginRegistryVersion", + "files": [ + "src/gateway/server-methods/__mocks__/tools-effective.runtime.ts", + "src/plugins/runtime.ts" + ] + }, + { + "name": "getChatChannelMeta", + "files": [ + "src/channels/chat-meta.ts", + "src/plugin-sdk/core.ts" + ], + "sdk": true + }, + { + "name": "getFreeGatewayPort", + "files": [ + "src/gateway/gateway-cli-backend.live-helpers.ts", + "src/gateway/test-helpers.e2e.ts" + ] + }, + { + "name": "getFreePort", + "files": [ + "src/gateway/test-helpers.server.ts", + "src/test-utils/ports.ts" + ] + }, + { + "name": "getLatestSubagentRunByChildSessionKey", + "files": [ + "src/agents/subagent-registry-read.ts", + "src/agents/subagent-registry.ts" + ] + }, + { + "name": "getReplyFromConfig", + "files": [ + "src/auto-reply/reply/get-reply.ts", + "src/gateway/test-helpers.runtime-state.ts" + ], + "sdk": true + }, + { + "name": "getRuntimeAuthForModel", + "files": [ + "src/plugin-sdk/provider-auth-runtime.ts", + "src/plugins/runtime/runtime-model-auth.runtime.ts" + ], + "sdk": true + }, + { + "name": "hasOwnProperty", + "files": [ + "src/secrets/runtime-shared.ts", + "src/tts/tts-settings.ts" + ], + "sdk": true + }, + { + "name": "hasUsableOAuthCredential", + "files": [ + "src/agents/auth-profiles/credential-state.ts", + "src/agents/auth-profiles/oauth-shared.ts" + ], + "sdk": true + }, + { + "name": "inferToolMetaFromArgs", + "files": [ + "src/agents/embedded-agent-utils.ts", + "src/plugin-sdk/agent-harness-runtime.ts" + ], + "sdk": true + }, + { + "name": "inspectPluginRegistry", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/plugin-registry-snapshot.ts" + ] + }, + { + "name": "inspectPortUsage", + "files": [ + "src/daemon/test-helpers/schtasks-fixtures.ts", + "src/infra/ports-inspect.ts" + ] + }, + { + "name": "installHooksFromNpmSpec", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/hooks/install.ts" + ] + }, + { + "name": "installHooksFromPath", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/hooks/install.ts" + ] + }, + { + "name": "installPluginFromClawHub", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/clawhub.ts" + ] + }, + { + "name": "installPluginFromGitSpec", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/git-install.ts" + ] + }, + { + "name": "installPluginFromMarketplace", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/marketplace.ts" + ] + }, + { + "name": "installPluginFromNpmPackArchive", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/install-npm-pack.ts" + ] + }, + { + "name": "installPluginFromNpmSpec", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/install-npm.ts" + ] + }, + { + "name": "installPluginFromPath", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/install-package.ts" + ] + }, + { + "name": "isClaudeCliProvider", + "files": [ + "src/agents/cli-runner/helpers.ts", + "src/plugin-sdk/anthropic-cli.ts" + ], + "sdk": true + }, + { + "name": "isCommandMessage", + "files": [ + "src/auto-reply/commands-registry.ts", + "src/tui/tui-formatters.ts" + ], + "sdk": true + }, + { + "name": "isQaRuntimeAvailable", + "files": [ + "src/plugin-sdk/qa-runner-runtime.ts", + "src/plugin-sdk/qa-runtime.ts" + ], + "sdk": true + }, + { + "name": "killProcessTree", + "files": [ + "src/agents/shell-utils.ts", + "src/daemon/test-helpers/schtasks-fixtures.ts" + ] + }, + { + "name": "listDescendantRunsForRequester", + "files": [ + "src/agents/subagent-registry-read.ts", + "src/agents/subagent-registry.ts" + ] + }, + { + "name": "listManagedPluginNpmRoots", + "files": [ + "src/commands/doctor-plugin-host-links.ts", + "src/plugins/npm-project-roots.ts" + ] + }, + { + "name": "listMemoryEmbeddingProviders", + "files": [ + "src/plugins/memory-embedding-provider-runtime.ts", + "src/plugins/memory-embedding-providers.ts" + ], + "sdk": true + }, + { + "name": "listSessionEntries", + "files": [ + "src/config/sessions/session-accessor.entry.ts", + "src/plugin-sdk/session-store-runtime.ts" + ], + "sdk": true + }, + { + "name": "listSubagentRunsForController", + "files": [ + "src/agents/subagent-registry-read.ts", + "src/agents/subagent-registry.ts" + ] + }, + { + "name": "loadBundledPluginPublicSurface", + "files": [ + "src/plugin-sdk/test-helpers/public-surface-loader.ts", + "src/test-utils/bundled-plugin-public-surface.ts" + ], + "sdk": true + }, + { + "name": "loadBundledPluginPublicSurfaceModuleSync", + "files": [ + "src/plugin-sdk/facade-loader.ts", + "src/plugin-sdk/facade-runtime.ts" + ], + "sdk": true + }, + { + "name": "loadCodexBundleMcpThreadConfig", + "files": [ + "src/agents/codex-mcp-config.ts", + "src/plugin-sdk/agent-harness-runtime.ts" + ], + "sdk": true + }, + { + "name": "loadCombinedSessionStoreForGateway", + "files": [ + "src/config/sessions/combined-store-gateway.ts", + "src/plugin-sdk/session-transcript-hit.ts" + ], + "sdk": true + }, + { + "name": "loadConfig", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/config/io.runtime.ts" + ], + "sdk": true + }, + { + "name": "loadGatewayTlsRuntime", + "files": [ + "src/gateway/server/tls.ts", + "src/infra/tls/gateway.ts" + ] + }, + { + "name": "loadJsonFile", + "files": [ + "src/infra/json-file.ts", + "src/plugin-sdk/json-store.ts" + ], + "sdk": true + }, + { + "name": "loadPluginManifestRegistry", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/manifest-registry.ts" + ], + "sdk": true + }, + { + "name": "loadQaRuntimeModule", + "files": [ + "src/plugin-sdk/qa-runner-runtime.ts", + "src/plugin-sdk/qa-runtime.ts" + ], + "sdk": true + }, + { + "name": "loadSessionStore", + "files": [ + "src/library.ts", + "src/plugin-sdk/session-store-runtime.ts" + ], + "sdk": true + }, + { + "name": "loadWebMedia", + "files": [ + "src/media/web-media.ts", + "src/plugins/runtime/runtime-web-channel-plugin.ts" + ], + "sdk": true + }, + { + "name": "loadWebMediaRaw", + "files": [ + "src/media/web-media.ts", + "src/plugins/runtime/runtime-web-channel-plugin.ts" + ], + "sdk": true + }, + { + "name": "log", + "files": [ + "src/agents/auth-profiles/constants.ts", + "src/agents/embedded-agent-runner/logger.ts", + "src/agents/main-session-restart-recovery-shared.ts", + "src/system-agent/setup-inference-core.ts", + "src/tasks/task-registry-state.ts" + ] + }, + { + "name": "makeTempDir", + "files": [ + "src/infra/exec-approvals-test-helpers.ts", + "src/plugins/loader.test-fixtures.ts" + ] + }, + { + "name": "materializeRequesterScopedMcpToolsForHarnessRun", + "files": [ + "src/agents/agent-bundle-mcp-harness.ts", + "src/plugin-sdk/agent-harness-runtime.ts" + ], + "sdk": true + }, + { + "name": "materializeStaticMcpToolsForScheduledHarnessRun", + "files": [ + "src/agents/agent-bundle-mcp-harness.ts", + "src/plugin-sdk/codex-mcp-projection.ts" + ], + "sdk": true + }, + { + "name": "MAX_TIMER_DELAY_MS", + "files": [ + "src/cron/service/timer-execution-timeout.ts", + "src/gateway/probe.ts" + ] + }, + { + "name": "maybeApplyTtsToPayload", + "files": [ + "src/tts/tts-payload.ts", + "src/tts/tts.ts" + ], + "sdk": true + }, + { + "name": "mergeSessionEntry", + "files": [ + "src/config/sessions/types.ts", + "src/infra/state-migrations.session-store.ts" + ] + }, + { + "name": "nodePendingHandlers", + "files": [ + "src/gateway/server-methods/nodes-pending.ts", + "src/gateway/server-methods/nodes.pending.ts" + ] + }, + { + "name": "normalizeAccountId", + "files": [ + "src/routing/account-id.ts", + "src/utils/account-id.ts" + ], + "sdk": true + }, + { + "name": "normalizeChannelId", + "files": [ + "src/channels/plugins/registry.ts", + "src/channels/registry.ts" + ], + "sdk": true + }, + { + "name": "normalizeContainerPath", + "files": [ + "src/agents/sandbox/path-utils.ts", + "src/agents/sandbox/remote-fs-bridge-paths.ts" + ] + }, + { + "name": "normalizeCronRunDiagnostics", + "files": [ + "src/cron/run-diagnostics-normalize.ts", + "src/cron/run-diagnostics.ts" + ] + }, + { + "name": "normalizeSqliteNumber", + "files": [ + "src/config/sessions/session-accessor.sqlite-normalize.ts", + "src/infra/sqlite-number.ts" + ] + }, + { + "name": "normalizeStringRecord", + "files": [ + "src/agents/bundle-mcp-adapter.ts", + "src/plugins/manifest-capability-normalizers.ts" + ] + }, + { + "name": "normalizeToolName", + "files": [ + "src/agents/tool-display-common.ts", + "src/agents/tool-policy-shared.ts", + "src/cron/run-diagnostics-normalize.ts" + ] + }, + { + "name": "notifyGatewayPluginMetadataChanged", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/cli/plugins-update-gateway-signal.ts" + ] + }, + { + "name": "optimizeImageToJpeg", + "files": [ + "src/media/web-media.ts", + "src/plugins/runtime/runtime-web-channel-plugin.ts" + ], + "sdk": true + }, + { + "name": "pad", + "files": [ + "src/agents/bash-tools.shared.ts", + "src/commands/models/list.format.ts" + ] + }, + { + "name": "parseClawHubPluginSpec", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/infra/clawhub-spec.ts" + ] + }, + { + "name": "parseCsvFilter", + "files": [ + "src/image-generation/live-test-helpers.ts", + "src/video-generation/live-test-helpers.ts" + ] + }, + { + "name": "parseFrontmatter", + "files": [ + "src/agents/utils/frontmatter.ts", + "src/hooks/frontmatter.ts", + "src/skills/loading/frontmatter.ts" + ] + }, + { + "name": "parseInlineDirectives", + "files": [ + "src/auto-reply/reply/directive-handling.parse.ts", + "src/utils/directive-tags.ts" + ], + "sdk": true + }, + { + "name": "parseModelRef", + "files": [ + "src/agents/model-selection-normalize.ts", + "src/commands/doctor/shared/codex-route-model-ref.ts" + ], + "sdk": true + }, + { + "name": "parseQaTarget", + "files": [ + "src/plugin-sdk/qa-channel-protocol.ts", + "src/plugin-sdk/qa-channel.ts" + ], + "sdk": true + }, + { + "name": "parseTimeoutMs", + "files": [ + "src/cli/parse-timeout.ts", + "src/commands/gateway-status/helpers.ts" + ] + }, + { + "name": "pathExists", + "files": [ + "src/agents/worktrees/git.ts", + "src/utils.ts" + ], + "sdk": true + }, + { + "name": "pathsEqual", + "files": [ + "src/commands/doctor/shared/missing-configured-plugin-install.records.ts", + "src/plugins/update-config.ts" + ] + }, + { + "name": "peekSessionMcpRuntime", + "files": [ + "src/agents/agent-bundle-mcp-manager-api.ts", + "src/gateway/server-methods/__mocks__/tools-effective.runtime.ts" + ] + }, + { + "name": "persistSessionEntry", + "files": [ + "src/agents/command/attempt-execution.shared.ts", + "src/agents/command/session-helpers.ts", + "src/auto-reply/reply/commands-session-store.ts" + ] + }, + { + "name": "planPluginUninstall", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/uninstall.ts" + ] + }, + { + "name": "preserveConfigSnapshotAsClobbered", + "files": [ + "src/config/io.observe-recovery.ts", + "src/config/io.runtime.ts" + ] + }, + { + "name": "primeConfiguredBindingRegistry", + "files": [ + "src/channels/plugins/binding-registry.ts", + "src/channels/plugins/configured-binding-registry.ts" + ] + }, + { + "name": "promoteConfigSnapshotToLastKnownGood", + "files": [ + "src/config/io.observe-recovery.ts", + "src/config/io.runtime.ts" + ] + }, + { + "name": "promptYesNo", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/cli/prompt.ts" + ] + }, + { + "name": "readAmbientTranscriptWatermark", + "files": [ + "src/config/sessions/ambient-transcript-watermark.ts", + "src/plugin-sdk/session-store-runtime.ts" + ], + "sdk": true + }, + { + "name": "readAskUserQuestionId", + "files": [ + "src/auto-reply/reply/dispatch-from-config.payloads.ts", + "src/infra/question-reaction-runtime.ts" + ] + }, + { + "name": "readConfigFileSnapshot", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/config/io.runtime.ts" + ], + "sdk": true + }, + { + "name": "readConfigFileSnapshotForWrite", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/config/io.runtime.ts" + ], + "sdk": true + }, + { + "name": "readLatestSessionUsageFromTranscriptAsync", + "files": [ + "src/gateway/session-transcript-readers.ts", + "src/gateway/session-utils.fs.ts" + ] + }, + { + "name": "readPersistedInstalledPluginIndex", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/installed-plugin-index-store.ts" + ] + }, + { + "name": "readSessionEntry", + "files": [ + "src/agents/subagent-announce.runtime.ts", + "src/cron/isolated-agent.turn-test-helpers.ts" + ] + }, + { + "name": "readSessionTranscriptVisibleMessageDelta", + "files": [ + "src/config/sessions/session-accessor.sqlite-active-events.ts", + "src/plugin-sdk/session-transcript-runtime.ts" + ], + "sdk": true + }, + { + "name": "readStringParam", + "files": [ + "src/agents/tools/common.ts", + "src/agents/tools/transcripts-tool-runtime.ts" + ], + "sdk": true + }, + { + "name": "recordHookInstall", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/hooks/installs.ts" + ] + }, + { + "name": "recordPluginInstall", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/installs.ts" + ] + }, + { + "name": "recordTaskRunProgressByRunId", + "files": [ + "src/tasks/detached-task-runtime.ts", + "src/tasks/task-executor.ts" + ] + }, + { + "name": "recoverConfigFromLastKnownGood", + "files": [ + "src/config/io.observe-recovery.ts", + "src/config/io.runtime.ts" + ] + }, + { + "name": "redactSecrets", + "files": [ + "src/commands/status-all/format.ts", + "src/logging/redact.ts" + ], + "sdk": true + }, + { + "name": "refreshPluginRegistry", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/plugin-registry-snapshot.ts" + ] + }, + { + "name": "registerPluginsCli", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/cli/plugins-cli.ts" + ] + }, + { + "name": "registerSubCliByName", + "files": [ + "src/cli/program/register.subclis-core.ts", + "src/cli/program/register.subclis.ts" + ] + }, + { + "name": "registerSubCliCommands", + "files": [ + "src/cli/program/register.subclis-core.ts", + "src/cli/program/register.subclis.ts" + ] + }, + { + "name": "replaceConfigFile", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/config/mutate.ts" + ], + "sdk": true + }, + { + "name": "replaceRuntimeAuthProfileStoreSnapshots", + "files": [ + "src/agents/auth-profiles/runtime-snapshots.ts", + "src/agents/auth-profiles/store.ts" + ], + "sdk": true + }, + { + "name": "replaceSubagentRunAfterSteer", + "files": [ + "src/agents/subagent-registry-runtime.ts", + "src/agents/subagent-registry.ts" + ] + }, + { + "name": "reportClawHubPluginInstallTelemetry", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/infra/clawhub.ts" + ] + }, + { + "name": "requestSafeGatewayRestart", + "files": [ + "src/cli/daemon-cli/lifecycle-safe-restart.ts", + "src/infra/restart-coordinator.ts" + ] + }, + { + "name": "requireRecord", + "files": [ + "src/acp/translator.bridge-test-helpers.ts", + "src/gateway/test-helpers.assertions.ts" + ] + }, + { + "name": "requireValidConfig", + "files": [ + "src/commands/agents.command-shared.ts", + "src/commands/channels/shared.ts" + ] + }, + { + "name": "requireValidConfigFileSnapshot", + "files": [ + "src/commands/agents.command-shared.ts", + "src/commands/config-validation.ts" + ] + }, + { + "name": "resetPreparedModelCatalogForTest", + "files": [ + "src/gateway/server-model-catalog.ts", + "src/gateway/server-start.ts", + "src/gateway/server.ts" + ] + }, + { + "name": "resolveAdvertisedLanHost", + "files": [ + "src/infra/advertised-lan-host.ts", + "src/plugin-sdk/gateway-runtime.ts" + ], + "sdk": true + }, + { + "name": "resolveAllowlistModelKey", + "files": [ + "src/agents/model-selection-shared.ts", + "src/agents/model-selection.ts" + ] + }, + { + "name": "resolveApiKeyForProvider", + "files": [ + "src/agents/model-auth-provider.ts", + "src/plugin-sdk/provider-auth-runtime.ts" + ], + "sdk": true + }, + { + "name": "resolveConcreteSessionStorePath", + "files": [ + "src/config/sessions/session-accessor.transcript-target.ts", + "src/gateway/session-utils-store.ts" + ] + }, + { + "name": "resolveConfiguredBinding", + "files": [ + "src/channels/plugins/binding-registry.ts", + "src/channels/plugins/configured-binding-registry.ts" + ] + }, + { + "name": "resolveConfiguredBindingRecord", + "files": [ + "src/channels/plugins/binding-registry.ts", + "src/channels/plugins/configured-binding-registry.ts" + ] + }, + { + "name": "resolveConfiguredBindingRecordBySessionKey", + "files": [ + "src/channels/plugins/binding-registry.ts", + "src/channels/plugins/configured-binding-registry.ts" + ] + }, + { + "name": "resolveConfiguredFallbackModel", + "files": [ + "src/agents/embedded-agent-runner/model.configured-fallback.ts", + "src/auto-reply/reply/agent-runner-core.ts" + ] + }, + { + "name": "resolveDirectStatusReplyForSession", + "files": [ + "src/plugin-sdk/command-status-runtime.ts", + "src/plugin-sdk/command-status.runtime.ts" + ], + "sdk": true + }, + { + "name": "resolveEffectiveOAuthCredential", + "files": [ + "src/agents/auth-profiles/effective-oauth.ts", + "src/agents/auth-profiles/oauth-manager.ts" + ] + }, + { + "name": "resolveEffectivePluginActivationState", + "files": [ + "src/plugins/config-policy.ts", + "src/plugins/config-state.ts" + ] + }, + { + "name": "resolveEffectiveToolInventory", + "files": [ + "src/agents/tools-effective-inventory.ts", + "src/gateway/server-methods/__mocks__/tools-effective.runtime.ts" + ] + }, + { + "name": "resolveEffectiveToolInventoryRuntimeModelContextAsync", + "files": [ + "src/agents/tools-effective-inventory.ts", + "src/gateway/server-methods/__mocks__/tools-effective.runtime.ts" + ] + }, + { + "name": "resolveGatewayServiceProbeHosts", + "files": [ + "src/daemon/gateway-service-probe-hosts.ts", + "src/daemon/test-helpers/schtasks-fixtures.ts" + ] + }, + { + "name": "resolveHeartbeatPrompt", + "files": [ + "src/auto-reply/heartbeat.ts", + "src/infra/heartbeat-runner-config.ts" + ], + "sdk": true + }, + { + "name": "resolveHomeDir", + "files": [ + "src/daemon/paths.ts", + "src/utils.ts" + ], + "sdk": true + }, + { + "name": "resolveMarketplaceInstallShortcut", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/marketplace.ts" + ] + }, + { + "name": "resolveMemorySlotDecision", + "files": [ + "src/plugins/config-policy.ts", + "src/plugins/config-state.ts" + ] + }, + { + "name": "resolveMissingPluginCommandMessage", + "files": [ + "src/cli/run-main-policy.ts", + "src/cli/run-main.ts" + ] + }, + { + "name": "resolveNode", + "files": [ + "src/agents/tools/nodes-utils.ts", + "src/cli/nodes-cli/rpc.ts" + ] + }, + { + "name": "resolveNodeClaudePlacement", + "files": [ + "src/agents/cli-runner/execute-node-claude.ts", + "src/agents/cli-runner/prepare-claude.ts" + ] + }, + { + "name": "resolveNodeId", + "files": [ + "src/agents/tools/nodes-utils.ts", + "src/cli/nodes-cli/rpc.ts" + ] + }, + { + "name": "resolveOpenClawMetadata", + "files": [ + "src/hooks/frontmatter.ts", + "src/skills/loading/frontmatter.ts" + ] + }, + { + "name": "resolveProviderThinkingProfile", + "files": [ + "src/plugins/provider-runtime.ts", + "src/plugins/provider-thinking.ts" + ] + }, + { + "name": "resolveProviderUsageDisplayName", + "files": [ + "src/infra/provider-usage.admin.ts", + "src/infra/provider-usage.shared.ts" + ], + "sdk": true + }, + { + "name": "resolveQueueSettings", + "files": [ + "src/auto-reply/reply/queue/settings-runtime.ts", + "src/auto-reply/reply/queue/settings.ts" + ] + }, + { + "name": "resolveSecretInputString", + "files": [ + "src/config/types.secrets.ts", + "src/secrets/resolve-secret-input-string.ts" + ], + "sdk": true + }, + { + "name": "resolveSecretPlanTargetByPath", + "files": [ + "src/plugin-sdk/secret-ref-runtime.ts", + "src/secrets/target-registry-query.ts" + ], + "sdk": true + }, + { + "name": "resolveSessionCatalogCreateTarget", + "files": [ + "src/gateway/server-methods/session-catalog.ts", + "src/plugins/runtime/runtime-agent-session-catalog.ts" + ] + }, + { + "name": "resolveSessionFilePath", + "files": [ + "src/config/sessions/paths.ts", + "src/plugin-sdk/session-store-runtime.ts" + ], + "sdk": true + }, + { + "name": "resolveSessionKey", + "files": [ + "src/acp/session-mapper.ts", + "src/config/sessions/session-key.ts" + ], + "sdk": true + }, + { + "name": "resolveSessionMcpConfigSummary", + "files": [ + "src/agents/agent-bundle-mcp-runtime-config.ts", + "src/gateway/server-methods/__mocks__/tools-effective.runtime.ts" + ] + }, + { + "name": "resolveStorePath", + "files": [ + "src/config/sessions/paths.ts", + "src/plugin-sdk/session-store-runtime.ts" + ], + "sdk": true + }, + { + "name": "resolveThinkingDefaultForModel", + "files": [ + "src/auto-reply/thinking.shared.ts", + "src/auto-reply/thinking.ts" + ] + }, + { + "name": "restorePersistedInstalledPluginIndexIfCurrent", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/installed-plugin-index-store.ts" + ] + }, + { + "name": "runChannelInboundEvent", + "files": [ + "src/channels/message/inbound-reply-dispatch.ts", + "src/channels/turn/kernel.ts" + ], + "sdk": true + }, + { + "name": "runExec", + "files": [ + "src/agents/code-mode-execution.ts", + "src/process/exec.ts" + ], + "sdk": true + }, + { + "name": "runGitUpdate", + "files": [ + "src/cli/update-cli/update-command-git.ts", + "src/infra/update-runner-git.ts" + ] + }, + { + "name": "runModelsAuthLoginFlow", + "files": [ + "src/commands/models/auth.ts", + "src/plugin-sdk/provider-auth-login-flow-runtime.ts" + ], + "sdk": true + }, + { + "name": "runPreparedInboundReply", + "files": [ + "src/channels/message/inbound-reply-dispatch.ts", + "src/channels/turn/execution.ts" + ], + "sdk": true + }, + { + "name": "runSystemAgentWithInference", + "files": [ + "src/cli/program.test-mocks.ts", + "src/commands/system-agent-with-inference.ts" + ] + }, + { + "name": "runtimeLogs", + "files": [ + "src/cli/daemon-cli/test-helpers/lifecycle-core-harness.ts", + "src/cli/plugins-cli-test-helpers.ts" + ] + }, + { + "name": "runTui", + "files": [ + "src/cli/program.test-mocks.ts", + "src/tui/tui.ts" + ] + }, + { + "name": "safeParseJson", + "files": [ + "src/gateway/server-json.ts", + "src/utils.ts" + ], + "sdk": true + }, + { + "name": "saveJsonFile", + "files": [ + "src/infra/json-file.ts", + "src/plugin-sdk/json-store.ts" + ], + "sdk": true + }, + { + "name": "sendDurableMessageBatch", + "files": [ + "src/channels/message/send.ts", + "src/plugin-sdk/channel-outbound.ts" + ], + "sdk": true + }, + { + "name": "setDetachedTaskDeliveryStatusByRunId", + "files": [ + "src/tasks/detached-task-runtime.ts", + "src/tasks/task-executor.ts" + ] + }, + { + "name": "setupCommand", + "files": [ + "src/cli/program.test-mocks.ts", + "src/commands/setup.ts" + ] + }, + { + "name": "setupWizardCommand", + "files": [ + "src/cli/program.test-mocks.ts", + "src/commands/onboard.ts" + ] + }, + { + "name": "sha256HexPrefix", + "files": [ + "src/infra/crypto-digest.ts", + "src/logging/redact-identifier.ts" + ], + "sdk": true + }, + { + "name": "sleep", + "files": [ + "src/agents/utils/sleep.ts", + "src/tui/tui-pty-test-support.ts", + "src/utils/sleep.ts" + ], + "sdk": true + }, + { + "name": "startGatewayServer", + "files": [ + "src/gateway/server-start.ts", + "src/gateway/server.ts", + "src/gateway/test-helpers.server.ts" + ] + }, + { + "name": "startTaskRunByRunId", + "files": [ + "src/tasks/detached-task-runtime.ts", + "src/tasks/task-executor.ts" + ] + }, + { + "name": "stopWithText", + "files": [ + "src/auto-reply/reply/commands-acp/shared.ts", + "src/auto-reply/reply/commands-subagents/shared.ts" + ] + }, + { + "name": "stripTargetKindPrefix", + "files": [ + "src/infra/outbound/channel-target-prefix.ts", + "src/plugin-sdk/core.ts" + ], + "sdk": true + }, + { + "name": "textToSpeech", + "files": [ + "src/tts/tts-synthesis.ts", + "src/tts/tts.ts" + ], + "sdk": true + }, + { + "name": "theme", + "files": [ + "src/agents/modes/interactive/theme/theme.ts", + "src/tui/theme/theme.ts" + ], + "sdk": true + }, + { + "name": "toDotPath", + "files": [ + "src/cli/config-cli-path.ts", + "src/secrets/shared.ts" + ] + }, + { + "name": "toError", + "files": [ + "src/agents/prepared-model-runtime.owner.ts", + "src/worker/worker-connection-contract.ts" + ] + }, + { + "name": "toPosixPath", + "files": [ + "src/daemon/output.ts", + "src/shared/ignore-rules.ts" + ] + }, + { + "name": "triggerInternalHook", + "files": [ + "src/agents/embedded-agent-runner/compact.hooks.harness.ts", + "src/hooks/internal-hooks.ts" + ], + "sdk": true + }, + { + "name": "truncateText", + "files": [ + "src/agents/harness/native-hook-relay-utils.ts", + "src/agents/tools/web-fetch-utils.ts" + ], + "sdk": true + }, + { + "name": "tryDispatchAcpReply", + "files": [ + "src/auto-reply/reply/dispatch-acp.runtime.ts", + "src/auto-reply/reply/dispatch-acp.ts" + ] + }, + { + "name": "updateNpmInstalledHookPacks", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/hooks/update.ts" + ] + }, + { + "name": "updateNpmInstalledPlugins", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/update-installed.ts" + ] + }, + { + "name": "VERSION", + "files": [ + "src/agents/config.ts", + "src/version.ts" + ], + "sdk": true + }, + { + "name": "withDurableMessageSendContext", + "files": [ + "src/channels/message/send.ts", + "src/plugin-sdk/channel-outbound.ts" + ], + "sdk": true + }, + { + "name": "withServer", + "files": [ + "src/gateway/test-with-server.ts", + "src/plugin-sdk/test-helpers/http-test-server.ts" + ], + "sdk": true + }, + { + "name": "withTempDir", + "files": [ + "src/infra/install-source-utils.ts", + "src/test-helpers/temp-dir.ts", + "src/test-utils/temp-dir.ts" + ], + "sdk": true + }, + { + "name": "withTempHome", + "files": [ + "src/config/test-helpers.ts", + "src/plugin-sdk/test-helpers/temp-home.ts" + ], + "sdk": true + }, + { + "name": "withTimeout", + "files": [ + "src/infra/provider-usage.shared.ts", + "src/node-host/with-timeout.ts" + ], + "sdk": true + }, + { + "name": "withTrustedEnvProxyGuardedFetchMode", + "files": [ + "src/infra/net/fetch-guard.ts", + "src/plugin-sdk/fetch-runtime.ts" + ], + "sdk": true + }, + { + "name": "writeConfigFile", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/config/io.runtime.ts" + ], + "sdk": true + }, + { + "name": "writePersistedInstalledPluginIndexInstallRecordsWithLease", + "files": [ + "src/cli/plugins-cli-test-helpers.ts", + "src/plugins/installed-plugin-index-records.ts" + ] + }, + { + "name": "writeSkill", + "files": [ + "src/skills/test-support/e2e-test-helpers.ts", + "src/skills/test-support/test-helpers.ts" + ], + "sdk": true + } +] diff --git a/scripts/lib/ts-guard-utils.mts b/scripts/lib/ts-guard-utils.mts index f676e4358ea7..519d56f68e11 100644 --- a/scripts/lib/ts-guard-utils.mts +++ b/scripts/lib/ts-guard-utils.mts @@ -40,7 +40,7 @@ export function resolveSourceRoots(repoRoot: string, relativeRoots: string[]) { return relativeRoots.map((root) => path.join(repoRoot, ...root.split("/").filter(Boolean))); } -function isTestLikeTypeScriptFile(filePath: string, extraTestSuffixes: string[] = []) { +export function isTestLikeTypeScriptFile(filePath: string, extraTestSuffixes: string[] = []) { return [...baseTestSuffixes, ...extraTestSuffixes].some((suffix) => filePath.endsWith(suffix)); } diff --git a/test/scripts/check-export-name-collisions.test.ts b/test/scripts/check-export-name-collisions.test.ts new file mode 100644 index 000000000000..2aae8248883d --- /dev/null +++ b/test/scripts/check-export-name-collisions.test.ts @@ -0,0 +1,237 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { + collectModuleExportNames, + collectRepositoryCollisions, + compareExportNameCollisionDebt, + 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("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", + 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"] } }], + }); + }); +}); diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index d99e201819df..b8d7fcd9cbc5 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -3509,6 +3509,11 @@ NODE group: "session-accessor-boundary", runner: "blacksmith-4vcpu-ubuntu-2404", }); + expect(workflow.jobs["check-additional-shard"].strategy.matrix.include).toContainEqual({ + check_name: "check-export-name-collisions", + group: "export-name-collisions", + runner: "blacksmith-4vcpu-ubuntu-2404", + }); expect(workflow.jobs["check-additional-shard"].strategy.matrix.include).toContainEqual({ check_name: "check-sqlite-session-schema-baseline", group: "sqlite-session-schema-baseline", @@ -3798,6 +3803,25 @@ NODE ); }); + it("runs the export name collision ratchet as a visible additional check", () => { + const workflow = readCiWorkflow(); + const additionalJob = workflow.jobs["check-additional-shard"]; + const matrixRows = additionalJob.strategy.matrix.include; + expect(matrixRows).toContainEqual({ + check_name: "check-export-name-collisions", + group: "export-name-collisions", + runner: "blacksmith-4vcpu-ubuntu-2404", + }); + + const runStep = additionalJob.steps.find( + (step: WorkflowStep) => step.name === "Run additional check shard", + ); + expect(runStep.run).toContain("export-name-collisions)"); + expect(runStep.run).toContain( + 'run_check "lint:tmp:export-name-collisions" pnpm run lint:tmp:export-name-collisions', + ); + }); + it("runs the transcript reader ratchet as a visible additional check", () => { const workflow = readCiWorkflow(); const additionalJob = workflow.jobs["check-additional-shard"];