mirror of
https://github.com/YuzuZensai/Termix.git
synced 2026-11-12 19:31:43 +00:00
193 lines
7.1 KiB
TypeScript
193 lines
7.1 KiB
TypeScript
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 "../../../database/routes/terminal-image-storage-settings.js";
|
||
|
|
import { SettingsRepository } from "../../../database/repositories/settings-repository.js";
|
||
|
|
import { TestSqliteDatabase } from "../repositories/test-support.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<string, string> = {}) {
|
||
|
|
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 persisted values through the real settings repository", async () => {
|
||
|
|
const adapter = new TestSqliteDatabase();
|
||
|
|
try {
|
||
|
|
const context = await adapter.connect();
|
||
|
|
const repository = new SettingsRepository(context);
|
||
|
|
await repository.set(TERMINAL_IMAGE_STORAGE_KEYS.mode, "local");
|
||
|
|
await repository.set(
|
||
|
|
TERMINAL_IMAGE_STORAGE_KEYS.localDir,
|
||
|
|
"/persisted/images",
|
||
|
|
);
|
||
|
|
|
||
|
|
const resolved = await resolveTerminalImageStorageSettings(repository, {
|
||
|
|
TERMIX_IMAGE_DIR: "/host-tmp/images",
|
||
|
|
});
|
||
|
|
expect(resolved.mode).toBe("local");
|
||
|
|
expect(resolved.localDir).toBe(localDir("/persisted/images"));
|
||
|
|
} finally {
|
||
|
|
await adapter.close();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|