import path from "node:path"; import { describe, expect, it } from "vitest"; import { DEFAULT_IMAGE_HOST_PATH, DEFAULT_IMAGE_MAX_BYTES, DEFAULT_IMAGE_MAX_COUNT, DEFAULT_IMAGE_TTL_MS, defaultImageLocalDir, parseImageHostPath, parseImageLocalDir, parseTerminalImageStorageMode, resolveTerminalImageStorageSettings, TERMINAL_IMAGE_STORAGE_KEYS, } from "../../../src/backend/images/image-storage-settings.js"; import { createFakeContext } from "@termix/plugin-sdk/testing"; import { readImageStorageSettings } from "../../../src/backend/settings.js"; // parseImageLocalDir resolves against the host platform, so a POSIX literal // becomes a drive-rooted path on Windows. Compare against the same resolution. function localDir(posixPath: string): string { return path.resolve(posixPath); } function stubSettings(values: Record = {}) { return { get: async (key: string) => values[key] ?? null, }; } const EMPTY_ENV: NodeJS.ProcessEnv = {}; describe("terminal image storage settings", () => { describe("mode parsing", () => { it("accepts the three documented modes case-insensitively", () => { expect(parseTerminalImageStorageMode("auto")).toBe("auto"); expect(parseTerminalImageStorageMode("LOCAL")).toBe("local"); expect(parseTerminalImageStorageMode(" remote-sftp ")).toBe( "remote-sftp", ); }); it("rejects unknown modes and non-strings", () => { expect(parseTerminalImageStorageMode("s3")).toBeNull(); expect(parseTerminalImageStorageMode("")).toBeNull(); expect(parseTerminalImageStorageMode(undefined)).toBeNull(); }); }); describe("path validation", () => { it("accepts absolute local directories and normalizes them", () => { expect(parseImageLocalDir("/var/lib/termix/images")).toBe( localDir("/var/lib/termix/images"), ); expect(parseImageLocalDir("/var/lib/termix/../termix/images")).toBeNull(); }); it("rejects relative, empty and NUL-containing local directories", () => { expect(parseImageLocalDir("images")).toBeNull(); expect(parseImageLocalDir("./db/data/images")).toBeNull(); expect(parseImageLocalDir("")).toBeNull(); expect(parseImageLocalDir("/tmp/a\0b")).toBeNull(); }); it("requires the agent-visible host path to be POSIX-absolute", () => { expect(parseImageHostPath("/tmp/termix-image-v0")).toBe( "/tmp/termix-image-v0", ); expect(parseImageHostPath("tmp/termix-image-v0")).toBeNull(); expect(parseImageHostPath("C:\\images")).toBeNull(); expect(parseImageHostPath("/tmp/a\0b")).toBeNull(); }); }); describe("resolution precedence", () => { it("uses built-in defaults when neither the database nor env has values", async () => { const resolved = await resolveTerminalImageStorageSettings( stubSettings(), EMPTY_ENV, ); expect(resolved.mode).toBe("auto"); expect(resolved.localDir).toBe(defaultImageLocalDir(EMPTY_ENV)); expect(resolved.hostPath).toBe(DEFAULT_IMAGE_HOST_PATH); expect(resolved.ttlMs).toBe(DEFAULT_IMAGE_TTL_MS); expect(resolved.maxCount).toBe(DEFAULT_IMAGE_MAX_COUNT); expect(resolved.maxBytes).toBe(DEFAULT_IMAGE_MAX_BYTES); }); it("seeds defaults from legacy TERMIX_IMAGE_* env when no DB value exists", async () => { const resolved = await resolveTerminalImageStorageSettings( stubSettings(), { TERMIX_IMAGE_DIR: "/host-tmp/images", TERMIX_IMAGE_HOST_PATH: "/tmp/images", TERMIX_IMAGE_TTL_MS: "60000", TERMIX_MAX_IMAGE_COUNT: "5", TERMIX_MAX_IMAGE_STORAGE_BYTES: "10485760", }, ); expect(resolved.localDir).toBe(localDir("/host-tmp/images")); expect(resolved.hostPath).toBe("/tmp/images"); expect(resolved.ttlMs).toBe(60_000); expect(resolved.maxCount).toBe(5); expect(resolved.maxBytes).toBe(10_485_760); }); it("lets persisted DB values win over legacy env values", async () => { const resolved = await resolveTerminalImageStorageSettings( stubSettings({ [TERMINAL_IMAGE_STORAGE_KEYS.localDir]: "/db/images", [TERMINAL_IMAGE_STORAGE_KEYS.ttlMs]: "1000", [TERMINAL_IMAGE_STORAGE_KEYS.maxCount]: "7", }), { TERMIX_IMAGE_DIR: "/host-tmp/images", TERMIX_IMAGE_TTL_MS: "60000", TERMIX_MAX_IMAGE_COUNT: "5", }, ); expect(resolved.localDir).toBe(localDir("/db/images")); expect(resolved.ttlMs).toBe(1_000); expect(resolved.maxCount).toBe(7); }); it("falls through to env and defaults when the DB value is invalid", async () => { const resolved = await resolveTerminalImageStorageSettings( stubSettings({ [TERMINAL_IMAGE_STORAGE_KEYS.localDir]: "relative/path", [TERMINAL_IMAGE_STORAGE_KEYS.ttlMs]: "not-a-number", }), { TERMIX_IMAGE_DIR: "/host-tmp/images" }, ); expect(resolved.localDir).toBe(localDir("/host-tmp/images")); expect(resolved.ttlMs).toBe(DEFAULT_IMAGE_TTL_MS); }); it("keeps legacy explicit local mappings on local mode", async () => { const resolved = await resolveTerminalImageStorageSettings( stubSettings(), { TERMIX_IMAGE_DIR: "/host-tmp/images" }, ); expect(resolved.mode).toBe("local"); // hostPath falls back to the legacy local dir, which resolves natively. expect(resolved.hostPath).toBe(localDir("/host-tmp/images")); expect(resolved.localMappingConfigured).toBe(true); }); it("lets a persisted DB mode override the legacy local mapping", async () => { const resolved = await resolveTerminalImageStorageSettings( stubSettings({ [TERMINAL_IMAGE_STORAGE_KEYS.mode]: "remote-sftp", }), { TERMIX_IMAGE_DIR: "/host-tmp/images" }, ); expect(resolved.mode).toBe("remote-sftp"); }); it("clamps out-of-range numeric values like the legacy env parsing did", async () => { const resolved = await resolveTerminalImageStorageSettings( stubSettings(), { TERMIX_IMAGE_TTL_MS: "-5", TERMIX_MAX_IMAGE_COUNT: "0", TERMIX_MAX_IMAGE_STORAGE_BYTES: "10", }, ); expect(resolved.ttlMs).toBe(0); expect(resolved.maxCount).toBe(1); expect(resolved.maxBytes).toBe(1_048_576); }); it("reads the plugin's admin settings, and treats unset fields as unset", async () => { const { ctx } = createFakeContext({ settings: { imageStorageMode: "local", imageLocalDir: "/persisted/images", }, }); const resolved = await readImageStorageSettings(ctx); expect(resolved.mode).toBe("local"); expect(resolved.localDir).toBe(localDir("/persisted/images")); // Nothing stored and no environment: the built-in default. expect(resolved.maxCount).toBe(DEFAULT_IMAGE_MAX_COUNT); }); it("keeps TERMINAL_IMAGE_STORAGE_KEYS as the lookup names", () => { expect(TERMINAL_IMAGE_STORAGE_KEYS.mode).toBe( "terminal_image_storage_mode", ); }); }); });