diff --git a/src/logging/console-capture.test.ts b/src/logging/console-capture.test.ts index 39acaf108ef..a16c51581a7 100644 --- a/src/logging/console-capture.test.ts +++ b/src/logging/console-capture.test.ts @@ -86,7 +86,10 @@ describe("enableConsoleCapture", () => { console.warn("[EventQueue] Slow listener detected"); expect(warn).toHaveBeenCalledTimes(1); const firstArg = String(warn.mock.calls[0]?.[0] ?? ""); - expect(firstArg.startsWith("2026-01-17T18:01:02.000Z [EventQueue]")).toBe(true); + // Timestamp uses local time with timezone offset instead of UTC "Z" suffix + expect(firstArg).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2} \[EventQueue\]/, + ); vi.useRealTimers(); }); diff --git a/src/logging/console-timestamp.test.ts b/src/logging/console-timestamp.test.ts new file mode 100644 index 00000000000..5bc5aaf0083 --- /dev/null +++ b/src/logging/console-timestamp.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import { formatConsoleTimestamp } from "./console.js"; + +describe("formatConsoleTimestamp", () => { + it("pretty style returns local HH:MM:SS", () => { + 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); + }); + + 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$/); + }); + + 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$/); + }); + + it("timestamp contains the correct local date components", () => { + const before = new Date(); + const result = formatConsoleTimestamp("compact"); + const after = new Date(); + // The date portion should match the local date + const datePart = result.slice(0, 10); + const beforeDate = `${before.getFullYear()}-${String(before.getMonth() + 1).padStart(2, "0")}-${String(before.getDate()).padStart(2, "0")}`; + const afterDate = `${after.getFullYear()}-${String(after.getMonth() + 1).padStart(2, "0")}-${String(after.getDate()).padStart(2, "0")}`; + // Allow for date boundary crossing during test + expect([beforeDate, afterDate]).toContain(datePart); + }); +}); diff --git a/src/logging/console.ts b/src/logging/console.ts index dbff864ba2f..1e28391145c 100644 --- a/src/logging/console.ts +++ b/src/logging/console.ts @@ -135,16 +135,32 @@ function isEpipeError(err: unknown): boolean { return code === "EPIPE" || code === "EIO"; } -function formatConsoleTimestamp(style: ConsoleStyle): string { - const now = new Date().toISOString(); +export function formatConsoleTimestamp(style: ConsoleStyle): string { + const now = new Date(); if (style === "pretty") { - return now.slice(11, 19); + const h = String(now.getHours()).padStart(2, "0"); + const m = String(now.getMinutes()).padStart(2, "0"); + const s = String(now.getSeconds()).padStart(2, "0"); + return `${h}:${m}:${s}`; } - return now; + 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}`; } function hasTimestampPrefix(value: string): boolean { - return /^(?:\d{2}:\d{2}:\d{2}|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?)/.test(value); + return /^(?:\d{2}:\d{2}:\d{2}|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?)/.test( + value, + ); } function isJsonPayload(value: string): boolean {