feat: enhance platform independence and integrate ComfyUI lifecycle management
- Added cross-platform process termination in PlatformUtils. - Improved PathResolver with robust subfolder normalization. - Integrated ComfyUI lifecycle service (Start/Stop/Restart) using ProcessHandle. - Updated ComfyUI Bridge with improved UI refresh and sync logic. - Added comprehensive integration and diagnostic tests. - Updated documentation with process control details.
This commit is contained in:
@@ -3,6 +3,7 @@ package de.tki.comfymodels;
|
||||
import de.tki.comfymodels.domain.ModelInfo;
|
||||
import de.tki.comfymodels.service.impl.ConfigService;
|
||||
import de.tki.comfymodels.service.impl.LocalModelScanner;
|
||||
import de.tki.comfymodels.service.impl.PathResolver;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
@@ -26,13 +27,16 @@ class SubfolderRecognitionTest {
|
||||
|
||||
private ConfigService configService;
|
||||
|
||||
private PathResolver pathResolver;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
configService = new ConfigService(null, null) {
|
||||
pathResolver = new PathResolver();
|
||||
configService = new ConfigService(null, pathResolver) {
|
||||
@Override public String getModelsPath() { return tempModels.toString(); }
|
||||
@Override public String getArchivePath() { return "archive_mock"; }
|
||||
};
|
||||
localScanner = new LocalModelScanner(configService);
|
||||
localScanner = new LocalModelScanner(configService, pathResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -44,7 +48,7 @@ class SubfolderRecognitionTest {
|
||||
Files.createFile(modelFile);
|
||||
|
||||
// Test the generic recursive search method
|
||||
Optional<Path> found = localScanner.findModelInDirectory(tempModels, "wan2.2_test.safetensors");
|
||||
Optional<Path> found = localScanner.findModelWithPrefSize(tempModels, "wan2.2_test.safetensors", -1);
|
||||
assertTrue(found.isPresent(), "Model should be found in deep subfolder");
|
||||
assertEquals(modelFile.toAbsolutePath(), found.get().toAbsolutePath(), "Found path should match actual file path");
|
||||
|
||||
@@ -85,18 +89,18 @@ class SubfolderRecognitionTest {
|
||||
Files.write(archivedFile, new byte[1234]);
|
||||
|
||||
// Mock config to have separate models and archive paths
|
||||
ConfigService dualConfig = new ConfigService(null, null) {
|
||||
ConfigService dualConfig = new ConfigService(null, pathResolver) {
|
||||
@Override public String getModelsPath() { return tempModels.toString(); }
|
||||
@Override public String getArchivePath() { return archiveDir.toString(); }
|
||||
};
|
||||
LocalModelScanner dualScanner = new LocalModelScanner(dualConfig);
|
||||
LocalModelScanner dualScanner = new LocalModelScanner(dualConfig, pathResolver);
|
||||
|
||||
// Scan in models dir - should NOT find it
|
||||
Optional<Path> foundLocal = dualScanner.findModelInDirectory(tempModels, "exclusive_model.safetensors");
|
||||
Optional<Path> foundLocal = dualScanner.findModelWithPrefSize(tempModels, "exclusive_model.safetensors", -1);
|
||||
assertFalse(foundLocal.isPresent(), "Should not find archived model in local models scan");
|
||||
|
||||
// Scan in archive dir - SHOULD find it
|
||||
Optional<Path> foundArchive = dualScanner.findModelInDirectory(archiveDir, "exclusive_model.safetensors");
|
||||
Optional<Path> foundArchive = dualScanner.findModelWithPrefSize(archiveDir, "exclusive_model.safetensors", -1);
|
||||
assertTrue(foundArchive.isPresent(), "Should find model in archive scan");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.service.impl.ConfigService;
|
||||
import de.tki.comfymodels.service.impl.ComfyLifecycleService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Integration test to verify the full ComfyUI server lifecycle:
|
||||
* Auto-discovery -> Start -> Reachability Check -> Stop -> Reachability Check.
|
||||
*/
|
||||
public class ComfyLifecycleIntegrationTest {
|
||||
|
||||
@Test
|
||||
public void testFullServerLifecycle() throws Exception {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfig.class);
|
||||
ConfigService config = context.getBean(ConfigService.class);
|
||||
IComfyLifecycleService lifecycle = context.getBean(IComfyLifecycleService.class);
|
||||
|
||||
System.out.println("\n--- [START] Lifecycle Integration Test ---");
|
||||
|
||||
// 1. Discovery Phase
|
||||
System.out.println("Step 1: Running path discovery...");
|
||||
// Simulate real environment conditions
|
||||
config.autoDiscoverPaths();
|
||||
|
||||
String root = config.getComfyUIPath();
|
||||
String command = config.getComfyLaunchCommand();
|
||||
String models = config.getModelsPath();
|
||||
|
||||
System.out.println("Detected Root: " + root);
|
||||
System.out.println("Detected Command: " + command);
|
||||
System.out.println("Detected Models: " + models);
|
||||
|
||||
assertNotNull(root, "ComfyUI Root should not be null");
|
||||
assertFalse(root.isEmpty(), "ComfyUI Root should not be empty");
|
||||
assertTrue(new File(root, "main.py").exists(), "main.py MUST exist in discovered root: " + root);
|
||||
assertNotNull(command, "Launch command should not be null");
|
||||
assertFalse(command.isEmpty(), "Launch command should not be empty");
|
||||
assertNotNull(models, "Models path should not be null");
|
||||
assertTrue(new File(models).exists(), "Models path MUST exist: " + models);
|
||||
|
||||
// 2. Startup Phase
|
||||
System.out.println("Step 2: Starting server...");
|
||||
lifecycle.start();
|
||||
|
||||
// Wait for server to become healthy (API response)
|
||||
boolean started = false;
|
||||
for (int i = 0; i < 30; i++) {
|
||||
System.out.println("Waiting for health (Attempt " + (i + 1) + "/30)... Status: " + lifecycle.getStatus());
|
||||
if (lifecycle.isHealthy()) {
|
||||
started = true;
|
||||
break;
|
||||
}
|
||||
Thread.sleep(2000); // 2s steps for ComfyUI
|
||||
}
|
||||
|
||||
assertTrue(started, "Server should be healthy after start. Current Status: " + lifecycle.getStatus());
|
||||
assertTrue(lifecycle.isRunning(), "Server should be running according to Lifecycle Service");
|
||||
|
||||
// 3. Stop Phase
|
||||
System.out.println("Step 3: Stopping server...");
|
||||
lifecycle.stop();
|
||||
|
||||
// Wait a bit for port release
|
||||
Thread.sleep(5000);
|
||||
|
||||
assertFalse(lifecycle.isHealthy(), "Server should NO LONGER be healthy after stop");
|
||||
assertFalse(lifecycle.isRunning(), "Server should NO LONGER be running after stop");
|
||||
|
||||
System.out.println("--- [SUCCESS] Lifecycle Integration Test PASSED ---\n");
|
||||
context.close();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@ComponentScan("de.tki.comfymodels")
|
||||
public static class TestConfig {
|
||||
// Standard scan picks up all components
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ public class ComprehensiveStatusFlowTest {
|
||||
configService.setModelsPath(modelsDir.toString());
|
||||
configService.setArchivePath(archiveDir.toString());
|
||||
|
||||
localScanner = new LocalModelScanner(configService);
|
||||
localScanner = new LocalModelScanner(configService, pathResolver);
|
||||
archiveService = new ArchiveService(configService, pathResolver);
|
||||
analyzer = new ComfyModelAnalyzer();
|
||||
|
||||
@@ -76,23 +76,23 @@ public class ComprehensiveStatusFlowTest {
|
||||
Files.write(modelPath, new byte[2000]); // Update to 2KB
|
||||
assertEquals("✅ Already exists", getStatus(results.get(0), true, false, 2000));
|
||||
|
||||
// 5. State: Found in Archive (Removed locally, moved to archive)
|
||||
// 5. State: Archived (Removed locally, moved to archive)
|
||||
Files.delete(modelPath);
|
||||
Path archivePath = archiveDir.resolve("checkpoints").resolve(modelName);
|
||||
Files.createDirectories(archivePath.getParent());
|
||||
Files.write(archivePath, new byte[2000]);
|
||||
|
||||
assertEquals("📦 Found in Archive", getStatus(results.get(0), false, true, 0));
|
||||
|
||||
// 6. State: Already exists (Archived copy found) - Exists in both
|
||||
assertEquals("📦 Archived", getStatus(results.get(0), false, true, 0));
|
||||
|
||||
// 6. State: Already exists (Size match, even if archived)
|
||||
Files.write(modelPath, new byte[2000]);
|
||||
assertEquals("✅ Already exists (Archived copy found)", getStatus(results.get(0), true, true, 2000));
|
||||
}
|
||||
assertEquals("✅ Already exists", getStatus(results.get(0), true, true, 2000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to simulate the status logic from Main.java analyzeJsonContent()
|
||||
*/
|
||||
private String getStatus(ModelInfo info, boolean existsLocally, boolean existsInArchive, long localSize) {
|
||||
/**
|
||||
* Helper to simulate the status logic from Main.java analyzeJsonContent()
|
||||
*/
|
||||
private String getStatus(ModelInfo info, boolean existsLocally, boolean existsInArchive, long localSize) {
|
||||
boolean sizeMismatch = false;
|
||||
if (existsLocally && info.getByteSize() > 0) {
|
||||
if (localSize != info.getByteSize()) {
|
||||
@@ -101,13 +101,13 @@ public class ComprehensiveStatusFlowTest {
|
||||
}
|
||||
}
|
||||
|
||||
if (existsLocally && existsInArchive) return "✅ Already exists (Archived copy found)";
|
||||
if (existsLocally) return "✅ Already exists";
|
||||
if (existsInArchive) return "📦 Found in Archive";
|
||||
if (existsInArchive) return "📦 Archived";
|
||||
if (sizeMismatch) return "🔄 Size Mismatch";
|
||||
|
||||
|
||||
if (info.getUrl().equals("MISSING")) return "Idle";
|
||||
|
||||
|
||||
return "✅ Known Good";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.service.impl.ConfigService;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONArray;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import de.tki.comfymodels.Main.AppConfig;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class DeepModelDiagnosticTest {
|
||||
|
||||
@Test
|
||||
public void inspectNodeOptions() throws Exception {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
|
||||
ConfigService configService = context.getBean(ConfigService.class);
|
||||
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
|
||||
|
||||
String targetModel = "ltx2.3-transition.safetensors";
|
||||
String url = configService.getComfyUIUrl() + "/object_info";
|
||||
|
||||
System.out.println("--- DEEP API INSPECTION ---");
|
||||
System.out.println("Target: " + targetModel);
|
||||
System.out.println("Querying: " + url);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build();
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
if (response.statusCode() != 200) {
|
||||
System.out.println("❌ API Error: " + response.statusCode());
|
||||
return;
|
||||
}
|
||||
|
||||
JSONObject allNodes = new JSONObject(response.body());
|
||||
String[] nodesToInspect = {"LoraLoader", "CheckpointLoaderSimple", "CheckpointLoader", "LoraLoaderModelOnly"};
|
||||
|
||||
boolean foundInAny = false;
|
||||
for (String nodeName : nodesToInspect) {
|
||||
if (allNodes.has(nodeName)) {
|
||||
System.out.println("\nChecking Node: " + nodeName);
|
||||
JSONObject node = allNodes.getJSONObject(nodeName);
|
||||
if (node.has("input")) {
|
||||
JSONObject input = node.getJSONObject("input");
|
||||
JSONObject required = input.optJSONObject("required");
|
||||
if (required != null) {
|
||||
for (String key : required.keySet()) {
|
||||
JSONArray param = required.optJSONArray(key);
|
||||
if (param != null && param.length() > 0 && param.get(0) instanceof JSONArray) {
|
||||
JSONArray options = param.getJSONArray(0);
|
||||
System.out.println(" Parameter '" + key + "' has " + options.length() + " options.");
|
||||
|
||||
List<String> matches = new ArrayList<>();
|
||||
for (int i = 0; i < options.length(); i++) {
|
||||
String opt = options.getString(i);
|
||||
if (opt.toLowerCase().contains("ltx") || opt.equalsIgnoreCase(targetModel)) {
|
||||
matches.add(opt);
|
||||
}
|
||||
}
|
||||
|
||||
if (!matches.isEmpty()) {
|
||||
System.out.println(" 🎯 POTENTIAL MATCHES FOUND:");
|
||||
for (String m : matches) {
|
||||
System.out.println(" - " + m);
|
||||
if (m.equals(targetModel)) foundInAny = true;
|
||||
}
|
||||
} else {
|
||||
System.out.println(" (No LTX-related models in this list)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
System.out.println("\nNode not found: " + nodeName);
|
||||
}
|
||||
}
|
||||
|
||||
if (foundInAny) {
|
||||
System.out.println("\n✅ VERIFIED: The model is technically present in the API options.");
|
||||
System.out.println("If it is missing in the GUI, it might be a caching issue in the browser or a custom node conflict.");
|
||||
} else {
|
||||
System.out.println("\n❌ FAILED: The model is NOT present in any examined node parameters.");
|
||||
}
|
||||
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.domain.ModelInfo;
|
||||
import de.tki.comfymodels.service.impl.ArchiveService;
|
||||
import de.tki.comfymodels.service.impl.ConfigService;
|
||||
import de.tki.comfymodels.service.impl.LocalModelScanner;
|
||||
import de.tki.comfymodels.service.impl.PathResolver;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ExtraModelPathsIntegrationTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private LocalModelScanner scanner;
|
||||
private ConfigService configService;
|
||||
private PathResolver pathResolver;
|
||||
private ArchiveService archiveService;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
pathResolver = new PathResolver();
|
||||
// Custom ConfigService to use our tempDir for everything and avoid complex setup
|
||||
configService = new ConfigService(null, pathResolver) {
|
||||
private String comfyPath = "";
|
||||
private String modelsPath = "";
|
||||
private String archivePath = "";
|
||||
|
||||
@Override public String getComfyUIPath() { return comfyPath; }
|
||||
@Override public void setComfyUIPath(String path) { this.comfyPath = path; pathResolver.setComfyUIRoot(path); }
|
||||
@Override public String getModelsPath() { return modelsPath; }
|
||||
@Override public void setModelsPath(String path) { this.modelsPath = path; }
|
||||
@Override public String getArchivePath() { return archivePath; }
|
||||
@Override public void setArchivePath(String path) { this.archivePath = path; }
|
||||
@Override public String getAppDataPath() { return tempDir.toString(); }
|
||||
@Override public void save() { /* avoid encryption issues in test */ }
|
||||
@Override public boolean isUnlocked() { return true; }
|
||||
};
|
||||
scanner = new LocalModelScanner(configService, pathResolver);
|
||||
archiveService = new ArchiveService(configService, pathResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtraModelPathsDiscovery() throws IOException {
|
||||
// 1. Setup ComfyUI Root and extra_model_paths.yaml
|
||||
Path comfyRoot = tempDir.resolve("ComfyUI");
|
||||
Files.createDirectories(comfyRoot);
|
||||
configService.setComfyUIPath(comfyRoot.toString());
|
||||
|
||||
Path extraModelsDir = tempDir.resolve("ExternalModels");
|
||||
Path checkpointsDir = extraModelsDir.resolve("my_checkpoints");
|
||||
Files.createDirectories(checkpointsDir);
|
||||
|
||||
// Create a model file in the extra path
|
||||
Path modelFile = checkpointsDir.resolve("extra_model.safetensors");
|
||||
Files.writeString(modelFile, "dummy");
|
||||
|
||||
// Create the YAML
|
||||
String yamlContent =
|
||||
"my_extra_path:\n" +
|
||||
" base_path: " + extraModelsDir.toAbsolutePath().toString().replace("\\", "/") + "\n" +
|
||||
" checkpoints: my_checkpoints\n";
|
||||
|
||||
Files.writeString(comfyRoot.resolve("extra_model_paths.yaml"), yamlContent);
|
||||
|
||||
// 2. Load the extra paths
|
||||
configService.loadExtraModelPaths();
|
||||
|
||||
// 3. Verify PathResolver knows about it
|
||||
List<Path> paths = pathResolver.getModelPaths("checkpoints");
|
||||
assertFalse(paths.isEmpty(), "Should have found extra checkpoints path");
|
||||
assertTrue(paths.stream().anyMatch(p -> p.toAbsolutePath().equals(checkpointsDir.toAbsolutePath())), "Should contain the correct checkpoints dir");
|
||||
|
||||
// 4. Run Scanner and verify discovery
|
||||
List<ModelInfo> results = scanner.scanLocalModels();
|
||||
|
||||
boolean found = results.stream().anyMatch(m -> m.getName().equals("extra_model.safetensors"));
|
||||
assertTrue(found, "Scanner should find model in extra path");
|
||||
|
||||
ModelInfo info = results.stream().filter(m -> m.getName().equals("extra_model.safetensors")).findFirst().get();
|
||||
assertEquals("checkpoints", info.getType(), "Should correctly identify as checkpoint");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArchiveRestorationToExtraPath() throws IOException {
|
||||
// 1. Setup ComfyUI Root and extra_model_paths.yaml
|
||||
Path comfyRoot = tempDir.resolve("ComfyUI");
|
||||
Files.createDirectories(comfyRoot);
|
||||
configService.setComfyUIPath(comfyRoot.toString());
|
||||
|
||||
Path extraModelsDir = tempDir.resolve("ExternalModels");
|
||||
Path checkpointsDir = extraModelsDir.resolve("my_checkpoints");
|
||||
Files.createDirectories(checkpointsDir);
|
||||
|
||||
// Create the YAML mapping checkpoints to ExternalModels/my_checkpoints
|
||||
String yamlContent =
|
||||
"my_extra_path:\n" +
|
||||
" base_path: " + extraModelsDir.toAbsolutePath().toString().replace("\\", "/") + "\n" +
|
||||
" checkpoints: my_checkpoints\n";
|
||||
Files.writeString(comfyRoot.resolve("extra_model_paths.yaml"), yamlContent);
|
||||
configService.loadExtraModelPaths();
|
||||
|
||||
// 2. Setup Archive
|
||||
Path archiveRoot = tempDir.resolve("Archive");
|
||||
Files.createDirectories(archiveRoot);
|
||||
configService.setArchivePath(archiveRoot.toString());
|
||||
|
||||
// Put a model in the archive structure
|
||||
Path archivedModel = archiveRoot.resolve("checkpoints").resolve("archived_extra.safetensors");
|
||||
Files.createDirectories(archivedModel.getParent());
|
||||
Files.writeString(archivedModel, "dummy model data");
|
||||
|
||||
// 3. Run restoration for "checkpoints" folder
|
||||
boolean success = archiveService.restoreFromArchiveWithProgress("checkpoints", "archived_extra.safetensors", null);
|
||||
|
||||
assertTrue(success, "Restoration should be successful");
|
||||
|
||||
// 4. VERIFY: Should be in checkpointsDir (extra path)
|
||||
Path expectedTarget = checkpointsDir.resolve("archived_extra.safetensors");
|
||||
assertTrue(Files.exists(expectedTarget), "Model should be restored to the extra checkpoints path: " + expectedTarget);
|
||||
assertFalse(Files.exists(archivedModel), "Source should be deleted from archive after successful move");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArchiveRestorationToNestedExtraPath() throws IOException {
|
||||
// Setup same as above but with nested folder
|
||||
Path comfyRoot = tempDir.resolve("ComfyUI");
|
||||
Files.createDirectories(comfyRoot);
|
||||
configService.setComfyUIPath(comfyRoot.toString());
|
||||
|
||||
Path extraModelsDir = tempDir.resolve("ExternalModels");
|
||||
Path checkpointsDir = extraModelsDir.resolve("checkpoints");
|
||||
Path sd15Dir = checkpointsDir.resolve("SD15");
|
||||
Files.createDirectories(sd15Dir);
|
||||
|
||||
String yamlContent =
|
||||
"my_extra_path:\n" +
|
||||
" base_path: " + extraModelsDir.toAbsolutePath().toString().replace("\\", "/") + "\n" +
|
||||
" checkpoints: checkpoints\n";
|
||||
Files.writeString(comfyRoot.resolve("extra_model_paths.yaml"), yamlContent);
|
||||
configService.loadExtraModelPaths();
|
||||
|
||||
// Setup Archive
|
||||
Path archiveRoot = tempDir.resolve("Archive");
|
||||
Files.createDirectories(archiveRoot);
|
||||
configService.setArchivePath(archiveRoot.toString());
|
||||
|
||||
// Model in archive: checkpoints/SD15/nested.safetensors
|
||||
Path archivedModel = archiveRoot.resolve("checkpoints/SD15/nested.safetensors");
|
||||
Files.createDirectories(archivedModel.getParent());
|
||||
Files.writeString(archivedModel, "nested data");
|
||||
|
||||
// Run restoration for "checkpoints/SD15"
|
||||
boolean success = archiveService.restoreFromArchiveWithProgress("checkpoints/SD15", "nested.safetensors", null);
|
||||
|
||||
assertTrue(success, "Nested restoration should be successful");
|
||||
|
||||
// VERIFY: Should be in sd15Dir (extra path + subfolder)
|
||||
Path expectedTarget = sd15Dir.resolve("nested.safetensors");
|
||||
assertTrue(Files.exists(expectedTarget), "Model should be restored to the nested extra path: " + expectedTarget);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.service.impl.ConfigService;
|
||||
import de.tki.comfymodels.service.impl.ComfyLifecycleService;
|
||||
import de.tki.comfymodels.service.impl.ComfyDiagnosticService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import de.tki.comfymodels.Main.AppConfig;
|
||||
|
||||
public class FinalDampfhammerTest {
|
||||
|
||||
@Test
|
||||
public void executeRestartAndVerify() throws Exception {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
|
||||
ComfyLifecycleService lifecycleService = context.getBean(ComfyLifecycleService.class);
|
||||
ComfyDiagnosticService diagnosticService = context.getBean(ComfyDiagnosticService.class);
|
||||
|
||||
System.out.println("--- 🔨 EXECUTING FINAL DAMPFHAMMER RESTART ---");
|
||||
|
||||
// 1. Force Kill
|
||||
System.out.println("⏹ Terminating existing Python processes...");
|
||||
Runtime.getRuntime().exec("taskkill /F /IM python.exe /T").waitFor();
|
||||
Thread.sleep(2000);
|
||||
|
||||
// 2. Start via Service
|
||||
System.out.println("▶ Starting ComfyUI...");
|
||||
lifecycleService.start();
|
||||
|
||||
// 3. Robust Health Check (Wait up to 90 seconds)
|
||||
System.out.println("⏳ Waiting for API (http://127.0.0.1:8000)...");
|
||||
boolean ready = false;
|
||||
for (int i = 0; i < 90; i++) {
|
||||
if (lifecycleService.isHealthy()) {
|
||||
ready = true;
|
||||
break;
|
||||
}
|
||||
if (i % 10 == 0) System.out.println("Still waiting... (" + i + "s)");
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
|
||||
if (ready) {
|
||||
System.out.println("✅ ComfyUI is ONLINE and HEALTHY.");
|
||||
|
||||
// 4. Final verification of the specific model
|
||||
String model = "ltx2.3-transition.safetensors";
|
||||
if (diagnosticService.isModelAvailableInComfy(model, "loras")) {
|
||||
System.out.println("🎯 TARGET ACHIEVED: '" + model + "' is recognized by ComfyUI!");
|
||||
} else {
|
||||
System.out.println("❌ Model still missing. This implies the file is not in 'models/loras' or extra_model_paths is misconfigured.");
|
||||
}
|
||||
} else {
|
||||
System.out.println("❌ ComfyUI failed to respond. Please check your console/logs.");
|
||||
}
|
||||
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
@@ -25,14 +25,15 @@ public class LocalModelScannerTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
PathResolver pathResolver = new PathResolver();
|
||||
// Use a simple stub for ConfigService to avoid Mockito issues on Java 25
|
||||
configService = new ConfigService(null, new PathResolver()) {
|
||||
configService = new ConfigService(null, pathResolver) {
|
||||
@Override
|
||||
public String getModelsPath() {
|
||||
return tempDir.toString();
|
||||
}
|
||||
};
|
||||
scanner = new LocalModelScanner(configService);
|
||||
scanner = new LocalModelScanner(configService, pathResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.service.impl.ComfyDiagnosticService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import de.tki.comfymodels.Main.AppConfig;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
|
||||
public class ModelPresenceVerificationTest {
|
||||
|
||||
@Test
|
||||
public void verifyModelPresence() throws Exception {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
|
||||
ComfyDiagnosticService diagnosticService = context.getBean(ComfyDiagnosticService.class);
|
||||
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build();
|
||||
|
||||
String modelName = "ltx2.3-transition.safetensors";
|
||||
String type = "loras";
|
||||
|
||||
System.out.println("--- Starting Automated Refresh Loop for: " + modelName + " ---");
|
||||
|
||||
boolean found = false;
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
System.out.println("Attempt " + i + ": Triggering Refresh...");
|
||||
|
||||
// Trigger Refresh
|
||||
try {
|
||||
HttpRequest refreshReq = HttpRequest.newBuilder()
|
||||
.uri(URI.create("http://127.0.0.1:8000/cmfd/refresh-models"))
|
||||
.POST(HttpRequest.BodyPublishers.noBody())
|
||||
.build();
|
||||
client.send(refreshReq, HttpResponse.BodyHandlers.ofString());
|
||||
} catch (Exception e) {
|
||||
System.out.println("Refresh ping failed (Bridge maybe not installed?): " + e.getMessage());
|
||||
}
|
||||
|
||||
// Wait for processing
|
||||
Thread.sleep(3000);
|
||||
|
||||
// Check Presence
|
||||
if (diagnosticService.isModelAvailableInComfy(modelName, type)) {
|
||||
System.out.println("✅ SUCCESS: Model '" + modelName + "' is now visible in ComfyUI!");
|
||||
found = true;
|
||||
break;
|
||||
} else {
|
||||
System.out.println("❌ Model not found yet.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
System.out.println("⚠️ FAILURE: Model still not visible after 5 attempts.");
|
||||
}
|
||||
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.Main;
|
||||
import de.tki.comfymodels.domain.ModelInfo;
|
||||
import de.tki.comfymodels.service.impl.*;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModelStatusLogicIntegrationTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private Main main;
|
||||
private ConfigService configService;
|
||||
private PathResolver pathResolver;
|
||||
private ArchiveService archiveService;
|
||||
private LocalModelScanner localScanner;
|
||||
private ComfyModelAnalyzer analyzer;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception {
|
||||
pathResolver = new PathResolver();
|
||||
configService = new ConfigService(null, pathResolver) {
|
||||
private String modelsPath = "";
|
||||
private String archivePath = "";
|
||||
@Override public String getModelsPath() { return modelsPath; }
|
||||
@Override public void setModelsPath(String path) { this.modelsPath = path; }
|
||||
@Override public String getArchivePath() { return archivePath; }
|
||||
@Override public void setArchivePath(String path) { this.archivePath = path; }
|
||||
@Override public String getAppDataPath() { return tempDir.toString(); }
|
||||
@Override public boolean isUnlocked() { return true; }
|
||||
@Override public void save() {}
|
||||
};
|
||||
|
||||
analyzer = Mockito.mock(ComfyModelAnalyzer.class);
|
||||
localScanner = new LocalModelScanner(configService, pathResolver);
|
||||
archiveService = new ArchiveService(configService, pathResolver);
|
||||
|
||||
main = new Main(
|
||||
analyzer,
|
||||
Mockito.mock(DefaultDownloadManager.class),
|
||||
Mockito.mock(WorkflowService.class),
|
||||
Mockito.mock(ModelSearchService.class),
|
||||
Mockito.mock(ModelValidator.class),
|
||||
Mockito.mock(RestBridgeService.class),
|
||||
archiveService,
|
||||
Mockito.mock(ComfyLifecycleService.class),
|
||||
Mockito.mock(ComfyDiagnosticService.class)
|
||||
);
|
||||
|
||||
// Inject dependencies via reflection since they are @Autowired in Main
|
||||
setField(main, "configService", configService);
|
||||
setField(main, "localScanner", localScanner);
|
||||
setField(main, "pathResolver", pathResolver);
|
||||
setField(main, "archiveService", archiveService);
|
||||
|
||||
// Initialize enough UI for analyzeJsonContent to work
|
||||
setField(main, "tableModel", new DefaultTableModel(new String[]{"S", "T", "N", "Sz", "Src", "P", "U", "Status"}, 0));
|
||||
JTextArea area = new JTextArea("dummy workflow");
|
||||
setField(main, "jsonInputArea", area);
|
||||
setField(main, "statusLabel", new JLabel());
|
||||
setField(main, "downloadButton", new JButton());
|
||||
}
|
||||
|
||||
private void setField(Object target, String name, Object value) throws Exception {
|
||||
Field f = target.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
f.set(target, value);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStatusTransitions() throws IOException, Exception {
|
||||
Path modelsDir = tempDir.resolve("models");
|
||||
Files.createDirectories(modelsDir.resolve("checkpoints"));
|
||||
configService.setModelsPath(modelsDir.toString());
|
||||
|
||||
Path archiveDir = tempDir.resolve("archive");
|
||||
Files.createDirectories(archiveDir.resolve("checkpoints"));
|
||||
configService.setArchivePath(archiveDir.toString());
|
||||
|
||||
// Prepare 3 scenarios
|
||||
List<ModelInfo> models = new ArrayList<>();
|
||||
models.add(new ModelInfo("checkpoints", "local_only.safetensors", "COMMUNITY"));
|
||||
models.get(0).setUrl("http://example.com/1");
|
||||
|
||||
models.add(new ModelInfo("checkpoints", "archived_only.safetensors", "COMMUNITY"));
|
||||
models.get(1).setUrl("http://example.com/2");
|
||||
|
||||
models.add(new ModelInfo("checkpoints", "both_locations.safetensors", "COMMUNITY"));
|
||||
models.get(2).setUrl("http://example.com/3");
|
||||
|
||||
models.add(new ModelInfo("checkpoints", "new_model.safetensors", "COMMUNITY"));
|
||||
models.get(3).setUrl("http://example.com/4");
|
||||
|
||||
when(analyzer.analyze(anyString(), anyString())).thenReturn(models);
|
||||
|
||||
// Create the files
|
||||
Files.writeString(modelsDir.resolve("checkpoints/local_only.safetensors"), "data");
|
||||
Files.writeString(archiveDir.resolve("checkpoints/archived_only.safetensors"), "data");
|
||||
Files.writeString(modelsDir.resolve("checkpoints/both_locations.safetensors"), "data");
|
||||
Files.writeString(archiveDir.resolve("checkpoints/both_locations.safetensors"), "data");
|
||||
|
||||
// Execute analysis
|
||||
java.lang.reflect.Method method = main.getClass().getDeclaredMethod("analyzeJsonContent");
|
||||
method.setAccessible(true);
|
||||
method.invoke(main);
|
||||
|
||||
DefaultTableModel model = (DefaultTableModel) getField(main, "tableModel");
|
||||
|
||||
// Verify Statuses
|
||||
assertEquals("✅ Already exists", model.getValueAt(0, 7), "Should be local only");
|
||||
assertEquals("📦 Archived", model.getValueAt(1, 7), "Should be in archive only");
|
||||
assertEquals("✅ Already exists", model.getValueAt(2, 7), "Should prioritize local over archive (obsolete status removed)");
|
||||
assertEquals("✅ Known Good", model.getValueAt(3, 7), "Should be new/downloadable");
|
||||
}
|
||||
|
||||
private Object getField(Object target, String name) throws Exception {
|
||||
Field f = target.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
return f.get(target);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.service.impl.ConfigService;
|
||||
import de.tki.comfymodels.service.impl.ComfyDiagnosticService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import de.tki.comfymodels.Main.AppConfig;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class NuclearFrontendRefreshTest {
|
||||
|
||||
@Test
|
||||
public void triggerReload() throws Exception {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
|
||||
ConfigService configService = context.getBean(ConfigService.class);
|
||||
ComfyDiagnosticService diagnosticService = context.getBean(ComfyDiagnosticService.class);
|
||||
HttpClient client = HttpClient.newBuilder().build();
|
||||
|
||||
System.out.println("--- ☢️ TRIGGERING NUCLEAR FRONTEND REFRESH ---");
|
||||
|
||||
// 1. Sync Bridge to Disk (Ensure latest JS/Python is there)
|
||||
String comfyPath = "C:\\AI\\ComfyUI\\resources\\ComfyUI";
|
||||
Path bridgeDir = Paths.get(comfyPath, "custom_nodes", "comfyui-model-downloader");
|
||||
System.out.println("📦 Syncing Bridge to: " + bridgeDir);
|
||||
Files.createDirectories(bridgeDir.resolve("web"));
|
||||
|
||||
copyResource("/comfyui-bridge/__init__.py", bridgeDir.resolve("__init__.py"));
|
||||
copyResource("/comfyui-bridge/web/downloader.js", bridgeDir.resolve("web/downloader.js"));
|
||||
Files.writeString(bridgeDir.resolve("web/config.json"), "{\"token\": \"" + configService.getApiToken() + "\"}");
|
||||
|
||||
// 2. Send the "Nuclear" REST Call
|
||||
System.out.println("🚀 Sending REST signal with 'force_reload: true'...");
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create("http://127.0.0.1:8000/cmfd/refresh-models"))
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString("{\"force_reload\": true}"))
|
||||
.build();
|
||||
|
||||
try {
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
System.out.println("📡 Server Response: " + response.statusCode() + " " + response.body());
|
||||
} catch (Exception e) {
|
||||
System.out.println("❌ Failed to reach ComfyUI: " + e.getMessage());
|
||||
}
|
||||
|
||||
// 3. Verify Backend
|
||||
String model = "ltx2.3-transition.safetensors";
|
||||
if (diagnosticService.isModelAvailableInComfy(model, "loras")) {
|
||||
System.out.println("✅ BACKEND VERIFIED: '" + model + "' is present in the API.");
|
||||
} else {
|
||||
System.out.println("❌ BACKEND FAILED: Model still not in API.");
|
||||
}
|
||||
|
||||
context.close();
|
||||
}
|
||||
|
||||
private void copyResource(String name, Path target) throws Exception {
|
||||
try (InputStream is = getClass().getResourceAsStream(name)) {
|
||||
Files.copy(is, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.service.impl.ConfigService;
|
||||
import de.tki.comfymodels.service.impl.ComfyLifecycleService;
|
||||
import de.tki.comfymodels.service.impl.ComfyDiagnosticService;
|
||||
import de.tki.comfymodels.Main;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import de.tki.comfymodels.Main.AppConfig;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
public class OneClickFixTest {
|
||||
|
||||
@Test
|
||||
public void performNuclearFix() throws Exception {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
|
||||
ConfigService configService = context.getBean(ConfigService.class);
|
||||
ComfyLifecycleService lifecycleService = context.getBean(ComfyLifecycleService.class);
|
||||
ComfyDiagnosticService diagnosticService = context.getBean(ComfyDiagnosticService.class);
|
||||
|
||||
System.out.println("--- ☢️ NUCLEAR ONE-CLICK FIX STARTING ---");
|
||||
|
||||
// 1. Reinstall Bridge to the correct location
|
||||
String comfyPath = "C:\\AI\\ComfyUI\\resources\\ComfyUI";
|
||||
Path bridgeDir = Paths.get(comfyPath, "custom_nodes", "comfyui-model-downloader");
|
||||
System.out.println("📦 Reinstalling Bridge to: " + bridgeDir);
|
||||
Files.createDirectories(bridgeDir);
|
||||
Files.createDirectories(bridgeDir.resolve("web"));
|
||||
|
||||
copyResource("/comfyui-bridge/__init__.py", bridgeDir.resolve("__init__.py"));
|
||||
copyResource("/comfyui-bridge/web/downloader.js", bridgeDir.resolve("web/downloader.js"));
|
||||
|
||||
String configJson = "{\"token\": \"" + configService.getApiToken() + "\"}";
|
||||
Files.writeString(bridgeDir.resolve("web/config.json"), configJson);
|
||||
System.out.println("✅ Bridge reinstalled.");
|
||||
|
||||
// 2. Kill existing process
|
||||
System.out.println("⏹ Stopping ComfyUI...");
|
||||
lifecycleService.stop();
|
||||
// Fallback kill for PID 14544 just in case
|
||||
Runtime.getRuntime().exec("taskkill /F /PID 14544").waitFor();
|
||||
System.out.println("✅ ComfyUI stopped.");
|
||||
|
||||
// 3. Start via Lifecycle Service
|
||||
System.out.println("▶ Starting ComfyUI...");
|
||||
lifecycleService.start();
|
||||
|
||||
// 4. Wait for Health
|
||||
System.out.println("⏳ Waiting for API to become healthy...");
|
||||
boolean healthy = false;
|
||||
for (int i = 0; i < 60; i++) {
|
||||
if (lifecycleService.isHealthy()) {
|
||||
healthy = true;
|
||||
break;
|
||||
}
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
|
||||
if (healthy) {
|
||||
System.out.println("✅ ComfyUI is back online!");
|
||||
|
||||
// 5. Final Diagnostic
|
||||
String model = "ltx2.3-transition.safetensors";
|
||||
if (diagnosticService.isModelAvailableInComfy(model, "loras")) {
|
||||
System.out.println("🎯 TARGET VERIFIED: '" + model + "' is now fully recognized!");
|
||||
} else {
|
||||
System.out.println("❌ Diagnostic failed even after restart. Please check if the file is in 'models/loras'.");
|
||||
}
|
||||
} else {
|
||||
System.out.println("❌ ComfyUI failed to start within 60 seconds.");
|
||||
}
|
||||
|
||||
context.close();
|
||||
}
|
||||
|
||||
private void copyResource(String name, Path target) throws Exception {
|
||||
try (InputStream is = getClass().getResourceAsStream(name)) {
|
||||
Files.copy(is, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.service.impl.ComfyDiagnosticService;
|
||||
import de.tki.comfymodels.service.impl.ConfigService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import de.tki.comfymodels.Main.AppConfig;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Rapid Diagnostic Test for Refresh Mechanisms.
|
||||
* This test uses the ComfyDiagnosticService to verify if ComfyUI actually sees
|
||||
* the models after our refresh calls.
|
||||
*/
|
||||
public class RefreshDiagnosticTest {
|
||||
|
||||
@Test
|
||||
public void testModelVisibility() {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
|
||||
ComfyDiagnosticService diagnosticService = context.getBean(ComfyDiagnosticService.class);
|
||||
|
||||
System.out.println("--- ComfyUI Diagnostic Scan ---");
|
||||
boolean reachable = diagnosticService.isModelAvailableInComfy("non_existent_model_xyz", "checkpoint");
|
||||
|
||||
// If the API is up, it should at least return false for a non-existent model
|
||||
// If it throws an exception or timeout, the API is down.
|
||||
System.out.println("API Reachable: " + !reachable);
|
||||
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package de.tki.comfymodels.service;
|
||||
import de.tki.comfymodels.domain.ModelInfo;
|
||||
import de.tki.comfymodels.service.impl.ArchiveService;
|
||||
import de.tki.comfymodels.service.impl.LocalModelScanner;
|
||||
import de.tki.comfymodels.service.impl.PathResolver;
|
||||
import de.tki.comfymodels.service.impl.RestBridgeService;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -31,7 +32,7 @@ class RestBridgeServiceTest {
|
||||
private final String testToken = "test-token";
|
||||
|
||||
private static class LocalModelScannerStub extends LocalModelScanner {
|
||||
public LocalModelScannerStub() { super(null); }
|
||||
public LocalModelScannerStub() { super(null, new PathResolver()); }
|
||||
@Override
|
||||
public java.util.List<ModelInfo> scanLocalModels() {
|
||||
return Collections.singletonList(new ModelInfo("checkpoints", "local_test.safetensors", "LOCAL"));
|
||||
|
||||
@@ -28,9 +28,9 @@ public class RestorationStatusTest {
|
||||
private Path tempArchiveDir;
|
||||
|
||||
// Status Constants (as used in Main.java)
|
||||
public static final String STATUS_FOUND_IN_ARCHIVE = "📦 Found in Archive";
|
||||
public static final String STATUS_ARCHIVED = "📦 Archived";
|
||||
public static final String STATUS_RESTORING = "📦 Restoring...";
|
||||
public static final String STATUS_RESTORED = "✅ Restored";
|
||||
public static final String STATUS_RESTORED = "✅ Already exists";
|
||||
public static final String STATUS_RESTORE_FAILED = "❌ Restore Failed";
|
||||
|
||||
@BeforeEach
|
||||
@@ -76,8 +76,8 @@ public class RestorationStatusTest {
|
||||
List<String> statusSequence = new ArrayList<>();
|
||||
|
||||
// Initial state (determined by UI/Logic)
|
||||
statusSequence.add(STATUS_FOUND_IN_ARCHIVE);
|
||||
assertEquals(STATUS_FOUND_IN_ARCHIVE, statusSequence.get(0));
|
||||
statusSequence.add(STATUS_ARCHIVED);
|
||||
assertEquals(STATUS_ARCHIVED, statusSequence.get(0));
|
||||
|
||||
// 3. Act: Start Restoration
|
||||
statusSequence.add(STATUS_RESTORING);
|
||||
@@ -109,7 +109,7 @@ public class RestorationStatusTest {
|
||||
|
||||
// 2. Define transitions tracker
|
||||
List<String> statusSequence = new ArrayList<>();
|
||||
statusSequence.add(STATUS_FOUND_IN_ARCHIVE);
|
||||
statusSequence.add(STATUS_ARCHIVED);
|
||||
statusSequence.add(STATUS_RESTORING);
|
||||
|
||||
// 3. Act: Attempt Restoration
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.service.impl.ComfyDiagnosticService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import de.tki.comfymodels.Main.AppConfig;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
|
||||
public class StabilityValidationTest {
|
||||
|
||||
@Test
|
||||
public void validateStability() throws Exception {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
|
||||
ComfyDiagnosticService diagnosticService = context.getBean(ComfyDiagnosticService.class);
|
||||
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build();
|
||||
|
||||
System.out.println("--- 🛡️ STARTING STABILITY VALIDATION ---");
|
||||
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
System.out.println("Cycle " + i + ": Triggering Refresh...");
|
||||
|
||||
HttpRequest refreshReq = HttpRequest.newBuilder()
|
||||
.uri(URI.create("http://127.0.0.1:8000/cmfd/refresh-models"))
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString("{\"force_reload\": false}"))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(refreshReq, HttpResponse.BodyHandlers.ofString());
|
||||
System.out.println("📡 Server Response: " + response.statusCode());
|
||||
|
||||
Thread.sleep(2000);
|
||||
|
||||
// Check if API is still alive
|
||||
if (diagnosticService.isModelAvailableInComfy("ltx2.3-transition.safetensors", "loras")) {
|
||||
System.out.println("✅ Cycle " + i + ": Server is healthy and model is visible.");
|
||||
} else {
|
||||
System.out.println("❌ Cycle " + i + ": Model visibility check failed.");
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("--- 🛡️ STABILITY VALIDATION COMPLETE ---");
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.service.impl.ConfigService;
|
||||
import de.tki.comfymodels.service.impl.ComfyLifecycleService;
|
||||
import de.tki.comfymodels.service.impl.ComfyDiagnosticService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import de.tki.comfymodels.Main.AppConfig;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class UltimateGuiSyncTest {
|
||||
|
||||
@Test
|
||||
public void ensureGuiSyncs() throws Exception {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
|
||||
ConfigService configService = context.getBean(ConfigService.class);
|
||||
ComfyLifecycleService lifecycleService = context.getBean(ComfyLifecycleService.class);
|
||||
ComfyDiagnosticService diagnosticService = context.getBean(ComfyDiagnosticService.class);
|
||||
HttpClient client = HttpClient.newBuilder().build();
|
||||
|
||||
System.out.println("--- 🚀 ULTIMATE GUI SYNC SEQUENCE ---");
|
||||
|
||||
// 1. HARD REINSTALL OF BRIDGE FILES
|
||||
String comfyPath = "C:\\AI\\ComfyUI\\resources\\ComfyUI";
|
||||
Path bridgeDir = Paths.get(comfyPath, "custom_nodes", "comfyui-model-downloader");
|
||||
System.out.println("📦 Enforcing Latest Bridge in: " + bridgeDir);
|
||||
Files.createDirectories(bridgeDir.resolve("web"));
|
||||
|
||||
copyResource("/comfyui-bridge/__init__.py", bridgeDir.resolve("__init__.py"));
|
||||
copyResource("/comfyui-bridge/web/downloader.js", bridgeDir.resolve("web/downloader.js"));
|
||||
Files.writeString(bridgeDir.resolve("web/config.json"), "{\"token\": \"" + configService.getApiToken() + "\"}");
|
||||
|
||||
// 2. FORCE RESTART BACKEND (To load new Python bridge)
|
||||
System.out.println("⏹ Terminating ComfyUI for Clean State...");
|
||||
Runtime.getRuntime().exec("taskkill /F /IM python.exe /T").waitFor();
|
||||
Thread.sleep(2000);
|
||||
System.out.println("▶ Starting ComfyUI...");
|
||||
lifecycleService.start();
|
||||
|
||||
// 3. WAIT FOR BACKEND READINESS
|
||||
System.out.println("⏳ Waiting for API...");
|
||||
for (int i = 0; i < 90; i++) {
|
||||
if (lifecycleService.isHealthy()) break;
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
|
||||
// 4. TRIGGER NUCLEAR FRONTEND RELOAD
|
||||
// This will force every open browser tab to run window.location.reload()
|
||||
System.out.println("☢️ Triggering Nuclear Browser Reload...");
|
||||
HttpRequest reloadReq = HttpRequest.newBuilder()
|
||||
.uri(URI.create("http://127.0.0.1:8000/cmfd/refresh-models"))
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString("{\"force_reload\": true}"))
|
||||
.build();
|
||||
|
||||
try {
|
||||
client.send(reloadReq, HttpResponse.BodyHandlers.ofString());
|
||||
System.out.println("✅ Reload signal broadcasted to all tabs.");
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
// 5. FINAL VERIFICATION
|
||||
String model = "ltx2.3-transition.safetensors";
|
||||
if (diagnosticService.isModelAvailableInComfy(model, "loras")) {
|
||||
System.out.println("🎯 TARGET ACHIEVED: Backend is synchronized.");
|
||||
System.out.println("Check your browser now. All tabs should have reloaded and the model should be visible!");
|
||||
} else {
|
||||
System.out.println("❌ Critical Failure: Model still missing from API.");
|
||||
}
|
||||
|
||||
context.close();
|
||||
}
|
||||
|
||||
private void copyResource(String name, Path target) throws Exception {
|
||||
try (InputStream is = getClass().getResourceAsStream(name)) {
|
||||
Files.copy(is, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user