mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 16:51:25 +00:00
perf(test): reduce module reload overhead in key suites
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
restoreStateDirEnv,
|
||||
setStateDirEnv,
|
||||
snapshotStateDirEnv,
|
||||
} from "../test-helpers/state-dir-env.js";
|
||||
import { loadOrCreateDeviceIdentity } from "./device-identity.js";
|
||||
|
||||
describe("device identity state dir defaults", () => {
|
||||
let envSnapshot: ReturnType<typeof snapshotStateDirEnv>;
|
||||
@@ -16,7 +17,6 @@ describe("device identity state dir defaults", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetModules();
|
||||
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 stateDir = path.join(tempRoot, "state");
|
||||
setStateDirEnv(stateDir);
|
||||
vi.resetModules();
|
||||
|
||||
const { loadOrCreateDeviceIdentity } = await import("./device-identity.js");
|
||||
const identity = loadOrCreateDeviceIdentity();
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { STATE_DIR } from "../config/paths.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
|
||||
export type DeviceIdentity = {
|
||||
deviceId: string;
|
||||
@@ -17,8 +17,9 @@ type StoredIdentity = {
|
||||
createdAtMs: number;
|
||||
};
|
||||
|
||||
const DEFAULT_DIR = path.join(STATE_DIR, "identity");
|
||||
const DEFAULT_FILE = path.join(DEFAULT_DIR, "device.json");
|
||||
function resolveDefaultIdentityPath(): string {
|
||||
return path.join(resolveStateDir(), "identity", "device.json");
|
||||
}
|
||||
|
||||
function ensureDir(filePath: string) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
@@ -61,7 +62,9 @@ function generateIdentity(): DeviceIdentity {
|
||||
return { deviceId, publicKeyPem, privateKeyPem };
|
||||
}
|
||||
|
||||
export function loadOrCreateDeviceIdentity(filePath: string = DEFAULT_FILE): DeviceIdentity {
|
||||
export function loadOrCreateDeviceIdentity(
|
||||
filePath: string = resolveDefaultIdentityPath(),
|
||||
): DeviceIdentity {
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
const raw = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
@@ -1,27 +1,33 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
async function loadWakeModule() {
|
||||
vi.resetModules();
|
||||
return import("./heartbeat-wake.js");
|
||||
}
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
hasHeartbeatWakeHandler,
|
||||
hasPendingHeartbeatWake,
|
||||
requestHeartbeatNow,
|
||||
resetHeartbeatWakeStateForTests,
|
||||
setHeartbeatWakeHandler,
|
||||
} from "./heartbeat-wake.js";
|
||||
|
||||
describe("heartbeat-wake", () => {
|
||||
beforeEach(() => {
|
||||
resetHeartbeatWakeStateForTests();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetHeartbeatWakeStateForTests();
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("coalesces multiple wake requests into one run", async () => {
|
||||
vi.useFakeTimers();
|
||||
const wake = await loadWakeModule();
|
||||
const handler = vi.fn().mockResolvedValue({ status: "skipped", reason: "disabled" });
|
||||
wake.setHeartbeatWakeHandler(handler);
|
||||
setHeartbeatWakeHandler(handler);
|
||||
|
||||
wake.requestHeartbeatNow({ reason: "interval", coalesceMs: 200 });
|
||||
wake.requestHeartbeatNow({ reason: "exec-event", coalesceMs: 200 });
|
||||
wake.requestHeartbeatNow({ reason: "retry", coalesceMs: 200 });
|
||||
requestHeartbeatNow({ reason: "interval", coalesceMs: 200 });
|
||||
requestHeartbeatNow({ reason: "exec-event", coalesceMs: 200 });
|
||||
requestHeartbeatNow({ reason: "retry", coalesceMs: 200 });
|
||||
|
||||
expect(wake.hasPendingHeartbeatWake()).toBe(true);
|
||||
expect(hasPendingHeartbeatWake()).toBe(true);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(199);
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
@@ -29,19 +35,18 @@ describe("heartbeat-wake", () => {
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
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 () => {
|
||||
vi.useFakeTimers();
|
||||
const wake = await loadWakeModule();
|
||||
const handler = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ status: "skipped", reason: "requests-in-flight" })
|
||||
.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);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
@@ -56,19 +61,18 @@ describe("heartbeat-wake", () => {
|
||||
|
||||
it("keeps retry cooldown even when a sooner request arrives", async () => {
|
||||
vi.useFakeTimers();
|
||||
const wake = await loadWakeModule();
|
||||
const handler = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ status: "skipped", reason: "requests-in-flight" })
|
||||
.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);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
|
||||
// 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);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -79,14 +83,13 @@ describe("heartbeat-wake", () => {
|
||||
|
||||
it("retries thrown handler errors after the default retry delay", async () => {
|
||||
vi.useFakeTimers();
|
||||
const wake = await loadWakeModule();
|
||||
const handler = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error("boom"))
|
||||
.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);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
@@ -101,42 +104,40 @@ describe("heartbeat-wake", () => {
|
||||
|
||||
it("stale disposer does not clear a newer handler", async () => {
|
||||
vi.useFakeTimers();
|
||||
const wake = await loadWakeModule();
|
||||
const handlerA = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 });
|
||||
const handlerB = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 });
|
||||
|
||||
// Runner A registers its handler
|
||||
const disposeA = wake.setHeartbeatWakeHandler(handlerA);
|
||||
const disposeA = setHeartbeatWakeHandler(handlerA);
|
||||
|
||||
// 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
|
||||
disposeA();
|
||||
expect(wake.hasHeartbeatWakeHandler()).toBe(true);
|
||||
expect(hasHeartbeatWakeHandler()).toBe(true);
|
||||
|
||||
// handlerB should still work
|
||||
wake.requestHeartbeatNow({ reason: "interval", coalesceMs: 0 });
|
||||
requestHeartbeatNow({ reason: "interval", coalesceMs: 0 });
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(handlerB).toHaveBeenCalledTimes(1);
|
||||
expect(handlerA).not.toHaveBeenCalled();
|
||||
|
||||
// Runner B's dispose should work
|
||||
disposeB();
|
||||
expect(wake.hasHeartbeatWakeHandler()).toBe(false);
|
||||
expect(hasHeartbeatWakeHandler()).toBe(false);
|
||||
});
|
||||
|
||||
it("preempts existing timer when a sooner schedule is requested", async () => {
|
||||
vi.useFakeTimers();
|
||||
const wake = await loadWakeModule();
|
||||
const handler = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 });
|
||||
wake.setHeartbeatWakeHandler(handler);
|
||||
setHeartbeatWakeHandler(handler);
|
||||
|
||||
// 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
|
||||
wake.requestHeartbeatNow({ reason: "fast", coalesceMs: 100 });
|
||||
requestHeartbeatNow({ reason: "fast", coalesceMs: 100 });
|
||||
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
@@ -146,15 +147,14 @@ describe("heartbeat-wake", () => {
|
||||
|
||||
it("keeps existing timer when later schedule is requested", async () => {
|
||||
vi.useFakeTimers();
|
||||
const wake = await loadWakeModule();
|
||||
const handler = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 });
|
||||
wake.setHeartbeatWakeHandler(handler);
|
||||
setHeartbeatWakeHandler(handler);
|
||||
|
||||
// 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
|
||||
wake.requestHeartbeatNow({ reason: "slow", coalesceMs: 5000 });
|
||||
requestHeartbeatNow({ reason: "slow", coalesceMs: 5000 });
|
||||
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
@@ -162,12 +162,11 @@ describe("heartbeat-wake", () => {
|
||||
|
||||
it("does not downgrade a higher-priority pending reason", async () => {
|
||||
vi.useFakeTimers();
|
||||
const wake = await loadWakeModule();
|
||||
const handler = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 });
|
||||
wake.setHeartbeatWakeHandler(handler);
|
||||
setHeartbeatWakeHandler(handler);
|
||||
|
||||
wake.requestHeartbeatNow({ reason: "exec-event", coalesceMs: 100 });
|
||||
wake.requestHeartbeatNow({ reason: "retry", coalesceMs: 100 });
|
||||
requestHeartbeatNow({ reason: "exec-event", coalesceMs: 100 });
|
||||
requestHeartbeatNow({ reason: "retry", coalesceMs: 100 });
|
||||
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
@@ -176,14 +175,13 @@ describe("heartbeat-wake", () => {
|
||||
|
||||
it("drains pending wake once a handler is registered", async () => {
|
||||
vi.useFakeTimers();
|
||||
const wake = await loadWakeModule();
|
||||
|
||||
wake.requestHeartbeatNow({ reason: "manual", coalesceMs: 0 });
|
||||
requestHeartbeatNow({ reason: "manual", coalesceMs: 0 });
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(wake.hasPendingHeartbeatWake()).toBe(true);
|
||||
expect(hasPendingHeartbeatWake()).toBe(true);
|
||||
|
||||
const handler = vi.fn().mockResolvedValue({ status: "skipped", reason: "disabled" });
|
||||
wake.setHeartbeatWakeHandler(handler);
|
||||
setHeartbeatWakeHandler(handler);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(249);
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
@@ -191,6 +189,6 @@ describe("heartbeat-wake", () => {
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(handler).toHaveBeenCalledWith({ reason: "manual" });
|
||||
expect(wake.hasPendingHeartbeatWake()).toBe(false);
|
||||
expect(hasPendingHeartbeatWake()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -173,3 +173,17 @@ export function hasHeartbeatWakeHandler() {
|
||||
export function hasPendingHeartbeatWake() {
|
||||
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 { describe, expect, it, vi } from "vitest";
|
||||
import { withTempHome } from "../../test/helpers/temp-home.js";
|
||||
import { resolveProviderAuths } from "./provider-usage.auth.js";
|
||||
|
||||
describe("resolveProviderAuths key normalization", () => {
|
||||
it("strips embedded CR/LF from env keys", async () => {
|
||||
await withTempHome(
|
||||
async () => {
|
||||
vi.resetModules();
|
||||
const { resolveProviderAuths } = await import("./provider-usage.auth.js");
|
||||
|
||||
const auths = await resolveProviderAuths({
|
||||
providers: ["zai", "minimax", "xiaomi"],
|
||||
});
|
||||
@@ -50,9 +48,6 @@ describe("resolveProviderAuths key normalization", () => {
|
||||
"utf8",
|
||||
);
|
||||
|
||||
vi.resetModules();
|
||||
const { resolveProviderAuths } = await import("./provider-usage.auth.js");
|
||||
|
||||
const auths = await resolveProviderAuths({
|
||||
providers: ["minimax", "xiaomi"],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user