Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion novelforge/llm/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def call_image_api(prompt: str, *, filename_prefix: str = "illustration") -> str

# Download with streaming and size limit (20 MB max)
_MAX_IMAGE_BYTES = 20 * 1024 * 1024
img_resp = requests.get(image_url, timeout=60, stream=True)
img_resp = requests.get(image_url, timeout=config.IMAGE_TIMEOUT, stream=True)
img_resp.raise_for_status()
chunks: list[bytes] = []
downloaded = 0
Expand Down
94 changes: 94 additions & 0 deletions tests/test_image_download_timeout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Tests that call_image_api uses config.IMAGE_TIMEOUT for the image download request."""

import base64
import json
Comment on lines +3 to +4
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

base64 and json are imported but not used in this test module. Removing unused imports will keep the tests clean and avoid failing future linting/static checks if introduced.

Suggested change
import base64
import json

Copilot uses AI. Check for mistakes.
from unittest.mock import MagicMock, patch


def _make_api_response(payload: dict) -> MagicMock:
"""Build a mock requests.Response for the image generation POST call."""
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = payload
resp.raise_for_status.return_value = None
return resp


def _make_download_response(content: bytes = b"fake_image_data") -> MagicMock:
"""Build a mock streaming requests.Response for the image download GET call."""
resp = MagicMock()
resp.status_code = 200
resp.raise_for_status.return_value = None
resp.iter_content.return_value = iter([content])
return resp


class TestImageDownloadUsesConfigTimeout:
"""Verify that requests.get for image URL download uses config.IMAGE_TIMEOUT."""

def test_download_uses_config_image_timeout(self, tmp_path, monkeypatch):
import novelforge.config as config
import novelforge.llm.image as image_mod

# Configure necessary settings
monkeypatch.setattr(config, "IMAGE_API_KEY", "test-key")
monkeypatch.setattr(config, "IMAGE_API_URL", "https://api.example.com/images")
monkeypatch.setattr(config, "IMAGE_MODEL", "dall-e-3")
monkeypatch.setattr(config, "IMAGE_SIZE", "1024x1024")
monkeypatch.setattr(config, "IMAGE_TIMEOUT", 120)
monkeypatch.setattr(config, "EXPORT_DIR", str(tmp_path))

api_resp = _make_api_response({
"data": [{"url": "https://cdn.example.com/image.png"}]
})
download_resp = _make_download_response(b"\x89PNG\r\n")

get_calls = []

def fake_get(url, **kwargs):
get_calls.append({"url": url, "kwargs": kwargs})
return download_resp

with patch.object(image_mod.requests, "post", return_value=api_resp), \
patch.object(image_mod.requests, "get", side_effect=fake_get):
result = image_mod.call_image_api("a brave hero", filename_prefix="test")

assert result is not None, "call_image_api should return a filename on success"
assert len(get_calls) == 1, "requests.get should be called once for the download"

actual_timeout = get_calls[0]["kwargs"].get("timeout")
assert actual_timeout == 120, (
f"Expected timeout=config.IMAGE_TIMEOUT (120), got {actual_timeout!r}"
)

def test_download_timeout_reflects_updated_config(self, tmp_path, monkeypatch):
"""Changing IMAGE_TIMEOUT should change the download timeout accordingly."""
import novelforge.config as config
import novelforge.llm.image as image_mod

monkeypatch.setattr(config, "IMAGE_API_KEY", "test-key")
monkeypatch.setattr(config, "IMAGE_API_URL", "https://api.example.com/images")
monkeypatch.setattr(config, "IMAGE_MODEL", "dall-e-3")
monkeypatch.setattr(config, "IMAGE_SIZE", "1024x1024")
monkeypatch.setattr(config, "IMAGE_TIMEOUT", 300)
monkeypatch.setattr(config, "EXPORT_DIR", str(tmp_path))

api_resp = _make_api_response({
"data": [{"url": "https://cdn.example.com/image.png"}]
})
download_resp = _make_download_response(b"\x89PNG\r\n")

get_calls = []

def fake_get(url, **kwargs):
get_calls.append({"url": url, "kwargs": kwargs})
return download_resp

with patch.object(image_mod.requests, "post", return_value=api_resp), \
patch.object(image_mod.requests, "get", side_effect=fake_get):
image_mod.call_image_api("sunset scene", filename_prefix="cover")

actual_timeout = get_calls[0]["kwargs"].get("timeout")
assert actual_timeout == 300, (
f"Expected timeout=config.IMAGE_TIMEOUT (300), got {actual_timeout!r}"
)
Comment on lines +89 to +94
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_download_timeout_reflects_updated_config accesses get_calls[0] without first asserting that requests.get was called. Adding an explicit len(get_calls) == 1 assertion (as in the previous test) will produce a clearer failure if the download path changes.

Copilot uses AI. Check for mistakes.
Loading