mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix: local-time timestamps include offset (#14771) (thanks @0xRaini)
This commit is contained in:
@@ -15,10 +15,18 @@ Docs: https://docs.openclaw.ai
|
||||
- Security: fix unauthenticated Nostr profile API remote config tampering. (#13719) Thanks @coygeek.
|
||||
- Security: remove bundled soul-evil hook. (#14757) Thanks @Imccccc.
|
||||
- Gateway: raise WS payload/buffer limits so 5,000,000-byte image attachments work reliably. (#14486) Thanks @0xRaini.
|
||||
- Logging/CLI: use local timezone timestamps for console prefixing, and include `±HH:MM` offsets when using `openclaw logs --local-time` to avoid ambiguity. (#14771) Thanks @0xRaini.
|
||||
- Gateway: drain active turns before restart to prevent message loss. (#13931) Thanks @0xRaini.
|
||||
- Gateway: auto-generate auth token during install to prevent launchd restart loops. (#13813) Thanks @cathrynlavery.
|
||||
- Gateway: prevent `undefined`/missing token in auth config. (#13809) Thanks @asklee-klawd.
|
||||
- Gateway: handle async `EPIPE` on stdout/stderr during shutdown. (#13414) Thanks @keshav55.
|
||||
- Cron: use requested `agentId` for isolated job auth resolution. (#13983) Thanks @0xRaini.
|
||||
- Cron: prevent cron jobs from skipping execution when `nextRunAtMs` advances. (#14068) Thanks @WalterSumbon.
|
||||
- Cron: pass `agentId` to `runHeartbeatOnce` for main-session jobs. (#14140) Thanks @ishikawa-pro.
|
||||
- Cron: re-arm timers when `onTimer` fires while a job is still executing. (#14233) Thanks @tomron87.
|
||||
- Cron: prevent duplicate fires when multiple jobs trigger simultaneously. (#14256) Thanks @xinhuagu.
|
||||
- Cron: isolate scheduler errors so one bad job does not break all jobs. (#14385) Thanks @MarvinDontPanic.
|
||||
- Cron: prevent one-shot `at` jobs from re-firing on restart after skipped/errored runs. (#13878) Thanks @lailoo.
|
||||
- WhatsApp: convert Markdown bold/strikethrough to WhatsApp formatting. (#14285) Thanks @Raikan10.
|
||||
- WhatsApp: allow media-only sends and normalize leading blank payloads. (#14408) Thanks @karimnaguib.
|
||||
- WhatsApp: default MIME type for voice messages when Baileys omits it. (#14444) Thanks @mcaxtr.
|
||||
|
||||
@@ -132,9 +132,8 @@ describe("logs cli", () => {
|
||||
it("formats local time in plain mode when localTime is true", () => {
|
||||
const utcTime = "2025-01-01T12:00:00.000Z";
|
||||
const result = formatLogTimestamp(utcTime, "plain", true);
|
||||
// Should be local time without 'Z' suffix
|
||||
expect(result).not.toContain("Z");
|
||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
||||
// Should be local time with explicit timezone offset (not 'Z' suffix).
|
||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/);
|
||||
// The exact time depends on timezone, but should be different from UTC
|
||||
expect(result).not.toBe(utcTime);
|
||||
});
|
||||
|
||||
@@ -72,11 +72,25 @@ export function formatLogTimestamp(
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const formatLocalIsoWithOffset = (now: Date) => {
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(now.getDate()).padStart(2, "0");
|
||||
const h = String(now.getHours()).padStart(2, "0");
|
||||
const m = String(now.getMinutes()).padStart(2, "0");
|
||||
const s = String(now.getSeconds()).padStart(2, "0");
|
||||
const ms = String(now.getMilliseconds()).padStart(3, "0");
|
||||
const tzOffset = now.getTimezoneOffset();
|
||||
const tzSign = tzOffset <= 0 ? "+" : "-";
|
||||
const tzHours = String(Math.floor(Math.abs(tzOffset) / 60)).padStart(2, "0");
|
||||
const tzMinutes = String(Math.abs(tzOffset) % 60).padStart(2, "0");
|
||||
return `${year}-${month}-${day}T${h}:${m}:${s}.${ms}${tzSign}${tzHours}:${tzMinutes}`;
|
||||
};
|
||||
|
||||
let timeString: string;
|
||||
if (localTime) {
|
||||
const tzoffset = parsed.getTimezoneOffset() * 60000; // offset in milliseconds
|
||||
const localISOTime = new Date(parsed.getTime() - tzoffset).toISOString().slice(0, -1);
|
||||
timeString = localISOTime;
|
||||
timeString = formatLocalIsoWithOffset(parsed);
|
||||
} else {
|
||||
timeString = parsed.toISOString();
|
||||
}
|
||||
|
||||
@@ -1,31 +1,69 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { formatConsoleTimestamp } from "./console.js";
|
||||
|
||||
describe("formatConsoleTimestamp", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
function pad2(n: number) {
|
||||
return String(n).padStart(2, "0");
|
||||
}
|
||||
|
||||
function pad3(n: number) {
|
||||
return String(n).padStart(3, "0");
|
||||
}
|
||||
|
||||
function formatExpectedLocalIsoWithOffset(now: Date) {
|
||||
const year = now.getFullYear();
|
||||
const month = pad2(now.getMonth() + 1);
|
||||
const day = pad2(now.getDate());
|
||||
const h = pad2(now.getHours());
|
||||
const m = pad2(now.getMinutes());
|
||||
const s = pad2(now.getSeconds());
|
||||
const ms = pad3(now.getMilliseconds());
|
||||
const tzOffset = now.getTimezoneOffset();
|
||||
const tzSign = tzOffset <= 0 ? "+" : "-";
|
||||
const tzHours = pad2(Math.floor(Math.abs(tzOffset) / 60));
|
||||
const tzMinutes = pad2(Math.abs(tzOffset) % 60);
|
||||
return `${year}-${month}-${day}T${h}:${m}:${s}.${ms}${tzSign}${tzHours}:${tzMinutes}`;
|
||||
}
|
||||
|
||||
it("pretty style returns local HH:MM:SS", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-17T18:01:02.345Z"));
|
||||
|
||||
const result = formatConsoleTimestamp("pretty");
|
||||
expect(result).toMatch(/^\d{2}:\d{2}:\d{2}$/);
|
||||
// Verify it uses local time, not UTC
|
||||
const now = new Date();
|
||||
const expectedHour = String(now.getHours()).padStart(2, "0");
|
||||
expect(result.slice(0, 2)).toBe(expectedHour);
|
||||
expect(result).toBe(
|
||||
`${pad2(now.getHours())}:${pad2(now.getMinutes())}:${pad2(now.getSeconds())}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("compact style returns local ISO-like timestamp with timezone offset", () => {
|
||||
const result = formatConsoleTimestamp("compact");
|
||||
// Should match: YYYY-MM-DDTHH:MM:SS.mmm+HH:MM or -HH:MM
|
||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/);
|
||||
// Should NOT end with Z (UTC indicator)
|
||||
expect(result).not.toMatch(/Z$/);
|
||||
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-17T18:01:02.345Z"));
|
||||
const now = new Date();
|
||||
expect(formatConsoleTimestamp("compact")).toBe(formatExpectedLocalIsoWithOffset(now));
|
||||
});
|
||||
|
||||
it("json style returns local ISO-like timestamp with timezone offset", () => {
|
||||
const result = formatConsoleTimestamp("json");
|
||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/);
|
||||
expect(result).not.toMatch(/Z$/);
|
||||
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-17T18:01:02.345Z"));
|
||||
const now = new Date();
|
||||
expect(formatConsoleTimestamp("json")).toBe(formatExpectedLocalIsoWithOffset(now));
|
||||
});
|
||||
|
||||
it("timestamp contains the correct local date components", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-17T18:01:02.345Z"));
|
||||
|
||||
const before = new Date();
|
||||
const result = formatConsoleTimestamp("compact");
|
||||
const after = new Date();
|
||||
|
||||
Reference in New Issue
Block a user