mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-28 19:01:47 +03:00
perf(test): reduce module reload overhead in key suites
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||||
|
import { resolveBrowserExecutableForPlatform } from "./chrome.executables.js";
|
||||||
|
|
||||||
vi.mock("node:child_process", () => ({
|
vi.mock("node:child_process", () => ({
|
||||||
execFileSync: vi.fn(),
|
execFileSync: vi.fn(),
|
||||||
@@ -17,11 +18,10 @@ import * as fs from "node:fs";
|
|||||||
|
|
||||||
describe("browser default executable detection", () => {
|
describe("browser default executable detection", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.resetModules();
|
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prefers default Chromium browser on macOS", async () => {
|
it("prefers default Chromium browser on macOS", () => {
|
||||||
vi.mocked(execFileSync).mockImplementation((cmd, args) => {
|
vi.mocked(execFileSync).mockImplementation((cmd, args) => {
|
||||||
const argsStr = Array.isArray(args) ? args.join(" ") : "";
|
const argsStr = Array.isArray(args) ? args.join(" ") : "";
|
||||||
if (cmd === "/usr/bin/plutil" && argsStr.includes("LSHandlers")) {
|
if (cmd === "/usr/bin/plutil" && argsStr.includes("LSHandlers")) {
|
||||||
@@ -45,7 +45,6 @@ describe("browser default executable detection", () => {
|
|||||||
return value.includes("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome");
|
return value.includes("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome");
|
||||||
});
|
});
|
||||||
|
|
||||||
const { resolveBrowserExecutableForPlatform } = await import("./chrome.executables.js");
|
|
||||||
const exe = resolveBrowserExecutableForPlatform(
|
const exe = resolveBrowserExecutableForPlatform(
|
||||||
{} as Parameters<typeof resolveBrowserExecutableForPlatform>[0],
|
{} as Parameters<typeof resolveBrowserExecutableForPlatform>[0],
|
||||||
"darwin",
|
"darwin",
|
||||||
@@ -55,7 +54,7 @@ describe("browser default executable detection", () => {
|
|||||||
expect(exe?.kind).toBe("chrome");
|
expect(exe?.kind).toBe("chrome");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back when default browser is non-Chromium on macOS", async () => {
|
it("falls back when default browser is non-Chromium on macOS", () => {
|
||||||
vi.mocked(execFileSync).mockImplementation((cmd, args) => {
|
vi.mocked(execFileSync).mockImplementation((cmd, args) => {
|
||||||
const argsStr = Array.isArray(args) ? args.join(" ") : "";
|
const argsStr = Array.isArray(args) ? args.join(" ") : "";
|
||||||
if (cmd === "/usr/bin/plutil" && argsStr.includes("LSHandlers")) {
|
if (cmd === "/usr/bin/plutil" && argsStr.includes("LSHandlers")) {
|
||||||
@@ -73,7 +72,6 @@ describe("browser default executable detection", () => {
|
|||||||
return value.includes("Google Chrome.app/Contents/MacOS/Google Chrome");
|
return value.includes("Google Chrome.app/Contents/MacOS/Google Chrome");
|
||||||
});
|
});
|
||||||
|
|
||||||
const { resolveBrowserExecutableForPlatform } = await import("./chrome.executables.js");
|
|
||||||
const exe = resolveBrowserExecutableForPlatform(
|
const exe = resolveBrowserExecutableForPlatform(
|
||||||
{} as Parameters<typeof resolveBrowserExecutableForPlatform>[0],
|
{} as Parameters<typeof resolveBrowserExecutableForPlatform>[0],
|
||||||
"darwin",
|
"darwin",
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import fs from "node:fs/promises";
|
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 { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
import {
|
import {
|
||||||
restoreStateDirEnv,
|
restoreStateDirEnv,
|
||||||
setStateDirEnv,
|
setStateDirEnv,
|
||||||
snapshotStateDirEnv,
|
snapshotStateDirEnv,
|
||||||
} from "../test-helpers/state-dir-env.js";
|
} from "../test-helpers/state-dir-env.js";
|
||||||
|
import { createCanvasHostHandler } from "./server.js";
|
||||||
|
|
||||||
describe("canvas host state dir defaults", () => {
|
describe("canvas host state dir defaults", () => {
|
||||||
let envSnapshot: ReturnType<typeof snapshotStateDirEnv>;
|
let envSnapshot: ReturnType<typeof snapshotStateDirEnv>;
|
||||||
@@ -17,7 +18,6 @@ describe("canvas host state dir defaults", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.resetModules();
|
|
||||||
restoreStateDirEnv(envSnapshot);
|
restoreStateDirEnv(envSnapshot);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -25,9 +25,6 @@ describe("canvas host state dir defaults", () => {
|
|||||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-state-"));
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-state-"));
|
||||||
const stateDir = path.join(tempRoot, "state");
|
const stateDir = path.join(tempRoot, "state");
|
||||||
setStateDirEnv(stateDir);
|
setStateDirEnv(stateDir);
|
||||||
vi.resetModules();
|
|
||||||
|
|
||||||
const { createCanvasHostHandler } = await import("./server.js");
|
|
||||||
const handler = await createCanvasHostHandler({
|
const handler = await createCanvasHostHandler({
|
||||||
runtime: defaultRuntime,
|
runtime: defaultRuntime,
|
||||||
allowInTests: true,
|
allowInTests: true,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import http, { type IncomingMessage, type Server, type ServerResponse } from "no
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { type WebSocket, WebSocketServer } from "ws";
|
import { type WebSocket, WebSocketServer } from "ws";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { STATE_DIR } from "../config/paths.js";
|
import { resolveStateDir } from "../config/paths.js";
|
||||||
import { isTruthyEnvValue } from "../infra/env.js";
|
import { isTruthyEnvValue } from "../infra/env.js";
|
||||||
import { SafeOpenError, openFileWithinRoot } from "../infra/fs-safe.js";
|
import { SafeOpenError, openFileWithinRoot } from "../infra/fs-safe.js";
|
||||||
import { detectMime } from "../media/mime.js";
|
import { detectMime } from "../media/mime.js";
|
||||||
@@ -235,7 +235,7 @@ async function prepareCanvasRoot(rootDir: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveDefaultCanvasRoot(): string {
|
function resolveDefaultCanvasRoot(): string {
|
||||||
const candidates = [path.join(STATE_DIR, "canvas")];
|
const candidates = [path.join(resolveStateDir(), "canvas")];
|
||||||
const existing = candidates.find((dir) => {
|
const existing = candidates.find((dir) => {
|
||||||
try {
|
try {
|
||||||
return fsSync.statSync(dir).isDirectory();
|
return fsSync.statSync(dir).isDirectory();
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
import { GatewayClient } from "./client.js";
|
||||||
|
|
||||||
|
const wsMockState = vi.hoisted(() => ({
|
||||||
|
last: null as { url: unknown; opts: unknown } | null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("ws", () => ({
|
||||||
|
WebSocket: class MockWebSocket {
|
||||||
|
on = vi.fn();
|
||||||
|
close = vi.fn();
|
||||||
|
send = vi.fn();
|
||||||
|
|
||||||
|
constructor(url: unknown, opts: unknown) {
|
||||||
|
wsMockState.last = { url, opts };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
describe("GatewayClient", () => {
|
describe("GatewayClient", () => {
|
||||||
test("uses a large maxPayload for node snapshots", async () => {
|
test("uses a large maxPayload for node snapshots", () => {
|
||||||
vi.resetModules();
|
wsMockState.last = null;
|
||||||
|
|
||||||
class MockWebSocket {
|
|
||||||
static last: { url: unknown; opts: unknown } | null = null;
|
|
||||||
|
|
||||||
on = vi.fn();
|
|
||||||
close = vi.fn();
|
|
||||||
send = vi.fn();
|
|
||||||
|
|
||||||
constructor(url: unknown, opts: unknown) {
|
|
||||||
MockWebSocket.last = { url, opts };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vi.doMock("ws", () => ({
|
|
||||||
WebSocket: MockWebSocket,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { GatewayClient } = await import("./client.js");
|
|
||||||
const client = new GatewayClient({ url: "ws://127.0.0.1:1" });
|
const client = new GatewayClient({ url: "ws://127.0.0.1:1" });
|
||||||
client.start();
|
client.start();
|
||||||
|
|
||||||
expect(MockWebSocket.last?.url).toBe("ws://127.0.0.1:1");
|
expect(wsMockState.last?.url).toBe("ws://127.0.0.1:1");
|
||||||
expect(MockWebSocket.last?.opts).toEqual(
|
expect(wsMockState.last?.opts).toEqual(
|
||||||
expect.objectContaining({ maxPayload: 25 * 1024 * 1024 }),
|
expect.objectContaining({ maxPayload: 25 * 1024 * 1024 }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import fs from "node:fs/promises";
|
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 { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
restoreStateDirEnv,
|
restoreStateDirEnv,
|
||||||
setStateDirEnv,
|
setStateDirEnv,
|
||||||
snapshotStateDirEnv,
|
snapshotStateDirEnv,
|
||||||
} from "../test-helpers/state-dir-env.js";
|
} from "../test-helpers/state-dir-env.js";
|
||||||
|
import { loadOrCreateDeviceIdentity } from "./device-identity.js";
|
||||||
|
|
||||||
describe("device identity state dir defaults", () => {
|
describe("device identity state dir defaults", () => {
|
||||||
let envSnapshot: ReturnType<typeof snapshotStateDirEnv>;
|
let envSnapshot: ReturnType<typeof snapshotStateDirEnv>;
|
||||||
@@ -16,7 +17,6 @@ describe("device identity state dir defaults", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.resetModules();
|
|
||||||
restoreStateDirEnv(envSnapshot);
|
restoreStateDirEnv(envSnapshot);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -24,9 +24,6 @@ describe("device identity state dir defaults", () => {
|
|||||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-identity-state-"));
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-identity-state-"));
|
||||||
const stateDir = path.join(tempRoot, "state");
|
const stateDir = path.join(tempRoot, "state");
|
||||||
setStateDirEnv(stateDir);
|
setStateDirEnv(stateDir);
|
||||||
vi.resetModules();
|
|
||||||
|
|
||||||
const { loadOrCreateDeviceIdentity } = await import("./device-identity.js");
|
|
||||||
const identity = loadOrCreateDeviceIdentity();
|
const identity = loadOrCreateDeviceIdentity();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { STATE_DIR } from "../config/paths.js";
|
import { resolveStateDir } from "../config/paths.js";
|
||||||
|
|
||||||
export type DeviceIdentity = {
|
export type DeviceIdentity = {
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
@@ -17,8 +17,9 @@ type StoredIdentity = {
|
|||||||
createdAtMs: number;
|
createdAtMs: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_DIR = path.join(STATE_DIR, "identity");
|
function resolveDefaultIdentityPath(): string {
|
||||||
const DEFAULT_FILE = path.join(DEFAULT_DIR, "device.json");
|
return path.join(resolveStateDir(), "identity", "device.json");
|
||||||
|
}
|
||||||
|
|
||||||
function ensureDir(filePath: string) {
|
function ensureDir(filePath: string) {
|
||||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||||
@@ -61,7 +62,9 @@ function generateIdentity(): DeviceIdentity {
|
|||||||
return { deviceId, publicKeyPem, privateKeyPem };
|
return { deviceId, publicKeyPem, privateKeyPem };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadOrCreateDeviceIdentity(filePath: string = DEFAULT_FILE): DeviceIdentity {
|
export function loadOrCreateDeviceIdentity(
|
||||||
|
filePath: string = resolveDefaultIdentityPath(),
|
||||||
|
): DeviceIdentity {
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(filePath)) {
|
if (fs.existsSync(filePath)) {
|
||||||
const raw = fs.readFileSync(filePath, "utf8");
|
const raw = fs.readFileSync(filePath, "utf8");
|
||||||
|
|||||||
@@ -1,27 +1,33 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
async function loadWakeModule() {
|
hasHeartbeatWakeHandler,
|
||||||
vi.resetModules();
|
hasPendingHeartbeatWake,
|
||||||
return import("./heartbeat-wake.js");
|
requestHeartbeatNow,
|
||||||
}
|
resetHeartbeatWakeStateForTests,
|
||||||
|
setHeartbeatWakeHandler,
|
||||||
|
} from "./heartbeat-wake.js";
|
||||||
|
|
||||||
describe("heartbeat-wake", () => {
|
describe("heartbeat-wake", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetHeartbeatWakeStateForTests();
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
resetHeartbeatWakeStateForTests();
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("coalesces multiple wake requests into one run", async () => {
|
it("coalesces multiple wake requests into one run", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const wake = await loadWakeModule();
|
|
||||||
const handler = vi.fn().mockResolvedValue({ status: "skipped", reason: "disabled" });
|
const handler = vi.fn().mockResolvedValue({ status: "skipped", reason: "disabled" });
|
||||||
wake.setHeartbeatWakeHandler(handler);
|
setHeartbeatWakeHandler(handler);
|
||||||
|
|
||||||
wake.requestHeartbeatNow({ reason: "interval", coalesceMs: 200 });
|
requestHeartbeatNow({ reason: "interval", coalesceMs: 200 });
|
||||||
wake.requestHeartbeatNow({ reason: "exec-event", coalesceMs: 200 });
|
requestHeartbeatNow({ reason: "exec-event", coalesceMs: 200 });
|
||||||
wake.requestHeartbeatNow({ reason: "retry", coalesceMs: 200 });
|
requestHeartbeatNow({ reason: "retry", coalesceMs: 200 });
|
||||||
|
|
||||||
expect(wake.hasPendingHeartbeatWake()).toBe(true);
|
expect(hasPendingHeartbeatWake()).toBe(true);
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(199);
|
await vi.advanceTimersByTimeAsync(199);
|
||||||
expect(handler).not.toHaveBeenCalled();
|
expect(handler).not.toHaveBeenCalled();
|
||||||
@@ -29,19 +35,18 @@ describe("heartbeat-wake", () => {
|
|||||||
await vi.advanceTimersByTimeAsync(1);
|
await vi.advanceTimersByTimeAsync(1);
|
||||||
expect(handler).toHaveBeenCalledTimes(1);
|
expect(handler).toHaveBeenCalledTimes(1);
|
||||||
expect(handler).toHaveBeenCalledWith({ reason: "exec-event" });
|
expect(handler).toHaveBeenCalledWith({ reason: "exec-event" });
|
||||||
expect(wake.hasPendingHeartbeatWake()).toBe(false);
|
expect(hasPendingHeartbeatWake()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("retries requests-in-flight after the default retry delay", async () => {
|
it("retries requests-in-flight after the default retry delay", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const wake = await loadWakeModule();
|
|
||||||
const handler = vi
|
const handler = vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValueOnce({ status: "skipped", reason: "requests-in-flight" })
|
.mockResolvedValueOnce({ status: "skipped", reason: "requests-in-flight" })
|
||||||
.mockResolvedValueOnce({ status: "ran", durationMs: 1 });
|
.mockResolvedValueOnce({ status: "ran", durationMs: 1 });
|
||||||
wake.setHeartbeatWakeHandler(handler);
|
setHeartbeatWakeHandler(handler);
|
||||||
|
|
||||||
wake.requestHeartbeatNow({ reason: "interval", coalesceMs: 0 });
|
requestHeartbeatNow({ reason: "interval", coalesceMs: 0 });
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(1);
|
await vi.advanceTimersByTimeAsync(1);
|
||||||
expect(handler).toHaveBeenCalledTimes(1);
|
expect(handler).toHaveBeenCalledTimes(1);
|
||||||
@@ -56,19 +61,18 @@ describe("heartbeat-wake", () => {
|
|||||||
|
|
||||||
it("keeps retry cooldown even when a sooner request arrives", async () => {
|
it("keeps retry cooldown even when a sooner request arrives", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const wake = await loadWakeModule();
|
|
||||||
const handler = vi
|
const handler = vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValueOnce({ status: "skipped", reason: "requests-in-flight" })
|
.mockResolvedValueOnce({ status: "skipped", reason: "requests-in-flight" })
|
||||||
.mockResolvedValueOnce({ status: "ran", durationMs: 1 });
|
.mockResolvedValueOnce({ status: "ran", durationMs: 1 });
|
||||||
wake.setHeartbeatWakeHandler(handler);
|
setHeartbeatWakeHandler(handler);
|
||||||
|
|
||||||
wake.requestHeartbeatNow({ reason: "interval", coalesceMs: 0 });
|
requestHeartbeatNow({ reason: "interval", coalesceMs: 0 });
|
||||||
await vi.advanceTimersByTimeAsync(1);
|
await vi.advanceTimersByTimeAsync(1);
|
||||||
expect(handler).toHaveBeenCalledTimes(1);
|
expect(handler).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
// Retry is now waiting for 1000ms. This should not preempt cooldown.
|
// Retry is now waiting for 1000ms. This should not preempt cooldown.
|
||||||
wake.requestHeartbeatNow({ reason: "hook:wake", coalesceMs: 0 });
|
requestHeartbeatNow({ reason: "hook:wake", coalesceMs: 0 });
|
||||||
await vi.advanceTimersByTimeAsync(998);
|
await vi.advanceTimersByTimeAsync(998);
|
||||||
expect(handler).toHaveBeenCalledTimes(1);
|
expect(handler).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
@@ -79,14 +83,13 @@ describe("heartbeat-wake", () => {
|
|||||||
|
|
||||||
it("retries thrown handler errors after the default retry delay", async () => {
|
it("retries thrown handler errors after the default retry delay", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const wake = await loadWakeModule();
|
|
||||||
const handler = vi
|
const handler = vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockRejectedValueOnce(new Error("boom"))
|
.mockRejectedValueOnce(new Error("boom"))
|
||||||
.mockResolvedValueOnce({ status: "skipped", reason: "disabled" });
|
.mockResolvedValueOnce({ status: "skipped", reason: "disabled" });
|
||||||
wake.setHeartbeatWakeHandler(handler);
|
setHeartbeatWakeHandler(handler);
|
||||||
|
|
||||||
wake.requestHeartbeatNow({ reason: "exec-event", coalesceMs: 0 });
|
requestHeartbeatNow({ reason: "exec-event", coalesceMs: 0 });
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(1);
|
await vi.advanceTimersByTimeAsync(1);
|
||||||
expect(handler).toHaveBeenCalledTimes(1);
|
expect(handler).toHaveBeenCalledTimes(1);
|
||||||
@@ -101,42 +104,40 @@ describe("heartbeat-wake", () => {
|
|||||||
|
|
||||||
it("stale disposer does not clear a newer handler", async () => {
|
it("stale disposer does not clear a newer handler", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const wake = await loadWakeModule();
|
|
||||||
const handlerA = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 });
|
const handlerA = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 });
|
||||||
const handlerB = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 });
|
const handlerB = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 });
|
||||||
|
|
||||||
// Runner A registers its handler
|
// Runner A registers its handler
|
||||||
const disposeA = wake.setHeartbeatWakeHandler(handlerA);
|
const disposeA = setHeartbeatWakeHandler(handlerA);
|
||||||
|
|
||||||
// Runner B registers its handler (replaces A)
|
// Runner B registers its handler (replaces A)
|
||||||
const disposeB = wake.setHeartbeatWakeHandler(handlerB);
|
const disposeB = setHeartbeatWakeHandler(handlerB);
|
||||||
|
|
||||||
// Runner A's stale cleanup runs — should NOT clear handlerB
|
// Runner A's stale cleanup runs — should NOT clear handlerB
|
||||||
disposeA();
|
disposeA();
|
||||||
expect(wake.hasHeartbeatWakeHandler()).toBe(true);
|
expect(hasHeartbeatWakeHandler()).toBe(true);
|
||||||
|
|
||||||
// handlerB should still work
|
// handlerB should still work
|
||||||
wake.requestHeartbeatNow({ reason: "interval", coalesceMs: 0 });
|
requestHeartbeatNow({ reason: "interval", coalesceMs: 0 });
|
||||||
await vi.advanceTimersByTimeAsync(1);
|
await vi.advanceTimersByTimeAsync(1);
|
||||||
expect(handlerB).toHaveBeenCalledTimes(1);
|
expect(handlerB).toHaveBeenCalledTimes(1);
|
||||||
expect(handlerA).not.toHaveBeenCalled();
|
expect(handlerA).not.toHaveBeenCalled();
|
||||||
|
|
||||||
// Runner B's dispose should work
|
// Runner B's dispose should work
|
||||||
disposeB();
|
disposeB();
|
||||||
expect(wake.hasHeartbeatWakeHandler()).toBe(false);
|
expect(hasHeartbeatWakeHandler()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preempts existing timer when a sooner schedule is requested", async () => {
|
it("preempts existing timer when a sooner schedule is requested", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const wake = await loadWakeModule();
|
|
||||||
const handler = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 });
|
const handler = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 });
|
||||||
wake.setHeartbeatWakeHandler(handler);
|
setHeartbeatWakeHandler(handler);
|
||||||
|
|
||||||
// Schedule for 5 seconds from now
|
// Schedule for 5 seconds from now
|
||||||
wake.requestHeartbeatNow({ reason: "slow", coalesceMs: 5000 });
|
requestHeartbeatNow({ reason: "slow", coalesceMs: 5000 });
|
||||||
|
|
||||||
// Schedule for 100ms from now — should preempt the 5s timer
|
// Schedule for 100ms from now — should preempt the 5s timer
|
||||||
wake.requestHeartbeatNow({ reason: "fast", coalesceMs: 100 });
|
requestHeartbeatNow({ reason: "fast", coalesceMs: 100 });
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(100);
|
await vi.advanceTimersByTimeAsync(100);
|
||||||
expect(handler).toHaveBeenCalledTimes(1);
|
expect(handler).toHaveBeenCalledTimes(1);
|
||||||
@@ -146,15 +147,14 @@ describe("heartbeat-wake", () => {
|
|||||||
|
|
||||||
it("keeps existing timer when later schedule is requested", async () => {
|
it("keeps existing timer when later schedule is requested", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const wake = await loadWakeModule();
|
|
||||||
const handler = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 });
|
const handler = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 });
|
||||||
wake.setHeartbeatWakeHandler(handler);
|
setHeartbeatWakeHandler(handler);
|
||||||
|
|
||||||
// Schedule for 100ms from now
|
// Schedule for 100ms from now
|
||||||
wake.requestHeartbeatNow({ reason: "fast", coalesceMs: 100 });
|
requestHeartbeatNow({ reason: "fast", coalesceMs: 100 });
|
||||||
|
|
||||||
// Schedule for 5 seconds from now — should NOT preempt
|
// Schedule for 5 seconds from now — should NOT preempt
|
||||||
wake.requestHeartbeatNow({ reason: "slow", coalesceMs: 5000 });
|
requestHeartbeatNow({ reason: "slow", coalesceMs: 5000 });
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(100);
|
await vi.advanceTimersByTimeAsync(100);
|
||||||
expect(handler).toHaveBeenCalledTimes(1);
|
expect(handler).toHaveBeenCalledTimes(1);
|
||||||
@@ -162,12 +162,11 @@ describe("heartbeat-wake", () => {
|
|||||||
|
|
||||||
it("does not downgrade a higher-priority pending reason", async () => {
|
it("does not downgrade a higher-priority pending reason", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const wake = await loadWakeModule();
|
|
||||||
const handler = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 });
|
const handler = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 });
|
||||||
wake.setHeartbeatWakeHandler(handler);
|
setHeartbeatWakeHandler(handler);
|
||||||
|
|
||||||
wake.requestHeartbeatNow({ reason: "exec-event", coalesceMs: 100 });
|
requestHeartbeatNow({ reason: "exec-event", coalesceMs: 100 });
|
||||||
wake.requestHeartbeatNow({ reason: "retry", coalesceMs: 100 });
|
requestHeartbeatNow({ reason: "retry", coalesceMs: 100 });
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(100);
|
await vi.advanceTimersByTimeAsync(100);
|
||||||
expect(handler).toHaveBeenCalledTimes(1);
|
expect(handler).toHaveBeenCalledTimes(1);
|
||||||
@@ -176,14 +175,13 @@ describe("heartbeat-wake", () => {
|
|||||||
|
|
||||||
it("drains pending wake once a handler is registered", async () => {
|
it("drains pending wake once a handler is registered", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const wake = await loadWakeModule();
|
|
||||||
|
|
||||||
wake.requestHeartbeatNow({ reason: "manual", coalesceMs: 0 });
|
requestHeartbeatNow({ reason: "manual", coalesceMs: 0 });
|
||||||
await vi.advanceTimersByTimeAsync(1);
|
await vi.advanceTimersByTimeAsync(1);
|
||||||
expect(wake.hasPendingHeartbeatWake()).toBe(true);
|
expect(hasPendingHeartbeatWake()).toBe(true);
|
||||||
|
|
||||||
const handler = vi.fn().mockResolvedValue({ status: "skipped", reason: "disabled" });
|
const handler = vi.fn().mockResolvedValue({ status: "skipped", reason: "disabled" });
|
||||||
wake.setHeartbeatWakeHandler(handler);
|
setHeartbeatWakeHandler(handler);
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(249);
|
await vi.advanceTimersByTimeAsync(249);
|
||||||
expect(handler).not.toHaveBeenCalled();
|
expect(handler).not.toHaveBeenCalled();
|
||||||
@@ -191,6 +189,6 @@ describe("heartbeat-wake", () => {
|
|||||||
await vi.advanceTimersByTimeAsync(1);
|
await vi.advanceTimersByTimeAsync(1);
|
||||||
expect(handler).toHaveBeenCalledTimes(1);
|
expect(handler).toHaveBeenCalledTimes(1);
|
||||||
expect(handler).toHaveBeenCalledWith({ reason: "manual" });
|
expect(handler).toHaveBeenCalledWith({ reason: "manual" });
|
||||||
expect(wake.hasPendingHeartbeatWake()).toBe(false);
|
expect(hasPendingHeartbeatWake()).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -173,3 +173,17 @@ export function hasHeartbeatWakeHandler() {
|
|||||||
export function hasPendingHeartbeatWake() {
|
export function hasPendingHeartbeatWake() {
|
||||||
return pendingWake !== null || Boolean(timer) || scheduled;
|
return pendingWake !== null || Boolean(timer) || scheduled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resetHeartbeatWakeStateForTests() {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
timer = null;
|
||||||
|
timerDueAt = null;
|
||||||
|
timerKind = null;
|
||||||
|
pendingWake = null;
|
||||||
|
scheduled = false;
|
||||||
|
running = false;
|
||||||
|
handlerGeneration += 1;
|
||||||
|
handler = null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,14 +2,12 @@ import fs from "node:fs/promises";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { withTempHome } from "../../test/helpers/temp-home.js";
|
import { withTempHome } from "../../test/helpers/temp-home.js";
|
||||||
|
import { resolveProviderAuths } from "./provider-usage.auth.js";
|
||||||
|
|
||||||
describe("resolveProviderAuths key normalization", () => {
|
describe("resolveProviderAuths key normalization", () => {
|
||||||
it("strips embedded CR/LF from env keys", async () => {
|
it("strips embedded CR/LF from env keys", async () => {
|
||||||
await withTempHome(
|
await withTempHome(
|
||||||
async () => {
|
async () => {
|
||||||
vi.resetModules();
|
|
||||||
const { resolveProviderAuths } = await import("./provider-usage.auth.js");
|
|
||||||
|
|
||||||
const auths = await resolveProviderAuths({
|
const auths = await resolveProviderAuths({
|
||||||
providers: ["zai", "minimax", "xiaomi"],
|
providers: ["zai", "minimax", "xiaomi"],
|
||||||
});
|
});
|
||||||
@@ -50,9 +48,6 @@ describe("resolveProviderAuths key normalization", () => {
|
|||||||
"utf8",
|
"utf8",
|
||||||
);
|
);
|
||||||
|
|
||||||
vi.resetModules();
|
|
||||||
const { resolveProviderAuths } = await import("./provider-usage.auth.js");
|
|
||||||
|
|
||||||
const auths = await resolveProviderAuths({
|
const auths = await resolveProviderAuths({
|
||||||
providers: ["minimax", "xiaomi"],
|
providers: ["minimax", "xiaomi"],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,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 { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { discoverOpenClawPlugins } from "./discovery.js";
|
||||||
|
|
||||||
const tempDirs: string[] = [];
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
@@ -18,7 +19,6 @@ async function withStateDir<T>(stateDir: string, fn: () => Promise<T>) {
|
|||||||
const prevBundled = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
const prevBundled = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||||
process.env.OPENCLAW_STATE_DIR = stateDir;
|
process.env.OPENCLAW_STATE_DIR = stateDir;
|
||||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
||||||
vi.resetModules();
|
|
||||||
try {
|
try {
|
||||||
return await fn();
|
return await fn();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -32,7 +32,6 @@ async function withStateDir<T>(stateDir: string, fn: () => Promise<T>) {
|
|||||||
} else {
|
} else {
|
||||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = prevBundled;
|
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = prevBundled;
|
||||||
}
|
}
|
||||||
vi.resetModules();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +59,6 @@ describe("discoverOpenClawPlugins", () => {
|
|||||||
fs.writeFileSync(path.join(workspaceExt, "beta.ts"), "export default function () {}", "utf-8");
|
fs.writeFileSync(path.join(workspaceExt, "beta.ts"), "export default function () {}", "utf-8");
|
||||||
|
|
||||||
const { candidates } = await withStateDir(stateDir, async () => {
|
const { candidates } = await withStateDir(stateDir, async () => {
|
||||||
const { discoverOpenClawPlugins } = await import("./discovery.js");
|
|
||||||
return discoverOpenClawPlugins({ workspaceDir });
|
return discoverOpenClawPlugins({ workspaceDir });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,7 +92,6 @@ describe("discoverOpenClawPlugins", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { candidates } = await withStateDir(stateDir, async () => {
|
const { candidates } = await withStateDir(stateDir, async () => {
|
||||||
const { discoverOpenClawPlugins } = await import("./discovery.js");
|
|
||||||
return discoverOpenClawPlugins({});
|
return discoverOpenClawPlugins({});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -123,7 +120,6 @@ describe("discoverOpenClawPlugins", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { candidates } = await withStateDir(stateDir, async () => {
|
const { candidates } = await withStateDir(stateDir, async () => {
|
||||||
const { discoverOpenClawPlugins } = await import("./discovery.js");
|
|
||||||
return discoverOpenClawPlugins({});
|
return discoverOpenClawPlugins({});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -147,7 +143,6 @@ describe("discoverOpenClawPlugins", () => {
|
|||||||
fs.writeFileSync(path.join(packDir, "index.js"), "module.exports = {}", "utf-8");
|
fs.writeFileSync(path.join(packDir, "index.js"), "module.exports = {}", "utf-8");
|
||||||
|
|
||||||
const { candidates } = await withStateDir(stateDir, async () => {
|
const { candidates } = await withStateDir(stateDir, async () => {
|
||||||
const { discoverOpenClawPlugins } = await import("./discovery.js");
|
|
||||||
return discoverOpenClawPlugins({ extraPaths: [packDir] });
|
return discoverOpenClawPlugins({ extraPaths: [packDir] });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+13
-15
@@ -1,19 +1,22 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { resetTelegramFetchStateForTests, resolveTelegramFetch } from "./fetch.js";
|
||||||
|
|
||||||
|
const setDefaultAutoSelectFamily = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
|
vi.mock("node:net", async () => {
|
||||||
|
const actual = await vi.importActual<typeof import("node:net")>("node:net");
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
setDefaultAutoSelectFamily,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe("resolveTelegramFetch", () => {
|
describe("resolveTelegramFetch", () => {
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
|
|
||||||
const loadModule = async () => {
|
|
||||||
const setDefaultAutoSelectFamily = vi.fn();
|
|
||||||
vi.resetModules();
|
|
||||||
vi.doMock("node:net", () => ({
|
|
||||||
setDefaultAutoSelectFamily,
|
|
||||||
}));
|
|
||||||
const mod = await import("./fetch.js");
|
|
||||||
return { resolveTelegramFetch: mod.resolveTelegramFetch, setDefaultAutoSelectFamily };
|
|
||||||
};
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
resetTelegramFetchStateForTests();
|
||||||
|
setDefaultAutoSelectFamily.mockReset();
|
||||||
vi.unstubAllEnvs();
|
vi.unstubAllEnvs();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
if (originalFetch) {
|
if (originalFetch) {
|
||||||
@@ -26,14 +29,12 @@ describe("resolveTelegramFetch", () => {
|
|||||||
it("returns wrapped global fetch when available", async () => {
|
it("returns wrapped global fetch when available", async () => {
|
||||||
const fetchMock = vi.fn(async () => ({}));
|
const fetchMock = vi.fn(async () => ({}));
|
||||||
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
||||||
const { resolveTelegramFetch } = await loadModule();
|
|
||||||
const resolved = resolveTelegramFetch();
|
const resolved = resolveTelegramFetch();
|
||||||
expect(resolved).toBeTypeOf("function");
|
expect(resolved).toBeTypeOf("function");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prefers proxy fetch when provided", async () => {
|
it("prefers proxy fetch when provided", async () => {
|
||||||
const fetchMock = vi.fn(async () => ({}));
|
const fetchMock = vi.fn(async () => ({}));
|
||||||
const { resolveTelegramFetch } = await loadModule();
|
|
||||||
const resolved = resolveTelegramFetch(fetchMock as unknown as typeof fetch);
|
const resolved = resolveTelegramFetch(fetchMock as unknown as typeof fetch);
|
||||||
expect(resolved).toBeTypeOf("function");
|
expect(resolved).toBeTypeOf("function");
|
||||||
});
|
});
|
||||||
@@ -41,14 +42,12 @@ describe("resolveTelegramFetch", () => {
|
|||||||
it("honors env enable override", async () => {
|
it("honors env enable override", async () => {
|
||||||
vi.stubEnv("OPENCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY", "1");
|
vi.stubEnv("OPENCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY", "1");
|
||||||
globalThis.fetch = vi.fn(async () => ({})) as unknown as typeof fetch;
|
globalThis.fetch = vi.fn(async () => ({})) as unknown as typeof fetch;
|
||||||
const { resolveTelegramFetch, setDefaultAutoSelectFamily } = await loadModule();
|
|
||||||
resolveTelegramFetch();
|
resolveTelegramFetch();
|
||||||
expect(setDefaultAutoSelectFamily).toHaveBeenCalledWith(true);
|
expect(setDefaultAutoSelectFamily).toHaveBeenCalledWith(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses config override when provided", async () => {
|
it("uses config override when provided", async () => {
|
||||||
globalThis.fetch = vi.fn(async () => ({})) as unknown as typeof fetch;
|
globalThis.fetch = vi.fn(async () => ({})) as unknown as typeof fetch;
|
||||||
const { resolveTelegramFetch, setDefaultAutoSelectFamily } = await loadModule();
|
|
||||||
resolveTelegramFetch(undefined, { network: { autoSelectFamily: true } });
|
resolveTelegramFetch(undefined, { network: { autoSelectFamily: true } });
|
||||||
expect(setDefaultAutoSelectFamily).toHaveBeenCalledWith(true);
|
expect(setDefaultAutoSelectFamily).toHaveBeenCalledWith(true);
|
||||||
});
|
});
|
||||||
@@ -57,7 +56,6 @@ describe("resolveTelegramFetch", () => {
|
|||||||
vi.stubEnv("OPENCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY", "0");
|
vi.stubEnv("OPENCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY", "0");
|
||||||
vi.stubEnv("OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY", "1");
|
vi.stubEnv("OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY", "1");
|
||||||
globalThis.fetch = vi.fn(async () => ({})) as unknown as typeof fetch;
|
globalThis.fetch = vi.fn(async () => ({})) as unknown as typeof fetch;
|
||||||
const { resolveTelegramFetch, setDefaultAutoSelectFamily } = await loadModule();
|
|
||||||
resolveTelegramFetch(undefined, { network: { autoSelectFamily: true } });
|
resolveTelegramFetch(undefined, { network: { autoSelectFamily: true } });
|
||||||
expect(setDefaultAutoSelectFamily).toHaveBeenCalledWith(false);
|
expect(setDefaultAutoSelectFamily).toHaveBeenCalledWith(false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,3 +42,7 @@ export function resolveTelegramFetch(
|
|||||||
}
|
}
|
||||||
return fetchImpl;
|
return fetchImpl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resetTelegramFetchStateForTests(): void {
|
||||||
|
appliedAutoSelectFamily = null;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user