mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-19 12:11:37 +00:00
fix(plugin-sdk): cover the full public API baseline
This commit is contained in:
@@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- **Plugin SDK API baseline:** cover every public entrypoint, preserve complete declaration shapes without source-line churn, and run baseline and export-surface guards from changed-file validation.
|
||||
- **SQLite terminal session recovery:** track physical transcript mutation time in the agent database so killed or timed-out main sessions rotate when transcript writes outlive the registry update, while preserving legacy transcript mtimes during doctor import.
|
||||
- **Gateway chat typecheck:** import chat event types from their owning protocol schema after the retired aggregate type module was removed, restoring full project typechecks.
|
||||
- **Packaged Crabbox commands:** include the lease-freshness helper imported by the published wrapper so `crabbox:*` commands do not fail with `ERR_MODULE_NOT_FOUND` in npm installs.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
0597218863ded2b9e86ab56773c5d92b9bd6dcb5e5d537e8e83397fcbb56a5d5 plugin-sdk-api-baseline.json
|
||||
6d7406435ac48216be84c10d216703c0459577762899d20c5921993e9f1e3478 plugin-sdk-api-baseline.jsonl
|
||||
af04c051b9697ffc27468c0948aa51c122366e7b97b93cf66f19b1b87f846caf plugin-sdk-api-baseline.json
|
||||
7dbae052b16721a5af5c011d1add527af028cc8f32dc84f2dfc4534004e22d69 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -40,6 +40,8 @@ export function shouldRunPromptSnapshotCheck(paths: string[]): boolean;
|
||||
export function shouldRunPromptSnapshotOwnerTest(paths: string[]): boolean;
|
||||
export function shouldRunRuntimeSidecarBaselineCheck(paths: string[]): boolean;
|
||||
export function shouldRunSqliteSessionSchemaBaselineCheck(paths: string[]): boolean;
|
||||
export function shouldRunPluginSdkApiBaselineCheck(paths: string[]): boolean;
|
||||
export function shouldRunPluginSdkSurfaceChecks(paths: string[]): boolean;
|
||||
export function shouldRunCanvasA2uiNativeResourceCheck(paths: string[]): boolean;
|
||||
export function shouldRunAppcastOwnerTest(paths: string[]): boolean;
|
||||
export function shouldRunTestTempCreationReport(paths: string[]): boolean;
|
||||
|
||||
@@ -39,6 +39,10 @@ const RUNTIME_SIDECAR_BASELINE_PATH_RE =
|
||||
/^(?:scripts\/generate-runtime-sidecar-paths-baseline\.ts|scripts\/lib\/bundled-runtime-sidecar-paths\.json|src\/plugins\/runtime-sidecar-paths(?:-baseline)?\.ts)$/u;
|
||||
const SQLITE_SESSION_SCHEMA_BASELINE_PATH_RE =
|
||||
/^(?:src\/state\/openclaw-agent-schema\.sql|scripts\/(?:generate-sqlite-session-schema-baseline\.ts|lib\/sqlite-session-schema-baseline\.ts)|test\/scripts\/sqlite-session-schema-baseline\.test\.ts|docs\/\.generated\/sqlite-session-transcript-schema-baseline\.sha256)$/u;
|
||||
const PLUGIN_SDK_API_BASELINE_PATH_RE =
|
||||
/^(?:src\/|packages\/|extensions\/|pnpm-lock\.yaml$|tsconfig\.json$|scripts\/(?:generate-plugin-sdk-api-baseline\.ts|lib\/plugin-sdk-(?:doc-metadata\.ts|entries\.mjs|entrypoints\.json|private-local-only-subpaths\.json))|docs\/\.generated\/plugin-sdk-api-baseline\.sha256$)/u;
|
||||
const PLUGIN_SDK_SURFACE_PATH_RE =
|
||||
/^(?:package\.json$|src\/plugin-sdk\/|scripts\/(?:plugin-sdk-surface-report\.mjs|sync-plugin-sdk-exports\.mjs|lib\/plugin-sdk-(?:declaration-budget\.mjs|deprecated-barrel-subpaths\.json|deprecated-public-subpaths\.json|entries\.mjs|entrypoints\.json|private-local-only-subpaths\.json)))/u;
|
||||
const CANVAS_A2UI_NATIVE_RESOURCE_PATH_RE =
|
||||
/^(?:pnpm-lock\.yaml$|apps\/shared\/OpenClawKit\/Sources\/OpenClawKit\/Resources\/CanvasA2UI\/|extensions\/canvas\/(?:package\.json$|scripts\/bundle-a2ui\.mjs$|src\/host\/a2ui(?:\/(?:index\.html|a2ui\.bundle\.js|\.bundle\.hash)$|-app\/))|scripts\/(?:bundle-a2ui|sync-native-a2ui)\.mjs$)/u;
|
||||
const CORE_OXLINT_TS_CONFIG = "config/tsconfig/oxlint.core.json";
|
||||
@@ -208,6 +212,24 @@ export function shouldRunSqliteSessionSchemaBaselineCheck(paths) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Returns whether changed files can alter the published Plugin SDK API contract. */
|
||||
export function shouldRunPluginSdkApiBaselineCheck(paths) {
|
||||
return paths.some((changedPath) => {
|
||||
const normalizedPath = normalizeChangedPath(changedPath);
|
||||
return (
|
||||
!getChangedPathFacts(normalizedPath).isTestOnly &&
|
||||
PLUGIN_SDK_API_BASELINE_PATH_RE.test(normalizedPath)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** Returns whether changed files can alter Plugin SDK exports or surface budgets. */
|
||||
export function shouldRunPluginSdkSurfaceChecks(paths) {
|
||||
return paths.some((changedPath) =>
|
||||
PLUGIN_SDK_SURFACE_PATH_RE.test(normalizeChangedPath(changedPath)),
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldRunCanvasA2uiNativeResourceCheck(paths) {
|
||||
return paths.some((changedPath) =>
|
||||
CANVAS_A2UI_NATIVE_RESOURCE_PATH_RE.test(normalizeChangedPath(changedPath)),
|
||||
@@ -335,6 +357,13 @@ export function createChangedCheckPlan(result, options = {}) {
|
||||
if (shouldRunSqliteSessionSchemaBaselineCheck(result.paths)) {
|
||||
add("SQLite sessions/transcripts schema baseline", ["sqlite:sessions-schema:check"]);
|
||||
}
|
||||
if (shouldRunPluginSdkApiBaselineCheck(result.paths)) {
|
||||
add("Plugin SDK API baseline", ["plugin-sdk:api:check"]);
|
||||
}
|
||||
if (!result.lanes.releaseMetadata && shouldRunPluginSdkSurfaceChecks(result.paths)) {
|
||||
add("Plugin SDK package exports", ["plugin-sdk:check-exports"]);
|
||||
add("Plugin SDK surface budget", ["plugin-sdk:surface:check"]);
|
||||
}
|
||||
if (shouldRunCanvasA2uiNativeResourceCheck(result.paths)) {
|
||||
addCommand(
|
||||
"Canvas A2UI native resource sync",
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
*/
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { publicPluginSdkEntrypoints } from "../../scripts/lib/plugin-sdk-entries.mjs";
|
||||
import {
|
||||
listPluginSdkApiBaselineEntrypoints,
|
||||
normalizePluginSdkApiDeclarationText,
|
||||
normalizePluginSdkApiSourcePath,
|
||||
renderPluginSdkApiBaseline,
|
||||
} from "./api-baseline.js";
|
||||
|
||||
describe("Plugin SDK API baseline", () => {
|
||||
@@ -60,4 +63,59 @@ describe("Plugin SDK API baseline", () => {
|
||||
|
||||
expect(normalizePluginSdkApiSourcePath(repoRoot, sourcePath)).toBe("src/plugin-sdk/core.ts");
|
||||
});
|
||||
|
||||
it("renders complete declarations for the canonical public entrypoint inventory", async () => {
|
||||
expect(listPluginSdkApiBaselineEntrypoints()).toEqual(publicPluginSdkEntrypoints);
|
||||
|
||||
const rendered = await renderPluginSdkApiBaseline({
|
||||
entrypoints: [
|
||||
"agent-harness-runtime",
|
||||
"approval-gateway-runtime",
|
||||
"infra-runtime",
|
||||
"provider-catalog-live-runtime",
|
||||
"provider-oauth-runtime",
|
||||
"provider-selection-runtime",
|
||||
"provider-web-search-config-contract",
|
||||
"realtime-voice",
|
||||
"sqlite-runtime-testing",
|
||||
],
|
||||
});
|
||||
const findDeclaration = (exportName: string) =>
|
||||
rendered.baseline.modules
|
||||
.flatMap((moduleSurface) => moduleSurface.exports)
|
||||
.find((exportSurface) => exportSurface.exportName === exportName)?.declaration;
|
||||
|
||||
expect(rendered.baseline.modules.find((entry) => entry.entrypoint === "infra-runtime")).toEqual(
|
||||
expect.objectContaining({
|
||||
category: null,
|
||||
importSpecifier: "openclaw/plugin-sdk/infra-runtime",
|
||||
}),
|
||||
);
|
||||
expect(findDeclaration("OAuthProviderInterface")).toContain("readonly id: OAuthProviderId;");
|
||||
expect(findDeclaration("OAuthProviderInterface")).toContain(
|
||||
"login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials>;",
|
||||
);
|
||||
expect(findDeclaration("LiveModelCatalogHttpError")).toContain("readonly status: number;");
|
||||
expect(findDeclaration("LiveModelCatalogHttpError")).toContain(
|
||||
"constructor(providerId: string, status: number);",
|
||||
);
|
||||
expect(findDeclaration("LiveModelCatalogHttpError")).not.toContain("super(");
|
||||
expect(findDeclaration("ApprovalResolveResult")).not.toContain("see source");
|
||||
expect(findDeclaration("RealtimeVoiceAgentConsultRuntime")).not.toContain("see source");
|
||||
expect(findDeclaration("createWebSearchProviderContractFields")).toContain(
|
||||
"export function createWebSearchProviderContractFields(",
|
||||
);
|
||||
expect(findDeclaration("createWebSearchProviderContractFields")).not.toContain(
|
||||
"createBaseWebSearchProviderContractFields",
|
||||
);
|
||||
expect(findDeclaration("OPENCLAW_VERSION")).toContain("export const OPENCLAW_VERSION:");
|
||||
expect(findDeclaration("SqliteTrajectoryRuntimeEventForTest")).toContain(
|
||||
"export type SqliteTrajectoryRuntimeEventForTest =",
|
||||
);
|
||||
expect(findDeclaration("ProviderSelection")).toContain(
|
||||
"export type ProviderSelection<TProvider> =",
|
||||
);
|
||||
expect(rendered.json).not.toContain('"line":');
|
||||
expect(rendered.jsonl).not.toContain('"sourceLine":');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,11 +3,9 @@ import { createHash } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import ts from "typescript";
|
||||
import {
|
||||
pluginSdkDocMetadata,
|
||||
resolvePluginSdkDocImportSpecifier,
|
||||
type PluginSdkDocCategory,
|
||||
type PluginSdkDocEntrypoint,
|
||||
} from "../../scripts/lib/plugin-sdk-doc-metadata.ts";
|
||||
@@ -27,8 +25,6 @@ export type PluginSdkApiExportKind =
|
||||
|
||||
/** Repo source location for a public SDK declaration or module. */
|
||||
export type PluginSdkApiSourceLink = {
|
||||
/** One-based source line for docs and review links. */
|
||||
line: number;
|
||||
/** Repo-relative source file path. */
|
||||
path: string;
|
||||
};
|
||||
@@ -47,10 +43,10 @@ export type PluginSdkApiExport = {
|
||||
|
||||
/** API baseline record for one public SDK module/subpath. */
|
||||
export type PluginSdkApiModule = {
|
||||
/** Documentation category used to group SDK entrypoints. */
|
||||
category: PluginSdkDocCategory;
|
||||
/** Entry point metadata from the SDK docs registry. */
|
||||
entrypoint: PluginSdkDocEntrypoint;
|
||||
/** Documentation category used to group SDK entrypoints when documented. */
|
||||
category: PluginSdkDocCategory | null;
|
||||
/** Canonical public SDK entrypoint. */
|
||||
entrypoint: string;
|
||||
/** Public exports discovered from the TypeScript program. */
|
||||
exports: PluginSdkApiExport[];
|
||||
/** Package specifier shown to plugin authors. */
|
||||
@@ -160,7 +156,7 @@ export function normalizePluginSdkApiDeclarationText(repoRoot: string, value: st
|
||||
);
|
||||
}
|
||||
|
||||
function createCompilerContext(repoRoot: string) {
|
||||
function createCompilerContext(repoRoot: string, entrypoints: readonly string[]) {
|
||||
const configPath = ts.findConfigFile(
|
||||
repoRoot,
|
||||
(filePath) => ts.sys.fileExists(filePath),
|
||||
@@ -172,31 +168,32 @@ function createCompilerContext(repoRoot: string) {
|
||||
throw new Error(ts.flattenDiagnosticMessageText(configFile.error.messageText, "\n"));
|
||||
}
|
||||
const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, repoRoot);
|
||||
const fileNames = parsedConfig.fileNames.toSorted((left, right) =>
|
||||
compareText(
|
||||
relativePath(repoRoot, path.resolve(left)),
|
||||
relativePath(repoRoot, path.resolve(right)),
|
||||
),
|
||||
);
|
||||
const fileNames = entrypoints
|
||||
.map((entrypoint) => path.join(repoRoot, "src", "plugin-sdk", `${entrypoint}.ts`))
|
||||
.toSorted((left, right) =>
|
||||
compareText(
|
||||
relativePath(repoRoot, path.resolve(left)),
|
||||
relativePath(repoRoot, path.resolve(right)),
|
||||
),
|
||||
);
|
||||
const program = ts.createProgram(fileNames, parsedConfig.options);
|
||||
return {
|
||||
checker: program.getTypeChecker(),
|
||||
printer: ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }),
|
||||
printer: ts.createPrinter({
|
||||
newLine: ts.NewLineKind.LineFeed,
|
||||
removeComments: true,
|
||||
}),
|
||||
program,
|
||||
};
|
||||
}
|
||||
|
||||
function buildSourceLink(
|
||||
repoRoot: string,
|
||||
program: ts.Program,
|
||||
filePath: string,
|
||||
start: number,
|
||||
): PluginSdkApiSourceLink {
|
||||
const sourceFile = program.getSourceFile(filePath);
|
||||
assert(sourceFile, `Unable to read source file for ${relativePath(repoRoot, filePath)}`);
|
||||
const line = sourceFile.getLineAndCharacterOfPosition(start).line + 1;
|
||||
/** List canonical public SDK entrypoints included in the API baseline. */
|
||||
export function listPluginSdkApiBaselineEntrypoints(): string[] {
|
||||
return [...publicPluginSdkEntrypoints];
|
||||
}
|
||||
|
||||
function buildSourceLink(repoRoot: string, filePath: string): PluginSdkApiSourceLink {
|
||||
return {
|
||||
line,
|
||||
path: relativePath(repoRoot, filePath),
|
||||
};
|
||||
}
|
||||
@@ -278,30 +275,219 @@ function resolveSymbolAndDeclaration(
|
||||
return { declaration, resolvedSymbol };
|
||||
}
|
||||
|
||||
const DECLARATION_TYPE_FORMAT_FLAGS =
|
||||
ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.MultilineObjectLiterals;
|
||||
const DECLARATION_NODE_BUILDER_FLAGS = ts.NodeBuilderFlags.NoTruncation;
|
||||
|
||||
function declarationModifiers(node: ts.Node): readonly ts.Modifier[] | undefined {
|
||||
return ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
|
||||
}
|
||||
|
||||
function inferDeclarationTypeNode(
|
||||
checker: ts.TypeChecker,
|
||||
declaration: ts.Declaration,
|
||||
explicitType: ts.TypeNode | undefined,
|
||||
): ts.TypeNode | undefined {
|
||||
return (
|
||||
explicitType ??
|
||||
checker.typeToTypeNode(
|
||||
checker.getTypeAtLocation(declaration),
|
||||
declaration,
|
||||
DECLARATION_NODE_BUILDER_FLAGS,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function inferDeclarationReturnTypeNode(
|
||||
checker: ts.TypeChecker,
|
||||
declaration: ts.SignatureDeclaration,
|
||||
explicitType: ts.TypeNode | undefined,
|
||||
): ts.TypeNode | undefined {
|
||||
if (explicitType) {
|
||||
return explicitType;
|
||||
}
|
||||
const signature = checker.getSignatureFromDeclaration(declaration);
|
||||
return signature
|
||||
? checker.typeToTypeNode(
|
||||
checker.getReturnTypeOfSignature(signature),
|
||||
declaration,
|
||||
DECLARATION_NODE_BUILDER_FLAGS,
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function stripParameterInitializer(parameter: ts.ParameterDeclaration): ts.ParameterDeclaration {
|
||||
return ts.factory.updateParameterDeclaration(
|
||||
parameter,
|
||||
declarationModifiers(parameter),
|
||||
parameter.dotDotDotToken,
|
||||
parameter.name,
|
||||
parameter.questionToken,
|
||||
parameter.type,
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
|
||||
function stripClassMemberImplementation(
|
||||
checker: ts.TypeChecker,
|
||||
member: ts.ClassElement,
|
||||
): ts.ClassElement | null {
|
||||
if (ts.isClassStaticBlockDeclaration(member)) {
|
||||
return null;
|
||||
}
|
||||
if (ts.isConstructorDeclaration(member)) {
|
||||
return ts.factory.updateConstructorDeclaration(
|
||||
member,
|
||||
declarationModifiers(member),
|
||||
member.parameters.map(stripParameterInitializer),
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
if (ts.isMethodDeclaration(member)) {
|
||||
return ts.factory.updateMethodDeclaration(
|
||||
member,
|
||||
declarationModifiers(member),
|
||||
member.asteriskToken,
|
||||
member.name,
|
||||
member.questionToken,
|
||||
member.typeParameters,
|
||||
member.parameters.map(stripParameterInitializer),
|
||||
inferDeclarationReturnTypeNode(checker, member, member.type),
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
if (ts.isGetAccessorDeclaration(member)) {
|
||||
return ts.factory.updateGetAccessorDeclaration(
|
||||
member,
|
||||
declarationModifiers(member),
|
||||
member.name,
|
||||
member.parameters.map(stripParameterInitializer),
|
||||
inferDeclarationReturnTypeNode(checker, member, member.type),
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
if (ts.isSetAccessorDeclaration(member)) {
|
||||
return ts.factory.updateSetAccessorDeclaration(
|
||||
member,
|
||||
declarationModifiers(member),
|
||||
member.name,
|
||||
member.parameters.map(stripParameterInitializer),
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
if (ts.isPropertyDeclaration(member)) {
|
||||
return ts.factory.updatePropertyDeclaration(
|
||||
member,
|
||||
declarationModifiers(member),
|
||||
member.name,
|
||||
member.questionToken ?? member.exclamationToken,
|
||||
inferDeclarationTypeNode(checker, member, member.type),
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
return member;
|
||||
}
|
||||
|
||||
function stripClassImplementation(
|
||||
checker: ts.TypeChecker,
|
||||
declaration: ts.ClassDeclaration,
|
||||
exportName: string,
|
||||
): ts.ClassDeclaration {
|
||||
const members = declaration.members.flatMap((member) => {
|
||||
const stripped = stripClassMemberImplementation(checker, member);
|
||||
return stripped ? [stripped] : [];
|
||||
});
|
||||
return ts.factory.updateClassDeclaration(
|
||||
declaration,
|
||||
declarationModifiers(declaration),
|
||||
ts.factory.createIdentifier(exportName),
|
||||
declaration.typeParameters,
|
||||
declaration.heritageClauses,
|
||||
members,
|
||||
);
|
||||
}
|
||||
|
||||
function renameStructuredDeclarationForExport(
|
||||
checker: ts.TypeChecker,
|
||||
declaration: ts.Declaration,
|
||||
exportName: string,
|
||||
): ts.Declaration {
|
||||
const name = ts.factory.createIdentifier(exportName);
|
||||
if (ts.isClassDeclaration(declaration)) {
|
||||
return stripClassImplementation(checker, declaration, exportName);
|
||||
}
|
||||
if (ts.isInterfaceDeclaration(declaration)) {
|
||||
return ts.factory.updateInterfaceDeclaration(
|
||||
declaration,
|
||||
declarationModifiers(declaration),
|
||||
name,
|
||||
declaration.typeParameters,
|
||||
declaration.heritageClauses,
|
||||
declaration.members,
|
||||
);
|
||||
}
|
||||
if (ts.isEnumDeclaration(declaration)) {
|
||||
return ts.factory.updateEnumDeclaration(
|
||||
declaration,
|
||||
declarationModifiers(declaration),
|
||||
name,
|
||||
declaration.members,
|
||||
);
|
||||
}
|
||||
if (ts.isModuleDeclaration(declaration) && ts.isIdentifier(declaration.name)) {
|
||||
return ts.factory.updateModuleDeclaration(
|
||||
declaration,
|
||||
declarationModifiers(declaration),
|
||||
name,
|
||||
declaration.body,
|
||||
);
|
||||
}
|
||||
return declaration;
|
||||
}
|
||||
|
||||
function ensureExportedDeclarationText(value: string): string {
|
||||
return /^export\b/u.test(value) ? value : `export ${value}`;
|
||||
}
|
||||
|
||||
function printTypeParameters(printer: ts.Printer, declaration: ts.TypeAliasDeclaration): string {
|
||||
if (!declaration.typeParameters?.length) {
|
||||
return "";
|
||||
}
|
||||
const sourceFile = declaration.getSourceFile();
|
||||
const parameters = declaration.typeParameters.map((typeParameter) =>
|
||||
printer.printNode(ts.EmitHint.Unspecified, typeParameter, sourceFile).trim(),
|
||||
);
|
||||
return `<${parameters.join(", ")}>`;
|
||||
}
|
||||
|
||||
function printNode(
|
||||
repoRoot: string,
|
||||
checker: ts.TypeChecker,
|
||||
printer: ts.Printer,
|
||||
declaration: ts.Declaration,
|
||||
exportName: string,
|
||||
): string | null {
|
||||
if (ts.isFunctionDeclaration(declaration)) {
|
||||
const signatures = checker.getTypeAtLocation(declaration).getCallSignatures();
|
||||
if (signatures.length === 0) {
|
||||
return `export function ${declaration.name?.text ?? "anonymous"}();`;
|
||||
return `export function ${exportName}();`;
|
||||
}
|
||||
return normalizePluginSdkApiDeclarationText(
|
||||
repoRoot,
|
||||
signatures
|
||||
.map(
|
||||
(signature) =>
|
||||
`export function ${declaration.name?.text ?? "anonymous"}${checker.signatureToString(signature)};`,
|
||||
`export function ${exportName}${checker.signatureToString(
|
||||
signature,
|
||||
declaration,
|
||||
DECLARATION_TYPE_FORMAT_FLAGS,
|
||||
)};`,
|
||||
)
|
||||
.join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
if (ts.isVariableDeclaration(declaration)) {
|
||||
const name = declaration.name.getText();
|
||||
const type = checker.getTypeAtLocation(declaration);
|
||||
const prefix =
|
||||
declaration.parent && (ts.getCombinedNodeFlags(declaration.parent) & ts.NodeFlags.Const) !== 0
|
||||
@@ -309,52 +495,39 @@ function printNode(
|
||||
: "let";
|
||||
return normalizePluginSdkApiDeclarationText(
|
||||
repoRoot,
|
||||
`export ${prefix} ${name}: ${checker.typeToString(type, declaration, ts.TypeFormatFlags.NoTruncation)};`,
|
||||
`export ${prefix} ${exportName}: ${checker.typeToString(
|
||||
type,
|
||||
declaration,
|
||||
DECLARATION_TYPE_FORMAT_FLAGS,
|
||||
)};`,
|
||||
);
|
||||
}
|
||||
|
||||
if (ts.isInterfaceDeclaration(declaration)) {
|
||||
return `export interface ${declaration.name.text}`;
|
||||
}
|
||||
|
||||
if (ts.isClassDeclaration(declaration)) {
|
||||
return `export class ${declaration.name?.text ?? "AnonymousClass"}`;
|
||||
}
|
||||
|
||||
if (ts.isEnumDeclaration(declaration)) {
|
||||
return `export enum ${declaration.name.text}`;
|
||||
}
|
||||
|
||||
if (ts.isModuleDeclaration(declaration)) {
|
||||
return `export namespace ${declaration.name.getText()}`;
|
||||
}
|
||||
|
||||
if (ts.isTypeAliasDeclaration(declaration)) {
|
||||
const type = checker.getTypeAtLocation(declaration);
|
||||
const rendered = normalizePluginSdkApiDeclarationText(
|
||||
const typeParameters = printTypeParameters(printer, declaration);
|
||||
return normalizePluginSdkApiDeclarationText(
|
||||
repoRoot,
|
||||
`export type ${declaration.name.text} = ${checker.typeToString(
|
||||
`export type ${exportName}${typeParameters} = ${checker.typeToString(
|
||||
type,
|
||||
declaration,
|
||||
ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.MultilineObjectLiterals,
|
||||
DECLARATION_TYPE_FORMAT_FLAGS,
|
||||
)};`,
|
||||
);
|
||||
if (rendered.length > 1200) {
|
||||
return `export type ${declaration.name.text} = /* see source */`;
|
||||
}
|
||||
return rendered;
|
||||
}
|
||||
|
||||
const printableDeclaration = renameStructuredDeclarationForExport(
|
||||
checker,
|
||||
declaration,
|
||||
exportName,
|
||||
);
|
||||
const text = printer
|
||||
.printNode(ts.EmitHint.Unspecified, declaration, declaration.getSourceFile())
|
||||
.printNode(ts.EmitHint.Unspecified, printableDeclaration, declaration.getSourceFile())
|
||||
.trim();
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
const normalizedText = normalizePluginSdkApiDeclarationText(repoRoot, text);
|
||||
return normalizedText.length > 1200
|
||||
? `${truncateUtf16Safe(normalizedText, 1175).trimEnd()}\n/* truncated; see source */`
|
||||
: normalizedText;
|
||||
return normalizePluginSdkApiDeclarationText(repoRoot, ensureExportedDeclarationText(text));
|
||||
}
|
||||
|
||||
function compareText(left: string, right: string): number {
|
||||
@@ -391,24 +564,19 @@ function compareDeclarations(
|
||||
function buildExportSurface(params: {
|
||||
checker: ts.TypeChecker;
|
||||
printer: ts.Printer;
|
||||
program: ts.Program;
|
||||
repoRoot: string;
|
||||
symbol: ts.Symbol;
|
||||
}): PluginSdkApiExport {
|
||||
const { checker, printer, program, repoRoot, symbol } = params;
|
||||
const { checker, printer, repoRoot, symbol } = params;
|
||||
const { declaration, resolvedSymbol } = resolveSymbolAndDeclaration(checker, repoRoot, symbol);
|
||||
const exportName = symbol.getName();
|
||||
return {
|
||||
declaration: declaration ? printNode(repoRoot, checker, printer, declaration) : null,
|
||||
exportName: symbol.getName(),
|
||||
kind: inferExportKind(resolvedSymbol, declaration),
|
||||
source: declaration
|
||||
? buildSourceLink(
|
||||
repoRoot,
|
||||
program,
|
||||
declaration.getSourceFile().fileName,
|
||||
declaration.getStart(),
|
||||
)
|
||||
declaration: declaration
|
||||
? printNode(repoRoot, checker, printer, declaration, exportName)
|
||||
: null,
|
||||
exportName,
|
||||
kind: inferExportKind(resolvedSymbol, declaration),
|
||||
source: declaration ? buildSourceLink(repoRoot, declaration.getSourceFile().fileName) : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -437,11 +605,14 @@ function buildModuleSurface(params: {
|
||||
printer: ts.Printer;
|
||||
program: ts.Program;
|
||||
repoRoot: string;
|
||||
entrypoint: PluginSdkDocEntrypoint;
|
||||
entrypoint: string;
|
||||
}): PluginSdkApiModule {
|
||||
const { checker, printer, program, repoRoot, entrypoint } = params;
|
||||
const metadata = pluginSdkDocMetadata[entrypoint];
|
||||
const importSpecifier = resolvePluginSdkDocImportSpecifier(entrypoint);
|
||||
const metadata = Object.hasOwn(pluginSdkDocMetadata, entrypoint)
|
||||
? pluginSdkDocMetadata[entrypoint as PluginSdkDocEntrypoint]
|
||||
: undefined;
|
||||
const importSpecifier =
|
||||
entrypoint === "index" ? "openclaw/plugin-sdk" : `openclaw/plugin-sdk/${entrypoint}`;
|
||||
const moduleSourcePath = path.join(repoRoot, "src", "plugin-sdk", `${entrypoint}.ts`);
|
||||
const sourceFile = program.getSourceFile(moduleSourcePath);
|
||||
assert(sourceFile, `Missing source file for ${importSpecifier}`);
|
||||
@@ -456,7 +627,6 @@ function buildModuleSurface(params: {
|
||||
buildExportSurface({
|
||||
checker,
|
||||
printer,
|
||||
program,
|
||||
repoRoot,
|
||||
symbol,
|
||||
}),
|
||||
@@ -464,11 +634,11 @@ function buildModuleSurface(params: {
|
||||
.toSorted(sortExports);
|
||||
|
||||
return {
|
||||
category: metadata.category,
|
||||
category: metadata?.category ?? null,
|
||||
entrypoint,
|
||||
exports,
|
||||
importSpecifier,
|
||||
source: buildSourceLink(repoRoot, program, moduleSourcePath, 0),
|
||||
source: buildSourceLink(repoRoot, moduleSourcePath),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -482,7 +652,6 @@ function buildJsonlLines(baseline: PluginSdkApiBaseline): string[] {
|
||||
entrypoint: moduleSurface.entrypoint,
|
||||
importSpecifier: moduleSurface.importSpecifier,
|
||||
recordType: "module",
|
||||
sourceLine: moduleSurface.source.line,
|
||||
sourcePath: moduleSurface.source.path,
|
||||
}),
|
||||
);
|
||||
@@ -496,7 +665,6 @@ function buildJsonlLines(baseline: PluginSdkApiBaseline): string[] {
|
||||
importSpecifier: moduleSurface.importSpecifier,
|
||||
kind: exportSurface.kind,
|
||||
recordType: "export",
|
||||
sourceLine: exportSurface.source?.line ?? null,
|
||||
sourcePath: exportSurface.source?.path ?? null,
|
||||
}),
|
||||
);
|
||||
@@ -509,11 +677,13 @@ function buildJsonlLines(baseline: PluginSdkApiBaseline): string[] {
|
||||
/** Render the current public SDK API baseline without writing generated artifacts. */
|
||||
export async function renderPluginSdkApiBaseline(params?: {
|
||||
repoRoot?: string;
|
||||
entrypoints?: readonly string[];
|
||||
}): Promise<PluginSdkApiBaselineRender> {
|
||||
const repoRoot = params?.repoRoot ?? resolveRepoRoot();
|
||||
const entrypoints = params?.entrypoints ?? listPluginSdkApiBaselineEntrypoints();
|
||||
validateMetadata();
|
||||
const { checker, printer, program } = createCompilerContext(repoRoot);
|
||||
const modules = (Object.keys(pluginSdkDocMetadata) as PluginSdkDocEntrypoint[])
|
||||
const { checker, printer, program } = createCompilerContext(repoRoot, entrypoints);
|
||||
const modules = entrypoints
|
||||
.map((entrypoint) =>
|
||||
buildModuleSurface({
|
||||
checker,
|
||||
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
shouldRunPromptSnapshotOwnerTest,
|
||||
shouldRunRuntimeSidecarBaselineCheck,
|
||||
shouldRunShrinkwrapGuard,
|
||||
shouldRunPluginSdkApiBaselineCheck,
|
||||
shouldRunPluginSdkSurfaceChecks,
|
||||
shouldRunSqliteSessionSchemaBaselineCheck,
|
||||
shouldRunTestTempCreationReport,
|
||||
createShrinkwrapGuardCommand,
|
||||
@@ -1700,6 +1702,69 @@ describe("scripts/changed-lanes", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("runs Plugin SDK API checks for transitive public contract changes", () => {
|
||||
expect(
|
||||
shouldRunPluginSdkApiBaselineCheck([
|
||||
"src/config/sessions/session-accessor.ts",
|
||||
"packages/gateway-protocol/src/schema/approvals.ts",
|
||||
"extensions/memory-core/index.ts",
|
||||
"scripts/generate-plugin-sdk-api-baseline.ts",
|
||||
"scripts/lib/plugin-sdk-doc-metadata.ts",
|
||||
"docs/.generated/plugin-sdk-api-baseline.sha256",
|
||||
]),
|
||||
).toBe(true);
|
||||
expect(shouldRunPluginSdkApiBaselineCheck(["docs/help/troubleshooting.md"])).toBe(false);
|
||||
|
||||
const result = detectChangedLanes(["src/config/sessions/session-accessor.ts"]);
|
||||
const plan = createChangedCheckPlan(result);
|
||||
|
||||
expect(plan.commands).toContainEqual({
|
||||
name: "Plugin SDK API baseline",
|
||||
args: ["plugin-sdk:api:check"],
|
||||
});
|
||||
expect(plan.commands.map((command) => command.args[0])).not.toContain(
|
||||
"plugin-sdk:surface:check",
|
||||
);
|
||||
});
|
||||
|
||||
it("runs Plugin SDK export and surface checks for direct SDK changes", () => {
|
||||
expect(
|
||||
shouldRunPluginSdkSurfaceChecks([
|
||||
"src/plugin-sdk/core.ts",
|
||||
"scripts/plugin-sdk-surface-report.mjs",
|
||||
"scripts/sync-plugin-sdk-exports.mjs",
|
||||
"scripts/lib/plugin-sdk-entrypoints.json",
|
||||
"package.json",
|
||||
]),
|
||||
).toBe(true);
|
||||
expect(shouldRunPluginSdkSurfaceChecks(["src/config/sessions/session-accessor.ts"])).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
const result = detectChangedLanes(["src/plugin-sdk/core.ts"]);
|
||||
const plan = createChangedCheckPlan(result);
|
||||
|
||||
expect(plan.commands).toContainEqual({
|
||||
name: "Plugin SDK API baseline",
|
||||
args: ["plugin-sdk:api:check"],
|
||||
});
|
||||
expect(plan.commands).toContainEqual({
|
||||
name: "Plugin SDK package exports",
|
||||
args: ["plugin-sdk:check-exports"],
|
||||
});
|
||||
expect(plan.commands).toContainEqual({
|
||||
name: "Plugin SDK surface budget",
|
||||
args: ["plugin-sdk:surface:check"],
|
||||
});
|
||||
|
||||
const releaseMetadataPlan = createChangedCheckPlan(
|
||||
detectChangedLanes(["CHANGELOG.md", "package.json"]),
|
||||
);
|
||||
expect(releaseMetadataPlan.commands.map((command) => command.args[0])).not.toContain(
|
||||
"plugin-sdk:check-exports",
|
||||
);
|
||||
});
|
||||
|
||||
it("guards release metadata package changes to the top-level version field", () => {
|
||||
const dir = makeTempRepoRoot(tempDirs, "openclaw-release-metadata-");
|
||||
git(dir, ["init", "-q", "--initial-branch=main"]);
|
||||
|
||||
Reference in New Issue
Block a user