From b511810f69d8e8ebbe8aad85029d4902f4408eab Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 12 Mar 2026 04:03:43 -0400 Subject: [PATCH] UI: restore cron type compatibility --- ui/src/i18n/test/translate.test.ts | 69 +++++++++++++++++++++++------- ui/src/ui/app-settings.test.ts | 4 +- ui/src/ui/types.ts | 43 ++++++++++++++++++- 3 files changed, 97 insertions(+), 19 deletions(-) diff --git a/ui/src/i18n/test/translate.test.ts b/ui/src/i18n/test/translate.test.ts index 29915c63b01..d373d3a47c9 100644 --- a/ui/src/i18n/test/translate.test.ts +++ b/ui/src/i18n/test/translate.test.ts @@ -1,53 +1,90 @@ -import { describe, it, expect, beforeEach, vi } from "vitest"; -import { i18n, t } from "../lib/translate.ts"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { pt_BR } from "../locales/pt-BR.ts"; import { zh_CN } from "../locales/zh-CN.ts"; import { zh_TW } from "../locales/zh-TW.ts"; +type TranslateModule = typeof import("../lib/translate.ts"); + +function createStorageMock(): Storage { + const store = new Map(); + return { + get length() { + return store.size; + }, + clear() { + store.clear(); + }, + getItem(key: string) { + return store.get(key) ?? null; + }, + key(index: number) { + return Array.from(store.keys())[index] ?? null; + }, + removeItem(key: string) { + store.delete(key); + }, + setItem(key: string, value: string) { + store.set(key, String(value)); + }, + }; +} + describe("i18n", () => { + let translate: TranslateModule; + beforeEach(async () => { + vi.resetModules(); + vi.stubGlobal("localStorage", createStorageMock()); + vi.stubGlobal("navigator", { language: "en-US" } as Navigator); + translate = await import("../lib/translate.ts"); localStorage.clear(); // Reset to English - await i18n.setLocale("en"); + await translate.i18n.setLocale("en"); + }); + + afterEach(() => { + vi.unstubAllGlobals(); }); it("should return the key if translation is missing", () => { - expect(t("non.existent.key")).toBe("non.existent.key"); + expect(translate.t("non.existent.key")).toBe("non.existent.key"); }); it("should return the correct English translation", () => { - expect(t("common.health")).toBe("Health"); + expect(translate.t("common.health")).toBe("Health"); }); it("should replace parameters correctly", () => { - expect(t("overview.stats.cronNext", { time: "10:00" })).toBe("Next wake 10:00"); + expect(translate.t("overview.stats.cronNext", { time: "10:00" })).toBe("Next wake 10:00"); }); it("should fallback to English if key is missing in another locale", async () => { // We haven't registered other locales in the test environment yet, // but the logic should fallback to 'en' map which is always there. - await i18n.setLocale("zh-CN"); + await translate.i18n.setLocale("zh-CN"); // Since we don't mock the import, it might fail to load zh-CN, // but let's assume it falls back to English for now. - expect(t("common.health")).toBeDefined(); + expect(translate.t("common.health")).toBeDefined(); }); it("loads translations even when setting the same locale again", async () => { - const internal = i18n as unknown as { + const internal = translate.i18n as unknown as { locale: string; translations: Record; }; internal.locale = "zh-CN"; delete internal.translations["zh-CN"]; - await i18n.setLocale("zh-CN"); - expect(t("common.health")).toBe("健康状况"); + await translate.i18n.setLocale("zh-CN"); + expect(translate.t("common.health")).toBe("健康状况"); }); it("loads saved non-English locale on startup", async () => { - localStorage.setItem("openclaw.i18n.locale", "zh-CN"); vi.resetModules(); - const fresh = await import("../lib/translate.ts?startup-locale"); + vi.stubGlobal("localStorage", createStorageMock()); + vi.stubGlobal("navigator", { language: "en-US" } as Navigator); + localStorage.setItem("openclaw.i18n.locale", "zh-CN"); + const fresh = await import("../lib/translate.ts"); await vi.waitFor(() => { expect(fresh.i18n.getLocale()).toBe("zh-CN"); }); @@ -56,8 +93,8 @@ describe("i18n", () => { }); it("keeps the version label available in shipped locales", () => { - expect(pt_BR.common.version).toBeTruthy(); - expect(zh_CN.common.version).toBeTruthy(); - expect(zh_TW.common.version).toBeTruthy(); + expect((pt_BR.common as { version?: string }).version).toBeTruthy(); + expect((zh_CN.common as { version?: string }).version).toBeTruthy(); + expect((zh_TW.common as { version?: string }).version).toBeTruthy(); }); }); diff --git a/ui/src/ui/app-settings.test.ts b/ui/src/ui/app-settings.test.ts index ff726d925d0..08c939403ea 100644 --- a/ui/src/ui/app-settings.test.ts +++ b/ui/src/ui/app-settings.test.ts @@ -124,7 +124,7 @@ describe("setTabFromRoute", () => { vi.stubGlobal("window", { setInterval, clearInterval, - } as Window & typeof globalThis); + } as unknown as Window & typeof globalThis); }); afterEach(() => { @@ -203,7 +203,7 @@ describe("setTabFromRoute", () => { setInterval, clearInterval, matchMedia, - } as Window & typeof globalThis); + } as unknown as Window & typeof globalThis); const host = createHost("chat"); host.theme = "knot" as unknown as ThemeName & ThemeMode; diff --git a/ui/src/ui/types.ts b/ui/src/ui/types.ts index a2308100db5..d398bafbaf1 100644 --- a/ui/src/ui/types.ts +++ b/ui/src/ui/types.ts @@ -482,17 +482,58 @@ export type CronStatus = { nextWakeAtMs?: number | null; }; +export type CronJobsEnabledFilter = "all" | "enabled" | "disabled"; +export type CronJobsSortBy = "nextRunAtMs" | "updatedAtMs" | "name"; +export type CronSortDir = "asc" | "desc"; +export type CronRunsStatusFilter = "all" | "ok" | "error" | "skipped"; +export type CronRunsStatusValue = "ok" | "error" | "skipped"; +export type CronDeliveryStatus = "delivered" | "not-delivered" | "unknown" | "not-requested"; +export type CronRunScope = "job" | "all"; + export type CronRunLogEntry = { ts: number; jobId: string; - status: "ok" | "error" | "skipped"; + jobName?: string; + status?: CronRunsStatusValue; durationMs?: number; error?: string; summary?: string; + deliveryStatus?: CronDeliveryStatus; + deliveryError?: string; + delivered?: boolean; + runAtMs?: number; + nextRunAtMs?: number; + model?: string; + provider?: string; + usage?: { + input_tokens?: number; + output_tokens?: number; + total_tokens?: number; + cache_read_tokens?: number; + cache_write_tokens?: number; + }; sessionId?: string; sessionKey?: string; }; +export type CronJobsListResult = { + jobs?: CronJob[]; + total?: number; + offset?: number; + limit?: number; + hasMore?: boolean; + nextOffset?: number | null; +}; + +export type CronRunsResult = { + entries?: CronRunLogEntry[]; + total?: number; + offset?: number; + limit?: number; + hasMore?: boolean; + nextOffset?: number | null; +}; + export type SkillsStatusConfigCheck = { path: string; satisfied: boolean;