From b667e11e8f82f7041e30aeb134aabca110c42316 Mon Sep 17 00:00:00 2001 From: Tony Coder <407243179@qq.com> Date: Mon, 17 Aug 2026 10:40:48 +0000 Subject: [PATCH] fix(core): use exact UTC offset for exported session filenames getOffsetDatetime split the timezone offset into whole hours plus leftover minutes. Math.floor rounds negative offsets away from zero, so zones east of UTC that are not a whole number of hours ahead (UTC+05:30, +05:45, +09:30, ...) were shifted an extra hour: a session shared at 15:30 IST was named 20260817T163000_session.md. Shift by the whole offset in milliseconds instead, which is exact for every zone and also stops the helper from mutating its argument. --- core/util/historyUtils.ts | 11 +++---- core/util/historyUtils.vitest.ts | 50 ++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 core/util/historyUtils.vitest.ts diff --git a/core/util/historyUtils.ts b/core/util/historyUtils.ts index fc0939c7bcf..c52493281d7 100644 --- a/core/util/historyUtils.ts +++ b/core/util/historyUtils.ts @@ -10,13 +10,10 @@ import { getContinueGlobalPath } from "../util/paths.js"; // If useful elsewhere, helper funcs should move to core/util/index.ts or similar function getOffsetDatetime(date: Date): Date { - const offset = date.getTimezoneOffset(); - const offsetHours = Math.floor(offset / 60); - const offsetMinutes = offset % 60; - date.setHours(date.getHours() - offsetHours); - date.setMinutes(date.getMinutes() - offsetMinutes); - - return date; + // Shift by the whole offset at once: splitting it into hours and minutes + // rounds the wrong way for zones east of UTC that aren't a whole number of + // hours ahead, e.g. UTC+05:30 would be treated as UTC+06:30. + return new Date(date.getTime() - date.getTimezoneOffset() * 60 * 1000); } function asBasicISOString(date: Date): string { diff --git a/core/util/historyUtils.vitest.ts b/core/util/historyUtils.vitest.ts new file mode 100644 index 00000000000..caec41d849f --- /dev/null +++ b/core/util/historyUtils.vitest.ts @@ -0,0 +1,50 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { shareSession } from "./historyUtils"; + +describe("shareSession", () => { + const originalTz = process.env.TZ; + let outputDir: string; + + const shareAt = async (timeZone: string, isoTime: string) => { + process.env.TZ = timeZone; + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(new Date(isoTime)); + + const ide = { writeFile: vi.fn(), openFile: vi.fn() }; + const fileUrl = await shareSession(ide as any, [], outputDir); + return path.basename(fileUrl); + }; + + beforeEach(() => { + outputDir = fs.mkdtempSync(path.join(os.tmpdir(), "continue-share-")); + }); + + afterEach(() => { + vi.useRealTimers(); + process.env.TZ = originalTz; + fs.rmSync(outputDir, { recursive: true, force: true }); + }); + + it("names the file after local time in whole-hour timezones", async () => { + // UTC-04:00 in August + const name = await shareAt("America/New_York", "2026-08-17T10:00:00.000Z"); + expect(name).toBe("20260817T060000_session.md"); + }); + + it("names the file after local time in half-hour timezones", async () => { + // UTC+05:30 + const name = await shareAt("Asia/Kolkata", "2026-08-17T10:00:00.000Z"); + expect(name).toBe("20260817T153000_session.md"); + }); + + it("names the file after local time in quarter-hour timezones", async () => { + // UTC+05:45 + const name = await shareAt("Asia/Kathmandu", "2026-08-17T10:00:00.000Z"); + expect(name).toBe("20260817T154500_session.md"); + }); +});