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:
2026-08-03 11:06:23 +02:00
parent 22c6bd803d
commit 171c6c923c
59 changed files with 3654 additions and 291 deletions

View File

@ -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) {