feat: move session sharing and collab rooms into a plugin (B12)

This commit is contained in:
LukeGus committed 2026-09-23 23:42:46 -05:00
1 parent 791d8656a1
commit a276f6898c
207 files changed
+35771 -11072

No files matched your search

@@ -0,0 +1,84 @@
/**
* ctx.services with named providers: a plugin may only register the names
* its manifest lists, a consumer reaches one provider by name, and each is
* disposed with the plugin that provided it.
*/
import { afterEach, describe, expect, it, vi } from "vitest";
vi.mock("../../utils/audit-logger.js", () => ({ logAudit: async () => {} }));
import { createPluginContext, createPluginHandle } from "../../plugins/ctx.js";
import {
clearServiceRegistry,
getRegistration,
} from "../../plugins/service-registry.js";
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
function contextFor(pluginId: string, names?: string[]) {
const manifest = {
id: pluginId,
name: pluginId,
version: "1.0.0",
description: "",
author: { name: "test" },
license: "MIT",
category: "Productivity",
engine: { termix: ">=2.9.0", api: "1" },
capabilities: [],
provides: [
{
service: "sample.live",
version: "1.0.0",
permission: `${pluginId}.use`,
...(names ? { names } : {}),
},
],
} as PluginManifest;
const handle = createPluginHandle(pluginId, { activate: () => {} });
return { ctx: createPluginContext(manifest, handle), handle };
}
afterEach(() => clearServiceRegistry());
describe("ctx.services named providers", () => {
it("registers a declared name and lists it", () => {
const { ctx } = contextFor("terminal-like", ["ssh"]);
ctx.services.provide("sample.live", { get: () => 1 }, { name: "ssh" });
expect(getRegistration("sample.live", "ssh")?.pluginId).toBe(
"terminal-like",
);
expect(ctx.services.providers("sample.live")).toEqual(["ssh"]);
});
it("refuses a name the manifest does not list", () => {
const { ctx } = contextFor("terminal-like", ["ssh"]);
expect(() =>
ctx.services.provide("sample.live", {}, { name: "rdp" }),
).toThrow(/provides\[\]\.names does not list it/);
expect(() => ctx.services.provide("sample.live", {})).toThrow(
/does not list it/,
);
});
it("refuses a name when the manifest declares an unnamed service", () => {
const { ctx } = contextFor("plain");
expect(() =>
ctx.services.provide("sample.live", {}, { name: "ssh" }),
).toThrow(/does not list it/);
});
it("reaches one provider by name through a handle", () => {
const { ctx } = contextFor("terminal-like", ["ssh"]);
ctx.services.provide("sample.live", { get: () => 1 }, { name: "ssh" });
const named = ctx.services.get<{ get: () => Promise<number> }>(
"sample.live",
{ provider: "ssh" },
);
const other = ctx.services.get("sample.live", { provider: "rdp" });
expect("get" in named).toBe(true);
expect("get" in other).toBe(false);
});
});
@@ -589,6 +589,19 @@ describe("panels, dashboard cards and guest pages", () => {
expect(errors).toMatch(/dashboardCards\[0\]\.titleKey/);
});
it("accepts guest views on a guest plugin and refuses them otherwise", () => {
expect(
validateManifest(
base({ contributes: { guest: true, guestViews: ["shared"] } }),
),
).toEqual([]);
const errors = validateManifest(
base({ contributes: { guestViews: ["Shared View"] } }),
).join();
expect(errors).toMatch(/contributes\.guestViews entries must be/);
expect(errors).toMatch(/needs "contributes\.guest"/);
});
it("rejects an unknown field inside a view and a non-boolean guest", () => {
const errors = validateManifest(
base({
@@ -602,3 +615,41 @@ describe("panels, dashboard cards and guest pages", () => {
expect(errors).toMatch(/contributes\.guest" must be a boolean/);
});
});
describe("named service providers", () => {
const provides = (names: unknown) => [
{
service: "sample.live",
version: "1.0.0",
permission: "sample-plugin.use",
names,
},
];
const permissions = {
permissions: [
{
name: "use",
titleKey: "permissions.use.title",
descriptionKey: "permissions.use.description",
},
],
};
it("accepts a list of provider names", () => {
expect(
validateManifest(
base({ provides: provides(["ssh", "rdp"]), contributes: permissions }),
),
).toEqual([]);
});
it("refuses an empty, duplicated or malformed list", () => {
for (const names of [[], ["ssh", "ssh"], ["SSH"], "ssh"]) {
expect(
validateManifest(
base({ provides: provides(names), contributes: permissions }),
).join(),
).toMatch(/provides\[0\]\.names/);
}
});
});
@@ -22,6 +22,7 @@ const {
createServiceHandle,
getRegistration,
getServiceImplementation,
listProviderNames,
listServices,
provideService,
resolveRequirements,
@@ -99,6 +100,94 @@ describe("service registry", () => {
).toBeUndefined();
});
describe("named providers", () => {
it("keeps several providers of one service side by side", () => {
const ssh = register({
name: "ssh",
implementation: { kind: async () => "ssh" },
});
const rdp = register({
name: "rdp",
pluginId: "other-plugin",
implementation: { kind: async () => "rdp" },
});
expect(listProviderNames("testplugin.greet").sort()).toEqual([
"rdp",
"ssh",
]);
expect(getRegistration("testplugin.greet", "ssh")).toBe(ssh);
expect(getRegistration("testplugin.greet", "rdp")).toBe(rdp);
expect(getRegistration("testplugin.greet")).toBeUndefined();
expect(listServices()).toHaveLength(2);
});
it("hands core a named implementation only by its name", () => {
const ssh = register({ name: "ssh" });
expect(
getServiceImplementation("testplugin.greet", "^1.0.0", "ssh"),
).toBe(ssh.implementation);
expect(
getServiceImplementation("testplugin.greet", "^1.0.0"),
).toBeUndefined();
});
it("revokes one provider and leaves the other", () => {
const ssh = register({ name: "ssh" });
const rdp = register({ name: "rdp" });
expect(revokeService("testplugin.greet", ssh)).toBe(true);
expect(getRegistration("testplugin.greet", "ssh")).toBeUndefined();
expect(getRegistration("testplugin.greet", "rdp")).toBe(rdp);
});
it("routes a named handle to its provider and fails typed once it goes", async () => {
const ssh = register({
name: "ssh",
implementation: { hello: async () => "from ssh" },
});
register({
name: "rdp",
implementation: { hello: async () => "from rdp" },
});
const context = {
resolveUserId: () => "user-1",
hasPermission: async () => true,
audit: () => {},
};
const handle = createServiceHandle<{ hello: () => Promise<string> }>(
"testplugin.greet",
"consumer-plugin",
context,
"ssh",
);
const unnamed = createServiceHandle<{ hello?: () => Promise<string> }>(
"testplugin.greet",
"consumer-plugin",
context,
);
await expect(handle.hello()).resolves.toBe("from ssh");
expect("hello" in unnamed).toBe(false);
revokeService("testplugin.greet", ssh);
expect(() => handle.hello()).toThrow(PluginServiceUnavailableError);
});
it("satisfies a requirement with any compatible provider", () => {
register({ name: "rdp", version: "2.0.0" });
register({ name: "ssh", version: "1.2.0" });
expect(
resolveRequirements(
manifestRequiring([
{ service: "testplugin.greet", versionRange: "^1.0.0" },
]),
).satisfied,
).toBe(true);
});
});
describe("resolution", () => {
it("is satisfied when the version is in range", () => {
register({ version: "1.2.0" });