From fd1fcaa221017aea5d4d2bb8c8b04e70745b86dc Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 25 Apr 2026 15:06:56 +0200 Subject: [PATCH] refactor: implement dependency injection for encryption and enhance model analysis --- build_and_run.sh | 11 ++ .../de/tki/comfymodels/BatchVerification.java | 88 ------------ src/main/java/de/tki/comfymodels/Main.java | 77 ++++++++--- src/main/java/de/tki/comfymodels/PngTest.java | 49 ------- .../service/impl/ComfyModelAnalyzer.java | 129 ++++++++---------- .../service/impl/ConfigService.java | 20 ++- .../service/impl/DefaultDownloadManager.java | 2 +- .../service/impl/EncryptionUtils.java | 15 +- .../service/impl/GeminiAIService.java | 16 +-- .../service/impl/LocalAIService.java | 16 ++- .../service/impl/ModelSearchService.java | 4 +- .../comfymodels/BatchVerificationTest.java | 67 +++++++++ .../tki/comfymodels/DownloadStatusTest.java | 11 +- .../comfymodels/HiDreamReproductionTest.java | 6 +- .../tki/comfymodels/QualityAssuranceTest.java | 10 +- .../de/tki/comfymodels/ReproductionTest.java | 4 +- .../service/CosmosRegressionTest.java | 80 +++++++++++ .../service/DownloadSelectionTest.java | 10 +- .../service/ExistingModelSizeTest.java | 33 +++++ .../comfymodels/service/FluxSchnellTest.java | 78 +++++++++++ .../service/HiDreamComprehensiveTest.java | 83 +++++++++++ .../service/PngExtractionTest.java | 84 ++++++++++++ .../service/QwenReproductionTest.java | 18 +-- .../service/StatusTransitionTest.java | 5 +- .../service/VaultIntegrationTest.java | 26 +++- 25 files changed, 638 insertions(+), 304 deletions(-) create mode 100644 build_and_run.sh delete mode 100644 src/main/java/de/tki/comfymodels/BatchVerification.java delete mode 100644 src/main/java/de/tki/comfymodels/PngTest.java create mode 100644 src/test/java/de/tki/comfymodels/BatchVerificationTest.java create mode 100644 src/test/java/de/tki/comfymodels/service/CosmosRegressionTest.java create mode 100644 src/test/java/de/tki/comfymodels/service/ExistingModelSizeTest.java create mode 100644 src/test/java/de/tki/comfymodels/service/FluxSchnellTest.java create mode 100644 src/test/java/de/tki/comfymodels/service/HiDreamComprehensiveTest.java create mode 100644 src/test/java/de/tki/comfymodels/service/PngExtractionTest.java diff --git a/build_and_run.sh b/build_and_run.sh new file mode 100644 index 0000000..133244a --- /dev/null +++ b/build_and_run.sh @@ -0,0 +1,11 @@ +#!/bin/bash +echo "=== Building ComfyUI Model Downloader ===" +mvn clean package -DskipTests +if [ $? -ne 0 ]; then + echo "[ERROR] Build failed!" + exit 1 +fi + +echo "" +echo "=== Starting Application ===" +java -jar target/ComfyUIModelDownloader.jar "$@" diff --git a/src/main/java/de/tki/comfymodels/BatchVerification.java b/src/main/java/de/tki/comfymodels/BatchVerification.java deleted file mode 100644 index 4af0241..0000000 --- a/src/main/java/de/tki/comfymodels/BatchVerification.java +++ /dev/null @@ -1,88 +0,0 @@ -package de.tki.comfymodels; - -import de.tki.comfymodels.domain.ModelInfo; -import de.tki.comfymodels.service.impl.ComfyModelAnalyzer; -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.nio.file.*; -import java.util.List; -import java.util.zip.InflaterInputStream; - -public class BatchVerification { - public static void main(String[] args) throws Exception { - ComfyModelAnalyzer analyzer = new ComfyModelAnalyzer(); - Path root = Paths.get("C:/pinokio/api/comfy.git/workflows/ComfyUI_examples"); - - System.out.println("=== BATCH VERIFICATION START ==="); - - Files.walk(root) - .filter(p -> p.toString().endsWith(".json") || p.toString().endsWith(".png")) - .limit(30) // Stichprobe von 30 verschiedenen Workflows - .forEach(path -> { - try { - System.out.println("\nFILE: " + path.getFileName()); - String content; - if (path.toString().endsWith(".png")) { - content = extractWorkflowFromPng(path.toFile()); - } else { - content = Files.readString(path); - } - - if (content == null) { - System.out.println(" [!] No workflow found in PNG"); - return; - } - - List results = analyzer.analyze(content, path.getFileName().toString()); - if (results.isEmpty()) { - System.out.println(" [?] No models detected"); - } else { - for (ModelInfo info : results) { - System.out.printf(" [%s] %s | URL: %s | Source: %s\n", - info.getType(), info.getName(), - info.getUrl().equals("MISSING") ? "Missing" : "FOUND", - info.getPopularity()); - } - } - } catch (Exception e) { - System.out.println(" [ERROR] " + e.getMessage()); - } - }); - - System.out.println("\n=== BATCH VERIFICATION END ==="); - } - - private static String extractWorkflowFromPng(File file) { - try (DataInputStream dis = new DataInputStream(new FileInputStream(file))) { - if (dis.readLong() != 0x89504E470D0A1A0AL) return null; - while (true) { - int length = dis.readInt(); - byte[] typeBytes = new byte[4]; - dis.readFully(typeBytes); - String type = new String(typeBytes, StandardCharsets.US_ASCII); - if ("tEXt".equals(type) || "iTXt".equals(type) || "zTXt".equals(type)) { - byte[] data = new byte[length]; - dis.readFully(data); - int nullPos = 0; while (nullPos < data.length && data[nullPos] != 0) nullPos++; - String key = new String(data, 0, nullPos, StandardCharsets.UTF_8); - String value = null; - if ("tEXt".equals(type)) value = new String(data, nullPos + 1, data.length - (nullPos + 1), StandardCharsets.UTF_8); - else if ("zTXt".equals(type)) { - try (InflaterInputStream iis = new InflaterInputStream(new ByteArrayInputStream(data, nullPos + 2, data.length - (nullPos + 2)))) { value = new String(iis.readAllBytes(), StandardCharsets.UTF_8); } - } else if ("iTXt".equals(type)) { - int currentPos = nullPos + 3; int nullCount = 0; - while (currentPos < data.length && nullCount < 2) { if (data[currentPos] == 0) nullCount++; currentPos++; } - if (currentPos < data.length) { - if (data[nullPos + 1] != 0) { - try (InflaterInputStream iis = new InflaterInputStream(new ByteArrayInputStream(data, currentPos, data.length - currentPos))) { value = new String(iis.readAllBytes(), StandardCharsets.UTF_8); } - } else value = new String(data, currentPos, data.length - currentPos, StandardCharsets.UTF_8); - } - } - if ("workflow".equalsIgnoreCase(key) || "prompt".equalsIgnoreCase(key)) return value; - dis.readInt(); - } else if ("IEND".equals(type)) break; else dis.skipBytes(length + 4); - } - } catch (Exception e) {} - return null; - } -} diff --git a/src/main/java/de/tki/comfymodels/Main.java b/src/main/java/de/tki/comfymodels/Main.java index f45b129..1535d9c 100644 --- a/src/main/java/de/tki/comfymodels/Main.java +++ b/src/main/java/de/tki/comfymodels/Main.java @@ -101,8 +101,10 @@ public class Main extends JFrame { } else { FlatLightLaf.setup(); } + FlatLaf.updateUI(); // Modern UI tweaks + UIManager.put("TitlePane.unifiedBackground", true); UIManager.put("Button.arc", 8); UIManager.put("Component.arc", 8); UIManager.put("TextComponent.arc", 8); @@ -255,6 +257,9 @@ public class Main extends JFrame { setTitle("ComfyUIModel-Downloader"); setSize(1450, 1000); setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + + // Enable FlatLaf window decorations for this frame + getRootPane().putClientProperty("flatlaf.useWindowDecorations", true); JPanel mainContainer = new JPanel(new BorderLayout(10, 10)); mainContainer.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); @@ -295,7 +300,7 @@ public class Main extends JFrame { pathRow.add(pathButtons, BorderLayout.EAST); JPanel apiRow = new JPanel(new BorderLayout()); - apiRow.setBorder(BorderFactory.createTitledBorder("Gemini AI API Key")); + apiRow.setBorder(BorderFactory.createTitledBorder("Gemini AI API Key (Optional)")); geminiKeyField = new JPasswordField(); JButton saveGeminiBtn = new JButton("Save & Discover"); saveGeminiBtn.addActionListener(e -> { @@ -306,7 +311,7 @@ public class Main extends JFrame { apiRow.add(saveGeminiBtn, BorderLayout.EAST); JPanel hfRow = new JPanel(new BorderLayout()); - hfRow.setBorder(BorderFactory.createTitledBorder("Hugging Face Access Token")); + hfRow.setBorder(BorderFactory.createTitledBorder("Hugging Face Access Token (Optional)")); hfTokenField = new JPasswordField(); JButton saveHfBtn = new JButton("Save HF Token"); saveHfBtn.addActionListener(e -> { @@ -330,6 +335,7 @@ public class Main extends JFrame { shutdownCheck = new JCheckBox("Shutdown after Queue"); shutdownCheck.addActionListener(e -> configService.setShutdownAfterDownloadEnabled(shutdownCheck.isSelected())); + // Most important feature (irony): Dark mode for Reddit... or at least for this tool. darkCheck = new JCheckBox("Dark Mode"); darkCheck.addActionListener(e -> { boolean isDark = darkCheck.isSelected(); @@ -411,7 +417,7 @@ public class Main extends JFrame { JTable modelTable = new JTable(tableModel); modelTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); - // Sofortiger Abbruch des Editors bei Klick, um Wert zu übernehmen + // Immediate termination of the editor on click to apply the value modelTable.addMouseListener(new MouseAdapter() { @Override public void mouseReleased(MouseEvent e) { @@ -566,13 +572,25 @@ public class Main extends JFrame { String status = info.getUrl().equals("MISSING") ? "Idle" : "✅ Known Good"; boolean isSelected = true; Path localPath = Paths.get(baseModelsPath, info.getSave_path() != null ? info.getSave_path() : type, info.getName()); - if (Files.exists(localPath)) { + boolean existsLocally = Files.exists(localPath); + + if (existsLocally) { status = "✅ Already exists"; isSelected = false; // Deselect if already present + try { + long localSizeBytes = Files.size(localPath); + info.setSize(searchService.formatSize(localSizeBytes)); + } catch (IOException e) { + info.setSize("Local (Error)"); + } } tableModel.addRow(new Object[]{isSelected, info.getType(), info.getName(), info.getSize(), info.getPopularity(), targetSubDir, info.getUrl(), status}); - if (!info.getUrl().equals("MISSING")) fetchSizeInBackground(info, i); + + // Only fetch remote size if NOT already existing locally + if (!existsLocally && !info.getUrl().equals("MISSING")) { + fetchSizeInBackground(info, i); + } } downloadButton.setEnabled(!modelsToDownload.isEmpty()); } @@ -695,26 +713,38 @@ public class Main extends JFrame { } private void showHelpDialog() { - String helpText = "" + - "

