{"record":{"id":"a456ec9c12a1d87e","repo":"harry0703/MoneyPrinterTurbo","slug":"max-age-days-must-be-a-positive-integer-or-none","errorCode":null,"errorMessage":"max_age_days must be a positive integer or None","messagePattern":"max_age_days must be a positive integer or None","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"app/services/cache_manager.py","lineNumber":120,"sourceCode":"    entry: _VideoCacheEntry,\n    max_age_days: int | None,\n    now: float,\n) -> bool:\n    if max_age_days is None:\n        return True\n    return entry.mtime < now - max_age_days * _SECONDS_PER_DAY\n\n\ndef _validate_max_age_days(max_age_days: int | None) -> None:\n    \"\"\"即使缓存目录为空，也应稳定拒绝无效清理参数。\"\"\"\n    if max_age_days is None:\n        return\n    if (\n        isinstance(max_age_days, bool)\n        or not isinstance(max_age_days, int)\n        or max_age_days <= 0\n    ):\n        raise ValueError(\"max_age_days must be a positive integer or None\")\n\n\ndef get_video_cache_stats(max_age_days: int | None = None) -> VideoCacheStats:\n    \"\"\"\n    统计全部缓存，或预览修改时间早于指定天数的可清理缓存。\n\n    ``max_age_days=None`` 表示全部缓存。统计过程只读取目录项的大小和修改时间，\n    不读取视频内容，因此即使缓存总容量很大也不会产生与容量成比例的 I/O。\n    \"\"\"\n\n    _validate_max_age_days(max_age_days)\n    now = time.time()\n    file_count = 0\n    total_size = 0\n    oldest_mtime = None\n    newest_mtime = None\n\n    for entry in _iter_video_cache_entries():","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/harry0703/MoneyPrinterTurbo/blob/1f9f19c2021a68d04df228f33e9099a0c947f6f8/app/services/cache_manager.py#L102-L138","documentation":"ValueError from _validate_max_age_days in the cache manager: max_age_days must be None or a positive int, but the caller passed a non-int, a bool, zero, or a negative number. The explicit isinstance(max_age_days, bool) check rejects True/False even though bool subclasses int. Validation runs even when the cache directory is empty, so bad parameters fail deterministically instead of silently passing.","triggerScenarios":"Calling get_video_cache_stats(max_age_days=...), cleanup, or any API that funnels into _validate_max_age_days with 0, -1, 7.0, '7', or True. WebUI form values parsed as strings/floats are the classic source.","commonSituations":"Form/query params arriving as strings ('30') and being passed through unconverted; floats from division (days*1.0); using True as a truthy 'purge everything' flag; 0 intended to mean 'all' instead of None.","solutions":["Pass None for 'all caches' and a positive int (e.g. 30) for age-based stats/cleanup.","Coerce user input at the boundary: days = int(str(days)) and validate days > 0, or map falsy to None.","Never rely on True/False — booleans are deliberately rejected."],"exampleFix":"// before\nget_video_cache_stats(max_age_days=request.args.get('days', type=float))  # 7.0 -> ValueError\n\n// after\ndays = request.args.get('days', type=int)\nget_video_cache_stats(max_age_days=days if days and days > 0 else None)","handlingStrategy":"type-guard","validationCode":"def normalize_max_age_days(value):\n    if value is None:\n        return None\n    days = int(value)\n    if isinstance(value, bool) or days <= 0:\n        raise ValueError('max_age_days must be a positive integer or None')\n    return days","typeGuard":"def is_valid_max_age_days(value) -> bool:\n    \"\"\"True only for None or positive non-bool ints.\"\"\"\n    if value is None:\n        return True\n    return not isinstance(value, bool) and isinstance(value, int) and value > 0","tryCatchPattern":"try:\n    get_video_cache_stats(max_age_days=days)\nexcept ValueError as e:\n    if 'max_age_days' in str(e):\n        days = None  # fall back to full stats\n        get_video_cache_stats(max_age_days=days)","preventionTips":["Convert form/query params with type=int at the framework boundary.","Use None (not 0) to mean 'all caches'.","Never pass booleans — they are explicitly rejected because bool subclasses int."],"tags":["cache","validation","value-error","type-coercion"],"backgroundTag":null,"analyzedSha":"1f9f19c2021a68d04df228f33e9099a0c947f6f8","analyzedAt":"2026-08-14T19:41:05.568Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}