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:
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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" }));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user