feat(docs): update AGENTS.md with improved testing and development instructions

- Simplify pytest coverage command by consolidating --cov flags
- Remove redundant JSON coverage report
- Reorganize frontend section into "Frontend Development (Standalone Web UI)" and "Vue Widget Development"
- Add npm commands for Vue widget development (dev, build, typecheck, tests)
- Consolidate Python code style sections (imports, formatting, naming, error handling, async)
- Update architecture overview with clearer service descriptions
- Fix truncated line in API endpoints note
- Improve readability and remove redundant information
This commit is contained in:
Will Miao
2026-02-08 08:30:57 +08:00
parent e2f1520e7f
commit 199c9f742c
2 changed files with 197 additions and 260 deletions

181
AGENTS.md
View File

@@ -25,168 +25,127 @@ pytest tests/test_recipes.py::test_function_name
# Run backend tests with coverage # Run backend tests with coverage
COVERAGE_FILE=coverage/backend/.coverage pytest \ COVERAGE_FILE=coverage/backend/.coverage pytest \
--cov=py \ --cov=py --cov=standalone \
--cov=standalone \
--cov-report=term-missing \ --cov-report=term-missing \
--cov-report=html:coverage/backend/html \ --cov-report=html:coverage/backend/html \
--cov-report=xml:coverage/backend/coverage.xml \ --cov-report=xml:coverage/backend/coverage.xml
--cov-report=json:coverage/backend/coverage.json
``` ```
### Frontend Development ### Frontend Development (Standalone Web UI)
```bash ```bash
# Install frontend dependencies
npm install npm install
npm test # Run all tests (JS + Vue)
npm run test:js # Run JS tests only
npm run test:watch # Watch mode
npm run test:coverage # Generate coverage report
```
# Run frontend tests ### Vue Widget Development
npm test
# Run frontend tests in watch mode ```bash
npm run test:watch cd vue-widgets
npm install
# Run frontend tests with coverage npm run dev # Build in watch mode
npm run test:coverage npm run build # Build production bundle
npm run typecheck # Run TypeScript type checking
npm test # Run Vue widget tests
npm run test:watch # Watch mode
npm run test:coverage # Generate coverage report
``` ```
## Python Code Style ## Python Code Style
### Imports ### Imports & Formatting
- Use `from __future__ import annotations` for forward references in type hints - Use `from __future__ import annotations` for forward references
- Group imports: standard library, third-party, local (separated by blank lines) - Group imports: standard library, third-party, local (blank line separated)
- Use absolute imports within `py/` package: `from ..services import X` - Absolute imports within `py/`: `from ..services import X`
- Mock ComfyUI dependencies in tests using `tests/conftest.py` patterns - PEP 8 with 4-space indentation, type hints required
### Formatting & Types
- PEP 8 with 4-space indentation
- Type hints required for function signatures and class attributes
- Use `TYPE_CHECKING` guard for type-checking-only imports
- Prefer dataclasses for simple data containers
- Use `Optional[T]` for nullable types, `Union[T, None]` only when necessary
### Naming Conventions ### Naming Conventions
- Files: `snake_case.py` (e.g., `model_scanner.py`, `lora_service.py`) - Files: `snake_case.py`, Classes: `PascalCase`, Functions/vars: `snake_case`
- Classes: `PascalCase` (e.g., `ModelScanner`, `LoraService`) - Constants: `UPPER_SNAKE_CASE`, Private: `_protected`, `__mangled`
- Functions/variables: `snake_case` (e.g., `get_instance`, `model_type`)
- Constants: `UPPER_SNAKE_CASE` (e.g., `VALID_LORA_TYPES`)
- Private members: `_single_underscore` (protected), `__double_underscore` (name-mangled)
### Error Handling ### Error Handling & Async
- Use `logging.getLogger(__name__)` for module-level loggers - Use `logging.getLogger(__name__)`, define custom exceptions in `py/services/errors.py`
- Define custom exceptions in `py/services/errors.py` - `async def` for I/O, `@pytest.mark.asyncio` for async tests
- Use `asyncio.Lock` for thread-safe singleton patterns - Singleton with `asyncio.Lock`: see `ModelScanner.get_instance()`
- Raise specific exceptions with descriptive messages - Return `aiohttp.web.json_response` or `web.Response`
- Log errors at appropriate levels (DEBUG, INFO, WARNING, ERROR, CRITICAL)
### Async Patterns ### Testing
- Use `async def` for I/O-bound operations - `pytest` with `--import-mode=importlib`
- Mark async tests with `@pytest.mark.asyncio` - Fixtures in `tests/conftest.py`, use `tmp_path_factory` for isolation
- Use `async with` for context managers - Mark tests needing real paths: `@pytest.mark.no_settings_dir_isolation`
- Singleton pattern with class-level locks: see `ModelScanner.get_instance()` - Mock ComfyUI dependencies via conftest patterns
- Use `aiohttp.web.Response` for HTTP responses
### Testing Patterns ## JavaScript/TypeScript Code Style
- Use `pytest` with `--import-mode=importlib`
- Fixtures in `tests/conftest.py` handle ComfyUI mocking
- Use `@pytest.mark.no_settings_dir_isolation` for tests needing real paths
- Test files: `tests/test_*.py`
- Use `tmp_path_factory` for temporary directory isolation
## JavaScript Code Style
### Imports & Modules ### Imports & Modules
- ES modules with `import`/`export` - ES modules: `import { app } from "../../scripts/app.js"` for ComfyUI
- Use `import { app } from "../../scripts/app.js"` for ComfyUI integration - Vue: `import { ref, computed } from 'vue'`, type imports: `import type { Foo }`
- Export named functions/classes: `export function foo() {}` - Export named functions: `export function foo() {}`
- Widget files use `*_widget.js` suffix
### Naming & Formatting ### Naming & Formatting
- camelCase for functions, variables, object properties - camelCase for functions/vars/props, PascalCase for classes
- PascalCase for classes/constructors - Constants: `UPPER_SNAKE_CASE`, Files: `snake_case.js` or `kebab-case.js`
- Constants: `UPPER_SNAKE_CASE` (e.g., `CONVERTED_TYPE`)
- Files: `snake_case.js` or `kebab-case.js`
- 2-space indentation preferred (follow existing file conventions) - 2-space indentation preferred (follow existing file conventions)
- Vue Single File Components: `<script setup lang="ts">` preferred
### Widget Development ### Widget Development
- Use `app.registerExtension()` to register ComfyUI extensions - ComfyUI: `app.registerExtension()`, `node.addDOMWidget(name, type, element, options)`
- Use `node.addDOMWidget(name, type, element, options)` for custom widgets - Event handlers via `addEventListener` or widget callbacks
- Event handlers attached via `addEventListener` or widget callbacks - Shared utilities: `web/comfyui/utils.js`
- See `web/comfyui/utils.js` for shared utilities
### Vue Composables Pattern
- Use composition API: `useXxxState(widget)`, return reactive refs and methods
- Guard restoration loops with flag: `let isRestoring = false`
- Build config from state: `const buildConfig = (): Config => { ... }`
## Architecture Patterns ## Architecture Patterns
### Service Layer ### Service Layer
- Use `ServiceRegistry` singleton for dependency injection - `ServiceRegistry` singleton for DI, services use `get_instance()` classmethod
- Services follow singleton pattern via `get_instance()` class method
- Separate scanners (discovery) from services (business logic) - Separate scanners (discovery) from services (business logic)
- Handlers in `py/routes/handlers/` implement route logic - Handlers in `py/routes/handlers/` are pure functions with deps as params
### Model Types ### Model Types & Routes
- BaseModelService is abstract base for LoRA, Checkpoint, Embedding services - `BaseModelService` base for LoRA, Checkpoint, Embedding
- ModelScanner provides file discovery and hash-based deduplication - `ModelScanner` for file discovery, hash deduplication
- Persistent cache in SQLite via `PersistentModelCache` - `PersistentModelCache` (SQLite) for persistence
- Metadata sync from CivitAI/CivArchive via `MetadataSyncService` - Route registrars: `ModelRouteRegistrar`, endpoints: `/loras/*`, `/checkpoints/*`, `/embeddings/*`
- WebSocket via `WebSocketManager` for real-time updates
### Routes & Handlers
- Route registrars organize endpoints by domain: `ModelRouteRegistrar`, etc.
- Handlers are pure functions taking dependencies as parameters
- Use `WebSocketManager` for real-time progress updates
- Return `aiohttp.web.json_response` or `web.Response`
### Recipe System ### Recipe System
- Base metadata in `py/recipes/base.py` - Base: `py/recipes/base.py`, Enrichment: `RecipeEnrichmentService`
- Enrichment adds model metadata: `RecipeEnrichmentService` - Parsers: `py/recipes/parsers/`
- Parsers for different formats in `py/recipes/parsers/`
## Important Notes ## Important Notes
- Always use English for comments (per copilot-instructions.md) - ALWAYS use English for comments (per copilot-instructions.md)
- Dual mode: ComfyUI plugin (uses folder_paths) vs standalone (reads settings.json) - Dual mode: ComfyUI plugin (folder_paths) vs standalone (settings.json)
- Detection: `os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1"` - Detection: `os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1"`
- Settings auto-saved in user directory or portable mode
- WebSocket broadcasts for real-time updates (downloads, scans)
- Symlink handling requires normalized paths
- API endpoints follow `/loras/*`, `/checkpoints/*`, `/embeddings/*` patterns
- Run `python scripts/sync_translation_keys.py` after UI string updates - Run `python scripts/sync_translation_keys.py` after UI string updates
- Symlinks require normalized paths
## Frontend UI Architecture ## Frontend UI Architecture
This project has two distinct UI systems: ### 1. Standalone Web UI
### 1. Standalone Lora Manager Web UI
- Location: `./static/` and `./templates/` - Location: `./static/` and `./templates/`
- Purpose: Full-featured web application for managing LoRA models - Tech: Vanilla JS + CSS, served by standalone server
- Tech stack: Vanilla JS + CSS, served by the standalone server - Tests via npm in root directory
- Development: Uses npm for frontend testing (`npm test`, `npm run test:watch`, etc.)
### 2. ComfyUI Custom Node Widgets ### 2. ComfyUI Custom Node Widgets
- Location: `./web/comfyui/` - Location: `./web/comfyui/` (Vanilla JS) + `./vue-widgets/` (Vue)
- Purpose: Widgets and UI logic that ComfyUI loads as custom node extensions - Primary styles: `./web/comfyui/lm_styles.css` (NOT `./static/css/`)
- Tech stack: Vanilla JS + Vue.js widgets (in `./vue-widgets/` and built to `./web/comfyui/vue-widgets/`) - Vue builds to `./web/comfyui/vue-widgets/`, typecheck via `vue-tsc`
- Widget styling: Primary styles in `./web/comfyui/lm_styles.css` (NOT `./static/css/`)
- Development: No npm build step for these widgets (Vue widgets use build system)
### Widget Development Guidelines
- Use `app.registerExtension()` to register ComfyUI extensions (ComfyUI integration layer)
- Use `node.addDOMWidget()` for custom DOM widgets
- Widget styles should follow the patterns in `./web/comfyui/lm_styles.css`
- Selected state: `rgba(66, 153, 225, 0.3)` background, `rgba(66, 153, 225, 0.6)` border
- Hover state: `rgba(66, 153, 225, 0.2)` background
- Color palette matches the Lora Manager accent color (blue #4299e1)
- Use oklch() for color values when possible (defined in `./static/css/base.css`)
- Vue widget components are in `./vue-widgets/src/components/` and built to `./web/comfyui/vue-widgets/`
- When modifying widget styles, check `./web/comfyui/lm_styles.css` for consistency with other ComfyUI widgets

276
CLAUDE.md
View File

@@ -8,17 +8,22 @@ ComfyUI LoRA Manager is a comprehensive LoRA management system for ComfyUI that
## Development Commands ## Development Commands
### Backend Development ### Backend
```bash
# Install dependencies
pip install -r requirements.txt
# Install development dependencies (for testing) ```bash
pip install -r requirements.txt
pip install -r requirements-dev.txt pip install -r requirements-dev.txt
# Run standalone server (port 8188 by default) # Run standalone server (port 8188 by default)
python standalone.py --port 8188 python standalone.py --port 8188
# Run all backend tests
pytest
# Run specific test file or function
pytest tests/test_recipes.py
pytest tests/test_recipes.py::test_function_name
# Run backend tests with coverage # Run backend tests with coverage
COVERAGE_FILE=coverage/backend/.coverage pytest \ COVERAGE_FILE=coverage/backend/.coverage pytest \
--cov=py \ --cov=py \
@@ -27,185 +32,158 @@ COVERAGE_FILE=coverage/backend/.coverage pytest \
--cov-report=html:coverage/backend/html \ --cov-report=html:coverage/backend/html \
--cov-report=xml:coverage/backend/coverage.xml \ --cov-report=xml:coverage/backend/coverage.xml \
--cov-report=json:coverage/backend/coverage.json --cov-report=json:coverage/backend/coverage.json
# Run specific test file
pytest tests/test_recipes.py
``` ```
### Frontend Development ### Frontend
```bash
# Install frontend dependencies
npm install
# Run frontend tests There are three test suites run by `npm test`: vanilla JS tests (vitest at root) and Vue widget tests (`vue-widgets/` vitest).
```bash
npm install
cd vue-widgets && npm install && cd ..
# Run all frontend tests (JS + Vue)
npm test npm test
# Run frontend tests in watch mode # Run only vanilla JS tests
npm run test:js
# Run only Vue widget tests
npm run test:vue
# Watch mode (JS tests only)
npm run test:watch npm run test:watch
# Run frontend tests with coverage # Frontend coverage
npm run test:coverage npm run test:coverage
# Build Vue widgets (output to web/comfyui/vue-widgets/)
cd vue-widgets && npm run build
# Vue widget dev mode (watch + rebuild)
cd vue-widgets && npm run dev
# Typecheck Vue widgets
cd vue-widgets && npm run typecheck
``` ```
### Localization ### Localization
```bash ```bash
# Sync translation keys after UI string updates # Sync translation keys after UI string updates
python scripts/sync_translation_keys.py python scripts/sync_translation_keys.py
``` ```
Locale files are in `locales/` (en, zh-CN, zh-TW, ja, ko, fr, de, es, ru, he).
## Architecture ## Architecture
### Backend Structure (Python) ### Dual Mode Operation
**Core Entry Points:** The system runs in two modes:
- `__init__.py` - ComfyUI plugin entry point, registers nodes and routes - **ComfyUI plugin mode**: Integrates with ComfyUI's PromptServer, uses `folder_paths` for model discovery
- `standalone.py` - Standalone server that mocks ComfyUI dependencies - **Standalone mode**: `standalone.py` mocks ComfyUI dependencies, reads paths from `settings.json`
- `py/lora_manager.py` - Main LoraManager class that registers HTTP routes
**Service Layer** (`py/services/`):
- `ServiceRegistry` - Singleton service registry for dependency management
- `ModelServiceFactory` - Factory for creating model services (LoRA, Checkpoint, Embedding)
- Scanner services (`lora_scanner.py`, `checkpoint_scanner.py`, `embedding_scanner.py`) - Model file discovery and indexing
- `model_scanner.py` - Base scanner with hash-based deduplication and metadata extraction
- `persistent_model_cache.py` - SQLite-based cache for model metadata
- `metadata_sync_service.py` - Syncs metadata from CivitAI/CivArchive APIs
- `civitai_client.py` / `civarchive_client.py` - API clients for external services
- `downloader.py` / `download_manager.py` - Model download orchestration
- `recipe_scanner.py` - Recipe file management and image association
- `settings_manager.py` - Application settings with migration support
- `websocket_manager.py` - WebSocket broadcasting for real-time updates
- `use_cases/` - Business logic orchestration (auto-organize, bulk refresh, downloads)
**Routes Layer** (`py/routes/`):
- Route registrars organize endpoints by domain (models, recipes, previews, example images, updates)
- `handlers/` - Request handlers implementing business logic
- Routes use aiohttp and integrate with ComfyUI's PromptServer
**Recipe System** (`py/recipes/`):
- `base.py` - Base recipe metadata structure
- `enrichment.py` - Enriches recipes with model metadata
- `merger.py` - Merges recipe data from multiple sources
- `parsers/` - Parsers for different recipe formats (PNG, JSON, workflow)
**Custom Nodes** (`py/nodes/`):
- `lora_loader.py` - LoRA loader nodes with preset support
- `save_image.py` - Enhanced save image with pattern-based filenames
- `trigger_word_toggle.py` - Toggle trigger words in prompts
- `lora_stacker.py` - Stack multiple LoRAs
- `prompt.py` - Prompt node with autocomplete
- `wanvideo_lora_select.py` - WanVideo-specific LoRA selection
**Configuration** (`py/config.py`):
- Manages folder paths for models, checkpoints, embeddings
- Handles symlink mappings for complex directory structures
- Auto-saves paths to settings.json in ComfyUI mode
### Frontend Structure (JavaScript)
**ComfyUI Widgets** (`web/comfyui/`):
- Vanilla JavaScript ES modules extending ComfyUI's LiteGraph-based UI
- `loras_widget.js` - Main LoRA selection widget with preview
- `loras_widget_events.js` - Event handling for widget interactions
- `autocomplete.js` - Autocomplete for trigger words and embeddings
- `preview_tooltip.js` - Preview tooltip for model cards
- `top_menu_extension.js` - Adds "Launch LoRA Manager" menu item
- `trigger_word_highlight.js` - Syntax highlighting for trigger words
- `utils.js` - Shared utilities and API helpers
**Widget Development:**
- Widgets use `app.registerExtension` and `getCustomWidgets` hooks
- `node.addDOMWidget(name, type, element, options)` embeds HTML in nodes
- See `docs/dom_widget_dev_guide.md` for complete DOMWidget development guide
**Web Source** (`web-src/`):
- Modern frontend components (if migrating from static)
- `components/` - Reusable UI components
- `styles/` - CSS styling
### Key Patterns
**Dual Mode Operation:**
- ComfyUI plugin mode: Integrates with ComfyUI's PromptServer, uses folder_paths
- Standalone mode: Mocks ComfyUI dependencies via `standalone.py`, reads paths from settings.json
- Detection: `os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1"` - Detection: `os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1"`
**Settings Management:** ### Backend (Python)
- Settings stored in user directory (via `platformdirs`) or portable mode (in repo)
- Migration system tracks settings schema version
- Template in `settings.json.example` defines defaults
**Model Scanning Flow:** **Entry points:**
1. Scanner walks folder paths, computes file hashes - `__init__.py` — ComfyUI plugin entry: registers nodes via `NODE_CLASS_MAPPINGS`, sets `WEB_DIRECTORY`, calls `LoraManager.add_routes()`
2. Hash-based deduplication prevents duplicate processing - `standalone.py` — Standalone server: mocks `folder_paths` and node modules, starts aiohttp server
3. Metadata extracted from safetensors headers - `py/lora_manager.py` — Main `LoraManager` class that registers all HTTP routes
4. Persistent cache stores results in SQLite
5. Background sync fetches CivitAI/CivArchive metadata
6. WebSocket broadcasts updates to connected clients
**Recipe System:** **Service layer** (`py/services/`):
- Recipes store LoRA combinations with parameters - `ServiceRegistry` singleton for dependency injection; services follow `get_instance()` singleton pattern
- Supports import from workflow JSON, PNG metadata - `BaseModelService` abstract base → `LoraService`, `CheckpointService`, `EmbeddingService`
- Images associated with recipes via sibling file detection - `ModelScanner` base → `LoraScanner`, `CheckpointScanner`, `EmbeddingScanner` for file discovery with hash-based deduplication
- Enrichment adds model metadata for display - `PersistentModelCache` — SQLite-based metadata cache
- `MetadataSyncService` — Background sync from CivitAI/CivArchive APIs
- `SettingsManager` — Settings with schema migration support
- `WebSocketManager` — Real-time progress broadcasting
- `ModelServiceFactory` — Creates the right service for each model type
- Use cases in `py/services/use_cases/` orchestrate complex business logic (auto-organize, bulk refresh, downloads)
**Frontend-Backend Communication:** **Routes** (`py/routes/`):
- REST API for CRUD operations - Route registrars organize endpoints by domain: `ModelRouteRegistrar`, `RecipeRouteRegistrar`, etc.
- WebSocket for real-time progress updates (downloads, scans) - Request handlers in `py/routes/handlers/` implement route logic
- API endpoints follow `/loras/*` pattern - API endpoints follow `/loras/*`, `/checkpoints/*`, `/embeddings/*` patterns
- All routes use aiohttp, return `web.json_response` or `web.Response`
**Recipe system** (`py/recipes/`):
- `base.py` — Recipe metadata structure
- `enrichment.py` — Enriches recipes with model metadata
- `parsers/` — Parsers for PNG metadata, JSON, and workflow formats
**Custom nodes** (`py/nodes/`):
- Each node class has a `NAME` class attribute used as key in `NODE_CLASS_MAPPINGS`
- Standard ComfyUI node pattern: `INPUT_TYPES()` classmethod, `RETURN_TYPES`, `FUNCTION`
- All nodes registered in `__init__.py`
**Configuration** (`py/config.py`):
- Manages folder paths for models, handles symlink mappings
- Auto-saves paths to settings.json in ComfyUI mode
### Frontend — Two Distinct UI Systems
#### 1. Standalone Manager Web UI
- **Location:** `static/` (JS/CSS) and `templates/` (HTML)
- **Tech:** Vanilla JS + CSS, served by standalone server
- **Structure:** `static/js/core.js` (shared), `loras.js`, `checkpoints.js`, `embeddings.js`, `recipes.js`, `statistics.js`
- **Tests:** `tests/frontend/**/*.test.js` (vitest + jsdom)
#### 2. ComfyUI Custom Node Widgets
- **Vanilla JS widgets:** `web/comfyui/*.js` — ES modules extending ComfyUI's LiteGraph UI
- `loras_widget.js` / `loras_widget_events.js` — Main LoRA selection widget
- `autocomplete.js` — Trigger word and embedding autocomplete
- `preview_tooltip.js` — Model card preview tooltips
- `top_menu_extension.js` — "Launch LoRA Manager" menu item
- `utils.js` — Shared utilities and API helpers
- Widget styling in `web/comfyui/lm_styles.css` (NOT `static/css/`)
- **Vue widgets:** `vue-widgets/src/` → built to `web/comfyui/vue-widgets/`
- Vue 3 + TypeScript + PrimeVue + vue-i18n
- Vite build with CSS-injected-by-JS plugin
- Components: `LoraPoolWidget`, `LoraRandomizerWidget`, `LoraCyclerWidget`, `AutocompleteTextWidget`
- Auto-built on ComfyUI startup via `py/vue_widget_builder.py`
- Tests: `vue-widgets/tests/**/*.test.ts` (vitest)
**Widget registration pattern:**
- Widgets use `app.registerExtension()` and `getCustomWidgets` hooks
- `node.addDOMWidget(name, type, element, options)` embeds HTML in LiteGraph nodes
- See `docs/dom_widget_dev_guide.md` for DOMWidget development guide
## Code Style ## Code Style
**Python:** **Python:**
- PEP 8 with 4-space indentation - PEP 8, 4-space indentation, English comments only
- snake_case for files, functions, variables - Use `from __future__ import annotations` for forward references
- PascalCase for classes - Use `TYPE_CHECKING` guard for type-checking-only imports
- Type hints preferred
- English comments only (per copilot-instructions.md)
- Loggers via `logging.getLogger(__name__)` - Loggers via `logging.getLogger(__name__)`
- Custom exceptions in `py/services/errors.py`
- Async patterns: `async def` for I/O, `@pytest.mark.asyncio` for async tests
- Singleton pattern with class-level `asyncio.Lock` (see `ModelScanner.get_instance()`)
**JavaScript:** **JavaScript:**
- ES modules with camelCase - ES modules, camelCase functions/variables, PascalCase classes
- Files use `*_widget.js` suffix for ComfyUI widgets - Widget files use `*_widget.js` suffix
- Prefer vanilla JS, avoid framework dependencies - Prefer vanilla JS for `web/comfyui/` widgets, avoid framework dependencies (except Vue widgets)
## Testing ## Testing
**Backend Tests:** **Backend (pytest):**
- pytest with `--import-mode=importlib` - Config in `pytest.ini`: `--import-mode=importlib`, testpaths=`tests`
- Test files: `tests/test_*.py` - Fixtures in `tests/conftest.py` handle ComfyUI dependency mocking
- Fixtures in `tests/conftest.py` - Markers: `@pytest.mark.asyncio`, `@pytest.mark.no_settings_dir_isolation`
- Mock ComfyUI dependencies using standalone.py patterns - Uses `tmp_path_factory` for directory isolation
- Markers: `@pytest.mark.asyncio` for async tests, `@pytest.mark.no_settings_dir_isolation` for real paths
**Frontend Tests:** **Frontend (vitest):**
- Vitest with jsdom environment - Vanilla JS tests: `tests/frontend/**/*.test.js` with jsdom
- Test files: `tests/frontend/**/*.test.js` - Vue widget tests: `vue-widgets/tests/**/*.test.ts` with jsdom + @vue/test-utils
- Setup in `tests/frontend/setup.js` - Setup in `tests/frontend/setup.js`
- Coverage via `npm run test:coverage`
## Important Notes ## Key Integration Points
**Settings Location:** - **Settings:** Stored in user directory (via `platformdirs`) or portable mode (`"use_portable_settings": true`)
- ComfyUI mode: Auto-saves folder paths to user settings directory - **CivitAI/CivArchive:** API clients for metadata sync and model downloads; CivitAI API key in settings
- Standalone mode: Use `settings.json` (copy from `settings.json.example`) - **Symlink handling:** Config scans symlinks to map virtual→physical paths; fingerprinting prevents redundant rescans
- Portable mode: Set `"use_portable_settings": true` in settings.json - **WebSocket:** Broadcasts real-time progress for downloads, scans, and metadata sync
- **Model scanning flow:** Walk folders → compute hashes → deduplicate → extract safetensors metadata → cache in SQLite → background CivitAI sync → WebSocket broadcast
**API Integration:**
- CivitAI API key required for downloads (add to settings)
- CivArchive API used as fallback for deleted models
- Metadata archive database available for offline metadata
**Symlink Handling:**
- Config scans symlinks to map virtual paths to physical locations
- Preview validation uses normalized preview root paths
- Fingerprinting prevents redundant symlink rescans
**ComfyUI Node Development:**
- Nodes defined in `py/nodes/`, registered in `__init__.py`
- Frontend widgets in `web/comfyui/`, matched by node type
- Use `WEB_DIRECTORY = "./web/comfyui"` convention
**Recipe Image Association:**
- Recipes scan for sibling images in same directory
- Supports repair/migration of recipe image paths
- See `py/services/recipe_scanner.py` for implementation details