mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-24 00:55:35 +00:00
* fix(auth): recover from unreadable sessions Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(scanner): defer unavailable scan tasks safely Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(web): prefer the SkillHub CLI install command Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(auth): decode session cookies during recovery Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(runtime): address scanner and session review findings Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(scanner): defer all server-side outages Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * docs(scanner): clarify deferred failure semantics Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * test(scanner): cover recovery boundaries Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(scanner): register startup hook on router Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * test(e2e): align install defaults and reuse auth session Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --------- Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
131 lines
4.1 KiB
Python
131 lines
4.1 KiB
Python
import asyncio
|
|
import importlib.util
|
|
import sys
|
|
import tempfile
|
|
import types
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
|
|
class _FakeRouter:
|
|
def __init__(self):
|
|
self.handlers = []
|
|
|
|
def add_event_handler(self, _event, _handler):
|
|
self.handlers.append((_event, _handler))
|
|
|
|
|
|
class _FakeApp:
|
|
def __init__(self):
|
|
self.router = _FakeRouter()
|
|
|
|
def middleware(self, _kind):
|
|
return lambda function: function
|
|
|
|
|
|
class _FakeResponse:
|
|
def __init__(self, status_code, content, headers):
|
|
self.status_code = status_code
|
|
self.content = content
|
|
self.headers = headers
|
|
|
|
|
|
class _Request:
|
|
method = "POST"
|
|
url = types.SimpleNamespace(path="/scan-upload")
|
|
|
|
|
|
def _load_module():
|
|
fastapi = types.ModuleType("fastapi")
|
|
fastapi.Request = object
|
|
responses = types.ModuleType("fastapi.responses")
|
|
responses.JSONResponse = _FakeResponse
|
|
api = types.ModuleType("skill_scanner.api.api")
|
|
api.app = _FakeApp()
|
|
stubs = {
|
|
"fastapi": fastapi,
|
|
"fastapi.responses": responses,
|
|
"skill_scanner": types.ModuleType("skill_scanner"),
|
|
"skill_scanner.api": types.ModuleType("skill_scanner.api"),
|
|
"skill_scanner.api.api": api,
|
|
}
|
|
with patch.dict(sys.modules, stubs):
|
|
module_path = Path(__file__).parents[1] / "skillhub_scanner_app.py"
|
|
spec = importlib.util.spec_from_file_location("skillhub_scanner_app_under_test", module_path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
class SkillHubScannerAppTest(unittest.IsolatedAsyncioTestCase):
|
|
async def asyncSetUp(self):
|
|
self.module = _load_module()
|
|
|
|
async def test_excess_scan_is_rejected(self):
|
|
self.module._active_scans = 1
|
|
|
|
response = await self.module.limit_concurrent_scans(_Request(), lambda _request: None)
|
|
|
|
self.assertEqual(503, response.status_code)
|
|
self.assertEqual("30", response.headers["Retry-After"])
|
|
|
|
async def test_client_disconnect_keeps_slot_until_scan_finishes(self):
|
|
release = asyncio.Event()
|
|
|
|
async def scan(_request):
|
|
await release.wait()
|
|
return "done"
|
|
|
|
request_task = asyncio.create_task(self.module.limit_concurrent_scans(_Request(), scan))
|
|
await asyncio.sleep(0)
|
|
request_task.cancel()
|
|
await asyncio.sleep(0)
|
|
|
|
self.assertEqual(1, self.module._active_scans)
|
|
response = await self.module.limit_concurrent_scans(_Request(), scan)
|
|
self.assertEqual(503, response.status_code)
|
|
|
|
release.set()
|
|
with self.assertRaises(asyncio.CancelledError):
|
|
await request_task
|
|
self.assertEqual(0, self.module._active_scans)
|
|
|
|
async def test_hard_timeout_requests_process_restart(self):
|
|
self.module._HARD_TIMEOUT_SECONDS = 0.01
|
|
|
|
async def stuck_scan(_request):
|
|
await asyncio.Event().wait()
|
|
|
|
with patch.object(
|
|
self.module,
|
|
"_restart_after_hard_timeout",
|
|
side_effect=RuntimeError("restart requested")) as restart:
|
|
with self.assertRaisesRegex(RuntimeError, "restart requested"):
|
|
await self.module.limit_concurrent_scans(_Request(), stuck_scan)
|
|
|
|
restart.assert_called_once_with("/scan-upload")
|
|
self.assertEqual(0, self.module._active_scans)
|
|
|
|
async def test_startup_cleanup_removes_only_scanner_directories(self):
|
|
with tempfile.TemporaryDirectory() as temp_root:
|
|
root = Path(temp_root)
|
|
stale = root / "skill_scanner_abcd"
|
|
unrelated = root / "skillhub-data"
|
|
stale.mkdir()
|
|
unrelated.mkdir()
|
|
|
|
self.module._cleanup_stale_scan_directories(root)
|
|
|
|
self.assertFalse(stale.exists())
|
|
self.assertTrue(unrelated.exists())
|
|
|
|
async def test_startup_cleanup_is_registered_on_the_upstream_router(self):
|
|
self.assertEqual(
|
|
[("startup", self.module._cleanup_stale_scan_directories)],
|
|
self.module.app.router.handlers,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|