diff --git a/README.zh-CN.md b/README.zh-CN.md index 640baa2..ac1d6ac 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -245,6 +245,7 @@ ALLOW_UNAUTHENTICATED=true .venv/bin/granian --interface asgi backend.app.main:a | `PROMPT_OPTIMIZER_*` | 可选提示词优化器配置。 | | `AI_ASSISTANT_*` | AI Assistant 默认启用;如需关闭,设置 `AI_ASSISTANT_ENABLED=false`。API URL、密钥、文本模型、超时、路径和 host allowlist 复用 `PROMPT_OPTIMIZER_*`。`AI_ASSISTANT_MAX_CONCURRENCY` 限制并发上游 Assistant 调用,`AI_ASSISTANT_BATCH_MAX_IMAGES` 限制单次 Gallery AI 批量分析图片数。 | | `R2_*` | 可选 Cloudflare R2 Gallery 备份配置;自定义 endpoint host 需要配置 `R2_ENDPOINT_HOST_ALLOWLIST`。 | +| `NODEIMAGE_API_KEY` | 可选 NodeImage API key,用于服务端 Gallery 图片上传;也可在 Web Settings 中配置 env ref。 | | `PUBLIC_ORIGIN` / `ALLOWED_HOSTS` | 反向代理 Host/CSRF 加固。 | | `ENABLE_NGINX_ACCEL_REDIRECT` / `PUBLIC_IMAGE_BASE_URL` / `PUBLIC_THUMBNAIL_BASE_URL` | 可选 nginx/CDN 图片字节服务行为。 | | `GRANIAN_*` | 生产运行时进程、线程和静态资源调优。 | @@ -262,10 +263,10 @@ Overall Config 会把 override 持久化到 SQLite。部分配置可热更新; 3. 打开 Settings。 4. 创建或选择 API 预设。 5. 设置 API base URL、API path、模型、response format 和 API key/env ref。 -6. 按需配置 SOCKS5 代理、webhook、提示词优化器、AI Assistant、R2 备份或 Overall Config override。 +6. 按需配置 SOCKS5 代理、webhook、提示词优化器、AI Assistant、R2 备份、NodeImage 上传或 Overall Config override。 7. 保存预设,必要时执行健康检查。 8. 输入 prompt 生成图片,或上传/选择源图执行编辑。 -9. 在 Gallery 中复用参数、筛选、收藏、批量操作、导入导出或执行 R2 同步。 +9. 在 Gallery 中复用参数、筛选、收藏、批量操作、导入导出、执行 R2 同步或上传到 NodeImage。 ## 支持的上游路径 diff --git a/README.zh-TW.md b/README.zh-TW.md index 969f932..085146e 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -245,6 +245,7 @@ ALLOW_UNAUTHENTICATED=true .venv/bin/granian --interface asgi backend.app.main:a | `PROMPT_OPTIMIZER_*` | 選用的伺服器端提示詞最佳化器設定。 | | `AI_ASSISTANT_*` | AI Assistant 預設啟用;可設 `AI_ASSISTANT_ENABLED=false` 關閉。API URL、密鑰、文字模型、逾時、路徑與 Host 允許清單沿用 `PROMPT_OPTIMIZER_*`。`AI_ASSISTANT_MAX_CONCURRENCY` 限制同時進行的上游 Assistant 呼叫,`AI_ASSISTANT_BATCH_MAX_IMAGES` 限制單次 Gallery AI 批次處理的圖片數。 | | `R2_*` | 選用的 Cloudflare R2 Gallery 備份設定;自訂 Endpoint Host 須設定 `R2_ENDPOINT_HOST_ALLOWLIST`。 | +| `NODEIMAGE_API_KEY` | 選用的 NodeImage API key,用於從伺服器上傳 Gallery 圖片;也可在 Web Settings 中設定 env ref。 | | `PUBLIC_ORIGIN` / `ALLOWED_HOSTS` | Reverse Proxy Host/CSRF 強化。 | | `ENABLE_NGINX_ACCEL_REDIRECT` / `PUBLIC_IMAGE_BASE_URL` / `PUBLIC_THUMBNAIL_BASE_URL` | 選用的 nginx/CDN 圖片位元組傳送行為。 | | `GRANIAN_*` | 正式環境程序、執行緒與靜態資源調校。 | @@ -262,10 +263,10 @@ Overall Config 會將 Override 持久化至 SQLite。部分設定可熱更新; 3. 開啟 Settings。 4. 建立或選取 API 預設。 5. 設定 API base URL、API path、模型、response format 與 API key/env ref。 -6. 視需要設定 SOCKS5 Proxy、webhook、提示詞最佳化器、AI Assistant、R2 備份或 Overall Config Override。 +6. 視需要設定 SOCKS5 Proxy、webhook、提示詞最佳化器、AI Assistant、R2 備份、NodeImage 上傳或 Overall Config Override。 7. 儲存預設,必要時執行健康檢查。 8. 輸入 Prompt 生成圖片,或上傳/選取來源圖片進行編輯。 -9. 在 Gallery 中沿用參數、篩選、收藏、批次操作、匯入/匯出,或執行 R2 同步。 +9. 在 Gallery 中沿用參數、篩選、收藏、批次操作、匯入/匯出、執行 R2 同步,或上傳至 NodeImage。 ## 支援的上游路徑 diff --git a/VERSION b/VERSION index 66d62a8..c432e90 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.4.1 +v1.4.2 diff --git a/backend/app/api/app_state.py b/backend/app/api/app_state.py index efec9ab..ab767c1 100644 --- a/backend/app/api/app_state.py +++ b/backend/app/api/app_state.py @@ -239,7 +239,14 @@ async def lifespan(app: FastAPI): job_scheduler.run_image_unit_dispatcher(app.state.worker_id) ) app.state.gallery_export_lock = asyncio.Lock() - app.state.gallery_job_subscribers = {"export": {}, "export_direct": {}, "sync": {}, "import": {}, "ai_analyze": {}} + app.state.gallery_job_subscribers = { + "export": {}, + "export_direct": {}, + "sync": {}, + "import": {}, + "ai_analyze": {}, + "nodeimage_upload": {}, + } app.state.gallery_job_sse_poller_tasks = {} from ..services import gallery_jobs, gallery_maintenance from ..services import assistant_batch @@ -256,6 +263,9 @@ async def lifespan(app: FastAPI): app.state.gallery_import_dispatcher_task = asyncio.create_task( gallery_jobs.run_gallery_import_dispatcher(app.state.worker_id) ) + app.state.gallery_nodeimage_upload_dispatcher_task = asyncio.create_task( + gallery_jobs.run_gallery_nodeimage_upload_dispatcher(app.state.worker_id) + ) app.state.gallery_export_gc_task = asyncio.create_task( gallery_maintenance.gc_gallery_export_jobs(app.state.worker_id) ) @@ -311,6 +321,13 @@ async def lifespan(app: FastAPI): gallery_import_dispatcher_task = getattr(app.state, "gallery_import_dispatcher_task", None) if gallery_import_dispatcher_task and not gallery_import_dispatcher_task.done(): gallery_import_dispatcher_task.cancel() + gallery_nodeimage_upload_dispatcher_task = getattr( + app.state, + "gallery_nodeimage_upload_dispatcher_task", + None, + ) + if gallery_nodeimage_upload_dispatcher_task and not gallery_nodeimage_upload_dispatcher_task.done(): + gallery_nodeimage_upload_dispatcher_task.cancel() thumbnail_dispatcher_task = getattr(app.state, "thumbnail_dispatcher_task", None) if thumbnail_dispatcher_task and not thumbnail_dispatcher_task.done(): thumbnail_dispatcher_task.cancel() @@ -363,6 +380,7 @@ async def lifespan(app: FastAPI): gallery_export_dispatcher_task, gallery_sync_dispatcher_task, gallery_import_dispatcher_task, + gallery_nodeimage_upload_dispatcher_task, gc_task, file_gc_task, scheduled_sync_task, diff --git a/backend/app/api/presets.py b/backend/app/api/presets.py index 4eed9f2..ffde66e 100644 --- a/backend/app/api/presets.py +++ b/backend/app/api/presets.py @@ -18,6 +18,10 @@ from ..core.validators import ( normalize_webhook_url, resolve_env_var_ref, ) +from ..integrations.nodeimage.client import ( + NodeImageConfigurationError, + resolve_nodeimage_settings, +) from ..repositories.settings import ( load_ai_assistant_settings, load_prompt_optimizer_settings, @@ -83,7 +87,7 @@ def api_key_response_fields(api_key: str) -> dict: "api_key_env_var": env_var, "api_key_secret_id": None, } - if value: + if value in secrets.configured_secret_ids(): return { "api_key_masked": value, "has_api_key": True, @@ -91,6 +95,14 @@ def api_key_response_fields(api_key: str) -> dict: "api_key_env_var": None, "api_key_secret_id": value, } + if value: + return { + "api_key_masked": mask_key(value), + "has_api_key": True, + "api_key_source": "stored", + "api_key_env_var": None, + "api_key_secret_id": None, + } return { "api_key_masked": "***", "has_api_key": False, @@ -100,19 +112,6 @@ def api_key_response_fields(api_key: str) -> dict: } -def nodeimage_api_key_response_fields(api_key: str) -> dict: - value = str(api_key or "").strip() - if value and not get_api_key_env_var(value) and value not in secrets.configured_secret_ids(): - return { - "api_key_masked": mask_key(value), - "has_api_key": True, - "api_key_source": "stored", - "api_key_env_var": None, - "api_key_secret_id": None, - } - return api_key_response_fields(value) - - def secret_response_fields(value: str, prefix: str) -> dict: fields = api_key_response_fields(value) return { @@ -519,9 +518,15 @@ def build_r2_backup_settings_response(raw: dict | None) -> R2BackupSettingsRespo def build_nodeimage_settings_response(raw: dict | None) -> NodeImageSettingsResponse: settings = load_nodeimage_settings() if raw is None else raw + try: + resolve_nodeimage_settings(settings, require_enabled=False) + api_key_resolvable = True + except NodeImageConfigurationError: + api_key_resolvable = False return NodeImageSettingsResponse( enabled=bool(settings.get("enabled", False)), - **nodeimage_api_key_response_fields(str(settings.get("api_key") or "")), + api_key_resolvable=api_key_resolvable, + **api_key_response_fields(str(settings.get("api_key") or "")), ) diff --git a/backend/app/api/routers/edits.py b/backend/app/api/routers/edits.py index 60403d6..290fc1d 100644 --- a/backend/app/api/routers/edits.py +++ b/backend/app/api/routers/edits.py @@ -1,5 +1,4 @@ import asyncio -import mimetypes import os import tempfile from pathlib import Path @@ -15,14 +14,17 @@ from ...services.job_queue import ( queue_edit_job, ) from ..uploads import ( - IMAGE_UPLOAD_CONTENT_TYPES, is_image_upload, resolve_upload_content_type, validate_upload_image_bytes, ) from ...core import settings as config from ...repositories.gallery.queries import get_gallery_entry -from ...repositories.image_files import safe_image_path, validate_image_file +from ...repositories.image_files import ( + image_content_type_for_filename, + safe_image_path, + validate_image_file, +) from ...schemas.generation import EditRequest, GenerateJobResponse from ...services.blocking import run_db_operation, run_image_operation @@ -251,10 +253,7 @@ async def read_gallery_edit_source(image_id: str) -> EditImageSource: if not path or not path.exists(): raise HTTPException(status_code=404, detail="Gallery image file not found") - image_content_type = ( - mimetypes.guess_type(path.name)[0] - or IMAGE_UPLOAD_CONTENT_TYPES.get(path.suffix.lower(), "application/octet-stream") - ) + image_content_type = image_content_type_for_filename(path.name) return await run_image_operation( copy_edit_source_file_to_temp, diff --git a/backend/app/api/routers/gallery_batch.py b/backend/app/api/routers/gallery_batch.py index 8e52e19..02a99d7 100644 --- a/backend/app/api/routers/gallery_batch.py +++ b/backend/app/api/routers/gallery_batch.py @@ -2,7 +2,6 @@ import asyncio import hashlib import inspect import logging -import mimetypes import os import time import uuid @@ -83,8 +82,7 @@ from ...repositories.image_files import ( safe_image_path, safe_thumbnail_path, ) -from ...repositories.settings import load_r2_backup_settings -from ...repositories.settings import load_nodeimage_settings +from ...repositories.settings import load_nodeimage_settings, load_r2_backup_settings from ...repositories.thumbnail_jobs import ( THUMBNAIL_JOB_LEASE_SECONDS, claim_next_thumbnail_job, @@ -109,20 +107,11 @@ from ...schemas.gallery import ( GallerySyncRequest, GallerySyncJobStatus, ) -from ...schemas.nodeimage import ( - NodeImageBatchUploadItem, - NodeImageBatchUploadResponse, -) -from ...integrations.nodeimage.client import ( - NodeImageAuthError, - NodeImageConfigurationError, - NodeImageUploadError, - resolve_nodeimage_settings, - upload_image_bytes, -) +from ...services.gallery_maintenance import kick_gallery_job_dispatchers from ...services.gallery_common import * from ...services.gallery_jobs import * from ...services.gallery_maintenance import kick_gallery_file_gc +from ...schemas.nodeimage import NodeImageUploadJobStatus router = APIRouter() logger = logging.getLogger(__name__) @@ -293,7 +282,8 @@ async def download_gallery_batch(req: GalleryBatchRequest): @router.post( "/api/gallery/batch/nodeimage-upload", - response_model=NodeImageBatchUploadResponse, + response_model=NodeImageUploadJobStatus, + status_code=202, ) async def upload_gallery_batch_to_nodeimage(req: GalleryBatchRequest): ids, entries, requested_count, missing_ids = await _resolve_nodeimage_batch_ids(req) @@ -301,120 +291,19 @@ async def upload_gallery_batch_to_nodeimage(req: GalleryBatchRequest): raise HTTPException(status_code=404, detail="Gallery entries not found") try: - effective = resolve_nodeimage_settings( + resolve_nodeimage_settings( await asyncio.to_thread(load_nodeimage_settings) ) except NodeImageConfigurationError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc - auth_error = "NodeImage API key was rejected." - auth_failed = asyncio.Event() - semaphore = asyncio.Semaphore(4) - - def error_item(entry: GalleryEntry, error: str) -> NodeImageBatchUploadItem: - return NodeImageBatchUploadItem( - image_id=entry.id, - filename=entry.filename, - status="error", - error=error, - ) - - async def read_entry( - entry: GalleryEntry, - ) -> tuple[Path | None, bytes | None, NodeImageBatchUploadItem | None]: - path = safe_image_path(entry.filename) - if not path: - return None, None, error_item(entry, "Image file not found") - try: - if not path.exists(): - return None, None, error_item(entry, "Image file not found") - return path, await asyncio.to_thread(path.read_bytes), None - except OSError: - return path, None, error_item(entry, "Image file could not be read") - - async def upload_entry( - entry: GalleryEntry, - *, - prepared: tuple[Path, bytes] | None = None, - ) -> NodeImageBatchUploadItem: - async with semaphore: - if auth_failed.is_set(): - return error_item(entry, auth_error) - try: - if prepared is None: - path, image_bytes, read_error = await read_entry(entry) - if read_error: - return read_error - assert path is not None and image_bytes is not None - else: - path, image_bytes = prepared - if auth_failed.is_set(): - return error_item(entry, auth_error) - result = await upload_image_bytes(image_bytes, path.name, effective) - return NodeImageBatchUploadItem( - image_id=entry.id, - filename=entry.filename, - status="ok", - url=result.url, - markdown=result.markdown, - ) - except NodeImageAuthError: - auth_failed.set() - return error_item(entry, auth_error) - except NodeImageUploadError as exc: - return error_item(entry, str(exc)) - except Exception as exc: - logger.error( - "Unexpected NodeImage batch upload failure for gallery entry %s (%s)", - entry.id, - type(exc).__name__, - ) - return error_item(entry, "Unexpected upload failure") - - result_by_id: dict[str, NodeImageBatchUploadItem] = {} - probe_entry: GalleryEntry | None = None - probe_file: tuple[Path, bytes] | None = None - for entry in entries: - path, image_bytes, read_error = await read_entry(entry) - if read_error: - result_by_id[entry.id] = read_error - continue - assert path is not None and image_bytes is not None - probe_entry = entry - probe_file = (path, image_bytes) - break - - if probe_entry is not None and probe_file is not None: - probe_result = await upload_entry(probe_entry, prepared=probe_file) - result_by_id[probe_entry.id] = probe_result - remaining_entries = [ - entry for entry in entries if entry.id not in result_by_id - ] - if auth_failed.is_set(): - for entry in remaining_entries: - _path, _image_bytes, read_error = await read_entry(entry) - result_by_id[entry.id] = read_error or error_item(entry, auth_error) - else: - uploaded_items = await asyncio.gather( - *(upload_entry(entry) for entry in remaining_entries) - ) - result_by_id.update({item.image_id: item for item in uploaded_items}) - - for missing_id in missing_ids: - result_by_id[missing_id] = NodeImageBatchUploadItem( - image_id=missing_id, - filename=None, - status="error", - error="Gallery entry not found", - ) - results = [result_by_id[image_id] for image_id in ids if image_id in result_by_id] - uploaded_count = sum(item.status == "ok" for item in results) - return NodeImageBatchUploadResponse( - requested_count=requested_count, - uploaded_count=uploaded_count, - failed_count=len(results) - uploaded_count, - results=results, + job = await _create_reserved_nodeimage_upload_job( + ids, + requested_count, + missing_ids, ) + kick_gallery_job_dispatchers() + return NodeImageUploadJobStatus(**_nodeimage_upload_payload(job)) @router.delete("/api/gallery", response_model=MessageResponse) diff --git a/backend/app/api/routers/gallery_queries.py b/backend/app/api/routers/gallery_queries.py index e83bf2d..8b3498d 100644 --- a/backend/app/api/routers/gallery_queries.py +++ b/backend/app/api/routers/gallery_queries.py @@ -2,7 +2,6 @@ import asyncio import hashlib import inspect import logging -import mimetypes import os import time import uuid @@ -81,11 +80,11 @@ from ...repositories.gallery.sync_state import ( ) from ...repositories.image_files import ( THUMBNAIL_CONTENT_TYPE, + image_content_type_for_filename, safe_image_path, safe_thumbnail_path, ) -from ...repositories.settings import load_r2_backup_settings -from ...repositories.settings import load_nodeimage_settings +from ...repositories.settings import load_nodeimage_settings, load_r2_backup_settings from ...repositories.thumbnail_jobs import ( THUMBNAIL_JOB_LEASE_SECONDS, claim_next_thumbnail_job, @@ -119,7 +118,7 @@ from ...integrations.nodeimage.client import ( NodeImageConfigurationError, NodeImageUploadError, resolve_nodeimage_settings, - upload_image_bytes, + upload_image_file, ) from ...services.gallery_common import * from ...services.gallery_maintenance import kick_thumbnail_dispatcher @@ -303,22 +302,23 @@ async def upload_gallery_item_to_nodeimage(image_id: str): if not entry: raise HTTPException(status_code=404, detail="Gallery entry not found") - path = await asyncio.to_thread(_resolve_gallery_image_path, entry.filename) - if not path: + path = safe_image_path(entry.filename) + if not path or not path.exists(): raise HTTPException(status_code=404, detail="Image file not found") try: effective = resolve_nodeimage_settings( await asyncio.to_thread(load_nodeimage_settings) ) - image_bytes = await asyncio.to_thread(path.read_bytes) - result = await upload_image_bytes(image_bytes, path.name, effective) + result = await upload_image_file(path, path.name, effective) except NodeImageConfigurationError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc except NodeImageAuthError as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc except NodeImageUploadError as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc + except FileNotFoundError as exc: + raise HTTPException(status_code=404, detail="Image file not found") from exc except OSError as exc: raise HTTPException(status_code=404, detail="Image file could not be read") from exc @@ -338,7 +338,7 @@ async def _image_file_response(filename: str, *, download: bool = False): if not path: raise HTTPException(status_code=404, detail="Image not found") - media_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream" + media_type = image_content_type_for_filename(path.name) if config.ENABLE_NGINX_ACCEL_REDIRECT: return _x_accel_response( path, diff --git a/backend/app/api/routers/gallery_tasks.py b/backend/app/api/routers/gallery_tasks.py index 839734c..b37a3ce 100644 --- a/backend/app/api/routers/gallery_tasks.py +++ b/backend/app/api/routers/gallery_tasks.py @@ -2,7 +2,6 @@ import asyncio import hashlib import inspect import logging -import mimetypes import os import uuid from collections.abc import Iterable, Iterator @@ -45,6 +44,7 @@ from ...repositories.coordination import ( get_gallery_job, get_gallery_jobs_updated_at_edges, list_gallery_job_ids_with_files, + request_gallery_job_cancellation, release_background_lease, release_import_upload_reservation, reserve_gallery_job_capacity, @@ -109,6 +109,7 @@ from ...schemas.gallery import ( GallerySyncRequest, GallerySyncJobStatus, ) +from ...schemas.nodeimage import NodeImageUploadJobStatus from ...services.gallery_common import * from ...services.gallery_jobs import * from ...services.gallery_maintenance import ( @@ -349,6 +350,49 @@ async def stream_gallery_import_job(job_id: str, request: Request): ) +@router.get("/api/gallery/nodeimage-upload-jobs/{job_id}", response_model=NodeImageUploadJobStatus) +async def get_gallery_nodeimage_upload_job(job_id: str): + job = await asyncio.to_thread(get_gallery_job, NODEIMAGE_UPLOAD_JOB_KIND, job_id) + if not job: + raise HTTPException(status_code=404, detail="NodeImage upload job not found") + return NodeImageUploadJobStatus(**_nodeimage_upload_payload(job)) + + +@router.get("/api/gallery/nodeimage-upload-jobs/{job_id}/events") +async def stream_gallery_nodeimage_upload_job(job_id: str, request: Request): + return await stream_gallery_job( + kind=NODEIMAGE_UPLOAD_JOB_KIND, + job_id=job_id, + request=request, + event_name="nodeimage_upload", + terminal_statuses=NODEIMAGE_UPLOAD_TERMINAL_STATUSES, + payload_builder=_nodeimage_upload_payload, + not_found_detail="NodeImage upload job not found", + ) + + +async def _cancel_gallery_nodeimage_upload_job(job_id: str): + job = await asyncio.to_thread( + request_gallery_job_cancellation, + NODEIMAGE_UPLOAD_JOB_KIND, + job_id, + ) + if not job: + raise HTTPException(status_code=404, detail="NodeImage upload job not found") + _publish_gallery_job_sse(job) + return NodeImageUploadJobStatus(**_nodeimage_upload_payload(job)) + + +@router.delete("/api/gallery/nodeimage-upload-jobs/{job_id}", response_model=NodeImageUploadJobStatus) +async def cancel_gallery_nodeimage_upload_job(job_id: str): + return await _cancel_gallery_nodeimage_upload_job(job_id) + + +@router.post("/api/gallery/nodeimage-upload-jobs/{job_id}/cancel", response_model=NodeImageUploadJobStatus) +async def request_gallery_nodeimage_upload_cancellation(job_id: str): + return await _cancel_gallery_nodeimage_upload_job(job_id) + + @router.get("/api/download-all") async def download_all_images(export_job_id: str | None = Query(default=None)): gallery_count = await asyncio.to_thread(get_gallery_count) diff --git a/backend/app/api/routers/settings.py b/backend/app/api/routers/settings.py index 321d528..8f9a35e 100644 --- a/backend/app/api/routers/settings.py +++ b/backend/app/api/routers/settings.py @@ -16,7 +16,9 @@ from ..presets import ( get_active_preset, get_api_key_env_var, get_api_presets, + get_effective_preset_api_key, get_exception_message, + get_nodeimage_settings, get_preset_by_id, get_upstream_socks5_proxy, get_webhook_url, @@ -339,8 +341,6 @@ async def update_settings(req: SettingsRequest): _validate_r2_secret_bindings(updated_r2) await asyncio.to_thread(save_r2_backup_settings, updated_r2) if req.nodeimage is not None: - from ..presets import get_nodeimage_settings - current_nodeimage = get_nodeimage_settings() updated_nodeimage = apply_nodeimage_settings( current_nodeimage, diff --git a/backend/app/api/uploads.py b/backend/app/api/uploads.py index c5532ca..41cd2c0 100644 --- a/backend/app/api/uploads.py +++ b/backend/app/api/uploads.py @@ -1,4 +1,3 @@ -import mimetypes from pathlib import Path from fastapi import HTTPException, UploadFile @@ -8,6 +7,7 @@ from ..repositories.image_files import ( IMAGE_EXTENSION_FORMATS, IMAGE_FILE_EXTENSIONS, IMAGE_FORMAT_CONTENT_TYPES, + image_content_type_for_filename, validate_image_header_bytes, ) @@ -23,14 +23,7 @@ def resolve_upload_content_type(upload: UploadFile) -> str: if upload.content_type and upload.content_type.startswith("image/"): return upload.content_type - guessed_type = mimetypes.guess_type(upload.filename or "")[0] - if guessed_type and guessed_type.startswith("image/"): - return guessed_type - - return IMAGE_UPLOAD_CONTENT_TYPES.get( - Path(upload.filename or "").suffix.lower(), - "application/octet-stream", - ) + return image_content_type_for_filename(upload.filename or "") def is_image_upload(upload: UploadFile) -> bool: diff --git a/backend/app/core/overall_config.py b/backend/app/core/overall_config.py index 4de4cea..8943684 100644 --- a/backend/app/core/overall_config.py +++ b/backend/app/core/overall_config.py @@ -124,6 +124,7 @@ OVERALL_CONFIG_REGISTRY: tuple[OverallConfigSpec, ...] = ( _spec("WEBHOOK_TIMEOUT_SECONDS", "float", "5", "Webhooks", "Webhook timeout per attempt.", min_value=0.1), _spec("WEBHOOK_MAX_ATTEMPTS", "int", "3", "Webhooks", "Webhook delivery attempts.", min_value=1), _spec("MAX_FILE_SIZE_MB", "int", "50", "Limits", "Max uploaded/downloaded image size.", min_value=1), + _spec("NODEIMAGE_UPLOAD_CONCURRENCY", "int", "4", "NodeImage", "Concurrent NodeImage uploads per worker process.", min_value=1), _spec("MAX_JSON_BODY_MB", "int", "1", "Limits", "Max JSON request body size.", min_value=1), _spec("MAX_UPSTREAM_JSON_MB", "int", "128", "Limits", "Max upstream JSON/SSE response size.", min_value=1), _spec("MAX_IMAGE_PIXELS", "int", "100000000", "Limits", "Max decoded image pixels.", min_value=1), diff --git a/backend/app/core/secrets.py b/backend/app/core/secrets.py index 03e4c35..fa5dcde 100644 --- a/backend/app/core/secrets.py +++ b/backend/app/core/secrets.py @@ -296,6 +296,58 @@ def resolve_secret( return value +def resolve_secret_reference( + value: object, + *, + purpose: SecretPurpose, + target_url: str, + host_allowlist: str, + field_name: str, +) -> str: + """Resolve a settings secret reference without exposing its source details. + + Settings may contain an empty value, an explicit environment reference, a + predeclared registry ID, or (when plaintext storage is enabled at the + settings boundary) a literal value. Registry IDs are always validated for + purpose, origin, and the startup host allowlist before resolution. + """ + + raw = str(value or "").strip() + if not raw: + return "" + + # Keep the parser local to avoid making the validators module import the + # secrets registry during application startup. + from .validators import get_env_var_ref_name, resolve_env_var_ref + + env_var = get_env_var_ref_name(raw) + if env_var: + resolved = resolve_env_var_ref(raw) + if resolved: + return resolved + raise SecretRegistryError( + f"{field_name} environment variable {env_var} is not set or empty." + ) + + if "${" in raw or "}" in raw: + raise SecretRegistryError( + f"{field_name} env ref must be formatted as ${{ENV_VAR_NAME}}." + ) + + if raw in configured_secret_ids(): + try: + return resolve_secret( + raw, + purpose=purpose, + target_url=target_url, + host_allowlist=host_allowlist, + ) + except SecretRegistryError as exc: + raise SecretRegistryError(f"{field_name}: {exc}") from exc + + return raw + + def active_secret_values() -> tuple[str, ...]: global _active_secret_values_cache diff --git a/backend/app/core/settings.py b/backend/app/core/settings.py index f691431..9d7565f 100644 --- a/backend/app/core/settings.py +++ b/backend/app/core/settings.py @@ -95,6 +95,7 @@ WEBHOOK_SIGNING_SECRET = os.getenv("WEBHOOK_SIGNING_SECRET", "").strip() WEBHOOK_TIMEOUT_SECONDS = float(os.getenv("WEBHOOK_TIMEOUT_SECONDS", "5")) WEBHOOK_MAX_ATTEMPTS = int(os.getenv("WEBHOOK_MAX_ATTEMPTS", "3")) MAX_FILE_SIZE_MB = int(os.getenv("MAX_FILE_SIZE_MB", "50")) +NODEIMAGE_UPLOAD_CONCURRENCY = max(1, int(os.getenv("NODEIMAGE_UPLOAD_CONCURRENCY", "4"))) MAX_JSON_BODY_MB = max(1, int(os.getenv("MAX_JSON_BODY_MB", "1"))) MAX_UPSTREAM_JSON_MB = max(1, int(os.getenv("MAX_UPSTREAM_JSON_MB", "128"))) MAX_UPSTREAM_IMAGE_BYTES_PER_TASK_MB = max( diff --git a/backend/app/integrations/nodeimage/__init__.py b/backend/app/integrations/nodeimage/__init__.py index fb258bc..590bcf8 100644 --- a/backend/app/integrations/nodeimage/__init__.py +++ b/backend/app/integrations/nodeimage/__init__.py @@ -5,10 +5,13 @@ from .client import ( NodeImageAuthError, NodeImageConfigurationError, NodeImageEffectiveSettings, + NodeImageTransientError, NodeImageUploadError, NodeImageUploadResult, resolve_nodeimage_settings, upload_image_bytes, + upload_image_file, + upload_image_source, ) __all__ = [ @@ -16,8 +19,11 @@ __all__ = [ "NodeImageAuthError", "NodeImageConfigurationError", "NodeImageEffectiveSettings", + "NodeImageTransientError", "NodeImageUploadError", "NodeImageUploadResult", "resolve_nodeimage_settings", "upload_image_bytes", + "upload_image_file", + "upload_image_source", ] diff --git a/backend/app/integrations/nodeimage/client.py b/backend/app/integrations/nodeimage/client.py index 691fc7d..6476165 100644 --- a/backend/app/integrations/nodeimage/client.py +++ b/backend/app/integrations/nodeimage/client.py @@ -4,27 +4,30 @@ from __future__ import annotations import asyncio import json -import mimetypes +from collections.abc import Callable from dataclasses import dataclass from pathlib import Path from typing import Any -from urllib.parse import urlsplit +from urllib.parse import quote, urlsplit import aiohttp from ...core import secrets from ...core.redaction import redact_sensitive_text -from ...core.validators import get_env_var_ref_name, resolve_env_var_ref +from ...repositories.image_files import image_content_type_for_filename from ..session_pool import TIMEOUT_NODEIMAGE, get_pool NODEIMAGE_API_URL = "https://api.nodeimage.com" NODEIMAGE_UPLOAD_URL = f"{NODEIMAGE_API_URL}/api/upload" NODEIMAGE_HOST_ALLOWLIST = "api.nodeimage.com" MAX_NODEIMAGE_RESPONSE_BYTES = 2 * 1024 * 1024 +NODEIMAGE_MAX_ATTEMPTS = 2 +NODEIMAGE_RETRY_BACKOFF_SECONDS = 0.2 RETRYABLE_CONNECT_ERRORS = ( aiohttp.ClientConnectorError, aiohttp.ClientProxyConnectionError, ) +NodeImageFileSource = Path | str | Callable[[], Any] class NodeImageConfigurationError(ValueError): @@ -39,6 +42,14 @@ class NodeImageUploadError(RuntimeError): pass +class NodeImageTransientError(NodeImageUploadError): + """An upload failure that is safe to retry before a response is complete.""" + + +class _NodeImageResponseReadError(NodeImageUploadError): + pass + + @dataclass(frozen=True) class NodeImageEffectiveSettings: enabled: bool @@ -52,28 +63,16 @@ class NodeImageUploadResult: def _resolve_secret(value: Any) -> str: - raw = str(value or "").strip() - if not raw: - return "" - env_var = get_env_var_ref_name(raw) - if env_var: - resolved = resolve_env_var_ref(raw) - if resolved: - return resolved - raise NodeImageConfigurationError( - f"NodeImage API key environment variable {env_var} is not set or empty." - ) - if raw not in secrets.configured_secret_ids(): - return raw try: - return secrets.resolve_secret( - raw, + return secrets.resolve_secret_reference( + value, purpose="nodeimage_api_key", target_url=NODEIMAGE_API_URL, host_allowlist=NODEIMAGE_HOST_ALLOWLIST, + field_name="NodeImage API key", ) except secrets.SecretRegistryError as exc: - raise NodeImageConfigurationError(f"NodeImage API key: {exc}") from exc + raise NodeImageConfigurationError(str(exc)) from exc def resolve_nodeimage_settings( @@ -107,14 +106,18 @@ async def _read_response_json(response: aiohttp.ClientResponse) -> dict[str, Any chunks.append(chunk) except NodeImageUploadError: raise - except (aiohttp.ClientError, asyncio.TimeoutError) as exc: - raise NodeImageUploadError("NodeImage returned an unreadable response.") from exc + except (aiohttp.ClientError, asyncio.TimeoutError, OSError) as exc: + raise _NodeImageResponseReadError( + "NodeImage returned an unreadable response." + ) from exc try: parsed = json.loads(b"".join(chunks).decode("utf-8")) except (UnicodeDecodeError, json.JSONDecodeError) as exc: - raise NodeImageUploadError("NodeImage returned an invalid JSON response.") from exc + raise _NodeImageResponseReadError( + "NodeImage returned an invalid JSON response." + ) from exc if not isinstance(parsed, dict): - raise NodeImageUploadError("NodeImage returned an invalid response.") + raise _NodeImageResponseReadError("NodeImage returned an invalid response.") return parsed @@ -143,79 +146,146 @@ def _validate_direct_url(value: Any) -> str: return direct -async def upload_image_bytes( - image_bytes: bytes, +def _markdown_escape_alt(value: str) -> str: + # Preserve ordinary filename punctuation while escaping inline constructs. + escaped = str(value or "image.png").replace("\\", "\\\\") + for character in "`*_[]()<>": + escaped = escaped.replace(character, f"\\{character}") + return escaped + + +def _markdown_for_direct_url(direct: str, filename: str) -> str: + safe_filename = Path(str(filename or "image.png")).name or "image.png" + alt = _markdown_escape_alt(safe_filename) + encoded_url = quote(direct, safe=":/?#[]@!$&'*+,;=%-._~") + return f"![{alt}]({encoded_url})" + + +def _open_file_source(source: NodeImageFileSource): + if isinstance(source, (str, Path)): + return Path(source).open("rb") + handle = source() + if not hasattr(handle, "read") or not hasattr(handle, "close"): + raise NodeImageUploadError("NodeImage file source was not readable.") + return handle + + +async def _upload_image_source_once( + image_source: bytes | NodeImageFileSource, + safe_filename: str, + effective: NodeImageEffectiveSettings, +) -> NodeImageUploadResult: + file_handle = None + if not isinstance(image_source, bytes): + try: + file_handle = _open_file_source(image_source) + except OSError: + raise + except NodeImageUploadError: + raise + except Exception as exc: + raise NodeImageUploadError("Image file could not be read") from exc + + try: + form_source = image_source if isinstance(image_source, bytes) else file_handle + form = aiohttp.FormData() + form.add_field( + "image", + form_source, + filename=safe_filename, + content_type=image_content_type_for_filename(safe_filename), + ) + session = get_pool().get(timeout_kind=TIMEOUT_NODEIMAGE) + async with session.post( + NODEIMAGE_UPLOAD_URL, + data=form, + headers={"X-API-Key": effective.api_key}, + allow_redirects=False, + ) as response: + if response.status in {401, 403}: + raise NodeImageAuthError("NodeImage API key was rejected.") + try: + payload = await _read_response_json(response) + except _NodeImageResponseReadError as exc: + if response.status >= 500: + raise NodeImageTransientError(str(exc)) from exc + raise + + message = _error_text(payload, response.status, effective.api_key) + if response.status >= 500: + raise NodeImageTransientError(message) + if _is_auth_error(message): + raise NodeImageAuthError("NodeImage API key was rejected.") + if response.status >= 400 or payload.get("success") is False: + raise NodeImageUploadError(message) + + links = payload.get("links") + if not isinstance(links, dict): + raise NodeImageUploadError( + "NodeImage response did not include upload links." + ) + direct = _validate_direct_url(links.get("direct")) + return NodeImageUploadResult( + url=direct, + markdown=_markdown_for_direct_url(direct, safe_filename), + ) + except NodeImageAuthError: + raise + except NodeImageTransientError: + raise + except NodeImageUploadError: + raise + except RETRYABLE_CONNECT_ERRORS as exc: + raise NodeImageTransientError("NodeImage upload request failed.") from exc + except (aiohttp.ClientError, asyncio.TimeoutError, OSError) as exc: + raise NodeImageUploadError("NodeImage upload request failed.") from exc + finally: + if file_handle is not None: + file_handle.close() + + +async def upload_image_source( + image_source: bytes | NodeImageFileSource, filename: str, effective: NodeImageEffectiveSettings, ) -> NodeImageUploadResult: if not effective.enabled: raise NodeImageConfigurationError("NodeImage upload is disabled.") - if not image_bytes: + if isinstance(image_source, bytes) and not image_source: + raise NodeImageUploadError("Image file is empty.") + if ( + isinstance(image_source, (str, Path)) + and Path(image_source).stat().st_size <= 0 + ): raise NodeImageUploadError("Image file is empty.") safe_filename = Path(str(filename or "image.png")).name or "image.png" - content_type = mimetypes.guess_type(safe_filename)[0] or "application/octet-stream" - headers = {"X-API-Key": effective.api_key} - - for attempt in range(2): - form = aiohttp.FormData() - form.add_field( - "image", - image_bytes, - filename=safe_filename, - content_type=content_type, - ) + attempt = 1 + while True: try: - session = get_pool().get(timeout_kind=TIMEOUT_NODEIMAGE) - async with session.post( - NODEIMAGE_UPLOAD_URL, - data=form, - headers=headers, - allow_redirects=False, - ) as response: - if response.status in {401, 403}: - raise NodeImageAuthError("NodeImage API key was rejected.") - try: - payload = await _read_response_json(response) - except NodeImageUploadError as exc: - if response.status >= 500 and attempt == 0: - await asyncio.sleep(0.2) - continue - raise + return await _upload_image_source_once( + image_source, + safe_filename, + effective, + ) + except NodeImageTransientError: + if attempt >= NODEIMAGE_MAX_ATTEMPTS: + raise + attempt += 1 + await asyncio.sleep(NODEIMAGE_RETRY_BACKOFF_SECONDS) - message = _error_text(payload, response.status, effective.api_key) - if response.status >= 500: - if attempt == 0: - await asyncio.sleep(0.2) - continue - raise NodeImageUploadError(message) - if _is_auth_error(message): - raise NodeImageAuthError("NodeImage API key was rejected.") - if response.status >= 400 or payload.get("success") is False: - raise NodeImageUploadError(message) - links = payload.get("links") - if not isinstance(links, dict): - raise NodeImageUploadError( - "NodeImage response did not include upload links." - ) - direct = _validate_direct_url(links.get("direct")) - markdown = str(links.get("markdown") or "").strip() - if not markdown: - raise NodeImageUploadError( - "NodeImage response did not include upload links." - ) - return NodeImageUploadResult(url=direct, markdown=markdown) - except NodeImageAuthError: - raise - except NodeImageUploadError: - raise - except RETRYABLE_CONNECT_ERRORS as exc: - if attempt == 0: - await asyncio.sleep(0.2) - continue - raise NodeImageUploadError("NodeImage upload request failed.") from exc - except (aiohttp.ClientError, asyncio.TimeoutError, OSError) as exc: - raise NodeImageUploadError("NodeImage upload request failed.") from exc +async def upload_image_file( + path: Path | str, + filename: str, + effective: NodeImageEffectiveSettings, +) -> NodeImageUploadResult: + return await upload_image_source(path, filename, effective) - raise NodeImageUploadError("NodeImage upload request failed.") + +async def upload_image_bytes( + image_bytes: bytes, + filename: str, + effective: NodeImageEffectiveSettings, +) -> NodeImageUploadResult: + return await upload_image_source(image_bytes, filename, effective) diff --git a/backend/app/integrations/r2/config.py b/backend/app/integrations/r2/config.py index 87c628b..3a5c862 100644 --- a/backend/app/integrations/r2/config.py +++ b/backend/app/integrations/r2/config.py @@ -10,12 +10,7 @@ from typing import Any, Callable, Iterable from ...core import secrets from ...core import settings as config from ...core.utils import utc_now -from ...core.validators import ( - get_env_var_ref_name, - normalize_r2_endpoint_url, - validate_r2_endpoint_url, - resolve_env_var_ref, -) +from ...core.validators import normalize_r2_endpoint_url, validate_r2_endpoint_url from ...repositories.image_files import safe_image_path @@ -150,28 +145,16 @@ def _resolve_secret( purpose: secrets.SecretPurpose, endpoint_url: str, ) -> str: - raw = str(value or "").strip() - if not raw: - return "" - env_var = get_env_var_ref_name(raw) - if env_var: - resolved = resolve_env_var_ref(raw) - if resolved: - return resolved - raise R2ConfigurationError( - f"{field_name} environment variable {env_var} is not set or empty." - ) - if raw not in secrets.configured_secret_ids(): - return raw try: - return secrets.resolve_secret( - raw, + return secrets.resolve_secret_reference( + value, purpose=purpose, target_url=endpoint_url, host_allowlist=config.R2_ENDPOINT_HOST_ALLOWLIST, + field_name=field_name, ) except secrets.SecretRegistryError as exc: - raise R2ConfigurationError(f"{field_name}: {exc}") from exc + raise R2ConfigurationError(str(exc)) from exc def resolve_r2_backup_settings( diff --git a/backend/app/integrations/r2/sync.py b/backend/app/integrations/r2/sync.py index f89d68a..7d8a776 100644 --- a/backend/app/integrations/r2/sync.py +++ b/backend/app/integrations/r2/sync.py @@ -1,5 +1,4 @@ import logging -import mimetypes import uuid from contextlib import nullcontext from concurrent.futures import ThreadPoolExecutor, as_completed @@ -16,7 +15,7 @@ from ...core.validators import ( normalize_r2_endpoint_url, resolve_env_var_ref, ) -from ...repositories.image_files import safe_image_path +from ...repositories.image_files import image_content_type_for_filename, safe_image_path HealthStatus = str @@ -230,7 +229,7 @@ def _local_sync_candidates_for_batch( def _content_type_for(path: Path) -> str: - return mimetypes.guess_type(path.name)[0] or "application/octet-stream" + return image_content_type_for_filename(path.name) def _metadata_for_entry(entry: Any, byte_size: int) -> dict[str, str]: diff --git a/backend/app/repositories/coordination.py b/backend/app/repositories/coordination.py index 11bf855..0280a3a 100644 --- a/backend/app/repositories/coordination.py +++ b/backend/app/repositories/coordination.py @@ -268,6 +268,27 @@ def update_gallery_job( with _connect() as conn: with _transaction(conn): if lease_owner: + if "payload_json" in normalized: + current_payload_row = conn.execute( + "SELECT payload_json FROM gallery_jobs WHERE job_id = ? AND lease_owner = ?", + (job_id, lease_owner), + ).fetchone() + current_payload = _json_loads_dict( + current_payload_row["payload_json"] + ) if current_payload_row else {} + next_payload = _json_loads_dict(normalized["payload_json"]) + if current_payload.get("cancel_requested"): + next_payload["cancel_requested"] = True + if current_payload.get("cancel_requested_at"): + next_payload.setdefault( + "cancel_requested_at", + current_payload["cancel_requested_at"], + ) + normalized["payload_json"] = json.dumps( + next_payload, + ensure_ascii=False, + sort_keys=True, + ) cursor = conn.execute( f""" UPDATE gallery_jobs @@ -337,6 +358,119 @@ def update_gallery_job_progress( return cursor.rowcount > 0 +def request_gallery_job_cancellation(kind: str, job_id: str) -> dict[str, Any] | None: + """Request cooperative cancellation while preserving completed item results.""" + _ensure_database() + normalized_kind = str(kind or "").strip() + normalized_job_id = str(job_id or "").strip() + if not normalized_kind or not normalized_job_id: + return None + now = utc_now() + with _connect() as conn: + with _transaction(conn): + row = conn.execute( + f""" + SELECT {', '.join(GALLERY_JOB_COLUMNS)} + FROM gallery_jobs + WHERE kind = ? AND job_id = ? + """, + (normalized_kind, normalized_job_id), + ).fetchone() + if not row: + return None + + status = str(row["status"] or "") + payload = _json_loads_dict(row["payload_json"]) + if status == "queued": + ids = [str(value) for value in payload.get("ids") or [] if str(value)] + existing_results = [ + value + for value in payload.get("results") or [] + if isinstance(value, dict) and str(value.get("image_id") or "") + ] + existing_ids = {str(value["image_id"]) for value in existing_results} + results = list(existing_results) + for image_id in ids: + if image_id in existing_ids: + continue + results.append( + { + "image_id": image_id, + "filename": None, + "status": "cancelled", + "url": None, + "markdown": None, + "error": "Cancelled before upload", + } + ) + payload["results"] = results + requested_count = max( + int(row["requested_count"] or 0), + len(ids), + ) + uploaded_count = sum(value.get("status") == "ok" for value in results) + failed_count = sum(value.get("status") == "error" for value in results) + conn.execute( + """ + UPDATE gallery_jobs + SET status = 'cancelled', + stage = 'cancelled', + message = 'NodeImage upload cancelled', + progress = 100, + requested_count = ?, + processed_count = ?, + uploaded_count = ?, + failed_count = ?, + completed_at = ?, + updated_at = ?, + error = NULL, + lease_owner = NULL, + lease_expires_at = NULL, + payload_json = ? + WHERE kind = ? AND job_id = ? AND status = 'queued' + """, + ( + requested_count, + requested_count, + uploaded_count, + failed_count, + now, + now, + json.dumps(payload, ensure_ascii=False, sort_keys=True), + normalized_kind, + normalized_job_id, + ), + ) + elif status == "running": + payload["cancel_requested"] = True + payload["cancel_requested_at"] = now + conn.execute( + """ + UPDATE gallery_jobs + SET stage = 'cancelling', + message = 'Cancelling NodeImage upload', + updated_at = ?, + payload_json = ? + WHERE kind = ? AND job_id = ? AND status = 'running' + """, + ( + now, + json.dumps(payload, ensure_ascii=False, sort_keys=True), + normalized_kind, + normalized_job_id, + ), + ) + row = conn.execute( + f""" + SELECT {', '.join(GALLERY_JOB_COLUMNS)} + FROM gallery_jobs + WHERE kind = ? AND job_id = ? + """, + (normalized_kind, normalized_job_id), + ).fetchone() + return _gallery_job_from_row(row) if row else None + + def renew_gallery_job_lease( *, job_id: str, @@ -656,22 +790,30 @@ def cleanup_expired_gallery_jobs(kind: str) -> list[dict[str, Any]]: return [_gallery_job_from_row(row) for row in rows] -def cleanup_stale_gallery_jobs(kind: str, ttl_seconds: int) -> list[dict[str, Any]]: +def cleanup_stale_gallery_jobs( + kind: str, + ttl_seconds: int, + terminal_statuses: Sequence[str] = ("success", "error"), +) -> list[dict[str, Any]]: _ensure_database() cutoff = datetime.fromtimestamp( time.time() - max(0, int(ttl_seconds or 0)), tz=timezone.utc, ).isoformat() + statuses = [str(status) for status in terminal_statuses if str(status)] + if not statuses: + return [] + placeholders = ", ".join("?" for _ in statuses) with _connect() as conn: with _transaction(conn): rows = conn.execute( f""" SELECT {", ".join(GALLERY_JOB_COLUMNS)} FROM gallery_jobs - WHERE kind = ? AND status IN ('success', 'error') + WHERE kind = ? AND status IN ({placeholders}) AND updated_at <= ? """, - (kind, cutoff), + (kind, *statuses, cutoff), ).fetchall() if rows: conn.executemany( diff --git a/backend/app/repositories/db.py b/backend/app/repositories/db.py index a160814..0e0dba8 100644 --- a/backend/app/repositories/db.py +++ b/backend/app/repositories/db.py @@ -396,7 +396,11 @@ def _default_secret_reference(secret_id: str, value: str | None) -> str: return "" -def _default_r2_secret_reference(secret_id: str, env_var: str, value: str | None) -> str: +def _default_env_backed_secret_reference( + secret_id: str, + env_var: str, + value: str | None, +) -> str: if secret_id in configured_secret_ids(): return secret_id normalized = str(value or "").strip() @@ -650,12 +654,12 @@ def _default_r2_backup_settings() -> dict: "bucket_name": config.R2_BUCKET_NAME, "region": config.R2_REGION or "auto", "key_prefix": _normalize_r2_key_prefix(config.R2_KEY_PREFIX), - "access_key_id": _default_r2_secret_reference( + "access_key_id": _default_env_backed_secret_reference( "builtin-r2-access-key-id", "R2_ACCESS_KEY_ID", config.R2_ACCESS_KEY_ID, ), - "secret_access_key": _default_r2_secret_reference( + "secret_access_key": _default_env_backed_secret_reference( "builtin-r2-secret-access-key", "R2_SECRET_ACCESS_KEY", config.R2_SECRET_ACCESS_KEY, @@ -667,7 +671,7 @@ def _default_r2_backup_settings() -> dict: def _default_nodeimage_settings() -> dict: return { "enabled": False, - "api_key": _default_r2_secret_reference( + "api_key": _default_env_backed_secret_reference( "builtin-nodeimage-api-key", "NODEIMAGE_API_KEY", config.NODEIMAGE_API_KEY, diff --git a/backend/app/repositories/image_files.py b/backend/app/repositories/image_files.py index 731aab6..ecc23d6 100644 --- a/backend/app/repositories/image_files.py +++ b/backend/app/repositories/image_files.py @@ -1,4 +1,5 @@ import io +import mimetypes import struct import tempfile import uuid @@ -85,6 +86,16 @@ PILLOW_FORMATS = { } +def image_content_type_for_filename(filename: str | Path) -> str: + """Return the repository's stable image MIME type for a filename.""" + + name = str(filename or "") + image_format = IMAGE_EXTENSION_FORMATS.get(Path(name).suffix.lower()) + if image_format: + return IMAGE_FORMAT_CONTENT_TYPES[image_format] + return mimetypes.guess_type(name)[0] or "application/octet-stream" + + def configure_pillow_image_limits() -> None: if Image is not None: Image.MAX_IMAGE_PIXELS = config.MAX_IMAGE_PIXELS diff --git a/backend/app/schemas/nodeimage.py b/backend/app/schemas/nodeimage.py index 3d2f8a7..7fe4173 100644 --- a/backend/app/schemas/nodeimage.py +++ b/backend/app/schemas/nodeimage.py @@ -11,7 +11,7 @@ class NodeImageUploadResponse(BaseModel): class NodeImageBatchUploadItem(BaseModel): image_id: str filename: Optional[str] = None - status: Literal["ok", "error"] + status: Literal["ok", "error", "cancelled"] url: Optional[str] = None markdown: Optional[str] = None error: Optional[str] = None @@ -22,3 +22,28 @@ class NodeImageBatchUploadResponse(BaseModel): uploaded_count: int failed_count: int results: list[NodeImageBatchUploadItem] = Field(default_factory=list) + + +class NodeImageUploadJobStatus(BaseModel): + job_id: str + status: Literal["queued", "running", "success", "partial_failure", "cancelled", "error"] + stage: Optional[str] = None + message: Optional[str] = None + progress: int = 0 + requested_count: int = 0 + processed_count: int = 0 + uploaded_count: int = 0 + failed_count: int = 0 + cancelled_count: int = 0 + results: list[NodeImageBatchUploadItem] = Field(default_factory=list) + created_at: Optional[str] = None + started_at: Optional[str] = None + completed_at: Optional[str] = None + updated_at: Optional[str] = None + error: Optional[str] = None + status_url: Optional[str] = None + events_url: Optional[str] = None + cancel_url: Optional[str] = None + + +NodeImageBatchUploadCreateResponse = NodeImageUploadJobStatus diff --git a/backend/app/schemas/settings.py b/backend/app/schemas/settings.py index 4fd360e..cb246a6 100644 --- a/backend/app/schemas/settings.py +++ b/backend/app/schemas/settings.py @@ -416,6 +416,7 @@ class NodeImageSettingsResponse(BaseModel): enabled: bool = False api_key_masked: str = "***" has_api_key: bool = False + api_key_resolvable: bool = False api_key_source: ApiKeySource = "empty" api_key_env_var: Optional[str] = None api_key_secret_id: Optional[str] = None diff --git a/backend/app/services/gallery_common.py b/backend/app/services/gallery_common.py index 7bee11a..fc70029 100644 --- a/backend/app/services/gallery_common.py +++ b/backend/app/services/gallery_common.py @@ -120,9 +120,12 @@ IMMUTABLE_GALLERY_CACHE_CONTROL = "private, max-age=31536000, immutable" GALLERY_EXPORT_TERMINAL_STATUSES = {"success", "error"} GALLERY_SYNC_TERMINAL_STATUSES = {"success", "error"} GALLERY_IMPORT_TERMINAL_STATUSES = {"success", "error"} +NODEIMAGE_UPLOAD_TERMINAL_STATUSES = {"success", "partial_failure", "cancelled", "error"} MAX_ACTIVE_EXPORT_JOBS = 5 MAX_ACTIVE_SYNC_JOBS = 1 MAX_ACTIVE_IMPORT_JOBS = 1 +MAX_ACTIVE_NODEIMAGE_UPLOAD_JOBS = 1 +MAX_ACTIVE_NODEIMAGE_JOBS = MAX_ACTIVE_NODEIMAGE_UPLOAD_JOBS EXPORT_JOB_TTL_SECONDS = 1800 EXPORT_JOB_GC_INTERVAL_SECONDS = 300 SYNC_JOB_TTL_SECONDS = 1800 @@ -148,6 +151,8 @@ GALLERY_SELECTION_TOKEN_KIND = "batch_selection" GALLERY_SELECTION_TOKEN_TTL_SECONDS = 15 * 60 AI_ANALYZE_JOB_KIND = "ai_analyze" AI_ANALYZE_JOB_TTL_SECONDS = 1800 +NODEIMAGE_UPLOAD_JOB_KIND = "nodeimage_upload" +NODEIMAGE_UPLOAD_JOB_TTL_SECONDS = 1800 def _trusted_gallery_job_dir(kind: str) -> Path: diff --git a/backend/app/services/gallery_jobs.py b/backend/app/services/gallery_jobs.py index fb2d613..c72e1be 100644 --- a/backend/app/services/gallery_jobs.py +++ b/backend/app/services/gallery_jobs.py @@ -2,7 +2,6 @@ import asyncio import hashlib import inspect import logging -import mimetypes import os import time import uuid @@ -54,6 +53,7 @@ from ..repositories.coordination import ( release_background_lease, release_import_upload_reservation, reserve_gallery_job_capacity, + renew_gallery_job_lease, update_gallery_job, update_gallery_job_progress, ) @@ -88,7 +88,7 @@ from ..repositories.image_files import ( safe_image_path, safe_thumbnail_path, ) -from ..repositories.settings import load_r2_backup_settings +from ..repositories.settings import load_nodeimage_settings, load_r2_backup_settings from ..repositories.thumbnail_jobs import ( THUMBNAIL_JOB_LEASE_SECONDS, claim_next_thumbnail_job, @@ -113,6 +113,15 @@ from ..schemas.gallery import ( GallerySyncRequest, GallerySyncJobStatus, ) +from ..schemas.nodeimage import NodeImageBatchUploadItem +from ..integrations.nodeimage.client import ( + NodeImageAuthError, + NodeImageConfigurationError, + NodeImageUploadError, + NodeImageUploadResult, + resolve_nodeimage_settings, + upload_image_file, +) from .claim_loop import run_claim_loop from .gallery_common import ( BACKGROUND_TASK_ERROR_BACKOFF_INITIAL_SECONDS, @@ -128,7 +137,11 @@ from .gallery_common import ( GALLERY_JOB_SSE_QUEUE_MAXSIZE, MAX_ACTIVE_EXPORT_JOBS, MAX_ACTIVE_IMPORT_JOBS, + MAX_ACTIVE_NODEIMAGE_UPLOAD_JOBS, MAX_ACTIVE_SYNC_JOBS, + NODEIMAGE_UPLOAD_JOB_KIND, + NODEIMAGE_UPLOAD_JOB_TTL_SECONDS, + NODEIMAGE_UPLOAD_TERMINAL_STATUSES, PRIVATE_GALLERY_CACHE_CONTROL, GalleryProgressThrottler, _progress_item_count, @@ -138,6 +151,15 @@ from .gallery_common import ( logger = logging.getLogger(__name__) +NODEIMAGE_UPLOAD_SEMAPHORE = asyncio.Semaphore( + max(1, int(getattr(config, "NODEIMAGE_UPLOAD_CONCURRENCY", 4) or 1)) +) +_NODEIMAGE_UPLOAD_SEMAPHORE_LOOP: asyncio.AbstractEventLoop | None = None +_NODEIMAGE_UPLOAD_SEMAPHORE_CAPACITY = max( + 1, + int(getattr(config, "NODEIMAGE_UPLOAD_CONCURRENCY", 4) or 1), +) + async def _gallery_zip_response( entries, filename_prefix: str, @@ -499,6 +521,70 @@ def _gallery_import_payload(job: dict) -> dict: return payload +def _nodeimage_result_item( + image_id: str, + filename: str | None, + status: str, + *, + url: str | None = None, + markdown: str | None = None, + error: str | None = None, +) -> dict: + return NodeImageBatchUploadItem( + image_id=str(image_id), + filename=filename, + status=status, + url=url, + markdown=markdown, + error=error, + ).model_dump() + + +def _nodeimage_result_counts(results: Iterable[dict]) -> tuple[int, int, int, int]: + normalized = list(results) + uploaded_count = sum(item.get("status") == "ok" for item in normalized) + failed_count = sum(item.get("status") == "error" for item in normalized) + cancelled_count = sum(item.get("status") == "cancelled" for item in normalized) + return len(normalized), uploaded_count, failed_count, cancelled_count + + +def _nodeimage_upload_payload(job: dict) -> dict: + stored_payload = job.get("payload") or {} + job_id = str(job.get("job_id") or "") + encoded_job_id = quote(job_id, safe="") if job_id else "" + ids = [str(value) for value in stored_payload.get("ids") or [] if str(value)] + results_by_id: dict[str, dict] = {} + for value in stored_payload.get("results") or []: + if not isinstance(value, dict): + continue + image_id = str(value.get("image_id") or "") + if image_id and image_id not in results_by_id: + results_by_id[image_id] = value + results = [results_by_id[image_id] for image_id in ids if image_id in results_by_id] + _, uploaded_count, failed_count, cancelled_count = _nodeimage_result_counts(results) + return { + "job_id": job_id, + "status": job.get("status"), + "stage": job.get("stage"), + "message": job.get("message"), + "progress": job.get("progress") or 0, + "requested_count": job.get("requested_count") or len(ids), + "processed_count": job.get("processed_count") or len(results), + "uploaded_count": job.get("uploaded_count") or uploaded_count, + "failed_count": job.get("failed_count") or failed_count, + "cancelled_count": cancelled_count, + "results": results, + "created_at": job.get("created_at"), + "started_at": job.get("started_at"), + "completed_at": job.get("completed_at"), + "updated_at": job.get("updated_at"), + "error": job.get("error"), + "status_url": f"/api/gallery/nodeimage-upload-jobs/{encoded_job_id}" if encoded_job_id else None, + "events_url": f"/api/gallery/nodeimage-upload-jobs/{encoded_job_id}/events" if encoded_job_id else None, + "cancel_url": f"/api/gallery/nodeimage-upload-jobs/{encoded_job_id}/cancel" if encoded_job_id else None, + } + + def _gallery_job_event_name(kind: str) -> str: if kind == "sync": return "sync" @@ -506,6 +592,8 @@ def _gallery_job_event_name(kind: str) -> str: return "import" if kind == "ai_analyze": return "analysis" + if kind == NODEIMAGE_UPLOAD_JOB_KIND: + return "nodeimage_upload" return "export" @@ -534,6 +622,8 @@ def _gallery_job_payload(kind: str, job: dict) -> dict: return _gallery_import_payload(job) if kind == "ai_analyze": return _gallery_ai_analyze_payload(job) + if kind == NODEIMAGE_UPLOAD_JOB_KIND: + return _nodeimage_upload_payload(job) return _gallery_export_payload(job) @@ -942,30 +1032,269 @@ def _create_gallery_sync_job(total_count: int, payload: dict | None = None) -> d ) -def _create_gallery_import_job( +def _build_gallery_import_job( zip_path: Path, total_count: int, payload: dict | None = None, ) -> dict: job_id = os.urandom(16).hex() now = utc_now() - return create_gallery_job( - job_id=job_id, - kind="import", - status="queued", - stage="queued", - message="Queued gallery ZIP import", - progress=0, - created_at=now, - updated_at=now, - error=None, - path=str(zip_path), - requested_count=total_count, - processed_count=0, - exported_count=0, - missing_count=0, - payload=payload or {}, + return { + "job_id": job_id, + "kind": "import", + "status": "queued", + "stage": "queued", + "message": "Queued gallery ZIP import", + "progress": 0, + "created_at": now, + "updated_at": now, + "error": None, + "path": str(zip_path), + "requested_count": total_count, + "processed_count": 0, + "exported_count": 0, + "missing_count": 0, + "payload": payload or {}, + } + + +def _get_nodeimage_upload_semaphore() -> asyncio.Semaphore: + global NODEIMAGE_UPLOAD_SEMAPHORE + global _NODEIMAGE_UPLOAD_SEMAPHORE_CAPACITY, _NODEIMAGE_UPLOAD_SEMAPHORE_LOOP + current_loop = asyncio.get_running_loop() + capacity = max(1, int(getattr(config, "NODEIMAGE_UPLOAD_CONCURRENCY", 4) or 1)) + if ( + capacity != _NODEIMAGE_UPLOAD_SEMAPHORE_CAPACITY + or _NODEIMAGE_UPLOAD_SEMAPHORE_LOOP is not current_loop + ): + NODEIMAGE_UPLOAD_SEMAPHORE = asyncio.Semaphore(capacity) + _NODEIMAGE_UPLOAD_SEMAPHORE_CAPACITY = capacity + _NODEIMAGE_UPLOAD_SEMAPHORE_LOOP = current_loop + return NODEIMAGE_UPLOAD_SEMAPHORE + + +def _build_nodeimage_upload_job( + ids: list[str], + requested_count: int, + missing_ids: list[str], +) -> dict: + job_id = os.urandom(16).hex() + now = utc_now() + initial_results = [ + _nodeimage_result_item( + image_id, + None, + "error", + error="Gallery entry not found", + ) + for image_id in missing_ids + ] + processed_count, _uploaded_count, failed_count, _cancelled_count = _nodeimage_result_counts(initial_results) + return { + "job_id": job_id, + "kind": NODEIMAGE_UPLOAD_JOB_KIND, + "status": "queued", + "stage": "queued", + "message": "Queued NodeImage upload", + "progress": round((processed_count / max(1, requested_count)) * 100), + "requested_count": requested_count, + "processed_count": processed_count, + "uploaded_count": 0, + "failed_count": failed_count, + "created_at": now, + "updated_at": now, + "error": None, + "payload": { + "ids": list(ids), + "results": initial_results, + "cancel_requested": False, + }, + } + + +async def _create_reserved_nodeimage_upload_job( + ids: list[str], + requested_count: int, + missing_ids: list[str], +) -> dict: + job = await asyncio.to_thread( + reserve_gallery_job_capacity, + job=_build_nodeimage_upload_job(ids, requested_count, missing_ids), + counted_kinds=(NODEIMAGE_UPLOAD_JOB_KIND,), + max_active=MAX_ACTIVE_NODEIMAGE_UPLOAD_JOBS, ) + if not job: + active_count = await asyncio.to_thread( + count_active_gallery_jobs, + NODEIMAGE_UPLOAD_JOB_KIND, + ) + raise HTTPException( + status_code=429, + detail=f"Too many active NodeImage upload jobs ({active_count}). " + "Please wait for the existing upload to complete.", + ) + return job + + +NodeImageFileInspection = Path | dict + + +def _inspect_nodeimage_file(entry: GalleryEntry) -> NodeImageFileInspection: + path = safe_image_path(entry.filename) + if not path: + return _nodeimage_result_item( + entry.id, + entry.filename, + "error", + error="Image file not found", + ) + try: + file_size = path.stat().st_size + except FileNotFoundError: + return _nodeimage_result_item( + entry.id, + entry.filename, + "error", + error="Image file not found", + ) + except OSError: + return _nodeimage_result_item( + entry.id, + entry.filename, + "error", + error="Image file could not be read", + ) + if file_size > config.MAX_FILE_SIZE_MB * 1024 * 1024: + return _nodeimage_result_item( + entry.id, + entry.filename, + "error", + error=f"Image file is too large. Max size is {config.MAX_FILE_SIZE_MB} MB", + ) + try: + with path.open("rb"): + pass + except FileNotFoundError: + return _nodeimage_result_item( + entry.id, + entry.filename, + "error", + error="Image file not found", + ) + except OSError: + return _nodeimage_result_item( + entry.id, + entry.filename, + "error", + error="Image file could not be read", + ) + if file_size <= 0: + return _nodeimage_result_item( + entry.id, + entry.filename, + "error", + error="Image file is empty", + ) + return path + + +async def _nodeimage_job_cancel_requested(job_id: str) -> bool: + current = await asyncio.to_thread(get_gallery_job, NODEIMAGE_UPLOAD_JOB_KIND, job_id) + if not current: + return True + return bool((current.get("payload") or {}).get("cancel_requested")) + + +def _nodeimage_cancelled_item(entry: GalleryEntry) -> dict: + return _nodeimage_result_item( + entry.id, + entry.filename, + "cancelled", + error="Cancelled before upload", + ) + + +async def _upload_nodeimage_entry( + job_id: str, + entry: GalleryEntry, + effective, + auth_failed: asyncio.Event, + *, + inspected_path: Path | None = None, +) -> dict: + semaphore = _get_nodeimage_upload_semaphore() + async with semaphore: + if await _nodeimage_job_cancel_requested(job_id): + return _nodeimage_cancelled_item(entry) + + if auth_failed.is_set(): + inspection = await asyncio.to_thread(_inspect_nodeimage_file, entry) + if isinstance(inspection, dict): + return inspection + return _nodeimage_result_item( + entry.id, + entry.filename, + "error", + error="NodeImage API key was rejected.", + ) + + path = inspected_path + if path is None: + inspection = await asyncio.to_thread(_inspect_nodeimage_file, entry) + if isinstance(inspection, dict): + return inspection + path = inspection + if auth_failed.is_set(): + return _nodeimage_result_item( + entry.id, + entry.filename, + "error", + error="NodeImage API key was rejected.", + ) + try: + result: NodeImageUploadResult = await upload_image_file( + path, + path.name, + effective, + ) + return _nodeimage_result_item( + entry.id, + entry.filename, + "ok", + url=result.url, + markdown=result.markdown, + ) + except NodeImageAuthError: + auth_failed.set() + return _nodeimage_result_item( + entry.id, + entry.filename, + "error", + error="NodeImage API key was rejected.", + ) + except NodeImageUploadError as exc: + return _nodeimage_result_item( + entry.id, + entry.filename, + "error", + error=str(exc) or "NodeImage upload failed.", + ) + except FileNotFoundError: + return _nodeimage_result_item(entry.id, entry.filename, "error", error="Image file not found") + except OSError: + return _nodeimage_result_item(entry.id, entry.filename, "error", error="Image file could not be read") + except Exception as exc: + logger.exception( + "Unexpected NodeImage upload failure for gallery entry %s (%s)", + entry.id, + type(exc).__name__, + ) + return _nodeimage_result_item( + entry.id, + entry.filename, + "error", + error="Unexpected upload failure", + ) async def _create_reserved_gallery_export_job( @@ -1014,7 +1343,7 @@ async def _create_reserved_gallery_import_job( ) -> dict: job = await asyncio.to_thread( reserve_gallery_job_capacity, - job=_create_gallery_import_job(zip_path, total_count, payload), + job=_build_gallery_import_job(zip_path, total_count, payload), counted_kinds=("import",), max_active=MAX_ACTIVE_IMPORT_JOBS, ) @@ -1342,11 +1671,258 @@ async def _run_gallery_import_job(job: dict) -> None: await asyncio.to_thread(release_import_upload_reservation, reservation_id) +class _NodeImageLeaseLost(RuntimeError): + pass + + +async def _publish_nodeimage_job( + job_id: str, + lease_owner: str, + updates: dict, +) -> dict: + job = await run_db_operation( + update_gallery_job, + job_id, + updates, + lease_owner=lease_owner, + metric_name="update_nodeimage_upload_job", + ) + if not job: + raise _NodeImageLeaseLost() + _publish_gallery_job_sse(job) + return job + + +async def _run_nodeimage_upload_job(job: dict) -> None: + job_id = str(job["job_id"]) + lease_owner = str(job.get("lease_owner") or "") + stored_payload = job.get("payload") or {} + ids = [str(value) for value in stored_payload.get("ids") or [] if str(value)] + payload = dict(stored_payload) + results_by_id: dict[str, dict] = {} + for value in payload.get("results") or []: + if not isinstance(value, dict): + continue + image_id = str(value.get("image_id") or "") + if image_id and image_id not in results_by_id: + results_by_id[image_id] = value + payload["ids"] = ids + requested_count = max(int(job.get("requested_count") or 0), len(ids)) + auth_failed = asyncio.Event() + lease_lost = asyncio.Event() + stop_lease_renewal = asyncio.Event() + + async def renew_lease() -> None: + try: + while True: + try: + await asyncio.wait_for( + stop_lease_renewal.wait(), + timeout=max(1.0, GALLERY_JOB_LEASE_SECONDS / 3), + ) + return + except asyncio.TimeoutError: + pass + renewed = await asyncio.to_thread( + renew_gallery_job_lease, + job_id=job_id, + lease_owner=lease_owner, + lease_expires_at=_gallery_job_lease_expires_at(), + now=utc_now(), + ) + if not renewed: + lease_lost.set() + return + except asyncio.CancelledError: + raise + + renew_task = asyncio.create_task(renew_lease()) + + def ordered_results() -> list[dict]: + return [results_by_id[image_id] for image_id in ids if image_id in results_by_id] + + async def persist_results(*, status: str = "running", stage: str = "uploading", message: str = "Uploading images to NodeImage") -> None: + results = ordered_results() + processed_count, uploaded_count, failed_count, _cancelled_count = _nodeimage_result_counts(results) + payload["results"] = results + if lease_lost.is_set(): + raise _NodeImageLeaseLost() + await _publish_nodeimage_job( + job_id, + lease_owner, + { + "status": status, + "stage": stage, + "message": message, + "progress": 100 if status in NODEIMAGE_UPLOAD_TERMINAL_STATUSES else round( + (processed_count / max(1, requested_count)) * 100 + ), + "requested_count": requested_count, + "processed_count": processed_count, + "uploaded_count": uploaded_count, + "failed_count": failed_count, + "payload": payload, + "error": None, + }, + ) + + async def persist_item(item: dict) -> None: + results_by_id[str(item["image_id"])] = item + await persist_results() + + try: + effective = resolve_nodeimage_settings( + await asyncio.to_thread(load_nodeimage_settings) + ) + entries = await asyncio.to_thread(get_gallery_entries_by_ids, ids) + entries_by_id = {entry.id: entry for entry in entries} + for image_id in ids: + if image_id in results_by_id or image_id in entries_by_id: + continue + results_by_id[image_id] = _nodeimage_result_item( + image_id, + None, + "error", + error="Gallery entry not found", + ) + + pending_entries = [ + entries_by_id[image_id] + for image_id in ids + if image_id in entries_by_id and image_id not in results_by_id + ] + await persist_results(message="Preparing NodeImage upload") + + while pending_entries and not await _nodeimage_job_cancel_requested(job_id): + probe_entry = pending_entries.pop(0) + probe_inspection = await asyncio.to_thread( + _inspect_nodeimage_file, + probe_entry, + ) + if isinstance(probe_inspection, dict): + await persist_item(probe_inspection) + continue + await persist_item( + await _upload_nodeimage_entry( + job_id, + probe_entry, + effective, + auth_failed, + inspected_path=probe_inspection, + ) + ) + break + + if pending_entries and not await _nodeimage_job_cancel_requested(job_id): + tasks = [ + asyncio.create_task( + _upload_nodeimage_entry( + job_id, + entry, + effective, + auth_failed, + ) + ) + for entry in pending_entries + ] + try: + for task in asyncio.as_completed(tasks): + await persist_item(await task) + finally: + for task in tasks: + if not task.done(): + task.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + + if await _nodeimage_job_cancel_requested(job_id): + for entry in pending_entries: + if entry.id not in results_by_id: + results_by_id[entry.id] = _nodeimage_cancelled_item(entry) + results = ordered_results() + _processed_count, uploaded_count, failed_count, cancelled_count = _nodeimage_result_counts(results) + if cancelled_count: + terminal_status = "cancelled" + terminal_message = "NodeImage upload cancelled" + elif failed_count: + terminal_status = "partial_failure" + terminal_message = "NodeImage upload completed with failures" + else: + terminal_status = "success" + terminal_message = "NodeImage upload complete" + payload["results"] = results + await _publish_nodeimage_job( + job_id, + lease_owner, + { + "status": terminal_status, + "stage": "completed" if terminal_status != "cancelled" else "cancelled", + "message": terminal_message, + "progress": 100, + "requested_count": requested_count, + "processed_count": len(results), + "uploaded_count": uploaded_count, + "failed_count": failed_count, + "completed_at": utc_now(), + "lease_owner": None, + "lease_expires_at": None, + "payload": payload, + "error": None, + }, + ) + except _NodeImageLeaseLost: + logger.info("NodeImage upload job %s stopped after losing its lease", job_id) + except asyncio.CancelledError: + raise + except Exception: + logger.exception("NodeImage upload job %s failed", job_id) + try: + results = ordered_results() + remaining_error = "NodeImage upload job failed" + entries = await asyncio.to_thread(get_gallery_entries_by_ids, ids) + for entry in entries: + if entry.id not in results_by_id: + results_by_id[entry.id] = _nodeimage_result_item( + entry.id, + entry.filename, + "error", + error=remaining_error, + ) + payload["results"] = ordered_results() + _processed_count, uploaded_count, failed_count, _cancelled_count = _nodeimage_result_counts(payload["results"]) + await _publish_nodeimage_job( + job_id, + lease_owner, + { + "status": "error", + "stage": "error", + "message": "NodeImage upload failed", + "progress": 100, + "requested_count": requested_count, + "processed_count": len(payload["results"]), + "uploaded_count": uploaded_count, + "failed_count": failed_count, + "completed_at": utc_now(), + "lease_owner": None, + "lease_expires_at": None, + "payload": payload, + "error": remaining_error, + }, + ) + except _NodeImageLeaseLost: + logger.info("NodeImage upload job %s stopped after losing its lease", job_id) + finally: + stop_lease_renewal.set() + if not renew_task.done(): + renew_task.cancel() + await asyncio.gather(renew_task, return_exceptions=True) + + async def _run_gallery_job_dispatcher(kind: str, worker_id: str, running_limit: int) -> None: runner_by_kind = { "export": _run_gallery_export_job, "sync": _run_gallery_sync_job, "import": _run_gallery_import_job, + NODEIMAGE_UPLOAD_JOB_KIND: _run_nodeimage_upload_job, } runner = runner_by_kind[kind] @@ -1389,5 +1965,13 @@ async def run_gallery_import_dispatcher(worker_id: str) -> None: await _run_gallery_job_dispatcher("import", worker_id, MAX_ACTIVE_IMPORT_JOBS) +async def run_gallery_nodeimage_upload_dispatcher(worker_id: str) -> None: + await _run_gallery_job_dispatcher( + NODEIMAGE_UPLOAD_JOB_KIND, + worker_id, + MAX_ACTIVE_NODEIMAGE_UPLOAD_JOBS, + ) + + __all__ = [name for name in globals() if not name.startswith("__")] diff --git a/backend/app/services/gallery_maintenance.py b/backend/app/services/gallery_maintenance.py index e66d1f6..391c03e 100644 --- a/backend/app/services/gallery_maintenance.py +++ b/backend/app/services/gallery_maintenance.py @@ -40,6 +40,9 @@ from .gallery_common import ( GALLERY_FILE_GC_INTERVAL_SECONDS, IMPORT_JOB_TTL_SECONDS, MAX_ACTIVE_SYNC_JOBS, + NODEIMAGE_UPLOAD_JOB_KIND, + NODEIMAGE_UPLOAD_JOB_TTL_SECONDS, + NODEIMAGE_UPLOAD_TERMINAL_STATUSES, SCHEDULED_R2_SYNC_DISABLED_POLL_SECONDS, SYNC_JOB_TTL_SECONDS, THUMBNAIL_DISPATCH_INTERVAL_SECONDS, @@ -53,6 +56,7 @@ from .gallery_jobs import ( _sleep_while_renewing_background_lease, run_gallery_export_dispatcher, run_gallery_import_dispatcher, + run_gallery_nodeimage_upload_dispatcher, run_gallery_sync_dispatcher, ) @@ -168,6 +172,7 @@ def kick_gallery_job_dispatchers() -> None: ("gallery_export_dispatcher_task", run_gallery_export_dispatcher), ("gallery_sync_dispatcher_task", run_gallery_sync_dispatcher), ("gallery_import_dispatcher_task", run_gallery_import_dispatcher), + ("gallery_nodeimage_upload_dispatcher_task", run_gallery_nodeimage_upload_dispatcher), ): task = getattr(app.state, name, None) if task and not task.done(): @@ -372,6 +377,12 @@ async def gc_gallery_export_jobs(worker_id: str) -> None: AI_ANALYZE_JOB_KIND, AI_ANALYZE_JOB_TTL_SECONDS, ) + stale_nodeimage_upload_jobs = await asyncio.to_thread( + cleanup_stale_gallery_jobs, + NODEIMAGE_UPLOAD_JOB_KIND, + NODEIMAGE_UPLOAD_JOB_TTL_SECONDS, + NODEIMAGE_UPLOAD_TERMINAL_STATUSES, + ) expired_direct_slots = await asyncio.to_thread( cleanup_expired_gallery_jobs, "export_direct", @@ -398,15 +409,17 @@ async def gc_gallery_export_jobs(worker_id: str) -> None: or stale_imports or stale_direct_exports or stale_ai_analyze_jobs + or stale_nodeimage_upload_jobs or expired_direct_slots ): logger.info( - "GC cleaned up %d gallery export job(s), %d sync job(s), %d import job(s), %d completed direct export job(s), %d AI analyze job(s), and %d direct export slot(s)", + "GC cleaned up %d gallery export job(s), %d sync job(s), %d import job(s), %d completed direct export job(s), %d AI analyze job(s), %d NodeImage upload job(s), and %d direct export slot(s)", len(stale_exports), len(stale_syncs), len(stale_imports), len(stale_direct_exports), len(stale_ai_analyze_jobs), + len(stale_nodeimage_upload_jobs), len(expired_direct_slots), ) exports_dir = Path(config.DATA_DIR) / "exports" diff --git a/backend/tests/support/contract.py b/backend/tests/support/contract.py index 1f2bd7a..d9c40d3 100644 --- a/backend/tests/support/contract.py +++ b/backend/tests/support/contract.py @@ -214,6 +214,7 @@ def _configure_runtime(tmp_path: Path, *, access_key: str = "", allow_unauthenti config.R2_SECRET_ACCESS_KEY = "" config.R2_SYNC_INTERVAL_HOURS = 0 config.NODEIMAGE_API_KEY = "" + config.NODEIMAGE_UPLOAD_CONCURRENCY = 4 config.MAX_SSE_SUBSCRIBERS_GLOBAL = 200 config.MAX_SSE_SUBSCRIBERS_PER_IP = 10 config.SSE_CONNECTION_TTL_SECONDS = 3600 diff --git a/backend/tests/test_access_settings_contract.py b/backend/tests/test_access_settings_contract.py index aa2c72b..af66322 100644 --- a/backend/tests/test_access_settings_contract.py +++ b/backend/tests/test_access_settings_contract.py @@ -1497,19 +1497,20 @@ def test_r2_health_uses_draft_settings_and_preserves_masked_credentials(client, "/api/settings/r2/health", json={ "enabled": True, - "endpoint_url": "https://draft.r2.cloudflarestorage.com", + "endpoint_url": "https://account.r2.cloudflarestorage.com", "bucket_name": "draft-backups", "region": "auto", "key_prefix": "draft/", "access_key_id": "********", "secret_access_key": "********", + "use_credentials": True, }, ) assert health.status_code == 200 assert health.json()["status"] == "ok" assert health.json()["checks"][0]["name"] == "configuration" - assert seen["draft"]["endpoint_url"] == "https://draft.r2.cloudflarestorage.com" + assert seen["draft"]["endpoint_url"] == "https://account.r2.cloudflarestorage.com" assert seen["draft"]["bucket_name"] == "draft-backups" assert seen["draft"]["access_key_id"] == "${TEST_R2_ACCESS_KEY_ID}" assert seen["draft"]["secret_access_key"] == "${TEST_R2_SECRET_ACCESS_KEY}" @@ -1808,7 +1809,10 @@ def test_prompt_snippets_crud_search_and_validation(client): first_body["id"], ] - searched = client.get("/api/prompt-snippets", params={"query": "portrait"}) + searched = client.post( + "/api/prompt-snippets/search", + json={"query": "portrait"}, + ) assert searched.status_code == 200 assert [snippet["id"] for snippet in searched.json()["snippets"]] == [ first_body["id"], @@ -2049,7 +2053,10 @@ def test_preset_health_and_env_api_key_resolution(client, monkeypatch): assert updated_body["api_key_masked"] == "${OPENAI_API_KEY}" assert "env-secret" not in json.dumps(updated_body) - health = client.post(f"/api/settings/presets/{active_preset_id}/health") + health = client.post( + f"/api/settings/presets/{active_preset_id}/health", + json={"use_credentials": True}, + ) assert health.status_code == 200 assert health.json()["status"] == "ok" assert seen["probe_key"] == "env-secret" @@ -2101,32 +2108,23 @@ def test_overall_config_syncs_env_and_hot_override(client, monkeypatch): assert item["value"] is False -def test_overall_config_secret_mask_and_preserve(client): +def test_overall_config_startup_secret_cannot_be_overridden(client): response = client.put( "/api/settings/overall-config", json={"updates": [{"name": "WEBHOOK_SIGNING_SECRET", "value": "super-secret"}]}, ) assert response.status_code == 422 - assert "ALLOW_PLAINTEXT_SECRETS" in response.text + assert "process startup" in response.text config.ALLOW_PLAINTEXT_SECRETS = True response = client.put( "/api/settings/overall-config", json={"updates": [{"name": "WEBHOOK_SIGNING_SECRET", "value": "super-secret"}]}, ) - assert response.status_code == 200 - item = next(i for i in response.json()["items"] if i["name"] == "WEBHOOK_SIGNING_SECRET") - assert item["value"] == "********" - assert item["value_masked"] == "********" - assert "super-secret" not in response.text - - response = client.put( - "/api/settings/overall-config", - json={"updates": [{"name": "WEBHOOK_SIGNING_SECRET", "value": "********"}]}, - ) - assert response.status_code == 200 + assert response.status_code == 422 + assert "process startup" in response.text rows = settings_repo.list_overall_config_values() - assert rows["WEBHOOK_SIGNING_SECRET"]["override_value"] == "super-secret" + assert rows["WEBHOOK_SIGNING_SECRET"]["override_value"] is None def test_overall_config_secret_env_value_is_not_persisted(client, monkeypatch): @@ -2165,12 +2163,18 @@ def test_overall_config_validation_errors(client): def test_overall_config_restart_and_build_only_badges(client): + startup_only = client.put( + "/api/settings/overall-config", + json={"updates": [{"name": "IP_ALLOWLIST", "value": "192.0.2.1"}]}, + ) + assert startup_only.status_code == 422 + assert "process startup" in startup_only.text + response = client.put( "/api/settings/overall-config", json={ "updates": [ {"name": "ACCESS_KEY_COOKIE_NAME", "value": "custom_access"}, - {"name": "IP_ALLOWLIST", "value": "192.0.2.1"}, {"name": "PYTHON_BASE_IMAGE", "value": "python:3.12-slim"}, ] }, @@ -2179,12 +2183,12 @@ def test_overall_config_restart_and_build_only_badges(client): body = response.json() assert set(body["restart_required_names"]) == { "ACCESS_KEY_COOKIE_NAME", - "IP_ALLOWLIST", "PYTHON_BASE_IMAGE", } items = {item["name"]: item for item in body["items"]} assert items["ACCESS_KEY_COOKIE_NAME"]["restart_required"] is True assert items["IP_ALLOWLIST"]["restart_required"] is True + assert items["IP_ALLOWLIST"]["startup_only"] is True assert items["PYTHON_BASE_IMAGE"]["build_only"] is True @@ -2251,18 +2255,16 @@ def test_missing_env_api_key_is_reported(client, monkeypatch): ) assert updated.status_code == 200 - health = client.post(f"/api/settings/presets/{active_preset_id}/health") - assert health.status_code == 200 - body = health.json() - assert body["status"] == "error" - assert any( - check["name"] == "api_key" and check["status"] == "error" - for check in body["checks"] + health = client.post( + f"/api/settings/presets/{active_preset_id}/health", + json={"use_credentials": True}, ) + assert health.status_code == 422 + assert "MISSING_IMAGE_KEY" in health.json()["detail"] resp = client.post( "/api/generate", json={"prompt": "missing env", "model": "gpt-image-2"}, ) - assert resp.status_code == 400 + assert resp.status_code == 422 assert "MISSING_IMAGE_KEY" in resp.json()["detail"] diff --git a/backend/tests/test_assistant_contract.py b/backend/tests/test_assistant_contract.py index d7fdef7..89742f6 100644 --- a/backend/tests/test_assistant_contract.py +++ b/backend/tests/test_assistant_contract.py @@ -932,7 +932,7 @@ def test_ai_assistant_gallery_batch_analysis_validates_runtime_before_queueing(c json={"ids": ["assistant-batch-config-error"]}, ) - assert submitted.status_code == 400 + assert submitted.status_code == 422 assert "MISSING_ASSISTANT_API_KEY is not set or empty" in submitted.json()["detail"] assert coordination_repo.count_active_gallery_jobs("ai_analyze") == 0 diff --git a/backend/tests/test_nodeimage_upload_contract.py b/backend/tests/test_nodeimage_upload_contract.py index ecaf8a3..24c744f 100644 --- a/backend/tests/test_nodeimage_upload_contract.py +++ b/backend/tests/test_nodeimage_upload_contract.py @@ -1,6 +1,11 @@ import asyncio +import io import json +import logging +import threading +import time from pathlib import Path +from types import SimpleNamespace import aiohttp import pytest @@ -16,7 +21,9 @@ from backend.tests.support.contract import ( from backend.app.api.routers import gallery_batch from backend.app.core import secrets from backend.app.integrations.nodeimage import client as nodeimage_client -from backend.app.services import gallery_common +from backend.app.repositories.coordination import create_gallery_job, get_gallery_job +from backend.app.schemas.gallery import GalleryEntry +from backend.app.services import gallery_jobs def _enable_nodeimage() -> None: @@ -28,14 +35,54 @@ def _enable_nodeimage() -> None: ) +def _wait_for_nodeimage_job(test_client, job_id: str, timeout: float = 5.0): + deadline = time.time() + timeout + last = None + while time.time() < deadline: + response = test_client.get( + f"/api/gallery/nodeimage-upload-jobs/{job_id}" + ) + assert response.status_code == 200 + last = response.json() + if last["status"] in { + "success", + "partial_failure", + "cancelled", + "error", + }: + return last + time.sleep(0.05) + raise AssertionError(f"NodeImage job {job_id} did not finish: {last}") + + +def _start_nodeimage_batch(test_client, ids: list[str]): + response = test_client.post( + "/api/gallery/batch/nodeimage-upload", + json={"ids": ids}, + ) + assert response.status_code == 202 + body = response.json() + assert body["status_url"].endswith( + f"/api/gallery/nodeimage-upload-jobs/{body['job_id']}" + ) + assert body["events_url"].endswith( + f"/api/gallery/nodeimage-upload-jobs/{body['job_id']}/events" + ) + assert body["cancel_url"].endswith( + f"/api/gallery/nodeimage-upload-jobs/{body['job_id']}/cancel" + ) + return body + + def test_single_nodeimage_upload_success_and_missing_entry(client, monkeypatch): _fake_gallery_entry("node-single", "single", "1024x1024", "node-single.png") _enable_nodeimage() seen = {} - async def fake_upload(image_bytes, filename, effective): + async def fake_upload(path, filename, effective): seen.update( - image_bytes=image_bytes, + path=path, + size=path.stat().st_size, filename=filename, api_key=effective.api_key, ) @@ -44,7 +91,12 @@ def test_single_nodeimage_upload_success_and_missing_entry(client, monkeypatch): markdown="![single](https://cdn.nodeimage.com/single.png)", ) - monkeypatch.setattr(gallery_queries_router, "upload_image_bytes", fake_upload) + monkeypatch.setattr(gallery_queries_router, "upload_image_file", fake_upload) + monkeypatch.setattr( + gallery_queries_router, + "_resolve_gallery_image_path", + lambda _filename: pytest.fail("single upload must not re-query gallery references"), + ) response = client.post("/api/gallery/node-single/nodeimage-upload") @@ -54,13 +106,54 @@ def test_single_nodeimage_upload_success_and_missing_entry(client, monkeypatch): "markdown": "![single](https://cdn.nodeimage.com/single.png)", } assert seen == { - "image_bytes": PNG_BYTES, + "path": Path(config.IMAGES_DIR) / "node-single.png", + "size": len(PNG_BYTES), "filename": "node-single.png", "api_key": "nodeimage-api-key", } assert client.post("/api/gallery/missing/nodeimage-upload").status_code == 404 +def test_single_nodeimage_upload_preserves_missing_and_unreadable_file_errors( + client, + monkeypatch, +): + missing = _fake_gallery_entry( + "node-missing-file", + "missing", + "1024x1024", + "node-missing-file.png", + ) + unreadable = _fake_gallery_entry( + "node-unreadable-file", + "unreadable", + "1024x1024", + "node-unreadable-file.png", + ) + _enable_nodeimage() + (Path(config.IMAGES_DIR) / missing.filename).unlink() + + missing_response = client.post( + "/api/gallery/node-missing-file/nodeimage-upload" + ) + assert missing_response.status_code == 404 + assert missing_response.json()["detail"] == "Image file not found" + + async def unreadable_upload(*_args): + raise OSError("permission denied") + + monkeypatch.setattr( + gallery_queries_router, + "upload_image_file", + unreadable_upload, + ) + unreadable_response = client.post( + "/api/gallery/node-unreadable-file/nodeimage-upload" + ) + assert unreadable_response.status_code == 404 + assert unreadable_response.json()["detail"] == "Image file could not be read" + + def test_single_nodeimage_upload_rejects_missing_config_and_upstream_failure( client, monkeypatch, @@ -76,19 +169,19 @@ def test_single_nodeimage_upload_rejects_missing_config_and_upstream_failure( async def fail_upload(*_args): raise nodeimage_client.NodeImageUploadError("NodeImage quota exceeded") - monkeypatch.setattr(gallery_queries_router, "upload_image_bytes", fail_upload) + monkeypatch.setattr(gallery_queries_router, "upload_image_file", fail_upload) failed = client.post("/api/gallery/node-failure/nodeimage-upload") assert failed.status_code == 502 assert failed.json()["detail"] == "NodeImage quota exceeded" -def test_batch_nodeimage_upload_reports_partial_results(client, monkeypatch): +def test_batch_nodeimage_upload_reports_partial_results(client, monkeypatch, caplog): _fake_gallery_entry("node-batch-1", "one", "1024x1024", "node-batch-1.png") _fake_gallery_entry("node-batch-2", "two", "1024x1024", "node-batch-2.png") _fake_gallery_entry("node-batch-3", "three", "1024x1024", "node-batch-3.png") _enable_nodeimage() - async def fake_upload(_image_bytes, filename, _effective): + async def fake_upload(_path, filename, _effective): if filename == "node-batch-2.png": raise nodeimage_client.NodeImageUploadError("upstream unavailable") if filename == "node-batch-3.png": @@ -98,21 +191,19 @@ def test_batch_nodeimage_upload_reports_partial_results(client, monkeypatch): markdown=f"![image](https://cdn.nodeimage.com/{filename})", ) - monkeypatch.setattr(gallery_batch, "upload_image_bytes", fake_upload) - response = client.post( - "/api/gallery/batch/nodeimage-upload", - json={ - "ids": [ - "node-batch-1", - "missing-node", - "node-batch-2", - "node-batch-3", - ] - }, + monkeypatch.setattr(gallery_jobs, "upload_image_file", fake_upload) + caplog.set_level(logging.ERROR, logger=gallery_jobs.__name__) + created = _start_nodeimage_batch( + client, + [ + "node-batch-1", + "missing-node", + "node-batch-2", + "node-batch-3", + ], ) + body = _wait_for_nodeimage_job(client, created["job_id"]) - assert response.status_code == 200 - body = response.json() assert body["requested_count"] == 4 assert body["uploaded_count"] == 1 assert body["failed_count"] == 3 @@ -130,6 +221,13 @@ def test_batch_nodeimage_upload_reports_partial_results(client, monkeypatch): assert body["results"][2]["filename"] == "node-batch-2.png" assert body["results"][3]["error"] == "Unexpected upload failure" assert body["results"][3]["filename"] == "node-batch-3.png" + assert "unexpected upstream shape" not in json.dumps(body) + unexpected_record = next( + record + for record in caplog.records + if "Unexpected NodeImage upload failure" in record.getMessage() + ) + assert unexpected_record.exc_info is not None def test_batch_nodeimage_auth_probe_stops_uploads_and_preserves_file_errors( @@ -165,45 +263,33 @@ def test_batch_nodeimage_auth_probe_stops_uploads_and_preserves_file_errors( calls = [] - async def reject_upload(_image_bytes, filename, _effective): + async def reject_upload(_path, filename, _effective): calls.append(filename) raise nodeimage_client.NodeImageAuthError( "NodeImage API key was rejected." ) - original_read_bytes = Path.read_bytes + unreadable_path = Path(config.IMAGES_DIR) / "node-unreadable.png" + unreadable_path.unlink() + unreadable_path.mkdir() - def read_bytes(path): - if path.name == "node-unreadable.png": - raise PermissionError("not readable") - return original_read_bytes(path) + def fail_read_bytes(_path): + raise AssertionError("NodeImage batch upload must not read the whole file") - def fail_reference_check(_filename): - raise AssertionError("batch upload performed a gallery reference query") + monkeypatch.setattr(gallery_jobs, "upload_image_file", reject_upload) + monkeypatch.setattr(Path, "read_bytes", fail_read_bytes) - monkeypatch.setattr(gallery_batch, "upload_image_bytes", reject_upload) - monkeypatch.setattr(Path, "read_bytes", read_bytes) - monkeypatch.setattr( - gallery_common, - "is_gallery_filename_referenced", - fail_reference_check, + created = _start_nodeimage_batch( + client, + [ + "node-missing-file", + "missing-node", + "node-auth-probe", + "node-unreadable", + "node-not-attempted", + ], ) - - response = client.post( - "/api/gallery/batch/nodeimage-upload", - json={ - "ids": [ - "node-missing-file", - "missing-node", - "node-auth-probe", - "node-unreadable", - "node-not-attempted", - ] - }, - ) - - assert response.status_code == 200 - body = response.json() + body = _wait_for_nodeimage_job(client, created["job_id"]) assert calls == ["node-auth-probe.png"] assert body["requested_count"] == 5 assert body["uploaded_count"] == 0 @@ -246,7 +332,7 @@ def test_batch_nodeimage_probe_then_limits_concurrency(client, monkeypatch): max_active = 0 calls = [] - async def fake_upload(_image_bytes, filename, _effective): + async def fake_upload(_path, filename, _effective): nonlocal active, max_active calls.append(filename) active += 1 @@ -258,14 +344,10 @@ def test_batch_nodeimage_probe_then_limits_concurrency(client, monkeypatch): markdown=f"![image](https://cdn.nodeimage.com/{filename})", ) - monkeypatch.setattr(gallery_batch, "upload_image_bytes", fake_upload) - response = client.post( - "/api/gallery/batch/nodeimage-upload", - json={"ids": ids}, - ) - - assert response.status_code == 200 - body = response.json() + config.NODEIMAGE_UPLOAD_CONCURRENCY = 4 + monkeypatch.setattr(gallery_jobs, "upload_image_file", fake_upload) + created = _start_nodeimage_batch(client, ids) + body = _wait_for_nodeimage_job(client, created["job_id"]) assert calls[0] == "node-concurrent-0.png" assert len(calls) == 7 assert max_active == 4 @@ -290,7 +372,7 @@ def test_batch_nodeimage_stops_queued_uploads_after_late_auth_failure( calls = [] - async def fake_upload(_image_bytes, filename, _effective): + async def fake_upload(_path, filename, _effective): calls.append(filename) if filename == "node-late-auth-0.png": return nodeimage_client.NodeImageUploadResult( @@ -308,15 +390,14 @@ def test_batch_nodeimage_stops_queued_uploads_after_late_auth_failure( markdown=f"![image](https://cdn.nodeimage.com/{filename})", ) - monkeypatch.setattr(gallery_batch, "upload_image_bytes", fake_upload) - response = client.post( - "/api/gallery/batch/nodeimage-upload", - json={"ids": ids}, - ) - - assert response.status_code == 200 - body = response.json() - assert calls == [f"node-late-auth-{index}.png" for index in range(5)] + config.NODEIMAGE_UPLOAD_CONCURRENCY = 4 + monkeypatch.setattr(gallery_jobs, "upload_image_file", fake_upload) + created = _start_nodeimage_batch(client, ids) + body = _wait_for_nodeimage_job(client, created["job_id"]) + assert calls[0] == "node-late-auth-0.png" + assert set(calls) == { + f"node-late-auth-{index}.png" for index in range(5) + } assert body["uploaded_count"] == 4 assert body["failed_count"] == 3 assert [item["error"] for item in body["results"][5:]] == [ @@ -346,13 +427,13 @@ def test_batch_nodeimage_upload_accepts_filtered_selection_token(client, monkeyp ) _enable_nodeimage() - async def fake_upload(_image_bytes, filename, _effective): + async def fake_upload(_path, filename, _effective): return nodeimage_client.NodeImageUploadResult( url=f"https://cdn.nodeimage.com/{filename}", markdown=f"![image](https://cdn.nodeimage.com/{filename})", ) - monkeypatch.setattr(gallery_batch, "upload_image_bytes", fake_upload) + monkeypatch.setattr(gallery_jobs, "upload_image_file", fake_upload) token_response = client.post( "/api/gallery/batch/selection-tokens", json={"filters": {"prompt": "nodeimage token match"}}, @@ -360,13 +441,12 @@ def test_batch_nodeimage_upload_accepts_filtered_selection_token(client, monkeyp assert token_response.status_code == 201 assert token_response.json()["count"] == 2 - response = client.post( + created = client.post( "/api/gallery/batch/nodeimage-upload", json={"selection_token": token_response.json()["selection_token"]}, ) - - assert response.status_code == 200 - body = response.json() + assert created.status_code == 202 + body = _wait_for_nodeimage_job(client, created.json()["job_id"]) assert body["requested_count"] == 2 assert body["uploaded_count"] == 2 assert body["failed_count"] == 0 @@ -388,11 +468,7 @@ def test_batch_nodeimage_upload_rejects_selection_token_over_limit( f"node-token-limit-{index}.png", ) - async def fail_upload(_image_bytes, _filename, _effective): - raise AssertionError("NodeImage upload should not start over the batch cap") - monkeypatch.setattr(gallery_batch, "NODEIMAGE_BATCH_MAX", 2) - monkeypatch.setattr(gallery_batch, "upload_image_bytes", fail_upload) token_response = client.post( "/api/gallery/batch/selection-tokens", json={"filters": {"prompt": "nodeimage token over limit"}}, @@ -410,10 +486,173 @@ def test_batch_nodeimage_upload_rejects_selection_token_over_limit( assert "selected 3" in response.json()["detail"] +def test_nodeimage_batch_rejects_oversized_files_before_upload(client, monkeypatch): + entry = _fake_gallery_entry( + "node-oversized", + "oversized", + "1024x1024", + "node-oversized.png", + ) + _enable_nodeimage() + config.MAX_FILE_SIZE_MB = 1 + oversized_path = Path(config.IMAGES_DIR) / entry.filename + oversized_path.write_bytes(b"x" * (config.MAX_FILE_SIZE_MB * 1024 * 1024 + 1)) + calls = [] + + async def fail_upload(_path, filename, _effective): + calls.append(filename) + raise AssertionError("oversized files must fail before upload") + + monkeypatch.setattr(gallery_jobs, "upload_image_file", fail_upload) + created = _start_nodeimage_batch(client, [entry.id]) + body = _wait_for_nodeimage_job(client, created["job_id"]) + + assert calls == [] + assert body["status"] == "partial_failure" + assert body["failed_count"] == 1 + assert body["results"][0]["error"] == "Image file is too large. Max size is 1 MB" + + +def test_nodeimage_batch_cancellation_preserves_inflight_results(client, monkeypatch): + ids = [f"node-cancel-{index}" for index in range(3)] + for image_id in ids: + _fake_gallery_entry(image_id, image_id, "1024x1024", f"{image_id}.png") + _enable_nodeimage() + config.NODEIMAGE_UPLOAD_CONCURRENCY = 1 + started = threading.Event() + release = threading.Event() + calls = [] + + async def slow_upload(_path, filename, _effective): + calls.append(filename) + started.set() + await asyncio.to_thread(release.wait, 3) + return nodeimage_client.NodeImageUploadResult( + url=f"https://cdn.nodeimage.com/{filename}", + markdown=f"![image](https://cdn.nodeimage.com/{filename})", + ) + + monkeypatch.setattr(gallery_jobs, "upload_image_file", slow_upload) + created = _start_nodeimage_batch(client, ids) + assert started.wait(2) + + cancel_response = client.delete( + f"/api/gallery/nodeimage-upload-jobs/{created['job_id']}" + ) + assert cancel_response.status_code == 200 + assert cancel_response.json()["stage"] == "cancelling" + release.set() + + body = _wait_for_nodeimage_job(client, created["job_id"]) + assert body["status"] == "cancelled" + assert body["uploaded_count"] == 1 + assert body["failed_count"] == 0 + assert body["cancelled_count"] == 2 + assert [item["status"] for item in body["results"]] == [ + "ok", + "cancelled", + "cancelled", + ] + assert calls == [f"{ids[0]}.png"] + + +def test_nodeimage_batch_capacity_is_reserved_before_dispatch(client, monkeypatch): + ids = ["node-capacity-0", "node-capacity-1"] + for image_id in ids: + _fake_gallery_entry(image_id, image_id, "1024x1024", f"{image_id}.png") + _enable_nodeimage() + started = threading.Event() + release = threading.Event() + + async def slow_upload(_path, filename, _effective): + started.set() + await asyncio.to_thread(release.wait, 3) + return nodeimage_client.NodeImageUploadResult( + url=f"https://cdn.nodeimage.com/{filename}", + markdown=f"![image](https://cdn.nodeimage.com/{filename})", + ) + + monkeypatch.setattr(gallery_jobs, "upload_image_file", slow_upload) + created = _start_nodeimage_batch(client, ids) + assert started.wait(2) + + rejected = client.post( + "/api/gallery/batch/nodeimage-upload", + json={"ids": [ids[0]]}, + ) + assert rejected.status_code == 429 + assert "active NodeImage upload jobs" in rejected.json()["detail"] + release.set() + _wait_for_nodeimage_job(client, created["job_id"]) + + +def test_nodeimage_batch_sse_replays_persisted_terminal_results(client, monkeypatch): + _fake_gallery_entry("node-sse", "sse", "1024x1024", "node-sse.png") + _enable_nodeimage() + + async def fake_upload(_path, filename, _effective): + return nodeimage_client.NodeImageUploadResult( + url=f"https://cdn.nodeimage.com/{filename}", + markdown=f"![image](https://cdn.nodeimage.com/{filename})", + ) + + monkeypatch.setattr(gallery_jobs, "upload_image_file", fake_upload) + created = _start_nodeimage_batch(client, ["node-sse"]) + body = _wait_for_nodeimage_job(client, created["job_id"]) + assert body["status"] == "success" + + events = client.get(body["events_url"]) + assert events.status_code == 200 + assert "event: nodeimage_upload" in events.text + assert '"status":"success"' in events.text + assert "https://cdn.nodeimage.com/node-sse.png" in events.text + + +def test_nodeimage_batch_recovery_skips_persisted_completed_items(client, monkeypatch): + ids = ["node-recovery-0", "node-recovery-1"] + for image_id in ids: + _fake_gallery_entry(image_id, image_id, "1024x1024", f"{image_id}.png") + _enable_nodeimage() + calls = [] + first_result = gallery_jobs._nodeimage_result_item( + ids[0], + f"{ids[0]}.png", + "ok", + url="https://cdn.nodeimage.com/recovered.png", + markdown="![recovered](https://cdn.nodeimage.com/recovered.png)", + ) + job = gallery_jobs._build_nodeimage_upload_job(ids, 2, []) + job.update( + status="running", + stage="uploading", + lease_owner="recovery-worker", + lease_expires_at=gallery_jobs._gallery_job_lease_expires_at(), + payload={"ids": ids, "results": [first_result], "cancel_requested": False}, + ) + stored_job = create_gallery_job(**job) + + async def fake_upload(_path, filename, _effective): + calls.append(filename) + return nodeimage_client.NodeImageUploadResult( + url=f"https://cdn.nodeimage.com/{filename}", + markdown=f"![image](https://cdn.nodeimage.com/{filename})", + ) + + monkeypatch.setattr(gallery_jobs, "upload_image_file", fake_upload) + asyncio.run(gallery_jobs._run_nodeimage_upload_job(stored_job)) + recovered = get_gallery_job("nodeimage_upload", stored_job["job_id"]) + + assert recovered is not None + assert recovered["status"] == "success" + assert calls == [f"{ids[1]}.png"] + assert [item["image_id"] for item in recovered["payload"]["results"]] == ids + + def test_nodeimage_settings_round_trip_and_secret_origin_binding(client): current = client.get("/api/settings").json() assert current["nodeimage"]["enabled"] is False assert current["nodeimage"]["has_api_key"] is False + assert current["nodeimage"]["api_key_resolvable"] is False payload = { "active_preset_id": current["active_preset_id"], @@ -434,6 +673,7 @@ def test_nodeimage_settings_round_trip_and_secret_origin_binding(client): "enabled": True, "api_key_masked": "${TEST_NODEIMAGE_API_KEY}", "has_api_key": True, + "api_key_resolvable": True, "api_key_source": "env", "api_key_env_var": "TEST_NODEIMAGE_API_KEY", "api_key_secret_id": None, @@ -457,6 +697,62 @@ def test_nodeimage_settings_round_trip_and_secret_origin_binding(client): assert "not bound to the target origin" in rejected.json()["detail"] +def test_nodeimage_settings_reports_whether_saved_key_can_be_resolved( + client, + monkeypatch, + request, +): + settings_repo.save_nodeimage_settings( + { + "enabled": True, + "api_key": "${UNSET_NODEIMAGE_API_KEY}", + } + ) + monkeypatch.delenv("UNSET_NODEIMAGE_API_KEY", raising=False) + + unresolved = client.get("/api/settings") + assert unresolved.status_code == 200 + assert unresolved.json()["nodeimage"] == { + "enabled": True, + "api_key_masked": "${UNSET_NODEIMAGE_API_KEY}", + "has_api_key": True, + "api_key_resolvable": False, + "api_key_source": "env", + "api_key_env_var": "UNSET_NODEIMAGE_API_KEY", + "api_key_secret_id": None, + } + + monkeypatch.setenv("UNSET_NODEIMAGE_API_KEY", "resolved-nodeimage-key") + resolved = client.get("/api/settings") + assert resolved.status_code == 200 + assert resolved.json()["nodeimage"]["api_key_resolvable"] is True + + request.addfinalizer(lambda: secrets.configure_registry("")) + secrets.configure_registry( + json.dumps( + { + "nodeimage-invalid-binding": { + "purpose": "nodeimage_api_key", + "origin": "https://example.com", + "env": "UNSET_NODEIMAGE_API_KEY", + } + } + ) + ) + settings_repo.save_nodeimage_settings( + { + "enabled": True, + "api_key": "nodeimage-invalid-binding", + } + ) + invalid_binding = client.get("/api/settings") + assert invalid_binding.status_code == 200 + assert invalid_binding.json()["nodeimage"]["has_api_key"] is True + assert invalid_binding.json()["nodeimage"]["api_key_source"] == "registry" + assert invalid_binding.json()["nodeimage"]["api_key_resolvable"] is False + secrets.configure_registry("") + + def test_nodeimage_settings_plaintext_path_is_opt_in_and_masked(client): current = client.get("/api/settings").json() payload = { @@ -531,6 +827,53 @@ class _Pool: return self.session +class _TrackingFile(io.BytesIO): + pass + + +def test_nodeimage_client_reopens_and_closes_streaming_source_on_retry(monkeypatch): + effective = nodeimage_client.NodeImageEffectiveSettings(True, "test-key") + session = _Session( + [ + _Response(503, {"success": False, "error": "temporary failure"}), + _Response( + 200, + { + "success": True, + "links": { + "direct": "https://cdn.nodeimage.com/stream.png", + "markdown": "![stream](https://cdn.nodeimage.com/stream.png)", + }, + }, + ), + ] + ) + opened = [] + + def source(): + handle = _TrackingFile(PNG_BYTES) + opened.append(handle) + return handle + + monkeypatch.setattr(nodeimage_client, "get_pool", lambda: _Pool(session)) + + async def no_sleep(_delay): + return None + + monkeypatch.setattr(nodeimage_client.asyncio, "sleep", no_sleep) + result = asyncio.run( + nodeimage_client.upload_image_source(source, "stream.png", effective) + ) + + assert result.url.endswith("stream.png") + assert len(opened) == 2 + assert all(handle.closed for handle in opened) + assert all( + getattr(call[1]["data"], "_fields", [({}, {}, handle)])[0][2] is handle + for call, handle in zip(session.calls, opened) + ) + + def test_nodeimage_client_retries_5xx_once_and_does_not_retry_auth(monkeypatch): effective = nodeimage_client.NodeImageEffectiveSettings(True, "test-key") session = _Session( @@ -677,6 +1020,170 @@ def test_nodeimage_client_retries_connect_error_once(monkeypatch): assert len(session.calls) == 2 +def test_nodeimage_client_retries_unreadable_5xx_response_once(monkeypatch): + effective = nodeimage_client.NodeImageEffectiveSettings(True, "test-key") + session = _Session( + [ + _Response(503, aiohttp.ClientConnectionError("response interrupted")), + _Response( + 200, + { + "success": True, + "links": { + "direct": "https://cdn.nodeimage.com/recovered.png", + }, + }, + ), + ] + ) + monkeypatch.setattr(nodeimage_client, "get_pool", lambda: _Pool(session)) + + async def no_sleep(_delay): + return None + + monkeypatch.setattr(nodeimage_client.asyncio, "sleep", no_sleep) + result = asyncio.run( + nodeimage_client.upload_image_bytes(PNG_BYTES, "recovered.png", effective) + ) + + assert result.url.endswith("recovered.png") + assert len(session.calls) == 2 + + +def test_nodeimage_client_generates_markdown_from_validated_direct_url(monkeypatch): + effective = nodeimage_client.NodeImageEffectiveSettings(True, "test-key") + direct = "https://cdn.nodeimage.com/image(1).png?caption=]()<>" + session = _Session( + [ + _Response( + 200, + { + "success": True, + "links": { + "direct": direct, + "markdown": "![attacker](javascript:alert(1))", + }, + }, + ) + ] + ) + monkeypatch.setattr(nodeimage_client, "get_pool", lambda: _Pool(session)) + + result = asyncio.run( + nodeimage_client.upload_image_bytes( + PNG_BYTES, + "../bad](*).png", + effective, + ) + ) + + assert result.url == direct + assert result.markdown == ( + r"![bad\]\(\*\).png](https://cdn.nodeimage.com/" + r"image%281%29.png?caption=]%28%29%3C%3E)" + ) + assert "javascript" not in result.markdown + + +@pytest.mark.parametrize( + ("filename", "expected_content_type"), + [ + ("image.webp", "image/webp"), + ("image.avif", "image/avif"), + ("image.heif", "image/heif"), + ("image.unknown-extension", "application/octet-stream"), + ], +) +def test_nodeimage_client_uses_stable_content_types( + monkeypatch, + filename, + expected_content_type, +): + effective = nodeimage_client.NodeImageEffectiveSettings(True, "test-key") + session = _Session( + [ + _Response( + 200, + { + "success": True, + "links": {"direct": f"https://cdn.nodeimage.com/{filename}"}, + }, + ) + ] + ) + monkeypatch.setattr(nodeimage_client, "get_pool", lambda: _Pool(session)) + + asyncio.run(nodeimage_client.upload_image_bytes(PNG_BYTES, filename, effective)) + + form = session.calls[0][1]["data"] + assert form._fields[0][1]["Content-Type"] == expected_content_type + + +def test_nodeimage_file_inspection_returns_path_or_structured_error( + tmp_path, + monkeypatch, +): + entry = GalleryEntry( + id="inspection", + prompt="inspection", + size="1x1", + filename="inspection.png", + created_at="2026-01-01T00:00:00Z", + ) + + monkeypatch.setattr(gallery_jobs, "safe_image_path", lambda _filename: None) + missing = gallery_jobs._inspect_nodeimage_file(entry) + assert isinstance(missing, dict) + assert missing["error"] == "Image file not found" + + missing_path = tmp_path / "missing.png" + monkeypatch.setattr( + gallery_jobs, + "safe_image_path", + lambda _filename: missing_path, + ) + assert gallery_jobs._inspect_nodeimage_file(entry)["error"] == "Image file not found" + + empty_path = tmp_path / "empty.png" + empty_path.write_bytes(b"") + monkeypatch.setattr( + gallery_jobs, + "safe_image_path", + lambda _filename: empty_path, + ) + assert gallery_jobs._inspect_nodeimage_file(entry)["error"] == "Image file is empty" + + class UnreadablePath: + def stat(self): + return SimpleNamespace(st_size=1) + + def open(self, _mode): + raise OSError("unreadable") + + monkeypatch.setattr( + gallery_jobs, + "safe_image_path", + lambda _filename: UnreadablePath(), + ) + unreadable = gallery_jobs._inspect_nodeimage_file(entry) + assert unreadable["error"] == "Image file could not be read" + + valid_path = tmp_path / "valid.png" + valid_path.write_bytes(PNG_BYTES) + original_limit = config.MAX_FILE_SIZE_MB + monkeypatch.setattr(config, "MAX_FILE_SIZE_MB", 0) + monkeypatch.setattr( + gallery_jobs, + "safe_image_path", + lambda _filename: valid_path, + ) + oversized = gallery_jobs._inspect_nodeimage_file(entry) + assert "too large" in oversized["error"] + + monkeypatch.setattr(config, "MAX_FILE_SIZE_MB", original_limit) + assert gallery_jobs._inspect_nodeimage_file(entry) == valid_path + + def test_nodeimage_client_does_not_retry_ambiguous_network_errors(monkeypatch): effective = nodeimage_client.NodeImageEffectiveSettings(True, "test-key") diff --git a/backend/tests/test_secret_registry_review_regressions.py b/backend/tests/test_secret_registry_review_regressions.py index 8cdcf41..aacfcb3 100644 --- a/backend/tests/test_secret_registry_review_regressions.py +++ b/backend/tests/test_secret_registry_review_regressions.py @@ -1,6 +1,10 @@ +import json + +import pytest from fastapi import FastAPI, HTTPException from fastapi.testclient import TestClient +from backend.app.api import presets from backend.app.api.middleware import register_exception_handlers from backend.app.core import secrets from backend.app.core import settings as config @@ -8,6 +12,100 @@ from backend.app.integrations.r2 import config as r2_config from backend.app.repositories import db as db_repo +def test_resolve_secret_reference_supports_all_setting_sources(monkeypatch): + target_url = "https://api.nodeimage.com" + options = { + "purpose": "nodeimage_api_key", + "target_url": target_url, + "host_allowlist": "api.nodeimage.com", + "field_name": "NodeImage API key", + } + + assert secrets.resolve_secret_reference("", **options) == "" + assert secrets.resolve_secret_reference("literal-key", **options) == "literal-key" + + monkeypatch.setenv("NODEIMAGE_REFERENCE_KEY", "env-key") + assert ( + secrets.resolve_secret_reference("${NODEIMAGE_REFERENCE_KEY}", **options) + == "env-key" + ) + monkeypatch.delenv("NODEIMAGE_REFERENCE_KEY") + with pytest.raises( + secrets.SecretRegistryError, + match="NodeImage API key environment variable NODEIMAGE_REFERENCE_KEY", + ): + secrets.resolve_secret_reference("${NODEIMAGE_REFERENCE_KEY}", **options) + + monkeypatch.setenv("NODEIMAGE_REGISTRY_KEY", "registry-key") + secrets.configure_registry( + json.dumps( + { + "nodeimage-registry-key": { + "purpose": "nodeimage_api_key", + "origin": target_url, + "env": "NODEIMAGE_REGISTRY_KEY", + } + } + ) + ) + assert ( + secrets.resolve_secret_reference("nodeimage-registry-key", **options) + == "registry-key" + ) + + with pytest.raises(secrets.SecretRegistryError, match="not permitted"): + secrets.resolve_secret_reference( + "nodeimage-registry-key", + **{**options, "purpose": "upstream_api"}, + ) + + +def test_api_key_response_fields_masks_plaintext_for_all_consumers(monkeypatch): + monkeypatch.setenv("RESPONSE_REGISTRY_KEY", "registry-value") + secrets.configure_registry( + json.dumps( + { + "response-registry-key": { + "purpose": "upstream_api", + "origin": "https://api.example.com", + "env": "RESPONSE_REGISTRY_KEY", + } + } + ) + ) + + fields = presets.api_key_response_fields("plain-secret-value") + assert fields == { + "api_key_masked": "plai***alue", + "has_api_key": True, + "api_key_source": "stored", + "api_key_env_var": None, + "api_key_secret_id": None, + } + assert "plain-secret-value" not in str(fields) + + assert presets.api_key_response_fields("${RESPONSE_ENV_KEY}")["api_key_source"] == "env" + registry_fields = presets.api_key_response_fields("response-registry-key") + assert registry_fields["api_key_source"] == "registry" + assert registry_fields["api_key_secret_id"] == "response-registry-key" + + r2 = presets.build_r2_backup_settings_response( + { + "enabled": True, + "endpoint_url": "https://account.r2.cloudflarestorage.com", + "bucket_name": "images", + "access_key_id": "plain-access-key", + "secret_access_key": "plain-secret-key", + } + ).model_dump() + assert r2["access_key_id_source"] == "stored" + assert r2["access_key_id_secret_id"] is None + assert r2["secret_access_key_source"] == "stored" + assert r2["secret_access_key_secret_id"] is None + assert "plain-access-key" not in str(r2) + assert "plain-secret-key" not in str(r2) + + def test_default_settings_do_not_emit_missing_builtin_secret_ids(monkeypatch): secrets.configure_registry("{}") monkeypatch.setattr(config, "DEFAULT_API_KEY", "${OPENAI_API_KEY}") diff --git a/backend/tests/test_security_import_contract.py b/backend/tests/test_security_import_contract.py index 7711ea1..43d7b09 100644 --- a/backend/tests/test_security_import_contract.py +++ b/backend/tests/test_security_import_contract.py @@ -2,15 +2,15 @@ from backend.tests.support.contract import * # noqa: F403 def test_import_archive(client): resp = _post_import_archive(client, _import_archive_bytes()) - assert resp.status_code == 200 - assert resp.json()["status"] == "success" - assert resp.json()["imported"] == 1 + assert resp.status_code == 202 + finished = _wait_for_gallery_import_job(client, resp.json()["job_id"]) + assert finished["status"] == "success" + assert finished["imported_count"] == 1 imported = gallery_queries.get_gallery_entry("import-1") assert imported is not None assert imported.bytes == len(PNG_BYTES) - assert imported.thumbnail_filename is None - assert imported.thumbnail_url == "/api/thumb/import-1.png" + assert imported.thumbnail_url.startswith("/api/thumb/") def test_import_archive_truncates_long_image_filename(client): @@ -22,7 +22,9 @@ def test_import_archive_truncates_long_image_filename(client): _import_archive_bytes(image_name=long_name), ) - assert resp.status_code == 200 + assert resp.status_code == 202 + finished = _wait_for_gallery_import_job(client, resp.json()["job_id"]) + assert finished["status"] == "success" imported = gallery_queries.get_gallery_entry("import-1") assert imported is not None assert imported.filename.endswith(".png") @@ -283,8 +285,10 @@ def test_import_archive_skips_mismatched_png_content(client): _import_archive_bytes(image_name="images/fake.png", image_bytes=b""), ) - assert resp.status_code == 400 - assert resp.json()["detail"] == "No importable images found" + assert resp.status_code == 202 + finished = _wait_for_gallery_import_job(client, resp.json()["job_id"]) + assert finished["status"] == "error" + assert finished["error"] == "No importable images found" def test_safe_image_paths_reject_traversal(client): diff --git a/docker-compose.yml b/docker-compose.yml index e02d4c6..b5a6bf5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: web: - image: ${GPT_IMAGE_PANEL_IMAGE:-ghcr.io/z1rconium/gpt-image-linux:v1.4.1} + image: ${GPT_IMAGE_PANEL_IMAGE:-ghcr.io/z1rconium/gpt-image-linux:v1.4.2} container_name: gpt-image-panel-web ports: - "127.0.0.1:9090:9090" diff --git a/frontend/src/lib/api/types/common.ts b/frontend/src/lib/api/types/common.ts index bf5cff0..a4bc00a 100644 --- a/frontend/src/lib/api/types/common.ts +++ b/frontend/src/lib/api/types/common.ts @@ -9,4 +9,5 @@ export type GenerateJobStatusValue = 'queued' | 'running' | 'success' | 'error' export type GalleryExportJobStatusValue = 'queued' | 'running' | 'success' | 'error'; export type GallerySyncJobStatusValue = 'queued' | 'running' | 'success' | 'error'; export type GalleryImportJobStatusValue = 'queued' | 'running' | 'success' | 'error'; +export type NodeImageUploadJobStatusValue = 'queued' | 'running' | 'success' | 'partial_failure' | 'cancelled' | 'error'; export type AssistantGalleryBatchJobStatusValue = GalleryImportJobStatusValue; diff --git a/frontend/src/lib/api/types/gallery.ts b/frontend/src/lib/api/types/gallery.ts index 295f900..db8b3a8 100644 --- a/frontend/src/lib/api/types/gallery.ts +++ b/frontend/src/lib/api/types/gallery.ts @@ -1,4 +1,9 @@ -import type { GalleryExportJobStatusValue, GalleryImportJobStatusValue, GallerySyncJobStatusValue } from './common'; +import type { + GalleryExportJobStatusValue, + GalleryImportJobStatusValue, + GallerySyncJobStatusValue, + NodeImageUploadJobStatusValue +} from './common'; export type GalleryEntry = { id: string; @@ -72,7 +77,7 @@ export type NodeImageUploadResponse = { export type NodeImageBatchUploadItem = { image_id: string; filename?: string | null; - status: 'ok' | 'error'; + status: 'ok' | 'error' | 'cancelled'; url?: string | null; markdown?: string | null; error?: string | null; @@ -85,6 +90,31 @@ export type NodeImageBatchUploadResponse = { results: NodeImageBatchUploadItem[]; }; +export type NodeImageUploadJobStatus = { + job_id: string; + status: NodeImageUploadJobStatusValue; + stage?: string | null; + message?: string | null; + progress: number; + requested_count: number; + processed_count: number; + uploaded_count: number; + failed_count: number; + cancelled_count: number; + results: NodeImageBatchUploadItem[]; + created_at?: string | null; + started_at?: string | null; + completed_at?: string | null; + updated_at?: string | null; + error?: string | null; + status_url?: string | null; + events_url?: string | null; + cancel_url?: string | null; +}; + +export type NodeImageBatchUploadJobStatus = NodeImageUploadJobStatus; +export type NodeImageBatchUploadCreateResponse = NodeImageUploadJobStatus; + export type GallerySelectionTokenResponse = { selection_token: string; count: number; diff --git a/frontend/src/lib/api/types/settings.ts b/frontend/src/lib/api/types/settings.ts index b8a657b..a77b9ff 100644 --- a/frontend/src/lib/api/types/settings.ts +++ b/frontend/src/lib/api/types/settings.ts @@ -131,6 +131,7 @@ export type NodeImageSettings = { enabled: boolean; api_key_masked: string; has_api_key: boolean; + api_key_resolvable: boolean; api_key_source: ApiKeySource; api_key_env_var?: string | null; api_key_secret_id?: string | null; diff --git a/frontend/src/lib/components/GalleryGrid.svelte b/frontend/src/lib/components/GalleryGrid.svelte index d47f73d..37ef750 100644 --- a/frontend/src/lib/components/GalleryGrid.svelte +++ b/frontend/src/lib/components/GalleryGrid.svelte @@ -202,7 +202,20 @@

