Files
openclaw/src/plugin-sdk/api-baseline.test.ts
Peter Steinberger c1dab6a5e0 test: speed up measured CI hotspots (#114367)
* test: remove redundant followup goal integration

* test: cover CLI help table in process

* test: inject usage cost settle timing

* test: reuse rendered plugin SDK baseline

* test: reuse bundled provider registrations

* test: isolate startup quarantine mapping

* test: inject session patch auth metadata

* test: avoid CLI help catch shadowing

* test(cli): keep gateway dependencies internal

* test(auto-reply): stabilize cold followup setup
2026-07-27 06:25:56 -04:00

170 lines
6.4 KiB
TypeScript

/**
* Tests the plugin SDK public API baseline.
*/
import path from "node:path";
import { beforeAll, describe, expect, it } from "vitest";
import { publicPluginSdkEntrypoints } from "../../scripts/lib/plugin-sdk-entries.mjs";
import {
computePluginSdkApiBaselineHashFileContent,
listPluginSdkApiBaselineEntrypoints,
normalizePluginSdkApiDeclarationText,
normalizePluginSdkApiSourcePath,
renderPluginSdkApiBaseline,
renderPluginSdkApiBaselineModules,
type PluginSdkApiBaselineRender,
} from "./api-baseline.js";
const TEST_ENTRYPOINTS = [
"agent-harness-runtime",
"approval-gateway-runtime",
"channel-policy",
"core",
"infra-runtime",
"provider-auth",
"provider-catalog-live-runtime",
"provider-oauth-runtime",
"provider-selection-runtime",
"provider-web-search-config-contract",
"realtime-voice",
"sqlite-runtime-testing",
] as const;
describe("Plugin SDK API baseline", () => {
let rendered: PluginSdkApiBaselineRender;
beforeAll(async () => {
rendered = await renderPluginSdkApiBaseline({ entrypoints: TEST_ENTRYPOINTS });
});
it("normalizes declaration import paths to repo-relative paths", () => {
const repoRoot = process.cwd();
const modelCatalogPath = path.join(repoRoot, "src", "agents", "agent-model-discovery");
const declaration = `export function setModelCatalogImportForTest(loader?: (() => Promise<typeof import("${modelCatalogPath}", { with: { "resolution-mode": "import" } })>) | undefined): void;`;
const normalized = normalizePluginSdkApiDeclarationText(repoRoot, declaration);
expect(normalized).not.toContain(repoRoot);
expect(normalized).toContain(
'import("src/agents/agent-model-discovery", { with: { "resolution-mode": "import" } })',
);
});
it("normalizes dependency source paths to stable node_modules paths", () => {
const repoRoot = path.join(path.sep, "workspace", "openclaw-worktree");
const linkedDependencyPath = path.join(
path.sep,
"workspace",
"openclaw",
"node_modules",
"@openclaw",
"fs-safe",
"dist",
"secret-file.d.ts",
);
const pnpmDependencyPath = path.join(
repoRoot,
"node_modules",
".pnpm",
"@openclaw+fs-safe@1.0.0",
"node_modules",
"@openclaw",
"fs-safe",
"dist",
"secret-file.d.ts",
);
expect(normalizePluginSdkApiSourcePath(repoRoot, linkedDependencyPath)).toBe(
"node_modules/@openclaw/fs-safe/dist/secret-file.d.ts",
);
expect(normalizePluginSdkApiSourcePath(repoRoot, pnpmDependencyPath)).toBe(
"node_modules/@openclaw/fs-safe/dist/secret-file.d.ts",
);
});
it("keeps repo source paths relative when a parent directory is named node_modules", () => {
const repoRoot = path.join(path.sep, "workspace", "node_modules", "openclaw");
const sourcePath = path.join(repoRoot, "src", "plugin-sdk", "core.ts");
expect(normalizePluginSdkApiSourcePath(repoRoot, sourcePath)).toBe("src/plugin-sdk/core.ts");
});
it("renders complete declarations for the canonical public entrypoint inventory", () => {
expect(listPluginSdkApiBaselineEntrypoints()).toEqual(publicPluginSdkEntrypoints);
const findDeclaration = (exportName: string) =>
rendered.baseline.modules
.flatMap((moduleSurface) => moduleSurface.exports)
.find(
(exportSurface) =>
exportSurface.exportName === exportName && exportSurface.declaration !== null,
)?.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("LiveModelRowProjection")).toContain(
"export type LiveModelRowProjection",
);
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":');
});
it("renders snapshots independently of entrypoint discovery order", () => {
const reverse = renderPluginSdkApiBaselineModules(rendered.baseline.modules.toReversed());
expect(reverse.json).toBe(rendered.json);
expect(reverse.jsonl).toBe(rendered.jsonl);
});
it("hashes entrypoints independently so unrelated API changes merge", () => {
const target = rendered.baseline.modules[0];
expect(target?.exports.length).toBeGreaterThan(0);
const changed = renderPluginSdkApiBaselineModules(
rendered.baseline.modules.map((moduleSurface) =>
moduleSurface === target
? {
...moduleSurface,
exports: moduleSurface.exports.map((exportSurface, index) =>
index === 0
? { ...exportSurface, declaration: `${exportSurface.declaration ?? ""} changed` }
: exportSurface,
),
}
: moduleSurface,
),
);
const before = computePluginSdkApiBaselineHashFileContent(rendered).split("\n");
const after = computePluginSdkApiBaselineHashFileContent(changed).split("\n");
expect(after[0]).not.toBe(before[0]);
expect(after.slice(1)).toEqual(before.slice(1));
});
});