refactor: eliminate jscpd clones and boost tests

This commit is contained in:
Peter Steinberger
2026-02-19 14:59:36 +00:00
parent 71983716ff
commit dcd592a601
16 changed files with 408 additions and 235 deletions

View File

@@ -1,30 +1,12 @@
import fs from "node:fs";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { peekSystemEvents, resetSystemEventsForTest } from "../infra/system-events.js";
import { getFinishedSession, resetProcessRegistryForTests } from "./bash-process-registry.js";
import { createExecTool, createProcessTool, execTool, processTool } from "./bash-tools.js";
import { buildDockerExecArgs } from "./bash-tools.shared.js";
import { sanitizeBinaryOutput } from "./shell-utils.js";
import { resolveShellFromPath, sanitizeBinaryOutput } from "./shell-utils.js";
const isWin = process.platform === "win32";
const resolveShellFromPath = (name: string) => {
const envPath = process.env.PATH ?? "";
if (!envPath) {
return undefined;
}
const entries = envPath.split(path.delimiter).filter(Boolean);
for (const entry of entries) {
const candidate = path.join(entry, name);
try {
fs.accessSync(candidate, fs.constants.X_OK);
return candidate;
} catch {
// ignore missing or non-executable entries
}
}
return undefined;
};
const defaultShell = isWin
? undefined
: process.env.OPENCLAW_TEST_SHELL || resolveShellFromPath("bash") || process.env.SHELL || "sh";

View File

