Repository navigation
fix: AI node skill invocation error - #7849
Conversation
| file_bytes = file.get_bytes() | ||
|
|
||
| with zipfile.ZipFile(io.BytesIO(file_bytes), "r") as zip_ref: | ||
| members = [m for m in zip_ref.namelist() if not m.startswith("__MACOSX/") and "__MACOSX" not in m] |
There was a problem hiding this comment.
Here are the issues, potential problems and optimization suggestions for this diff:
Correctness / Potential Issues
-
Caller contract change is a breaking API change. The function was previously
async; it is nowsync. Every call site must be updated (removedawait, wrapped in an executor/threadpool, e.g.sync_to_async(init_skills)(...)orasyncio.to_thread(...)). If any caller still doesawait init_skills(...), it will break at runtime. Verify all callers. -
The docstring says "cache miss DB query" for RSA key reading, but this
init_skillscode does not read RSA keys. The comment aboutrsa_long_decrypt/ RSA cache seems to describe surrounding context rather than the shown code — it is misleading here. Alsorsa_long_decryptremains imported but appears unused after the diff (dead import). -
list(QuerySet(Tool).filter(...))— wrapping aQuerySetinstance inQuerySet(Tool)is unusual/legacy and unnecessary; useTool.objects.filter(...)(orQuerySet(model=Tool).filter(...)) instead. The same applies toFile. -
N+1 pattern kept as-is: one query per tool for
File. Since all tools are fetched up front, you can batch fetch files:codes = [t.code for t in tools] files = {f.id: f for f in File.objects.filter(id__in=codes)}
This reduces N queries to 1 and matters if
skill_tool_idsgrows. -
file.get_bytes()may do I/O or return large blobs; with the function now synchronous, this I/O runs on whatever thread calls it. Ensure the caller offloads it (as the docstring states) — otherwise it will block the event loop if called from async code without a thread wrapper. -
Zip handling:
zip_ref.namelist()filtering keeps the existing logic, but note thatfile.get_bytes()returns bytes that are stored fully in memory. For large archives, prefer streaming (zipfile.ZipFile(file_obj)on a file handle) if feasible. Also consider Zip Slip protection when extracting toskills_dir(reject member paths containing..or absolute paths). -
r = ...variable naming: the renamed lambda default-argument workaround was removed, which is correct — but make sure no closure-capture bug existed for other variables within the loop (e.g.paramsassigned per iteration should be fine).
Style / Minor
- Long Chinese docstring in an English-based codebase — inconsistent with the rest of the module. Translate or shorten; also the multi-line format is fine but consider keeping it concise.
- Unused imports:
rsa_long_decrypt(andsync_to_asyncis removed intentionally — good) — removersa_long_decryptif truly unused. - Type hints missing: since this function is now sync and will be called from threads, adding
def init_skills(skill_tool_ids: list[int] | None, temp_dir: str) -> None:improves clarity. - No error handling per-file: if one zip is corrupt, the whole loop aborts. Consider
try/exceptper tool with logging so one bad tool doesn't break skill initialization.
Summary
The core change (making the function synchronous) is reasonable only if every caller is updated to run it in a worker thread. Fix the dead import, clarify the docstring, switch to Tool.objects/File.objects, and batch the File lookup to avoid N+1 queries.
| patch.object(agent_module, "init_skills", new=MagicMock()), | ||
| patch.object( | ||
| agent_module, "MultiServerMCPClient", return_value=SimpleNamespace(get_tools=AsyncMock(return_value=[])) | ||
| ), |
There was a problem hiding this comment.
Looking at this diff, here are the observations:
Issue: Replacing AsyncMock with MagicMock for an async function
The diff changes init_skills from being patched with AsyncMock() to MagicMock() in two test locations. If init_skills is a coroutine/async function that gets awaited in production code, this change will cause problems.
Problems introduced by this change
-
Awaitability breaks on call. If the production code does
await init_skills(...), aMagicMock()returns a plain (non-awaitable) object when called, raising:TypeError: object MagicMock can't be used in 'await' expressionUnless every awaited return value is explicitly configured with
return_value=..., tests may pass by accident/spuriously or fail inconsistently. -
Loss of async semantics verification.
AsyncMockasserts the call was awaited properly and correctly models async behavior (e.g., it's recognized byasyncio.iscoroutinefunction). Switching toMagicMocksilently removes that safety net — a test could pass even if the code stops awaitinginit_skills. -
Inconsistency risk across test suite. Tests still patching other async functions with
AsyncMock(e.g.,get_toolsremainsAsyncMock) but this one becomesMagicMock→ inconsistent mocking model, harder to reason about. -
Silent false positives. If
init_skillsisn't actually awaited (a latent bug),MagicMockhides it;AsyncMockwould surface it via a "coroutine was never awaited" warning.
Recommendation
- If
init_skillsis async: revert toAsyncMock(). This is almost certainly what you want. - If it's sync: then
AsyncMockwas arguably wrong originally, and this change is correct — but double-check whyget_tools(clearly async) usesAsyncMockwhileinit_skillsdoesn't; verify the actual signature ofagent_module.init_skills. - Confirm you're importing both mocks (
from unittest.mock import AsyncMock, MagicMock) — the diff shows usage but not imports, so make sureMagicMockis imported in each test file to avoidNameError.
Minor / optimization notes
- Both
withblocks use nearly identical patch setups (init_skills,MultiServerMCPClient). Consider extracting a shared pytest fixture or helper context manager to reduce duplication and keep mock types consistent. - If
init_skillsreturns something consumed downstream, set an explicitreturn_value(e.g.,MagicMock(return_value=...)orAsyncMock(return_value=...)) rather than relying on auto-generated mocks.
Bottom line: This change is suspicious. Verify whether init_skills is async — if so, this is a regression and should be reverted to AsyncMock().
| patch.object(agent_module, "init_skills", new=MagicMock()), | ||
| patch.object(agent_module.CONFIG, "get", return_value="1"), | ||
| patch.object( | ||
| agent_module, |
There was a problem hiding this comment.
The change from AsyncMock to MagicMock is risky and likely incorrect unless init_skills has been explicitly refactored to be a synchronous function.
Key Issues & Suggestions:
-
Asynchronous vs. Synchronous Mocks:
- If
init_skillsis originally anasync deffunction, replacing its patch withMagicMock()will break the code if any part of the test or implementation tries toawaitthe mock result.MagicMock()does not supportawait. - Recommendation: Keep
AsyncMock()if the underlying function remains asynchronous. Only useMagicMock()if you are certaininit_skillswas changed to a regular (non-async) function in the source code.
- If
-
Best Practice for Async Patches:
- Using
new=AsyncMock()is correct for mocking async functions. - If you want to control the return value, ensure it's an awaitable if the caller awaits it. For example:
patch("module.async_func", new_callable=AsyncMock, return_value="result").
- Using
-
Optimization/Readability:
- The
withstatement grouping multiple contexts using parentheses (as seen here) is valid Python 3.9+ syntax and improves readability. No issue here.
- The
Conclusion:
Revert to AsyncMock() unless init_skills is no longer an async function. Otherwise, you risk runtime errors (TypeError: object MagicMock can’t be used in 'await' expression).
fix: AI node skill invocation error