feat(FN-3970): consolidate Even plugin APIs into unified package with webho

Collapsed the Even plugin architecture into a single unified plugin by merging board card routes and replacing the transport stub with a real webhook implementation, removing the separate cards plugin from the workspace. The new unified plugin (`fusion-plugin-even-realities-glasses`) now exports con

Fusion-Task-Id: FN-3970
This commit is contained in:
Fusion
2026-05-11 00:15:01 -07:00
committed by gsxdsm
parent a9983df842
commit 007efcab0b
21 changed files with 600 additions and 116 deletions

View File

@@ -0,0 +1,65 @@
import { describe, expect, it, vi } from "vitest";
import plugin from "../index.js";
import type { Task } from "@fusion/core";
function makeTask(id: string, column: Task["column"], updatedAt: string): Task {
return {
id,
title: id,
description: id,
column,
status: "pending",
priority: "normal",
createdAt: "2026-05-08T10:00:00.000Z",
updatedAt,
currentStep: 0,
steps: [],
dependencies: [],
} as unknown as Task;
}
function createContext(tasks: Task[], apiKey = "secret") {
return {
pluginId: "fusion-plugin-even-realities-glasses",
settings: { apiKey },
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
taskStore: {
listTasks: vi.fn(async () => tasks),
getTask: vi.fn(async (id: string) => tasks.find((task) => task.id === id)),
},
} as never;
}
function route(path: string) {
return plugin.routes!.find((entry) => entry.path === path)!;
}
describe("board routes", () => {
it("returns 401 when auth header missing", async () => {
const response = (await route("/board/cards").handler({ headers: {} }, createContext([]))) as any;
expect(response.status).toBe(401);
});
it("returns 503 when plugin is not configured", async () => {
const response = (await route("/board/cards").handler({ headers: { authorization: "Bearer secret" } }, createContext([], ""))) as any;
expect(response.status).toBe(503);
});
it("returns deck on happy path", async () => {
const tasks = [makeTask("FN-1", "todo", "2026-05-08T11:00:00.000Z"), makeTask("FN-2", "in-progress", "2026-05-08T12:00:00.000Z")];
const response = (await route("/board/cards").handler({ headers: { authorization: "Bearer secret" } }, createContext(tasks))) as any;
expect(response.status).toBe(200);
expect(response.body.deck.cards[0].id).toBe("summary");
expect(response.body.deck.cards).toHaveLength(3);
});
it("returns task deck for known id", async () => {
const tasks = [makeTask("FN-1", "todo", "2026-05-08T11:00:00.000Z")];
const response = (await route("/tasks/:id/cards").handler(
{ headers: { authorization: "Bearer secret" }, params: { id: "FN-1" } },
createContext(tasks),
)) as any;
expect(response.status).toBe(200);
expect(response.body.deck.cards[0].id).toBe("FN-1");
});
});

View File