ComfyUIModel-Downloader Help

" + - "

Core Features

" + + String helpText = "" + + "

ComfyUI Model Downloader Help

" + + "

This tool automates the process of finding and downloading models required for your ComfyUI workflows.

" + + + "

🚀 Core Features

" + "" + - "

API Keys & Security

" + + + "

🛡️ Quality & Security

" + "" + + + "

⚙️ Advanced Settings

" + + "" + + + "

🔑 API Keys (Optional)

" + + "" + ""; @@ -731,6 +761,9 @@ public class Main extends JFrame { } public static void main(String[] args) { + // Enable modern window decorations BEFORE anything else + com.formdev.flatlaf.FlatLaf.setUseNativeWindowDecorations(true); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); Main main = context.getBean(Main.class); main.launch(args); diff --git a/src/main/java/de/tki/comfymodels/PngTest.java b/src/main/java/de/tki/comfymodels/PngTest.java deleted file mode 100644 index 0864b40..0000000 --- a/src/main/java/de/tki/comfymodels/PngTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package de.tki.comfymodels; - -import java.io.*; -import java.nio.charset.StandardCharsets; - -public class PngTest { - public static void main(String[] args) { - File file = new File("flux_canny_model_example.png"); - String workflow = extractWorkflowFromPng(file); - if (workflow != null) { - System.out.println("SUCCESS! Extracted workflow length: " + workflow.length()); - System.out.println("Snippet: " + workflow.substring(0, Math.min(200, workflow.length()))); - } else { - System.out.println("FAILED! No workflow found."); - } - } - - private static String extractWorkflowFromPng(File file) { - try (DataInputStream dis = new DataInputStream(new FileInputStream(file))) { - if (dis.readLong() != 0x89504E470D0A1A0AL) return null; - while (true) { - int length = dis.readInt(); - byte[] typeBytes = new byte[4]; - dis.readFully(typeBytes); - String type = new String(typeBytes, StandardCharsets.US_ASCII); - if ("tEXt".equals(type) || "iTXt".equals(type) || "zTXt".equals(type)) { - byte[] data = new byte[length]; - dis.readFully(data); - String content = new String(data, StandardCharsets.UTF_8); - if (content.contains("workflow") || content.contains("prompt")) { - for (int i = 0; i < data.length; i++) { - if (data[i] == 0) { - return new String(data, i + 1, data.length - i - 1, StandardCharsets.UTF_8).trim(); - } - } - } - dis.readInt(); // CRC - } else if ("IEND".equals(type)) { - break; - } else { - dis.skipBytes(length + 4); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } -} diff --git a/src/main/java/de/tki/comfymodels/service/impl/ComfyModelAnalyzer.java b/src/main/java/de/tki/comfymodels/service/impl/ComfyModelAnalyzer.java index e39cbf3..e358bb7 100644 --- a/src/main/java/de/tki/comfymodels/service/impl/ComfyModelAnalyzer.java +++ b/src/main/java/de/tki/comfymodels/service/impl/ComfyModelAnalyzer.java @@ -14,7 +14,7 @@ import java.util.regex.Pattern; @Service public class ComfyModelAnalyzer implements IModelAnalyzer { - @Autowired(required = false) + @Autowired private LocalAIService aiService; @Autowired @@ -23,14 +23,12 @@ public class ComfyModelAnalyzer implements IModelAnalyzer { @Autowired(required = false) private GeminiAIService geminiService; - private static final class ExpertPatterns { - static final Pattern FILE = Pattern.compile("([a-zA-Z0-9_\\-\\.\\/]+\\.(?:safetensors|sft|ckpt|pth|pt|bin|onnx|yaml))", Pattern.CASE_INSENSITIVE); - static final Pattern COMP_VAE = Pattern.compile("(vae|tokenizer|autoencoder)", Pattern.CASE_INSENSITIVE); - static final Pattern COMP_CLIP = Pattern.compile("(clip|t5|text_encoder|llama|gemma|mistral|t5xxl|clip_l|clip_g|qwen)", Pattern.CASE_INSENSITIVE); - static final Pattern COMP_UNET = Pattern.compile("(unet|diffusion|model|transformer|base|transformer|upscale|esrgan|z_image|lumina|flux|dit|mochi|ltx|cosmos|hunyuan|hy|wan|hidream|aura|svd)", Pattern.CASE_INSENSITIVE); - static final Pattern MD_LINK = Pattern.compile("\\[([^\\]]+)\\]\\((https?://[^\\)]+)\\)", Pattern.CASE_INSENSITIVE); - static final Pattern NAKED_URL = Pattern.compile("(https?://[a-zA-Z0-9\\-\\.\\/\\?\\=\\&\\%]+(?:\\.(?:safetensors|sft|ckpt|pth|pt|bin|onnx|yaml))(?:\\?[^\\s\\\"\\']*)?)", Pattern.CASE_INSENSITIVE); - } + private final Pattern FILE_PATTERN = Pattern.compile("([a-zA-Z0-9_\\-\\.\\/]+\\.(?:safetensors|sft|ckpt|pth|pt|bin|onnx|yaml))", Pattern.CASE_INSENSITIVE); + private final Pattern COMP_VAE_PATTERN = Pattern.compile("(vae|tokenizer|autoencoder|encoder|decoder)", Pattern.CASE_INSENSITIVE); + private final Pattern COMP_CLIP_PATTERN = Pattern.compile("(clip|text.encoder|t5|llama|gemma|mistral|qwen|bert|vit|embed)", Pattern.CASE_INSENSITIVE); + private final Pattern COMP_UNET_PATTERN = Pattern.compile("(unet|diffusion|transformer|dit|model|base|upscale|esrgan|resnet|sampling)", Pattern.CASE_INSENSITIVE); + private final Pattern MD_LINK_PATTERN = Pattern.compile("\\[([^\\]]+)\\]\\((https?://[^\\)]+)\\)", Pattern.CASE_INSENSITIVE); + private final Pattern NAKED_URL_PATTERN = Pattern.compile("(https?://[a-zA-Z0-9\\-\\.\\/\\?\\=\\&\\%]+(?:\\.(?:safetensors|sft|ckpt|pth|pt|bin|onnx|yaml))(?:\\?[^\\s\\\"\\']*)?)", Pattern.CASE_INSENSITIVE); @Override public List analyze(String jsonText, String fileName) { @@ -40,13 +38,12 @@ public class ComfyModelAnalyzer implements IModelAnalyzer { JSONObject jo = parseJson(jsonText); if (jo == null) return results; - // NEW: Globale Kontext-Erkennung vor der Analyse + // Global context detection based on frequency/filename rather than hardcoded list String globalContext = extractGlobalContext(jo, fileName); findModelsMetadata(jo, results); scanForModelFiles(jo, null, results); - // Wende globalen Kontext auf alle Ergebnisse an applyGlobalContext(results, globalContext); for (ModelInfo info : results) { @@ -59,13 +56,12 @@ public class ComfyModelAnalyzer implements IModelAnalyzer { info.setSave_path(match.getSave_path()); info.setPopularity("📂 MODEL LIST MATCH"); } else { - // Fallback to AI prediction if not in list - if (aiService == null) aiService = new LocalAIService(); - LocalAIService.Prediction prediction = aiService.predictProvider(info.getName()); - info.setPopularity(prediction.getLabel()); + if (aiService != null) { + LocalAIService.Prediction prediction = aiService.predictProvider(info.getName()); + info.setPopularity(prediction.getLabel()); + } - // Falls die Popularität noch generisch ist, füge den Kontext hinzu - if (globalContext != null && !globalContext.isEmpty() && info.getPopularity().contains("Community")) { + if (globalContext != null && !globalContext.isEmpty() && info.getPopularity() != null && info.getPopularity().contains("Community")) { info.setPopularity("✨ Context: " + globalContext + " (" + info.getPopularity() + ")"); } } @@ -75,26 +71,22 @@ public class ComfyModelAnalyzer implements IModelAnalyzer { } private String extractGlobalContext(JSONObject jo, String fileName) { - Map hints = new HashMap<>(); - - // 1. Aus Dateiname (höchste Prio) + // Generic approach: extract the most significant prefix from filename or content if (fileName != null && !fileName.toLowerCase().startsWith("workflow")) { String base = fileName.split("[_\\- ]")[0].toLowerCase(); if (base.length() >= 3) return base; } - // 2. Scan den ganzen JSON nach signifikanten Schlüsselwörtern + // Scan for frequently occurring specific terms in class_types (that are not standard keywords) + Map counts = new HashMap<>(); String fullText = jo.toString().toLowerCase(); - String[] keywords = {"hidream", "flux", "auraflow", "sd3", "hunyuan", "mochi", "ltxv", "cosmos", "wan2.1"}; - for (String kw : keywords) { - if (fullText.contains(kw)) hints.put(kw, hints.getOrDefault(kw, 0) + 5); + // Look for common architecture names as hints + String[] archHints = {"flux", "sdxl", "sd15", "sd3", "hunyuan", "mochi", "ltx", "cosmos", "wan", "hidream", "aura", "lumina"}; + for (String hint : archHints) { + if (fullText.contains(hint)) counts.put(hint, counts.getOrDefault(hint, 0) + 1); } - // 3. Scan Node-Typen - String nodesText = fullText; - if (nodesText.contains("hidream")) hints.put("hidream", hints.getOrDefault("hidream", 0) + 10); - - return hints.entrySet().stream() + return counts.entrySet().stream() .max(Map.Entry.comparingByValue()) .map(Map.Entry::getKey) .orElse(null); @@ -103,10 +95,10 @@ public class ComfyModelAnalyzer implements IModelAnalyzer { private void applyGlobalContext(List res, String ctx) { if (ctx == null) return; for (ModelInfo i : res) { - // Wenn der Dateiname den Kontext enthält oder der Dateiname sehr generisch ist (wie ae.safetensors) String n = i.getName().toLowerCase(); - if (n.contains(ctx) || n.equals("ae.safetensors") || n.contains("clip_l") || n.contains("t5xxl")) { - if (i.getPopularity() == null || i.getPopularity().isEmpty()) { + // If filename matches context or is a generic model file + if (n.contains(ctx) || n.startsWith("ae.") || n.contains("clip_") || n.contains("t5") || n.contains("diffusion")) { + if (i.getPopularity() == null || i.getPopularity().isEmpty() || i.getPopularity().contains("Community")) { i.setPopularity("✨ Context: " + ctx); } } @@ -133,7 +125,6 @@ public class ComfyModelAnalyzer implements IModelAnalyzer { if (m != null) { String n = m.optString("name", m.optString("filename")); if (n != null && !n.isEmpty()) { - // Strikte Übernahme des Pfades aus den Metadaten String rawPath = m.optString("directory", m.optString("type", "checkpoints")); ModelInfo info = new ModelInfo(rawPath, n, m.optString("url", "MISSING")); info.setSave_path(rawPath); @@ -153,6 +144,12 @@ public class ComfyModelAnalyzer implements IModelAnalyzer { if (obj instanceof JSONObject) { JSONObject jo = (JSONObject) obj; String node = jo.optString("class_type", jo.optString("type", null)); + + // Ignore information from markdown notes as requested + if (node != null && (node.equalsIgnoreCase("MarkdownNote") || node.equalsIgnoreCase("Note"))) { + return; + } + String curCtx = node != null ? (inferTypeFromNode(node) != null ? inferTypeFromNode(node) : ctx) : ctx; if (jo.has("widgets_values")) { JSONArray ws = jo.optJSONArray("widgets_values"); @@ -174,24 +171,19 @@ public class ComfyModelAnalyzer implements IModelAnalyzer { if (v == null || v.trim().isEmpty()) return; String vLow = v.toLowerCase().trim(); - // Ignoriere Standardwerte, die keine Dateinamen sind if (vLow.equals("none") || vLow.equals("default") || vLow.equals("undefined") || vLow.length() < 4) return; - // 1. Suche nach Markdown Links: [Name](URL) - Matcher mdMatcher = ExpertPatterns.MD_LINK.matcher(v); + Matcher mdMatcher = MD_LINK_PATTERN.matcher(v); while (mdMatcher.find()) { String name = mdMatcher.group(1); String url = fixUrl(mdMatcher.group(2)); - if (name.contains(".")) { // Nur wenn der Linktext wie ein Dateiname aussieht + if (name.contains(".")) { String type = hint != null ? inferTypeFromKey(hint) : null; - if ((type == null || type.equals("checkpoints")) && url.contains("/resolve/main/split_files/")) { - String sub = url.substring(url.indexOf("/split_files/") + 13); - if (sub.contains("/")) type = sub.substring(0, sub.indexOf("/")); - } else if ((type == null || type.equals("checkpoints")) && url.contains("/resolve/main/")) { + if ((type == null || type.equals("checkpoints")) && url.contains("/resolve/main/")) { String sub = url.substring(url.indexOf("/resolve/main/") + 14); if (sub.contains("/")) { String folder = sub.substring(0, sub.indexOf("/")).toLowerCase(); - if (folder.equals("loras") || folder.equals("vae") || folder.equals("checkpoints") || folder.equals("text_encoders") || folder.equals("diffusion_models") || folder.equals("controlnet")) { + if (folder.matches("(loras|vae|checkpoints|text_encoders|diffusion_models|controlnet|embeddings|upscale_models)")) { type = folder; } } @@ -201,8 +193,7 @@ public class ComfyModelAnalyzer implements IModelAnalyzer { } } - // 2. Suche nach nackten URLs - Matcher nakedMatcher = ExpertPatterns.NAKED_URL.matcher(v); + Matcher nakedMatcher = NAKED_URL_PATTERN.matcher(v); while (nakedMatcher.find()) { String url = fixUrl(nakedMatcher.group(1)); String name = url; @@ -216,33 +207,26 @@ public class ComfyModelAnalyzer implements IModelAnalyzer { } } - Matcher m = ExpertPatterns.FILE.matcher(v); + Matcher m = FILE_PATTERN.matcher(v); while (m.find()) { String match = m.group(1); - - // Ignoriere offensichtliche URLs (wir wollen nur Dateinamen) if (match.startsWith("http") || match.startsWith("//")) continue; - // Extrahiere nur den reinen Dateinamen, falls ein Pfad gematcht wurde String f = match; if (f.contains("/")) f = f.substring(f.lastIndexOf("/") + 1); if (f.contains("\\")) f = f.substring(f.lastIndexOf("\\") + 1); if (f.length() > 3) { - // Erneuter Check nach Pfad-Extraktion String fLow = f.toLowerCase(); if (fLow.equals("none.safetensors") || fLow.equals("default.safetensors")) continue; String type = hint != null ? inferTypeFromKey(hint) : null; if (type == null) type = ctx; - // Verfeinerung basierend auf Dateinamen (Höchste Priorität für Spezialmodelle) - if (fLow.contains("qwen-image-lightning")) type = "unet"; - else if (fLow.contains("vae_approx") || fLow.contains("vae-approx")) type = "vae_approx"; - else if (ExpertPatterns.COMP_VAE.matcher(fLow).find() && (type == null || type.equals("checkpoints"))) type = "vae"; - else if (fLow.contains("clip_vision")) type = "clip_vision"; - else if (ExpertPatterns.COMP_CLIP.matcher(fLow).find() && (type == null || type.equals("checkpoints"))) type = "clip"; - else if (ExpertPatterns.COMP_UNET.matcher(fLow).find() && (type == null || type.equals("checkpoints") || (type.equals("clip") && !fLow.contains("clip_l") && !fLow.contains("clip_g")))) type = "unet"; + // Generic refinement based on structural keywords + if (COMP_VAE_PATTERN.matcher(fLow).find() && (type == null || type.equals("checkpoints"))) type = "vae"; + else if (COMP_CLIP_PATTERN.matcher(fLow).find() && (type == null || type.equals("checkpoints"))) type = "clip"; + else if (COMP_UNET_PATTERN.matcher(fLow).find() && (type == null || type.equals("checkpoints") || (type.equals("clip") && !fLow.contains("clip_l") && !fLow.contains("clip_g")))) type = "unet"; addModelInfo(res, new ModelInfo(type != null ? type : "checkpoints", f, "MISSING")); } @@ -252,13 +236,12 @@ public class ComfyModelAnalyzer implements IModelAnalyzer { private String inferTypeFromNode(String nt) { nt = nt.toLowerCase(); if (nt.contains("lora")) return "loras"; - if (nt.contains("vae_approx") || nt.contains("vae-approx")) return "vae_approx"; if (nt.contains("vae")) return "vae"; if (nt.contains("controlnet") || nt.contains("t2i_adapter")) return "controlnet"; if (nt.contains("upscale") || nt.contains("esrgan")) return "upscale_models"; if (nt.contains("latent_upscale")) return "latent_upscale_models"; - if (nt.contains("clip_vision")) return "clip_vision"; - if (nt.contains("clip") || nt.contains("text_encoder") || nt.contains("cliploader") || nt.contains("llama") || nt.contains("mistral") || nt.contains("t5") || nt.contains("qwen")) return "clip"; + if (nt.contains("clip_vision") || nt.contains("clipvision")) return "clip_vision"; + if (nt.contains("clip") || nt.contains("text_encoder") || nt.contains("tokenizer")) return "clip"; if (nt.contains("diffusion_model")) return "diffusion_models"; if (nt.contains("unet") || nt.contains("diffusion")) return "unet"; if (nt.contains("gligen")) return "gligen"; @@ -276,12 +259,11 @@ public class ComfyModelAnalyzer implements IModelAnalyzer { private String inferTypeFromKey(String k) { k = k.toLowerCase(); if (k.contains("lora")) return "loras"; - if (k.contains("vae_approx") || k.contains("vae-approx")) return "vae_approx"; if (k.contains("vae")) return "vae"; - if (k.contains("clip_vision")) return "clip_vision"; - if (k.contains("clip") || k.contains("text_encoder") || k.contains("llama") || k.contains("mistral") || k.contains("t5") || k.contains("qwen")) return "clip"; + if (k.contains("clip_vision") || k.contains("clipvision")) return "clip_vision"; + if (k.contains("clip") || k.contains("text_encoder") || k.contains("tokenizer")) return "clip"; if (k.contains("diffusion_model")) return "diffusion_models"; - if (k.contains("unet") || k.contains("diffusion") || k.contains("hidream") || k.contains("aura") || k.contains("mochi") || k.contains("svd") || k.contains("z_image") || k.contains("lumina") || k.contains("flux") || k.contains("dit")) return "unet"; + if (k.contains("unet") || k.contains("diffusion")) return "unet"; if (k.contains("control") && k.contains("net")) return "controlnet"; if (k.contains("upscale") || k.contains("esrgan")) return "upscale_models"; if (k.contains("embedding")) return "embeddings"; @@ -297,18 +279,17 @@ public class ComfyModelAnalyzer implements IModelAnalyzer { private void addModelInfo(List res, ModelInfo info) { for (ModelInfo e : res) { if (e.getName().equalsIgnoreCase(info.getName())) { - if (!info.getUrl().equals("MISSING")) { - e.setUrl(info.getUrl()); + // If the existing entry is generic "checkpoints" but we found a more specific type + if ("checkpoints".equals(e.getType()) && !"checkpoints".equals(info.getType())) { e.setType(info.getType()); - e.setSave_path(info.getSave_path()); - } else if (e.getUrl().equals("MISSING")) { - boolean isEWeak = e.getType() == null || e.getType().equals("checkpoints") || e.getType().equals("clip"); - boolean isInfoStrong = info.getType() != null && (info.getType().equals("unet") || info.getType().equals("checkpoints")); - - if (isEWeak && isInfoStrong) { - e.setType(info.getType()); - e.setSave_path(info.getSave_path()); - } + if (info.getSave_path() != null) e.setSave_path(info.getSave_path()); + } + + // Always take URL if the current one is MISSING + if (e.getUrl().equals("MISSING") && !info.getUrl().equals("MISSING")) { + e.setUrl(info.getUrl()); + if (info.getSave_path() != null) e.setSave_path(info.getSave_path()); + if (info.getType() != null && !"checkpoints".equals(info.getType())) e.setType(info.getType()); } return; } diff --git a/src/main/java/de/tki/comfymodels/service/impl/ConfigService.java b/src/main/java/de/tki/comfymodels/service/impl/ConfigService.java index fd83701..5191631 100644 --- a/src/main/java/de/tki/comfymodels/service/impl/ConfigService.java +++ b/src/main/java/de/tki/comfymodels/service/impl/ConfigService.java @@ -1,6 +1,7 @@ package de.tki.comfymodels.service.impl; import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.File; @@ -9,11 +10,18 @@ import java.nio.file.Files; @Service public class ConfigService { - private static final String CONFIG_FILE = "app_settings.json"; - private static final String VAULT_FILE = "settings.vault"; + private final String CONFIG_FILE = "app_settings.json"; + private final String VAULT_FILE = "settings.vault"; private JSONObject settings = new JSONObject(); private String masterPassword = null; + private final EncryptionUtils encryptionUtils; + + @Autowired + public ConfigService(EncryptionUtils encryptionUtils) { + this.encryptionUtils = encryptionUtils; + } + /** * Use the current working directory for application data. */ @@ -32,7 +40,7 @@ public class ConfigService { if (vault.exists()) { String encrypted = Files.readString(vault.toPath(), StandardCharsets.UTF_8); try { - String decrypted = EncryptionUtils.decrypt(encrypted, password); + String decrypted = encryptionUtils.decrypt(encrypted, password); JSONObject decryptedJson = new JSONObject(decrypted); this.settings = decryptedJson; this.masterPassword = password; @@ -67,7 +75,7 @@ public class ConfigService { return; } try { - String encrypted = EncryptionUtils.encrypt(settings.toString(), masterPassword); + String encrypted = encryptionUtils.encrypt(settings.toString(), masterPassword); Files.writeString(getFileInAppData(VAULT_FILE).toPath(), encrypted, StandardCharsets.UTF_8); System.out.println("Vault saved successfully to: " + getFileInAppData(VAULT_FILE).getAbsolutePath()); } catch (Exception e) { @@ -91,7 +99,7 @@ public class ConfigService { public boolean isShutdownAfterDownloadEnabled() { return settings.optBoolean("shutdown_after_download", false); } public void setShutdownAfterDownloadEnabled(boolean enabled) { settings.put("shutdown_after_download", enabled); save(); } - public boolean isDarkMode() { return settings.optBoolean("dark_mode", true); } + public boolean isDarkMode() { return settings.optBoolean("dark_mode", false); } public void setDarkMode(boolean enabled) { settings.put("dark_mode", enabled); save(); } public void savePendingDownloads(String json) { @@ -117,4 +125,4 @@ public class ConfigService { public boolean isUnlocked() { return masterPassword != null; } -} \ No newline at end of file +} diff --git a/src/main/java/de/tki/comfymodels/service/impl/DefaultDownloadManager.java b/src/main/java/de/tki/comfymodels/service/impl/DefaultDownloadManager.java index 9d93d19..efdf690 100644 --- a/src/main/java/de/tki/comfymodels/service/impl/DefaultDownloadManager.java +++ b/src/main/java/de/tki/comfymodels/service/impl/DefaultDownloadManager.java @@ -261,7 +261,7 @@ public class DefaultDownloadManager implements IDownloadManager { } } } catch (Exception e) { - // Falls es ein Timing-Problem ist (Flagge wird gesetzt während Exception fliegt) + // In case of a timing issue (flag set while exception is thrown) try { Thread.sleep(50); } catch (InterruptedException ignored) {} if (isStopped || !isSelected(index) || Thread.currentThread().isInterrupted()) { diff --git a/src/main/java/de/tki/comfymodels/service/impl/EncryptionUtils.java b/src/main/java/de/tki/comfymodels/service/impl/EncryptionUtils.java index 9544fd3..1b8186c 100644 --- a/src/main/java/de/tki/comfymodels/service/impl/EncryptionUtils.java +++ b/src/main/java/de/tki/comfymodels/service/impl/EncryptionUtils.java @@ -1,5 +1,7 @@ package de.tki.comfymodels.service.impl; +import org.springframework.stereotype.Component; + import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; @@ -11,12 +13,13 @@ import java.security.SecureRandom; import java.security.spec.KeySpec; import java.util.Base64; +@Component public class EncryptionUtils { - private static final int ITERATIONS = 65536; - private static final int KEY_LENGTH = 256; - private static final byte[] SALT = "ComfyUI-Vault-Salt-2026".getBytes(StandardCharsets.UTF_8); + private final int ITERATIONS = 65536; + private final int KEY_LENGTH = 256; + private final byte[] SALT = "ComfyUI-Vault-Salt-2026".getBytes(StandardCharsets.UTF_8); - public static String encrypt(String strToEncrypt, String password) throws Exception { + public String encrypt(String strToEncrypt, String password) throws Exception { byte[] iv = new byte[16]; new SecureRandom().nextBytes(iv); IvParameterSpec ivspec = new IvParameterSpec(iv); @@ -37,7 +40,7 @@ public class EncryptionUtils { return Base64.getEncoder().encodeToString(combined); } - public static String decrypt(String strToDecrypt, String password) throws Exception { + public String decrypt(String strToDecrypt, String password) throws Exception { if (strToDecrypt == null || strToDecrypt.trim().isEmpty()) { throw new Exception("Encrypted data is empty"); } @@ -62,4 +65,4 @@ public class EncryptionUtils { cipher.init(Cipher.DECRYPT_MODE, secretKey, ivspec); return new String(cipher.doFinal(encrypted), StandardCharsets.UTF_8); } -} \ No newline at end of file +} diff --git a/src/main/java/de/tki/comfymodels/service/impl/GeminiAIService.java b/src/main/java/de/tki/comfymodels/service/impl/GeminiAIService.java index 8586d5f..0cd58ad 100644 --- a/src/main/java/de/tki/comfymodels/service/impl/GeminiAIService.java +++ b/src/main/java/de/tki/comfymodels/service/impl/GeminiAIService.java @@ -65,14 +65,14 @@ public class GeminiAIService { String context = metadataContext != null ? metadataContext : "No context"; String shortContext = context.length() > 25000 ? context.substring(0, 25000) : context; - String prompt = "Websuche: Ermittle das offizielle Hugging Face Repository für die Datei '" + modelName + "'.\n\n" + - "KONTEXT:\n" + - "Workflow-Datei: " + fileName + "\n" + - "Workflow-Daten: " + shortContext + "\n\n" + - "ANWEISUNG:\n" + - "1. Identifiziere das exakte Hugging Face Repository (z.B. black-forest-labs/FLUX.1-schnell).\n" + - "2. Antworte NUR mit der Repository ID (Format: creator/repo) oder 'UNKNOWN'.\n" + - "3. Wenn du einen direkten Download-Link findest, gib diesen stattdessen aus."; + String prompt = "Web search: Determine the official Hugging Face repository for the file '" + modelName + "'.\n\n" + + "CONTEXT:\n" + + "Workflow file: " + fileName + "\n" + + "Workflow data: " + shortContext + "\n\n" + + "INSTRUCTION:\n" + + "1. Identify the exact Hugging Face repository (e.g., black-forest-labs/FLUX.1-schnell).\n" + + "2. Respond ONLY with the repository ID (format: creator/repo) or 'UNKNOWN'.\n" + + "3. If you find a direct download link, output it instead."; JSONObject payload = new JSONObject(); JSONArray contents = new JSONArray(); diff --git a/src/main/java/de/tki/comfymodels/service/impl/LocalAIService.java b/src/main/java/de/tki/comfymodels/service/impl/LocalAIService.java index ccb4770..a323327 100644 --- a/src/main/java/de/tki/comfymodels/service/impl/LocalAIService.java +++ b/src/main/java/de/tki/comfymodels/service/impl/LocalAIService.java @@ -1,15 +1,17 @@ package de.tki.comfymodels.service.impl; import org.springframework.stereotype.Service; +import jakarta.annotation.PostConstruct; import java.util.*; @Service public class LocalAIService { - private static final Map KNOWLEDGE_BASE = new LinkedHashMap<>(); - private static final Map GLOBAL_IDF = new HashMap<>(); + private final Map KNOWLEDGE_BASE = new LinkedHashMap<>(); + private final Map GLOBAL_IDF = new HashMap<>(); - static { + @PostConstruct + public void init() { // --- TOP RESEARCH LABS --- add("black-forest-labs", "flux, flux1, flux2, bfl, schnell, dev, pro, mistral"); add("stabilityai", "sdxl, sd15, sd21, sd3, svd, cascade, turbo, lightning, stable, diffusion"); @@ -32,11 +34,11 @@ public class LocalAIService { calculateIdf(); } - private static void add(String provider, String keywords) { + private void add(String provider, String keywords) { KNOWLEDGE_BASE.put(provider, keywords.split(", ")); } - private static void calculateIdf() { + private void calculateIdf() { Set allTerms = new HashSet<>(); for (String[] terms : KNOWLEDGE_BASE.values()) allTerms.addAll(Arrays.asList(terms)); int docCount = KNOWLEDGE_BASE.size(); @@ -48,7 +50,7 @@ public class LocalAIService { public Prediction predictProvider(String fileName) { String name = fileName.toLowerCase(); - // Spezial-Check für komplexe Dateinamen + // Special check for complex filenames if (name.contains("mistral") && name.contains("flux2")) return new Prediction("black-forest-labs", 0.95); if (name.contains("pony") || name.contains("v6")) return new Prediction("PonyDiffusion", 0.85); @@ -78,7 +80,7 @@ public class LocalAIService { return new Prediction(bestProvider, Math.min(0.99, maxScore / 3.5)); } - public static class Prediction { + public class Prediction { public final String provider; public final double confidence; public Prediction(String provider, double confidence) { this.provider = provider; this.confidence = confidence; } diff --git a/src/main/java/de/tki/comfymodels/service/impl/ModelSearchService.java b/src/main/java/de/tki/comfymodels/service/impl/ModelSearchService.java index 10a3787..c008926 100644 --- a/src/main/java/de/tki/comfymodels/service/impl/ModelSearchService.java +++ b/src/main/java/de/tki/comfymodels/service/impl/ModelSearchService.java @@ -221,8 +221,8 @@ public class ModelSearchService implements IModelSearchService { public String formatSize(long bytes) { if (bytes == -401) return "🔒 Auth Required"; if (bytes <= 0) return "Unknown"; - if (bytes > 1024L * 1024L * 1024L) return String.format("%.2f GB", bytes / 1024.0 / 1024.0 / 1024.0); - return String.format("%.2f MB", bytes / 1024.0 / 1024.0); + if (bytes >= 1024L * 1024L * 1024L) return String.format(Locale.US, "%.2f GB", bytes / 1024.0 / 1024.0 / 1024.0); + return String.format(Locale.US, "%.2f MB", bytes / 1024.0 / 1024.0); } private boolean fetchHuggingFaceUrlInSpecificRepo(ModelInfo info, int rowIndex, String repoId, diff --git a/src/test/java/de/tki/comfymodels/BatchVerificationTest.java b/src/test/java/de/tki/comfymodels/BatchVerificationTest.java new file mode 100644 index 0000000..f827642 --- /dev/null +++ b/src/test/java/de/tki/comfymodels/BatchVerificationTest.java @@ -0,0 +1,67 @@ +package de.tki.comfymodels; + +import de.tki.comfymodels.domain.ModelInfo; +import de.tki.comfymodels.service.impl.ComfyModelAnalyzer; +import de.tki.comfymodels.service.impl.WorkflowService; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +/** + * Ported from BatchVerification.java main method. + * This test is disabled by default as it requires local files. + */ +public class BatchVerificationTest { + + @Test + @Disabled("Requires local ComfyUI workflow files") + public void testBatchVerification() throws Exception { + ComfyModelAnalyzer analyzer = new ComfyModelAnalyzer(); + WorkflowService workflowService = new WorkflowService(); + + // Path can be adjusted as needed + Path root = Paths.get("C:/pinokio/api/comfy.git/workflows/ComfyUI_examples"); + + if (!Files.exists(root)) { + System.out.println("Root path does not exist, skipping batch verification: " + root); + return; + } + + System.out.println("=== BATCH VERIFICATION START ==="); + + Files.walk(root) + .filter(p -> p.toString().endsWith(".json") || p.toString().endsWith(".png")) + .limit(30) + .forEach(path -> { + try { + System.out.println("\nFILE: " + path.getFileName()); + String content = workflowService.extractWorkflow(path.toFile()); + + if (content == null) { + System.out.println(" [!] No workflow found"); + return; + } + + List results = analyzer.analyze(content, path.getFileName().toString()); + if (results.isEmpty()) { + System.out.println(" [?] No models detected"); + } else { + for (ModelInfo info : results) { + System.out.printf(" [%s] %s | URL: %s | Source: %s\n", + info.getType(), info.getName(), + info.getUrl().equals("MISSING") ? "Missing" : "FOUND", + info.getPopularity()); + } + } + } catch (Exception e) { + System.out.println(" [ERROR] " + e.getMessage()); + } + }); + + System.out.println("\n=== BATCH VERIFICATION END ==="); + } +} diff --git a/src/test/java/de/tki/comfymodels/DownloadStatusTest.java b/src/test/java/de/tki/comfymodels/DownloadStatusTest.java index 4e53638..02f25aa 100644 --- a/src/test/java/de/tki/comfymodels/DownloadStatusTest.java +++ b/src/test/java/de/tki/comfymodels/DownloadStatusTest.java @@ -4,6 +4,7 @@ import com.sun.net.httpserver.HttpServer; import de.tki.comfymodels.domain.ModelInfo; import de.tki.comfymodels.service.impl.ConfigService; import de.tki.comfymodels.service.impl.DefaultDownloadManager; +import de.tki.comfymodels.service.impl.EncryptionUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -15,7 +16,6 @@ import java.lang.reflect.Field; import java.net.InetSocketAddress; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -36,8 +36,9 @@ public class DownloadStatusTest { tempDir = Files.createTempDirectory("downloader_test"); downloadManager = new DefaultDownloadManager(); - // Use real ConfigService - ConfigService configService = new ConfigService(); + // Use real ConfigService and EncryptionUtils + EncryptionUtils encryptionUtils = new EncryptionUtils(); + ConfigService configService = new ConfigService(encryptionUtils); Field field = DefaultDownloadManager.class.getDeclaredField("configService"); field.setAccessible(true); @@ -50,7 +51,7 @@ public class DownloadStatusTest { exchange.getResponseHeaders().add("Content-Length", String.valueOf(data.length)); exchange.sendResponseHeaders(200, data.length); try (OutputStream os = exchange.getResponseBody()) { - // Sende Daten langsam, damit wir Pause/Stop testen können + // Send data slowly so we can test pause/stop for (int i = 0; i < 10; i++) { os.write(data, i * (data.length / 10), data.length / 10); os.flush(); @@ -86,7 +87,7 @@ public class DownloadStatusTest { finishedLatch::countDown ); - // 1. Prüfe auf Start/Download + // 1. Check for Start/Download Thread.sleep(300); assertTrue(capturedStatus.stream().anyMatch(s -> s.contains("Downloading")), "Should show Downloading status"); diff --git a/src/test/java/de/tki/comfymodels/HiDreamReproductionTest.java b/src/test/java/de/tki/comfymodels/HiDreamReproductionTest.java index 8594d0e..2fcbfbc 100644 --- a/src/test/java/de/tki/comfymodels/HiDreamReproductionTest.java +++ b/src/test/java/de/tki/comfymodels/HiDreamReproductionTest.java @@ -27,8 +27,8 @@ public class HiDreamReproductionTest { ComfyModelAnalyzer analyzer = new ComfyModelAnalyzer(); List models = analyzer.analyze(json, "workflow.json"); - // Wir erwarten 7 Modelle: 1 UNET, 4 CLIPs, 1 VAE (aus Loadern) UND 1 UNET (aus der MarkdownNote) - assertEquals(7, models.size(), "Should detect 7 models (including one from MarkdownNote)"); + // We expect 6 models: 1 UNET, 4 CLIPs, 1 VAE (from loaders). The UNET in MarkdownNote is now ignored. + assertEquals(6, models.size(), "Should detect 6 models (ignoring one from MarkdownNote)"); boolean foundUnet = false; int clipsFound = 0; @@ -51,7 +51,7 @@ public class HiDreamReproductionTest { assertTrue(foundVae, "VAE not found"); assertEquals(4, clipsFound, "Should find 4 CLIP/TextEncoder models"); - // Überprüfe ob der Kontext "hidream" erkannt wurde + // Check if context "hidream" was detected boolean contextDetected = false; for (ModelInfo m : models) { System.out.println("Model: " + m.getName() + " | Popularity: " + m.getPopularity()); diff --git a/src/test/java/de/tki/comfymodels/QualityAssuranceTest.java b/src/test/java/de/tki/comfymodels/QualityAssuranceTest.java index 9fbad1f..ca0f6f9 100644 --- a/src/test/java/de/tki/comfymodels/QualityAssuranceTest.java +++ b/src/test/java/de/tki/comfymodels/QualityAssuranceTest.java @@ -13,7 +13,7 @@ public class QualityAssuranceTest { public void testFalsePositiveFiltering() { ComfyModelAnalyzer analyzer = new ComfyModelAnalyzer(); - // Workflow mit "None" und "default" Werten in Widgets + // Workflow with "None" and "default" values in widgets String json = "{" + "\"nodes\": [" + " {\"id\": 1, \"type\": \"CheckpointLoaderSimple\", \"widgets_values\": [\"None\", \"default\"]}," + @@ -23,7 +23,7 @@ public class QualityAssuranceTest { List models = analyzer.analyze(json, "test.json"); - // Es sollte nur "test.safetensors" gefunden werden + // Only "test.safetensors" should be found assertEquals(1, models.size(), "Should only find one real model"); assertEquals("test.safetensors", models.get(0).getName()); } @@ -32,12 +32,12 @@ public class QualityAssuranceTest { public void testMalformedJsonHandling() { ComfyModelAnalyzer analyzer = new ComfyModelAnalyzer(); - // Kaputtes JSON - String json = "{ \"nodes\": [ { \"id\": 1, \"type\": \"VAELoader\", \"widgets_values\": [\"ae.safetensors\""; // Fehlende Klammern + // Broken JSON + String json = "{ \"nodes\": [ { \"id\": 1, \"type\": \"VAELoader\", \"widgets_values\": [\"ae.safetensors\""; // Missing brackets List models = analyzer.analyze(json, "broken.json"); - // Sollte nicht abstürzen, sondern einfach 0 oder gefundene Modelle (je nach Parser-Robustheit) zurückgeben + // Should not crash, but simply return 0 or found models (depending on parser robustness) assertNotNull(models); } } diff --git a/src/test/java/de/tki/comfymodels/ReproductionTest.java b/src/test/java/de/tki/comfymodels/ReproductionTest.java index d28890c..13f6824 100644 --- a/src/test/java/de/tki/comfymodels/ReproductionTest.java +++ b/src/test/java/de/tki/comfymodels/ReproductionTest.java @@ -37,8 +37,8 @@ public class ReproductionTest { List models = analyzer.analyze(json, "test.json"); - // Erwartung: Nur EIN Eintrag für qwen_3_4b.safetensors - // Der Name darf NICHT die URL sein. + // Expectation: Only ONE entry for qwen_3_4b.safetensors + // The name must NOT be the URL. long count = models.stream() .filter(m -> m.getName().toLowerCase().contains("qwen")) .count(); diff --git a/src/test/java/de/tki/comfymodels/service/CosmosRegressionTest.java b/src/test/java/de/tki/comfymodels/service/CosmosRegressionTest.java new file mode 100644 index 0000000..6c37dfc --- /dev/null +++ b/src/test/java/de/tki/comfymodels/service/CosmosRegressionTest.java @@ -0,0 +1,80 @@ +package de.tki.comfymodels.service; + +import de.tki.comfymodels.domain.ModelInfo; +import de.tki.comfymodels.service.impl.ComfyModelAnalyzer; +import de.tki.comfymodels.service.impl.LocalAIService; +import de.tki.comfymodels.service.impl.ModelListService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class CosmosRegressionTest { + + private ComfyModelAnalyzer analyzer; + + @BeforeEach + public void setup() { + analyzer = new ComfyModelAnalyzer(); + LocalAIService aiService = new LocalAIService(); + aiService.init(); + + ReflectionTestUtils.setField(analyzer, "aiService", aiService); + ReflectionTestUtils.setField(analyzer, "modelListService", new ModelListService()); + } + + @Test + public void testFullCosmosWorkflow() { + String json = "{\n" + + " \"last_node_id\": 84,\n" + + " \"last_link_id\": 198,\n" + + " \"nodes\": [\n" + + " {\n" + + " \"id\": 38,\n" + + " \"type\": \"CLIPLoader\",\n" + + " \"widgets_values\": [\n" + + " \"oldt5_xxl_fp8_e4m3fn_scaled.safetensors\",\n" + + " \"cosmos\",\n" + + " \"default\"\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"id\": 39,\n" + + " \"type\": \"VAELoader\",\n" + + " \"widgets_values\": [\n" + + " \"cosmos_cv8x8x8_1.0.safetensors\"\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"id\": 37,\n" + + " \"type\": \"UNETLoader\",\n" + + " \"widgets_values\": [\n" + + " \"Cosmos-1_0-Diffusion-7B-Video2World.safetensors\",\n" + + " \"default\"\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"id\": 83,\n" + + " \"type\": \"CosmosImageToVideoLatent\",\n" + + " \"widgets_values\": [ 1024, 1024, 121, 1 ]\n" + + " }\n" + + " ]\n" + + "}"; + + List results = analyzer.analyze(json, "full_cosmos.json"); + + // Expectations + boolean foundT5 = results.stream().anyMatch(m -> m.getName().equals("oldt5_xxl_fp8_e4m3fn_scaled.safetensors")); + boolean foundDiffusion = results.stream().anyMatch(m -> m.getName().equals("Cosmos-1_0-Diffusion-7B-Video2World.safetensors")); + boolean foundVae = results.stream().anyMatch(m -> m.getName().equals("cosmos_cv8x8x8_1.0.safetensors")); + + assertTrue(foundT5, "T5 XXL not found"); + assertTrue(foundDiffusion, "Cosmos Diffusion not found"); + assertTrue(foundVae, "Cosmos VAE not found"); + + assertEquals(3, results.size()); + } +} diff --git a/src/test/java/de/tki/comfymodels/service/DownloadSelectionTest.java b/src/test/java/de/tki/comfymodels/service/DownloadSelectionTest.java index b28d6c8..76992cc 100644 --- a/src/test/java/de/tki/comfymodels/service/DownloadSelectionTest.java +++ b/src/test/java/de/tki/comfymodels/service/DownloadSelectionTest.java @@ -3,29 +3,29 @@ package de.tki.comfymodels.service; import de.tki.comfymodels.domain.ModelInfo; import de.tki.comfymodels.service.impl.ConfigService; import de.tki.comfymodels.service.impl.DefaultDownloadManager; +import de.tki.comfymodels.service.impl.EncryptionUtils; import org.junit.jupiter.api.Test; import org.springframework.test.util.ReflectionTestUtils; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class DownloadSelectionTest { + private final EncryptionUtils encryptionUtils = new EncryptionUtils(); + @Test public void testOnlySelectedItemsAreProcessed() throws InterruptedException { DefaultDownloadManager downloadManager = new DefaultDownloadManager(); // Mocking ConfigService to avoid NPE - ConfigService configService = new ConfigService(); + ConfigService configService = new ConfigService(encryptionUtils); ReflectionTestUtils.setField(downloadManager, "configService", configService); List models = new ArrayList<>(); @@ -61,7 +61,7 @@ public class DownloadSelectionTest { @Test public void testDynamicUncheckWhilePaused() throws InterruptedException { DefaultDownloadManager downloadManager = new DefaultDownloadManager(); - ConfigService configService = new ConfigService(); + ConfigService configService = new ConfigService(encryptionUtils); ReflectionTestUtils.setField(downloadManager, "configService", configService); List models = new ArrayList<>(); diff --git a/src/test/java/de/tki/comfymodels/service/ExistingModelSizeTest.java b/src/test/java/de/tki/comfymodels/service/ExistingModelSizeTest.java new file mode 100644 index 0000000..2a7f929 --- /dev/null +++ b/src/test/java/de/tki/comfymodels/service/ExistingModelSizeTest.java @@ -0,0 +1,33 @@ +package de.tki.comfymodels.service; + +import de.tki.comfymodels.domain.ModelInfo; +import de.tki.comfymodels.service.impl.ModelSearchService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ExistingModelSizeTest { + + @TempDir + Path tempDir; + + @Test + public void testLocalSizeFormatting() throws IOException { + ModelSearchService searchService = new ModelSearchService(); + + File dummyFile = tempDir.resolve("test.safetensors").toFile(); + byte[] content = new byte[1024 * 1024 * 5]; // 5MB + Files.write(dummyFile.toPath(), content); + + long size = dummyFile.length(); + String formatted = searchService.formatSize(size); + + assertEquals("5.00 MB", formatted); + } +} diff --git a/src/test/java/de/tki/comfymodels/service/FluxSchnellTest.java b/src/test/java/de/tki/comfymodels/service/FluxSchnellTest.java new file mode 100644 index 0000000..aa89ecd --- /dev/null +++ b/src/test/java/de/tki/comfymodels/service/FluxSchnellTest.java @@ -0,0 +1,78 @@ +package de.tki.comfymodels.service; + +import de.tki.comfymodels.domain.ModelInfo; +import de.tki.comfymodels.service.impl.ComfyModelAnalyzer; +import de.tki.comfymodels.service.impl.LocalAIService; +import de.tki.comfymodels.service.impl.ModelListService; +import de.tki.comfymodels.service.impl.ModelSearchService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class FluxSchnellTest { + + private ComfyModelAnalyzer analyzer; + private ModelSearchService searchService; + + @BeforeEach + public void setup() { + analyzer = new ComfyModelAnalyzer(); + LocalAIService aiService = new LocalAIService(); + aiService.init(); + + ReflectionTestUtils.setField(analyzer, "aiService", aiService); + ReflectionTestUtils.setField(analyzer, "modelListService", new ModelListService()); + + searchService = new ModelSearchService(); + } + + @Test + public void testFluxSchnellExtractionAndSizeValidation() { + String json = "{\n" + + " \"nodes\": [\n" + + " {\n" + + " \"id\": 30,\n" + + " \"type\": \"CheckpointLoaderSimple\",\n" + + " \"properties\": {\n" + + " \"models\": [\n" + + " {\n" + + " \"name\": \"flux1-schnell-fp8.safetensors\",\n" + + " \"url\": \"https://huggingface.co/Comfy-Org/flux1-schnell/resolve/main/flux1-schnell-fp8.safetensors?download=true\",\n" + + " \"directory\": \"checkpoints\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"widgets_values\": [ \"flux1-schnell-fp8.safetensors\" ]\n" + + " }\n" + + " ]\n" + + "}"; + + List results = analyzer.analyze(json, "flux_schnell.json"); + + assertFalse(results.isEmpty(), "Should extract at least one model"); + ModelInfo flux = results.get(0); + + assertEquals("flux1-schnell-fp8.safetensors", flux.getName()); + assertEquals("checkpoints", flux.getType()); + assertNotEquals("MISSING", flux.getUrl()); + + // Size validation logic test + // 1. Valid size + assertEquals("1.00 GB", searchService.formatSize(1024L * 1024L * 1024L)); + + // 2. Zero size check (Crucial: Should NOT return "0.00 MB") + String zeroResult = searchService.formatSize(0); + assertNotEquals("0.00 MB", zeroResult, "Size 0 should be handled as Unknown or Error, not 0.00 MB"); + assertEquals("Unknown", zeroResult); + + // 3. Negative size (Error) + assertEquals("Unknown", searchService.formatSize(-1)); + + // 4. Auth required + assertEquals("🔒 Auth Required", searchService.formatSize(-401)); + } +} diff --git a/src/test/java/de/tki/comfymodels/service/HiDreamComprehensiveTest.java b/src/test/java/de/tki/comfymodels/service/HiDreamComprehensiveTest.java new file mode 100644 index 0000000..d66faf8 --- /dev/null +++ b/src/test/java/de/tki/comfymodels/service/HiDreamComprehensiveTest.java @@ -0,0 +1,83 @@ +package de.tki.comfymodels.service; + +import de.tki.comfymodels.domain.ModelInfo; +import de.tki.comfymodels.service.impl.ComfyModelAnalyzer; +import de.tki.comfymodels.service.impl.LocalAIService; +import de.tki.comfymodels.service.impl.ModelListService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +public class HiDreamComprehensiveTest { + + private ComfyModelAnalyzer analyzer; + + @BeforeEach + public void setup() { + analyzer = new ComfyModelAnalyzer(); + LocalAIService aiService = new LocalAIService(); + aiService.init(); // Initialize the AI service + + ReflectionTestUtils.setField(analyzer, "aiService", aiService); + ReflectionTestUtils.setField(analyzer, "modelListService", new ModelListService()); + } + + @Test + public void testHiDreamWorkflowExtraction() { + String json = "{\n" + + " \"id\": \"01d66ae9-78be-4a8d-b737-24eee5e1d447\",\n" + + " \"nodes\": [\n" + + " {\n" + + " \"id\": 55,\n" + + " \"type\": \"VAELoader\",\n" + + " \"properties\": {\n" + + " \"models\": [\n" + + " {\n" + + " \"name\": \"ae.safetensors\",\n" + + " \"url\": \"https://huggingface.co/Comfy-Org/HiDream-I1_ComfyUI/resolve/main/split_files/vae/ae.safetensors\",\n" + + " \"directory\": \"vae\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"widgets_values\": [ \"ae.safetensors\" ]\n" + + " },\n" + + " {\n" + + " \"id\": 54,\n" + + " \"type\": \"QuadrupleCLIPLoader\",\n" + + " \"properties\": {\n" + + " \"models\": [\n" + + " { \"name\": \"clip_l_hidream.safetensors\", \"url\": \"https://huggingface.co/Comfy-Org/HiDream-I1_ComfyUI/resolve/main/split_files/text_encoders/clip_l_hidream.safetensors\", \"directory\": \"text_encoders\" }\n" + + " ]\n" + + " },\n" + + " \"widgets_values\": [ \"clip_l_hidream.safetensors\" ]\n" + + " },\n" + + " {\n" + + " \"id\": 72,\n" + + " \"type\": \"MarkdownNote\",\n" + + " \"widgets_values\": [\n" + + " \"* hidream_i1_full_fp16.safetensors\"\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + + List results = analyzer.analyze(json, "hidream_test.json"); + + assertFalse(results.isEmpty()); + + // Check for extraction from properties.models + assertTrue(results.stream().anyMatch(m -> m.getName().equals("ae.safetensors"))); + + // Check for extraction from text in MarkdownNote - SHOULD BE IGNORED NOW + assertTrue(results.stream().noneMatch(m -> m.getName().equals("hidream_i1_full_fp16.safetensors")), "Model from MarkdownNote should be ignored"); + + for (ModelInfo info : results) { + System.out.println("Extracted: " + info.getName() + " | Type: " + info.getType() + " | URL: " + info.getUrl()); + } + } +} diff --git a/src/test/java/de/tki/comfymodels/service/PngExtractionTest.java b/src/test/java/de/tki/comfymodels/service/PngExtractionTest.java new file mode 100644 index 0000000..70a6892 --- /dev/null +++ b/src/test/java/de/tki/comfymodels/service/PngExtractionTest.java @@ -0,0 +1,84 @@ +package de.tki.comfymodels.service; + +import de.tki.comfymodels.service.impl.WorkflowService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.zip.CRC32; +import java.util.zip.Deflater; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class PngExtractionTest { + + @TempDir + Path tempDir; + + @Test + public void testWorkflowExtractionFromSyntheticPng() throws IOException { + File pngFile = tempDir.resolve("test.png").toFile(); + String expectedWorkflow = "{\"nodes\": [{\"type\": \"CheckpointLoaderSimple\"}]}"; + + createTestPng(pngFile, expectedWorkflow); + + WorkflowService workflowService = new WorkflowService(); + String extracted = workflowService.extractWorkflow(pngFile); + + assertNotNull(extracted, "Workflow should be extracted from PNG"); + assertEquals(expectedWorkflow, extracted, "Extracted workflow should match the original"); + } + + private void createTestPng(File file, String workflow) throws IOException { + try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(file))) { + // PNG Signature + dos.writeLong(0x89504E470D0A1A0AL); + + // IHDR chunk (minimal) + dos.writeInt(13); + byte[] ihdrType = "IHDR".getBytes(StandardCharsets.US_ASCII); + dos.write(ihdrType); + dos.writeInt(1); // Width + dos.writeInt(1); // Height + dos.writeByte(8); // Bit depth + dos.writeByte(2); // Color type + dos.writeByte(0); // Compression + dos.writeByte(0); // Filter + dos.writeByte(0); // Interlace + dos.writeInt(calculateCrc(ihdrType, new byte[]{0,0,0,1, 0,0,0,1, 8, 2, 0, 0, 0})); + + // tEXt chunk with workflow + byte[] key = "workflow".getBytes(StandardCharsets.UTF_8); + byte[] value = workflow.getBytes(StandardCharsets.UTF_8); + byte[] textData = new byte[key.length + 1 + value.length]; + System.arraycopy(key, 0, textData, 0, key.length); + textData[key.length] = 0; + System.arraycopy(value, 0, textData, key.length + 1, value.length); + + dos.writeInt(textData.length); + byte[] textType = "tEXt".getBytes(StandardCharsets.US_ASCII); + dos.write(textType); + dos.write(textData); + dos.writeInt(calculateCrc(textType, textData)); + + // IEND chunk + dos.writeInt(0); + byte[] iendType = "IEND".getBytes(StandardCharsets.US_ASCII); + dos.write(iendType); + dos.writeInt(calculateCrc(iendType, new byte[0])); + } + } + + private int calculateCrc(byte[] type, byte[] data) { + CRC32 crc = new CRC32(); + crc.update(type); + crc.update(data); + return (int) crc.getValue(); + } +} diff --git a/src/test/java/de/tki/comfymodels/service/QwenReproductionTest.java b/src/test/java/de/tki/comfymodels/service/QwenReproductionTest.java index 07dbf6d..5e73150 100644 --- a/src/test/java/de/tki/comfymodels/service/QwenReproductionTest.java +++ b/src/test/java/de/tki/comfymodels/service/QwenReproductionTest.java @@ -11,7 +11,7 @@ public class QwenReproductionTest { @Test public void testQwenMarkdownModelDetection() { ComfyModelAnalyzer analyzer = new ComfyModelAnalyzer(); - // Minimaler Workflow mit der MarkdownNote des Nutzers + // Minimal workflow with the user's MarkdownNote String workflow = "{\n" + " \"nodes\": [\n" + " {\n" + @@ -26,12 +26,8 @@ public class QwenReproductionTest { List models = analyzer.analyze(workflow, "test.json"); - // Wir prüfen, ob beide Namen gefunden werden - boolean foundLightning = models.stream().anyMatch(m -> m.getName().equals("Qwen-Image-Lightning-4steps-V1.0.safetensors")); - boolean foundEdit = models.stream().anyMatch(m -> m.getName().equals("Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16.safetensors")); - - assertTrue(foundEdit, "Modell aus dem Link sollte gefunden werden"); - assertTrue(foundLightning, "Modell aus dem Text-Diagramm sollte gefunden werden"); + // We expect NO models because MarkdownNote is ignored + assertTrue(models.isEmpty(), "No models should be found from MarkdownNote"); } @Test @@ -50,12 +46,6 @@ public class QwenReproductionTest { List models = analyzer.analyze(workflow, "test.json"); - ModelInfo editModel = models.stream() - .filter(m -> m.getName().equals("Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16.safetensors")) - .findFirst().orElse(null); - - assertNotNull(editModel, "Modell sollte gefunden werden"); - assertEquals("https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Edit-2509/Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16.safetensors", - editModel.getUrl(), "URL sollte aus dem Markdown-Link extrahiert werden"); + assertTrue(models.isEmpty(), "No models should be found from MarkdownNote, even with links"); } } diff --git a/src/test/java/de/tki/comfymodels/service/StatusTransitionTest.java b/src/test/java/de/tki/comfymodels/service/StatusTransitionTest.java index 13c7b58..d6203c7 100644 --- a/src/test/java/de/tki/comfymodels/service/StatusTransitionTest.java +++ b/src/test/java/de/tki/comfymodels/service/StatusTransitionTest.java @@ -30,12 +30,13 @@ public class StatusTransitionTest { private String serverUrl; private Path tempDir; private ConfigService configService; + private final EncryptionUtils encryptionUtils = new EncryptionUtils(); @BeforeEach public void setup() throws Exception { tempDir = Files.createTempDirectory("transition_test"); downloadManager = new DefaultDownloadManager(); - configService = new ConfigService(); + configService = new ConfigService(encryptionUtils); ReflectionTestUtils.setField(downloadManager, "configService", configService); // Setup Local Test Server @@ -93,7 +94,7 @@ public class StatusTransitionTest { @Test public void testTransition_Idle_To_Searching_To_NotFound() throws InterruptedException { ModelSearchService searchService = new ModelSearchService(); - ConfigService cfg = new ConfigService(); + ConfigService cfg = new ConfigService(encryptionUtils); GeminiAIService gemini = new GeminiAIService(); ModelListService list = new ModelListService(); diff --git a/src/test/java/de/tki/comfymodels/service/VaultIntegrationTest.java b/src/test/java/de/tki/comfymodels/service/VaultIntegrationTest.java index 7f17422..2882fe6 100644 --- a/src/test/java/de/tki/comfymodels/service/VaultIntegrationTest.java +++ b/src/test/java/de/tki/comfymodels/service/VaultIntegrationTest.java @@ -1,6 +1,7 @@ package de.tki.comfymodels.service; import de.tki.comfymodels.service.impl.ConfigService; +import de.tki.comfymodels.service.impl.EncryptionUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.io.File; @@ -13,10 +14,12 @@ public class VaultIntegrationTest { @TempDir Path tempDir; + private final EncryptionUtils encryptionUtils = new EncryptionUtils(); + @Test public void testVaultSaveAndLoad() throws Exception { // Setup ConfigService with temp directory - ConfigService configService = new ConfigService() { + ConfigService configService = new ConfigService(encryptionUtils) { @Override public String getAppDataPath() { return tempDir.toString(); @@ -34,7 +37,7 @@ public class VaultIntegrationTest { configService.save(); // 3. Create NEW instance to simulate restart - ConfigService newConfigService = new ConfigService() { + ConfigService newConfigService = new ConfigService(encryptionUtils) { @Override public String getAppDataPath() { return tempDir.toString(); @@ -44,11 +47,24 @@ public class VaultIntegrationTest { // 4. Unlock with correct password newConfigService.unlock(password); - // 5. Verify key + // 5. Verify key and dark mode assertEquals(testKey, newConfigService.getGeminiApiKey(), "Key should be 4711 after loading"); + assertFalse(newConfigService.isDarkMode(), "Default should be false"); + + newConfigService.setDarkMode(true); + + // 5a. Reload again to verify dark mode persistence + ConfigService restartService = new ConfigService(encryptionUtils) { + @Override + public String getAppDataPath() { + return tempDir.toString(); + } + }; + restartService.unlock(password); + assertTrue(restartService.isDarkMode(), "Dark mode should be true after reload"); // 6. Verify wrong password fails - ConfigService wrongPassService = new ConfigService() { + ConfigService wrongPassService = new ConfigService(encryptionUtils) { @Override public String getAppDataPath() { return tempDir.toString(); @@ -59,4 +75,4 @@ public class VaultIntegrationTest { wrongPassService.unlock("wrong_password"); }, "Unlock should fail with wrong password"); } -} \ No newline at end of file +}