FN-7397: pass plugin context to lifecycle hooks

Ensure task lifecycle plugin hooks receive a PluginContext when fired through runtime event bridges.

- Append the plugin context for task-created, task-moved, and task-completed hooks when callers only provide raw lifecycle arguments.
- Preserve existing explicit-context hook calls and add coverage for loader and runner lifecycle dispatch paths.
- Cover Compound Engineering sync completion handling so taskStore is available in onTaskCompleted.

Files changed:
 .changeset/fn-7397-plugin-hook-context.md          |  7 +++
 packages/core/src/__tests__/plugin-loader.test.ts  | 52 +++++++++++++++++++++-
 packages/core/src/plugin-loader.ts                 | 34 +++++++++++++-
 .../engine/src/__tests__/plugin-runner.test.ts     | 25 ++++++++++-
 .../src/__tests__/sync.test.ts                     | 29 +++++++++++-
 5 files changed, 141 insertions(+), 6 deletions(-)

Fusion-Task-Id: FN-7397

Fusion-Task-Lineage: 7661714c-cf2a-4f4d-bec1-fc6bc11c0613

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-01 13:55:25 -07:00
parent 07aa1a0085
commit f998fe3ffc
5 changed files with 141 additions and 6 deletions

View File

@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { InteractiveAiSessionEvent, PluginContext, Task } from "@fusion/core";
import { TaskStore } from "@fusion/core";
import { PluginLoader, PluginStore, TaskStore } from "@fusion/core";
import plugin, {
CeOrchestrator,
CE_PLUGIN_ID,
@@ -137,6 +137,33 @@ describe("U8 inbound hooks (board → pipeline)", () => {
// The hook awaits NOTHING heavy; it returns synchronously-ish.
expect(Date.now() - start).toBeLessThan(1000);
});
it("runtime loader invocation gives onTaskCompleted a context and enqueues task_completed sync", async () => {
const pluginStore = new PluginStore(rootDir, { inMemoryDb: true, centralGlobalDir: rootDir });
await pluginStore.init();
await pluginStore.registerPlugin({
manifest: plugin.manifest,
path: join(rootDir, "compound-engineering.js"),
settings: { reconcileOnHooks: false },
});
const loader = new PluginLoader({ pluginStore, taskStore });
(loader as unknown as { plugins: Map<string, typeof plugin> }).plugins.set(CE_PLUGIN_ID, plugin);
const { task } = await landPipeline("plan");
await moveTo(task.id, "done");
await expect(loader.invokeHook("onTaskCompleted", { ...task, column: "done" })).resolves.toBeUndefined();
const installed = await pluginStore.getPlugin(CE_PLUGIN_ID);
expect(installed.state).not.toBe("error");
const pending = getCePipelineStore(ctx).listPendingSync();
expect(pending).toContainEqual(expect.objectContaining({
taskId: task.id,
reason: "task_completed",
fromColumn: null,
toColumn: "done",
processedAt: null,
}));
});
});
describe("U8 reconciler (convergence + outbound)", () => {