fix: local-time timestamps include offset (#14771) (thanks @0xRaini)

This commit is contained in:
Peter Steinberger
2026-02-12 17:53:59 +01:00
parent 468414cac4
commit 2b5df1dfea
4 changed files with 74 additions and 15 deletions

View File

@@ -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);
});

View File

@@ -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();
}

View File

@@ -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();