mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
perf(test): eliminate resetModules via injectable seams
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { resetProcessRegistryForTests } from "./bash-process-registry";
|
||||
import { createExecTool, setPtyModuleLoaderForTests } from "./bash-tools.exec";
|
||||
|
||||
afterEach(() => {
|
||||
resetProcessRegistryForTests();
|
||||
vi.resetModules();
|
||||
setPtyModuleLoaderForTests();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("exec falls back when PTY spawn fails", async () => {
|
||||
vi.doMock("@lydell/node-pty", () => ({
|
||||
setPtyModuleLoaderForTests(async () => ({
|
||||
spawn: () => {
|
||||
const err = new Error("spawn EBADF");
|
||||
(err as NodeJS.ErrnoException).code = "EBADF";
|
||||
@@ -16,7 +17,6 @@ test("exec falls back when PTY spawn fails", async () => {
|
||||
},
|
||||
}));
|
||||
|
||||
const { createExecTool } = await import("./bash-tools.exec");
|
||||
const tool = createExecTool({ allowBackground: false });
|
||||
const result = await tool.execute("toolcall", {
|
||||
command: "printf ok",
|
||||
|
||||
@@ -144,6 +144,19 @@ type PtySpawn = (
|
||||
env?: Record<string, string>;
|
||||
},
|
||||
) => PtyHandle;
|
||||
type PtyModule = {
|
||||
spawn?: PtySpawn;
|
||||
default?: { spawn?: PtySpawn };
|
||||
};
|
||||
type PtyModuleLoader = () => Promise<PtyModule>;
|
||||
|
||||
const loadPtyModuleDefault: PtyModuleLoader = async () =>
|
||||
(await import("@lydell/node-pty")) as unknown as PtyModule;
|
||||
let loadPtyModule: PtyModuleLoader = loadPtyModuleDefault;
|
||||
|
||||
export function setPtyModuleLoaderForTests(loader?: PtyModuleLoader): void {
|
||||
loadPtyModule = loader ?? loadPtyModuleDefault;
|
||||
}
|
||||
|
||||
type ExecProcessOutcome = {
|
||||
status: "completed" | "failed";
|
||||
@@ -477,10 +490,7 @@ async function runExecProcess(opts: {
|
||||
} else if (opts.usePty) {
|
||||
const { shell, args: shellArgs } = getShellConfig();
|
||||
try {
|
||||
const ptyModule = (await import("@lydell/node-pty")) as unknown as {
|
||||
spawn?: PtySpawn;
|
||||
default?: { spawn?: PtySpawn };
|
||||
};
|
||||
const ptyModule = await loadPtyModule();
|
||||
const spawnPty = ptyModule.spawn ?? ptyModule.default?.spawn;
|
||||
if (!spawnPty) {
|
||||
throw new Error("PTY support is unavailable (node-pty spawn not found).");
|
||||
|
||||
@@ -7,7 +7,6 @@ const execSyncMock = vi.fn();
|
||||
|
||||
describe("cli credentials", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
||||
|
||||
@@ -45,7 +45,6 @@ describe("models-config", () => {
|
||||
|
||||
it("normalizes gemini 3 ids to preview for google providers", async () => {
|
||||
await withTempHome(async () => {
|
||||
vi.resetModules();
|
||||
const { ensureOpenClawModelsJson } = await import("./models-config.js");
|
||||
const { resolveOpenClawAgentDir } = await import("./agent-paths.js");
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ describe("sanitizeSessionHistory", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(helpers.sanitizeSessionMessagesImages).mockImplementation(async (msgs) => msgs);
|
||||
vi.resetModules();
|
||||
({ sanitizeSessionHistory } = await import("./pi-embedded-runner/google.js"));
|
||||
});
|
||||
|
||||
@@ -94,7 +93,7 @@ describe("sanitizeSessionHistory", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not sanitize tool call ids for openai-responses", async () => {
|
||||
it("sanitizes tool call ids for openai-responses", async () => {
|
||||
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
||||
|
||||
await sanitizeSessionHistory({
|
||||
@@ -108,7 +107,11 @@ describe("sanitizeSessionHistory", () => {
|
||||
expect(helpers.sanitizeSessionMessagesImages).toHaveBeenCalledWith(
|
||||
mockMessages,
|
||||
"session:history",
|
||||
expect.objectContaining({ sanitizeMode: "images-only", sanitizeToolCallIds: false }),
|
||||
expect.objectContaining({
|
||||
sanitizeMode: "images-only",
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ describe("sanitizeSessionHistory", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(helpers.sanitizeSessionMessagesImages).mockImplementation(async (msgs) => msgs);
|
||||
vi.resetModules();
|
||||
({ sanitizeSessionHistory } = await import("./pi-embedded-runner/google.js"));
|
||||
});
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ vi.mock("../skills.js", async (importOriginal) => {
|
||||
describe("Agent-specific sandbox config", () => {
|
||||
beforeEach(() => {
|
||||
spawnCalls.length = 0;
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("should use agent-specific workspaceRoot", async () => {
|
||||
|
||||
@@ -58,7 +58,6 @@ const installRegistry = async () => {
|
||||
describe("resolveAnnounceTarget", () => {
|
||||
beforeEach(async () => {
|
||||
callGatewayMock.mockReset();
|
||||
vi.resetModules();
|
||||
await installRegistry();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { closePlaywrightBrowserConnection, getPageForTargetId } from "./pw-session.js";
|
||||
|
||||
const connectOverCdpMock = vi.fn();
|
||||
const getChromeWebSocketUrlMock = vi.fn();
|
||||
|
||||
vi.mock("playwright-core", () => ({
|
||||
chromium: {
|
||||
connectOverCDP: (...args: unknown[]) => connectOverCdpMock(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./chrome.js", () => ({
|
||||
getChromeWebSocketUrl: (...args: unknown[]) => getChromeWebSocketUrlMock(...args),
|
||||
}));
|
||||
|
||||
describe("pw-session getPageForTargetId", () => {
|
||||
it("falls back to the only page when CDP session attachment is blocked (extension relays)", async () => {
|
||||
vi.resetModules();
|
||||
connectOverCdpMock.mockReset();
|
||||
getChromeWebSocketUrlMock.mockReset();
|
||||
|
||||
const pageOn = vi.fn();
|
||||
const contextOn = vi.fn();
|
||||
@@ -31,24 +46,16 @@ describe("pw-session getPageForTargetId", () => {
|
||||
close: browserClose,
|
||||
} as unknown as import("playwright-core").Browser;
|
||||
|
||||
vi.doMock("playwright-core", () => ({
|
||||
chromium: {
|
||||
connectOverCDP: vi.fn(async () => browser),
|
||||
},
|
||||
}));
|
||||
connectOverCdpMock.mockResolvedValue(browser);
|
||||
getChromeWebSocketUrlMock.mockResolvedValue(null);
|
||||
|
||||
vi.doMock("./chrome.js", () => ({
|
||||
getChromeWebSocketUrl: vi.fn(async () => null),
|
||||
}));
|
||||
|
||||
const mod = await import("./pw-session.js");
|
||||
const resolved = await mod.getPageForTargetId({
|
||||
const resolved = await getPageForTargetId({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "NOT_A_TAB",
|
||||
});
|
||||
expect(resolved).toBe(page);
|
||||
|
||||
await mod.closePlaywrightBrowserConnection();
|
||||
await closePlaywrightBrowserConnection();
|
||||
expect(browserClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -242,14 +242,6 @@ describe("onboard (non-interactive): gateway and remote auth", () => {
|
||||
const port = await getFreeGatewayPort();
|
||||
const workspace = path.join(stateDir, "openclaw");
|
||||
|
||||
// Other test files mock ../config/config.js. This onboarding flow needs the real
|
||||
// implementation so it can persist the config and then read it back (Windows CI
|
||||
// otherwise sees a mocked writeConfigFile and the config never lands on disk).
|
||||
vi.resetModules();
|
||||
vi.doMock("../config/config.js", async () => {
|
||||
return await vi.importActual("../config/config.js");
|
||||
});
|
||||
|
||||
const { runNonInteractiveOnboarding } = await import("./onboard-non-interactive.js");
|
||||
await runNonInteractiveOnboarding(
|
||||
{
|
||||
|
||||
@@ -2,21 +2,28 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
ensureTailscaleEndpoint,
|
||||
resetGmailSetupUtilsCachesForTest,
|
||||
resolvePythonExecutablePath,
|
||||
} from "./gmail-setup-utils.js";
|
||||
|
||||
const itUnix = process.platform === "win32" ? it.skip : it;
|
||||
const runCommandWithTimeoutMock = vi.fn();
|
||||
|
||||
vi.mock("../process/exec.js", () => ({
|
||||
runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
runCommandWithTimeoutMock.mockReset();
|
||||
resetGmailSetupUtilsCachesForTest();
|
||||
});
|
||||
|
||||
describe("resolvePythonExecutablePath", () => {
|
||||
itUnix(
|
||||
"resolves a working python path and caches the result",
|
||||
async () => {
|
||||
vi.doMock("../process/exec.js", () => ({
|
||||
runCommandWithTimeout: vi.fn(),
|
||||
}));
|
||||
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-python-"));
|
||||
const originalPath = process.env.PATH;
|
||||
try {
|
||||
@@ -32,10 +39,7 @@ describe("resolvePythonExecutablePath", () => {
|
||||
|
||||
process.env.PATH = `${shimDir}${path.delimiter}/usr/bin`;
|
||||
|
||||
const { resolvePythonExecutablePath } = await import("./gmail-setup-utils.js");
|
||||
const { runCommandWithTimeout } = await import("../process/exec.js");
|
||||
const runCommand = vi.mocked(runCommandWithTimeout);
|
||||
runCommand.mockResolvedValue({
|
||||
runCommandWithTimeoutMock.mockResolvedValue({
|
||||
stdout: `${realPython}\n`,
|
||||
stderr: "",
|
||||
code: 0,
|
||||
@@ -49,7 +53,7 @@ describe("resolvePythonExecutablePath", () => {
|
||||
process.env.PATH = "/bin";
|
||||
const cached = await resolvePythonExecutablePath();
|
||||
expect(cached).toBe(realPython);
|
||||
expect(runCommand).toHaveBeenCalledTimes(1);
|
||||
expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
process.env.PATH = originalPath;
|
||||
await fs.rm(tmp, { recursive: true, force: true });
|
||||
@@ -61,15 +65,7 @@ describe("resolvePythonExecutablePath", () => {
|
||||
|
||||
describe("ensureTailscaleEndpoint", () => {
|
||||
it("includes stdout and exit code when tailscale serve fails", async () => {
|
||||
vi.doMock("../process/exec.js", () => ({
|
||||
runCommandWithTimeout: vi.fn(),
|
||||
}));
|
||||
|
||||
const { ensureTailscaleEndpoint } = await import("./gmail-setup-utils.js");
|
||||
const { runCommandWithTimeout } = await import("../process/exec.js");
|
||||
const runCommand = vi.mocked(runCommandWithTimeout);
|
||||
|
||||
runCommand
|
||||
runCommandWithTimeoutMock
|
||||
.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify({ Self: { DNSName: "host.tailnet.ts.net." } }),
|
||||
stderr: "",
|
||||
@@ -102,15 +98,7 @@ describe("ensureTailscaleEndpoint", () => {
|
||||
});
|
||||
|
||||
it("includes JSON parse failure details with stdout", async () => {
|
||||
vi.doMock("../process/exec.js", () => ({
|
||||
runCommandWithTimeout: vi.fn(),
|
||||
}));
|
||||
|
||||
const { ensureTailscaleEndpoint } = await import("./gmail-setup-utils.js");
|
||||
const { runCommandWithTimeout } = await import("../process/exec.js");
|
||||
const runCommand = vi.mocked(runCommandWithTimeout);
|
||||
|
||||
runCommand.mockResolvedValueOnce({
|
||||
runCommandWithTimeoutMock.mockResolvedValueOnce({
|
||||
stdout: "not-json",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
|
||||
@@ -8,6 +8,10 @@ import { normalizeServePath } from "./gmail.js";
|
||||
let cachedPythonPath: string | null | undefined;
|
||||
const MAX_OUTPUT_CHARS = 800;
|
||||
|
||||
export function resetGmailSetupUtilsCachesForTest(): void {
|
||||
cachedPythonPath = undefined;
|
||||
}
|
||||
|
||||
function trimOutput(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
|
||||
@@ -1,49 +1,32 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const loadSendMessageIMessage = async () => await import("./send.js");
|
||||
import type { ResolvedIMessageAccount } from "./accounts.js";
|
||||
import { sendMessageIMessage } from "./send.js";
|
||||
|
||||
const requestMock = vi.fn();
|
||||
const stopMock = vi.fn();
|
||||
|
||||
vi.mock("../config/config.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../config/config.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadConfig: () => ({}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./client.js", () => ({
|
||||
createIMessageRpcClient: vi.fn().mockResolvedValue({
|
||||
request: (...args: unknown[]) => requestMock(...args),
|
||||
stop: (...args: unknown[]) => stopMock(...args),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../web/media.js", () => ({
|
||||
loadWebMedia: vi.fn().mockResolvedValue({
|
||||
buffer: Buffer.from("data"),
|
||||
contentType: "image/jpeg",
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../media/store.js", () => ({
|
||||
saveMediaBuffer: vi.fn().mockResolvedValue({
|
||||
path: "/tmp/imessage-media.jpg",
|
||||
contentType: "image/jpeg",
|
||||
}),
|
||||
}));
|
||||
const defaultAccount: ResolvedIMessageAccount = {
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
configured: false,
|
||||
config: {},
|
||||
};
|
||||
|
||||
describe("sendMessageIMessage", () => {
|
||||
beforeEach(() => {
|
||||
requestMock.mockReset().mockResolvedValue({ ok: true });
|
||||
stopMock.mockReset().mockResolvedValue(undefined);
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("sends to chat_id targets", async () => {
|
||||
const { sendMessageIMessage } = await loadSendMessageIMessage();
|
||||
await sendMessageIMessage("chat_id:123", "hi");
|
||||
await sendMessageIMessage("chat_id:123", "hi", {
|
||||
account: defaultAccount,
|
||||
config: {},
|
||||
client: {
|
||||
request: (...args: unknown[]) => requestMock(...args),
|
||||
stop: (...args: unknown[]) => stopMock(...args),
|
||||
} as unknown as import("./client.js").IMessageRpcClient,
|
||||
});
|
||||
const params = requestMock.mock.calls[0]?.[1] as Record<string, unknown>;
|
||||
expect(requestMock).toHaveBeenCalledWith("send", expect.any(Object), expect.any(Object));
|
||||
expect(params.chat_id).toBe(123);
|
||||
@@ -51,16 +34,33 @@ describe("sendMessageIMessage", () => {
|
||||
});
|
||||
|
||||
it("applies sms service prefix", async () => {
|
||||
const { sendMessageIMessage } = await loadSendMessageIMessage();
|
||||
await sendMessageIMessage("sms:+1555", "hello");
|
||||
await sendMessageIMessage("sms:+1555", "hello", {
|
||||
account: defaultAccount,
|
||||
config: {},
|
||||
client: {
|
||||
request: (...args: unknown[]) => requestMock(...args),
|
||||
stop: (...args: unknown[]) => stopMock(...args),
|
||||
} as unknown as import("./client.js").IMessageRpcClient,
|
||||
});
|
||||
const params = requestMock.mock.calls[0]?.[1] as Record<string, unknown>;
|
||||
expect(params.service).toBe("sms");
|
||||
expect(params.to).toBe("+1555");
|
||||
});
|
||||
|
||||
it("adds file attachment with placeholder text", async () => {
|
||||
const { sendMessageIMessage } = await loadSendMessageIMessage();
|
||||
await sendMessageIMessage("chat_id:7", "", { mediaUrl: "http://x/y.jpg" });
|
||||
await sendMessageIMessage("chat_id:7", "", {
|
||||
mediaUrl: "http://x/y.jpg",
|
||||
account: defaultAccount,
|
||||
config: {},
|
||||
resolveAttachmentImpl: async () => ({
|
||||
path: "/tmp/imessage-media.jpg",
|
||||
contentType: "image/jpeg",
|
||||
}),
|
||||
client: {
|
||||
request: (...args: unknown[]) => requestMock(...args),
|
||||
stop: (...args: unknown[]) => stopMock(...args),
|
||||
} as unknown as import("./client.js").IMessageRpcClient,
|
||||
});
|
||||
const params = requestMock.mock.calls[0]?.[1] as Record<string, unknown>;
|
||||
expect(params.file).toBe("/tmp/imessage-media.jpg");
|
||||
expect(params.text).toBe("<media:image>");
|
||||
@@ -68,8 +68,14 @@ describe("sendMessageIMessage", () => {
|
||||
|
||||
it("returns message id when rpc provides one", async () => {
|
||||
requestMock.mockResolvedValue({ ok: true, id: 123 });
|
||||
const { sendMessageIMessage } = await loadSendMessageIMessage();
|
||||
const result = await sendMessageIMessage("chat_id:7", "hello");
|
||||
const result = await sendMessageIMessage("chat_id:7", "hello", {
|
||||
account: defaultAccount,
|
||||
config: {},
|
||||
client: {
|
||||
request: (...args: unknown[]) => requestMock(...args),
|
||||
stop: (...args: unknown[]) => stopMock(...args),
|
||||
} as unknown as import("./client.js").IMessageRpcClient,
|
||||
});
|
||||
expect(result.messageId).toBe("123");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { convertMarkdownTables } from "../markdown/tables.js";
|
||||
import { mediaKindFromMime } from "../media/constants.js";
|
||||
import { saveMediaBuffer } from "../media/store.js";
|
||||
import { loadWebMedia } from "../web/media.js";
|
||||
import { resolveIMessageAccount } from "./accounts.js";
|
||||
import { resolveIMessageAccount, type ResolvedIMessageAccount } from "./accounts.js";
|
||||
import { createIMessageRpcClient, type IMessageRpcClient } from "./client.js";
|
||||
import { formatIMessageChatTarget, type IMessageService, parseIMessageTarget } from "./targets.js";
|
||||
|
||||
@@ -19,6 +19,13 @@ export type IMessageSendOpts = {
|
||||
timeoutMs?: number;
|
||||
chatId?: number;
|
||||
client?: IMessageRpcClient;
|
||||
config?: ReturnType<typeof loadConfig>;
|
||||
account?: ResolvedIMessageAccount;
|
||||
resolveAttachmentImpl?: (
|
||||
mediaUrl: string,
|
||||
maxBytes: number,
|
||||
) => Promise<{ path: string; contentType?: string }>;
|
||||
createClient?: (params: { cliPath: string; dbPath?: string }) => Promise<IMessageRpcClient>;
|
||||
};
|
||||
|
||||
export type IMessageSendResult = {
|
||||
@@ -58,11 +65,13 @@ export async function sendMessageIMessage(
|
||||
text: string,
|
||||
opts: IMessageSendOpts = {},
|
||||
): Promise<IMessageSendResult> {
|
||||
const cfg = loadConfig();
|
||||
const account = resolveIMessageAccount({
|
||||
cfg,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
const cfg = opts.config ?? loadConfig();
|
||||
const account =
|
||||
opts.account ??
|
||||
resolveIMessageAccount({
|
||||
cfg,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
const cliPath = opts.cliPath?.trim() || account.config.cliPath?.trim() || "imsg";
|
||||
const dbPath = opts.dbPath?.trim() || account.config.dbPath?.trim();
|
||||
const target = parseIMessageTarget(opts.chatId ? formatIMessageChatTarget(opts.chatId) : to);
|
||||
@@ -81,7 +90,8 @@ export async function sendMessageIMessage(
|
||||
let filePath: string | undefined;
|
||||
|
||||
if (opts.mediaUrl?.trim()) {
|
||||
const resolved = await resolveAttachment(opts.mediaUrl.trim(), maxBytes);
|
||||
const resolveAttachmentFn = opts.resolveAttachmentImpl ?? resolveAttachment;
|
||||
const resolved = await resolveAttachmentFn(opts.mediaUrl.trim(), maxBytes);
|
||||
filePath = resolved.path;
|
||||
if (!message.trim()) {
|
||||
const kind = mediaKindFromMime(resolved.contentType ?? undefined);
|
||||
@@ -122,7 +132,11 @@ export async function sendMessageIMessage(
|
||||
params.to = target.to;
|
||||
}
|
||||
|
||||
const client = opts.client ?? (await createIMessageRpcClient({ cliPath, dbPath }));
|
||||
const client =
|
||||
opts.client ??
|
||||
(opts.createClient
|
||||
? await opts.createClient({ cliPath, dbPath })
|
||||
: await createIMessageRpcClient({ cliPath, dbPath }));
|
||||
const shouldClose = !opts.client;
|
||||
try {
|
||||
const result = await client.request<{ ok?: string }>("send", params, {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getResolvedConsoleSettings,
|
||||
routeLogsToStderr,
|
||||
setConsoleSubsystemFilter,
|
||||
setConsoleConfigLoaderForTests,
|
||||
setConsoleTimestampPrefix,
|
||||
shouldLogSubsystemToConsole,
|
||||
} from "./logging/console.js";
|
||||
@@ -36,6 +37,7 @@ export {
|
||||
getResolvedConsoleSettings,
|
||||
routeLogsToStderr,
|
||||
setConsoleSubsystemFilter,
|
||||
setConsoleConfigLoaderForTests,
|
||||
setConsoleTimestampPrefix,
|
||||
shouldLogSubsystemToConsole,
|
||||
ALLOWED_LOG_LEVELS,
|
||||
|
||||
@@ -16,29 +16,6 @@ vi.mock("./logger.js", () => ({
|
||||
}));
|
||||
|
||||
let loadConfigCalls = 0;
|
||||
vi.mock("node:module", async () => {
|
||||
const actual = await vi.importActual<typeof import("node:module")>("node:module");
|
||||
return Object.assign({}, actual, {
|
||||
createRequire: (url: string | URL) => {
|
||||
const realRequire = actual.createRequire(url);
|
||||
return (specifier: string) => {
|
||||
if (specifier.endsWith("config.js")) {
|
||||
return {
|
||||
loadConfig: () => {
|
||||
loadConfigCalls += 1;
|
||||
if (loadConfigCalls > 5) {
|
||||
return {};
|
||||
}
|
||||
console.error("config load failed");
|
||||
return {};
|
||||
},
|
||||
};
|
||||
}
|
||||
return realRequire(specifier);
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
type ConsoleSnapshot = {
|
||||
log: typeof console.log;
|
||||
info: typeof console.info;
|
||||
@@ -53,7 +30,6 @@ let snapshot: ConsoleSnapshot;
|
||||
|
||||
beforeEach(() => {
|
||||
loadConfigCalls = 0;
|
||||
vi.resetModules();
|
||||
snapshot = {
|
||||
log: console.log,
|
||||
info: console.info,
|
||||
@@ -66,7 +42,7 @@ beforeEach(() => {
|
||||
Object.defineProperty(process.stdout, "isTTY", { value: false, configurable: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
console.log = snapshot.log;
|
||||
console.info = snapshot.info;
|
||||
console.warn = snapshot.warn;
|
||||
@@ -74,6 +50,8 @@ afterEach(() => {
|
||||
console.debug = snapshot.debug;
|
||||
console.trace = snapshot.trace;
|
||||
Object.defineProperty(process.stdout, "isTTY", { value: originalIsTty, configurable: true });
|
||||
const logging = await import("../logging.js");
|
||||
logging.setConsoleConfigLoaderForTests();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
@@ -81,6 +59,14 @@ async function loadLogging() {
|
||||
const logging = await import("../logging.js");
|
||||
const state = await import("./state.js");
|
||||
state.loggingState.cachedConsoleSettings = null;
|
||||
logging.setConsoleConfigLoaderForTests(() => {
|
||||
loadConfigCalls += 1;
|
||||
if (loadConfigCalls > 5) {
|
||||
return {};
|
||||
}
|
||||
console.error("config load failed");
|
||||
return {};
|
||||
});
|
||||
return { logging, state };
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,22 @@ type ConsoleSettings = {
|
||||
export type ConsoleLoggerSettings = ConsoleSettings;
|
||||
|
||||
const requireConfig = createRequire(import.meta.url);
|
||||
type ConsoleConfigLoader = () => OpenClawConfig["logging"] | undefined;
|
||||
const loadConfigFallbackDefault: ConsoleConfigLoader = () => {
|
||||
try {
|
||||
const loaded = requireConfig("../config/config.js") as {
|
||||
loadConfig?: () => OpenClawConfig;
|
||||
};
|
||||
return loaded.loadConfig?.().logging;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
let loadConfigFallback: ConsoleConfigLoader = loadConfigFallbackDefault;
|
||||
|
||||
export function setConsoleConfigLoaderForTests(loader?: ConsoleConfigLoader): void {
|
||||
loadConfigFallback = loader ?? loadConfigFallbackDefault;
|
||||
}
|
||||
|
||||
function normalizeConsoleLevel(level?: string): LogLevel {
|
||||
if (isVerbose()) {
|
||||
@@ -43,12 +59,7 @@ function resolveConsoleSettings(): ConsoleSettings {
|
||||
} else {
|
||||
loggingState.resolvingConsoleSettings = true;
|
||||
try {
|
||||
const loaded = requireConfig("../config/config.js") as {
|
||||
loadConfig?: () => OpenClawConfig;
|
||||
};
|
||||
cfg = loaded.loadConfig?.().logging;
|
||||
} catch {
|
||||
cfg = undefined;
|
||||
cfg = loadConfigFallback();
|
||||
} finally {
|
||||
loggingState.resolvingConsoleSettings = false;
|
||||
}
|
||||
|
||||
@@ -1,45 +1,44 @@
|
||||
import JSZip from "jszip";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { saveMediaSource, setMediaStoreNetworkDepsForTest } from "./store.js";
|
||||
|
||||
const realOs = await vi.importActual<typeof import("node:os")>("node:os");
|
||||
const HOME = path.join(realOs.tmpdir(), "openclaw-home-redirect");
|
||||
const HOME = path.join(os.tmpdir(), "openclaw-home-redirect");
|
||||
const previousStateDir = process.env.OPENCLAW_STATE_DIR;
|
||||
const mockRequest = vi.fn();
|
||||
|
||||
vi.doMock("node:os", () => ({
|
||||
default: { homedir: () => HOME, tmpdir: () => realOs.tmpdir() },
|
||||
homedir: () => HOME,
|
||||
tmpdir: () => realOs.tmpdir(),
|
||||
}));
|
||||
|
||||
vi.doMock("node:https", () => ({
|
||||
request: (...args: unknown[]) => mockRequest(...args),
|
||||
}));
|
||||
vi.doMock("node:dns/promises", () => ({
|
||||
lookup: async () => [{ address: "93.184.216.34", family: 4 }],
|
||||
}));
|
||||
|
||||
const loadStore = async () => await import("./store.js");
|
||||
|
||||
describe("media store redirects", () => {
|
||||
beforeAll(async () => {
|
||||
await fs.rm(HOME, { recursive: true, force: true });
|
||||
process.env.OPENCLAW_STATE_DIR = HOME;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockRequest.mockReset();
|
||||
vi.resetModules();
|
||||
setMediaStoreNetworkDepsForTest({
|
||||
httpRequest: (...args) => mockRequest(...args),
|
||||
httpsRequest: (...args) => mockRequest(...args),
|
||||
resolvePinnedHostname: async () => ({
|
||||
lookup: async () => [{ address: "93.184.216.34", family: 4 }],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.rm(HOME, { recursive: true, force: true });
|
||||
if (previousStateDir === undefined) {
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_STATE_DIR = previousStateDir;
|
||||
}
|
||||
setMediaStoreNetworkDepsForTest();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("follows redirects and keeps detected mime/extension", async () => {
|
||||
const { saveMediaSource } = await loadStore();
|
||||
let call = 0;
|
||||
mockRequest.mockImplementation((_url, _opts, cb) => {
|
||||
call += 1;
|
||||
@@ -84,7 +83,6 @@ describe("media store redirects", () => {
|
||||
});
|
||||
|
||||
it("sniffs xlsx from zip content when headers and url extension are missing", async () => {
|
||||
const { saveMediaSource } = await loadStore();
|
||||
mockRequest.mockImplementationOnce((_url, _opts, cb) => {
|
||||
const res = new PassThrough();
|
||||
const req = {
|
||||
|
||||
@@ -13,6 +13,26 @@ const resolveMediaDir = () => path.join(resolveConfigDir(), "media");
|
||||
export const MEDIA_MAX_BYTES = 5 * 1024 * 1024; // 5MB default
|
||||
const MAX_BYTES = MEDIA_MAX_BYTES;
|
||||
const DEFAULT_TTL_MS = 2 * 60 * 1000; // 2 minutes
|
||||
type RequestImpl = typeof httpRequest;
|
||||
type ResolvePinnedHostnameImpl = typeof resolvePinnedHostname;
|
||||
|
||||
const defaultHttpRequestImpl: RequestImpl = httpRequest;
|
||||
const defaultHttpsRequestImpl: RequestImpl = httpsRequest;
|
||||
const defaultResolvePinnedHostnameImpl: ResolvePinnedHostnameImpl = resolvePinnedHostname;
|
||||
|
||||
let httpRequestImpl: RequestImpl = defaultHttpRequestImpl;
|
||||
let httpsRequestImpl: RequestImpl = defaultHttpsRequestImpl;
|
||||
let resolvePinnedHostnameImpl: ResolvePinnedHostnameImpl = defaultResolvePinnedHostnameImpl;
|
||||
|
||||
export function setMediaStoreNetworkDepsForTest(deps?: {
|
||||
httpRequest?: RequestImpl;
|
||||
httpsRequest?: RequestImpl;
|
||||
resolvePinnedHostname?: ResolvePinnedHostnameImpl;
|
||||
}): void {
|
||||
httpRequestImpl = deps?.httpRequest ?? defaultHttpRequestImpl;
|
||||
httpsRequestImpl = deps?.httpsRequest ?? defaultHttpsRequestImpl;
|
||||
resolvePinnedHostnameImpl = deps?.resolvePinnedHostname ?? defaultResolvePinnedHostnameImpl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a filename for cross-platform safety.
|
||||
@@ -107,8 +127,8 @@ async function downloadToFile(
|
||||
reject(new Error(`Invalid URL protocol: ${parsedUrl.protocol}. Only HTTP/HTTPS allowed.`));
|
||||
return;
|
||||
}
|
||||
const requestImpl = parsedUrl.protocol === "https:" ? httpsRequest : httpRequest;
|
||||
resolvePinnedHostname(parsedUrl.hostname)
|
||||
const requestImpl = parsedUrl.protocol === "https:" ? httpsRequestImpl : httpRequestImpl;
|
||||
resolvePinnedHostnameImpl(parsedUrl.hostname)
|
||||
.then((pinned) => {
|
||||
const req = requestImpl(parsedUrl, { headers, lookup: pinned.lookup }, (res) => {
|
||||
// Follow redirects
|
||||
|
||||
@@ -1,30 +1,20 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const loadJsonFile = vi.fn();
|
||||
const saveJsonFile = vi.fn();
|
||||
const resolveStateDir = vi.fn().mockReturnValue("/tmp/openclaw-state");
|
||||
|
||||
vi.mock("../infra/json-file.js", () => ({
|
||||
loadJsonFile,
|
||||
saveJsonFile,
|
||||
}));
|
||||
|
||||
vi.mock("../config/paths.js", () => ({
|
||||
resolveStateDir,
|
||||
}));
|
||||
import {
|
||||
deriveCopilotApiBaseUrlFromToken,
|
||||
resolveCopilotApiToken,
|
||||
} from "./github-copilot-token.js";
|
||||
|
||||
describe("github-copilot token", () => {
|
||||
const loadJsonFile = vi.fn();
|
||||
const saveJsonFile = vi.fn();
|
||||
const cachePath = "/tmp/openclaw-state/credentials/github-copilot.token.json";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
loadJsonFile.mockReset();
|
||||
saveJsonFile.mockReset();
|
||||
resolveStateDir.mockReset();
|
||||
resolveStateDir.mockReturnValue("/tmp/openclaw-state");
|
||||
});
|
||||
|
||||
it("derives baseUrl from token", async () => {
|
||||
const { deriveCopilotApiBaseUrlFromToken } = await import("./github-copilot-token.js");
|
||||
|
||||
expect(deriveCopilotApiBaseUrlFromToken("token;proxy-ep=proxy.example.com;")).toBe(
|
||||
"https://api.example.com",
|
||||
);
|
||||
@@ -41,11 +31,12 @@ describe("github-copilot token", () => {
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
const { resolveCopilotApiToken } = await import("./github-copilot-token.js");
|
||||
|
||||
const fetchImpl = vi.fn();
|
||||
const res = await resolveCopilotApiToken({
|
||||
githubToken: "gh",
|
||||
cachePath,
|
||||
loadJsonFileImpl: loadJsonFile,
|
||||
saveJsonFileImpl: saveJsonFile,
|
||||
fetchImpl: fetchImpl as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
@@ -71,6 +62,9 @@ describe("github-copilot token", () => {
|
||||
|
||||
const res = await resolveCopilotApiToken({
|
||||
githubToken: "gh",
|
||||
cachePath,
|
||||
loadJsonFileImpl: loadJsonFile,
|
||||
saveJsonFileImpl: saveJsonFile,
|
||||
fetchImpl: fetchImpl as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
|
||||
@@ -82,6 +82,9 @@ export async function resolveCopilotApiToken(params: {
|
||||
githubToken: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
fetchImpl?: typeof fetch;
|
||||
cachePath?: string;
|
||||
loadJsonFileImpl?: (path: string) => unknown;
|
||||
saveJsonFileImpl?: (path: string, value: CachedCopilotToken) => void;
|
||||
}): Promise<{
|
||||
token: string;
|
||||
expiresAt: number;
|
||||
@@ -89,8 +92,10 @@ export async function resolveCopilotApiToken(params: {
|
||||
baseUrl: string;
|
||||
}> {
|
||||
const env = params.env ?? process.env;
|
||||
const cachePath = resolveCopilotTokenCachePath(env);
|
||||
const cached = loadJsonFile(cachePath) as CachedCopilotToken | undefined;
|
||||
const cachePath = params.cachePath?.trim() || resolveCopilotTokenCachePath(env);
|
||||
const loadJsonFileFn = params.loadJsonFileImpl ?? loadJsonFile;
|
||||
const saveJsonFileFn = params.saveJsonFileImpl ?? saveJsonFile;
|
||||
const cached = loadJsonFileFn(cachePath) as CachedCopilotToken | undefined;
|
||||
if (cached && typeof cached.token === "string" && typeof cached.expiresAt === "number") {
|
||||
if (isTokenUsable(cached)) {
|
||||
return {
|
||||
@@ -121,7 +126,7 @@ export async function resolveCopilotApiToken(params: {
|
||||
expiresAt: json.expiresAt,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
saveJsonFile(cachePath, payload);
|
||||
saveJsonFileFn(cachePath, payload);
|
||||
|
||||
return {
|
||||
token: payload.token,
|
||||
|
||||
Reference in New Issue
Block a user