UI: restore cron type compatibility

This commit is contained in:
Vincent Koc
2026-03-12 04:03:43 -04:00
parent 5ab585ab4a
commit b511810f69
3 changed files with 97 additions and 19 deletions

View File

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

View File

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

View File

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