fix(ai-outfit): per-resolution timeout + _to_int default fallback

- §8 dynamic timeout: add resolution_timeout() (512/1K/2K/4K ->
  180/240/360/600s); generate() uses it for the POST and image download.
  timeout_seconds now defaults to 0 = auto-by-resolution; an explicit
  positive value overrides. Validation allows 0, rejects negatives.
- _to_int now returns the supplied default on parse failure (was 0), so a
  bad connect_timeout_seconds falls back to 30 instead of failing as 0.
- Tests: +5 (resolution map, auto vs override timeout, connect fallback).
- tasks.md §19.1/§19.2: tick unit-test item, note runner is pure-logic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 18:03:52 +08:00
co-authored by Claude Opus 4.8
parent 5b57a4d39a
commit 5ca615f7ae
3 changed files with 93 additions and 9 deletions
+55
View File
@@ -215,6 +215,59 @@ class TestImageApiClient(unittest.TestCase):
self.assertEqual(session.last_headers["Authorization"], "Bearer secret")
self.assertEqual(session.last_json["model"], "model-x")
def test_generate_uses_resolution_timeout_when_unset(self):
from services.ai_image_service import ImageApiClient
session = _FakeSession()
config = {"url": "https://api.example.test", "model": "m", "api_key": "k", "api_type": "chat"}
ImageApiClient(config, session=session).generate("p", self.image_path, resolution="4K")
# timeout_seconds absent -> (connect 30, read 600 for 4K)
self.assertEqual(session.last_timeout, (30, 600))
def test_generate_explicit_timeout_overrides_resolution(self):
from services.ai_image_service import ImageApiClient
session = _FakeSession()
config = {
"url": "https://api.example.test",
"model": "m",
"api_key": "k",
"api_type": "chat",
"timeout_seconds": 120,
}
ImageApiClient(config, session=session).generate("p", self.image_path, resolution="4K")
self.assertEqual(session.last_timeout, (30, 120))
class TestTimeoutHelpers(unittest.TestCase):
def test_resolution_timeout_mapping(self):
from services.ai_image_service import resolution_timeout
self.assertEqual(resolution_timeout("512"), 180)
self.assertEqual(resolution_timeout("1K"), 240)
self.assertEqual(resolution_timeout("2k"), 360) # case-insensitive
self.assertEqual(resolution_timeout("4K"), 600)
self.assertEqual(resolution_timeout("weird"), 240) # unknown -> default
def test_blank_timeout_is_auto_and_valid(self):
from services.ai_image_service import AiModelConfig, api_config_errors
cfg = AiModelConfig.from_dict({"url": "https://x", "model": "m", "api_key": "k"})
self.assertEqual(cfg.timeout_seconds, 0) # 0 = auto by resolution
self.assertEqual(api_config_errors(cfg), [])
def test_invalid_connect_timeout_falls_back_to_default(self):
from services.ai_image_service import AiModelConfig
cfg = AiModelConfig.from_dict(
{"url": "https://x", "model": "m", "api_key": "k", "connect_timeout_seconds": "bad"}
)
self.assertEqual(cfg.connect_timeout_seconds, 30)
class _FakeResponse:
def __init__(self, payload=None, content=b"downloaded"):
@@ -239,6 +292,7 @@ class _FakeSession:
self.last_post_url = None
self.last_headers = None
self.last_json = None
self.last_timeout = None
def get(self, url, timeout=60):
self.last_url = url
@@ -248,6 +302,7 @@ class _FakeSession:
self.last_post_url = url
self.last_headers = headers
self.last_json = json
self.last_timeout = timeout
return _FakeResponse()