@@ -6,6 +6,7 @@ describe("even realities plugin", () => {
expect(plugin.manifest.id).toBe("fusion-plugin-even-realities-glasses");
expect(Object.keys(plugin.manifest.settingsSchema ?? {}).sort()).toEqual([
"apiKey",
"companionWebhookUrl",
"enableAgentActions",
"fusionApiBaseUrl",
"fusionApiToken",
@@ -41,7 +42,11 @@ describe("even realities plugin", () => {
};
const ctx = {
pluginId: "known",
settings: { fusionApiToken: "token", fusionApiBaseUrl: "http://localhost:4040" },
settings: {
fusionApiToken: "token",
fusionApiBaseUrl: "http://localhost:4040",
companionWebhookUrl: "https://companion.example",
},
logger: console,
taskStore: { getPluginStore: () => ({ db }) },
} as never;

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
agentActionsEnabled,
getCompanionWebhookUrl,
getFusionBaseUrl,
getFusionToken,
getNotifyColumns,
@@ -12,6 +13,7 @@ describe("settings accessors", () => {
it("uses safe defaults", () => {
expect(getFusionBaseUrl({})).toBe("http://localhost:4040");
expect(getFusionToken({})).toBeUndefined();
expect(getCompanionWebhookUrl({})).toBeUndefined();
expect(getPollingIntervalMs({})).toBe(30000);
expect(getNotifyColumns({})).toEqual(["in-review"]);
expect(getQuickCaptureColumn({})).toBe("triage");
@@ -21,6 +23,9 @@ describe("settings accessors", () => {
it("trims string values", () => {
expect(getFusionBaseUrl({ fusionApiBaseUrl: " http://fusion.local:4040 " })).toBe("http://fusion.local:4040");
expect(getFusionToken({ fusionApiToken: " token " })).toBe("token");
expect(getCompanionWebhookUrl({ companionWebhookUrl: " https://companion.example/ingest " })).toBe(
"https://companion.example/ingest",
);
});
it("enforces polling minimum and finite values", () => {

View File

@@ -0,0 +1,28 @@
import { describe, expect, it, vi } from "vitest";
import { createTransportRoutes } from "../routes/transport-routes.js";
describe("transport routes", () => {
it("requires api key", async () => {
const routes = createTransportRoutes(() => undefined);
const route = routes.find((entry) => entry.path === "/transport/actions");
const res = await route?.handler({ headers: {} }, { settings: {}, pluginId: "p" } as never);
expect(res).toMatchObject({ status: 503 });
});
it("accepts action payloads", async () => {
const receiveAction = vi.fn(async () => undefined);
const routes = createTransportRoutes(() => ({ receiveAction } as never));
const route = routes.find((entry) => entry.path === "/transport/actions");
const res = await route?.handler(
{
headers: { authorization: "Bearer key" },
body: { type: "start-work", taskId: "FN-1", timestamp: new Date().toISOString() },
},
{ settings: { apiKey: "key" }, pluginId: "p" } as never,
);
expect(res).toMatchObject({ status: 202, body: { accepted: true } });
expect(receiveAction).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,22 +1,35 @@
import { describe, expect, it, vi } from "vitest";
import { StubGlassesTransport } from "../transport.js";
import { WebhookGlassesTransport } from "../transport.js";
describe("StubGlassesTransport", () => {
it("records pushes in order", async () => {
const transport = new StubGlassesTransport();
describe("WebhookGlassesTransport", () => {
it("posts cards to companion webhook", async () => {
const fetchImpl = vi.fn(async () => ({ ok: true, status: 200 })) as unknown as typeof fetch;
const transport = new WebhookGlassesTransport({
companionWebhookUrl: "https://companion.example",
fetchImpl,
});
await transport.connect();
await transport.pushCard({ id: "1", kind: "task", title: "A", lines: [], badge: "todo" });
await transport.pushCard({ id: "2", kind: "task", title: "B", lines: [], badge: "done" });
expect(transport.pushedCards.map((card) => card.id)).toEqual(["1", "2"]);
expect(fetchImpl).toHaveBeenCalledWith(
"https://companion.example/cards",
expect.objectContaining({ method: "POST" }),
);
expect(transport.connected).toBe(true);
expect(transport.status.lastPushAt).toEqual(expect.any(String));
});
it("emits synthetic actions to handlers", async () => {
const transport = new StubGlassesTransport();
it("tracks configuration and dispatches actions", async () => {
const transport = new WebhookGlassesTransport();
const handler = vi.fn();
transport.onAction(handler);
await transport.emitAction({ type: "quick-capture", text: "new task", timestamp: new Date().toISOString() });
await transport.connect();
await transport.receiveAction?.({ type: "quick-capture", text: "new task", timestamp: new Date().toISOString() });
expect(transport.connected).toBe(false);
expect(transport.status.endpointConfigured).toBe(false);
expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith(expect.objectContaining({ type: "quick-capture" }));
});
});