mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 05:01:23 +00:00
test: dedupe and optimize test suites
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
type FakeFsEntry = { kind: "file"; content: string } | { kind: "dir" };
|
||||
|
||||
@@ -73,16 +73,32 @@ vi.mock("./openclaw-root.js", () => ({
|
||||
resolveOpenClawPackageRootSync: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
let resolveControlUiRepoRoot: typeof import("./control-ui-assets.js").resolveControlUiRepoRoot;
|
||||
let resolveControlUiDistIndexPath: typeof import("./control-ui-assets.js").resolveControlUiDistIndexPath;
|
||||
let resolveControlUiDistIndexHealth: typeof import("./control-ui-assets.js").resolveControlUiDistIndexHealth;
|
||||
let resolveControlUiRootOverrideSync: typeof import("./control-ui-assets.js").resolveControlUiRootOverrideSync;
|
||||
let resolveControlUiRootSync: typeof import("./control-ui-assets.js").resolveControlUiRootSync;
|
||||
let openclawRoot: typeof import("./openclaw-root.js");
|
||||
|
||||
describe("control UI assets helpers (fs-mocked)", () => {
|
||||
beforeAll(async () => {
|
||||
({
|
||||
resolveControlUiRepoRoot,
|
||||
resolveControlUiDistIndexPath,
|
||||
resolveControlUiDistIndexHealth,
|
||||
resolveControlUiRootOverrideSync,
|
||||
resolveControlUiRootSync,
|
||||
} = await import("./control-ui-assets.js"));
|
||||
openclawRoot = await import("./openclaw-root.js");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
state.entries.clear();
|
||||
state.realpaths.clear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("resolves repo root from src argv1", async () => {
|
||||
const { resolveControlUiRepoRoot } = await import("./control-ui-assets.js");
|
||||
|
||||
it("resolves repo root from src argv1", () => {
|
||||
const root = abs("fixtures/ui-src");
|
||||
setFile(path.join(root, "ui", "vite.config.ts"), "export {};\n");
|
||||
|
||||
@@ -90,9 +106,7 @@ describe("control UI assets helpers (fs-mocked)", () => {
|
||||
expect(resolveControlUiRepoRoot(argv1)).toBe(root);
|
||||
});
|
||||
|
||||
it("resolves repo root by traversing up (dist argv1)", async () => {
|
||||
const { resolveControlUiRepoRoot } = await import("./control-ui-assets.js");
|
||||
|
||||
it("resolves repo root by traversing up (dist argv1)", () => {
|
||||
const root = abs("fixtures/ui-dist");
|
||||
setFile(path.join(root, "package.json"), "{}\n");
|
||||
setFile(path.join(root, "ui", "vite.config.ts"), "export {};\n");
|
||||
@@ -102,8 +116,6 @@ describe("control UI assets helpers (fs-mocked)", () => {
|
||||
});
|
||||
|
||||
it("resolves dist control-ui index path for dist argv1", async () => {
|
||||
const { resolveControlUiDistIndexPath } = await import("./control-ui-assets.js");
|
||||
|
||||
const argv1 = abs(path.join("fixtures", "pkg", "dist", "index.js"));
|
||||
const distDir = path.dirname(argv1);
|
||||
await expect(resolveControlUiDistIndexPath(argv1)).resolves.toBe(
|
||||
@@ -112,9 +124,6 @@ describe("control UI assets helpers (fs-mocked)", () => {
|
||||
});
|
||||
|
||||
it("uses resolveOpenClawPackageRoot when available", async () => {
|
||||
const openclawRoot = await import("./openclaw-root.js");
|
||||
const { resolveControlUiDistIndexPath } = await import("./control-ui-assets.js");
|
||||
|
||||
const pkgRoot = abs("fixtures/openclaw");
|
||||
(
|
||||
openclawRoot.resolveOpenClawPackageRoot as unknown as ReturnType<typeof vi.fn>
|
||||
@@ -126,8 +135,6 @@ describe("control UI assets helpers (fs-mocked)", () => {
|
||||
});
|
||||
|
||||
it("falls back to package.json name matching when root resolution fails", async () => {
|
||||
const { resolveControlUiDistIndexPath } = await import("./control-ui-assets.js");
|
||||
|
||||
const root = abs("fixtures/fallback");
|
||||
setFile(path.join(root, "package.json"), JSON.stringify({ name: "openclaw" }));
|
||||
setFile(path.join(root, "dist", "control-ui", "index.html"), "<html></html>\n");
|
||||
@@ -138,8 +145,6 @@ describe("control UI assets helpers (fs-mocked)", () => {
|
||||
});
|
||||
|
||||
it("returns null when fallback package name does not match", async () => {
|
||||
const { resolveControlUiDistIndexPath } = await import("./control-ui-assets.js");
|
||||
|
||||
const root = abs("fixtures/not-openclaw");
|
||||
setFile(path.join(root, "package.json"), JSON.stringify({ name: "malicious-pkg" }));
|
||||
setFile(path.join(root, "dist", "control-ui", "index.html"), "<html></html>\n");
|
||||
@@ -148,8 +153,6 @@ describe("control UI assets helpers (fs-mocked)", () => {
|
||||
});
|
||||
|
||||
it("reports health for missing + existing dist assets", async () => {
|
||||
const { resolveControlUiDistIndexHealth } = await import("./control-ui-assets.js");
|
||||
|
||||
const root = abs("fixtures/health");
|
||||
const indexPath = path.join(root, "dist", "control-ui", "index.html");
|
||||
|
||||
@@ -165,9 +168,7 @@ describe("control UI assets helpers (fs-mocked)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves control-ui root from override file or directory", async () => {
|
||||
const { resolveControlUiRootOverrideSync } = await import("./control-ui-assets.js");
|
||||
|
||||
it("resolves control-ui root from override file or directory", () => {
|
||||
const root = abs("fixtures/override");
|
||||
const uiDir = path.join(root, "dist", "control-ui");
|
||||
const indexPath = path.join(uiDir, "index.html");
|
||||
@@ -181,9 +182,6 @@ describe("control UI assets helpers (fs-mocked)", () => {
|
||||
});
|
||||
|
||||
it("resolves control-ui root for dist bundle argv1 and moduleUrl candidates", async () => {
|
||||
const openclawRoot = await import("./openclaw-root.js");
|
||||
const { resolveControlUiRootSync } = await import("./control-ui-assets.js");
|
||||
|
||||
const pkgRoot = abs("fixtures/openclaw-bundle");
|
||||
(
|
||||
openclawRoot.resolveOpenClawPackageRootSync as unknown as ReturnType<typeof vi.fn>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
type FakeFsEntry = { kind: "file"; content: string } | { kind: "dir" };
|
||||
|
||||
@@ -90,6 +90,14 @@ vi.mock("node:fs/promises", async (importOriginal) => {
|
||||
});
|
||||
|
||||
describe("resolveOpenClawPackageRoot", () => {
|
||||
let resolveOpenClawPackageRoot: typeof import("./openclaw-root.js").resolveOpenClawPackageRoot;
|
||||
let resolveOpenClawPackageRootSync: typeof import("./openclaw-root.js").resolveOpenClawPackageRootSync;
|
||||
|
||||
beforeAll(async () => {
|
||||
({ resolveOpenClawPackageRoot, resolveOpenClawPackageRootSync } =
|
||||
await import("./openclaw-root.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
state.entries.clear();
|
||||
state.realpaths.clear();
|
||||
@@ -97,8 +105,6 @@ describe("resolveOpenClawPackageRoot", () => {
|
||||
});
|
||||
|
||||
it("resolves package root from .bin argv1", async () => {
|
||||
const { resolveOpenClawPackageRootSync } = await import("./openclaw-root.js");
|
||||
|
||||
const project = fx("bin-scenario");
|
||||
const argv1 = path.join(project, "node_modules", ".bin", "openclaw");
|
||||
const pkgRoot = path.join(project, "node_modules", "openclaw");
|
||||
@@ -108,8 +114,6 @@ describe("resolveOpenClawPackageRoot", () => {
|
||||
});
|
||||
|
||||
it("resolves package root via symlinked argv1", async () => {
|
||||
const { resolveOpenClawPackageRootSync } = await import("./openclaw-root.js");
|
||||
|
||||
const project = fx("symlink-scenario");
|
||||
const bin = path.join(project, "bin", "openclaw");
|
||||
const realPkg = path.join(project, "real-pkg");
|
||||
@@ -132,8 +136,6 @@ describe("resolveOpenClawPackageRoot", () => {
|
||||
});
|
||||
|
||||
it("prefers moduleUrl candidates", async () => {
|
||||
const { resolveOpenClawPackageRootSync } = await import("./openclaw-root.js");
|
||||
|
||||
const pkgRoot = fx("moduleurl");
|
||||
setFile(path.join(pkgRoot, "package.json"), JSON.stringify({ name: "openclaw" }));
|
||||
const moduleUrl = pathToFileURL(path.join(pkgRoot, "dist", "index.js")).toString();
|
||||
@@ -142,8 +144,6 @@ describe("resolveOpenClawPackageRoot", () => {
|
||||
});
|
||||
|
||||
it("returns null for non-openclaw package roots", async () => {
|
||||
const { resolveOpenClawPackageRootSync } = await import("./openclaw-root.js");
|
||||
|
||||
const pkgRoot = fx("not-openclaw");
|
||||
setFile(path.join(pkgRoot, "package.json"), JSON.stringify({ name: "not-openclaw" }));
|
||||
|
||||
@@ -151,8 +151,6 @@ describe("resolveOpenClawPackageRoot", () => {
|
||||
});
|
||||
|
||||
it("async resolver matches sync behavior", async () => {
|
||||
const { resolveOpenClawPackageRoot } = await import("./openclaw-root.js");
|
||||
|
||||
const pkgRoot = fx("async");
|
||||
setFile(path.join(pkgRoot, "package.json"), JSON.stringify({ name: "openclaw" }));
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { slackPlugin } from "../../../extensions/slack/src/channel.js";
|
||||
import { telegramPlugin } from "../../../extensions/telegram/src/channel.js";
|
||||
import { whatsappPlugin } from "../../../extensions/whatsapp/src/channel.js";
|
||||
@@ -86,16 +86,32 @@ function createAlwaysConfiguredPluginConfig(account: Record<string, unknown> = {
|
||||
};
|
||||
}
|
||||
|
||||
describe("runMessageAction context isolation", () => {
|
||||
beforeEach(async () => {
|
||||
const { createPluginRuntime } = await import("../../plugins/runtime/index.js");
|
||||
const { setSlackRuntime } = await import("../../../extensions/slack/src/runtime.js");
|
||||
const { setTelegramRuntime } = await import("../../../extensions/telegram/src/runtime.js");
|
||||
const { setWhatsAppRuntime } = await import("../../../extensions/whatsapp/src/runtime.js");
|
||||
const runtime = createPluginRuntime();
|
||||
setSlackRuntime(runtime);
|
||||
let createPluginRuntime: typeof import("../../plugins/runtime/index.js").createPluginRuntime;
|
||||
let setSlackRuntime: typeof import("../../../extensions/slack/src/runtime.js").setSlackRuntime;
|
||||
let setTelegramRuntime: typeof import("../../../extensions/telegram/src/runtime.js").setTelegramRuntime;
|
||||
let setWhatsAppRuntime: typeof import("../../../extensions/whatsapp/src/runtime.js").setWhatsAppRuntime;
|
||||
|
||||
function installChannelRuntimes(params?: { includeTelegram?: boolean; includeWhatsApp?: boolean }) {
|
||||
const runtime = createPluginRuntime();
|
||||
setSlackRuntime(runtime);
|
||||
if (params?.includeTelegram !== false) {
|
||||
setTelegramRuntime(runtime);
|
||||
}
|
||||
if (params?.includeWhatsApp !== false) {
|
||||
setWhatsAppRuntime(runtime);
|
||||
}
|
||||
}
|
||||
|
||||
describe("runMessageAction context isolation", () => {
|
||||
beforeAll(async () => {
|
||||
({ createPluginRuntime } = await import("../../plugins/runtime/index.js"));
|
||||
({ setSlackRuntime } = await import("../../../extensions/slack/src/runtime.js"));
|
||||
({ setTelegramRuntime } = await import("../../../extensions/telegram/src/runtime.js"));
|
||||
({ setWhatsAppRuntime } = await import("../../../extensions/whatsapp/src/runtime.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
installChannelRuntimes();
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
@@ -222,59 +238,59 @@ describe("runMessageAction context isolation", () => {
|
||||
expect(result.kind).toBe("action");
|
||||
});
|
||||
|
||||
it("allows WhatsApp send when target matches current chat", async () => {
|
||||
it.each([
|
||||
{
|
||||
name: "whatsapp",
|
||||
channel: "whatsapp",
|
||||
target: "123@g.us",
|
||||
currentChannelId: "123@g.us",
|
||||
},
|
||||
{
|
||||
name: "imessage",
|
||||
channel: "imessage",
|
||||
target: "imessage:+15551234567",
|
||||
currentChannelId: "imessage:+15551234567",
|
||||
},
|
||||
] as const)("allows $name send when target matches current context", async (testCase) => {
|
||||
const result = await runDrySend({
|
||||
cfg: whatsappConfig,
|
||||
actionParams: {
|
||||
channel: "whatsapp",
|
||||
target: "123@g.us",
|
||||
channel: testCase.channel,
|
||||
target: testCase.target,
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "123@g.us" },
|
||||
toolContext: { currentChannelId: testCase.currentChannelId },
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("send");
|
||||
});
|
||||
|
||||
it("blocks WhatsApp send when target differs from current chat", async () => {
|
||||
it.each([
|
||||
{
|
||||
name: "whatsapp",
|
||||
channel: "whatsapp",
|
||||
target: "456@g.us",
|
||||
currentChannelId: "123@g.us",
|
||||
currentChannelProvider: "whatsapp",
|
||||
},
|
||||
{
|
||||
name: "imessage",
|
||||
channel: "imessage",
|
||||
target: "imessage:+15551230000",
|
||||
currentChannelId: "imessage:+15551234567",
|
||||
currentChannelProvider: "imessage",
|
||||
},
|
||||
] as const)("blocks $name send when target differs from current context", async (testCase) => {
|
||||
const result = await runDrySend({
|
||||
cfg: whatsappConfig,
|
||||
actionParams: {
|
||||
channel: "whatsapp",
|
||||
target: "456@g.us",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "123@g.us", currentChannelProvider: "whatsapp" },
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("send");
|
||||
});
|
||||
|
||||
it("allows iMessage send when target matches current handle", async () => {
|
||||
const result = await runDrySend({
|
||||
cfg: whatsappConfig,
|
||||
actionParams: {
|
||||
channel: "imessage",
|
||||
target: "imessage:+15551234567",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "imessage:+15551234567" },
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("send");
|
||||
});
|
||||
|
||||
it("blocks iMessage send when target differs from current handle", async () => {
|
||||
const result = await runDrySend({
|
||||
cfg: whatsappConfig,
|
||||
actionParams: {
|
||||
channel: "imessage",
|
||||
target: "imessage:+15551230000",
|
||||
channel: testCase.channel,
|
||||
target: testCase.target,
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: {
|
||||
currentChannelId: "imessage:+15551234567",
|
||||
currentChannelProvider: "imessage",
|
||||
currentChannelId: testCase.currentChannelId,
|
||||
currentChannelProvider: testCase.currentChannelProvider,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -498,11 +514,8 @@ describe("runMessageAction sendAttachment hydration", () => {
|
||||
});
|
||||
|
||||
describe("runMessageAction sandboxed media validation", () => {
|
||||
beforeEach(async () => {
|
||||
const { createPluginRuntime } = await import("../../plugins/runtime/index.js");
|
||||
const { setSlackRuntime } = await import("../../../extensions/slack/src/runtime.js");
|
||||
const runtime = createPluginRuntime();
|
||||
setSlackRuntime(runtime);
|
||||
beforeEach(() => {
|
||||
installChannelRuntimes({ includeTelegram: false, includeWhatsApp: false });
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
@@ -518,38 +531,38 @@ describe("runMessageAction sandboxed media validation", () => {
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
});
|
||||
|
||||
it("rejects media outside the sandbox root", async () => {
|
||||
await withSandbox(async (sandboxDir) => {
|
||||
await expect(
|
||||
runDrySend({
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "#C12345678",
|
||||
media: "/etc/passwd",
|
||||
message: "",
|
||||
},
|
||||
sandboxRoot: sandboxDir,
|
||||
}),
|
||||
).rejects.toThrow(/sandbox/i);
|
||||
});
|
||||
});
|
||||
it.each(["/etc/passwd", "file:///etc/passwd"])(
|
||||
"rejects out-of-sandbox media reference: %s",
|
||||
async (media) => {
|
||||
await withSandbox(async (sandboxDir) => {
|
||||
await expect(
|
||||
runDrySend({
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "#C12345678",
|
||||
media,
|
||||
message: "",
|
||||
},
|
||||
sandboxRoot: sandboxDir,
|
||||
}),
|
||||
).rejects.toThrow(/sandbox/i);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects file:// media outside the sandbox root", async () => {
|
||||
await withSandbox(async (sandboxDir) => {
|
||||
await expect(
|
||||
runDrySend({
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "#C12345678",
|
||||
media: "file:///etc/passwd",
|
||||
message: "",
|
||||
},
|
||||
sandboxRoot: sandboxDir,
|
||||
}),
|
||||
).rejects.toThrow(/sandbox/i);
|
||||
});
|
||||
it("rejects data URLs in media params", async () => {
|
||||
await expect(
|
||||
runDrySend({
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "#C12345678",
|
||||
media: "data:image/png;base64,abcd",
|
||||
message: "",
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/data:/i);
|
||||
});
|
||||
|
||||
it("rewrites sandbox-relative media paths", async () => {
|
||||
@@ -592,20 +605,6 @@ describe("runMessageAction sandboxed media validation", () => {
|
||||
expect(result.sendResult?.mediaUrl).toBe(path.join(sandboxDir, "data", "note.ogg"));
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects data URLs in media params", async () => {
|
||||
await expect(
|
||||
runDrySend({
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "#C12345678",
|
||||
media: "data:image/png;base64,abcd",
|
||||
message: "",
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/data:/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runMessageAction media caption behavior", () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { slackPlugin } from "../../../extensions/slack/src/channel.js";
|
||||
import { telegramPlugin } from "../../../extensions/telegram/src/channel.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
@@ -80,11 +80,18 @@ const defaultTelegramToolContext = {
|
||||
currentThreadTs: "42",
|
||||
} as const;
|
||||
|
||||
let createPluginRuntime: typeof import("../../plugins/runtime/index.js").createPluginRuntime;
|
||||
let setSlackRuntime: typeof import("../../../extensions/slack/src/runtime.js").setSlackRuntime;
|
||||
let setTelegramRuntime: typeof import("../../../extensions/telegram/src/runtime.js").setTelegramRuntime;
|
||||
|
||||
describe("runMessageAction threading auto-injection", () => {
|
||||
beforeEach(async () => {
|
||||
const { createPluginRuntime } = await import("../../plugins/runtime/index.js");
|
||||
const { setSlackRuntime } = await import("../../../extensions/slack/src/runtime.js");
|
||||
const { setTelegramRuntime } = await import("../../../extensions/telegram/src/runtime.js");
|
||||
beforeAll(async () => {
|
||||
({ createPluginRuntime } = await import("../../plugins/runtime/index.js"));
|
||||
({ setSlackRuntime } = await import("../../../extensions/slack/src/runtime.js"));
|
||||
({ setTelegramRuntime } = await import("../../../extensions/telegram/src/runtime.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const runtime = createPluginRuntime();
|
||||
setSlackRuntime(runtime);
|
||||
setTelegramRuntime(runtime);
|
||||
@@ -110,94 +117,73 @@ describe("runMessageAction threading auto-injection", () => {
|
||||
mocks.recordSessionMetaFromInbound.mockReset();
|
||||
});
|
||||
|
||||
it("uses toolContext thread when auto-threading is active", async () => {
|
||||
it.each([
|
||||
{
|
||||
name: "exact channel id",
|
||||
target: "channel:C123",
|
||||
threadTs: "111.222",
|
||||
expectedSessionKey: "agent:main:slack:channel:c123:thread:111.222",
|
||||
},
|
||||
{
|
||||
name: "case-insensitive channel id",
|
||||
target: "channel:c123",
|
||||
threadTs: "333.444",
|
||||
expectedSessionKey: "agent:main:slack:channel:c123:thread:333.444",
|
||||
},
|
||||
] as const)("auto-threads slack using $name", async (testCase) => {
|
||||
mockHandledSendAction();
|
||||
|
||||
const call = await runThreadingAction({
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "channel:C123",
|
||||
target: testCase.target,
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: {
|
||||
currentChannelId: "C123",
|
||||
currentThreadTs: "111.222",
|
||||
currentThreadTs: testCase.threadTs,
|
||||
replyToMode: "all",
|
||||
},
|
||||
});
|
||||
|
||||
expect(call?.ctx?.agentId).toBe("main");
|
||||
expect(call?.ctx?.mirror?.sessionKey).toBe("agent:main:slack:channel:c123:thread:111.222");
|
||||
expect(call?.ctx?.mirror?.sessionKey).toBe(testCase.expectedSessionKey);
|
||||
});
|
||||
|
||||
it("matches auto-threading when channel ids differ in case", async () => {
|
||||
mockHandledSendAction();
|
||||
|
||||
const call = await runThreadingAction({
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "channel:c123",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: {
|
||||
currentChannelId: "C123",
|
||||
currentThreadTs: "333.444",
|
||||
replyToMode: "all",
|
||||
},
|
||||
});
|
||||
|
||||
expect(call?.ctx?.mirror?.sessionKey).toBe("agent:main:slack:channel:c123:thread:333.444");
|
||||
});
|
||||
|
||||
it("auto-injects telegram threadId from toolContext when omitted", async () => {
|
||||
it.each([
|
||||
{
|
||||
name: "injects threadId for matching target",
|
||||
target: "telegram:123",
|
||||
expectedThreadId: "42",
|
||||
},
|
||||
{
|
||||
name: "injects threadId for prefixed group target",
|
||||
target: "telegram:group:123",
|
||||
expectedThreadId: "42",
|
||||
},
|
||||
{
|
||||
name: "skips threadId when target chat differs",
|
||||
target: "telegram:999",
|
||||
expectedThreadId: undefined,
|
||||
},
|
||||
] as const)("telegram auto-threading: $name", async (testCase) => {
|
||||
mockHandledSendAction();
|
||||
|
||||
const call = await runThreadingAction({
|
||||
cfg: telegramConfig,
|
||||
actionParams: {
|
||||
channel: "telegram",
|
||||
target: "telegram:123",
|
||||
target: testCase.target,
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: defaultTelegramToolContext,
|
||||
});
|
||||
|
||||
expect(call?.threadId).toBe("42");
|
||||
expect(call?.ctx?.params?.threadId).toBe("42");
|
||||
});
|
||||
|
||||
it("skips telegram auto-threading when target chat differs", async () => {
|
||||
mockHandledSendAction();
|
||||
|
||||
const call = await runThreadingAction({
|
||||
cfg: telegramConfig,
|
||||
actionParams: {
|
||||
channel: "telegram",
|
||||
target: "telegram:999",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: defaultTelegramToolContext,
|
||||
});
|
||||
|
||||
expect(call?.ctx?.params?.threadId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("matches telegram target with internal prefix variations", async () => {
|
||||
mockHandledSendAction();
|
||||
|
||||
const call = await runThreadingAction({
|
||||
cfg: telegramConfig,
|
||||
actionParams: {
|
||||
channel: "telegram",
|
||||
target: "telegram:group:123",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: defaultTelegramToolContext,
|
||||
});
|
||||
|
||||
expect(call?.ctx?.params?.threadId).toBe("42");
|
||||
expect(call?.ctx?.params?.threadId).toBe(testCase.expectedThreadId);
|
||||
if (testCase.expectedThreadId !== undefined) {
|
||||
expect(call?.threadId).toBe(testCase.expectedThreadId);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses explicit telegram threadId when provided", async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { spawn, type ChildProcess, type SpawnOptions } from "node:child_process";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
type MockSpawnChild = EventEmitter & {
|
||||
stdout?: EventEmitter & { setEncoding?: (enc: string) => void };
|
||||
@@ -40,9 +40,15 @@ vi.mock("node:child_process", () => {
|
||||
|
||||
const spawnMock = vi.mocked(spawn);
|
||||
|
||||
let parseSshConfigOutput: typeof import("./ssh-config.js").parseSshConfigOutput;
|
||||
let resolveSshConfig: typeof import("./ssh-config.js").resolveSshConfig;
|
||||
|
||||
describe("ssh-config", () => {
|
||||
it("parses ssh -G output", async () => {
|
||||
const { parseSshConfigOutput } = await import("./ssh-config.js");
|
||||
beforeAll(async () => {
|
||||
({ parseSshConfigOutput, resolveSshConfig } = await import("./ssh-config.js"));
|
||||
});
|
||||
|
||||
it("parses ssh -G output", () => {
|
||||
const parsed = parseSshConfigOutput(
|
||||
"user bob\nhostname example.com\nport 2222\nidentityfile none\nidentityfile /tmp/id\n",
|
||||
);
|
||||
@@ -53,7 +59,6 @@ describe("ssh-config", () => {
|
||||
});
|
||||
|
||||
it("resolves ssh config via ssh -G", async () => {
|
||||
const { resolveSshConfig } = await import("./ssh-config.js");
|
||||
const config = await resolveSshConfig({ user: "me", host: "alias", port: 22 });
|
||||
expect(config?.user).toBe("steipete");
|
||||
expect(config?.host).toBe("peters-mac-studio-1.sheep-coho.ts.net");
|
||||
@@ -74,7 +79,6 @@ describe("ssh-config", () => {
|
||||
},
|
||||
);
|
||||
|
||||
const { resolveSshConfig } = await import("./ssh-config.js");
|
||||
const config = await resolveSshConfig({ user: "me", host: "bad-host", port: 22 });
|
||||
expect(config).toBeNull();
|
||||
});
|
||||
|
||||
@@ -12,6 +12,10 @@ vi.mock("./backoff.js", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
function createRuntime() {
|
||||
return { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
|
||||
}
|
||||
|
||||
describe("waitForTransportReady", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
@@ -22,7 +26,7 @@ describe("waitForTransportReady", () => {
|
||||
});
|
||||
|
||||
it("returns when the check succeeds and logs after the delay", async () => {
|
||||
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
|
||||
const runtime = createRuntime();
|
||||
let attempts = 0;
|
||||
const readyPromise = waitForTransportReady({
|
||||
label: "test transport",
|
||||
@@ -48,7 +52,7 @@ describe("waitForTransportReady", () => {
|
||||
});
|
||||
|
||||
it("throws after the timeout", async () => {
|
||||
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
|
||||
const runtime = createRuntime();
|
||||
const waitPromise = waitForTransportReady({
|
||||
label: "test transport",
|
||||
timeoutMs: 110,
|
||||
@@ -65,7 +69,7 @@ describe("waitForTransportReady", () => {
|
||||
});
|
||||
|
||||
it("returns early when aborted", async () => {
|
||||
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
|
||||
const runtime = createRuntime();
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
await waitForTransportReady({
|
||||
|
||||
@@ -147,22 +147,23 @@ describe("update-startup", () => {
|
||||
return { log, parsed };
|
||||
}
|
||||
|
||||
it("logs update hint for npm installs when newer tag exists", async () => {
|
||||
const { log, parsed } = await runUpdateCheckAndReadState("stable");
|
||||
it.each([
|
||||
{
|
||||
name: "stable channel",
|
||||
channel: "stable" as const,
|
||||
},
|
||||
{
|
||||
name: "beta channel with older beta tag",
|
||||
channel: "beta" as const,
|
||||
},
|
||||
])("logs latest update hint for $name", async ({ channel }) => {
|
||||
const { log, parsed } = await runUpdateCheckAndReadState(channel);
|
||||
|
||||
expect(log.info).toHaveBeenCalledWith(
|
||||
expect.stringContaining("update available (latest): v2.0.0"),
|
||||
);
|
||||
expect(parsed.lastNotifiedVersion).toBe("2.0.0");
|
||||
expect(parsed.lastAvailableVersion).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("uses latest when beta tag is older than release", async () => {
|
||||
const { log, parsed } = await runUpdateCheckAndReadState("beta");
|
||||
|
||||
expect(log.info).toHaveBeenCalledWith(
|
||||
expect.stringContaining("update available (latest): v2.0.0"),
|
||||
);
|
||||
expect(parsed.lastNotifiedTag).toBe("latest");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user