fix(cli): harden official plugin recovery (#93325)

* fix(cli): harden official plugin recovery

* fix(config): preserve include write context

* fix(config): reject external include mutations

* fix(config): bind snapshots to config paths

* fix(config): preserve write ownership

* fix(cli): preflight plugin config mutations

* chore(plugin-sdk): refresh api baseline

* test(config): prove install env policy mutations

* fix(cli): preflight plugin updates

* fix(cli): preflight non-npm id migrations

* chore(plugin-sdk): refresh api baseline

* fix(cli): satisfy plugin recovery checks
This commit is contained in:
Vincent Koc
2026-06-15 23:07:29 +08:00
committed by GitHub
parent c1219d161d
commit 767e8280ac
39 changed files with 9380 additions and 898 deletions

View File

@@ -21,6 +21,10 @@ type ListMarketplacePluginsFn =
(typeof import("../plugins/marketplace.js"))["listMarketplacePlugins"];
type ResolveMarketplaceInstallShortcutFn =
(typeof import("../plugins/marketplace.js"))["resolveMarketplaceInstallShortcut"];
type UpdateNpmInstalledPluginsFn =
(typeof import("../plugins/update.js"))["updateNpmInstalledPlugins"];
type UpdateNpmInstalledHookPacksFn =
(typeof import("../hooks/update.js"))["updateNpmInstalledHookPacks"];
type PluginInstallRecordMap = Record<string, PluginInstallRecord>;
let mockInstalledPluginIndexInstallRecords: PluginInstallRecordMap = {};
@@ -37,6 +41,7 @@ function invokeMock<TArgs extends unknown[], TResult>(mock: unknown, ...args: TA
export const loadConfig: Mock<LoadConfigFn> = vi.fn<LoadConfigFn>(() => ({}) as OpenClawConfig);
export const readConfigFileSnapshot: AsyncUnknownMock = vi.fn();
export const readConfigFileSnapshotForWrite: AsyncUnknownMock = vi.fn();
export const writeConfigFile: AsyncUnknownMock = vi.fn(async () => undefined);
export const replaceConfigFile: AsyncUnknownMock = vi.fn(
async (params: { nextConfig: OpenClawConfig }) => await writeConfigFile(params.nextConfig),
@@ -73,8 +78,8 @@ export const applyExclusiveSlotSelection: UnknownMock = vi.fn();
export const planPluginUninstall: UnknownMock = vi.fn();
export const applyPluginUninstallDirectoryRemoval: AsyncUnknownMock = vi.fn();
const uninstallPlugin: AsyncUnknownMock = vi.fn();
export const updateNpmInstalledPlugins: AsyncUnknownMock = vi.fn();
export const updateNpmInstalledHookPacks: AsyncUnknownMock = vi.fn();
export const updateNpmInstalledPlugins: Mock<UpdateNpmInstalledPluginsFn> = vi.fn();
export const updateNpmInstalledHookPacks: Mock<UpdateNpmInstalledHookPacksFn> = vi.fn();
export const promptYesNo: AsyncUnknownMock = vi.fn();
export class PromptInputClosedError extends Error {
constructor() {
@@ -191,6 +196,16 @@ vi.mock("../config/config.js", () => ({
readConfigFileSnapshot,
...args,
)) as (typeof import("../config/config.js"))["readConfigFileSnapshot"],
readConfigFileSnapshotForWrite: ((
...args: Parameters<(typeof import("../config/config.js"))["readConfigFileSnapshotForWrite"]>
) =>
invokeMock<
Parameters<(typeof import("../config/config.js"))["readConfigFileSnapshotForWrite"]>,
ReturnType<(typeof import("../config/config.js"))["readConfigFileSnapshotForWrite"]>
>(
readConfigFileSnapshotForWrite,
...args,
)) as (typeof import("../config/config.js"))["readConfigFileSnapshotForWrite"],
writeConfigFile: ((config: OpenClawConfig) =>
invokeMock<
[OpenClawConfig],
@@ -481,18 +496,22 @@ vi.mock("../plugins/uninstall.js", async (importOriginal) => {
};
});
vi.mock("../plugins/update.js", () => ({
updateNpmInstalledPlugins: ((
...args: Parameters<(typeof import("../plugins/update.js"))["updateNpmInstalledPlugins"]>
) =>
invokeMock<
Parameters<(typeof import("../plugins/update.js"))["updateNpmInstalledPlugins"]>,
ReturnType<(typeof import("../plugins/update.js"))["updateNpmInstalledPlugins"]>
>(
updateNpmInstalledPlugins,
...args,
)) as (typeof import("../plugins/update.js"))["updateNpmInstalledPlugins"],
}));
vi.mock("../plugins/update.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../plugins/update.js")>();
return {
...actual,
updateNpmInstalledPlugins: ((
...args: Parameters<(typeof import("../plugins/update.js"))["updateNpmInstalledPlugins"]>
) =>
invokeMock<
Parameters<(typeof import("../plugins/update.js"))["updateNpmInstalledPlugins"]>,
ReturnType<(typeof import("../plugins/update.js"))["updateNpmInstalledPlugins"]>
>(
updateNpmInstalledPlugins,
...args,
)) as (typeof import("../plugins/update.js"))["updateNpmInstalledPlugins"],
};
});
vi.mock("../hooks/update.js", () => ({
updateNpmInstalledHookPacks: ((
@@ -679,6 +698,7 @@ export function resetPluginsCliTestState() {
restoreRuntimeCaptureMocks();
loadConfig.mockReset();
readConfigFileSnapshot.mockReset();
readConfigFileSnapshotForWrite.mockReset();
writeConfigFile.mockReset();
replaceConfigFile.mockReset();
resolveStateDir.mockReset();
@@ -737,6 +757,17 @@ export function resetPluginsCliTestState() {
legacyIssues: [],
};
});
readConfigFileSnapshotForWrite.mockImplementation(async () => {
const snapshot = (await readConfigFileSnapshot()) as { path: string };
return {
snapshot,
writeOptions: {
assertConfigPathForWrite: () => {},
expectedConfigPath: snapshot.path,
ownedConfigPathForWrite: snapshot.path,
},
};
});
writeConfigFile.mockResolvedValue(undefined);
replaceConfigFile.mockImplementation(
(async (params: { nextConfig: OpenClawConfig }) =>