mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(api): add tool filtering and DB-backed cleanup settings
Add feature flag support to skip disabled/experimental tools at startup by reading disabledTools and enableExperimentalTools from the settings table. Refactor cleanup.ts to read tempFileMaxAgeHours from DB settings (with env var fallback) and respect the startupCleanup setting.
This commit is contained in:
@@ -1,18 +1,56 @@
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { readdir, rm, stat } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { lt } from "drizzle-orm";
|
||||
import { eq, lt } from "drizzle-orm";
|
||||
import { env } from "../config.js";
|
||||
import { db, schema } from "../db/index.js";
|
||||
|
||||
/**
|
||||
* Read the temp file max age from DB settings, falling back to env var.
|
||||
* Called each cleanup cycle so changes take effect without restart.
|
||||
*/
|
||||
export function getMaxAgeMs(): number {
|
||||
try {
|
||||
const row = db
|
||||
.select()
|
||||
.from(schema.settings)
|
||||
.where(eq(schema.settings.key, "tempFileMaxAgeHours"))
|
||||
.get();
|
||||
if (row) {
|
||||
const hours = parseFloat(row.value);
|
||||
if (!isNaN(hours) && hours > 0) return hours * 60 * 60 * 1000;
|
||||
}
|
||||
} catch {
|
||||
/* DB not ready yet, use env */
|
||||
}
|
||||
return env.FILE_MAX_AGE_HOURS * 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether startup cleanup should run.
|
||||
* Returns true by default; only returns false when explicitly set to "false".
|
||||
*/
|
||||
export function shouldRunStartupCleanup(): boolean {
|
||||
try {
|
||||
const row = db
|
||||
.select()
|
||||
.from(schema.settings)
|
||||
.where(eq(schema.settings.key, "startupCleanup"))
|
||||
.get();
|
||||
return row ? row.value !== "false" : true;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function startCleanupCron() {
|
||||
// Ensure workspace directory exists
|
||||
mkdirSync(env.WORKSPACE_PATH, { recursive: true });
|
||||
|
||||
const intervalMs = env.CLEANUP_INTERVAL_MINUTES * 60 * 1000;
|
||||
const maxAgeMs = env.FILE_MAX_AGE_HOURS * 60 * 60 * 1000;
|
||||
|
||||
const cleanup = async () => {
|
||||
const maxAgeMs = getMaxAgeMs();
|
||||
try {
|
||||
const entries = await readdir(env.WORKSPACE_PATH, { withFileTypes: true }).catch(() => []);
|
||||
const now = Date.now();
|
||||
@@ -52,14 +90,16 @@ export function startCleanupCron() {
|
||||
}
|
||||
};
|
||||
|
||||
// Run on startup
|
||||
cleanup();
|
||||
purgeExpiredSessions();
|
||||
// Run on startup only if setting allows it
|
||||
if (shouldRunStartupCleanup()) {
|
||||
cleanup();
|
||||
purgeExpiredSessions();
|
||||
}
|
||||
|
||||
// Schedule recurring cleanup
|
||||
setInterval(cleanup, intervalMs);
|
||||
setInterval(purgeExpiredSessions, 60 * 60 * 1000); // Hourly
|
||||
console.log(
|
||||
`Cleanup scheduled: every ${env.CLEANUP_INTERVAL_MINUTES}m, max age ${env.FILE_MAX_AGE_HOURS}h`,
|
||||
`Cleanup scheduled: every ${env.CLEANUP_INTERVAL_MINUTES}m, max age configurable (env default: ${env.FILE_MAX_AGE_HOURS}h)`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { TOOLS } from "@stirling-image/shared";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { db, schema } from "../../db/index.js";
|
||||
import { registerBarcodeRead } from "./barcode-read.js";
|
||||
import { registerBlurFaces } from "./blur-faces.js";
|
||||
import { registerBorder } from "./border.js";
|
||||
// Phase 3: Optimization extras
|
||||
import { registerBulkRename } from "./bulk-rename.js";
|
||||
// Phase 3: Layout & Composition
|
||||
import { registerCollage } from "./collage.js";
|
||||
import { registerColorAdjustments } from "./color-adjustments.js";
|
||||
import { registerColorPalette } from "./color-palette.js";
|
||||
@@ -18,81 +19,119 @@ import { registerFavicon } from "./favicon.js";
|
||||
import { registerFindDuplicates } from "./find-duplicates.js";
|
||||
import { registerGifTools } from "./gif-tools.js";
|
||||
import { registerImageToPdf } from "./image-to-pdf.js";
|
||||
// Phase 3: Utilities
|
||||
import { registerInfo } from "./info.js";
|
||||
import { registerOcr } from "./ocr.js";
|
||||
import { registerQrGenerate } from "./qr-generate.js";
|
||||
// Phase 4: AI Tools
|
||||
import { registerRemoveBackground } from "./remove-background.js";
|
||||
// Phase 3: Adjustments extra
|
||||
import { registerReplaceColor } from "./replace-color.js";
|
||||
import { registerResize } from "./resize.js";
|
||||
import { registerRotate } from "./rotate.js";
|
||||
import { registerSmartCrop } from "./smart-crop.js";
|
||||
import { registerSplit } from "./split.js";
|
||||
import { registerStripMetadata } from "./strip-metadata.js";
|
||||
// Phase 3: Format & Conversion
|
||||
import { registerSvgToRaster } from "./svg-to-raster.js";
|
||||
import { registerTextOverlay } from "./text-overlay.js";
|
||||
import { registerUpscale } from "./upscale.js";
|
||||
import { registerVectorize } from "./vectorize.js";
|
||||
import { registerWatermarkImage } from "./watermark-image.js";
|
||||
// Phase 3: Watermark & Overlay
|
||||
import { registerWatermarkText } from "./watermark-text.js";
|
||||
|
||||
/**
|
||||
* Registry that imports and registers all tool routes.
|
||||
* Each tool uses the createToolRoute factory from tool-factory.ts.
|
||||
*
|
||||
* Tools listed in the `disabledTools` setting or marked `experimental`
|
||||
* (when `enableExperimentalTools` is off) are skipped at startup.
|
||||
*/
|
||||
export async function registerToolRoutes(app: FastifyInstance): Promise<void> {
|
||||
// Phase 2: Core tools
|
||||
registerResize(app);
|
||||
registerCrop(app);
|
||||
registerRotate(app);
|
||||
registerConvert(app);
|
||||
registerCompress(app);
|
||||
registerStripMetadata(app);
|
||||
registerColorAdjustments(app);
|
||||
// Read disabled tools from settings
|
||||
const disabledRow = db
|
||||
.select()
|
||||
.from(schema.settings)
|
||||
.where(eq(schema.settings.key, "disabledTools"))
|
||||
.get();
|
||||
const disabledTools: string[] = disabledRow ? JSON.parse(disabledRow.value) : [];
|
||||
|
||||
// Phase 3: Watermark & Overlay
|
||||
registerWatermarkText(app);
|
||||
registerWatermarkImage(app);
|
||||
registerTextOverlay(app);
|
||||
registerCompose(app);
|
||||
// Read experimental flag
|
||||
const expRow = db
|
||||
.select()
|
||||
.from(schema.settings)
|
||||
.where(eq(schema.settings.key, "enableExperimentalTools"))
|
||||
.get();
|
||||
const enableExperimental = expRow?.value === "true";
|
||||
|
||||
// Phase 3: Utilities
|
||||
registerInfo(app);
|
||||
registerCompare(app);
|
||||
registerFindDuplicates(app);
|
||||
registerColorPalette(app);
|
||||
registerQrGenerate(app);
|
||||
registerBarcodeRead(app);
|
||||
// Get experimental tool IDs from shared constants
|
||||
const experimentalToolIds = TOOLS.filter((t) => t.experimental).map((t) => t.id);
|
||||
|
||||
// Phase 3: Layout & Composition
|
||||
registerCollage(app);
|
||||
registerSplit(app);
|
||||
registerBorder(app);
|
||||
// Build skip set
|
||||
const skipTools = new Set([...disabledTools, ...(enableExperimental ? [] : experimentalToolIds)]);
|
||||
|
||||
// Phase 3: Format & Conversion
|
||||
registerSvgToRaster(app);
|
||||
registerVectorize(app);
|
||||
registerGifTools(app);
|
||||
const toolRegistrations: Array<{
|
||||
id: string;
|
||||
register: (app: FastifyInstance) => void;
|
||||
}> = [
|
||||
// Essentials
|
||||
{ id: "resize", register: registerResize },
|
||||
{ id: "crop", register: registerCrop },
|
||||
{ id: "rotate", register: registerRotate },
|
||||
{ id: "convert", register: registerConvert },
|
||||
{ id: "compress", register: registerCompress },
|
||||
{ id: "strip-metadata", register: registerStripMetadata },
|
||||
{ id: "color-adjustments", register: registerColorAdjustments },
|
||||
|
||||
// Phase 3: Optimization extras
|
||||
registerBulkRename(app);
|
||||
registerFavicon(app);
|
||||
registerImageToPdf(app);
|
||||
// Watermark & Overlay
|
||||
{ id: "watermark-text", register: registerWatermarkText },
|
||||
{ id: "watermark-image", register: registerWatermarkImage },
|
||||
{ id: "text-overlay", register: registerTextOverlay },
|
||||
{ id: "compose", register: registerCompose },
|
||||
|
||||
// Phase 3: Adjustments extra
|
||||
registerReplaceColor(app);
|
||||
// Utilities
|
||||
{ id: "info", register: registerInfo },
|
||||
{ id: "compare", register: registerCompare },
|
||||
{ id: "find-duplicates", register: registerFindDuplicates },
|
||||
{ id: "color-palette", register: registerColorPalette },
|
||||
{ id: "qr-generate", register: registerQrGenerate },
|
||||
{ id: "barcode-read", register: registerBarcodeRead },
|
||||
|
||||
// Phase 4: AI Tools
|
||||
registerRemoveBackground(app);
|
||||
registerUpscale(app);
|
||||
registerOcr(app);
|
||||
registerBlurFaces(app);
|
||||
registerEraseObject(app);
|
||||
registerSmartCrop(app);
|
||||
// Layout & Composition
|
||||
{ id: "collage", register: registerCollage },
|
||||
{ id: "split", register: registerSplit },
|
||||
{ id: "border", register: registerBorder },
|
||||
|
||||
app.log.info("Tool routes registered (32 tools, 35 endpoints)");
|
||||
// Format & Conversion
|
||||
{ id: "svg-to-raster", register: registerSvgToRaster },
|
||||
{ id: "vectorize", register: registerVectorize },
|
||||
{ id: "gif-tools", register: registerGifTools },
|
||||
|
||||
// Optimization extras
|
||||
{ id: "bulk-rename", register: registerBulkRename },
|
||||
{ id: "favicon", register: registerFavicon },
|
||||
{ id: "image-to-pdf", register: registerImageToPdf },
|
||||
|
||||
// Adjustments extra
|
||||
{ id: "replace-color", register: registerReplaceColor },
|
||||
|
||||
// AI Tools
|
||||
{ id: "remove-background", register: registerRemoveBackground },
|
||||
{ id: "upscale", register: registerUpscale },
|
||||
{ id: "ocr", register: registerOcr },
|
||||
{ id: "blur-faces", register: registerBlurFaces },
|
||||
{ id: "erase-object", register: registerEraseObject },
|
||||
{ id: "smart-crop", register: registerSmartCrop },
|
||||
];
|
||||
|
||||
let skipped = 0;
|
||||
for (const { id, register } of toolRegistrations) {
|
||||
if (skipTools.has(id)) {
|
||||
app.log.info(`Skipping disabled/experimental tool: ${id}`);
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
register(app);
|
||||
}
|
||||
|
||||
const registered = toolRegistrations.length - skipped;
|
||||
app.log.info(
|
||||
`Tool routes registered (${registered}/${toolRegistrations.length} tools, ${skipped} skipped)`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Integration tests for Phase 1 settings keys:
|
||||
* disabledTools, enableExperimentalTools, tempFileMaxAgeHours, startupCleanup
|
||||
*
|
||||
* These verify the settings store correctly persists and retrieves these keys
|
||||
* via the PUT/GET /api/v1/settings endpoints.
|
||||
*/
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { db, schema } from "../../apps/api/src/db/index.js";
|
||||
import { buildTestApp, loginAsAdmin, type TestApp } from "./test-server.js";
|
||||
|
||||
let testApp: TestApp;
|
||||
let app: TestApp["app"];
|
||||
let adminToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
testApp = await buildTestApp();
|
||||
app = testApp.app;
|
||||
adminToken = await loginAsAdmin(app);
|
||||
}, 30_000);
|
||||
|
||||
afterAll(async () => {
|
||||
await testApp.cleanup();
|
||||
}, 10_000);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// disabledTools
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
describe("disabledTools setting", () => {
|
||||
it("can be saved as a JSON array and retrieved", async () => {
|
||||
const disabledTools = ["resize", "crop", "rotate"];
|
||||
|
||||
// Save
|
||||
const putRes = await app.inject({
|
||||
method: "PUT",
|
||||
url: "/api/v1/settings",
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
payload: { disabledTools },
|
||||
});
|
||||
expect(putRes.statusCode).toBe(200);
|
||||
expect(JSON.parse(putRes.body).ok).toBe(true);
|
||||
|
||||
// Retrieve via GET all
|
||||
const getRes = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/v1/settings",
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
});
|
||||
expect(getRes.statusCode).toBe(200);
|
||||
const body = JSON.parse(getRes.body);
|
||||
const parsed = JSON.parse(body.settings.disabledTools);
|
||||
expect(parsed).toEqual(disabledTools);
|
||||
|
||||
// Retrieve via GET specific key
|
||||
const keyRes = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/v1/settings/disabledTools",
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
});
|
||||
expect(keyRes.statusCode).toBe(200);
|
||||
const keyBody = JSON.parse(keyRes.body);
|
||||
expect(JSON.parse(keyBody.value)).toEqual(disabledTools);
|
||||
});
|
||||
|
||||
it("can be updated to an empty array", async () => {
|
||||
const putRes = await app.inject({
|
||||
method: "PUT",
|
||||
url: "/api/v1/settings",
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
payload: { disabledTools: [] },
|
||||
});
|
||||
expect(putRes.statusCode).toBe(200);
|
||||
|
||||
const keyRes = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/v1/settings/disabledTools",
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
});
|
||||
expect(keyRes.statusCode).toBe(200);
|
||||
expect(JSON.parse(JSON.parse(keyRes.body).value)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// enableExperimentalTools
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
describe("enableExperimentalTools setting", () => {
|
||||
it("can be saved and retrieved as 'true'", async () => {
|
||||
const putRes = await app.inject({
|
||||
method: "PUT",
|
||||
url: "/api/v1/settings",
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
payload: { enableExperimentalTools: "true" },
|
||||
});
|
||||
expect(putRes.statusCode).toBe(200);
|
||||
|
||||
const keyRes = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/v1/settings/enableExperimentalTools",
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
});
|
||||
expect(keyRes.statusCode).toBe(200);
|
||||
expect(JSON.parse(keyRes.body).value).toBe("true");
|
||||
});
|
||||
|
||||
it("can be saved and retrieved as 'false'", async () => {
|
||||
const putRes = await app.inject({
|
||||
method: "PUT",
|
||||
url: "/api/v1/settings",
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
payload: { enableExperimentalTools: "false" },
|
||||
});
|
||||
expect(putRes.statusCode).toBe(200);
|
||||
|
||||
const keyRes = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/v1/settings/enableExperimentalTools",
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
});
|
||||
expect(keyRes.statusCode).toBe(200);
|
||||
expect(JSON.parse(keyRes.body).value).toBe("false");
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// tempFileMaxAgeHours
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
describe("tempFileMaxAgeHours setting", () => {
|
||||
it("can be saved and retrieved as a numeric string", async () => {
|
||||
const putRes = await app.inject({
|
||||
method: "PUT",
|
||||
url: "/api/v1/settings",
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
payload: { tempFileMaxAgeHours: "48" },
|
||||
});
|
||||
expect(putRes.statusCode).toBe(200);
|
||||
|
||||
const keyRes = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/v1/settings/tempFileMaxAgeHours",
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
});
|
||||
expect(keyRes.statusCode).toBe(200);
|
||||
expect(JSON.parse(keyRes.body).value).toBe("48");
|
||||
});
|
||||
|
||||
it("can be updated to a different value", async () => {
|
||||
// Set initial value
|
||||
await app.inject({
|
||||
method: "PUT",
|
||||
url: "/api/v1/settings",
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
payload: { tempFileMaxAgeHours: "12" },
|
||||
});
|
||||
|
||||
// Update
|
||||
const putRes = await app.inject({
|
||||
method: "PUT",
|
||||
url: "/api/v1/settings",
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
payload: { tempFileMaxAgeHours: "72" },
|
||||
});
|
||||
expect(putRes.statusCode).toBe(200);
|
||||
|
||||
const keyRes = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/v1/settings/tempFileMaxAgeHours",
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
});
|
||||
expect(JSON.parse(keyRes.body).value).toBe("72");
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// startupCleanup
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
describe("startupCleanup setting", () => {
|
||||
it("can be saved and retrieved as 'true'", async () => {
|
||||
const putRes = await app.inject({
|
||||
method: "PUT",
|
||||
url: "/api/v1/settings",
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
payload: { startupCleanup: "true" },
|
||||
});
|
||||
expect(putRes.statusCode).toBe(200);
|
||||
|
||||
const keyRes = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/v1/settings/startupCleanup",
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
});
|
||||
expect(keyRes.statusCode).toBe(200);
|
||||
expect(JSON.parse(keyRes.body).value).toBe("true");
|
||||
});
|
||||
|
||||
it("can be saved and retrieved as 'false'", async () => {
|
||||
const putRes = await app.inject({
|
||||
method: "PUT",
|
||||
url: "/api/v1/settings",
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
payload: { startupCleanup: "false" },
|
||||
});
|
||||
expect(putRes.statusCode).toBe(200);
|
||||
|
||||
const keyRes = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/v1/settings/startupCleanup",
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
});
|
||||
expect(keyRes.statusCode).toBe(200);
|
||||
expect(JSON.parse(keyRes.body).value).toBe("false");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Tests for cleanup.ts helper functions:
|
||||
* getMaxAgeMs and shouldRunStartupCleanup.
|
||||
*
|
||||
* These test the DB-backed settings lookup with fallback to env vars.
|
||||
* Requires migrations to be run first (shared DB from vitest env).
|
||||
*/
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import { db, schema } from "../../../apps/api/src/db/index.js";
|
||||
import { runMigrations } from "../../../apps/api/src/db/migrate.js";
|
||||
import { getMaxAgeMs, shouldRunStartupCleanup } from "../../../apps/api/src/lib/cleanup.js";
|
||||
|
||||
// Run migrations once to ensure the settings table exists
|
||||
beforeAll(() => {
|
||||
runMigrations();
|
||||
});
|
||||
|
||||
// Helper to insert a setting
|
||||
function setSetting(key: string, value: string) {
|
||||
const existing = db.select().from(schema.settings).where(eq(schema.settings.key, key)).get();
|
||||
if (existing) {
|
||||
db.update(schema.settings)
|
||||
.set({ value, updatedAt: new Date() })
|
||||
.where(eq(schema.settings.key, key))
|
||||
.run();
|
||||
} else {
|
||||
db.insert(schema.settings).values({ key, value }).run();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to remove a setting
|
||||
function removeSetting(key: string) {
|
||||
db.delete(schema.settings).where(eq(schema.settings.key, key)).run();
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
removeSetting("tempFileMaxAgeHours");
|
||||
removeSetting("startupCleanup");
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// getMaxAgeMs
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
describe("getMaxAgeMs", () => {
|
||||
it("returns DB value when tempFileMaxAgeHours is set", () => {
|
||||
setSetting("tempFileMaxAgeHours", "48");
|
||||
const result = getMaxAgeMs();
|
||||
expect(result).toBe(48 * 60 * 60 * 1000);
|
||||
});
|
||||
|
||||
it("returns env fallback when no DB setting exists", () => {
|
||||
removeSetting("tempFileMaxAgeHours");
|
||||
const result = getMaxAgeMs();
|
||||
// vitest.config.ts sets FILE_MAX_AGE_HOURS=1
|
||||
expect(result).toBe(1 * 60 * 60 * 1000);
|
||||
});
|
||||
|
||||
it("returns env fallback for invalid (non-numeric) DB value", () => {
|
||||
setSetting("tempFileMaxAgeHours", "notanumber");
|
||||
const result = getMaxAgeMs();
|
||||
expect(result).toBe(1 * 60 * 60 * 1000);
|
||||
});
|
||||
|
||||
it("returns env fallback for zero or negative DB value", () => {
|
||||
setSetting("tempFileMaxAgeHours", "0");
|
||||
const result = getMaxAgeMs();
|
||||
expect(result).toBe(1 * 60 * 60 * 1000);
|
||||
|
||||
setSetting("tempFileMaxAgeHours", "-5");
|
||||
const result2 = getMaxAgeMs();
|
||||
expect(result2).toBe(1 * 60 * 60 * 1000);
|
||||
});
|
||||
|
||||
it("handles fractional hours", () => {
|
||||
setSetting("tempFileMaxAgeHours", "0.5");
|
||||
const result = getMaxAgeMs();
|
||||
expect(result).toBe(0.5 * 60 * 60 * 1000);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// shouldRunStartupCleanup
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
describe("shouldRunStartupCleanup", () => {
|
||||
it("returns false when setting is 'false'", () => {
|
||||
setSetting("startupCleanup", "false");
|
||||
expect(shouldRunStartupCleanup()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when setting is 'true'", () => {
|
||||
setSetting("startupCleanup", "true");
|
||||
expect(shouldRunStartupCleanup()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when setting is not set", () => {
|
||||
removeSetting("startupCleanup");
|
||||
expect(shouldRunStartupCleanup()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for any value other than 'false'", () => {
|
||||
setSetting("startupCleanup", "yes");
|
||||
expect(shouldRunStartupCleanup()).toBe(true);
|
||||
|
||||
setSetting("startupCleanup", "1");
|
||||
expect(shouldRunStartupCleanup()).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user