diff --git a/src/commands/status.update.test.ts b/src/commands/status.update.test.ts new file mode 100644 index 00000000000..8f3f4e3aff4 --- /dev/null +++ b/src/commands/status.update.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from "vitest"; +import type { UpdateCheckResult } from "../infra/update-check.js"; +import { VERSION } from "../version.js"; +import { formatUpdateOneLiner, resolveUpdateAvailability } from "./status.update.js"; + +function buildUpdate(partial: Partial): UpdateCheckResult { + return { + root: null, + installKind: "unknown", + packageManager: "unknown", + ...partial, + }; +} + +function nextMajorVersion(version: string): string { + const [majorPart] = version.split("."); + const major = Number.parseInt(majorPart ?? "", 10); + if (Number.isFinite(major) && major >= 0) { + return `${major + 1}.0.0`; + } + return "999999.0.0"; +} + +describe("resolveUpdateAvailability", () => { + it("flags git update when behind upstream", () => { + const update = buildUpdate({ + installKind: "git", + git: { + root: "/tmp/repo", + sha: null, + tag: null, + branch: "main", + upstream: "origin/main", + dirty: false, + ahead: 0, + behind: 3, + fetchOk: true, + }, + }); + expect(resolveUpdateAvailability(update)).toEqual({ + available: true, + hasGitUpdate: true, + hasRegistryUpdate: false, + latestVersion: null, + gitBehind: 3, + }); + }); + + it("flags registry update when latest version is newer", () => { + const latestVersion = nextMajorVersion(VERSION); + const update = buildUpdate({ + installKind: "package", + packageManager: "pnpm", + registry: { latestVersion }, + }); + const availability = resolveUpdateAvailability(update); + expect(availability.available).toBe(true); + expect(availability.hasGitUpdate).toBe(false); + expect(availability.hasRegistryUpdate).toBe(true); + expect(availability.latestVersion).toBe(latestVersion); + }); +}); + +describe("formatUpdateOneLiner", () => { + it("renders git status and registry latest summary", () => { + const update = buildUpdate({ + installKind: "git", + git: { + root: "/tmp/repo", + sha: "abc123456789", + tag: null, + branch: "main", + upstream: "origin/main", + dirty: true, + ahead: 0, + behind: 2, + fetchOk: true, + }, + registry: { latestVersion: VERSION }, + deps: { + manager: "pnpm", + status: "ok", + lockfilePath: "pnpm-lock.yaml", + markerPath: "node_modules/.modules.yaml", + }, + }); + + expect(formatUpdateOneLiner(update)).toBe( + `Update: git main · ↔ origin/main · dirty · behind 2 · npm latest ${VERSION} · deps ok`, + ); + }); + + it("renders package-manager mode with registry error", () => { + const update = buildUpdate({ + installKind: "package", + packageManager: "npm", + registry: { latestVersion: null, error: "offline" }, + deps: { + manager: "npm", + status: "missing", + lockfilePath: "package-lock.json", + markerPath: "node_modules", + }, + }); + + expect(formatUpdateOneLiner(update)).toBe("Update: npm · npm latest unknown · deps missing"); + }); +}); diff --git a/src/logging/timestamps.test.ts b/src/logging/timestamps.test.ts new file mode 100644 index 00000000000..f2d72125987 --- /dev/null +++ b/src/logging/timestamps.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; +import { formatLocalIsoWithOffset } from "./timestamps.js"; + +function buildFakeDate(parts: { + year: number; + month: number; + day: number; + hour: number; + minute: number; + second: number; + millisecond: number; + timezoneOffsetMinutes: number; +}): Date { + return { + getFullYear: () => parts.year, + getMonth: () => parts.month - 1, + getDate: () => parts.day, + getHours: () => parts.hour, + getMinutes: () => parts.minute, + getSeconds: () => parts.second, + getMilliseconds: () => parts.millisecond, + getTimezoneOffset: () => parts.timezoneOffsetMinutes, + } as unknown as Date; +} + +describe("formatLocalIsoWithOffset", () => { + it("formats positive offset with millisecond padding", () => { + const value = formatLocalIsoWithOffset( + buildFakeDate({ + year: 2026, + month: 1, + day: 2, + hour: 3, + minute: 4, + second: 5, + millisecond: 6, + timezoneOffsetMinutes: -150, // UTC+02:30 + }), + ); + expect(value).toBe("2026-01-02T03:04:05.006+02:30"); + }); + + it("formats negative offset", () => { + const value = formatLocalIsoWithOffset( + buildFakeDate({ + year: 2026, + month: 12, + day: 31, + hour: 23, + minute: 59, + second: 58, + millisecond: 321, + timezoneOffsetMinutes: 300, // UTC-05:00 + }), + ); + expect(value).toBe("2026-12-31T23:59:58.321-05:00"); + }); +});