diff --git a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx
index 8b155975cf..43da508ffb 100644
--- a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx
+++ b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx
@@ -30,6 +30,16 @@ import {
const MAX_KEY_NAME_LENGTH = 200;
const MAX_SELECTED_MODELS = 500;
+function toLocalDateTimeInputValue(value: string | null | undefined): string {
+ if (!value) return "";
+ const date = new Date(value);
+ if (Number.isNaN(date.getTime())) return "";
+ const pad = (n: number) => String(n).padStart(2, "0");
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(
+ date.getHours()
+ )}:${pad(date.getMinutes())}`;
+}
+
// Debounce hook for search optimization
function useDebouncedValue(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
@@ -2128,15 +2138,32 @@ const PermissionsModal = memo(function PermissionsModal({
Key will automatically stop working after this date.
- {
- const val = e.target.value;
- setExpiresAt(val ? new Date(val).toISOString() : "");
- }}
- className="w-full px-2 py-1.5 text-sm border border-border rounded-md bg-background text-text-main"
- />
+
+ {
+ const val = e.target.value;
+ if (!val) {
+ setExpiresAt("");
+ return;
+ }
+ const date = new Date(val);
+ if (!Number.isNaN(date.getTime())) {
+ setExpiresAt(date.toISOString());
+ }
+ }}
+ className="min-w-0 flex-1 px-2 py-1.5 text-sm border border-border rounded-md bg-background text-text-main"
+ />
+
+
{/* Management Access */}
diff --git a/tests/unit/api-manager-page-static.test.ts b/tests/unit/api-manager-page-static.test.ts
index 43a2cc2f33..29d920e82f 100644
--- a/tests/unit/api-manager-page-static.test.ts
+++ b/tests/unit/api-manager-page-static.test.ts
@@ -35,6 +35,21 @@ test("permissions modal uses i18n for management access description", () => {
assert.doesNotMatch(managementBlock, /Allow this API key to manage OmniRoute configuration\./);
});
+test("permissions modal converts API key expiration ISO timestamps to local datetime input values", () => {
+ const source = readApiManagerPage();
+ const expirationBlock = source.slice(
+ source.indexOf("{/* Expiration Date */}", source.indexOf("const PermissionsModal")),
+ source.indexOf("{/* Management Access */}", source.indexOf("const PermissionsModal"))
+ );
+
+ assert.match(expirationBlock, /value=\{toLocalDateTimeInputValue\(expiresAt\)\}/);
+ assert.match(expirationBlock, /const date = new Date\(val\)/);
+ assert.match(expirationBlock, /setExpiresAt\(date\.toISOString\(\)\)/);
+ assert.match(expirationBlock, /onClick=\{\(\) => setExpiresAt\(""\)\}/);
+ assert.match(expirationBlock, /\{tc\("clear"\)\}/);
+ assert.doesNotMatch(expirationBlock, /expiresAt\.slice\(0, 16\)/);
+});
+
test("permissions modal switch buttons declare button type", () => {
const source = readApiManagerPage();
const modalStart = source.indexOf("const PermissionsModal");
diff --git a/tests/unit/batch-processor-expiration.test.ts b/tests/unit/batch-processor-expiration.test.ts
index ec884b5f1f..4c8281ce98 100644
--- a/tests/unit/batch-processor-expiration.test.ts
+++ b/tests/unit/batch-processor-expiration.test.ts
@@ -1,6 +1,5 @@
-import { afterEach, describe, it } from "node:test";
+import { describe, it } from "node:test";
import assert from "node:assert";
-import { deleteFile, listFiles } from "@/lib/localDb";
// Helper functions from batchProcessor.ts
function parseBatchWindowSeconds(window: string | null | undefined): number {
@@ -18,15 +17,6 @@ function parseBatchWindowSeconds(window: string | null | undefined): number {
}
describe("Batch Processor - File Expiration", () => {
- afterEach(() => {
- const allFiles = listFiles({ limit: 1000 });
- for (const f of allFiles) {
- if (f.filename?.includes("test-batch-")) {
- deleteFile(f.id);
- }
- }
- });
-
describe("getBatchOutputExpiresAt logic", () => {
it("should calculate output expiration as 30 days from batch completion", () => {
const completedAt = Math.floor(Date.now() / 1000) - 5 * 24 * 60 * 60; // 5 days ago
diff --git a/tests/unit/console-log-levels.test.ts b/tests/unit/console-log-levels.test.ts
index 119da1f1ff..a691d7e3bd 100644
--- a/tests/unit/console-log-levels.test.ts
+++ b/tests/unit/console-log-levels.test.ts
@@ -8,7 +8,13 @@ import { updateSettings } from "../../src/lib/db/settings";
const TEST_LOG_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-console-log-levels-"));
const TEST_LOG_PATH = path.join(TEST_LOG_DIR, "app.log");
-process.once("exit", () => fs.rmSync(TEST_LOG_DIR, { recursive: true, force: true }));
+process.once("exit", () => {
+ try {
+ fs.rmSync(TEST_LOG_DIR, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
+ } catch {
+ // Best-effort cleanup only; tmpdir residue must not fail otherwise-passing tests.
+ }
+});
const originalLogFilePath = process.env.APP_LOG_FILE_PATH;
process.env.APP_LOG_FILE_PATH = TEST_LOG_PATH;