@@ -49,7 +49,7 @@ export function getShellConfig(): { shell: string; args: string[] } {
return { shell, args: ["-c"] };
}
function resolveShellFromPath(name: string): string | undefined {
export function resolveShellFromPath(name: string): string | undefined {
const envPath = process.env.PATH ?? "";
if (!envPath) {
return undefined;

View File

@@ -38,6 +38,20 @@ describe("cdp", () => {
return wsPort;
};
const startVersionHttpServer = async (versionBody: Record<string, unknown>) => {
httpServer = createServer((req, res) => {
if (req.url === "/json/version") {
res.setHeader("content-type", "application/json");
res.end(JSON.stringify(versionBody));
return;
}
res.statusCode = 404;
res.end("not found");
});
await new Promise<void>((resolve) => httpServer?.listen(0, "127.0.0.1", resolve));
return (httpServer.address() as { port: number }).port;
};
afterEach(async () => {
await new Promise<void>((resolve) => {
if (!httpServer) {
@@ -68,23 +82,10 @@ describe("cdp", () => {
);
});
httpServer = createServer((req, res) => {
if (req.url === "/json/version") {
res.setHeader("content-type", "application/json");
res.end(
JSON.stringify({
webSocketDebuggerUrl: `ws://127.0.0.1:${wsPort}/devtools/browser/TEST`,
}),
);
return;
}
res.statusCode = 404;
res.end("not found");
const httpPort = await startVersionHttpServer({
webSocketDebuggerUrl: `ws://127.0.0.1:${wsPort}/devtools/browser/TEST`,
});
await new Promise<void>((resolve) => httpServer?.listen(0, "127.0.0.1", resolve));
const httpPort = (httpServer.address() as { port: number }).port;
const created = await createTargetViaCdp({
cdpUrl: `http://127.0.0.1:${httpPort}`,
url: "https://example.com",
@@ -122,23 +123,10 @@ describe("cdp", () => {
);
});
httpServer = createServer((req, res) => {
if (req.url === "/json/version") {
res.setHeader("content-type", "application/json");
res.end(
JSON.stringify({
webSocketDebuggerUrl: `ws://127.0.0.1:${wsPort}/devtools/browser/TEST`,
}),
);
return;
}
res.statusCode = 404;
res.end("not found");
const httpPort = await startVersionHttpServer({
webSocketDebuggerUrl: `ws://127.0.0.1:${wsPort}/devtools/browser/TEST`,
});
await new Promise<void>((resolve) => httpServer?.listen(0, "127.0.0.1", resolve));
const httpPort = (httpServer.address() as { port: number }).port;
const created = await createTargetViaCdp({
cdpUrl: `http://127.0.0.1:${httpPort}`,
url: "http://127.0.0.1:8080",
@@ -174,6 +162,16 @@ describe("cdp", () => {
expect(res.result.value).toBe(2);
});
it("fails when /json/version omits webSocketDebuggerUrl", async () => {
const httpPort = await startVersionHttpServer({});
await expect(
createTargetViaCdp({
cdpUrl: `http://127.0.0.1:${httpPort}`,
url: "https://example.com",
}),
).rejects.toThrow("CDP /json/version missing webSocketDebuggerUrl");
});
it("captures an aria snapshot via CDP", async () => {
const wsPort = await startWsServerWithMessages((msg, socket) => {
if (msg.method === "Accessibility.enable") {

View File

@@ -39,6 +39,14 @@ export function getBrowserControlServerBaseUrl(): string {
return `http://127.0.0.1:${state.testPort}`;
}
export function restoreGatewayPortEnv(prevGatewayPort: string | undefined): void {
if (prevGatewayPort === undefined) {
delete process.env.OPENCLAW_GATEWAY_PORT;
return;
}
process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort;
}
export function setBrowserControlServerCreateTargetId(targetId: string | null): void {
state.createTargetId = targetId;
}
@@ -332,11 +340,7 @@ export function installBrowserControlServerHooks() {
afterEach(async () => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
if (state.prevGatewayPort === undefined) {
delete process.env.OPENCLAW_GATEWAY_PORT;
} else {
process.env.OPENCLAW_GATEWAY_PORT = state.prevGatewayPort;
}
restoreGatewayPortEnv(state.prevGatewayPort);
if (state.prevGatewayToken === undefined) {
delete process.env.OPENCLAW_GATEWAY_TOKEN;
} else {

View File

@@ -8,6 +8,7 @@ import {
installBrowserControlServerHooks,
makeResponse,
getPwMocks,
restoreGatewayPortEnv,
startBrowserControlServerFromConfig,
stopBrowserControlServer,
} from "./server.control-server.test-harness.js";
@@ -80,11 +81,7 @@ describe("profile CRUD endpoints", () => {
afterEach(async () => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
if (state.prevGatewayPort === undefined) {
delete process.env.OPENCLAW_GATEWAY_PORT;
} else {
process.env.OPENCLAW_GATEWAY_PORT = state.prevGatewayPort;
}
restoreGatewayPortEnv(state.prevGatewayPort);
await stopBrowserControlServer();
});

View File

@@ -3,6 +3,7 @@ import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
import type { ChannelPlugin } from "../channels/plugins/types.js";
import { createChannelTestPluginBase } from "../test-utils/channel-plugins.js";
import { setRegistry } from "./server.agent.gateway-server-agent.mocks.js";
import { createRegistry } from "./server.e2e-registry-helpers.js";
import {
@@ -95,22 +96,15 @@ const createStubChannelPlugin = (params: {
label: string;
resolveAllowFrom?: (cfg: Record<string, unknown>) => string[];
}): ChannelPlugin => ({
id: params.id,
meta: {
...createChannelTestPluginBase({
id: params.id,
label: params.label,
selectionLabel: params.label,
docsPath: `/channels/${params.id}`,
blurb: "test stub.",
},
capabilities: { chatTypes: ["direct"] },
config: {
listAccountIds: () => ["default"],
resolveAccount: () => ({}),
resolveAllowFrom: params.resolveAllowFrom
? ({ cfg }) => params.resolveAllowFrom?.(cfg as Record<string, unknown>) ?? []
: undefined,
},
config: {
resolveAllowFrom: params.resolveAllowFrom
? ({ cfg }) => params.resolveAllowFrom?.(cfg as Record<string, unknown>) ?? []
: undefined,
},
}),
outbound: {
deliveryMode: "direct",
resolveTarget: ({ to, allowFrom }) => {

View File

@@ -7,11 +7,11 @@ import { whatsappPlugin } from "../../extensions/whatsapp/src/channel.js";
import { BARE_SESSION_RESET_PROMPT } from "../auto-reply/reply/session-reset-prompt.js";
import type { ChannelPlugin } from "../channels/plugins/types.js";
import { emitAgentEvent, registerAgentRunContext } from "../infra/agent-events.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
import { setRegistry } from "./server.agent.gateway-server-agent.mocks.js";
import { createRegistry } from "./server.e2e-registry-helpers.js";
import {
agentCommand,
connectWebchatClient,
connectOk,
installGatewayTestHooks,
onceMessage,
@@ -367,18 +367,7 @@ describe("gateway server agent", () => {
test("agent events stream to webchat clients when run context is registered", async () => {
await writeMainSessionEntry({ sessionId: "sess-main" });
const webchatWs = new WebSocket(`ws://127.0.0.1:${port}`, {
headers: { origin: `http://127.0.0.1:${port}` },
});
await new Promise<void>((resolve) => webchatWs.once("open", resolve));
await connectOk(webchatWs, {
client: {
id: GATEWAY_CLIENT_NAMES.WEBCHAT,
version: "1.0.0",
platform: "test",
mode: GATEWAY_CLIENT_MODES.WEBCHAT,
},
});
const webchatWs = await connectWebchatClient({ port });
registerAgentRunContext("run-auto-1", { sessionKey: "main" });

View File

@@ -83,6 +83,16 @@ function scopedCanvasPath(capability: string, path: string): string {
return `${CANVAS_CAPABILITY_PATH_PREFIX}/${encodeURIComponent(capability)}${path}`;
}
const allowCanvasHostHttp: CanvasHostHandler["handleHttpRequest"] = async (req, res) => {
const url = new URL(req.url ?? "/", "http://localhost");
if (url.pathname !== CANVAS_HOST_PATH && !url.pathname.startsWith(`${CANVAS_HOST_PATH}/`)) {
return false;
}
res.statusCode = 200;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("ok");
return true;
};
async function withCanvasGatewayHarness(params: {
resolvedAuth: ResolvedGatewayAuth;
listenHost?: string;
@@ -162,19 +172,7 @@ describe("gateway canvas host auth", () => {
run: async () => {
await withCanvasGatewayHarness({
resolvedAuth,
handleHttpRequest: async (req, res) => {
const url = new URL(req.url ?? "/", "http://localhost");
if (
url.pathname !== CANVAS_HOST_PATH &&
!url.pathname.startsWith(`${CANVAS_HOST_PATH}/`)
) {
return false;
}
res.statusCode = 200;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("ok");
return true;
},
handleHttpRequest: allowCanvasHostHttp,
run: async ({ listener, clients }) => {
const host = "127.0.0.1";
const operatorOnlyCapability = "operator-only";
@@ -287,19 +285,7 @@ describe("gateway canvas host auth", () => {
run: async () => {
await withCanvasGatewayHarness({
resolvedAuth,
handleHttpRequest: async (req, res) => {
const url = new URL(req.url ?? "/", "http://localhost");
if (
url.pathname !== CANVAS_HOST_PATH &&
!url.pathname.startsWith(`${CANVAS_HOST_PATH}/`)
) {
return false;
}
res.statusCode = 200;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("ok");
return true;
},
handleHttpRequest: allowCanvasHostHttp,
run: async ({ listener, clients }) => {
clients.add(
makeWsClient({

View File

@@ -2,6 +2,7 @@ import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
import type { ChannelPlugin } from "../channels/plugins/types.js";
import type { PluginRegistry } from "../plugins/registry.js";
import { setActivePluginRegistry } from "../plugins/runtime.js";
import { createChannelTestPluginBase } from "../test-utils/channel-plugins.js";
import { createRegistry } from "./server.e2e-registry-helpers.js";
import {
connectOk,
@@ -48,20 +49,11 @@ const createStubChannelPlugin = (params: {
summary?: Record<string, unknown>;
logoutCleared?: boolean;
}): ChannelPlugin => ({
id: params.id,
meta: {
...createChannelTestPluginBase({
id: params.id,
label: params.label,
selectionLabel: params.label,
docsPath: `/channels/${params.id}`,
blurb: "test stub.",
},
capabilities: { chatTypes: ["direct"] },
config: {
listAccountIds: () => ["default"],
resolveAccount: () => ({}),
isConfigured: async () => false,
},
config: { isConfigured: async () => false },
}),
status: {
buildChannelSummary: async () => ({
configured: false,

View File

@@ -573,6 +573,43 @@ export async function connectOk(ws: WebSocket, opts?: Parameters<typeof connectR
return res.payload as { type: "hello-ok" };
}
export async function connectWebchatClient(params: {
port: number;
origin?: string;
client?: NonNullable<Parameters<typeof connectReq>[1]>["client"];
}): Promise<WebSocket> {
const origin = params.origin ?? `http://127.0.0.1:${params.port}`;
const ws = new WebSocket(`ws://127.0.0.1:${params.port}`, {
headers: { origin },
});
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("timeout waiting for ws open")), 10_000);
const onOpen = () => {
clearTimeout(timer);
ws.off("error", onError);
resolve();
};
const onError = (err: Error) => {
clearTimeout(timer);
ws.off("open", onOpen);
reject(err);
};
ws.once("open", onOpen);
ws.once("error", onError);
});
await connectOk(ws, {
client:
params.client ??
({
id: GATEWAY_CLIENT_NAMES.WEBCHAT,
version: "1.0.0",
platform: "test",
mode: GATEWAY_CLIENT_MODES.WEBCHAT,
} as NonNullable<Parameters<typeof connectReq>[1]>["client"]),
});
return ws;
}
export async function rpcReq<T extends Record<string, unknown>>(
ws: WebSocket,
method: string,

View File

@@ -13,11 +13,10 @@ import { resolveSafeInstallDir, unscopedPackageName } from "../infra/install-saf
import {
type NpmIntegrityDrift,
type NpmSpecResolution,
packNpmSpecToArchive,
resolveArchiveSourcePath,
withTempDir,
} from "../infra/install-source-utils.js";
import { resolveNpmIntegrityDriftWithDefaultMessage } from "../infra/npm-integrity.js";
import { installFromNpmSpecArchive } from "../infra/npm-pack-install.js";
import { validateRegistryNpmSpec } from "../infra/npm-registry-spec.js";
import { isPathInside, isPathInsideWithRealpath } from "../security/scan-paths.js";
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
@@ -415,58 +414,38 @@ export async function installHooksFromNpmSpec(params: {
return { ok: false, error: specError };
}
return await withTempDir("openclaw-hook-pack-", async (tmpDir) => {
logger.info?.(`Downloading ${spec}`);
const packedResult = await packNpmSpecToArchive({
spec,
timeoutMs,
cwd: tmpDir,
});
if (!packedResult.ok) {
return packedResult;
}
const npmResolution: NpmSpecResolution = {
...packedResult.metadata,
resolvedAt: new Date().toISOString(),
};
const driftResult = await resolveNpmIntegrityDriftWithDefaultMessage({
spec,
expectedIntegrity: params.expectedIntegrity,
resolution: npmResolution,
onIntegrityDrift: params.onIntegrityDrift,
warn: (message) => {
logger.warn?.(message);
},
});
const integrityDrift = driftResult.integrityDrift;
if (driftResult.error) {
return {
ok: false,
error: driftResult.error,
};
}
const installResult = await installHooksFromArchive({
archivePath: packedResult.archivePath,
hooksDir: params.hooksDir,
timeoutMs,
logger,
mode,
dryRun,
expectedHookPackId,
});
if (!installResult.ok) {
return installResult;
}
return {
...installResult,
npmResolution,
integrityDrift,
};
logger.info?.(`Downloading ${spec}`);
const flowResult = await installFromNpmSpecArchive({
tempDirPrefix: "openclaw-hook-pack-",
spec,
timeoutMs,
expectedIntegrity: params.expectedIntegrity,
onIntegrityDrift: params.onIntegrityDrift,
warn: (message) => {
logger.warn?.(message);
},
installFromArchive: async ({ archivePath }) =>
await installHooksFromArchive({
archivePath,
hooksDir: params.hooksDir,
timeoutMs,
logger,
mode,
dryRun,
expectedHookPackId,
}),
});
if (!flowResult.ok) {
return flowResult;
}
if (!flowResult.installResult.ok) {
return flowResult.installResult;
}
return {
...flowResult.installResult,
npmResolution: flowResult.npmResolution,
integrityDrift: flowResult.integrityDrift,
};
}
export async function installHooksFromPath(params: {

View File

@@ -0,0 +1,96 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { packNpmSpecToArchive, withTempDir } from "./install-source-utils.js";
import { installFromNpmSpecArchive } from "./npm-pack-install.js";
vi.mock("./install-source-utils.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./install-source-utils.js")>();
return {
...actual,
withTempDir: vi.fn(async (_prefix: string, fn: (tmpDir: string) => Promise<unknown>) => {
return await fn("/tmp/openclaw-npm-pack-install-test");
}),
packNpmSpecToArchive: vi.fn(),
};
});
describe("installFromNpmSpecArchive", () => {
beforeEach(() => {
vi.mocked(packNpmSpecToArchive).mockReset();
vi.mocked(withTempDir).mockClear();
});
it("returns pack errors without invoking installer", async () => {
vi.mocked(packNpmSpecToArchive).mockResolvedValue({ ok: false, error: "pack failed" });
const installFromArchive = vi.fn(async () => ({ ok: true as const }));
const result = await installFromNpmSpecArchive({
tempDirPrefix: "openclaw-test-",
spec: "@openclaw/test@1.0.0",
timeoutMs: 1000,
installFromArchive,
});
expect(result).toEqual({ ok: false, error: "pack failed" });
expect(installFromArchive).not.toHaveBeenCalled();
expect(withTempDir).toHaveBeenCalledWith("openclaw-test-", expect.any(Function));
});
it("returns resolution metadata and installer result on success", async () => {
vi.mocked(packNpmSpecToArchive).mockResolvedValue({
ok: true,
archivePath: "/tmp/openclaw-test.tgz",
metadata: {
name: "@openclaw/test",
version: "1.0.0",
resolvedSpec: "@openclaw/test@1.0.0",
integrity: "sha512-same",
},
});
const installFromArchive = vi.fn(async () => ({ ok: true as const, target: "done" }));
const result = await installFromNpmSpecArchive({
tempDirPrefix: "openclaw-test-",
spec: "@openclaw/test@1.0.0",
timeoutMs: 1000,
expectedIntegrity: "sha512-same",
installFromArchive,
});
expect(result.ok).toBe(true);
if (!result.ok) {
return;
}
expect(result.installResult).toEqual({ ok: true, target: "done" });
expect(result.integrityDrift).toBeUndefined();
expect(result.npmResolution.resolvedSpec).toBe("@openclaw/test@1.0.0");
expect(result.npmResolution.resolvedAt).toBeTruthy();
expect(installFromArchive).toHaveBeenCalledWith({ archivePath: "/tmp/openclaw-test.tgz" });
});
it("aborts when integrity drift callback rejects drift", async () => {
vi.mocked(packNpmSpecToArchive).mockResolvedValue({
ok: true,
archivePath: "/tmp/openclaw-test.tgz",
metadata: {
resolvedSpec: "@openclaw/test@1.0.0",
integrity: "sha512-new",
},
});
const installFromArchive = vi.fn(async () => ({ ok: true as const }));
const result = await installFromNpmSpecArchive({
tempDirPrefix: "openclaw-test-",
spec: "@openclaw/test@1.0.0",
timeoutMs: 1000,
expectedIntegrity: "sha512-old",
onIntegrityDrift: async () => false,
installFromArchive,
});
expect(result).toEqual({
ok: false,
error: "aborted: npm package integrity drift detected for @openclaw/test@1.0.0",
});
expect(installFromArchive).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,73 @@
import {
type NpmIntegrityDrift,
type NpmSpecResolution,
packNpmSpecToArchive,
withTempDir,
} from "./install-source-utils.js";
import {
type NpmIntegrityDriftPayload,
resolveNpmIntegrityDriftWithDefaultMessage,
} from "./npm-integrity.js";
export type NpmSpecArchiveInstallFlowResult<TResult extends { ok: boolean }> =
| {
ok: false;
error: string;
}
| {
ok: true;
installResult: TResult;
npmResolution: NpmSpecResolution;
integrityDrift?: NpmIntegrityDrift;
};
export async function installFromNpmSpecArchive<TResult extends { ok: boolean }>(params: {
tempDirPrefix: string;
spec: string;
timeoutMs: number;
expectedIntegrity?: string;
onIntegrityDrift?: (payload: NpmIntegrityDriftPayload) => boolean | Promise<boolean>;
warn?: (message: string) => void;
installFromArchive: (params: { archivePath: string }) => Promise<TResult>;
}): Promise<NpmSpecArchiveInstallFlowResult<TResult>> {
return await withTempDir(params.tempDirPrefix, async (tmpDir) => {
const packedResult = await packNpmSpecToArchive({
spec: params.spec,
timeoutMs: params.timeoutMs,
cwd: tmpDir,
});
if (!packedResult.ok) {
return packedResult;
}
const npmResolution: NpmSpecResolution = {
...packedResult.metadata,
resolvedAt: new Date().toISOString(),
};
const driftResult = await resolveNpmIntegrityDriftWithDefaultMessage({
spec: params.spec,
expectedIntegrity: params.expectedIntegrity,
resolution: npmResolution,
onIntegrityDrift: params.onIntegrityDrift,
warn: params.warn,
});
if (driftResult.error) {
return {
ok: false,
error: driftResult.error,
};
}
const installResult = await params.installFromArchive({
archivePath: packedResult.archivePath,
});
return {
ok: true,
installResult,
npmResolution,
integrityDrift: driftResult.integrityDrift,
};
});
}

View File

@@ -17,11 +17,10 @@ import {
import {
type NpmIntegrityDrift,
type NpmSpecResolution,
packNpmSpecToArchive,
resolveArchiveSourcePath,
withTempDir,
} from "../infra/install-source-utils.js";
import { resolveNpmIntegrityDriftWithDefaultMessage } from "../infra/npm-integrity.js";
import { installFromNpmSpecArchive } from "../infra/npm-pack-install.js";
import { validateRegistryNpmSpec } from "../infra/npm-registry-spec.js";
import { extensionUsesSkippedScannerPath, isPathInside } from "../security/scan-paths.js";
import * as skillScanner from "../security/skill-scanner.js";
@@ -443,58 +442,38 @@ export async function installPluginFromNpmSpec(params: {
return { ok: false, error: specError };
}
return await withTempDir("openclaw-npm-pack-", async (tmpDir) => {
logger.info?.(`Downloading ${spec}`);
const packedResult = await packNpmSpecToArchive({
spec,
timeoutMs,
cwd: tmpDir,
});
if (!packedResult.ok) {
return packedResult;
}
const npmResolution: NpmSpecResolution = {
...packedResult.metadata,
resolvedAt: new Date().toISOString(),
};
const driftResult = await resolveNpmIntegrityDriftWithDefaultMessage({
spec,
expectedIntegrity: params.expectedIntegrity,
resolution: npmResolution,
onIntegrityDrift: params.onIntegrityDrift,
warn: (message) => {
logger.warn?.(message);
},
});
const integrityDrift = driftResult.integrityDrift;
if (driftResult.error) {
return {
ok: false,
error: driftResult.error,
};
}
const installResult = await installPluginFromArchive({
archivePath: packedResult.archivePath,
extensionsDir: params.extensionsDir,
timeoutMs,
logger,
mode,
dryRun,
expectedPluginId,
});
if (!installResult.ok) {
return installResult;
}
return {
...installResult,
npmResolution,
integrityDrift,
};
logger.info?.(`Downloading ${spec}`);
const flowResult = await installFromNpmSpecArchive({
tempDirPrefix: "openclaw-npm-pack-",
spec,
timeoutMs,
expectedIntegrity: params.expectedIntegrity,
onIntegrityDrift: params.onIntegrityDrift,
warn: (message) => {
logger.warn?.(message);
},
installFromArchive: async ({ archivePath }) =>
await installPluginFromArchive({
archivePath,
extensionsDir: params.extensionsDir,
timeoutMs,
logger,
mode,
dryRun,
expectedPluginId,
}),
});
if (!flowResult.ok) {
return flowResult;
}
if (!flowResult.installResult.ok) {
return flowResult.installResult;
}
return {
...flowResult.installResult,
npmResolution: flowResult.npmResolution,
integrityDrift: flowResult.integrityDrift,
};
}
export async function installPluginFromPath(params: {

View File

@@ -0,0 +1,50 @@
import { describe, expect, it } from "vitest";
import { createChannelTestPluginBase, createOutboundTestPlugin } from "./channel-plugins.js";
describe("createChannelTestPluginBase", () => {
it("builds a plugin base with defaults", () => {
const cfg = {} as never;
const base = createChannelTestPluginBase({ id: "telegram", label: "Telegram" });
expect(base.id).toBe("telegram");
expect(base.meta.label).toBe("Telegram");
expect(base.meta.selectionLabel).toBe("Telegram");
expect(base.meta.docsPath).toBe("/channels/telegram");
expect(base.capabilities.chatTypes).toEqual(["direct"]);
expect(base.config.listAccountIds(cfg)).toEqual(["default"]);
expect(base.config.resolveAccount(cfg)).toEqual({});
});
it("honors config and metadata overrides", async () => {
const cfg = {} as never;
const base = createChannelTestPluginBase({
id: "discord",
label: "Discord Bot",
docsPath: "/custom/discord",
capabilities: { chatTypes: ["group"] },
config: {
listAccountIds: () => ["acct-1"],
isConfigured: async () => true,
},
});
expect(base.meta.docsPath).toBe("/custom/discord");
expect(base.capabilities.chatTypes).toEqual(["group"]);
expect(base.config.listAccountIds(cfg)).toEqual(["acct-1"]);
const account = base.config.resolveAccount(cfg);
await expect(base.config.isConfigured?.(account, cfg)).resolves.toBe(true);
});
});
describe("createOutboundTestPlugin", () => {
it("keeps outbound test plugin account list behavior", () => {
const cfg = {} as never;
const plugin = createOutboundTestPlugin({
id: "signal",
outbound: {
deliveryMode: "direct",
resolveTarget: () => ({ ok: true, to: "target" }),
sendText: async () => ({ channel: "signal", messageId: "m1" }),
},
});
expect(plugin.config.listAccountIds(cfg)).toEqual([]);
});
});

View File

@@ -28,13 +28,13 @@ export const createTestRegistry = (channels: TestChannelRegistration[] = []): Pl
diagnostics: [],
});
export const createOutboundTestPlugin = (params: {
export const createChannelTestPluginBase = (params: {
id: ChannelId;
outbound: ChannelOutboundAdapter;
label?: string;
docsPath?: string;
capabilities?: ChannelCapabilities;
}): ChannelPlugin => ({
config?: Partial<ChannelPlugin["config"]>;
}): Pick<ChannelPlugin, "id" | "meta" | "capabilities" | "config"> => ({
id: params.id,
meta: {
id: params.id,
@@ -45,8 +45,25 @@ export const createOutboundTestPlugin = (params: {
},
capabilities: params.capabilities ?? { chatTypes: ["direct"] },
config: {
listAccountIds: () => [],
listAccountIds: () => ["default"],
resolveAccount: () => ({}),
...params.config,
},
});
export const createOutboundTestPlugin = (params: {
id: ChannelId;
outbound: ChannelOutboundAdapter;
label?: string;
docsPath?: string;
capabilities?: ChannelCapabilities;
}): ChannelPlugin => ({
...createChannelTestPluginBase({
id: params.id,
label: params.label,
docsPath: params.docsPath,
capabilities: params.capabilities,
config: { listAccountIds: () => [] },
}),
outbound: params.outbound,
});