{operationStatus.label}

{operationStatus.detail}

-
{operationStatus.progress === null ? $t.gallery.notInterruptible : `${operationStatus.progress}%`}
+
+
{operationStatus.progress === null ? $t.gallery.notInterruptible : `${operationStatus.progress}%`}
+ {#if operationStatus.cancel} + + {/if} +
{#if operationStatus.progress === null} @@ -279,9 +292,9 @@

{image.prompt}

-

{displayImageSize(image)} / {image.model || '-'}

+

{displayImageSize(image)} / {image.model || '-'}

-
+ diff --git a/frontend/src/lib/features/workspace/Workspace.svelte b/frontend/src/lib/features/workspace/Workspace.svelte index dd627b9..c81d124 100644 --- a/frontend/src/lib/features/workspace/Workspace.svelte +++ b/frontend/src/lib/features/workspace/Workspace.svelte @@ -152,7 +152,9 @@ import type { PromptSnippet, PromptSnippetCreateInput, PromptSnippetUpdateInput r2BackupSettings.has_secret_access_key ); $: nodeImageSettings = $settingsStore.settings?.nodeimage || null; - $: nodeImageAvailable = Boolean(nodeImageSettings?.enabled && nodeImageSettings.has_api_key); + $: nodeImageAvailable = Boolean( + nodeImageSettings?.enabled && nodeImageSettings.api_key_resolvable + ); $: syncFormDefaultsToActivePreset($settingsStore.settings); $: if (optimizerAssistantEnabled) void ensurePanel('optimizer', false); diff --git a/frontend/src/lib/i18n/locales/en.ts b/frontend/src/lib/i18n/locales/en.ts index 0cb6837..4ac9370 100644 --- a/frontend/src/lib/i18n/locales/en.ts +++ b/frontend/src/lib/i18n/locales/en.ts @@ -361,19 +361,28 @@ const en = { uploadToNodeImage: 'Upload to NodeImage', uploadSelectedToNodeImage: 'Upload selected to NodeImage', uploadingToNodeImage: 'Uploading to NodeImage...', - nodeImageUploadPreparing: (count: number) => `Uploading ${count} image${count === 1 ? '' : 's'}. Keep this page open.`, + nodeImageUploadPreparing: (count: number) => `Preparing ${count} NodeImage upload${count === 1 ? '' : 's'}.`, + nodeImageUploadProgress: (done: number, total: number, uploaded: number, failed: number) => + `Uploaded ${done} / ${total}; succeeded ${uploaded}, failed ${failed}`, + nodeImageUploadCancelling: 'Cancelling after current uploads finish...', + nodeImageUploadCancelled: (uploaded: number, failed: number) => `NodeImage upload cancelled. ${uploaded} uploaded, ${failed} failed.`, nodeImageResultTitle: 'NodeImage upload results', - nodeImageResultSummary: (uploaded: number, failed: number) => `${uploaded} uploaded${failed ? `, ${failed} failed` : ''}. These links are not saved by GPT Image Panel.`, + nodeImageResultSummary: (uploaded: number, failed: number, cancelled = 0) => `${uploaded} uploaded${failed ? `, ${failed} failed` : ''}${cancelled ? `, ${cancelled} cancelled` : ''}. These links are not saved by GPT Image Panel.`, nodeImageUploaded: 'Uploaded', nodeImageFailed: 'Failed', + nodeImageCancelled: 'Not uploaded', nodeImageDirectLink: 'Direct link', nodeImageMarkdownLink: 'Markdown', copyNodeImageDirectLink: 'Copy direct link', copyNodeImageMarkdownLink: 'Copy Markdown link', + copyAllNodeImageDirectLinks: 'Copy all direct links', + copyAllNodeImageMarkdownLinks: 'Copy all Markdown links', + nodeImageShowMore: (remaining: number) => `Show more (${remaining} remaining)`, nodeImageCopied: 'Copied', nodeImageCopyFailed: 'Could not copy the link', nodeImageUnknownError: 'NodeImage upload failed without an error message.', closeNodeImageResults: 'Close NodeImage results', + cancelOperation: 'Cancel', aiAnalyzeSelected: 'AI analyze', aiAnalyzing: 'Analyzing...', importingArchive: 'Importing archive', @@ -406,7 +415,7 @@ const en = { aiAnalyzeComplete: (done: number, failed: number, missing = 0) => `AI analysis complete. Analyzed ${done}${missing ? `, missing ${missing}` : ''}${failed ? `, failed ${failed}` : ''}.`, browserSavingDownload: 'Receiving ZIP archive. The browser save dialog may appear after download completes.', - notInterruptible: 'Keep open', + notInterruptible: 'Working', filters: 'Filters', favoriteSelected: 'Favorite selected', unfavoriteSelected: 'Unfavorite selected', @@ -510,6 +519,7 @@ const en = { nodeImageUploadComplete: 'Image uploaded to NodeImage', nodeImageUploadFailed: (reason: string) => `NodeImage upload failed: ${reason}`, nodeImageBatchComplete: (success: number, failed: number) => `NodeImage upload complete: ${success} succeeded, ${failed} failed`, + nodeImageBatchCancelled: (success: number, failed: number) => `NodeImage upload cancelled: ${success} succeeded, ${failed} failed`, galleryEditLabel: (filename: string) => `Gallery: ${filename}`, galleryImageReady: 'Gallery image ready for edits', galleryImageNotFound: 'Image not found', diff --git a/frontend/src/lib/i18n/locales/zh-CN.ts b/frontend/src/lib/i18n/locales/zh-CN.ts index 9d5a34d..f8a79af 100644 --- a/frontend/src/lib/i18n/locales/zh-CN.ts +++ b/frontend/src/lib/i18n/locales/zh-CN.ts @@ -363,19 +363,28 @@ const zh: Translation = { uploadToNodeImage: '上传到 NodeImage', uploadSelectedToNodeImage: '上传所选到 NodeImage', uploadingToNodeImage: '正在上传到 NodeImage...', - nodeImageUploadPreparing: (count) => `正在上传 ${count} 张图片,请保持页面打开。`, + nodeImageUploadPreparing: (count) => `正在准备 ${count} 张 NodeImage 上传任务。`, + nodeImageUploadProgress: (done, total, uploaded, failed) => + `已处理 ${done} / ${total};成功 ${uploaded} 张,失败 ${failed} 张`, + nodeImageUploadCancelling: '正在取消,当前上传完成后停止...', + nodeImageUploadCancelled: (uploaded, failed) => `NodeImage 上传已取消。成功 ${uploaded} 张,失败 ${failed} 张。`, nodeImageResultTitle: 'NodeImage 上传结果', - nodeImageResultSummary: (uploaded, failed) => `成功上传 ${uploaded} 张${failed ? `,失败 ${failed} 张` : ''}。GPT Image Panel 不会保存这些链接。`, + nodeImageResultSummary: (uploaded, failed, cancelled = 0) => `成功上传 ${uploaded} 张${failed ? `,失败 ${failed} 张` : ''}${cancelled ? `,取消 ${cancelled} 张` : ''}。GPT Image Panel 不会保存这些链接。`, nodeImageUploaded: '已上传', nodeImageFailed: '失败', + nodeImageCancelled: '未上传', nodeImageDirectLink: '直链', nodeImageMarkdownLink: 'Markdown', copyNodeImageDirectLink: '复制直链', copyNodeImageMarkdownLink: '复制 Markdown 链接', + copyAllNodeImageDirectLinks: '复制全部直链', + copyAllNodeImageMarkdownLinks: '复制全部 Markdown 链接', + nodeImageShowMore: (remaining) => `显示更多(剩余 ${remaining} 条)`, nodeImageCopied: '已复制', nodeImageCopyFailed: '无法复制链接', nodeImageUnknownError: 'NodeImage 上传失败,未返回错误信息。', closeNodeImageResults: '关闭 NodeImage 结果', + cancelOperation: '取消', aiAnalyzeSelected: 'AI 分析', aiAnalyzing: '分析中...', importingArchive: '正在导入归档', @@ -408,7 +417,7 @@ const zh: Translation = { aiAnalyzeComplete: (done, failed, missing = 0) => `AI 分析完成。已分析 ${done}${missing ? `,缺失 ${missing}` : ''}${failed ? `,失败 ${failed}` : ''}。`, browserSavingDownload: '正在接收 ZIP 归档。下载完成后浏览器会保存文件。', - notInterruptible: '保持页面打开', + notInterruptible: '处理中', filters: '筛选', favoriteSelected: '收藏所选', unfavoriteSelected: '取消收藏所选', @@ -512,6 +521,7 @@ const zh: Translation = { nodeImageUploadComplete: '图片已上传到 NodeImage', nodeImageUploadFailed: (reason) => `NodeImage 上传失败:${reason}`, nodeImageBatchComplete: (success, failed) => `NodeImage 上传完成:成功 ${success} 张,失败 ${failed} 张`, + nodeImageBatchCancelled: (success, failed) => `NodeImage 上传已取消:成功 ${success} 张,失败 ${failed} 张`, galleryEditLabel: (filename) => `图库:${filename}`, galleryImageReady: '图库图片已设为编辑源', galleryImageNotFound: '未找到图片', diff --git a/frontend/src/lib/stores/gallery.ts b/frontend/src/lib/stores/gallery.ts index db7df7d..c89b2fe 100644 --- a/frontend/src/lib/stores/gallery.ts +++ b/frontend/src/lib/stores/gallery.ts @@ -35,6 +35,8 @@ export type GalleryOperationStatus = { label: string; detail: string; progress: number | null; + cancel?: () => void | Promise; + cancelPending?: boolean; }; export type GalleryNavigation = 'next' | 'prev' | 'jump'; diff --git a/frontend/src/lib/stores/galleryDeferredActions.ts b/frontend/src/lib/stores/galleryDeferredActions.ts index b7664f2..770ec4c 100644 --- a/frontend/src/lib/stores/galleryDeferredActions.ts +++ b/frontend/src/lib/stores/galleryDeferredActions.ts @@ -13,6 +13,7 @@ import type { GalleryResponse, GallerySyncJobStatus, NodeImageBatchUploadResponse, + NodeImageUploadJobStatus, NodeImageUploadResponse } from '$lib/api/types/gallery'; import type { GalleryLoadOptions, GalleryNavigation, GalleryOperationStatus, GalleryState } from '$lib/stores/gallery'; @@ -20,6 +21,8 @@ import { nodeImageResult, type NodeImageResultItem } from '$lib/stores/nodeImage const STREAMING_ZIP_DOWNLOAD_BYTES_THRESHOLD = 64 * 1024 * 1024; const GALLERY_JOB_EVENT_NETWORK_TIMEOUT_MS = 30_000; +const NODE_IMAGE_CANCEL_POLL_INTERVAL_MS = 250; +const NODE_IMAGE_UPLOAD_TERMINAL_STATUSES = ['success', 'partial_failure', 'cancelled', 'error']; export type GalleryActionDeps = { getState: () => GalleryState; @@ -40,6 +43,8 @@ export type GalleryWaitOptions = { eventsUrl: string; eventNames: string[]; signal?: AbortSignal; + terminalStatuses?: string[]; + resolveErrorStatuses?: boolean; }; function downloadBlob(blob: Blob, filename: string) { @@ -196,6 +201,24 @@ function abortError() { return new DOMException('Gallery operation cancelled', 'AbortError'); } +function waitForAbortableDelay(delayMs: number, signal?: AbortSignal) { + return new Promise((resolve, reject) => { + if (signal?.aborted) { + reject(abortError()); + return; + } + const handleAbort = () => { + clearTimeout(timer); + reject(abortError()); + }; + const timer = setTimeout(() => { + signal?.removeEventListener('abort', handleAbort); + resolve(); + }, delayMs); + signal?.addEventListener('abort', handleAbort, { once: true }); + }); +} + function isAbortError(error: unknown) { return error instanceof Error && error.name === 'AbortError'; } @@ -209,6 +232,7 @@ export function waitForGalleryJob void ): Promise { return new Promise((resolve, reject) => { + const terminalStatuses = new Set(options.terminalStatuses || ['success', 'error']); let settled = false; let source: EventSource | null = null; let networkTimer: ReturnType | null = null; @@ -243,7 +267,7 @@ export function waitForGalleryJob { clearNetworkTimer(); onJob(data); - if (data.status === 'success') { + if (terminalStatuses.has(data.status) && (data.status !== 'error' || options.resolveErrorStatuses)) { settle(() => resolve(data)); } else if (data.status === 'error') { settle(() => reject(new Error(data.error || data.message || get(t).messages.requestFailed))); @@ -312,6 +336,58 @@ function waitForGalleryImportJob( ); } +function waitForNodeImageUploadJob( + jobId: string, + onJob: (job: NodeImageUploadJobStatus) => void, + options: { eventsUrl?: string; signal?: AbortSignal } = {} +): Promise { + return waitForGalleryJob( + { + eventsUrl: options.eventsUrl || `/api/gallery/nodeimage-upload-jobs/${encodeURIComponent(jobId)}/events`, + eventNames: ['nodeimage_upload'], + terminalStatuses: NODE_IMAGE_UPLOAD_TERMINAL_STATUSES, + resolveErrorStatuses: true, + signal: options.signal + }, + onJob + ); +} + +function isNodeImageUploadTerminal(job: NodeImageUploadJobStatus) { + return NODE_IMAGE_UPLOAD_TERMINAL_STATUSES.includes(job.status); +} + +async function waitForNodeImageUploadTerminal( + initialJob: NodeImageUploadJobStatus, + statusUrl: string, + signal?: AbortSignal +) { + let job = initialJob; + while (!isNodeImageUploadTerminal(job)) { + await waitForAbortableDelay(NODE_IMAGE_CANCEL_POLL_INTERVAL_MS, signal); + job = await apiFetch( + statusUrl, + { signal }, + 'checking NodeImage upload cancellation' + ); + } + return job; +} + +function nodeImageJobDetail(job: NodeImageUploadJobStatus, fallbackCount: number) { + const labels = get(t).gallery; + const total = job.requested_count || fallbackCount; + if (job.status === 'cancelled') return labels.nodeImageUploadCancelled(job.uploaded_count, job.failed_count); + if (job.stage === 'cancelling') return labels.nodeImageUploadCancelling; + if (job.status === 'success') return labels.nodeImageUploadProgress(total, total, job.uploaded_count, job.failed_count); + return labels.nodeImageUploadProgress( + Math.min(job.processed_count, total), + total, + job.uploaded_count, + job.failed_count + ); +} + export function createDeferredGalleryActions(deps: GalleryActionDeps) { let nodeImageUploadInProgress = false; @@ -467,7 +543,8 @@ export function createDeferredGalleryActions(deps: GalleryActionDeps) { } ], uploadedCount: 1, - failedCount: 0 + failedCount: 0, + cancelledCount: 0 }); showToast(get(t).messages.nodeImageUploadComplete); } catch (error) { @@ -490,26 +567,19 @@ export function createDeferredGalleryActions(deps: GalleryActionDeps) { nodeImageUploadInProgress = true; const controller = new AbortController(); const unregister = deps.registerAbortController(controller); + let jobId: string | null = null; + let cancelRequested = false; + let cancelUrl: string | null = null; + let statusUrl: string | null = null; + let cancellationPromise: Promise | null = null; + let latestJob: NodeImageUploadJobStatus | null = null; + let terminalHandled = false; const entryLabels = new Map( (state.gallery?.images || []).map((image) => [image.id, image.filename]) ); - deps.setOperationStatus({ - kind: 'nodeimage_upload', - label: get(t).gallery.uploadingToNodeImage, - detail: get(t).gallery.nodeImageUploadPreparing(count), - progress: null - }); - try { - const result = await apiFetch( - '/api/gallery/batch/nodeimage-upload', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(batchRequestBody(state)), - signal: controller.signal - }, - 'uploading selected images to NodeImage' - ); + + const showResult = (result: NodeImageUploadJobStatus | NodeImageBatchUploadResponse) => { + const jobResult = 'job_id' in result ? result : null; const items: NodeImageResultItem[] = result.results.map((item) => ({ imageId: item.image_id, label: item.filename || entryLabels.get(item.image_id) || item.image_id, @@ -521,18 +591,155 @@ export function createDeferredGalleryActions(deps: GalleryActionDeps) { nodeImageResult.show({ items, uploadedCount: result.uploaded_count, - failedCount: result.failed_count + failedCount: result.failed_count, + cancelledCount: jobResult?.cancelled_count || 0 }); + }; + + const showTerminalResult = (job: NodeImageUploadJobStatus) => { + if (terminalHandled) return; + terminalHandled = true; + showResult(job); deps.clearSelection(); + if (job.status === 'error') { + showToast( + get(t).messages.nodeImageUploadFailed(job.error || job.message || get(t).gallery.nodeImageUnknownError), + 'error' + ); + return; + } showToast( - get(t).messages.nodeImageBatchComplete(result.uploaded_count, result.failed_count), - result.failed_count ? 'error' : 'status' + job.status === 'cancelled' + ? get(t).messages.nodeImageBatchCancelled(job.uploaded_count, job.failed_count) + : get(t).messages.nodeImageBatchComplete(job.uploaded_count, job.failed_count), + job.failed_count || job.status === 'cancelled' ? 'error' : 'status' ); + }; + + const cancelUpload = () => { + if (cancellationPromise) return cancellationPromise; + cancellationPromise = (async () => { + cancelRequested = true; + deps.setOperationStatus({ + kind: 'nodeimage_upload', + label: get(t).gallery.uploadingToNodeImage, + detail: get(t).gallery.nodeImageUploadCancelling, + progress: latestJob?.progress ?? null, + cancel: cancelUpload, + cancelPending: true + }); + if (!jobId) { + controller.abort(); + return; + } + + const cancellationController = new AbortController(); + const unregisterCancellation = deps.registerAbortController(cancellationController); + try { + const fallbackStatusUrl = `/api/gallery/nodeimage-upload-jobs/${encodeURIComponent(jobId)}`; + const cancellationRequestUrl = cancelUrl || fallbackStatusUrl; + const requested = await apiFetch( + cancellationRequestUrl, + { + method: cancelUrl ? 'POST' : 'DELETE', + signal: cancellationController.signal + }, + 'cancelling NodeImage upload' + ); + const cancelled = await waitForNodeImageUploadTerminal( + requested, + statusUrl || requested.status_url || fallbackStatusUrl, + cancellationController.signal + ); + latestJob = cancelled; + showTerminalResult(cancelled); + controller.abort(); + } catch (error) { + if (isAbortError(error)) return; + cancelRequested = false; + cancellationPromise = null; + if (latestJob && !isNodeImageUploadTerminal(latestJob)) { + deps.setOperationStatus({ + kind: 'nodeimage_upload', + label: get(t).gallery.uploadingToNodeImage, + detail: nodeImageJobDetail(latestJob, count), + progress: Math.round((Math.min(latestJob.processed_count, latestJob.requested_count || count) / Math.max(1, latestJob.requested_count || count)) * 100), + cancel: cancelUpload, + cancelPending: false + }); + } + const reason = error instanceof Error ? error.message : get(t).messages.requestFailed; + showToast(get(t).messages.nodeImageUploadFailed(reason), 'error'); + } finally { + unregisterCancellation(); + } + })(); + return cancellationPromise; + }; + + deps.setOperationStatus({ + kind: 'nodeimage_upload', + label: get(t).gallery.uploadingToNodeImage, + detail: get(t).gallery.nodeImageUploadPreparing(count), + progress: 0 + }); + try { + const result = await apiFetch( + '/api/gallery/batch/nodeimage-upload', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(batchRequestBody(state)), + signal: controller.signal + }, + 'uploading selected images to NodeImage' + ); + if (!('job_id' in result)) { + showResult(result); + deps.clearSelection(); + showToast( + get(t).messages.nodeImageBatchComplete(result.uploaded_count, result.failed_count), + result.failed_count ? 'error' : 'status' + ); + return; + } + + jobId = result.job_id; + cancelUrl = result.cancel_url || null; + statusUrl = result.status_url || null; + latestJob = result; + deps.setOperationStatus({ + kind: 'nodeimage_upload', + label: get(t).gallery.uploadingToNodeImage, + detail: nodeImageJobDetail(result, count), + progress: Math.round((Math.min(result.processed_count, count) / Math.max(1, count)) * 100), + cancel: cancelUpload + }); + const completedJob = await waitForNodeImageUploadJob( + result.job_id, + (nextJob) => { + latestJob = nextJob; + deps.setOperationStatus({ + kind: 'nodeimage_upload', + label: get(t).gallery.uploadingToNodeImage, + detail: nodeImageJobDetail(nextJob, count), + progress: Math.round((Math.min(nextJob.processed_count, nextJob.requested_count || count) / Math.max(1, nextJob.requested_count || count)) * 100), + cancel: cancelUpload, + cancelPending: cancelRequested + }); + }, + { eventsUrl: result.events_url || undefined, signal: controller.signal } + ); + if (cancelRequested && cancellationPromise) await cancellationPromise; + showTerminalResult(completedJob); } catch (error) { if (isAbortError(error)) return; + if (cancelRequested && cancellationPromise) await cancellationPromise; + if (terminalHandled) return; const reason = error instanceof Error ? error.message : get(t).messages.requestFailed; showToast(get(t).messages.nodeImageUploadFailed(reason), 'error'); } finally { + if (cancellationPromise) await cancellationPromise; unregister(); deps.setOperationStatus(null); nodeImageUploadInProgress = false; diff --git a/frontend/src/lib/stores/nodeImage.ts b/frontend/src/lib/stores/nodeImage.ts index ba2a152..35095fc 100644 --- a/frontend/src/lib/stores/nodeImage.ts +++ b/frontend/src/lib/stores/nodeImage.ts @@ -3,7 +3,7 @@ import { writable } from 'svelte/store'; export type NodeImageResultItem = { imageId: string; label: string; - status: 'ok' | 'error'; + status: 'ok' | 'error' | 'cancelled'; url: string; markdown: string; error: string; @@ -13,6 +13,7 @@ export type NodeImageResultState = { items: NodeImageResultItem[]; uploadedCount: number; failedCount: number; + cancelledCount: number; }; function createNodeImageResultStore() { diff --git a/frontend/tests/e2e/fixtures/mockApi.ts b/frontend/tests/e2e/fixtures/mockApi.ts index f01a1c1..1787b5f 100644 --- a/frontend/tests/e2e/fixtures/mockApi.ts +++ b/frontend/tests/e2e/fixtures/mockApi.ts @@ -40,6 +40,42 @@ type PromptSnippetFixture = { updated_at: string; }; +type NodeImageResultStatus = 'ok' | 'error' | 'cancelled'; +type NodeImageJobStatus = 'queued' | 'running' | 'success' | 'partial_failure' | 'cancelled' | 'error'; + +type NodeImageResultFixture = { + image_id: string; + filename: string | null; + status: NodeImageResultStatus; + url: string | null; + markdown: string | null; + error: string | null; +}; + +type NodeImageJobFixture = { + job_id: string; + status: NodeImageJobStatus; + stage: string; + message: string; + progress: number; + requested_count: number; + processed_count: number; + uploaded_count: number; + failed_count: number; + cancelled_count: number; + results: NodeImageResultFixture[]; + created_at: string; + started_at: string | null; + completed_at: string | null; + updated_at: string; + error: string | null; + status_url: string; + events_url: string; + cancel_url: string; + cancel_requested: boolean; + ids: string[]; +}; + type MockOptions = { authenticated?: boolean; editUploadFailure?: boolean; @@ -55,6 +91,11 @@ type MockOptions = { optimizedPrompts?: string[]; optimizeFailureAt?: number; optimizeDelayMs?: number; + nodeImageBatchDelayMs?: number; + nodeImageBatchFailureIds?: string[]; + nodeImageCancelFailure?: boolean; + nodeImageCancelStatusFailure?: boolean; + nodeImageCancelReturnsCompleted?: boolean; }; const baseGalleryImages: GalleryImageFixture[] = [ @@ -159,6 +200,7 @@ const settingsResponse = { enabled: true, api_key_masked: '${TEST_NODEIMAGE_API_KEY}', has_api_key: true, + api_key_resolvable: true, api_key_source: 'env', api_key_env_var: 'TEST_NODEIMAGE_API_KEY', api_key_secret_id: null @@ -440,8 +482,13 @@ async function mockApi(page: Page, options: MockOptions = {}) { let mockedOverallConfig: any = structuredClone(overallConfigResponse); let optimizerSystemPrompt = 'Default optimizer system prompt'; let selectionTokenSeq = 0; + let nodeImageJobSeq = 0; let imagePromptOptimizeCount = 0; + let nodeImageCancelStatusFailurePending = Boolean(options.nodeImageCancelStatusFailure); const selectionTokens = new Map(); + const nodeImageJobs = new Map(); + const nodeImageCancellationTerminals = new Map(); + const nodeImageFailureIds = new Set(options.nodeImageBatchFailureIds || []); const runningJobs = options.runningJobs ?? []; const generatedJobs = options.generatedJobs?.length ? options.generatedJobs @@ -466,6 +513,178 @@ async function mockApi(page: Page, options: MockOptions = {}) { return body.ids || []; } + function nodeImageResult( + imageId: string, + status: NodeImageResultStatus, + error: string | null = null + ): NodeImageResultFixture { + const image = galleryImages.find((entry) => entry.id === imageId); + const filename = image?.filename || null; + if (status === 'ok' && image) { + const direct = `https://cdn.nodeimage.com/${encodeURIComponent(image.filename)}`; + return { + image_id: imageId, + filename, + status, + url: direct, + markdown: `![${image.prompt}](${direct})`, + error: null + }; + } + return { + image_id: imageId, + filename, + status, + url: null, + markdown: null, + error + }; + } + + function nodeImageResultsById(results: NodeImageResultFixture[]) { + return new Map(results.map((item) => [item.image_id, item])); + } + + function orderedNodeImageResults(job: NodeImageJobFixture, results: NodeImageResultFixture[]) { + const resultsById = nodeImageResultsById(results); + return job.ids.flatMap((imageId) => { + const item = resultsById.get(imageId); + return item ? [item] : []; + }); + } + + function nodeImageSnapshot( + job: NodeImageJobFixture, + status: NodeImageJobStatus, + stage: string, + message: string, + results: NodeImageResultFixture[], + cancelRequested = job.cancel_requested + ): NodeImageJobFixture { + const uploadedCount = results.filter((item) => item.status === 'ok').length; + const failedCount = results.filter((item) => item.status === 'error').length; + const cancelledCount = results.filter((item) => item.status === 'cancelled').length; + const processedCount = results.length; + const terminal = status === 'success' || status === 'partial_failure' || status === 'cancelled' || status === 'error'; + return { + ...job, + status, + stage, + message, + progress: terminal ? 100 : Math.round((processedCount / Math.max(1, job.requested_count)) * 100), + processed_count: processedCount, + uploaded_count: uploadedCount, + failed_count: failedCount, + cancelled_count: cancelledCount, + results, + started_at: status === 'queued' ? null : job.started_at || '2026-05-18T12:00:01Z', + completed_at: terminal ? '2026-05-18T12:00:02Z' : null, + updated_at: '2026-05-18T12:00:01Z', + error: status === 'error' ? message : null, + cancel_requested: cancelRequested + }; + } + + function publicNodeImageJob(job: NodeImageJobFixture) { + const { cancel_requested: _cancelRequested, ids: _ids, ...publicJob } = job; + return publicJob; + } + + function createNodeImageJob(ids: string[]) { + const jobId = `nodeimage-job-${++nodeImageJobSeq}`; + const initialResults = ids.flatMap((imageId) => + galleryImages.some((image) => image.id === imageId) + ? [] + : [nodeImageResult(imageId, 'error', 'Gallery entry not found')] + ); + const baseJob: NodeImageJobFixture = { + job_id: jobId, + status: 'queued', + stage: 'queued', + message: 'Queued NodeImage upload', + progress: 0, + requested_count: ids.length, + processed_count: initialResults.length, + uploaded_count: 0, + failed_count: initialResults.length, + cancelled_count: 0, + results: initialResults, + created_at: '2026-05-18T12:00:00Z', + started_at: null, + completed_at: null, + updated_at: '2026-05-18T12:00:00Z', + error: null, + status_url: `/api/gallery/nodeimage-upload-jobs/${encodeURIComponent(jobId)}`, + events_url: `/api/gallery/nodeimage-upload-jobs/${encodeURIComponent(jobId)}/events`, + cancel_url: `/api/gallery/nodeimage-upload-jobs/${encodeURIComponent(jobId)}/cancel`, + cancel_requested: false, + ids + }; + return baseJob; + } + + function buildNodeImageEvents(job: NodeImageJobFixture) { + const events: NodeImageJobFixture[] = []; + let results = [...job.results]; + let current = nodeImageSnapshot(job, 'running', 'uploading', 'Uploading images to NodeImage', results); + events.push(current); + + for (const imageId of job.ids) { + if (results.some((item) => item.image_id === imageId)) continue; + const status: NodeImageResultStatus = nodeImageFailureIds.has(imageId) ? 'error' : 'ok'; + const item = status === 'error' + ? nodeImageResult(imageId, status, 'NodeImage upload failed in fixture') + : nodeImageResult(imageId, status); + results = [...results, item]; + current = nodeImageSnapshot(job, 'running', 'uploading', 'Uploading images to NodeImage', results); + events.push(current); + } + + const terminalStatus: NodeImageJobStatus = current.failed_count ? 'partial_failure' : 'success'; + events.push( + nodeImageSnapshot( + current, + terminalStatus, + 'completed', + terminalStatus === 'success' ? 'NodeImage upload complete' : 'NodeImage upload completed with failures', + results + ) + ); + return events; + } + + function buildCancelledNodeImageJob(job: NodeImageJobFixture) { + const resultsById = nodeImageResultsById(job.results); + let completedAnUpload = job.results.some((item) => item.status === 'ok'); + for (const imageId of job.ids) { + if (resultsById.has(imageId)) continue; + const image = galleryImages.find((entry) => entry.id === imageId); + if (!image) { + resultsById.set(imageId, nodeImageResult(imageId, 'error', 'Gallery entry not found')); + } else if (!completedAnUpload) { + resultsById.set(imageId, nodeImageResult(imageId, 'ok')); + completedAnUpload = true; + } else { + resultsById.set(imageId, nodeImageResult(imageId, 'cancelled', 'Cancelled before upload')); + } + } + const results = orderedNodeImageResults(job, [...resultsById.values()]); + return nodeImageSnapshot( + job, + 'cancelled', + 'cancelled', + 'NodeImage upload cancelled', + results, + true + ); + } + + function sseBody(events: NodeImageJobFixture[]) { + return events + .map((event) => `event: nodeimage_upload\ndata: ${JSON.stringify(publicNodeImageJob(event))}\n\n`) + .join(''); + } + await page.addInitScript((languageValue: 'en' | 'zh-CN' | null) => { if (languageValue) { localStorage.setItem('gpt-image-panel-language', languageValue); @@ -842,6 +1061,98 @@ async function mockApi(page: Page, options: MockOptions = {}) { ); return; } + const nodeImageJobMatch = url.pathname.match(/^\/api\/gallery\/nodeimage-upload-jobs\/([^/]+)$/); + if (nodeImageJobMatch && request.method() === 'GET') { + const jobId = decodeURIComponent(nodeImageJobMatch[1]); + const cancelledTerminal = nodeImageCancellationTerminals.get(jobId); + if (cancelledTerminal && nodeImageCancelStatusFailurePending) { + nodeImageCancelStatusFailurePending = false; + await route.fulfill(json({ detail: 'Temporary cancellation status failure' }, 503)); + return; + } + if (cancelledTerminal) { + nodeImageCancellationTerminals.delete(jobId); + nodeImageJobs.set(jobId, cancelledTerminal); + } + const nodeImageJob = cancelledTerminal || nodeImageJobs.get(jobId); + await route.fulfill( + nodeImageJob + ? json(publicNodeImageJob(nodeImageJob)) + : json({ detail: 'NodeImage upload job not found' }, 404) + ); + return; + } + const nodeImageEventsMatch = url.pathname.match(/^\/api\/gallery\/nodeimage-upload-jobs\/([^/]+)\/events$/); + if (nodeImageEventsMatch && request.method() === 'GET') { + const jobId = decodeURIComponent(nodeImageEventsMatch[1]); + const nodeImageJob = nodeImageJobs.get(jobId); + if (!nodeImageJob) { + await route.fulfill(json({ detail: 'NodeImage upload job not found' }, 404)); + return; + } + + const events = buildNodeImageEvents(nodeImageJob); + const delayMs = Math.max(0, options.nodeImageBatchDelayMs || 0); + if (delayMs > 0) { + const firstProgress = events[1] || events[0] || nodeImageJob; + nodeImageJobs.set(jobId, firstProgress); + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + + const latestJob = nodeImageJobs.get(jobId) || nodeImageJob; + if (latestJob.cancel_requested || latestJob.status === 'cancelled') { + const cancelledJob = latestJob.status === 'cancelled' ? latestJob : buildCancelledNodeImageJob(latestJob); + nodeImageJobs.set(jobId, cancelledJob); + await route.fulfill({ status: 200, contentType: 'text/event-stream', body: sseBody([cancelledJob]) }); + } else { + const terminalJob = events[events.length - 1] || nodeImageJob; + nodeImageJobs.set(jobId, terminalJob); + await route.fulfill({ status: 200, contentType: 'text/event-stream', body: sseBody(events) }); + } + return; + } + const nodeImageCancelMatch = url.pathname.match(/^\/api\/gallery\/nodeimage-upload-jobs\/([^/]+)(?:\/cancel)?$/); + if ( + nodeImageCancelMatch && + (request.method() === 'DELETE' || (request.method() === 'POST' && url.pathname.endsWith('/cancel'))) + ) { + const jobId = decodeURIComponent(nodeImageCancelMatch[1]); + const nodeImageJob = nodeImageJobs.get(jobId); + if (!nodeImageJob) { + await route.fulfill(json({ detail: 'NodeImage upload job not found' }, 404)); + return; + } + if (options.nodeImageCancelFailure) { + await route.fulfill(json({ detail: 'Temporary cancellation failure' }, 503)); + return; + } + if (options.nodeImageCancelReturnsCompleted) { + const completedJob = buildNodeImageEvents(nodeImageJob).at(-1) || nodeImageJob; + nodeImageJobs.set(jobId, completedJob); + await route.fulfill(json(publicNodeImageJob(completedJob))); + return; + } + const cancelledJob = nodeImageJob.status === 'cancelled' + ? nodeImageJob + : buildCancelledNodeImageJob({ ...nodeImageJob, cancel_requested: true }); + if (nodeImageJob.status === 'queued') { + nodeImageJobs.set(jobId, cancelledJob); + await route.fulfill(json(publicNodeImageJob(cancelledJob))); + return; + } + const cancellingJob = nodeImageSnapshot( + nodeImageJob, + 'running', + 'cancelling', + 'Cancelling NodeImage upload', + nodeImageJob.results, + true + ); + nodeImageJobs.set(jobId, cancellingJob); + nodeImageCancellationTerminals.set(jobId, cancelledJob); + await route.fulfill(json(publicNodeImageJob(cancellingJob))); + return; + } if (url.pathname.match(/^\/api\/gallery\/[^/]+$/) && request.method() === 'GET') { const id = decodeURIComponent(url.pathname.split('/').pop() || ''); const image = galleryImages.find((entry) => entry.id === id); @@ -885,21 +1196,15 @@ async function mockApi(page: Page, options: MockOptions = {}) { return; } if (url.pathname === '/api/gallery/batch/nodeimage-upload' && request.method() === 'POST') { - const body = JSON.parse(request.postData() || '{}'); + const body = request.postDataJSON() as { ids?: string[]; selection_token?: string }; const ids = resolveBatchIds(body); - const results = ids.map((id) => { - const image = galleryImages.find((entry) => entry.id === id); - if (!image) return { image_id: id, filename: null, status: 'error', url: null, markdown: null, error: 'Gallery entry not found' }; - const direct = `https://cdn.nodeimage.com/${encodeURIComponent(image.filename)}`; - return { image_id: id, filename: image.filename, status: 'ok', url: direct, markdown: `![${image.prompt}](${direct})`, error: null }; - }); - const uploadedCount = results.filter((item) => item.status === 'ok').length; - await route.fulfill(json({ - requested_count: ids.length, - uploaded_count: uploadedCount, - failed_count: ids.length - uploadedCount, - results - })); + if (!ids.length) { + await route.fulfill(json({ detail: 'Gallery entries not found' }, 404)); + return; + } + const nodeImageJob = createNodeImageJob(ids); + nodeImageJobs.set(nodeImageJob.job_id, nodeImageJob); + await route.fulfill(json(publicNodeImageJob(nodeImageJob), 202)); return; } if (url.pathname.match(/^\/api\/gallery\/[^/]+\/favorite$/)) { diff --git a/frontend/tests/e2e/gallery.spec.ts b/frontend/tests/e2e/gallery.spec.ts index eee0676..c6702c8 100644 --- a/frontend/tests/e2e/gallery.spec.ts +++ b/frontend/tests/e2e/gallery.spec.ts @@ -70,6 +70,9 @@ test('gallery uploads single and selected images to NodeImage and copies result await loadApp(page); const firstCard = page.locator('.gallery-card').filter({ hasText: 'First gallery image' }); + const actionGrid = firstCard.locator('.gallery-card-actions'); + await expect(actionGrid.locator('button, a')).toHaveCount(7); + expect(await actionGrid.evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(' ').length)).toBe(4); const singleRequest = page.waitForRequest((request) => { const url = new URL(request.url()); return request.method() === 'POST' && url.pathname === '/api/gallery/img-1/nodeimage-upload'; @@ -91,13 +94,153 @@ test('gallery uploads single and selected images to NodeImage and copies result if (request.method() !== 'POST' || url.pathname !== '/api/gallery/batch/nodeimage-upload') return false; return (request.postDataJSON() as { ids?: string[] }).ids?.length === 2; }); + const batchCreateResponse = page.waitForResponse((response) => { + const url = new URL(response.url()); + return response.request().method() === 'POST' && url.pathname === '/api/gallery/batch/nodeimage-upload'; + }); + const batchEventsRequest = page.waitForRequest((request) => { + const url = new URL(request.url()); + return request.method() === 'GET' && /^\/api\/gallery\/nodeimage-upload-jobs\/[^/]+\/events$/.test(url.pathname); + }); await page.getByRole('button', { name: 'Upload selected to NodeImage', exact: true }).click(); await batchRequest; + await expect.poll(async () => (await batchCreateResponse).status()).toBe(202); + await batchEventsRequest; resultDialog = page.getByRole('dialog', { name: 'NodeImage upload results' }); await expect(resultDialog).toBeVisible(); await expect(resultDialog).toContainText('2 uploaded'); - await expect(resultDialog.getByText('Direct link')).toHaveCount(2); + await expect(resultDialog.getByText('Direct link', { exact: true })).toHaveCount(2); +}); + +test('NodeImage batch upload shows queued progress while waiting for job events', async ({ page }) => { + await loadApp(page, { nodeImageBatchDelayMs: 500 }); + + await page.getByRole('button', { name: 'Select' }).click(); + await page.getByRole('button', { name: 'Select page' }).click(); + const createResponse = page.waitForResponse((response) => { + const url = new URL(response.url()); + return response.request().method() === 'POST' && url.pathname === '/api/gallery/batch/nodeimage-upload'; + }); + const eventsRequest = page.waitForRequest((request) => { + const url = new URL(request.url()); + return request.method() === 'GET' && /^\/api\/gallery\/nodeimage-upload-jobs\/[^/]+\/events$/.test(url.pathname); + }); + + await page.getByRole('button', { name: 'Upload selected to NodeImage', exact: true }).click(); + await expect.poll(async () => (await createResponse).status()).toBe(202); + await expect(page.getByRole('status').filter({ hasText: 'Uploading to NodeImage' })).toContainText('Uploaded 0 / 2'); + await expect(page.getByRole('status').filter({ hasText: 'Uploading to NodeImage' })).toContainText('0%'); + await eventsRequest; + + const resultDialog = page.getByRole('dialog', { name: 'NodeImage upload results' }); + await expect(resultDialog).toBeVisible(); + await expect(resultDialog).toContainText('2 uploaded'); +}); + +test('NodeImage batch upload displays partial failure results from the job payload', async ({ page }) => { + await loadApp(page, { nodeImageBatchFailureIds: ['img-2'] }); + + await page.getByRole('button', { name: 'Select' }).click(); + await page.getByRole('button', { name: 'Select page' }).click(); + await page.getByRole('button', { name: 'Upload selected to NodeImage', exact: true }).click(); + + const resultDialog = page.getByRole('dialog', { name: 'NodeImage upload results' }); + await expect(resultDialog).toBeVisible(); + await expect(resultDialog).toContainText('1 uploaded, 1 failed'); + await expect(resultDialog.getByRole('heading', { name: 'img-1.png' })).toBeVisible(); + await expect(resultDialog.getByRole('heading', { name: 'img-2.png' })).toBeVisible(); + await expect(resultDialog).toContainText('NodeImage upload failed in fixture'); +}); + +test('NodeImage batch upload cancellation calls the job cancel endpoint and keeps completed results', async ({ page }) => { + await loadApp(page, { nodeImageBatchDelayMs: 1000 }); + + await page.getByRole('button', { name: 'Select' }).click(); + await page.getByRole('button', { name: 'Select page' }).click(); + const createResponse = page.waitForResponse((response) => { + const url = new URL(response.url()); + return response.request().method() === 'POST' && url.pathname === '/api/gallery/batch/nodeimage-upload'; + }); + const cancelRequest = page.waitForRequest((request) => { + const url = new URL(request.url()); + return request.method() === 'POST' && /^\/api\/gallery\/nodeimage-upload-jobs\/[^/]+\/cancel$/.test(url.pathname); + }); + const terminalStatusRequest = page.waitForRequest((request) => { + const url = new URL(request.url()); + return request.method() === 'GET' && /^\/api\/gallery\/nodeimage-upload-jobs\/[^/]+$/.test(url.pathname); + }); + + await page.getByRole('button', { name: 'Upload selected to NodeImage', exact: true }).click(); + await expect.poll(async () => (await createResponse).status()).toBe(202); + const operationStatus = page.getByRole('status').filter({ hasText: 'Uploading to NodeImage' }); + await expect(operationStatus).toContainText('Uploaded 0 / 2'); + await operationStatus.getByRole('button', { name: 'Cancel', exact: true }).click(); + await cancelRequest; + await terminalStatusRequest; + + const resultDialog = page.getByRole('dialog', { name: 'NodeImage upload results' }); + await expect(resultDialog).toBeVisible(); + await expect(resultDialog).toContainText('1 uploaded, 1 cancelled'); + await expect(resultDialog.getByText('Uploaded', { exact: true })).toHaveCount(1); + await expect(resultDialog.getByText('Not uploaded', { exact: true })).toHaveCount(1); +}); + +test('NodeImage batch upload keeps tracking the job when the cancellation request fails', async ({ page }) => { + await loadApp(page, { nodeImageBatchDelayMs: 750, nodeImageCancelFailure: true }); + + await page.getByRole('button', { name: 'Select' }).click(); + await page.getByRole('button', { name: 'Select page' }).click(); + await page.getByRole('button', { name: 'Upload selected to NodeImage', exact: true }).click(); + + const operationStatus = page.getByRole('status').filter({ hasText: 'Uploading to NodeImage' }); + await expect(operationStatus).toContainText('Uploaded 0 / 2'); + await operationStatus.getByRole('button', { name: 'Cancel', exact: true }).click(); + + await expect(page.getByRole('alert').filter({ hasText: 'Temporary cancellation failure' })).toBeVisible(); + await expect(operationStatus.getByRole('button', { name: 'Cancel', exact: true })).toBeEnabled(); + + const resultDialog = page.getByRole('dialog', { name: 'NodeImage upload results' }); + await expect(resultDialog).toBeVisible(); + await expect(resultDialog).toContainText('2 uploaded'); + await expect(page.getByRole('status').filter({ hasText: 'NodeImage upload complete: 2 succeeded, 0 failed' })).toBeVisible(); +}); + +test('NodeImage batch upload keeps tracking after cancellation status polling fails', async ({ page }) => { + await loadApp(page, { nodeImageBatchDelayMs: 1000, nodeImageCancelStatusFailure: true }); + + await page.getByRole('button', { name: 'Select' }).click(); + await page.getByRole('button', { name: 'Select page' }).click(); + await page.getByRole('button', { name: 'Upload selected to NodeImage', exact: true }).click(); + + const operationStatus = page.getByRole('status').filter({ hasText: 'Uploading to NodeImage' }); + await expect(operationStatus).toContainText('Uploaded 0 / 2'); + await operationStatus.getByRole('button', { name: 'Cancel', exact: true }).click(); + + await expect(page.getByRole('alert').filter({ hasText: 'Temporary cancellation status failure' })).toBeVisible(); + await expect(operationStatus.getByRole('button', { name: 'Cancel', exact: true })).toBeEnabled(); + + const resultDialog = page.getByRole('dialog', { name: 'NodeImage upload results' }); + await expect(resultDialog).toBeVisible(); + await expect(resultDialog).toContainText('1 uploaded, 1 cancelled'); +}); + +test('NodeImage cancellation reports a completed job as complete', async ({ page }) => { + await loadApp(page, { nodeImageBatchDelayMs: 1000, nodeImageCancelReturnsCompleted: true }); + + await page.getByRole('button', { name: 'Select' }).click(); + await page.getByRole('button', { name: 'Select page' }).click(); + await page.getByRole('button', { name: 'Upload selected to NodeImage', exact: true }).click(); + + const operationStatus = page.getByRole('status').filter({ hasText: 'Uploading to NodeImage' }); + await expect(operationStatus).toContainText('Uploaded 0 / 2'); + await operationStatus.getByRole('button', { name: 'Cancel', exact: true }).click(); + + const resultDialog = page.getByRole('dialog', { name: 'NodeImage upload results' }); + await expect(resultDialog).toBeVisible(); + await expect(resultDialog).toContainText('2 uploaded'); + await expect(page.getByRole('status').filter({ hasText: 'NodeImage upload complete: 2 succeeded, 0 failed' })).toBeVisible(); + await expect(page.getByText('NodeImage upload cancelled: 2 succeeded, 0 failed')).toHaveCount(0); }); test('NodeImage batch results use response filenames for cross-page selection', async ({ page }) => { @@ -150,15 +293,65 @@ test('NodeImage batch results fall back for legacy responses without filenames', await expect(resultDialog.getByRole('heading', { name: 'missing-legacy-id' })).toBeVisible(); }); +test('NodeImage results group failures first, progressively reveal successes, and copy complete ordered lists', async ({ page, context }) => { + await context.grantPermissions(['clipboard-read', 'clipboard-write']); + await loadApp(page, { + galleryImages: manyGalleryImages(25), + nodeImageBatchFailureIds: ['paged-img-25'] + }); + + await page.getByLabel('Filter prompt').fill('Paged gallery image'); + await expect(page.getByRole('img', { name: 'Paged gallery image 1', exact: true })).toBeVisible(); + await page.getByRole('button', { name: 'Select' }).click(); + await page.getByRole('button', { name: 'Select filtered' }).click(); + await page.getByRole('button', { name: 'Upload selected to NodeImage', exact: true }).click(); + + const resultDialog = page.getByRole('dialog', { name: 'NodeImage upload results' }); + await expect(resultDialog).toBeVisible(); + await expect(resultDialog.locator('h3')).toHaveText(['Failed (1)', 'Uploaded (24)']); + await expect(resultDialog.getByRole('heading', { name: 'paged-img-25.png' })).toBeVisible(); + await expect(resultDialog.getByRole('heading', { name: 'paged-img-20.png' })).toBeVisible(); + await expect(resultDialog.getByRole('heading', { name: 'paged-img-21.png' })).toBeHidden(); + await expect(resultDialog.getByRole('button', { name: 'Show more (4 remaining)' })).toBeVisible(); + await expect(resultDialog.getByText('Direct link', { exact: true })).toHaveCount(20); + + await resultDialog.getByRole('button', { name: 'Copy all direct links' }).click(); + await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe( + Array.from({ length: 24 }, (_, index) => `https://cdn.nodeimage.com/paged-img-${index + 1}.png`).join('\n') + ); + await resultDialog.getByRole('button', { name: 'Copy all Markdown links' }).click(); + await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe( + Array.from({ length: 24 }, (_, index) => `![Paged gallery image ${index + 1}](https://cdn.nodeimage.com/paged-img-${index + 1}.png)`).join('\n') + ); + + await resultDialog.getByRole('button', { name: 'Show more (4 remaining)' }).click(); + await expect(resultDialog.getByRole('heading', { name: 'paged-img-24.png' })).toBeVisible(); + await expect(resultDialog.getByText('Direct link', { exact: true })).toHaveCount(24); + await expect(resultDialog.getByRole('button', { name: /Show more/ })).toHaveCount(0); + + const desktopBounds = await resultDialog.boundingBox(); + expect(desktopBounds).not.toBeNull(); + expect(desktopBounds!.x).toBeGreaterThanOrEqual(0); + expect(desktopBounds!.x + desktopBounds!.width).toBeLessThanOrEqual(1280); + + await page.setViewportSize({ width: 390, height: 844 }); + const mobileBounds = await resultDialog.boundingBox(); + expect(mobileBounds).not.toBeNull(); + expect(mobileBounds!.x).toBeGreaterThanOrEqual(0); + expect(mobileBounds!.x + mobileBounds!.width).toBeLessThanOrEqual(390); + expect(await resultDialog.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true); +}); + test('gallery hides NodeImage upload actions when the integration is unavailable', async ({ page }) => { await loadApp(page, { settings: { ...settingsResponse, nodeimage: { ...settingsResponse.nodeimage, - enabled: false, - has_api_key: false, - api_key_source: 'empty' + enabled: true, + has_api_key: true, + api_key_resolvable: false, + api_key_source: 'env' } } }); @@ -167,6 +360,9 @@ test('gallery hides NodeImage upload actions when the integration is unavailable await page.getByRole('button', { name: 'Select' }).click(); await page.getByRole('button', { name: 'Select page' }).click(); await expect(page.getByRole('button', { name: 'Upload selected to NodeImage', exact: true })).toHaveCount(0); + const actionGrid = page.locator('.gallery-card').first().locator('.gallery-card-actions'); + await expect(actionGrid.locator('button, a')).toHaveCount(6); + expect(await actionGrid.evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(' ').length)).toBe(4); }); test('lightbox shows a ready thumbnail until the original image loads', async ({ page }) => { diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..08bc412 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,32 @@ +{ + "status": "failed", + "failedTests": [ + "3b3cfdc286a9b76f9528-d96e84e8719b726b0daa", + "3b3cfdc286a9b76f9528-ff8560063d270b1dd155", + "3b3cfdc286a9b76f9528-09014113d7a1df37ad39", + "3b3cfdc286a9b76f9528-c1979dee2cd0c00ecf7f", + "3b3cfdc286a9b76f9528-55c848396ea948a6c6e5", + "3b3cfdc286a9b76f9528-8644ab8290b3da46ff51", + "3b3cfdc286a9b76f9528-797f63156242dca59b00", + "3b3cfdc286a9b76f9528-3721fece076e28824ea0", + "3b3cfdc286a9b76f9528-f8c2b839ee2c69bad400", + "3b3cfdc286a9b76f9528-9e77518bb84ca0fb299d", + "3b3cfdc286a9b76f9528-6ee48429e208bfbb495e", + "3b3cfdc286a9b76f9528-577c35bc12cb66d42bcd", + "3b3cfdc286a9b76f9528-01b6307348578fd1d31d", + "3b3cfdc286a9b76f9528-648c627d9f21674ecacf", + "3b3cfdc286a9b76f9528-65d374b5c4a6eaa3ed1d", + "3b3cfdc286a9b76f9528-3c4b652250cce6219170", + "3b3cfdc286a9b76f9528-59385007ebc826eaa344", + "3b3cfdc286a9b76f9528-8cfce4dd345d93d71d73", + "3b3cfdc286a9b76f9528-85ca498f3207ebcda539", + "3b3cfdc286a9b76f9528-f44c5e6cf5bce1564f4f", + "3b3cfdc286a9b76f9528-e2f227ea31e8bc571e3f", + "3b3cfdc286a9b76f9528-d8fe035d1a6958b9906e", + "3b3cfdc286a9b76f9528-ee3318e221c12316ebe2", + "3b3cfdc286a9b76f9528-7f21b394415c9b25bc3b", + "3b3cfdc286a9b76f9528-e6a26fc8758cc2f32f27", + "3b3cfdc286a9b76f9528-ab132007c8bcff11c698", + "3b3cfdc286a9b76f9528-828b5ca61da2e52f147c" + ] +} \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-08657-images-across-gallery-pages/error-context.md b/test-results/frontend-tests-e2e-gallery-08657-images-across-gallery-pages/error-context.md new file mode 100644 index 0000000..d8fb45c --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-08657-images-across-gallery-pages/error-context.md @@ -0,0 +1,40 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> lightbox navigates images across gallery pages +- Location: frontend/tests/e2e/gallery.spec.ts:340:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-wsfsaD --remote-debugging-pipe --no-startup-window + pid=4576 +[pid=4576][err] [0814/203632.851864:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4576][err] [0814/203632.853373:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4576][err] [0814/203632.853806:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4576][err] [0814/203632.853875:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4576: Permission denied (1100) +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-wsfsaD --remote-debugging-pipe --no-startup-window + - pid=4576 + - [pid=4576][err] [0814/203632.851864:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4576][err] [0814/203632.853373:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4576][err] [0814/203632.853806:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4576][err] [0814/203632.853875:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4576: Permission denied (1100) + - [pid=4576] + - [pid=4576] + - [pid=4576] + - [pid=4576] exception while trying to kill process: Error: kill EPERM + - [pid=4576] + - [pid=4576] starting temporary directories cleanup + - [pid=4576] finished temporary directories cleanup + - [pid=4576] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-1893b--full-generation-parameters/error-context.md b/test-results/frontend-tests-e2e-gallery-1893b--full-generation-parameters/error-context.md new file mode 100644 index 0000000..fc97617 --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-1893b--full-generation-parameters/error-context.md @@ -0,0 +1,40 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> gallery cards can reuse prompt or full generation parameters +- Location: frontend/tests/e2e/gallery.spec.ts:302:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-6TeIgc --remote-debugging-pipe --no-startup-window + pid=4571 +[pid=4571][err] [0814/203632.144245:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4571][err] [0814/203632.146241:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4571][err] [0814/203632.146691:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4571][err] [0814/203632.146738:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4571: Permission denied (1100) +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-6TeIgc --remote-debugging-pipe --no-startup-window + - pid=4571 + - [pid=4571][err] [0814/203632.144245:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4571][err] [0814/203632.146241:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4571][err] [0814/203632.146691:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4571][err] [0814/203632.146738:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4571: Permission denied (1100) + - [pid=4571] + - [pid=4571] + - [pid=4571] + - [pid=4571] exception while trying to kill process: Error: kill EPERM + - [pid=4571] + - [pid=4571] starting temporary directories cleanup + - [pid=4571] finished temporary directories cleanup + - [pid=4571] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-1b480-ne-before-the-server-delete/error-context.md b/test-results/frontend-tests-e2e-gallery-1b480-ne-before-the-server-delete/error-context.md new file mode 100644 index 0000000..890297f --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-1b480-ne-before-the-server-delete/error-context.md @@ -0,0 +1,40 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> single image delete uses custom confirmation and can be undone before the server delete +- Location: frontend/tests/e2e/gallery.spec.ts:700:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-NaiYnN --remote-debugging-pipe --no-startup-window + pid=4600 +[pid=4600][err] [0814/203636.988405:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4600][err] [0814/203636.990020:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4600][err] [0814/203636.990453:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4600][err] [0814/203636.990463:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4600: Permission denied (1100) +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-NaiYnN --remote-debugging-pipe --no-startup-window + - pid=4600 + - [pid=4600][err] [0814/203636.988405:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4600][err] [0814/203636.990020:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4600][err] [0814/203636.990453:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4600][err] [0814/203636.990463:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4600: Permission denied (1100) + - [pid=4600] + - [pid=4600] + - [pid=4600] + - [pid=4600] exception while trying to kill process: Error: kill EPERM + - [pid=4600] + - [pid=4600] starting temporary directories cleanup + - [pid=4600] finished temporary directories cleanup + - [pid=4600] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-1fc69--with-the-remaining-matches/error-context.md b/test-results/frontend-tests-e2e-gallery-1fc69--with-the-remaining-matches/error-context.md new file mode 100644 index 0000000..116b30d --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-1fc69--with-the-remaining-matches/error-context.md @@ -0,0 +1,41 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> favorites-only batch unfavorite reloads the page with the remaining matches +- Location: frontend/tests/e2e/gallery.spec.ts:532:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-5yQ4RZ --remote-debugging-pipe --no-startup-window + pid=4588 +[pid=4588][err] [0814/203634.943384:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4588][err] [0814/203634.944935:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4588][err] [0814/203634.945319:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4588][err] [0814/203634.945427:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4588: Permission denied (1100) +[pid=4588] +[pid=4588] starting temporary directories cleanup +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-5yQ4RZ --remote-debugging-pipe --no-startup-window + - pid=4588 + - [pid=4588][err] [0814/203634.943384:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4588][err] [0814/203634.944935:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4588][err] [0814/203634.945319:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4588][err] [0814/203634.945427:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4588: Permission denied (1100) + - [pid=4588] + - [pid=4588] starting temporary directories cleanup + - [pid=4588] + - [pid=4588] + - [pid=4588] + - [pid=4588] finished temporary directories cleanup + - [pid=4588] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-20e3e-il-the-original-image-loads/error-context.md b/test-results/frontend-tests-e2e-gallery-20e3e-il-the-original-image-loads/error-context.md new file mode 100644 index 0000000..801e1da --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-20e3e-il-the-original-image-loads/error-context.md @@ -0,0 +1,40 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> lightbox shows a ready thumbnail until the original image loads +- Location: frontend/tests/e2e/gallery.spec.ts:255:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-KyD3d2 --remote-debugging-pipe --no-startup-window + pid=4566 +[pid=4566][err] [0814/203631.442148:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4566][err] [0814/203631.444217:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4566][err] [0814/203631.444595:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4566][err] [0814/203631.444685:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4566: Permission denied (1100) +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-KyD3d2 --remote-debugging-pipe --no-startup-window + - pid=4566 + - [pid=4566][err] [0814/203631.442148:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4566][err] [0814/203631.444217:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4566][err] [0814/203631.444595:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4566][err] [0814/203631.444685:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4566: Permission denied (1100) + - [pid=4566] + - [pid=4566] + - [pid=4566] + - [pid=4566] exception while trying to kill process: Error: kill EPERM + - [pid=4566] + - [pid=4566] starting temporary directories cleanup + - [pid=4566] finished temporary directories cleanup + - [pid=4566] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-21b2f-after-the-optimistic-update/error-context.md b/test-results/frontend-tests-e2e-gallery-21b2f-after-the-optimistic-update/error-context.md new file mode 100644 index 0000000..bc97a43 --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-21b2f-after-the-optimistic-update/error-context.md @@ -0,0 +1,40 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> cross-page batch delete refreshes filtered gallery state after the optimistic update +- Location: frontend/tests/e2e/gallery.spec.ts:498:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-4SxCNW --remote-debugging-pipe --no-startup-window + pid=4586 +[pid=4586][err] [0814/203634.602135:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4586][err] [0814/203634.603740:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4586][err] [0814/203634.604137:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4586][err] [0814/203634.604213:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4586: Permission denied (1100) +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-4SxCNW --remote-debugging-pipe --no-startup-window + - pid=4586 + - [pid=4586][err] [0814/203634.602135:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4586][err] [0814/203634.603740:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4586][err] [0814/203634.604137:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4586][err] [0814/203634.604213:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4586: Permission denied (1100) + - [pid=4586] + - [pid=4586] + - [pid=4586] + - [pid=4586] exception while trying to kill process: Error: kill EPERM + - [pid=4586] + - [pid=4586] starting temporary directories cleanup + - [pid=4586] finished temporary directories cleanup + - [pid=4586] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-26838-nd-edit-api-path-is-ignored/error-context.md b/test-results/frontend-tests-e2e-gallery-26838-nd-edit-api-path-is-ignored/error-context.md new file mode 100644 index 0000000..d9ce9de --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-26838-nd-edit-api-path-is-ignored/error-context.md @@ -0,0 +1,40 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> lightbox use all reuses parameters and edit api path is ignored +- Location: frontend/tests/e2e/gallery.spec.ts:326:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-ElObfZ --remote-debugging-pipe --no-startup-window + pid=4573 +[pid=4573][err] [0814/203632.501914:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4573][err] [0814/203632.503631:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4573][err] [0814/203632.504022:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4573][err] [0814/203632.504122:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4573: Permission denied (1100) +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-ElObfZ --remote-debugging-pipe --no-startup-window + - pid=4573 + - [pid=4573][err] [0814/203632.501914:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4573][err] [0814/203632.503631:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4573][err] [0814/203632.504022:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4573][err] [0814/203632.504122:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4573: Permission denied (1100) + - [pid=4573] + - [pid=4573] + - [pid=4573] + - [pid=4573] exception while trying to kill process: Error: kill EPERM + - [pid=4573] + - [pid=4573] starting temporary directories cleanup + - [pid=4573] finished temporary directories cleanup + - [pid=4573] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-2ea80--without-per-image-requests/error-context.md b/test-results/frontend-tests-e2e-gallery-2ea80--without-per-image-requests/error-context.md new file mode 100644 index 0000000..421692a --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-2ea80--without-per-image-requests/error-context.md @@ -0,0 +1,41 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> gallery thumbnail refresh batches all queued statuses without per-image requests +- Location: frontend/tests/e2e/gallery.spec.ts:588:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-G4lJpe --remote-debugging-pipe --no-startup-window + pid=4592 +[pid=4592][err] [0814/203635.625901:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4592][err] [0814/203635.627615:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4592][err] [0814/203635.628018:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4592][err] [0814/203635.628129:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4592: Permission denied (1100) +[pid=4592] +[pid=4592] starting temporary directories cleanup +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-G4lJpe --remote-debugging-pipe --no-startup-window + - pid=4592 + - [pid=4592][err] [0814/203635.625901:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4592][err] [0814/203635.627615:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4592][err] [0814/203635.628018:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4592][err] [0814/203635.628129:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4592: Permission denied (1100) + - [pid=4592] + - [pid=4592] starting temporary directories cleanup + - [pid=4592] + - [pid=4592] + - [pid=4592] + - [pid=4592] finished temporary directories cleanup + - [pid=4592] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-449d2-hile-waiting-for-job-events/error-context.md b/test-results/frontend-tests-e2e-gallery-449d2-hile-waiting-for-job-events/error-context.md new file mode 100644 index 0000000..4f7b484 --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-449d2-hile-waiting-for-job-events/error-context.md @@ -0,0 +1,41 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> NodeImage batch upload shows queued progress while waiting for job events +- Location: frontend/tests/e2e/gallery.spec.ts:113:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-zXtzQ3 --remote-debugging-pipe --no-startup-window + pid=4554 +[pid=4554][err] [0814/203629.322567:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4554][err] [0814/203629.324318:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4554][err] [0814/203629.324723:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4554][err] [0814/203629.324866:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4554: Permission denied (1100) +[pid=4554] +[pid=4554] starting temporary directories cleanup +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-zXtzQ3 --remote-debugging-pipe --no-startup-window + - pid=4554 + - [pid=4554][err] [0814/203629.322567:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4554][err] [0814/203629.324318:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4554][err] [0814/203629.324723:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4554][err] [0814/203629.324866:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4554: Permission denied (1100) + - [pid=4554] + - [pid=4554] starting temporary directories cleanup + - [pid=4554] + - [pid=4554] + - [pid=4554] + - [pid=4554] finished temporary directories cleanup + - [pid=4554] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-4bb1a-ing-filtering-and-selection/error-context.md b/test-results/frontend-tests-e2e-gallery-4bb1a-ing-filtering-and-selection/error-context.md new file mode 100644 index 0000000..c374d8c --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-4bb1a-ing-filtering-and-selection/error-context.md @@ -0,0 +1,40 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> gallery handles 500 mocked images with lightweight cursor paging, filtering, and selection +- Location: frontend/tests/e2e/gallery.spec.ts:436:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-Tr4L4z --remote-debugging-pipe --no-startup-window + pid=4582 +[pid=4582][err] [0814/203633.898698:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4582][err] [0814/203633.900253:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4582][err] [0814/203633.900728:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4582][err] [0814/203633.900828:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4582: Permission denied (1100) +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-Tr4L4z --remote-debugging-pipe --no-startup-window + - pid=4582 + - [pid=4582][err] [0814/203633.898698:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4582][err] [0814/203633.900253:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4582][err] [0814/203633.900728:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4582][err] [0814/203633.900828:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4582: Permission denied (1100) + - [pid=4582] + - [pid=4582] + - [pid=4582] + - [pid=4582] exception while trying to kill process: Error: kill EPERM + - [pid=4582] + - [pid=4582] starting temporary directories cleanup + - [pid=4582] finished temporary directories cleanup + - [pid=4582] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-508ab--integration-is-unavailable/error-context.md b/test-results/frontend-tests-e2e-gallery-508ab--integration-is-unavailable/error-context.md new file mode 100644 index 0000000..2a257d2 --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-508ab--integration-is-unavailable/error-context.md @@ -0,0 +1,40 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> gallery hides NodeImage upload actions when the integration is unavailable +- Location: frontend/tests/e2e/gallery.spec.ts:236:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-KHBH88 --remote-debugging-pipe --no-startup-window + pid=4564 +[pid=4564][err] [0814/203631.087446:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4564][err] [0814/203631.089068:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4564][err] [0814/203631.089470:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4564][err] [0814/203631.089527:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4564: Permission denied (1100) +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-KHBH88 --remote-debugging-pipe --no-startup-window + - pid=4564 + - [pid=4564][err] [0814/203631.087446:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4564][err] [0814/203631.089068:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4564][err] [0814/203631.089470:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4564][err] [0814/203631.089527:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4564: Permission denied (1100) + - [pid=4564] + - [pid=4564] + - [pid=4564] + - [pid=4564] exception while trying to kill process: Error: kill EPERM + - [pid=4564] + - [pid=4564] starting temporary directories cleanup + - [pid=4564] finished temporary directories cleanup + - [pid=4564] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-53611-ges-with-2000-mocked-images/error-context.md b/test-results/frontend-tests-e2e-gallery-53611-ges-with-2000-mocked-images/error-context.md new file mode 100644 index 0000000..f4f7086 --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-53611-ges-with-2000-mocked-images/error-context.md @@ -0,0 +1,41 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> lightbox navigates across pages with 2000 mocked images +- Location: frontend/tests/e2e/gallery.spec.ts:629:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-3f6Yo6 --remote-debugging-pipe --no-startup-window + pid=4596 +[pid=4596][err] [0814/203636.311708:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4596][err] [0814/203636.313354:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4596][err] [0814/203636.313810:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4596][err] [0814/203636.313906:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4596: Permission denied (1100) +[pid=4596] +[pid=4596] starting temporary directories cleanup +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-3f6Yo6 --remote-debugging-pipe --no-startup-window + - pid=4596 + - [pid=4596][err] [0814/203636.311708:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4596][err] [0814/203636.313354:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4596][err] [0814/203636.313810:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4596][err] [0814/203636.313906:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4596: Permission denied (1100) + - [pid=4596] + - [pid=4596] starting temporary directories cleanup + - [pid=4596] + - [pid=4596] + - [pid=4596] + - [pid=4596] finished temporary directories cleanup + - [pid=4596] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-70ec9-e-prefetched-lightbox-pages/error-context.md b/test-results/frontend-tests-e2e-gallery-70ec9-e-prefetched-lightbox-pages/error-context.md new file mode 100644 index 0000000..9953d98 --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-70ec9-e-prefetched-lightbox-pages/error-context.md @@ -0,0 +1,41 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> gallery mutations invalidate prefetched lightbox pages +- Location: frontend/tests/e2e/gallery.spec.ts:654:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-WOfmhD --remote-debugging-pipe --no-startup-window + pid=4598 +[pid=4598][err] [0814/203636.649881:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4598][err] [0814/203636.651548:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4598][err] [0814/203636.651965:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4598][err] [0814/203636.652037:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4598: Permission denied (1100) +[pid=4598] +[pid=4598] starting temporary directories cleanup +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-WOfmhD --remote-debugging-pipe --no-startup-window + - pid=4598 + - [pid=4598][err] [0814/203636.649881:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4598][err] [0814/203636.651548:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4598][err] [0814/203636.651965:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4598][err] [0814/203636.652037:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4598: Permission denied (1100) + - [pid=4598] + - [pid=4598] starting temporary directories cleanup + - [pid=4598] + - [pid=4598] + - [pid=4598] + - [pid=4598] finished temporary directories cleanup + - [pid=4598] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-77db7--by-a-stale-gallery-refresh/error-context.md b/test-results/frontend-tests-e2e-gallery-77db7--by-a-stale-gallery-refresh/error-context.md new file mode 100644 index 0000000..85086c2 --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-77db7--by-a-stale-gallery-refresh/error-context.md @@ -0,0 +1,41 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> single image delete is not revived by a stale gallery refresh +- Location: frontend/tests/e2e/gallery.spec.ts:724:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-yXAQRl --remote-debugging-pipe --no-startup-window + pid=4602 +[pid=4602][err] [0814/203637.327649:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4602][err] [0814/203637.329305:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4602][err] [0814/203637.329746:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4602][err] [0814/203637.329861:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4602: Permission denied (1100) +[pid=4602] +[pid=4602] starting temporary directories cleanup +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-yXAQRl --remote-debugging-pipe --no-startup-window + - pid=4602 + - [pid=4602][err] [0814/203637.327649:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4602][err] [0814/203637.329305:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4602][err] [0814/203637.329746:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4602][err] [0814/203637.329861:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4602: Permission denied (1100) + - [pid=4602] + - [pid=4602] starting temporary directories cleanup + - [pid=4602] + - [pid=4602] + - [pid=4602] + - [pid=4602] finished temporary directories cleanup + - [pid=4602] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-7cffa-the-requested-page-on-Enter/error-context.md b/test-results/frontend-tests-e2e-gallery-7cffa-the-requested-page-on-Enter/error-context.md new file mode 100644 index 0000000..fc3f067 --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-7cffa-the-requested-page-on-Enter/error-context.md @@ -0,0 +1,41 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> gallery page input jumps to the requested page on Enter +- Location: frontend/tests/e2e/gallery.spec.ts:415:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-uRPAj3 --remote-debugging-pipe --no-startup-window + pid=4580 +[pid=4580][err] [0814/203633.547256:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4580][err] [0814/203633.548720:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4580][err] [0814/203633.549101:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4580][err] [0814/203633.549202:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4580: Permission denied (1100) +[pid=4580] +[pid=4580] starting temporary directories cleanup +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-uRPAj3 --remote-debugging-pipe --no-startup-window + - pid=4580 + - [pid=4580][err] [0814/203633.547256:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4580][err] [0814/203633.548720:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4580][err] [0814/203633.549101:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4580][err] [0814/203633.549202:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4580: Permission denied (1100) + - [pid=4580] + - [pid=4580] starting temporary directories cleanup + - [pid=4580] + - [pid=4580] + - [pid=4580] + - [pid=4580] finished temporary directories cleanup + - [pid=4580] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-97e10-esults-from-the-job-payload/error-context.md b/test-results/frontend-tests-e2e-gallery-97e10-esults-from-the-job-payload/error-context.md new file mode 100644 index 0000000..a5ad88e --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-97e10-esults-from-the-job-payload/error-context.md @@ -0,0 +1,40 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> NodeImage batch upload displays partial failure results from the job payload +- Location: frontend/tests/e2e/gallery.spec.ts:138:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-kaRHJb --remote-debugging-pipe --no-startup-window + pid=4556 +[pid=4556][err] [0814/203629.672435:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4556][err] [0814/203629.674099:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4556][err] [0814/203629.674506:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4556][err] [0814/203629.674567:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4556: Permission denied (1100) +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-kaRHJb --remote-debugging-pipe --no-startup-window + - pid=4556 + - [pid=4556][err] [0814/203629.672435:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4556][err] [0814/203629.674099:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4556][err] [0814/203629.674506:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4556][err] [0814/203629.674567:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4556: Permission denied (1100) + - [pid=4556] + - [pid=4556] + - [pid=4556] + - [pid=4556] exception while trying to kill process: Error: kill EPERM + - [pid=4556] + - [pid=4556] starting temporary directories cleanup + - [pid=4556] finished temporary directories cleanup + - [pid=4556] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-9d6be-es-for-cross-page-selection/error-context.md b/test-results/frontend-tests-e2e-gallery-9d6be-es-for-cross-page-selection/error-context.md new file mode 100644 index 0000000..08357f5 --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-9d6be-es-for-cross-page-selection/error-context.md @@ -0,0 +1,40 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> NodeImage batch results use response filenames for cross-page selection +- Location: frontend/tests/e2e/gallery.spec.ts:186:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-e1Tl6n --remote-debugging-pipe --no-startup-window + pid=4560 +[pid=4560][err] [0814/203630.383955:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4560][err] [0814/203630.385473:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4560][err] [0814/203630.385958:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4560][err] [0814/203630.385982:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4560: Permission denied (1100) +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-e1Tl6n --remote-debugging-pipe --no-startup-window + - pid=4560 + - [pid=4560][err] [0814/203630.383955:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4560][err] [0814/203630.385473:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4560][err] [0814/203630.385958:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4560][err] [0814/203630.385982:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4560: Permission denied (1100) + - [pid=4560] + - [pid=4560] + - [pid=4560] + - [pid=4560] exception while trying to kill process: Error: kill EPERM + - [pid=4560] + - [pid=4560] starting temporary directories cleanup + - [pid=4560] finished temporary directories cleanup + - [pid=4560] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-9eb62-age-and-copies-result-links/error-context.md b/test-results/frontend-tests-e2e-gallery-9eb62-age-and-copies-result-links/error-context.md new file mode 100644 index 0000000..8b4aed9 --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-9eb62-age-and-copies-result-links/error-context.md @@ -0,0 +1,40 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> gallery uploads single and selected images to NodeImage and copies result links +- Location: frontend/tests/e2e/gallery.spec.ts:68:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-9QYgPI --remote-debugging-pipe --no-startup-window + pid=4552 +[pid=4552][err] [0814/203628.967048:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4552][err] [0814/203628.968795:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4552][err] [0814/203628.969153:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4552][err] [0814/203628.969241:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4552: Permission denied (1100) +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-9QYgPI --remote-debugging-pipe --no-startup-window + - pid=4552 + - [pid=4552][err] [0814/203628.967048:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4552][err] [0814/203628.968795:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4552][err] [0814/203628.969153:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4552][err] [0814/203628.969241:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4552: Permission denied (1100) + - [pid=4552] + - [pid=4552] + - [pid=4552] + - [pid=4552] exception while trying to kill process: Error: kill EPERM + - [pid=4552] + - [pid=4552] starting temporary directories cleanup + - [pid=4552] finished temporary directories cleanup + - [pid=4552] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-cd603--flows-work-with-mocked-API/error-context.md b/test-results/frontend-tests-e2e-gallery-cd603--flows-work-with-mocked-API/error-context.md new file mode 100644 index 0000000..b0f8e30 --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-cd603--flows-work-with-mocked-API/error-context.md @@ -0,0 +1,40 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> generation, gallery edit source, batch favorite, and lightbox flows work with mocked API +- Location: frontend/tests/e2e/gallery.spec.ts:21:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-fylJPg --remote-debugging-pipe --no-startup-window + pid=4550 +[pid=4550][err] [0814/203628.612174:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4550][err] [0814/203628.614456:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4550][err] [0814/203628.614934:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4550][err] [0814/203628.615537:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4550: Permission denied (1100) +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-fylJPg --remote-debugging-pipe --no-startup-window + - pid=4550 + - [pid=4550][err] [0814/203628.612174:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4550][err] [0814/203628.614456:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4550][err] [0814/203628.614934:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4550][err] [0814/203628.615537:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4550: Permission denied (1100) + - [pid=4550] + - [pid=4550] + - [pid=4550] + - [pid=4550] exception while trying to kill process: Error: kill EPERM + - [pid=4550] + - [pid=4550] starting temporary directories cleanup + - [pid=4550] finished temporary directories cleanup + - [pid=4550] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-dded5-ightbox-and-job-history-tab/error-context.md b/test-results/frontend-tests-e2e-gallery-dded5-ightbox-and-job-history-tab/error-context.md new file mode 100644 index 0000000..94f9fa8 --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-dded5-ightbox-and-job-history-tab/error-context.md @@ -0,0 +1,41 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> gallery url state restores durable filters, lightbox, and job history tab +- Location: frontend/tests/e2e/gallery.spec.ts:384:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-W00zlv --remote-debugging-pipe --no-startup-window + pid=4578 +[pid=4578][err] [0814/203633.197954:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4578][err] [0814/203633.199551:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4578][err] [0814/203633.199935:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4578][err] [0814/203633.200031:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4578: Permission denied (1100) +[pid=4578] +[pid=4578] starting temporary directories cleanup +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-W00zlv --remote-debugging-pipe --no-startup-window + - pid=4578 + - [pid=4578][err] [0814/203633.197954:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4578][err] [0814/203633.199551:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4578][err] [0814/203633.199935:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4578][err] [0814/203633.200031:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4578: Permission denied (1100) + - [pid=4578] + - [pid=4578] starting temporary directories cleanup + - [pid=4578] + - [pid=4578] + - [pid=4578] + - [pid=4578] finished temporary directories cleanup + - [pid=4578] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-def6f-firmation-before-submitting/error-context.md b/test-results/frontend-tests-e2e-gallery-def6f-firmation-before-submitting/error-context.md new file mode 100644 index 0000000..d279f17 --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-def6f-firmation-before-submitting/error-context.md @@ -0,0 +1,41 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> delete all requires typed confirmation before submitting +- Location: frontend/tests/e2e/gallery.spec.ts:786:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-ntAHUA --remote-debugging-pipe --no-startup-window + pid=4604 +[pid=4604][err] [0814/203637.665730:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4604][err] [0814/203637.667253:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4604][err] [0814/203637.667637:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4604][err] [0814/203637.667736:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4604: Permission denied (1100) +[pid=4604] +[pid=4604] starting temporary directories cleanup +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-ntAHUA --remote-debugging-pipe --no-startup-window + - pid=4604 + - [pid=4604][err] [0814/203637.665730:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4604][err] [0814/203637.667253:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4604][err] [0814/203637.667637:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4604][err] [0814/203637.667736:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4604: Permission denied (1100) + - [pid=4604] + - [pid=4604] starting temporary directories cleanup + - [pid=4604] + - [pid=4604] + - [pid=4604] + - [pid=4604] finished temporary directories cleanup + - [pid=4604] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-e3089--SSE-without-status-polling/error-context.md b/test-results/frontend-tests-e2e-gallery-e3089--SSE-without-status-polling/error-context.md new file mode 100644 index 0000000..6e313c5 --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-e3089--SSE-without-status-polling/error-context.md @@ -0,0 +1,41 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> gallery AI batch analysis uses SSE without status polling +- Location: frontend/tests/e2e/gallery.spec.ts:611:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-b34EQk --remote-debugging-pipe --no-startup-window + pid=4594 +[pid=4594][err] [0814/203635.972390:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4594][err] [0814/203635.973774:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4594][err] [0814/203635.974169:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4594][err] [0814/203635.974275:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4594: Permission denied (1100) +[pid=4594] +[pid=4594] starting temporary directories cleanup +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-b34EQk --remote-debugging-pipe --no-startup-window + - pid=4594 + - [pid=4594][err] [0814/203635.972390:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4594][err] [0814/203635.973774:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4594][err] [0814/203635.974169:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4594][err] [0814/203635.974275:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4594: Permission denied (1100) + - [pid=4594] + - [pid=4594] starting temporary directories cleanup + - [pid=4594] + - [pid=4594] + - [pid=4594] + - [pid=4594] finished temporary directories cleanup + - [pid=4594] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-e357a--until-thumbnails-are-ready/error-context.md b/test-results/frontend-tests-e2e-gallery-e357a--until-thumbnails-are-ready/error-context.md new file mode 100644 index 0000000..c733c5a --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-e357a--until-thumbnails-are-ready/error-context.md @@ -0,0 +1,41 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> gallery queued thumbnails use lightweight placeholders until thumbnails are ready +- Location: frontend/tests/e2e/gallery.spec.ts:561:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-iyQ77L --remote-debugging-pipe --no-startup-window + pid=4590 +[pid=4590][err] [0814/203635.282776:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4590][err] [0814/203635.284294:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4590][err] [0814/203635.284658:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4590][err] [0814/203635.284738:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4590: Permission denied (1100) +[pid=4590] +[pid=4590] starting temporary directories cleanup +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-iyQ77L --remote-debugging-pipe --no-startup-window + - pid=4590 + - [pid=4590][err] [0814/203635.282776:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4590][err] [0814/203635.284294:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4590][err] [0814/203635.284658:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4590][err] [0814/203635.284738:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4590: Permission denied (1100) + - [pid=4590] + - [pid=4590] starting temporary directories cleanup + - [pid=4590] + - [pid=4590] + - [pid=4590] + - [pid=4590] finished temporary directories cleanup + - [pid=4590] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-e7472-and-keeps-completed-results/error-context.md b/test-results/frontend-tests-e2e-gallery-e7472-and-keeps-completed-results/error-context.md new file mode 100644 index 0000000..d039c78 --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-e7472-and-keeps-completed-results/error-context.md @@ -0,0 +1,40 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> NodeImage batch upload cancellation calls the job cancel endpoint and keeps completed results +- Location: frontend/tests/e2e/gallery.spec.ts:153:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-fz4ZVs --remote-debugging-pipe --no-startup-window + pid=4558 +[pid=4558][err] [0814/203630.027386:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4558][err] [0814/203630.028860:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4558][err] [0814/203630.029250:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4558][err] [0814/203630.029362:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4558: Permission denied (1100) +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-fz4ZVs --remote-debugging-pipe --no-startup-window + - pid=4558 + - [pid=4558][err] [0814/203630.027386:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4558][err] [0814/203630.028860:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4558][err] [0814/203630.029250:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4558][err] [0814/203630.029362:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4558: Permission denied (1100) + - [pid=4558] + - [pid=4558] + - [pid=4558] + - [pid=4558] exception while trying to kill process: Error: kill EPERM + - [pid=4558] + - [pid=4558] starting temporary directories cleanup + - [pid=4558] finished temporary directories cleanup + - [pid=4558] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-f4b93-sults-through-a-batch-token/error-context.md b/test-results/frontend-tests-e2e-gallery-f4b93-sults-through-a-batch-token/error-context.md new file mode 100644 index 0000000..ede73e0 --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-f4b93-sults-through-a-batch-token/error-context.md @@ -0,0 +1,41 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> gallery selects current filtered results through a batch token +- Location: frontend/tests/e2e/gallery.spec.ts:472:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-l8PzKH --remote-debugging-pipe --no-startup-window + pid=4584 +[pid=4584][err] [0814/203634.249349:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4584][err] [0814/203634.250897:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4584][err] [0814/203634.251307:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4584][err] [0814/203634.251373:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4584: Permission denied (1100) +[pid=4584] +[pid=4584] starting temporary directories cleanup +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-l8PzKH --remote-debugging-pipe --no-startup-window + - pid=4584 + - [pid=4584][err] [0814/203634.249349:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4584][err] [0814/203634.250897:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4584][err] [0814/203634.251307:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4584][err] [0814/203634.251373:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4584: Permission denied (1100) + - [pid=4584] + - [pid=4584] starting temporary directories cleanup + - [pid=4584] + - [pid=4584] + - [pid=4584] + - [pid=4584] finished temporary directories cleanup + - [pid=4584] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-f4ba4-responses-without-filenames/error-context.md b/test-results/frontend-tests-e2e-gallery-f4ba4-responses-without-filenames/error-context.md new file mode 100644 index 0000000..3ad7057 --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-f4ba4-responses-without-filenames/error-context.md @@ -0,0 +1,40 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> NodeImage batch results fall back for legacy responses without filenames +- Location: frontend/tests/e2e/gallery.spec.ts:201:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-kYFRKT --remote-debugging-pipe --no-startup-window + pid=4562 +[pid=4562][err] [0814/203630.738274:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4562][err] [0814/203630.740049:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4562][err] [0814/203630.740444:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. +[pid=4562][err] [0814/203630.740519:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4562: Permission denied (1100) +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-kYFRKT --remote-debugging-pipe --no-startup-window + - pid=4562 + - [pid=4562][err] [0814/203630.738274:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4562][err] [0814/203630.740049:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4562][err] [0814/203630.740444:WARNING:net/dns/dns_config_service_posix.cc:197] Failed to read DnsConfig. + - [pid=4562][err] [0814/203630.740519:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4562: Permission denied (1100) + - [pid=4562] + - [pid=4562] + - [pid=4562] + - [pid=4562] exception while trying to kill process: Error: kill EPERM + - [pid=4562] + - [pid=4562] starting temporary directories cleanup + - [pid=4562] finished temporary directories cleanup + - [pid=4562] + +``` \ No newline at end of file diff --git a/test-results/frontend-tests-e2e-gallery-fc2b3-thout-reverse-prompt-action/error-context.md b/test-results/frontend-tests-e2e-gallery-fc2b3-thout-reverse-prompt-action/error-context.md new file mode 100644 index 0000000..d4bbe44 --- /dev/null +++ b/test-results/frontend-tests-e2e-gallery-fc2b3-thout-reverse-prompt-action/error-context.md @@ -0,0 +1,38 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: frontend/tests/e2e/gallery.spec.ts >> lightbox keeps describe, analyze, and stored AI metadata without reverse prompt action +- Location: frontend/tests/e2e/gallery.spec.ts:290:1 + +# Error details + +``` +Error: browserType.launch: Target page, context or browser has been closed +Browser logs: + + /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-825K9l --remote-debugging-pipe --no-startup-window + pid=4568 +[pid=4568][err] [0814/203631.793739:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 +[pid=4568][err] [0814/203631.795274:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. +[pid=4568][err] [0814/203631.795799:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4568: Permission denied (1100) +Call log: + - /Users/z1rconium/Library/Caches/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/1s/0_8nvzws13x62qvfpn2n25yh0000gn/T/playwright_chromiumdev_profile-825K9l --remote-debugging-pipe --no-startup-window + - pid=4568 + - [pid=4568][err] [0814/203631.793739:ERROR:base/power_monitor/thermal_state_observer_mac.mm:140] ThermalStateObserverMac unable to register to power notifications. Result: 9 + - [pid=4568][err] [0814/203631.795274:ERROR:net/dns/dns_config_service_posix.cc:138] DNS config watch failed to start. + - [pid=4568][err] [0814/203631.795799:FATAL:base/apple/mach_port_rendezvous_mac.cc:159] Check failed: kr == KERN_SUCCESS. bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer.4568: Permission denied (1100) + - [pid=4568] + - [pid=4568] + - [pid=4568] + - [pid=4568] exception while trying to kill process: Error: kill EPERM + - [pid=4568] + - [pid=4568] starting temporary directories cleanup + - [pid=4568] finished temporary directories cleanup + - [pid=4568] + +``` \ No newline at end of file