mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix: stabilize tests and logging
This commit is contained in:
@@ -5,7 +5,7 @@ import { isImageDimensionErrorMessage, parseImageDimensionError } from "./pi-emb
|
|||||||
describe("image dimension errors", () => {
|
describe("image dimension errors", () => {
|
||||||
it("parses anthropic image dimension errors", () => {
|
it("parses anthropic image dimension errors", () => {
|
||||||
const raw =
|
const raw =
|
||||||
"400 {\"type\":\"error\",\"error\":{\"type\":\"invalid_request_error\",\"message\":\"messages.84.content.1.image.source.base64.data: At least one of the image dimensions exceed max allowed size for many-image requests: 2000 pixels\"}}";
|
'400 {"type":"error","error":{"type":"invalid_request_error","message":"messages.84.content.1.image.source.base64.data: At least one of the image dimensions exceed max allowed size for many-image requests: 2000 pixels"}}';
|
||||||
const parsed = parseImageDimensionError(raw);
|
const parsed = parseImageDimensionError(raw);
|
||||||
expect(parsed).not.toBeNull();
|
expect(parsed).not.toBeNull();
|
||||||
expect(parsed?.maxDimensionPx).toBe(2000);
|
expect(parsed?.maxDimensionPx).toBe(2000);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ describe("isCloudCodeAssistFormatError", () => {
|
|||||||
expect(isCloudCodeAssistFormatError("rate limit exceeded")).toBe(false);
|
expect(isCloudCodeAssistFormatError("rate limit exceeded")).toBe(false);
|
||||||
expect(
|
expect(
|
||||||
isCloudCodeAssistFormatError(
|
isCloudCodeAssistFormatError(
|
||||||
"400 {\"type\":\"error\",\"error\":{\"type\":\"invalid_request_error\",\"message\":\"messages.84.content.1.image.source.base64.data: At least one of the image dimensions exceed max allowed size for many-image requests: 2000 pixels\"}}",
|
'400 {"type":"error","error":{"type":"invalid_request_error","message":"messages.84.content.1.image.source.base64.data: At least one of the image dimensions exceed max allowed size for many-image requests: 2000 pixels"}}',
|
||||||
),
|
),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ let runEmbeddedPiAgent: typeof import("./pi-embedded-runner.js").runEmbeddedPiAg
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
vi.resetModules();
|
|
||||||
({ runEmbeddedPiAgent } = await import("./pi-embedded-runner.js"));
|
({ runEmbeddedPiAgent } = await import("./pi-embedded-runner.js"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ let runEmbeddedPiAgent: typeof import("./pi-embedded-runner.js").runEmbeddedPiAg
|
|||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
vi.resetModules();
|
|
||||||
mockPiAi();
|
mockPiAi();
|
||||||
({ runEmbeddedPiAgent } = await import("./pi-embedded-runner.js"));
|
({ runEmbeddedPiAgent } = await import("./pi-embedded-runner.js"));
|
||||||
}, 20_000);
|
}, 20_000);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ describe("detectImageReferences", () => {
|
|||||||
expect(refs[0]?.raw).toBe("~/Pictures/vacation.png");
|
expect(refs[0]?.raw).toBe("~/Pictures/vacation.png");
|
||||||
expect(refs[0]?.type).toBe("path");
|
expect(refs[0]?.type).toBe("path");
|
||||||
// Resolved path should expand ~
|
// Resolved path should expand ~
|
||||||
expect(refs[0]?.resolved).not.toContain("~");
|
expect(refs[0]?.resolved?.startsWith("~")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("detects multiple image references in a prompt", () => {
|
it("detects multiple image references in a prompt", () => {
|
||||||
|
|||||||
@@ -109,11 +109,7 @@ function buildMessagingSection(params: {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDocsSection(params: {
|
function buildDocsSection(params: { docsPath?: string; isMinimal: boolean; readToolName: string }) {
|
||||||
docsPath?: string;
|
|
||||||
isMinimal: boolean;
|
|
||||||
readToolName: string;
|
|
||||||
}) {
|
|
||||||
const docsPath = params.docsPath?.trim();
|
const docsPath = params.docsPath?.trim();
|
||||||
if (!docsPath || params.isMinimal) return [];
|
if (!docsPath || params.isMinimal) return [];
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -58,7 +58,12 @@ async function resizeImageBase64IfNeeded(params: {
|
|||||||
const height = meta?.height;
|
const height = meta?.height;
|
||||||
const overBytes = buf.byteLength > params.maxBytes;
|
const overBytes = buf.byteLength > params.maxBytes;
|
||||||
const hasDimensions = typeof width === "number" && typeof height === "number";
|
const hasDimensions = typeof width === "number" && typeof height === "number";
|
||||||
if (hasDimensions && !overBytes && width <= params.maxDimensionPx && height <= params.maxDimensionPx) {
|
if (
|
||||||
|
hasDimensions &&
|
||||||
|
!overBytes &&
|
||||||
|
width <= params.maxDimensionPx &&
|
||||||
|
height <= params.maxDimensionPx
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
base64: params.base64,
|
base64: params.base64,
|
||||||
mimeType: params.mimeType,
|
mimeType: params.mimeType,
|
||||||
@@ -67,7 +72,10 @@ async function resizeImageBase64IfNeeded(params: {
|
|||||||
height,
|
height,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (hasDimensions && (width > params.maxDimensionPx || height > params.maxDimensionPx || overBytes)) {
|
if (
|
||||||
|
hasDimensions &&
|
||||||
|
(width > params.maxDimensionPx || height > params.maxDimensionPx || overBytes)
|
||||||
|
) {
|
||||||
log.warn("Image exceeds limits; resizing", {
|
log.warn("Image exceeds limits; resizing", {
|
||||||
label: params.label,
|
label: params.label,
|
||||||
width,
|
width,
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
|
import {
|
||||||
|
getChannelPlugin,
|
||||||
|
normalizeChannelId as normalizeAnyChannelId,
|
||||||
|
} from "../../channels/plugins/index.js";
|
||||||
|
import { normalizeChannelId as normalizeChatChannelId } from "../../channels/registry.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
|
|
||||||
const ANNOUNCE_SKIP_TOKEN = "ANNOUNCE_SKIP";
|
const ANNOUNCE_SKIP_TOKEN = "ANNOUNCE_SKIP";
|
||||||
@@ -21,7 +25,8 @@ export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget
|
|||||||
const id = rest.join(":").trim();
|
const id = rest.join(":").trim();
|
||||||
if (!id) return null;
|
if (!id) return null;
|
||||||
if (!channelRaw) return null;
|
if (!channelRaw) return null;
|
||||||
const normalizedChannel = normalizeChannelId(channelRaw);
|
const normalizedChannel =
|
||||||
|
normalizeAnyChannelId(channelRaw) ?? normalizeChatChannelId(channelRaw);
|
||||||
const channel = normalizedChannel ?? channelRaw.toLowerCase();
|
const channel = normalizedChannel ?? channelRaw.toLowerCase();
|
||||||
const kindTarget = (() => {
|
const kindTarget = (() => {
|
||||||
if (!normalizedChannel) return id;
|
if (!normalizedChannel) return id;
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
const callGatewayFromCli = vi.fn(
|
const callGatewayFromCli = vi.fn(async (method: string, _opts: unknown, params?: unknown) => {
|
||||||
async (method: string, _opts: unknown, params?: unknown) => {
|
if (method.endsWith(".get")) {
|
||||||
if (method.endsWith(".get")) {
|
return {
|
||||||
return {
|
path: "/tmp/exec-approvals.json",
|
||||||
path: "/tmp/exec-approvals.json",
|
exists: true,
|
||||||
exists: true,
|
hash: "hash-1",
|
||||||
hash: "hash-1",
|
file: { version: 1, agents: {} },
|
||||||
file: { version: 1, agents: {} },
|
};
|
||||||
};
|
}
|
||||||
}
|
return { method, params };
|
||||||
return { method, params };
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const runtimeLogs: string[] = [];
|
const runtimeLogs: string[] = [];
|
||||||
const runtimeErrors: string[] = [];
|
const runtimeErrors: string[] = [];
|
||||||
@@ -31,9 +29,7 @@ vi.mock("./gateway-rpc.js", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("./nodes-cli/rpc.js", async () => {
|
vi.mock("./nodes-cli/rpc.js", async () => {
|
||||||
const actual = await vi.importActual<typeof import("./nodes-cli/rpc.js")>(
|
const actual = await vi.importActual<typeof import("./nodes-cli/rpc.js")>("./nodes-cli/rpc.js");
|
||||||
"./nodes-cli/rpc.js",
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
resolveNodeId: vi.fn(async () => "node-1"),
|
resolveNodeId: vi.fn(async () => "node-1"),
|
||||||
@@ -57,11 +53,7 @@ describe("exec approvals CLI", () => {
|
|||||||
|
|
||||||
await program.parseAsync(["approvals", "get"], { from: "user" });
|
await program.parseAsync(["approvals", "get"], { from: "user" });
|
||||||
|
|
||||||
expect(callGatewayFromCli).toHaveBeenCalledWith(
|
expect(callGatewayFromCli).toHaveBeenCalledWith("exec.approvals.get", expect.anything(), {});
|
||||||
"exec.approvals.get",
|
|
||||||
expect.anything(),
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
expect(runtimeErrors).toHaveLength(0);
|
expect(runtimeErrors).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -77,11 +69,9 @@ describe("exec approvals CLI", () => {
|
|||||||
|
|
||||||
await program.parseAsync(["approvals", "get", "--node", "macbook"], { from: "user" });
|
await program.parseAsync(["approvals", "get", "--node", "macbook"], { from: "user" });
|
||||||
|
|
||||||
expect(callGatewayFromCli).toHaveBeenCalledWith(
|
expect(callGatewayFromCli).toHaveBeenCalledWith("exec.approvals.node.get", expect.anything(), {
|
||||||
"exec.approvals.node.get",
|
nodeId: "node-1",
|
||||||
expect.anything(),
|
});
|
||||||
{ nodeId: "node-1" },
|
|
||||||
);
|
|
||||||
expect(runtimeErrors).toHaveLength(0);
|
expect(runtimeErrors).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -151,15 +151,13 @@ export function registerExecApprovalsCli(program: Command) {
|
|||||||
});
|
});
|
||||||
nodesCallOpts(setCmd);
|
nodesCallOpts(setCmd);
|
||||||
|
|
||||||
const allowlist = approvals
|
const allowlist = approvals.command("allowlist").description("Edit the per-agent allowlist");
|
||||||
.command("allowlist")
|
|
||||||
.description("Edit the per-agent allowlist");
|
|
||||||
|
|
||||||
const allowlistAdd = allowlist
|
const allowlistAdd = allowlist
|
||||||
.command("add <pattern>")
|
.command("add <pattern>")
|
||||||
.description("Add a glob pattern to an allowlist")
|
.description("Add a glob pattern to an allowlist")
|
||||||
.option("--node <node>", "Target node id/name/IP (defaults to gateway)")
|
.option("--node <node>", "Target node id/name/IP (defaults to gateway)")
|
||||||
.option("--agent <id>", "Agent id (defaults to \"default\")")
|
.option("--agent <id>", 'Agent id (defaults to "default")')
|
||||||
.action(async (pattern: string, opts: ExecApprovalsCliOpts) => {
|
.action(async (pattern: string, opts: ExecApprovalsCliOpts) => {
|
||||||
const trimmed = pattern.trim();
|
const trimmed = pattern.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
@@ -196,7 +194,7 @@ export function registerExecApprovalsCli(program: Command) {
|
|||||||
.command("remove <pattern>")
|
.command("remove <pattern>")
|
||||||
.description("Remove a glob pattern from an allowlist")
|
.description("Remove a glob pattern from an allowlist")
|
||||||
.option("--node <node>", "Target node id/name/IP (defaults to gateway)")
|
.option("--node <node>", "Target node id/name/IP (defaults to gateway)")
|
||||||
.option("--agent <id>", "Agent id (defaults to \"default\")")
|
.option("--agent <id>", 'Agent id (defaults to "default")')
|
||||||
.action(async (pattern: string, opts: ExecApprovalsCliOpts) => {
|
.action(async (pattern: string, opts: ExecApprovalsCliOpts) => {
|
||||||
const trimmed = pattern.trim();
|
const trimmed = pattern.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
|
|||||||
@@ -87,19 +87,13 @@ describe("gateway SIGTERM", () => {
|
|||||||
const out: string[] = [];
|
const out: string[] = [];
|
||||||
const err: string[] = [];
|
const err: string[] = [];
|
||||||
|
|
||||||
|
const bunBin = process.env.BUN_INSTALL
|
||||||
|
? path.join(process.env.BUN_INSTALL, "bin", "bun")
|
||||||
|
: "bun";
|
||||||
|
|
||||||
child = spawn(
|
child = spawn(
|
||||||
process.execPath,
|
bunBin,
|
||||||
[
|
["src/entry.ts", "gateway", "--port", String(port), "--bind", "loopback", "--allow-unconfigured"],
|
||||||
"--import",
|
|
||||||
"tsx",
|
|
||||||
"src/index.ts",
|
|
||||||
"gateway",
|
|
||||||
"--port",
|
|
||||||
String(port),
|
|
||||||
"--bind",
|
|
||||||
"loopback",
|
|
||||||
"--allow-unconfigured",
|
|
||||||
],
|
|
||||||
{
|
{
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
env: {
|
env: {
|
||||||
|
|||||||
@@ -156,9 +156,7 @@ export function registerMemoryCli(program: Command) {
|
|||||||
for (const result of allResults) {
|
for (const result of allResults) {
|
||||||
const { agentId, status, embeddingProbe, indexError } = result;
|
const { agentId, status, embeddingProbe, indexError } = result;
|
||||||
if (opts.index) {
|
if (opts.index) {
|
||||||
const line = indexError
|
const line = indexError ? `Memory index failed: ${indexError}` : "Memory index complete.";
|
||||||
? `Memory index failed: ${indexError}`
|
|
||||||
: "Memory index complete.";
|
|
||||||
defaultRuntime.log(line);
|
defaultRuntime.log(line);
|
||||||
}
|
}
|
||||||
const lines = [
|
const lines = [
|
||||||
@@ -167,9 +165,7 @@ export function registerMemoryCli(program: Command) {
|
|||||||
`(requested: ${status.requestedProvider})`,
|
`(requested: ${status.requestedProvider})`,
|
||||||
)}`,
|
)}`,
|
||||||
`${label("Model")} ${info(status.model)}`,
|
`${label("Model")} ${info(status.model)}`,
|
||||||
status.sources?.length
|
status.sources?.length ? `${label("Sources")} ${info(status.sources.join(", "))}` : null,
|
||||||
? `${label("Sources")} ${info(status.sources.join(", "))}`
|
|
||||||
: null,
|
|
||||||
`${label("Indexed")} ${success(`${status.files} files · ${status.chunks} chunks`)}`,
|
`${label("Indexed")} ${success(`${status.files} files · ${status.chunks} chunks`)}`,
|
||||||
`${label("Dirty")} ${status.dirty ? warn("yes") : muted("no")}`,
|
`${label("Dirty")} ${status.dirty ? warn("yes") : muted("no")}`,
|
||||||
`${label("Store")} ${info(status.dbPath)}`,
|
`${label("Store")} ${info(status.dbPath)}`,
|
||||||
|
|||||||
@@ -116,12 +116,14 @@ export function registerNodesStatusCommands(nodes: Command) {
|
|||||||
const family = typeof obj.deviceFamily === "string" ? obj.deviceFamily : null;
|
const family = typeof obj.deviceFamily === "string" ? obj.deviceFamily : null;
|
||||||
const model = typeof obj.modelIdentifier === "string" ? obj.modelIdentifier : null;
|
const model = typeof obj.modelIdentifier === "string" ? obj.modelIdentifier : null;
|
||||||
const ip = typeof obj.remoteIp === "string" ? obj.remoteIp : null;
|
const ip = typeof obj.remoteIp === "string" ? obj.remoteIp : null;
|
||||||
const versions = formatNodeVersions(obj as {
|
const versions = formatNodeVersions(
|
||||||
platform?: string;
|
obj as {
|
||||||
version?: string;
|
platform?: string;
|
||||||
coreVersion?: string;
|
version?: string;
|
||||||
uiVersion?: string;
|
coreVersion?: string;
|
||||||
});
|
uiVersion?: string;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const parts: string[] = ["Node:", displayName, nodeId];
|
const parts: string[] = ["Node:", displayName, nodeId];
|
||||||
if (ip) parts.push(ip);
|
if (ip) parts.push(ip);
|
||||||
|
|||||||
@@ -176,10 +176,7 @@ export async function applyAuthChoicePluginProvider(
|
|||||||
if (result.defaultModel) {
|
if (result.defaultModel) {
|
||||||
if (params.setDefaultModel) {
|
if (params.setDefaultModel) {
|
||||||
nextConfig = applyDefaultModel(nextConfig, result.defaultModel);
|
nextConfig = applyDefaultModel(nextConfig, result.defaultModel);
|
||||||
await params.prompter.note(
|
await params.prompter.note(`Default model set to ${result.defaultModel}`, "Model configured");
|
||||||
`Default model set to ${result.defaultModel}`,
|
|
||||||
"Model configured",
|
|
||||||
);
|
|
||||||
} else if (params.agentId) {
|
} else if (params.agentId) {
|
||||||
agentModelOverride = result.defaultModel;
|
agentModelOverride = result.defaultModel;
|
||||||
await params.prompter.note(
|
await params.prompter.note(
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import { resolveGatewayPort } from "../config/config.js";
|
import { resolveGatewayPort } from "../config/config.js";
|
||||||
import { resolveGatewayLaunchAgentLabel, resolveNodeLaunchAgentLabel } from "../daemon/constants.js";
|
import {
|
||||||
|
resolveGatewayLaunchAgentLabel,
|
||||||
|
resolveNodeLaunchAgentLabel,
|
||||||
|
} from "../daemon/constants.js";
|
||||||
import { readLastGatewayErrorLine } from "../daemon/diagnostics.js";
|
import { readLastGatewayErrorLine } from "../daemon/diagnostics.js";
|
||||||
import {
|
import {
|
||||||
isLaunchAgentListed,
|
isLaunchAgentListed,
|
||||||
@@ -44,10 +47,7 @@ async function maybeRepairLaunchAgentBootstrap(params: {
|
|||||||
const plistExists = await launchAgentPlistExists(params.env);
|
const plistExists = await launchAgentPlistExists(params.env);
|
||||||
if (!plistExists) return false;
|
if (!plistExists) return false;
|
||||||
|
|
||||||
note(
|
note("LaunchAgent is listed but not loaded in launchd.", `${params.title} LaunchAgent`);
|
||||||
"LaunchAgent is listed but not loaded in launchd.",
|
|
||||||
`${params.title} LaunchAgent`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const shouldFix = await params.prompter.confirmSkipInNonInteractive({
|
const shouldFix = await params.prompter.confirmSkipInNonInteractive({
|
||||||
message: `Repair ${params.title} LaunchAgent bootstrap now?`,
|
message: `Repair ${params.title} LaunchAgent bootstrap now?`,
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ describe("onboard (non-interactive): remote gateway config", () => {
|
|||||||
process.env.HOME = tempHome;
|
process.env.HOME = tempHome;
|
||||||
delete process.env.CLAWDBOT_STATE_DIR;
|
delete process.env.CLAWDBOT_STATE_DIR;
|
||||||
delete process.env.CLAWDBOT_CONFIG_PATH;
|
delete process.env.CLAWDBOT_CONFIG_PATH;
|
||||||
vi.resetModules();
|
|
||||||
|
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
const token = "tok_remote_123";
|
const token = "tok_remote_123";
|
||||||
@@ -85,8 +84,8 @@ describe("onboard (non-interactive): remote gateway config", () => {
|
|||||||
runtime,
|
runtime,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { CONFIG_PATH_CLAWDBOT } = await import("../config/config.js");
|
const { resolveConfigPath } = await import("../config/config.js");
|
||||||
const cfg = JSON.parse(await fs.readFile(CONFIG_PATH_CLAWDBOT, "utf8")) as {
|
const cfg = JSON.parse(await fs.readFile(resolveConfigPath(), "utf8")) as {
|
||||||
gateway?: { mode?: string; remote?: { url?: string; token?: string } };
|
gateway?: { mode?: string; remote?: { url?: string; token?: string } };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ describe("applyPluginAutoEnable", () => {
|
|||||||
channels: { slack: { botToken: "x" } },
|
channels: { slack: { botToken: "x" } },
|
||||||
plugins: { allow: ["telegram"] },
|
plugins: { allow: ["telegram"] },
|
||||||
},
|
},
|
||||||
|
env: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.config.plugins?.entries?.slack?.enabled).toBe(true);
|
expect(result.config.plugins?.entries?.slack?.enabled).toBe(true);
|
||||||
@@ -21,6 +22,7 @@ describe("applyPluginAutoEnable", () => {
|
|||||||
channels: { slack: { botToken: "x" } },
|
channels: { slack: { botToken: "x" } },
|
||||||
plugins: { entries: { slack: { enabled: false } } },
|
plugins: { entries: { slack: { enabled: false } } },
|
||||||
},
|
},
|
||||||
|
env: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.config.plugins?.entries?.slack?.enabled).toBe(false);
|
expect(result.config.plugins?.entries?.slack?.enabled).toBe(false);
|
||||||
@@ -39,6 +41,7 @@ describe("applyPluginAutoEnable", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
env: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.config.plugins?.entries?.["google-antigravity-auth"]?.enabled).toBe(true);
|
expect(result.config.plugins?.entries?.["google-antigravity-auth"]?.enabled).toBe(true);
|
||||||
@@ -50,6 +53,7 @@ describe("applyPluginAutoEnable", () => {
|
|||||||
channels: { slack: { botToken: "x" } },
|
channels: { slack: { botToken: "x" } },
|
||||||
plugins: { enabled: false },
|
plugins: { enabled: false },
|
||||||
},
|
},
|
||||||
|
env: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.config.plugins?.entries?.slack?.enabled).toBeUndefined();
|
expect(result.config.plugins?.entries?.slack?.enabled).toBeUndefined();
|
||||||
|
|||||||
@@ -45,10 +45,7 @@ function recordHasKeys(value: unknown): boolean {
|
|||||||
return isRecord(value) && Object.keys(value).length > 0;
|
return isRecord(value) && Object.keys(value).length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function accountsHaveKeys(
|
function accountsHaveKeys(value: unknown, keys: string[]): boolean {
|
||||||
value: unknown,
|
|
||||||
keys: string[],
|
|
||||||
): boolean {
|
|
||||||
if (!isRecord(value)) return false;
|
if (!isRecord(value)) return false;
|
||||||
for (const account of Object.values(value)) {
|
for (const account of Object.values(value)) {
|
||||||
if (!isRecord(account)) continue;
|
if (!isRecord(account)) continue;
|
||||||
@@ -59,7 +56,10 @@ function accountsHaveKeys(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveChannelConfig(cfg: ClawdbotConfig, channelId: string): Record<string, unknown> | null {
|
function resolveChannelConfig(
|
||||||
|
cfg: ClawdbotConfig,
|
||||||
|
channelId: string,
|
||||||
|
): Record<string, unknown> | null {
|
||||||
const channels = cfg.channels as Record<string, unknown> | undefined;
|
const channels = cfg.channels as Record<string, unknown> | undefined;
|
||||||
const entry = channels?.[channelId];
|
const entry = channels?.[channelId];
|
||||||
return isRecord(entry) ? entry : null;
|
return isRecord(entry) ? entry : null;
|
||||||
@@ -234,7 +234,10 @@ function isProviderConfigured(cfg: ClawdbotConfig, providerId: string): boolean
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveConfiguredPlugins(cfg: ClawdbotConfig, env: NodeJS.ProcessEnv): PluginEnableChange[] {
|
function resolveConfiguredPlugins(
|
||||||
|
cfg: ClawdbotConfig,
|
||||||
|
env: NodeJS.ProcessEnv,
|
||||||
|
): PluginEnableChange[] {
|
||||||
const changes: PluginEnableChange[] = [];
|
const changes: PluginEnableChange[] = [];
|
||||||
for (const channelId of CHANNEL_PLUGIN_IDS) {
|
for (const channelId of CHANNEL_PLUGIN_IDS) {
|
||||||
if (isChannelConfigured(cfg, channelId, env)) {
|
if (isChannelConfigured(cfg, channelId, env)) {
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ export function parseClawdbotVersion(raw: string | null | undefined): ClawdbotVe
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function compareClawdbotVersions(a: string | null | undefined, b: string | null | undefined): number | null {
|
export function compareClawdbotVersions(
|
||||||
|
a: string | null | undefined,
|
||||||
|
b: string | null | undefined,
|
||||||
|
): number | null {
|
||||||
const parsedA = parseClawdbotVersion(a);
|
const parsedA = parseClawdbotVersion(a);
|
||||||
const parsedB = parseClawdbotVersion(b);
|
const parsedB = parseClawdbotVersion(b);
|
||||||
if (!parsedA || !parsedB) return null;
|
if (!parsedA || !parsedB) return null;
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ async function withLaunchctlStub(
|
|||||||
' fs.appendFileSync(logPath, JSON.stringify(args) + "\\n", "utf8");',
|
' fs.appendFileSync(logPath, JSON.stringify(args) + "\\n", "utf8");',
|
||||||
"}",
|
"}",
|
||||||
'if (args[0] === "list") {',
|
'if (args[0] === "list") {',
|
||||||
" const output = process.env.CLAWDBOT_TEST_LAUNCHCTL_LIST_OUTPUT || \"\";",
|
' const output = process.env.CLAWDBOT_TEST_LAUNCHCTL_LIST_OUTPUT || "";',
|
||||||
" process.stdout.write(output);",
|
" process.stdout.write(output);",
|
||||||
"}",
|
"}",
|
||||||
"process.exit(0);",
|
"process.exit(0);",
|
||||||
@@ -107,13 +107,10 @@ describe("launchd runtime parsing", () => {
|
|||||||
|
|
||||||
describe("launchctl list detection", () => {
|
describe("launchctl list detection", () => {
|
||||||
it("detects the resolved label in launchctl list", async () => {
|
it("detects the resolved label in launchctl list", async () => {
|
||||||
await withLaunchctlStub(
|
await withLaunchctlStub({ listOutput: "123 0 com.clawdbot.gateway\n" }, async ({ env }) => {
|
||||||
{ listOutput: "123 0 com.clawdbot.gateway\n" },
|
const listed = await isLaunchAgentListed({ env });
|
||||||
async ({ env }) => {
|
expect(listed).toBe(true);
|
||||||
const listed = await isLaunchAgentListed({ env });
|
});
|
||||||
expect(listed).toBe(true);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns false when the label is missing", async () => {
|
it("returns false when the label is missing", async () => {
|
||||||
|
|||||||
@@ -176,9 +176,7 @@ export async function isLaunchAgentListed(args: {
|
|||||||
const label = resolveLaunchAgentLabel({ env: args.env });
|
const label = resolveLaunchAgentLabel({ env: args.env });
|
||||||
const res = await execLaunchctl(["list"]);
|
const res = await execLaunchctl(["list"]);
|
||||||
if (res.code !== 0) return false;
|
if (res.code !== 0) return false;
|
||||||
return res.stdout
|
return res.stdout.split(/\r?\n/).some((line) => line.trim().split(/\s+/).at(-1) === label);
|
||||||
.split(/\r?\n/)
|
|
||||||
.some((line) => line.trim().split(/\s+/).at(-1) === label);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function launchAgentPlistExists(
|
export async function launchAgentPlistExists(
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { ChannelType, MessageType } from "@buape/carbon";
|
|||||||
import { Routes } from "discord-api-types/v10";
|
import { Routes } from "discord-api-types/v10";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { __resetDiscordChannelInfoCacheForTest } from "./monitor/message-utils.js";
|
||||||
|
|
||||||
const sendMock = vi.fn();
|
const sendMock = vi.fn();
|
||||||
const reactMock = vi.fn();
|
const reactMock = vi.fn();
|
||||||
const updateLastRouteMock = vi.fn();
|
const updateLastRouteMock = vi.fn();
|
||||||
@@ -42,7 +44,7 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
|
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
|
||||||
upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true });
|
upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true });
|
||||||
vi.resetModules();
|
__resetDiscordChannelInfoCacheForTest();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("discord tool result dispatch", () => {
|
describe("discord tool result dispatch", () => {
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ const DISCORD_CHANNEL_INFO_CACHE = new Map<
|
|||||||
{ value: DiscordChannelInfo | null; expiresAt: number }
|
{ value: DiscordChannelInfo | null; expiresAt: number }
|
||||||
>();
|
>();
|
||||||
|
|
||||||
|
export function __resetDiscordChannelInfoCacheForTest() {
|
||||||
|
DISCORD_CHANNEL_INFO_CACHE.clear();
|
||||||
|
}
|
||||||
|
|
||||||
export async function resolveDiscordChannelInfo(
|
export async function resolveDiscordChannelInfo(
|
||||||
client: Client,
|
client: Client,
|
||||||
channelId: string,
|
channelId: string,
|
||||||
|
|||||||
@@ -27,9 +27,10 @@ describe("runBootOnce", () => {
|
|||||||
|
|
||||||
it("skips when BOOT.md is missing", async () => {
|
it("skips when BOOT.md is missing", async () => {
|
||||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-boot-"));
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-boot-"));
|
||||||
await expect(
|
await expect(runBootOnce({ cfg: {}, deps: makeDeps(), workspaceDir })).resolves.toEqual({
|
||||||
runBootOnce({ cfg: {}, deps: makeDeps(), workspaceDir }),
|
status: "skipped",
|
||||||
).resolves.toEqual({ status: "skipped", reason: "missing" });
|
reason: "missing",
|
||||||
|
});
|
||||||
expect(agentCommand).not.toHaveBeenCalled();
|
expect(agentCommand).not.toHaveBeenCalled();
|
||||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
await fs.rm(workspaceDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
@@ -37,9 +38,10 @@ describe("runBootOnce", () => {
|
|||||||
it("skips when BOOT.md is empty", async () => {
|
it("skips when BOOT.md is empty", async () => {
|
||||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-boot-"));
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-boot-"));
|
||||||
await fs.writeFile(path.join(workspaceDir, "BOOT.md"), " \n", "utf-8");
|
await fs.writeFile(path.join(workspaceDir, "BOOT.md"), " \n", "utf-8");
|
||||||
await expect(
|
await expect(runBootOnce({ cfg: {}, deps: makeDeps(), workspaceDir })).resolves.toEqual({
|
||||||
runBootOnce({ cfg: {}, deps: makeDeps(), workspaceDir }),
|
status: "skipped",
|
||||||
).resolves.toEqual({ status: "skipped", reason: "empty" });
|
reason: "empty",
|
||||||
|
});
|
||||||
expect(agentCommand).not.toHaveBeenCalled();
|
expect(agentCommand).not.toHaveBeenCalled();
|
||||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
await fs.rm(workspaceDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
@@ -50,9 +52,9 @@ describe("runBootOnce", () => {
|
|||||||
await fs.writeFile(path.join(workspaceDir, "BOOT.md"), content, "utf-8");
|
await fs.writeFile(path.join(workspaceDir, "BOOT.md"), content, "utf-8");
|
||||||
|
|
||||||
agentCommand.mockResolvedValue(undefined);
|
agentCommand.mockResolvedValue(undefined);
|
||||||
await expect(
|
await expect(runBootOnce({ cfg: {}, deps: makeDeps(), workspaceDir })).resolves.toEqual({
|
||||||
runBootOnce({ cfg: {}, deps: makeDeps(), workspaceDir }),
|
status: "ran",
|
||||||
).resolves.toEqual({ status: "ran" });
|
});
|
||||||
|
|
||||||
expect(agentCommand).toHaveBeenCalledTimes(1);
|
expect(agentCommand).toHaveBeenCalledTimes(1);
|
||||||
const call = agentCommand.mock.calls[0]?.[0];
|
const call = agentCommand.mock.calls[0]?.[0];
|
||||||
|
|||||||
@@ -141,7 +141,6 @@ describe("gateway wizard (e2e)", () => {
|
|||||||
process.env.HOME = tempHome;
|
process.env.HOME = tempHome;
|
||||||
delete process.env.CLAWDBOT_STATE_DIR;
|
delete process.env.CLAWDBOT_STATE_DIR;
|
||||||
delete process.env.CLAWDBOT_CONFIG_PATH;
|
delete process.env.CLAWDBOT_CONFIG_PATH;
|
||||||
vi.resetModules();
|
|
||||||
|
|
||||||
const wizardToken = `wiz-${randomUUID()}`;
|
const wizardToken = `wiz-${randomUUID()}`;
|
||||||
const port = await getFreeGatewayPort();
|
const port = await getFreeGatewayPort();
|
||||||
@@ -187,8 +186,8 @@ describe("gateway wizard (e2e)", () => {
|
|||||||
expect(didSendToken).toBe(true);
|
expect(didSendToken).toBe(true);
|
||||||
expect(next.status).toBe("done");
|
expect(next.status).toBe("done");
|
||||||
|
|
||||||
const { CONFIG_PATH_CLAWDBOT } = await import("../config/config.js");
|
const { resolveConfigPath } = await import("../config/config.js");
|
||||||
const parsed = JSON.parse(await fs.readFile(CONFIG_PATH_CLAWDBOT, "utf8"));
|
const parsed = JSON.parse(await fs.readFile(resolveConfigPath(), "utf8"));
|
||||||
const token = (parsed as Record<string, unknown>)?.gateway as
|
const token = (parsed as Record<string, unknown>)?.gateway as
|
||||||
| Record<string, unknown>
|
| Record<string, unknown>
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import fs from "node:fs/promises";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||||
|
import type { PluginRegistry } from "../plugins/registry.js";
|
||||||
import {
|
import {
|
||||||
agentCommand,
|
agentCommand,
|
||||||
connectOk,
|
connectOk,
|
||||||
@@ -14,6 +16,33 @@ import {
|
|||||||
|
|
||||||
installGatewayTestHooks();
|
installGatewayTestHooks();
|
||||||
|
|
||||||
|
const registryState = vi.hoisted(() => ({
|
||||||
|
registry: {
|
||||||
|
plugins: [],
|
||||||
|
tools: [],
|
||||||
|
channels: [],
|
||||||
|
providers: [],
|
||||||
|
gatewayHandlers: {},
|
||||||
|
httpHandlers: [],
|
||||||
|
cliRegistrars: [],
|
||||||
|
services: [],
|
||||||
|
diagnostics: [],
|
||||||
|
} as PluginRegistry,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./server-plugins.js", async () => {
|
||||||
|
const { setActivePluginRegistry } = await import("../plugins/runtime.js");
|
||||||
|
return {
|
||||||
|
loadGatewayPlugins: (params: { baseMethods: string[] }) => {
|
||||||
|
setActivePluginRegistry(registryState.registry);
|
||||||
|
return {
|
||||||
|
pluginRegistry: registryState.registry,
|
||||||
|
gatewayMethods: params.baseMethods ?? [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const BASE_IMAGE_PNG =
|
const BASE_IMAGE_PNG =
|
||||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+X3mIAAAAASUVORK5CYII=";
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+X3mIAAAAASUVORK5CYII=";
|
||||||
|
|
||||||
@@ -22,8 +51,96 @@ function expectChannels(call: Record<string, unknown>, channel: string) {
|
|||||||
expect(call.messageChannel).toBe(channel);
|
expect(call.messageChannel).toBe(channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createRegistry = (channels: PluginRegistry["channels"]): PluginRegistry => ({
|
||||||
|
plugins: [],
|
||||||
|
tools: [],
|
||||||
|
channels,
|
||||||
|
providers: [],
|
||||||
|
gatewayHandlers: {},
|
||||||
|
httpHandlers: [],
|
||||||
|
cliRegistrars: [],
|
||||||
|
services: [],
|
||||||
|
diagnostics: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const createStubChannelPlugin = (params: {
|
||||||
|
id: ChannelPlugin["id"];
|
||||||
|
label: string;
|
||||||
|
resolveAllowFrom?: (cfg: Record<string, unknown>) => string[];
|
||||||
|
}): ChannelPlugin => ({
|
||||||
|
id: params.id,
|
||||||
|
meta: {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
outbound: {
|
||||||
|
deliveryMode: "direct",
|
||||||
|
resolveTarget: ({ to, allowFrom }) => {
|
||||||
|
const trimmed = to?.trim() ?? "";
|
||||||
|
if (trimmed) return { ok: true, to: trimmed };
|
||||||
|
const first = allowFrom?.[0];
|
||||||
|
if (first) return { ok: true, to: String(first) };
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: new Error(`missing target for ${params.id}`),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
sendText: async () => ({ channel: params.id, messageId: "msg-test" }),
|
||||||
|
sendMedia: async () => ({ channel: params.id, messageId: "msg-test" }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultRegistry = createRegistry([
|
||||||
|
{
|
||||||
|
pluginId: "whatsapp",
|
||||||
|
source: "test",
|
||||||
|
plugin: createStubChannelPlugin({
|
||||||
|
id: "whatsapp",
|
||||||
|
label: "WhatsApp",
|
||||||
|
resolveAllowFrom: (cfg) => {
|
||||||
|
const channels = cfg.channels as Record<string, unknown> | undefined;
|
||||||
|
const entry = channels?.whatsapp as Record<string, unknown> | undefined;
|
||||||
|
const allow = entry?.allowFrom;
|
||||||
|
return Array.isArray(allow) ? allow.map((value) => String(value)) : [];
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pluginId: "telegram",
|
||||||
|
source: "test",
|
||||||
|
plugin: createStubChannelPlugin({ id: "telegram", label: "Telegram" }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pluginId: "discord",
|
||||||
|
source: "test",
|
||||||
|
plugin: createStubChannelPlugin({ id: "discord", label: "Discord" }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pluginId: "slack",
|
||||||
|
source: "test",
|
||||||
|
plugin: createStubChannelPlugin({ id: "slack", label: "Slack" }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pluginId: "signal",
|
||||||
|
source: "test",
|
||||||
|
plugin: createStubChannelPlugin({ id: "signal", label: "Signal" }),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
describe("gateway server agent", () => {
|
describe("gateway server agent", () => {
|
||||||
test("agent marks implicit delivery when lastTo is stale", async () => {
|
test("agent marks implicit delivery when lastTo is stale", async () => {
|
||||||
|
registryState.registry = defaultRegistry;
|
||||||
testState.allowFrom = ["+436769770569"];
|
testState.allowFrom = ["+436769770569"];
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||||
@@ -63,6 +180,7 @@ describe("gateway server agent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("agent forwards sessionKey to agentCommand", async () => {
|
test("agent forwards sessionKey to agentCommand", async () => {
|
||||||
|
registryState.registry = defaultRegistry;
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||||
await writeSessionStore({
|
await writeSessionStore({
|
||||||
@@ -97,6 +215,7 @@ describe("gateway server agent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("agent forwards accountId to agentCommand", async () => {
|
test("agent forwards accountId to agentCommand", async () => {
|
||||||
|
registryState.registry = defaultRegistry;
|
||||||
testState.allowFrom = ["+1555"];
|
testState.allowFrom = ["+1555"];
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||||
@@ -136,6 +255,7 @@ describe("gateway server agent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("agent avoids lastAccountId when explicit to is provided", async () => {
|
test("agent avoids lastAccountId when explicit to is provided", async () => {
|
||||||
|
registryState.registry = defaultRegistry;
|
||||||
testState.allowFrom = ["+1555"];
|
testState.allowFrom = ["+1555"];
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||||
@@ -175,6 +295,7 @@ describe("gateway server agent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("agent keeps explicit accountId when explicit to is provided", async () => {
|
test("agent keeps explicit accountId when explicit to is provided", async () => {
|
||||||
|
registryState.registry = defaultRegistry;
|
||||||
testState.allowFrom = ["+1555"];
|
testState.allowFrom = ["+1555"];
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||||
@@ -215,6 +336,7 @@ describe("gateway server agent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("agent falls back to lastAccountId for implicit delivery", async () => {
|
test("agent falls back to lastAccountId for implicit delivery", async () => {
|
||||||
|
registryState.registry = defaultRegistry;
|
||||||
testState.allowFrom = ["+1555"];
|
testState.allowFrom = ["+1555"];
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||||
@@ -253,6 +375,7 @@ describe("gateway server agent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("agent forwards image attachments as images[]", async () => {
|
test("agent forwards image attachments as images[]", async () => {
|
||||||
|
registryState.registry = defaultRegistry;
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||||
await writeSessionStore({
|
await writeSessionStore({
|
||||||
@@ -299,6 +422,7 @@ describe("gateway server agent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("agent falls back to whatsapp when delivery requested and no last channel exists", async () => {
|
test("agent falls back to whatsapp when delivery requested and no last channel exists", async () => {
|
||||||
|
registryState.registry = defaultRegistry;
|
||||||
testState.allowFrom = ["+1555"];
|
testState.allowFrom = ["+1555"];
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||||
@@ -335,6 +459,7 @@ describe("gateway server agent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("agent routes main last-channel whatsapp", async () => {
|
test("agent routes main last-channel whatsapp", async () => {
|
||||||
|
registryState.registry = defaultRegistry;
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||||
await writeSessionStore({
|
await writeSessionStore({
|
||||||
@@ -374,6 +499,7 @@ describe("gateway server agent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("agent routes main last-channel telegram", async () => {
|
test("agent routes main last-channel telegram", async () => {
|
||||||
|
registryState.registry = defaultRegistry;
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||||
await writeSessionStore({
|
await writeSessionStore({
|
||||||
@@ -412,6 +538,7 @@ describe("gateway server agent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("agent routes main last-channel discord", async () => {
|
test("agent routes main last-channel discord", async () => {
|
||||||
|
registryState.registry = defaultRegistry;
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||||
await writeSessionStore({
|
await writeSessionStore({
|
||||||
@@ -450,6 +577,7 @@ describe("gateway server agent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("agent routes main last-channel slack", async () => {
|
test("agent routes main last-channel slack", async () => {
|
||||||
|
registryState.registry = defaultRegistry;
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||||
await writeSessionStore({
|
await writeSessionStore({
|
||||||
@@ -488,6 +616,7 @@ describe("gateway server agent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("agent routes main last-channel signal", async () => {
|
test("agent routes main last-channel signal", async () => {
|
||||||
|
registryState.registry = defaultRegistry;
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||||
await writeSessionStore({
|
await writeSessionStore({
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||||
|
import type { PluginRegistry } from "../plugins/registry.js";
|
||||||
import {
|
import {
|
||||||
connectOk,
|
connectOk,
|
||||||
installGatewayTestHooks,
|
installGatewayTestHooks,
|
||||||
@@ -10,6 +12,125 @@ const loadConfigHelpers = async () => await import("../config/config.js");
|
|||||||
|
|
||||||
installGatewayTestHooks();
|
installGatewayTestHooks();
|
||||||
|
|
||||||
|
const registryState = vi.hoisted(() => ({
|
||||||
|
registry: {
|
||||||
|
plugins: [],
|
||||||
|
tools: [],
|
||||||
|
channels: [],
|
||||||
|
providers: [],
|
||||||
|
gatewayHandlers: {},
|
||||||
|
httpHandlers: [],
|
||||||
|
cliRegistrars: [],
|
||||||
|
services: [],
|
||||||
|
diagnostics: [],
|
||||||
|
} as PluginRegistry,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./server-plugins.js", async () => {
|
||||||
|
const { setActivePluginRegistry } = await import("../plugins/runtime.js");
|
||||||
|
return {
|
||||||
|
loadGatewayPlugins: (params: { baseMethods: string[] }) => {
|
||||||
|
setActivePluginRegistry(registryState.registry);
|
||||||
|
return {
|
||||||
|
pluginRegistry: registryState.registry,
|
||||||
|
gatewayMethods: params.baseMethods ?? [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const createRegistry = (channels: PluginRegistry["channels"]): PluginRegistry => ({
|
||||||
|
plugins: [],
|
||||||
|
tools: [],
|
||||||
|
channels,
|
||||||
|
providers: [],
|
||||||
|
gatewayHandlers: {},
|
||||||
|
httpHandlers: [],
|
||||||
|
cliRegistrars: [],
|
||||||
|
services: [],
|
||||||
|
diagnostics: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const createStubChannelPlugin = (params: {
|
||||||
|
id: ChannelPlugin["id"];
|
||||||
|
label: string;
|
||||||
|
summary?: Record<string, unknown>;
|
||||||
|
logoutCleared?: boolean;
|
||||||
|
}): ChannelPlugin => ({
|
||||||
|
id: params.id,
|
||||||
|
meta: {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
buildChannelSummary: async () => ({
|
||||||
|
configured: false,
|
||||||
|
...(params.summary ?? {}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
gateway: {
|
||||||
|
logoutAccount: async () => ({
|
||||||
|
cleared: params.logoutCleared ?? false,
|
||||||
|
envToken: false,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const telegramPlugin: ChannelPlugin = {
|
||||||
|
...createStubChannelPlugin({
|
||||||
|
id: "telegram",
|
||||||
|
label: "Telegram",
|
||||||
|
summary: { tokenSource: "none", lastProbeAt: null },
|
||||||
|
logoutCleared: true,
|
||||||
|
}),
|
||||||
|
gateway: {
|
||||||
|
logoutAccount: async ({ cfg }) => {
|
||||||
|
const { writeConfigFile } = await import("../config/config.js");
|
||||||
|
const nextTelegram = cfg.channels?.telegram ? { ...cfg.channels.telegram } : {};
|
||||||
|
delete nextTelegram.botToken;
|
||||||
|
await writeConfigFile({
|
||||||
|
...cfg,
|
||||||
|
channels: {
|
||||||
|
...cfg.channels,
|
||||||
|
telegram: nextTelegram,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { cleared: true, envToken: false, loggedOut: true };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultRegistry = createRegistry([
|
||||||
|
{
|
||||||
|
pluginId: "whatsapp",
|
||||||
|
source: "test",
|
||||||
|
plugin: createStubChannelPlugin({ id: "whatsapp", label: "WhatsApp" }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pluginId: "telegram",
|
||||||
|
source: "test",
|
||||||
|
plugin: telegramPlugin,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pluginId: "signal",
|
||||||
|
source: "test",
|
||||||
|
plugin: createStubChannelPlugin({
|
||||||
|
id: "signal",
|
||||||
|
label: "Signal",
|
||||||
|
summary: { lastProbeAt: null },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
const servers: Array<Awaited<ReturnType<typeof startServerWithClient>>> = [];
|
const servers: Array<Awaited<ReturnType<typeof startServerWithClient>>> = [];
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@@ -28,6 +149,7 @@ afterEach(async () => {
|
|||||||
describe("gateway server channels", () => {
|
describe("gateway server channels", () => {
|
||||||
test("channels.status returns snapshot without probe", async () => {
|
test("channels.status returns snapshot without probe", async () => {
|
||||||
vi.stubEnv("TELEGRAM_BOT_TOKEN", undefined);
|
vi.stubEnv("TELEGRAM_BOT_TOKEN", undefined);
|
||||||
|
registryState.registry = defaultRegistry;
|
||||||
const result = await startServerWithClient();
|
const result = await startServerWithClient();
|
||||||
servers.push(result);
|
servers.push(result);
|
||||||
const { ws } = result;
|
const { ws } = result;
|
||||||
@@ -59,6 +181,7 @@ describe("gateway server channels", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("channels.logout reports no session when missing", async () => {
|
test("channels.logout reports no session when missing", async () => {
|
||||||
|
registryState.registry = defaultRegistry;
|
||||||
const result = await startServerWithClient();
|
const result = await startServerWithClient();
|
||||||
servers.push(result);
|
servers.push(result);
|
||||||
const { ws } = result;
|
const { ws } = result;
|
||||||
@@ -74,6 +197,7 @@ describe("gateway server channels", () => {
|
|||||||
|
|
||||||
test("channels.logout clears telegram bot token from config", async () => {
|
test("channels.logout clears telegram bot token from config", async () => {
|
||||||
vi.stubEnv("TELEGRAM_BOT_TOKEN", undefined);
|
vi.stubEnv("TELEGRAM_BOT_TOKEN", undefined);
|
||||||
|
registryState.registry = defaultRegistry;
|
||||||
const { readConfigFileSnapshot, writeConfigFile } = await loadConfigHelpers();
|
const { readConfigFileSnapshot, writeConfigFile } = await loadConfigHelpers();
|
||||||
await writeConfigFile({
|
await writeConfigFile({
|
||||||
channels: {
|
channels: {
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { createServer } from "node:net";
|
import { createServer } from "node:net";
|
||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
import { resolveCanvasHostUrl } from "../infra/canvas-host-url.js";
|
import { resolveCanvasHostUrl } from "../infra/canvas-host-url.js";
|
||||||
|
import { getChannelPlugin } from "../channels/plugins/index.js";
|
||||||
import { GatewayLockError } from "../infra/gateway-lock.js";
|
import { GatewayLockError } from "../infra/gateway-lock.js";
|
||||||
|
import type { ChannelOutboundAdapter } from "../channels/plugins/types.js";
|
||||||
|
import type { PluginRegistry } from "../plugins/registry.js";
|
||||||
|
import { getActivePluginRegistry, setActivePluginRegistry } from "../plugins/runtime.js";
|
||||||
|
import { createOutboundTestPlugin } from "../test-utils/channel-plugins.js";
|
||||||
import {
|
import {
|
||||||
connectOk,
|
connectOk,
|
||||||
getFreePort,
|
getFreePort,
|
||||||
@@ -16,6 +21,49 @@ import {
|
|||||||
|
|
||||||
installGatewayTestHooks();
|
installGatewayTestHooks();
|
||||||
|
|
||||||
|
const whatsappOutbound: ChannelOutboundAdapter = {
|
||||||
|
deliveryMode: "direct",
|
||||||
|
sendText: async ({ deps, to, text }) => {
|
||||||
|
if (!deps?.sendWhatsApp) {
|
||||||
|
throw new Error("Missing sendWhatsApp dep");
|
||||||
|
}
|
||||||
|
return { channel: "whatsapp", ...(await deps.sendWhatsApp(to, text, {})) };
|
||||||
|
},
|
||||||
|
sendMedia: async ({ deps, to, text, mediaUrl }) => {
|
||||||
|
if (!deps?.sendWhatsApp) {
|
||||||
|
throw new Error("Missing sendWhatsApp dep");
|
||||||
|
}
|
||||||
|
return { channel: "whatsapp", ...(await deps.sendWhatsApp(to, text, { mediaUrl })) };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const whatsappPlugin = createOutboundTestPlugin({
|
||||||
|
id: "whatsapp",
|
||||||
|
outbound: whatsappOutbound,
|
||||||
|
label: "WhatsApp",
|
||||||
|
});
|
||||||
|
|
||||||
|
const createRegistry = (channels: PluginRegistry["channels"]): PluginRegistry => ({
|
||||||
|
plugins: [],
|
||||||
|
tools: [],
|
||||||
|
channels,
|
||||||
|
providers: [],
|
||||||
|
gatewayHandlers: {},
|
||||||
|
httpHandlers: [],
|
||||||
|
cliRegistrars: [],
|
||||||
|
services: [],
|
||||||
|
diagnostics: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const whatsappRegistry = createRegistry([
|
||||||
|
{
|
||||||
|
pluginId: "whatsapp",
|
||||||
|
source: "test",
|
||||||
|
plugin: whatsappPlugin,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const emptyRegistry = createRegistry([]);
|
||||||
|
|
||||||
describe("gateway server misc", () => {
|
describe("gateway server misc", () => {
|
||||||
test("hello-ok advertises the gateway port for canvas host", async () => {
|
test("hello-ok advertises the gateway port for canvas host", async () => {
|
||||||
const prevToken = process.env.CLAWDBOT_GATEWAY_TOKEN;
|
const prevToken = process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||||
@@ -47,31 +95,38 @@ describe("gateway server misc", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("send dedupes by idempotencyKey", { timeout: 60_000 }, async () => {
|
test("send dedupes by idempotencyKey", { timeout: 60_000 }, async () => {
|
||||||
const { server, ws } = await startServerWithClient();
|
const prevRegistry = getActivePluginRegistry() ?? emptyRegistry;
|
||||||
await connectOk(ws);
|
try {
|
||||||
|
const { server, ws } = await startServerWithClient();
|
||||||
|
await connectOk(ws);
|
||||||
|
setActivePluginRegistry(whatsappRegistry);
|
||||||
|
expect(getChannelPlugin("whatsapp")).toBeDefined();
|
||||||
|
|
||||||
const idem = "same-key";
|
const idem = "same-key";
|
||||||
const res1P = onceMessage(ws, (o) => o.type === "res" && o.id === "a1");
|
const res1P = onceMessage(ws, (o) => o.type === "res" && o.id === "a1");
|
||||||
const res2P = onceMessage(ws, (o) => o.type === "res" && o.id === "a2");
|
const res2P = onceMessage(ws, (o) => o.type === "res" && o.id === "a2");
|
||||||
const sendReq = (id: string) =>
|
const sendReq = (id: string) =>
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: "req",
|
type: "req",
|
||||||
id,
|
id,
|
||||||
method: "send",
|
method: "send",
|
||||||
params: { to: "+15550000000", message: "hi", idempotencyKey: idem },
|
params: { to: "+15550000000", message: "hi", idempotencyKey: idem },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
sendReq("a1");
|
sendReq("a1");
|
||||||
sendReq("a2");
|
sendReq("a2");
|
||||||
|
|
||||||
const res1 = await res1P;
|
const res1 = await res1P;
|
||||||
const res2 = await res2P;
|
const res2 = await res2P;
|
||||||
expect(res1.ok).toBe(true);
|
expect(res1.ok).toBe(true);
|
||||||
expect(res2.ok).toBe(true);
|
expect(res2.ok).toBe(true);
|
||||||
expect(res1.payload).toEqual(res2.payload);
|
expect(res1.payload).toEqual(res2.payload);
|
||||||
ws.close();
|
ws.close();
|
||||||
await server.close();
|
await server.close();
|
||||||
|
} finally {
|
||||||
|
setActivePluginRegistry(prevRegistry);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("refuses to start when port already bound", async () => {
|
test("refuses to start when port already bound", async () => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
|
||||||
|
|
||||||
export type BridgeClientInfo = {
|
export type BridgeClientInfo = {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
@@ -91,6 +92,7 @@ export const testState = {
|
|||||||
agentConfig: undefined as Record<string, unknown> | undefined,
|
agentConfig: undefined as Record<string, unknown> | undefined,
|
||||||
agentsConfig: undefined as Record<string, unknown> | undefined,
|
agentsConfig: undefined as Record<string, unknown> | undefined,
|
||||||
bindingsConfig: undefined as Array<Record<string, unknown>> | undefined,
|
bindingsConfig: undefined as Array<Record<string, unknown>> | undefined,
|
||||||
|
channelsConfig: undefined as Record<string, unknown> | undefined,
|
||||||
sessionStorePath: undefined as string | undefined,
|
sessionStorePath: undefined as string | undefined,
|
||||||
sessionConfig: undefined as Record<string, unknown> | undefined,
|
sessionConfig: undefined as Record<string, unknown> | undefined,
|
||||||
allowFrom: undefined as string[] | undefined,
|
allowFrom: undefined as string[] | undefined,
|
||||||
@@ -259,49 +261,63 @@ vi.mock("../config/config.js", async () => {
|
|||||||
config: testState.migrationConfig ?? (raw as Record<string, unknown>),
|
config: testState.migrationConfig ?? (raw as Record<string, unknown>),
|
||||||
changes: testState.migrationChanges,
|
changes: testState.migrationChanges,
|
||||||
}),
|
}),
|
||||||
loadConfig: () => ({
|
loadConfig: () => {
|
||||||
agents: (() => {
|
const base = {
|
||||||
const defaults = {
|
agents: (() => {
|
||||||
model: "anthropic/claude-opus-4-5",
|
const defaults = {
|
||||||
workspace: path.join(os.tmpdir(), "clawd-gateway-test"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
...testState.agentConfig,
|
workspace: path.join(os.tmpdir(), "clawd-gateway-test"),
|
||||||
};
|
...testState.agentConfig,
|
||||||
if (testState.agentsConfig) {
|
};
|
||||||
return { ...testState.agentsConfig, defaults };
|
if (testState.agentsConfig) {
|
||||||
}
|
return { ...testState.agentsConfig, defaults };
|
||||||
return { defaults };
|
}
|
||||||
})(),
|
return { defaults };
|
||||||
bindings: testState.bindingsConfig,
|
})(),
|
||||||
channels: {
|
bindings: testState.bindingsConfig,
|
||||||
whatsapp: {
|
channels: (() => {
|
||||||
allowFrom: testState.allowFrom,
|
const baseChannels =
|
||||||
|
testState.channelsConfig && typeof testState.channelsConfig === "object"
|
||||||
|
? { ...testState.channelsConfig }
|
||||||
|
: {};
|
||||||
|
const existing = baseChannels.whatsapp;
|
||||||
|
const mergedWhatsApp =
|
||||||
|
existing && typeof existing === "object" && !Array.isArray(existing)
|
||||||
|
? { ...existing }
|
||||||
|
: {};
|
||||||
|
if (testState.allowFrom !== undefined) {
|
||||||
|
mergedWhatsApp.allowFrom = testState.allowFrom;
|
||||||
|
}
|
||||||
|
baseChannels.whatsapp = mergedWhatsApp;
|
||||||
|
return baseChannels;
|
||||||
|
})(),
|
||||||
|
session: {
|
||||||
|
mainKey: "main",
|
||||||
|
store: testState.sessionStorePath,
|
||||||
|
...testState.sessionConfig,
|
||||||
},
|
},
|
||||||
},
|
gateway: (() => {
|
||||||
session: {
|
const gateway: Record<string, unknown> = {};
|
||||||
mainKey: "main",
|
if (testState.gatewayBind) gateway.bind = testState.gatewayBind;
|
||||||
store: testState.sessionStorePath,
|
if (testState.gatewayAuth) gateway.auth = testState.gatewayAuth;
|
||||||
...testState.sessionConfig,
|
return Object.keys(gateway).length > 0 ? gateway : undefined;
|
||||||
},
|
})(),
|
||||||
gateway: (() => {
|
canvasHost: (() => {
|
||||||
const gateway: Record<string, unknown> = {};
|
const canvasHost: Record<string, unknown> = {};
|
||||||
if (testState.gatewayBind) gateway.bind = testState.gatewayBind;
|
if (typeof testState.canvasHostPort === "number")
|
||||||
if (testState.gatewayAuth) gateway.auth = testState.gatewayAuth;
|
canvasHost.port = testState.canvasHostPort;
|
||||||
return Object.keys(gateway).length > 0 ? gateway : undefined;
|
return Object.keys(canvasHost).length > 0 ? canvasHost : undefined;
|
||||||
})(),
|
})(),
|
||||||
canvasHost: (() => {
|
hooks: testState.hooksConfig,
|
||||||
const canvasHost: Record<string, unknown> = {};
|
cron: (() => {
|
||||||
if (typeof testState.canvasHostPort === "number")
|
const cron: Record<string, unknown> = {};
|
||||||
canvasHost.port = testState.canvasHostPort;
|
if (typeof testState.cronEnabled === "boolean") cron.enabled = testState.cronEnabled;
|
||||||
return Object.keys(canvasHost).length > 0 ? canvasHost : undefined;
|
if (typeof testState.cronStorePath === "string") cron.store = testState.cronStorePath;
|
||||||
})(),
|
return Object.keys(cron).length > 0 ? cron : undefined;
|
||||||
hooks: testState.hooksConfig,
|
})(),
|
||||||
cron: (() => {
|
} as ReturnType<typeof actual.loadConfig>;
|
||||||
const cron: Record<string, unknown> = {};
|
return applyPluginAutoEnable({ config: base }).config;
|
||||||
if (typeof testState.cronEnabled === "boolean") cron.enabled = testState.cronEnabled;
|
},
|
||||||
if (typeof testState.cronStorePath === "string") cron.store = testState.cronStorePath;
|
|
||||||
return Object.keys(cron).length > 0 ? cron : undefined;
|
|
||||||
})(),
|
|
||||||
}),
|
|
||||||
parseConfigJson5: (raw: string) => {
|
parseConfigJson5: (raw: string) => {
|
||||||
try {
|
try {
|
||||||
return { ok: true, parsed: JSON.parse(raw) as unknown };
|
return { ok: true, parsed: JSON.parse(raw) as unknown };
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export function installGatewayTestHooks() {
|
|||||||
testState.agentConfig = undefined;
|
testState.agentConfig = undefined;
|
||||||
testState.agentsConfig = undefined;
|
testState.agentsConfig = undefined;
|
||||||
testState.bindingsConfig = undefined;
|
testState.bindingsConfig = undefined;
|
||||||
|
testState.channelsConfig = undefined;
|
||||||
testState.allowFrom = undefined;
|
testState.allowFrom = undefined;
|
||||||
testIsNixMode.value = false;
|
testIsNixMode.value = false;
|
||||||
cronIsolatedRun.mockClear();
|
cronIsolatedRun.mockClear();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
import * as tar from "tar";
|
import * as tar from "tar";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
const tempDirs: string[] = [];
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
@@ -15,22 +15,6 @@ function makeTempDir() {
|
|||||||
return dir;
|
return dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function withStateDir<T>(stateDir: string, fn: () => Promise<T>) {
|
|
||||||
const prev = process.env.CLAWDBOT_STATE_DIR;
|
|
||||||
process.env.CLAWDBOT_STATE_DIR = stateDir;
|
|
||||||
vi.resetModules();
|
|
||||||
try {
|
|
||||||
return await fn();
|
|
||||||
} finally {
|
|
||||||
if (prev === undefined) {
|
|
||||||
delete process.env.CLAWDBOT_STATE_DIR;
|
|
||||||
} else {
|
|
||||||
process.env.CLAWDBOT_STATE_DIR = prev;
|
|
||||||
}
|
|
||||||
vi.resetModules();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
for (const dir of tempDirs.splice(0)) {
|
for (const dir of tempDirs.splice(0)) {
|
||||||
try {
|
try {
|
||||||
@@ -72,10 +56,9 @@ describe("installHooksFromArchive", () => {
|
|||||||
const buffer = await zip.generateAsync({ type: "nodebuffer" });
|
const buffer = await zip.generateAsync({ type: "nodebuffer" });
|
||||||
fs.writeFileSync(archivePath, buffer);
|
fs.writeFileSync(archivePath, buffer);
|
||||||
|
|
||||||
const result = await withStateDir(stateDir, async () => {
|
const hooksDir = path.join(stateDir, "hooks");
|
||||||
const { installHooksFromArchive } = await import("./install.js");
|
const { installHooksFromArchive } = await import("./install.js");
|
||||||
return await installHooksFromArchive({ archivePath });
|
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.ok).toBe(true);
|
expect(result.ok).toBe(true);
|
||||||
if (!result.ok) return;
|
if (!result.ok) return;
|
||||||
@@ -121,10 +104,9 @@ describe("installHooksFromArchive", () => {
|
|||||||
);
|
);
|
||||||
await tar.c({ cwd: workDir, file: archivePath }, ["package"]);
|
await tar.c({ cwd: workDir, file: archivePath }, ["package"]);
|
||||||
|
|
||||||
const result = await withStateDir(stateDir, async () => {
|
const hooksDir = path.join(stateDir, "hooks");
|
||||||
const { installHooksFromArchive } = await import("./install.js");
|
const { installHooksFromArchive } = await import("./install.js");
|
||||||
return await installHooksFromArchive({ archivePath });
|
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.ok).toBe(true);
|
expect(result.ok).toBe(true);
|
||||||
if (!result.ok) return;
|
if (!result.ok) return;
|
||||||
@@ -155,10 +137,9 @@ describe("installHooksFromPath", () => {
|
|||||||
);
|
);
|
||||||
fs.writeFileSync(path.join(hookDir, "handler.ts"), "export default async () => {};\n");
|
fs.writeFileSync(path.join(hookDir, "handler.ts"), "export default async () => {};\n");
|
||||||
|
|
||||||
const result = await withStateDir(stateDir, async () => {
|
const hooksDir = path.join(stateDir, "hooks");
|
||||||
const { installHooksFromPath } = await import("./install.js");
|
const { installHooksFromPath } = await import("./install.js");
|
||||||
return await installHooksFromPath({ path: hookDir });
|
const result = await installHooksFromPath({ path: hookDir, hooksDir });
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.ok).toBe(true);
|
expect(result.ok).toBe(true);
|
||||||
if (!result.ok) return;
|
if (!result.ok) return;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
|
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import * as logging from "../logging.js";
|
||||||
|
|
||||||
const createService = vi.fn();
|
const createService = vi.fn();
|
||||||
const shutdown = vi.fn();
|
const shutdown = vi.fn();
|
||||||
@@ -23,14 +25,6 @@ vi.mock("../logger.js", () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("../logging.js", async () => {
|
|
||||||
const actual = await vi.importActual<typeof import("../logging.js")>("../logging.js");
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
getLogger: () => ({ info: (...args: unknown[]) => getLoggerInfo(...args) }),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mock("@homebridge/ciao", () => {
|
vi.mock("@homebridge/ciao", () => {
|
||||||
return {
|
return {
|
||||||
Protocol: { TCP: "tcp" },
|
Protocol: { TCP: "tcp" },
|
||||||
@@ -60,6 +54,12 @@ describe("gateway bonjour advertiser", () => {
|
|||||||
|
|
||||||
const prevEnv = { ...process.env };
|
const prevEnv = { ...process.env };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.spyOn(logging, "getLogger").mockReturnValue({
|
||||||
|
info: (...args: unknown[]) => getLoggerInfo(...args),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
for (const key of Object.keys(process.env)) {
|
for (const key of Object.keys(process.env)) {
|
||||||
if (!(key in prevEnv)) delete process.env[key];
|
if (!(key in prevEnv)) delete process.env[key];
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ describe("exec approvals command resolution", () => {
|
|||||||
const script = path.join(cwd, "bin", "tool");
|
const script = path.join(cwd, "bin", "tool");
|
||||||
fs.mkdirSync(path.dirname(script), { recursive: true });
|
fs.mkdirSync(path.dirname(script), { recursive: true });
|
||||||
fs.writeFileSync(script, "");
|
fs.writeFileSync(script, "");
|
||||||
const res = resolveCommandResolution("\"./bin/tool\" --version", cwd, undefined);
|
const res = resolveCommandResolution('"./bin/tool" --version', cwd, undefined);
|
||||||
expect(res?.resolvedPath).toBe(script);
|
expect(res?.resolvedPath).toBe(script);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -86,7 +86,12 @@ export async function requestExecHostViaSocket(params: {
|
|||||||
idx = buffer.indexOf("\n");
|
idx = buffer.indexOf("\n");
|
||||||
if (!line) continue;
|
if (!line) continue;
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(line) as { type?: string; ok?: boolean; payload?: unknown; error?: unknown };
|
const msg = JSON.parse(line) as {
|
||||||
|
type?: string;
|
||||||
|
ok?: boolean;
|
||||||
|
payload?: unknown;
|
||||||
|
error?: unknown;
|
||||||
|
};
|
||||||
if (msg?.type === "exec-res") {
|
if (msg?.type === "exec-res") {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
if (msg.ok === true && msg.payload) {
|
if (msg.ok === true && msg.payload) {
|
||||||
|
|||||||
@@ -1,4 +1,31 @@
|
|||||||
export * from "./logging/console.js";
|
export {
|
||||||
|
enableConsoleCapture,
|
||||||
|
getConsoleSettings,
|
||||||
|
getResolvedConsoleSettings,
|
||||||
|
routeLogsToStderr,
|
||||||
|
setConsoleSubsystemFilter,
|
||||||
|
setConsoleTimestampPrefix,
|
||||||
|
shouldLogSubsystemToConsole,
|
||||||
|
} from "./logging/console.js";
|
||||||
|
export type { ConsoleLoggerSettings, ConsoleStyle } from "./logging/console.js";
|
||||||
export type { LogLevel } from "./logging/levels.js";
|
export type { LogLevel } from "./logging/levels.js";
|
||||||
export * from "./logging/logger.js";
|
export { ALLOWED_LOG_LEVELS, levelToMinLevel, normalizeLogLevel } from "./logging/levels.js";
|
||||||
export * from "./logging/subsystem.js";
|
export {
|
||||||
|
DEFAULT_LOG_DIR,
|
||||||
|
DEFAULT_LOG_FILE,
|
||||||
|
getChildLogger,
|
||||||
|
getLogger,
|
||||||
|
getResolvedLoggerSettings,
|
||||||
|
isFileLogLevelEnabled,
|
||||||
|
resetLogger,
|
||||||
|
setLoggerOverride,
|
||||||
|
toPinoLikeLogger,
|
||||||
|
} from "./logging/logger.js";
|
||||||
|
export type { LoggerResolvedSettings, LoggerSettings, PinoLikeLogger } from "./logging/logger.js";
|
||||||
|
export {
|
||||||
|
createSubsystemLogger,
|
||||||
|
createSubsystemRuntime,
|
||||||
|
runtimeForLogger,
|
||||||
|
stripRedundantSubsystemPrefixForConsole,
|
||||||
|
} from "./logging/subsystem.js";
|
||||||
|
export type { SubsystemLogger } from "./logging/subsystem.js";
|
||||||
|
|||||||
@@ -1,21 +1,60 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { isPathWithinBase } from "../../test/helpers/paths.js";
|
import { isPathWithinBase } from "../../test/helpers/paths.js";
|
||||||
import { withTempHome } from "../../test/helpers/temp-home.js";
|
|
||||||
|
|
||||||
describe("media store", () => {
|
describe("media store", () => {
|
||||||
|
let store: typeof import("./store.js");
|
||||||
|
let home = "";
|
||||||
|
const envSnapshot: Record<string, string | undefined> = {};
|
||||||
|
|
||||||
|
const snapshotEnv = () => {
|
||||||
|
for (const key of ["HOME", "USERPROFILE", "HOMEDRIVE", "HOMEPATH", "CLAWDBOT_STATE_DIR"]) {
|
||||||
|
envSnapshot[key] = process.env[key];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const restoreEnv = () => {
|
||||||
|
for (const [key, value] of Object.entries(envSnapshot)) {
|
||||||
|
if (value === undefined) delete process.env[key];
|
||||||
|
else process.env[key] = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
snapshotEnv();
|
||||||
|
home = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-test-home-"));
|
||||||
|
process.env.HOME = home;
|
||||||
|
process.env.USERPROFILE = home;
|
||||||
|
process.env.CLAWDBOT_STATE_DIR = path.join(home, ".clawdbot");
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
const match = home.match(/^([A-Za-z]:)(.*)$/);
|
||||||
|
if (match) {
|
||||||
|
process.env.HOMEDRIVE = match[1];
|
||||||
|
process.env.HOMEPATH = match[2] || "\\";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await fs.mkdir(path.join(home, ".clawdbot"), { recursive: true });
|
||||||
|
store = await import("./store.js");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
restoreEnv();
|
||||||
|
try {
|
||||||
|
await fs.rm(home, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// ignore cleanup failures in tests
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async function withTempStore<T>(
|
async function withTempStore<T>(
|
||||||
fn: (store: typeof import("./store.js"), home: string) => Promise<T>,
|
fn: (store: typeof import("./store.js"), home: string) => Promise<T>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
return await withTempHome(async (home) => {
|
return await fn(store, home);
|
||||||
vi.resetModules();
|
|
||||||
const store = await import("./store.js");
|
|
||||||
return await fn(store, home);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
it("creates and returns media directory", async () => {
|
it("creates and returns media directory", async () => {
|
||||||
|
|||||||
@@ -4,29 +4,30 @@ import fs from "node:fs/promises";
|
|||||||
import { request } from "node:https";
|
import { request } from "node:https";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { pipeline } from "node:stream/promises";
|
import { pipeline } from "node:stream/promises";
|
||||||
import { CONFIG_DIR } from "../utils.js";
|
import { resolveConfigDir } from "../utils.js";
|
||||||
import { detectMime, extensionForMime } from "./mime.js";
|
import { detectMime, extensionForMime } from "./mime.js";
|
||||||
|
|
||||||
const MEDIA_DIR = path.join(CONFIG_DIR, "media");
|
const resolveMediaDir = () => path.join(resolveConfigDir(), "media");
|
||||||
const MAX_BYTES = 5 * 1024 * 1024; // 5MB default
|
const MAX_BYTES = 5 * 1024 * 1024; // 5MB default
|
||||||
const DEFAULT_TTL_MS = 2 * 60 * 1000; // 2 minutes
|
const DEFAULT_TTL_MS = 2 * 60 * 1000; // 2 minutes
|
||||||
|
|
||||||
export function getMediaDir() {
|
export function getMediaDir() {
|
||||||
return MEDIA_DIR;
|
return resolveMediaDir();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureMediaDir() {
|
export async function ensureMediaDir() {
|
||||||
await fs.mkdir(MEDIA_DIR, { recursive: true });
|
const mediaDir = resolveMediaDir();
|
||||||
return MEDIA_DIR;
|
await fs.mkdir(mediaDir, { recursive: true });
|
||||||
|
return mediaDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cleanOldMedia(ttlMs = DEFAULT_TTL_MS) {
|
export async function cleanOldMedia(ttlMs = DEFAULT_TTL_MS) {
|
||||||
await ensureMediaDir();
|
const mediaDir = await ensureMediaDir();
|
||||||
const entries = await fs.readdir(MEDIA_DIR).catch(() => []);
|
const entries = await fs.readdir(mediaDir).catch(() => []);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
entries.map(async (file) => {
|
entries.map(async (file) => {
|
||||||
const full = path.join(MEDIA_DIR, file);
|
const full = path.join(mediaDir, file);
|
||||||
const stat = await fs.stat(full).catch(() => null);
|
const stat = await fs.stat(full).catch(() => null);
|
||||||
if (!stat) return;
|
if (!stat) return;
|
||||||
if (now - stat.mtimeMs > ttlMs) {
|
if (now - stat.mtimeMs > ttlMs) {
|
||||||
@@ -110,7 +111,8 @@ export async function saveMediaSource(
|
|||||||
headers?: Record<string, string>,
|
headers?: Record<string, string>,
|
||||||
subdir = "",
|
subdir = "",
|
||||||
): Promise<SavedMedia> {
|
): Promise<SavedMedia> {
|
||||||
const dir = subdir ? path.join(MEDIA_DIR, subdir) : MEDIA_DIR;
|
const baseDir = resolveMediaDir();
|
||||||
|
const dir = subdir ? path.join(baseDir, subdir) : baseDir;
|
||||||
await fs.mkdir(dir, { recursive: true });
|
await fs.mkdir(dir, { recursive: true });
|
||||||
await cleanOldMedia();
|
await cleanOldMedia();
|
||||||
const baseId = crypto.randomUUID();
|
const baseId = crypto.randomUUID();
|
||||||
@@ -154,7 +156,7 @@ export async function saveMediaBuffer(
|
|||||||
if (buffer.byteLength > maxBytes) {
|
if (buffer.byteLength > maxBytes) {
|
||||||
throw new Error(`Media exceeds ${(maxBytes / (1024 * 1024)).toFixed(0)}MB limit`);
|
throw new Error(`Media exceeds ${(maxBytes / (1024 * 1024)).toFixed(0)}MB limit`);
|
||||||
}
|
}
|
||||||
const dir = path.join(MEDIA_DIR, subdir);
|
const dir = path.join(resolveMediaDir(), subdir);
|
||||||
await fs.mkdir(dir, { recursive: true });
|
await fs.mkdir(dir, { recursive: true });
|
||||||
const baseId = crypto.randomUUID();
|
const baseId = crypto.randomUUID();
|
||||||
const headerExt = extensionForMime(contentType?.split(";")[0]?.trim() ?? undefined);
|
const headerExt = extensionForMime(contentType?.split(";")[0]?.trim() ?? undefined);
|
||||||
|
|||||||
@@ -183,7 +183,9 @@ async function fetchGeminiBatchStatus(params: {
|
|||||||
batchName: string;
|
batchName: string;
|
||||||
}): Promise<GeminiBatchStatus> {
|
}): Promise<GeminiBatchStatus> {
|
||||||
const baseUrl = getGeminiBaseUrl(params.gemini);
|
const baseUrl = getGeminiBaseUrl(params.gemini);
|
||||||
const name = params.batchName.startsWith("batches/") ? params.batchName : `batches/${params.batchName}`;
|
const name = params.batchName.startsWith("batches/")
|
||||||
|
? params.batchName
|
||||||
|
: `batches/${params.batchName}`;
|
||||||
const statusUrl = `${baseUrl}/${name}`;
|
const statusUrl = `${baseUrl}/${name}`;
|
||||||
debugLog("memory embeddings: gemini batch status", { statusUrl });
|
debugLog("memory embeddings: gemini batch status", { statusUrl });
|
||||||
const res = await fetch(statusUrl, {
|
const res = await fetch(statusUrl, {
|
||||||
@@ -328,7 +330,11 @@ export async function runGeminiEmbeddingBatches(params: {
|
|||||||
requests: group.length,
|
requests: group.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!params.wait && batchInfo.state && !["SUCCEEDED", "COMPLETED", "DONE"].includes(batchInfo.state)) {
|
if (
|
||||||
|
!params.wait &&
|
||||||
|
batchInfo.state &&
|
||||||
|
!["SUCCEEDED", "COMPLETED", "DONE"].includes(batchInfo.state)
|
||||||
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`gemini batch ${batchName} submitted; enable remote.batch.wait to await completion`,
|
`gemini batch ${batchName} submitted; enable remote.batch.wait to await completion`,
|
||||||
);
|
);
|
||||||
@@ -376,8 +382,7 @@ export async function runGeminiEmbeddingBatches(params: {
|
|||||||
errors.push(`${customId}: ${line.response.error.message}`);
|
errors.push(`${customId}: ${line.response.error.message}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const embedding =
|
const embedding = line.embedding?.values ?? line.response?.embedding?.values ?? [];
|
||||||
line.embedding?.values ?? line.response?.embedding?.values ?? [];
|
|
||||||
if (embedding.length === 0) {
|
if (embedding.length === 0) {
|
||||||
errors.push(`${customId}: empty embedding`);
|
errors.push(`${customId}: empty embedding`);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -3,14 +3,8 @@ import fsSync from "node:fs";
|
|||||||
import type { Llama, LlamaEmbeddingContext, LlamaModel } from "node-llama-cpp";
|
import type { Llama, LlamaEmbeddingContext, LlamaModel } from "node-llama-cpp";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import { resolveUserPath } from "../utils.js";
|
import { resolveUserPath } from "../utils.js";
|
||||||
import {
|
import { createGeminiEmbeddingProvider, type GeminiEmbeddingClient } from "./embeddings-gemini.js";
|
||||||
createGeminiEmbeddingProvider,
|
import { createOpenAiEmbeddingProvider, type OpenAiEmbeddingClient } from "./embeddings-openai.js";
|
||||||
type GeminiEmbeddingClient,
|
|
||||||
} from "./embeddings-gemini.js";
|
|
||||||
import {
|
|
||||||
createOpenAiEmbeddingProvider,
|
|
||||||
type OpenAiEmbeddingClient,
|
|
||||||
} from "./embeddings-openai.js";
|
|
||||||
import { importNodeLlamaCpp } from "./node-llama.js";
|
import { importNodeLlamaCpp } from "./node-llama.js";
|
||||||
|
|
||||||
export type { GeminiEmbeddingClient } from "./embeddings-gemini.js";
|
export type { GeminiEmbeddingClient } from "./embeddings-gemini.js";
|
||||||
@@ -68,7 +62,6 @@ function isMissingApiKeyError(err: unknown): boolean {
|
|||||||
return message.includes("No API key found for provider");
|
return message.includes("No API key found for provider");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function createLocalEmbeddingProvider(
|
async function createLocalEmbeddingProvider(
|
||||||
options: EmbeddingProviderOptions,
|
options: EmbeddingProviderOptions,
|
||||||
): Promise<EmbeddingProvider> {
|
): Promise<EmbeddingProvider> {
|
||||||
@@ -188,9 +181,7 @@ export async function createEmbeddingProvider(
|
|||||||
fallbackReason: reason,
|
fallbackReason: reason,
|
||||||
};
|
};
|
||||||
} catch (fallbackErr) {
|
} catch (fallbackErr) {
|
||||||
throw new Error(
|
throw new Error(`${reason}\n\nFallback to ${fallback} failed: ${formatError(fallbackErr)}`);
|
||||||
`${reason}\n\nFallback to ${fallback} failed: ${formatError(fallbackErr)}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(reason);
|
throw new Error(reason);
|
||||||
|
|||||||
@@ -697,9 +697,7 @@ export class MemoryIndexManager {
|
|||||||
|
|
||||||
private async removeIndexFiles(basePath: string): Promise<void> {
|
private async removeIndexFiles(basePath: string): Promise<void> {
|
||||||
const suffixes = ["", "-wal", "-shm"];
|
const suffixes = ["", "-wal", "-shm"];
|
||||||
await Promise.all(
|
await Promise.all(suffixes.map((suffix) => fs.rm(`${basePath}${suffix}`, { force: true })));
|
||||||
suffixes.map((suffix) => fs.rm(`${basePath}${suffix}`, { force: true })),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensureSchema() {
|
private ensureSchema() {
|
||||||
@@ -1064,8 +1062,8 @@ export class MemoryIndexManager {
|
|||||||
const batch = this.settings.remote?.batch;
|
const batch = this.settings.remote?.batch;
|
||||||
const enabled = Boolean(
|
const enabled = Boolean(
|
||||||
batch?.enabled &&
|
batch?.enabled &&
|
||||||
((this.openAi && this.provider.id === "openai") ||
|
((this.openAi && this.provider.id === "openai") ||
|
||||||
(this.gemini && this.provider.id === "gemini")),
|
(this.gemini && this.provider.id === "gemini")),
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
enabled,
|
enabled,
|
||||||
|
|||||||
@@ -63,7 +63,11 @@ describe("memory vector dedupe", () => {
|
|||||||
if (!result.manager) throw new Error("manager missing");
|
if (!result.manager) throw new Error("manager missing");
|
||||||
manager = result.manager;
|
manager = result.manager;
|
||||||
|
|
||||||
const db = (manager as unknown as { db: { exec: (sql: string) => void; prepare: (sql: string) => unknown } }).db;
|
const db = (
|
||||||
|
manager as unknown as {
|
||||||
|
db: { exec: (sql: string) => void; prepare: (sql: string) => unknown };
|
||||||
|
}
|
||||||
|
).db;
|
||||||
db.exec("CREATE TABLE IF NOT EXISTS chunks_vec (id TEXT PRIMARY KEY, embedding BLOB)");
|
db.exec("CREATE TABLE IF NOT EXISTS chunks_vec (id TEXT PRIMARY KEY, embedding BLOB)");
|
||||||
|
|
||||||
const sqlSeen: string[] = [];
|
const sqlSeen: string[] = [];
|
||||||
@@ -75,16 +79,20 @@ describe("memory vector dedupe", () => {
|
|||||||
return originalPrepare(sql);
|
return originalPrepare(sql);
|
||||||
};
|
};
|
||||||
|
|
||||||
(manager as unknown as { ensureVectorReady: (dims?: number) => Promise<boolean> }).ensureVectorReady =
|
(
|
||||||
async () => true;
|
manager as unknown as { ensureVectorReady: (dims?: number) => Promise<boolean> }
|
||||||
|
).ensureVectorReady = async () => true;
|
||||||
|
|
||||||
const entry = await buildFileEntry(path.join(workspaceDir, "MEMORY.md"), workspaceDir);
|
const entry = await buildFileEntry(path.join(workspaceDir, "MEMORY.md"), workspaceDir);
|
||||||
await (manager as unknown as { indexFile: (entry: unknown, options: { source: "memory" }) => Promise<void> }).indexFile(
|
await (
|
||||||
entry,
|
manager as unknown as {
|
||||||
{ source: "memory" },
|
indexFile: (entry: unknown, options: { source: "memory" }) => Promise<void>;
|
||||||
);
|
}
|
||||||
|
).indexFile(entry, { source: "memory" });
|
||||||
|
|
||||||
const deleteIndex = sqlSeen.findIndex((sql) => sql.includes("DELETE FROM chunks_vec WHERE id = ?"));
|
const deleteIndex = sqlSeen.findIndex((sql) =>
|
||||||
|
sql.includes("DELETE FROM chunks_vec WHERE id = ?"),
|
||||||
|
);
|
||||||
const insertIndex = sqlSeen.findIndex((sql) => sql.includes("INSERT INTO chunks_vec"));
|
const insertIndex = sqlSeen.findIndex((sql) => sql.includes("INSERT INTO chunks_vec"));
|
||||||
expect(deleteIndex).toBeGreaterThan(-1);
|
expect(deleteIndex).toBeGreaterThan(-1);
|
||||||
expect(insertIndex).toBeGreaterThan(-1);
|
expect(insertIndex).toBeGreaterThan(-1);
|
||||||
|
|||||||
@@ -576,7 +576,8 @@ async function handleInvoke(
|
|||||||
const skillAllow =
|
const skillAllow =
|
||||||
autoAllowSkills && resolution?.executableName ? bins.has(resolution.executableName) : false;
|
autoAllowSkills && resolution?.executableName ? bins.has(resolution.executableName) : false;
|
||||||
|
|
||||||
const useMacAppExec = process.platform === "darwin" && (execHostEnforced || !execHostFallbackAllowed);
|
const useMacAppExec =
|
||||||
|
process.platform === "darwin" && (execHostEnforced || !execHostFallbackAllowed);
|
||||||
if (useMacAppExec) {
|
if (useMacAppExec) {
|
||||||
const execRequest: ExecHostRequest = {
|
const execRequest: ExecHostRequest = {
|
||||||
command: argv,
|
command: argv,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
const tempDirs: string[] = [];
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
@@ -77,22 +77,6 @@ function packToArchive({
|
|||||||
return dest;
|
return dest;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function withStateDir<T>(stateDir: string, fn: () => Promise<T>) {
|
|
||||||
const prev = process.env.CLAWDBOT_STATE_DIR;
|
|
||||||
process.env.CLAWDBOT_STATE_DIR = stateDir;
|
|
||||||
vi.resetModules();
|
|
||||||
try {
|
|
||||||
return await fn();
|
|
||||||
} finally {
|
|
||||||
if (prev === undefined) {
|
|
||||||
delete process.env.CLAWDBOT_STATE_DIR;
|
|
||||||
} else {
|
|
||||||
process.env.CLAWDBOT_STATE_DIR = prev;
|
|
||||||
}
|
|
||||||
vi.resetModules();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
for (const dir of tempDirs.splice(0)) {
|
for (const dir of tempDirs.splice(0)) {
|
||||||
try {
|
try {
|
||||||
@@ -126,10 +110,9 @@ describe("installPluginFromArchive", () => {
|
|||||||
outName: "plugin.tgz",
|
outName: "plugin.tgz",
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await withStateDir(stateDir, async () => {
|
const extensionsDir = path.join(stateDir, "extensions");
|
||||||
const { installPluginFromArchive } = await import("./install.js");
|
const { installPluginFromArchive } = await import("./install.js");
|
||||||
return await installPluginFromArchive({ archivePath });
|
const result = await installPluginFromArchive({ archivePath, extensionsDir });
|
||||||
});
|
|
||||||
expect(result.ok).toBe(true);
|
expect(result.ok).toBe(true);
|
||||||
if (!result.ok) return;
|
if (!result.ok) return;
|
||||||
expect(result.pluginId).toBe("voice-call");
|
expect(result.pluginId).toBe("voice-call");
|
||||||
@@ -160,12 +143,10 @@ describe("installPluginFromArchive", () => {
|
|||||||
outName: "plugin.tgz",
|
outName: "plugin.tgz",
|
||||||
});
|
});
|
||||||
|
|
||||||
const { first, second } = await withStateDir(stateDir, async () => {
|
const extensionsDir = path.join(stateDir, "extensions");
|
||||||
const { installPluginFromArchive } = await import("./install.js");
|
const { installPluginFromArchive } = await import("./install.js");
|
||||||
const first = await installPluginFromArchive({ archivePath });
|
const first = await installPluginFromArchive({ archivePath, extensionsDir });
|
||||||
const second = await installPluginFromArchive({ archivePath });
|
const second = await installPluginFromArchive({ archivePath, extensionsDir });
|
||||||
return { first, second };
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(first.ok).toBe(true);
|
expect(first.ok).toBe(true);
|
||||||
expect(second.ok).toBe(false);
|
expect(second.ok).toBe(false);
|
||||||
@@ -191,10 +172,9 @@ describe("installPluginFromArchive", () => {
|
|||||||
const buffer = await zip.generateAsync({ type: "nodebuffer" });
|
const buffer = await zip.generateAsync({ type: "nodebuffer" });
|
||||||
fs.writeFileSync(archivePath, buffer);
|
fs.writeFileSync(archivePath, buffer);
|
||||||
|
|
||||||
const result = await withStateDir(stateDir, async () => {
|
const extensionsDir = path.join(stateDir, "extensions");
|
||||||
const { installPluginFromArchive } = await import("./install.js");
|
const { installPluginFromArchive } = await import("./install.js");
|
||||||
return await installPluginFromArchive({ archivePath });
|
const result = await installPluginFromArchive({ archivePath, extensionsDir });
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.ok).toBe(true);
|
expect(result.ok).toBe(true);
|
||||||
if (!result.ok) return;
|
if (!result.ok) return;
|
||||||
@@ -243,18 +223,23 @@ describe("installPluginFromArchive", () => {
|
|||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const result = await withStateDir(stateDir, async () => {
|
const extensionsDir = path.join(stateDir, "extensions");
|
||||||
const { installPluginFromArchive } = await import("./install.js");
|
const { installPluginFromArchive } = await import("./install.js");
|
||||||
const first = await installPluginFromArchive({ archivePath: archiveV1 });
|
const first = await installPluginFromArchive({
|
||||||
const second = await installPluginFromArchive({ archivePath: archiveV2, mode: "update" });
|
archivePath: archiveV1,
|
||||||
return { first, second };
|
extensionsDir,
|
||||||
|
});
|
||||||
|
const second = await installPluginFromArchive({
|
||||||
|
archivePath: archiveV2,
|
||||||
|
extensionsDir,
|
||||||
|
mode: "update",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.first.ok).toBe(true);
|
expect(first.ok).toBe(true);
|
||||||
expect(result.second.ok).toBe(true);
|
expect(second.ok).toBe(true);
|
||||||
if (!result.second.ok) return;
|
if (!second.ok) return;
|
||||||
const manifest = JSON.parse(
|
const manifest = JSON.parse(
|
||||||
fs.readFileSync(path.join(result.second.targetDir, "package.json"), "utf-8"),
|
fs.readFileSync(path.join(second.targetDir, "package.json"), "utf-8"),
|
||||||
) as { version?: string };
|
) as { version?: string };
|
||||||
expect(manifest.version).toBe("0.0.2");
|
expect(manifest.version).toBe("0.0.2");
|
||||||
});
|
});
|
||||||
@@ -276,10 +261,9 @@ describe("installPluginFromArchive", () => {
|
|||||||
outName: "bad.tgz",
|
outName: "bad.tgz",
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await withStateDir(stateDir, async () => {
|
const extensionsDir = path.join(stateDir, "extensions");
|
||||||
const { installPluginFromArchive } = await import("./install.js");
|
const { installPluginFromArchive } = await import("./install.js");
|
||||||
return await installPluginFromArchive({ archivePath });
|
const result = await installPluginFromArchive({ archivePath, extensionsDir });
|
||||||
});
|
|
||||||
expect(result.ok).toBe(false);
|
expect(result.ok).toBe(false);
|
||||||
if (result.ok) return;
|
if (result.ok) return;
|
||||||
expect(result.error).toContain("clawdbot.extensions");
|
expect(result.error).toContain("clawdbot.extensions");
|
||||||
|
|||||||
@@ -38,7 +38,10 @@ import {
|
|||||||
updateLastRoute,
|
updateLastRoute,
|
||||||
} from "../../config/sessions.js";
|
} from "../../config/sessions.js";
|
||||||
import { auditDiscordChannelPermissions } from "../../discord/audit.js";
|
import { auditDiscordChannelPermissions } from "../../discord/audit.js";
|
||||||
import { listDiscordDirectoryGroupsLive, listDiscordDirectoryPeersLive } from "../../discord/directory-live.js";
|
import {
|
||||||
|
listDiscordDirectoryGroupsLive,
|
||||||
|
listDiscordDirectoryPeersLive,
|
||||||
|
} from "../../discord/directory-live.js";
|
||||||
import { monitorDiscordProvider } from "../../discord/monitor.js";
|
import { monitorDiscordProvider } from "../../discord/monitor.js";
|
||||||
import { probeDiscord } from "../../discord/probe.js";
|
import { probeDiscord } from "../../discord/probe.js";
|
||||||
import { resolveDiscordChannelAllowlist } from "../../discord/resolve-channels.js";
|
import { resolveDiscordChannelAllowlist } from "../../discord/resolve-channels.js";
|
||||||
@@ -68,7 +71,10 @@ import { monitorSignalProvider } from "../../signal/index.js";
|
|||||||
import { probeSignal } from "../../signal/probe.js";
|
import { probeSignal } from "../../signal/probe.js";
|
||||||
import { sendMessageSignal } from "../../signal/send.js";
|
import { sendMessageSignal } from "../../signal/send.js";
|
||||||
import { monitorSlackProvider } from "../../slack/index.js";
|
import { monitorSlackProvider } from "../../slack/index.js";
|
||||||
import { listSlackDirectoryGroupsLive, listSlackDirectoryPeersLive } from "../../slack/directory-live.js";
|
import {
|
||||||
|
listSlackDirectoryGroupsLive,
|
||||||
|
listSlackDirectoryPeersLive,
|
||||||
|
} from "../../slack/directory-live.js";
|
||||||
import { probeSlack } from "../../slack/probe.js";
|
import { probeSlack } from "../../slack/probe.js";
|
||||||
import { resolveSlackChannelAllowlist } from "../../slack/resolve-channels.js";
|
import { resolveSlackChannelAllowlist } from "../../slack/resolve-channels.js";
|
||||||
import { resolveSlackUserAllowlist } from "../../slack/resolve-users.js";
|
import { resolveSlackUserAllowlist } from "../../slack/resolve-users.js";
|
||||||
@@ -137,12 +143,12 @@ export function createPluginRuntime(): PluginRuntime {
|
|||||||
registerMemoryCli,
|
registerMemoryCli,
|
||||||
},
|
},
|
||||||
channel: {
|
channel: {
|
||||||
text: {
|
text: {
|
||||||
chunkMarkdownText,
|
chunkMarkdownText,
|
||||||
chunkText,
|
chunkText,
|
||||||
resolveTextChunkLimit,
|
resolveTextChunkLimit,
|
||||||
hasControlCommand,
|
hasControlCommand,
|
||||||
},
|
},
|
||||||
reply: {
|
reply: {
|
||||||
dispatchReplyWithBufferedBlockDispatcher,
|
dispatchReplyWithBufferedBlockDispatcher,
|
||||||
createReplyDispatcherWithTyping,
|
createReplyDispatcherWithTyping,
|
||||||
@@ -181,12 +187,12 @@ export function createPluginRuntime(): PluginRuntime {
|
|||||||
createInboundDebouncer,
|
createInboundDebouncer,
|
||||||
resolveInboundDebounceMs,
|
resolveInboundDebounceMs,
|
||||||
},
|
},
|
||||||
commands: {
|
commands: {
|
||||||
resolveCommandAuthorizedFromAuthorizers,
|
resolveCommandAuthorizedFromAuthorizers,
|
||||||
isControlCommandMessage,
|
isControlCommandMessage,
|
||||||
shouldComputeCommandAuthorized,
|
shouldComputeCommandAuthorized,
|
||||||
shouldHandleTextCommands,
|
shouldHandleTextCommands,
|
||||||
},
|
},
|
||||||
discord: {
|
discord: {
|
||||||
messageActions: discordMessageActions,
|
messageActions: discordMessageActions,
|
||||||
auditChannelPermissions: auditDiscordChannelPermissions,
|
auditChannelPermissions: auditDiscordChannelPermissions,
|
||||||
|
|||||||
@@ -59,8 +59,7 @@ type MediaKindFromMime = typeof import("../../media/constants.js").mediaKindFrom
|
|||||||
type IsVoiceCompatibleAudio = typeof import("../../media/audio.js").isVoiceCompatibleAudio;
|
type IsVoiceCompatibleAudio = typeof import("../../media/audio.js").isVoiceCompatibleAudio;
|
||||||
type GetImageMetadata = typeof import("../../media/image-ops.js").getImageMetadata;
|
type GetImageMetadata = typeof import("../../media/image-ops.js").getImageMetadata;
|
||||||
type ResizeToJpeg = typeof import("../../media/image-ops.js").resizeToJpeg;
|
type ResizeToJpeg = typeof import("../../media/image-ops.js").resizeToJpeg;
|
||||||
type CreateMemoryGetTool =
|
type CreateMemoryGetTool = typeof import("../../agents/tools/memory-tool.js").createMemoryGetTool;
|
||||||
typeof import("../../agents/tools/memory-tool.js").createMemoryGetTool;
|
|
||||||
type CreateMemorySearchTool =
|
type CreateMemorySearchTool =
|
||||||
typeof import("../../agents/tools/memory-tool.js").createMemorySearchTool;
|
typeof import("../../agents/tools/memory-tool.js").createMemorySearchTool;
|
||||||
type RegisterMemoryCli = typeof import("../../cli/memory-cli.js").registerMemoryCli;
|
type RegisterMemoryCli = typeof import("../../cli/memory-cli.js").registerMemoryCli;
|
||||||
|
|||||||
@@ -23,23 +23,23 @@ describe("web logout", () => {
|
|||||||
|
|
||||||
it("deletes cached credentials when present", { timeout: 60_000 }, async () => {
|
it("deletes cached credentials when present", { timeout: 60_000 }, async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.resetModules();
|
const { logoutWeb } = await import("./session.js");
|
||||||
const { logoutWeb, WA_WEB_AUTH_DIR } = await import("./session.js");
|
const { resolveDefaultWebAuthDir } = await import("./auth-store.js");
|
||||||
|
const authDir = resolveDefaultWebAuthDir();
|
||||||
|
|
||||||
expect(isPathWithinBase(home, WA_WEB_AUTH_DIR)).toBe(true);
|
expect(isPathWithinBase(home, authDir)).toBe(true);
|
||||||
|
|
||||||
fs.mkdirSync(WA_WEB_AUTH_DIR, { recursive: true });
|
fs.mkdirSync(authDir, { recursive: true });
|
||||||
fs.writeFileSync(path.join(WA_WEB_AUTH_DIR, "creds.json"), "{}");
|
fs.writeFileSync(path.join(authDir, "creds.json"), "{}");
|
||||||
const result = await logoutWeb({ runtime: runtime as never });
|
const result = await logoutWeb({ runtime: runtime as never });
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
expect(fs.existsSync(WA_WEB_AUTH_DIR)).toBe(false);
|
expect(fs.existsSync(authDir)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("no-ops when nothing to delete", { timeout: 60_000 }, async () => {
|
it("no-ops when nothing to delete", { timeout: 60_000 }, async () => {
|
||||||
await withTempHome(async () => {
|
await withTempHome(async () => {
|
||||||
vi.resetModules();
|
|
||||||
const { logoutWeb } = await import("./session.js");
|
const { logoutWeb } = await import("./session.js");
|
||||||
const result = await logoutWeb({ runtime: runtime as never });
|
const result = await logoutWeb({ runtime: runtime as never });
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
@@ -49,7 +49,6 @@ describe("web logout", () => {
|
|||||||
|
|
||||||
it("keeps shared oauth.json when using legacy auth dir", async () => {
|
it("keeps shared oauth.json when using legacy auth dir", async () => {
|
||||||
await withTempHome(async () => {
|
await withTempHome(async () => {
|
||||||
vi.resetModules();
|
|
||||||
const { logoutWeb } = await import("./session.js");
|
const { logoutWeb } = await import("./session.js");
|
||||||
|
|
||||||
const { resolveOAuthDir } = await import("../config/paths.js");
|
const { resolveOAuthDir } = await import("../config/paths.js");
|
||||||
|
|||||||
Reference in New Issue
Block a user