feat(wave): #119/#122/#131/#132 verts + sprint plugins ESM/persistance #135/#136/#139
État d'intégration confiné à la branche batch. Les tickets #119 (skills → capacités agent découvrables), #122 (override permissions par défaut), #131 (effort par agent/presets) et #132 (outil MCP d'édition du contexte projet) sont verts en périmètre. Le sprint plugins multi-fichiers ESM / persistance plugin-owned (#135/#136/#139) est co-implémenté dans les MÊMES fichiers de câblage (frontend/src/ports/index.ts, backend/src/lib.rs, domain/ports.rs, backend/dto.rs), inséparable sans staging interactif (indisponible ici). Commit unique volontaire : préserve l'état vert QA sans découpe hunk risquée. NON mergé vers develop tant que #137 (QA e2e plugins) n'est pas vert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -210,6 +210,7 @@ describe("loadPlugins", () => {
|
||||
"pluginToolchain",
|
||||
"pluginEvents",
|
||||
"pluginConfig",
|
||||
"pluginStorage",
|
||||
].some((key) => key in ctx);
|
||||
globalThis.__serviceKeys = Object.keys(ctx.services).sort();
|
||||
globalThis.__workspaceServiceKeys = Object.keys(ctx.services.workspace).sort();
|
||||
@ -241,6 +242,7 @@ describe("loadPlugins", () => {
|
||||
"pluginDisplayName",
|
||||
"pluginId",
|
||||
"services",
|
||||
"storage",
|
||||
"subscriptions",
|
||||
"version",
|
||||
]);
|
||||
@ -294,6 +296,59 @@ describe("loadPlugins", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("injects plugin-owned storage scoped to the activating plugin id", async () => {
|
||||
const calls: unknown[] = [];
|
||||
const storageGateways = {
|
||||
...gateways,
|
||||
pluginStorage: {
|
||||
async get(input: unknown) {
|
||||
calls.push(["get", input]);
|
||||
return { launches: 1 };
|
||||
},
|
||||
async set(input: unknown) {
|
||||
calls.push(["set", input]);
|
||||
},
|
||||
async delete(input: unknown) {
|
||||
calls.push(["delete", input]);
|
||||
return true;
|
||||
},
|
||||
},
|
||||
} as PluginGatewaySet;
|
||||
const bundle = dataUrl(`
|
||||
export async function activate(ctx) {
|
||||
globalThis.__storageKeys = Object.keys(ctx.storage).sort();
|
||||
globalThis.__storedValue = await ctx.storage.get("helloPlugin.state");
|
||||
await ctx.storage.set("helloPlugin.state", { launches: 2 });
|
||||
await ctx.storage.delete("helloPlugin.state");
|
||||
}
|
||||
`);
|
||||
|
||||
const { failures } = await loadPlugins(
|
||||
[entry({ id: "dev.acme.storage", displayName: "Storage", bundleUrl: bundle })],
|
||||
storageGateways,
|
||||
);
|
||||
|
||||
expect(failures).toEqual([]);
|
||||
expect((globalThis as Record<string, unknown>).__storageKeys).toEqual([
|
||||
"delete",
|
||||
"get",
|
||||
"set",
|
||||
]);
|
||||
expect((globalThis as Record<string, unknown>).__storedValue).toEqual({ launches: 1 });
|
||||
expect(calls).toEqual([
|
||||
["get", { pluginId: "dev.acme.storage", key: "helloPlugin.state" }],
|
||||
[
|
||||
"set",
|
||||
{
|
||||
pluginId: "dev.acme.storage",
|
||||
key: "helloPlugin.state",
|
||||
value: { launches: 2 },
|
||||
},
|
||||
],
|
||||
["delete", { pluginId: "dev.acme.storage", key: "helloPlugin.state" }],
|
||||
]);
|
||||
});
|
||||
|
||||
it("loads the hello-plugin command and layout contribution shape", async () => {
|
||||
const bundle = dataUrl(`
|
||||
export function activate(ctx) {
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
* collected, never thrown past `loadPlugins`.
|
||||
*/
|
||||
|
||||
import type { PluginContributionDto, PluginRuntimePlugin } from "@/domain";
|
||||
import type { JsonValue, PluginContributionDto, PluginRuntimePlugin } from "@/domain";
|
||||
import {
|
||||
PluginCommandRegistry,
|
||||
PluginLayoutRegistry,
|
||||
@ -38,9 +38,16 @@ export interface IdeaPluginContext {
|
||||
commands: PluginCommandContext;
|
||||
layouts: PluginLayoutRegistry;
|
||||
menu: PluginMenuRegistry;
|
||||
storage: PluginStorage;
|
||||
services?: PluginServices;
|
||||
}
|
||||
|
||||
export interface PluginStorage {
|
||||
get<T extends JsonValue = JsonValue>(key: string): Promise<T | undefined>;
|
||||
set(key: string, value: JsonValue): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface PluginActivation {
|
||||
dispose?: () => void | Promise<void>;
|
||||
}
|
||||
@ -241,6 +248,7 @@ async function loadOne(
|
||||
const commands = new PluginCommandRegistry(pluginId, declaredCommandIds);
|
||||
const layouts = new PluginLayoutRegistry(pluginId, declaredLayoutTypes);
|
||||
const menu = new PluginMenuRegistry(pluginId);
|
||||
const storage = createPluginStorage(gateways, pluginId);
|
||||
|
||||
const ctx: IdeaPluginContext = {
|
||||
pluginId,
|
||||
@ -251,6 +259,7 @@ async function loadOne(
|
||||
commands: createCommandContext(commands),
|
||||
layouts,
|
||||
menu,
|
||||
storage,
|
||||
};
|
||||
if (hasCapability(entry, "tooling")) {
|
||||
ctx.services = createPluginServices(gateways);
|
||||
@ -287,6 +296,21 @@ async function loadOne(
|
||||
}
|
||||
}
|
||||
|
||||
function createPluginStorage(gateways: PluginGatewaySet, pluginId: string): PluginStorage {
|
||||
return {
|
||||
async get<T extends JsonValue = JsonValue>(key: string): Promise<T | undefined> {
|
||||
const value = await gateways.pluginStorage.get({ pluginId, key });
|
||||
return value === null ? undefined : (value as T);
|
||||
},
|
||||
async set(key, value) {
|
||||
await gateways.pluginStorage.set({ pluginId, key, value });
|
||||
},
|
||||
async delete(key) {
|
||||
await gateways.pluginStorage.delete({ pluginId, key });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads every plugin in the catalog into a fresh {@link PluginRuntimeRegistry}.
|
||||
* Called once at app bootstrap (and never again in the same session — carnet
|
||||
|
||||
@ -25,6 +25,7 @@ import type {
|
||||
GitGateway,
|
||||
PluginConfigGateway,
|
||||
PluginEventGateway,
|
||||
PluginStorageGateway,
|
||||
PluginTaskGateway,
|
||||
PluginToolchainGateway,
|
||||
PluginWorkspaceGateway,
|
||||
@ -48,6 +49,7 @@ export interface PluginGatewaySet {
|
||||
pluginToolchain: PluginToolchainGateway;
|
||||
pluginEvents: PluginEventGateway;
|
||||
pluginConfig: PluginConfigGateway;
|
||||
pluginStorage: PluginStorageGateway;
|
||||
}
|
||||
|
||||
/** A disposable handle returned by every `register*` call. */
|
||||
|
||||
Reference in New Issue
Block a user