mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
refactor: harden heartbeat reason classification
This commit is contained in:
52
src/infra/heartbeat-reason.test.ts
Normal file
52
src/infra/heartbeat-reason.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
isHeartbeatActionWakeReason,
|
||||
isHeartbeatEventDrivenReason,
|
||||
normalizeHeartbeatWakeReason,
|
||||
resolveHeartbeatReasonKind,
|
||||
} from "./heartbeat-reason.js";
|
||||
|
||||
describe("heartbeat-reason", () => {
|
||||
it("normalizes wake reasons with trim + requested fallback", () => {
|
||||
expect(normalizeHeartbeatWakeReason(" cron:job-1 ")).toBe("cron:job-1");
|
||||
expect(normalizeHeartbeatWakeReason(" ")).toBe("requested");
|
||||
expect(normalizeHeartbeatWakeReason(undefined)).toBe("requested");
|
||||
});
|
||||
|
||||
it("classifies known reason kinds", () => {
|
||||
expect(resolveHeartbeatReasonKind("retry")).toBe("retry");
|
||||
expect(resolveHeartbeatReasonKind("interval")).toBe("interval");
|
||||
expect(resolveHeartbeatReasonKind("manual")).toBe("manual");
|
||||
expect(resolveHeartbeatReasonKind("exec-event")).toBe("exec-event");
|
||||
expect(resolveHeartbeatReasonKind("wake")).toBe("wake");
|
||||
expect(resolveHeartbeatReasonKind("cron:job-1")).toBe("cron");
|
||||
expect(resolveHeartbeatReasonKind("hook:wake")).toBe("hook");
|
||||
expect(resolveHeartbeatReasonKind(" hook:wake ")).toBe("hook");
|
||||
});
|
||||
|
||||
it("classifies unknown reasons as other", () => {
|
||||
expect(resolveHeartbeatReasonKind("requested")).toBe("other");
|
||||
expect(resolveHeartbeatReasonKind("slow")).toBe("other");
|
||||
expect(resolveHeartbeatReasonKind("")).toBe("other");
|
||||
expect(resolveHeartbeatReasonKind(undefined)).toBe("other");
|
||||
});
|
||||
|
||||
it("matches event-driven behavior used by heartbeat preflight", () => {
|
||||
expect(isHeartbeatEventDrivenReason("exec-event")).toBe(true);
|
||||
expect(isHeartbeatEventDrivenReason("cron:job-1")).toBe(true);
|
||||
expect(isHeartbeatEventDrivenReason("wake")).toBe(true);
|
||||
expect(isHeartbeatEventDrivenReason("hook:gmail:sync")).toBe(true);
|
||||
expect(isHeartbeatEventDrivenReason("interval")).toBe(false);
|
||||
expect(isHeartbeatEventDrivenReason("manual")).toBe(false);
|
||||
expect(isHeartbeatEventDrivenReason("other")).toBe(false);
|
||||
});
|
||||
|
||||
it("matches action-priority wake behavior", () => {
|
||||
expect(isHeartbeatActionWakeReason("manual")).toBe(true);
|
||||
expect(isHeartbeatActionWakeReason("exec-event")).toBe(true);
|
||||
expect(isHeartbeatActionWakeReason("hook:wake")).toBe(true);
|
||||
expect(isHeartbeatActionWakeReason("interval")).toBe(false);
|
||||
expect(isHeartbeatActionWakeReason("cron:job-1")).toBe(false);
|
||||
expect(isHeartbeatActionWakeReason("retry")).toBe(false);
|
||||
});
|
||||
});
|
||||
54
src/infra/heartbeat-reason.ts
Normal file
54
src/infra/heartbeat-reason.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
export type HeartbeatReasonKind =
|
||||
| "retry"
|
||||
| "interval"
|
||||
| "manual"
|
||||
| "exec-event"
|
||||
| "wake"
|
||||
| "cron"
|
||||
| "hook"
|
||||
| "other";
|
||||
|
||||
function trimReason(reason?: string): string {
|
||||
return typeof reason === "string" ? reason.trim() : "";
|
||||
}
|
||||
|
||||
export function normalizeHeartbeatWakeReason(reason?: string): string {
|
||||
const trimmed = trimReason(reason);
|
||||
return trimmed.length > 0 ? trimmed : "requested";
|
||||
}
|
||||
|
||||
export function resolveHeartbeatReasonKind(reason?: string): HeartbeatReasonKind {
|
||||
const trimmed = trimReason(reason);
|
||||
if (trimmed === "retry") {
|
||||
return "retry";
|
||||
}
|
||||
if (trimmed === "interval") {
|
||||
return "interval";
|
||||
}
|
||||
if (trimmed === "manual") {
|
||||
return "manual";
|
||||
}
|
||||
if (trimmed === "exec-event") {
|
||||
return "exec-event";
|
||||
}
|
||||
if (trimmed === "wake") {
|
||||
return "wake";
|
||||
}
|
||||
if (trimmed.startsWith("cron:")) {
|
||||
return "cron";
|
||||
}
|
||||
if (trimmed.startsWith("hook:")) {
|
||||
return "hook";
|
||||
}
|
||||
return "other";
|
||||
}
|
||||
|
||||
export function isHeartbeatEventDrivenReason(reason?: string): boolean {
|
||||
const kind = resolveHeartbeatReasonKind(reason);
|
||||
return kind === "exec-event" || kind === "cron" || kind === "wake" || kind === "hook";
|
||||
}
|
||||
|
||||
export function isHeartbeatActionWakeReason(reason?: string): boolean {
|
||||
const kind = resolveHeartbeatReasonKind(reason);
|
||||
return kind === "manual" || kind === "exec-event" || kind === "hook";
|
||||
}
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
isExecCompletionEvent,
|
||||
} from "./heartbeat-events-filter.js";
|
||||
import { emitHeartbeatEvent, resolveIndicatorType } from "./heartbeat-events.js";
|
||||
import { resolveHeartbeatReasonKind } from "./heartbeat-reason.js";
|
||||
import { resolveHeartbeatVisibility } from "./heartbeat-visibility.js";
|
||||
import {
|
||||
type HeartbeatRunResult,
|
||||
@@ -491,10 +492,11 @@ type HeartbeatPreflight = HeartbeatReasonFlags & {
|
||||
};
|
||||
|
||||
function resolveHeartbeatReasonFlags(reason?: string): HeartbeatReasonFlags {
|
||||
const reasonKind = resolveHeartbeatReasonKind(reason);
|
||||
return {
|
||||
isExecEventReason: reason === "exec-event",
|
||||
isCronEventReason: Boolean(reason?.startsWith("cron:")),
|
||||
isWakeReason: reason === "wake" || Boolean(reason?.startsWith("hook:")),
|
||||
isExecEventReason: reasonKind === "exec-event",
|
||||
isCronEventReason: reasonKind === "cron",
|
||||
isWakeReason: reasonKind === "wake" || reasonKind === "hook",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
isHeartbeatActionWakeReason,
|
||||
normalizeHeartbeatWakeReason,
|
||||
resolveHeartbeatReasonKind,
|
||||
} from "./heartbeat-reason.js";
|
||||
|
||||
export type HeartbeatRunResult =
|
||||
| { status: "ran"; durationMs: number }
|
||||
| { status: "skipped"; reason: string }
|
||||
@@ -29,7 +35,6 @@ let timerKind: WakeTimerKind | null = null;
|
||||
|
||||
const DEFAULT_COALESCE_MS = 250;
|
||||
const DEFAULT_RETRY_MS = 1_000;
|
||||
const HOOK_REASON_PREFIX = "hook:";
|
||||
const REASON_PRIORITY = {
|
||||
RETRY: 0,
|
||||
INTERVAL: 1,
|
||||
@@ -37,29 +42,22 @@ const REASON_PRIORITY = {
|
||||
ACTION: 3,
|
||||
} as const;
|
||||
|
||||
function isActionWakeReason(reason: string): boolean {
|
||||
return reason === "manual" || reason === "exec-event" || reason.startsWith(HOOK_REASON_PREFIX);
|
||||
}
|
||||
|
||||
function resolveReasonPriority(reason: string): number {
|
||||
if (reason === "retry") {
|
||||
const kind = resolveHeartbeatReasonKind(reason);
|
||||
if (kind === "retry") {
|
||||
return REASON_PRIORITY.RETRY;
|
||||
}
|
||||
if (reason === "interval") {
|
||||
if (kind === "interval") {
|
||||
return REASON_PRIORITY.INTERVAL;
|
||||
}
|
||||
if (isActionWakeReason(reason)) {
|
||||
if (isHeartbeatActionWakeReason(reason)) {
|
||||
return REASON_PRIORITY.ACTION;
|
||||
}
|
||||
return REASON_PRIORITY.DEFAULT;
|
||||
}
|
||||
|
||||
function normalizeWakeReason(reason?: string): string {
|
||||
if (typeof reason !== "string") {
|
||||
return "requested";
|
||||
}
|
||||
const trimmed = reason.trim();
|
||||
return trimmed.length > 0 ? trimmed : "requested";
|
||||
return normalizeHeartbeatWakeReason(reason);
|
||||
}
|
||||
|
||||
function normalizeWakeTarget(value?: string): string | undefined {
|
||||
|
||||
Reference in New Issue
Block a user