Add typed hook system for plugins

Replace invokeHook and the per plugin on* methods with AsyncHook
collections on the controller, host, instance, clusterioctl and web
control objects. Plugin entrypoints export a default function that
receives a load context and attaches handlers to the hooks it needs.
Class exports still load through fromContext with a deprecation
warning. The create templates and docs use the entrypoint function.

This is clusterio/clusterio#921 recreated on top of master after the
ESM migration and the PluginDeclaration permissions change.

Closes #730

Co-Authored-By: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
committed 2026-09-11 22:49:09 +00:00
1 parent 8c03ac15ab
commit 7122cbccf8
64 files changed
+1878 -1184

No files matched your search

+50 -12
View File
@@ -1,21 +1,59 @@
import assert from "node:assert/strict";
import * as mock from "../mock.js";
import * as controller from "@clusterio/controller";
describe("controller/src/BaseControllerPlugin", function() {
describe("class BaseControllerPlugin", function() {
let controllerPlugin;
it("should be constructible", async function() {
controllerPlugin = new controller.BaseControllerPlugin({}, {}, {}, new mock.MockLogger());
await controllerPlugin.init();
const info = { name: "test" };
let mockController;
beforeEach(function() {
mockController = new mock.MockController();
});
it("should define defaults for hooks", async function() {
await controllerPlugin.onInstanceStatusChanged({}, "running", "initialized");
await controllerPlugin.onControllerConfigFieldChanged({}, "foo", null);
await controllerPlugin.onInstanceConfigFieldChanged({}, {}, "foo", null);
await controllerPlugin.onMetrics();
await controllerPlugin.onShutdown();
controllerPlugin.onHostConnectionEvent({}, "connect");
await controllerPlugin.onPrepareHostDisconnect({});
it("should be constructible", async function() {
const plugin = new controller.BaseControllerPlugin(info, mockController, {}, new mock.MockLogger());
await plugin.init();
assert.equal(mockController.hooks.size, 0);
});
it("should be constructible from a load context", async function() {
const logger = new mock.MockLogger();
const plugin = controller.BaseControllerPlugin.fromContext({
plugin: info, controller: mockController, metrics: {}, logger,
});
assert.equal(plugin.info, info);
assert.equal(plugin.controller, mockController);
assert.equal(plugin.logger, logger);
});
it("should attach overridden hooks", async function() {
let calls = [];
class ControllerPlugin extends controller.BaseControllerPlugin {
async onSaveData() { calls.push(["save", this]); }
async onInstanceStatusChanged(instance, prev) { calls.push(["status", instance, prev]); }
onHostConnectionEvent(connection, event) { calls.push(["host", connection, event]); }
}
const plugin = new ControllerPlugin(info, mockController, {}, new mock.MockLogger());
assert.deepEqual([...mockController.hooks.attached], ["test"]);
assert.equal(mockController.hooks.shutdown.size, 0);
await mockController.hooks.save.invoke();
await mockController.hooks.instanceStatusChanged.invoke("instance", "running");
await mockController.hooks.hostConnectionEvent.invoke("connection", "connect");
assert.deepEqual(calls, [
["save", plugin],
["status", "instance", "running"],
["host", "connection", "connect"],
]);
});
it("should detach hooks", async function() {
class ControllerPlugin extends controller.BaseControllerPlugin {
async onSaveData() { }
async onShutdown() { }
}
const plugin = new ControllerPlugin(info, mockController, {}, new mock.MockLogger());
assert.equal(mockController.hooks.size, 1);
plugin.detachHooks();
assert.equal(mockController.hooks.size, 0);
});
});
});
+2 -1
View File
@@ -85,7 +85,6 @@ describe("controller/src/routes", function() {
let endpoint;
beforeEach(function() {
endpoint = `http://localhost:${port}/api/plugins`;
controller.plugins = new Map([["foo", {}]]);
controller.mockConfigEntries.set("foo.load_plugin", true);
});
it("should strip static/ from the plugin bundle path", async function() {
@@ -93,6 +92,7 @@ describe("controller/src/routes", function() {
name: "foo", version: "1.0.0", npmPackage: "foo",
manifest: { "foo.js": "static/foo.abc.js" },
}];
controller.loadedPlugins = new Set(controller.pluginInfos);
let response = await fetch(endpoint);
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), [{
@@ -106,6 +106,7 @@ describe("controller/src/routes", function() {
{ name: "bar", version: "1.0.0", npmPackage: "bar", manifest: {} },
{ name: "baz", version: "1.0.0", npmPackage: "baz", manifest: { "baz.js": "remoteEntry.js" } },
];
controller.loadedPlugins = new Set([controller.pluginInfos[0]]);
let response = await fetch(endpoint);
assert.equal(response.status, 200);
let data = await response.json();
+11 -16
View File
@@ -1,21 +1,16 @@
import { BaseHostPlugin } from "@clusterio/host";
import { HostEcho, HostEchoReceived } from "./messages.js";
export class HostPlugin extends BaseHostPlugin {
async init() {
this.logger.info("test_plugin host loaded");
this.receivedEchoes = new Set();
this.host.handle(HostEcho, this.handleHostEcho.bind(this));
this.host.handle(HostEchoReceived, this.handleHostEchoReceived.bind(this));
}
// Uses the default export entrypoint, the controller and ctl parts of this
// plugin use the deprecated class export so both paths are exercised.
export default async function(context) {
const { host, logger } = context;
logger.info("test_plugin host loaded");
const receivedEchoes = new Set();
async handleHostEcho(event) {
this.logger.info(`test_plugin host echo ${event.text}`);
this.receivedEchoes.add(event.text);
}
host.handle(HostEcho, async (event) => {
logger.info(`test_plugin host echo ${event.text}`);
receivedEchoes.add(event.text);
});
async handleHostEchoReceived(request) {
return this.receivedEchoes.has(request.text);
}
host.handle(HostEchoReceived, async (request) => receivedEchoes.has(request.text));
}
+6 -5
View File
@@ -1,7 +1,8 @@
import { BaseInstancePlugin } from "@clusterio/host";
export default async function(context) {
const { instance, plugin, logger } = context;
logger.info("test_plugin instance loaded");
export class InstancePlugin extends BaseInstancePlugin {
async init() {
this.logger.info("test_plugin instance loaded");
}
instance.hooks.start.attach(plugin.name, async () => {
logger.info("test_plugin instance started");
});
}
+40 -10
View File
@@ -1,20 +1,50 @@
import assert from "node:assert/strict";
import * as mock from "../mock.js";
import * as host from "@clusterio/host";
describe("host/src/BaseHostPlugin", function() {
describe("class BaseHostPlugin", function() {
let hostPlugin;
it("should be constructible", async function() {
hostPlugin = new host.BaseHostPlugin({}, new mock.MockHost(), new mock.MockLogger());
await hostPlugin.init();
const info = { name: "test" };
let mockHost;
beforeEach(function() {
mockHost = new mock.MockHost();
});
it("should define defaults for hooks", async function() {
await hostPlugin.onHostConfigFieldChanged();
await hostPlugin.onMetrics();
await hostPlugin.onShutdown();
hostPlugin.onControllerConnectionEvent("connect");
await hostPlugin.onPrepareControllerDisconnect();
it("should be constructible", async function() {
const plugin = new host.BaseHostPlugin(info, mockHost, new mock.MockLogger());
await plugin.init();
assert.equal(mockHost.hooks.size, 0);
});
it("should be constructible from a load context", async function() {
const logger = new mock.MockLogger();
const plugin = host.BaseHostPlugin.fromContext({ plugin: info, host: mockHost, logger });
assert.equal(plugin.info, info);
assert.equal(plugin.host, mockHost);
assert.equal(plugin.logger, logger);
});
it("should attach overridden hooks", async function() {
let calls = [];
class HostPlugin extends host.BaseHostPlugin {
async onHostConfigFieldChanged(field, curr, prev) { calls.push(["config", field, curr, prev]); }
async onShutdown() { calls.push(["shutdown", this]); }
}
const plugin = new HostPlugin(info, mockHost, new mock.MockLogger());
assert.deepEqual([...mockHost.hooks.attached], ["test"]);
assert.equal(mockHost.hooks.metrics.size, 0);
await mockHost.hooks.hostConfigFieldChanged.invoke("host.name", "new", "old");
await mockHost.hooks.shutdown.invoke();
assert.deepEqual(calls, [["config", "host.name", "new", "old"], ["shutdown", plugin]]);
});
it("should detach hooks", async function() {
class HostPlugin extends host.BaseHostPlugin {
async onShutdown() { }
}
const plugin = new HostPlugin(info, mockHost, new mock.MockLogger());
assert.equal(mockHost.hooks.size, 1);
plugin.detachHooks();
assert.equal(mockHost.hooks.size, 0);
});
});
});
+41 -9
View File
@@ -5,19 +5,51 @@ import * as host from "@clusterio/host";
describe("host/src/BaseInstancePlugin", function() {
describe("class BaseInstancePlugin", function() {
const info = { name: "test" };
let instancePlugin;
it("should be constructible", async function() {
instancePlugin = new host.BaseInstancePlugin({}, new mock.MockInstance(), {});
instancePlugin = new host.BaseInstancePlugin(
info, new mock.MockInstance(), new mock.MockHost(), new mock.MockLogger()
);
await instancePlugin.init();
assert.equal(instancePlugin.instance.hooks.size, 0);
});
it("should define defaults for hooks", async function() {
await instancePlugin.onMetrics();
await instancePlugin.onStart();
await instancePlugin.onStop();
instancePlugin.onExit();
await instancePlugin.onOutput({});
instancePlugin.onControllerConnectionEvent("connect");
await instancePlugin.onPrepareControllerDisconnect();
it("should be constructible from a load context", async function() {
const instance = new mock.MockInstance();
const mockHost = new mock.MockHost();
const logger = new mock.MockLogger();
const plugin = host.BaseInstancePlugin.fromContext({ plugin: info, instance, host: mockHost, logger });
assert.equal(plugin.info, info);
assert.equal(plugin.instance, instance);
assert.equal(plugin.host, mockHost);
assert.equal(plugin.logger, logger);
});
it("should attach overridden hooks", async function() {
let calls = [];
class InstancePlugin extends host.BaseInstancePlugin {
async onStart() { calls.push(["start", this]); }
onExit() { calls.push(["exit"]); }
async onOutput(parsed, line) { calls.push(["output", parsed, line]); }
}
const instance = new mock.MockInstance();
const plugin = new InstancePlugin(info, instance, new mock.MockHost(), new mock.MockLogger());
assert.deepEqual([...instance.hooks.attached], ["test"]);
assert.equal(instance.hooks.stop.size, 0);
await instance.hooks.start.invoke();
await instance.hooks.exit.invoke();
await instance.hooks.output.invoke({ type: "generic" }, "line");
assert.deepEqual(calls, [["start", plugin], ["exit"], ["output", { type: "generic" }, "line"]]);
});
it("should detach hooks", async function() {
class InstancePlugin extends host.BaseInstancePlugin {
async onStart() { }
}
const instance = new mock.MockInstance();
const plugin = new InstancePlugin(info, instance, new mock.MockHost(), new mock.MockLogger());
assert.equal(instance.hooks.size, 1);
plugin.detachHooks();
assert.equal(instance.hooks.size, 0);
});
describe("sendRcon", function() {
it("should send commands out of order", async function() {
+2 -2
View File
@@ -235,7 +235,7 @@ export async function execCtl(command) {
inExecCtl = true;
process.chdir("temp/test");
try {
const initArgs = await initializeCtl(command, control.plugins, true);
const initArgs = await initializeCtl(command, control.hooks, true);
if (!initArgs.shouldRun) {
return;
}
@@ -394,7 +394,7 @@ before(async function() {
const initArgs = await initializeCtl(`--plugin-list ${pluginListPath} control-config list`, undefined, true);
control.config = initArgs.controlConfig;
control.plugins = initArgs.ctlPlugins;
control.hooks = initArgs.ctlHooks;
const testPack = lib.ModPack.fromJSON({});
testPack.id = 12;
+132
View File
@@ -0,0 +1,132 @@
import assert from "node:assert/strict";
import * as mock from "../mock.js";
import * as lib from "@clusterio/lib";
describe("lib/AsyncHook", function() {
describe("class AsyncHook", function() {
let logger;
let hook;
beforeEach(function() {
logger = new mock.MockLogger();
logger.errors = [];
logger.error = msg => logger.errors.push(msg);
hook = new lib.AsyncHook(logger);
});
it("should track attached handlers", function() {
assert.equal(hook.size, 0);
hook.attach("alpha", () => {});
hook.attach("beta", () => {});
assert.equal(hook.size, 2);
assert.deepEqual([...hook.attached], ["alpha", "beta"]);
});
it("should reject duplicate handler names", function() {
hook.attach("alpha", () => {});
assert.throws(
() => hook.attach("alpha", () => {}),
new Error("Handler with name alpha is already attached")
);
});
it("should detach handlers", async function() {
let called = false;
hook.attach("alpha", () => { called = true; });
hook.detach("alpha");
hook.detach("missing");
await hook.invoke();
assert.equal(called, false);
assert.equal(hook.size, 0);
});
it("should clear all handlers", function() {
hook.attach("alpha", () => {});
hook.attach("beta", () => {});
hook.clear();
assert.equal(hook.size, 0);
});
it("should invoke all handlers with the given arguments", async function() {
let calls = [];
hook.attach("alpha", (a, b) => { calls.push(["alpha", a, b]); });
hook.attach("beta", async (a, b) => { calls.push(["beta", a, b]); });
await hook.invoke(1, "two");
assert.deepEqual(calls, [["alpha", 1, "two"], ["beta", 1, "two"]]);
});
it("should log and ignore errors thrown by handlers", async function() {
let betaCalled = false;
hook.attach("alpha", () => { throw new Error("sync"); });
hook.attach("beta", async () => { betaCalled = true; });
hook.attach("gamma", async () => { throw new Error("async"); });
await hook.invoke();
assert(betaCalled, "beta was not called");
assert.equal(logger.errors.length, 2);
assert(logger.errors[0].startsWith('Ignoring error in hook "alpha":'), logger.errors[0]);
assert(logger.errors[1].startsWith('Ignoring error in hook "gamma":'), logger.errors[1]);
});
it("should time out handlers that never resolve", async function() {
hook = new lib.AsyncHook(logger, 10);
hook.attach("stuck", () => new Promise(() => {}));
hook.attach("fast", () => 1);
assert.deepEqual(await hook.collect(), [1]);
assert.equal(logger.errors.length, 1);
assert(logger.errors[0].includes("Hook stuck timed out after 10ms"), logger.errors[0]);
});
it("should collect results from handlers", async function() {
hook.attach("alpha", arg => arg);
hook.attach("beta", () => undefined);
hook.attach("gamma", async arg => arg * 2);
hook.attach("delta", () => { throw new Error("Test"); });
assert.deepEqual(await hook.collect(21), [21, 42]);
});
it("should collect results with their source", async function() {
hook.attach("alpha", arg => arg);
hook.attach("beta", () => undefined);
hook.attach("gamma", async arg => arg * 2);
assert.deepEqual(await hook.collectEntries(21), [["alpha", 21], ["gamma", 42]]);
});
it("should provide a stable listener bound to the hook", async function() {
let calls = [];
hook.attach("alpha", arg => { calls.push(arg); });
const { listener } = hook;
assert.equal(listener, hook.listener);
await listener("value");
assert.deepEqual(calls, ["value"]);
});
});
describe("class AsyncHookCollection", function() {
class TestHooks extends lib.AsyncHookCollection {
constructor(logger) {
super(logger);
this.first = this.newHook();
this.second = this.newHook();
}
}
let hooks;
beforeEach(function() {
hooks = new TestHooks(new mock.MockLogger());
});
it("should count each name once across hooks", function() {
hooks.first.attach("alpha", () => {});
hooks.second.attach("alpha", () => {});
hooks.second.attach("beta", () => {});
assert.equal(hooks.size, 2);
assert.deepEqual([...hooks.attached], ["alpha", "beta"]);
});
it("should detach a name from all hooks", function() {
hooks.first.attach("alpha", () => {});
hooks.second.attach("alpha", () => {});
hooks.second.attach("beta", () => {});
hooks.detachAll("alpha");
assert.equal(hooks.first.size, 0);
assert.deepEqual([...hooks.second.attached], ["beta"]);
});
it("should clear all hooks", function() {
hooks.first.attach("alpha", () => {});
hooks.second.attach("beta", () => {});
hooks.clearAll();
assert.equal(hooks.size, 0);
});
});
});
-36
View File
@@ -1,36 +0,0 @@
import assert from "node:assert/strict";
import * as mock from "../mock.js";
import * as lib from "@clusterio/lib";
describe("lib/plugin", function() {
describe("invokeHook()", function() {
let betaTestCalled = false;
let plugins = new Map([
["alpha", {
test: async function() { },
pass: async function(arg) { return arg; },
error: async function() { throw new Error("Test"); },
logger: new mock.MockLogger(),
}],
["beta", {
test: async function() { betaTestCalled = true; },
pass: async function() { },
error: async function() { },
logger: new mock.MockLogger(),
}],
]);
it("should invoke the hook on the plugin", async function() {
await lib.invokeHook(plugins, "test");
assert(betaTestCalled, "Hook was not called");
});
it("should pass and return args", async function() {
let result = await lib.invokeHook(plugins, "pass", 1234);
assert.deepEqual(result, [1234]);
});
it("should ignore errors", async function() {
await lib.invokeHook(plugins, "error");
});
});
});
+89 -35
View File
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
import * as mock from "../mock.js";
import * as lib from "@clusterio/lib";
import { BaseControllerPlugin } from "@clusterio/controller";
@@ -72,51 +73,104 @@ describe("lib/plugin_loader", function() {
);
});
});
describe("loadPluginClass()", function() {
let baseDir = path.join("temp", "test", "plugin");
let missingClass = path.join(baseDir, "missing_class_plugin");
let wrongParentClass = path.join(baseDir, "wrong_parent_class_plugin");
describe("loadPlugin()", function() {
const baseDir = path.join("temp", "test", "plugin");
const functionPlugin = path.resolve(baseDir, "function_plugin");
const classPlugin = path.resolve(baseDir, "class_plugin");
const throwingClassPlugin = path.resolve(baseDir, "throwing_class_plugin");
const missingClassPlugin = path.resolve(baseDir, "missing_class_plugin");
const wrongParentClassPlugin = path.resolve(baseDir, "wrong_parent_class_plugin");
let controller;
let logger;
let context;
before(async function() {
async function writeEntrypoint(pluginPath, content) {
await fs.mkdir(pluginPath, { recursive: true });
await fs.writeFile(path.join(pluginPath, "controller.js"), content);
}
await writeEntrypoint(missingClass, "");
await writeEntrypoint(
wrongParentClass, "export class ControllerPlugin {};\n"
await writeEntrypoint(functionPlugin, `
export default async function(context) {
context.controller.loadedWith = context;
}
`);
await writeEntrypoint(classPlugin, `
import { BaseControllerPlugin } from "@clusterio/controller";
export class ControllerPlugin extends BaseControllerPlugin {
async init() { this.controller.loadedWith = this; }
async onSaveData() { }
}
`);
await writeEntrypoint(throwingClassPlugin, `
import { BaseControllerPlugin } from "@clusterio/controller";
export class ControllerPlugin extends BaseControllerPlugin {
async init() { throw new Error("init failed"); }
async onSaveData() { }
}
`);
await writeEntrypoint(missingClassPlugin, "");
await writeEntrypoint(wrongParentClassPlugin, "export class ControllerPlugin {};\n");
});
beforeEach(function() {
controller = new mock.MockController();
logger = new mock.MockLogger();
logger.warnings = [];
logger.warn = msg => logger.warnings.push(msg);
context = { controller, metrics: {}, logger };
});
function info(requirePath, entrypoint = "controller.js") {
return { name: "test", requirePath, controllerEntrypoint: entrypoint };
}
it("should do nothing when the entrypoint is not set", async function() {
await lib.loadPlugin(
{ name: "test", requirePath: missingClassPlugin }, "controller", context,
"ControllerPlugin", BaseControllerPlugin,
);
assert.equal(controller.loadedWith, undefined);
});
it("should call the default export with the load context", async function() {
const pluginInfo = info(functionPlugin);
await lib.loadPlugin(pluginInfo, "controller", context, "ControllerPlugin", BaseControllerPlugin);
assert.equal(controller.loadedWith.controller, controller);
assert.equal(controller.loadedWith.plugin, pluginInfo);
assert.equal(controller.loadedWith.logger, logger);
assert.deepEqual(logger.warnings, []);
});
it("should load a deprecated class export", async function() {
const pluginInfo = info(classPlugin);
await lib.loadPlugin(pluginInfo, "controller", context, "ControllerPlugin", BaseControllerPlugin);
assert(controller.loadedWith instanceof BaseControllerPlugin);
assert.equal(controller.loadedWith.info, pluginInfo);
assert.deepEqual([...controller.hooks.save.attached], ["test"]);
assert.deepEqual(logger.warnings, ["Plugin test is using deprecated class export"]);
});
it("should detach hooks if init throws", async function() {
await assert.rejects(
lib.loadPlugin(
info(throwingClassPlugin), "controller", context, "ControllerPlugin", BaseControllerPlugin
),
new Error("init failed")
);
assert.equal(controller.hooks.size, 0);
});
it("should throw if neither a function nor a class is exported", async function() {
await assert.rejects(
lib.loadPlugin(
info(missingClassPlugin), "controller", context, "ControllerPlugin", BaseControllerPlugin
),
new Error("Plugin test must export either a default function or ControllerPlugin class")
);
});
it("should throw if class is missing from entrypoint", async function() {
const requirePath = path.resolve(missingClass);
it("should throw if the class is not a subclass of the base class", async function() {
await assert.rejects(
lib.loadPluginClass(
"test",
path.posix.join(requirePath, "controller.js"),
"ControllerPlugin",
BaseControllerPlugin,
lib.loadPlugin(
info(wrongParentClassPlugin), "controller", context, "ControllerPlugin", BaseControllerPlugin
),
{
message:
`PluginError: Expected ${path.posix.join(requirePath, "controller.js")} ` +
"to export a class named ControllerPlugin",
}
);
});
it("should throw if class is not a subclass of BaseControllerPlugin", async function() {
const requirePath = path.resolve(wrongParentClass);
await assert.rejects(
lib.loadPluginClass(
"test",
path.posix.join(requirePath, "controller.js"),
"ControllerPlugin",
BaseControllerPlugin,
),
{
message:
"PluginError: Expected ControllerPlugin exported from " +
`${path.posix.join(requirePath, "controller.js")} to be a subclass of BaseControllerPlugin`,
}
new Error("Expected ControllerPlugin exported from test to extend BaseControllerPlugin")
);
});
});
+2 -2
View File
@@ -74,7 +74,7 @@ describe("messages/plugin", function() {
description: "Description", npmPackage: "Npm Package",
}];
controller.plugins.set("Name", {});
controller.loadedPlugins.add(controller.pluginInfos[0]);
controller.config.set("Name.load_plugin", true);
const response = await controlConnection.handlePluginListRequest(new lib.PluginListRequest());
@@ -89,7 +89,7 @@ describe("messages/plugin", function() {
description: "Description", npmPackage: "Npm Package",
}];
host.plugins.set("Name", {});
host.loadedPlugins.add(host.pluginInfos[0]);
host.config.set("Name.load_plugin", true);
const response = await host.handlePluginListRequest(new lib.PluginListRequest());
+9 -2
View File
@@ -3,7 +3,8 @@ import http from "node:http";
import express from "express";
import * as lib from "@clusterio/lib";
import { User, UserManager, UserRecord, InstanceManager } from "@clusterio/controller";
import { User, UserManager, UserRecord, InstanceManager, ControllerHooks } from "@clusterio/controller";
import { HostHooks, InstanceHooks } from "@clusterio/host";
const addr = lib.Address.fromShorthand;
@@ -123,6 +124,8 @@ export class MockInstance extends lib.Link {
this.name = "test";
this.id = 7357;
this.status = "running";
this.hooks = new InstanceHooks(this.logger);
this.loadedPlugins = new Set();
this.mockConfigEntries = new Map([
["instance.id", 7357],
["factorio.enable_save_patching", true],
@@ -146,6 +149,8 @@ export class MockInstance extends lib.Link {
export class MockHost extends lib.Link {
constructor() {
super(new MockConnector(addr({ hostId: 1 }), addr("controller")));
this.hooks = new HostHooks(new MockLogger());
this.loadedPlugins = new Set();
}
}
@@ -192,6 +197,8 @@ export class MockController {
this.hosts = new lib.KeyValueDatastore();
this.handles = new Map();
this.hooks = new ControllerHooks(new MockLogger());
this.loadedPlugins = new Set();
}
get authSecret() {
@@ -235,7 +242,7 @@ export async function createControllerPlugin(ControllerPluginClass, info) {
export async function createInstancePlugin(InstancePluginClass, info) {
let instance = new MockInstance();
let host = new MockHost();
let plugin = new InstancePluginClass(info, instance, host);
let plugin = new InstancePluginClass(info, instance, host, new MockLogger());
await plugin.init();
return plugin;
}