Files
wehub-resource-sync 2114ccd278
CI / Lint & Test (Python 3.13) (push) Failing after 2s
CI / Lint & Test (Python 3.14) (push) Failing after 1s
CI / Lint & Test (Python 3.12) (push) Failing after 2s
CI / DCO Check (push) Has been skipped
Scorecard supply-chain security / Scorecard analysis (push) Failing after 2s
chore: import upstream snapshot with attribution
2026-07-13 12:23:39 +08:00

546 lines
22 KiB
Python

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Thematic tests: monkey-patch fragility (Reviewer Issue #2).
Proves that ``deepseek_compat()`` patches survive upstream changes by
verifying that the ``_verify_patch_targets`` guard catches broken
assumptions BEFORE any patches are applied.
Key invariants:
- Guard catches missing parameters (upstream renamed/removed)
- Guard catches keyword-only migration (positional → kwarg)
- Guard catches removed deep dependencies (Pydantic methods, Batch fields)
- Guard catches removed class attributes (response_schema)
- Guard passes cleanly against current upstream (no false positive)
- Guard runs atomically — if any check fails, no patches are applied
- Each of the 7 patches has unique, distinguishable guard coverage
See also: ``test_monkeypatch_invasiveness.py`` (thread-scoping proof).
"""
from __future__ import annotations
import asyncio
import dataclasses
import inspect
import sys
import unittest
from pathlib import Path
_project_root = Path(__file__).resolve().parents[3]
if str(_project_root) not in sys.path:
sys.path.insert(0, str(_project_root))
from skillspector.llm_analyzer_base import (
Batch,
LLMAnalyzerBase,
LLMAnalysisResult,
LLMFinding,
)
from skillspector.nodes.meta_analyzer import LLMMetaAnalyzer, MetaAnalyzerResult
from contrib.batch_scan.runner import (
_check_signature,
_original_asyncio_run,
_original_base_init,
_original_base_parse,
_original_base_build_prompt,
_original_meta_parse,
_original_meta_build_prompt,
_verify_patch_targets,
_apply_patches,
_restore_patches,
deepseek_compat,
)
# ═══════════════════════════════════════════════════════════════════════════
# Helpers
# ═══════════════════════════════════════════════════════════════════════════
def _force_restore() -> None:
"""Safety-net: restore all patches regardless of depth counter."""
import contrib.batch_scan.runner as _runner
while _runner._patches_depth > 0:
_runner._restore_patches()
class _TempAttributeOverride:
"""Context manager to temporarily replace / delete an attribute on an object.
Usage::
with _TempAttributeOverride(LLMAnalysisResult, "model_validate", None):
# model_validate is temporarily None
...
# model_validate restored
"""
def __init__(self, obj: object, attr: str, replacement=None, *, delete: bool = False):
self._obj = obj
self._attr = attr
self._replacement = replacement
self._delete = delete
self._saved = None
self._had_attr = False
def __enter__(self):
self._had_attr = hasattr(self._obj, self._attr)
if self._had_attr:
self._saved = getattr(self._obj, self._attr)
if self._delete:
if self._had_attr:
delattr(self._obj, self._attr)
else:
setattr(self._obj, self._attr, self._replacement)
return self
def __exit__(self, *args):
if self._had_attr:
setattr(self._obj, self._attr, self._saved)
elif not self._delete:
delattr(self._obj, self._attr)
# ═══════════════════════════════════════════════════════════════════════════
# Test 1: _check_signature — parameter-level guard
# ═══════════════════════════════════════════════════════════════════════════
class TestCheckSignature(unittest.TestCase):
"""``_check_signature()`` — the micro-guard behind every parameter check.
Three failure modes:
1. Missing parameter (upstream removed it)
2. KEYWORD_ONLY parameter (upstream made positional → kwarg)
3. Uninspectable function (C builtin, etc.)
"""
def test_passes_when_all_params_present(self) -> None:
def _sample(self, a, b, c):
pass
# Should not raise
_check_signature(_sample, ["self", "a", "b", "c"], "test_func", 99)
def test_raises_when_param_missing(self) -> None:
def _sample(self, a, b):
pass
with self.assertRaises(RuntimeError) as ctx:
_check_signature(_sample, ["self", "a", "b", "c"], "test_func", 99)
self.assertIn("no longer has 'c'", str(ctx.exception))
def test_raises_when_param_becomes_keyword_only(self) -> None:
def _sample(self, *, a, b, c):
pass
with self.assertRaises(RuntimeError) as ctx:
_check_signature(_sample, ["self", "a", "b", "c"], "test_func", 99)
self.assertIn("keyword-only", str(ctx.exception))
# ═══════════════════════════════════════════════════════════════════════════
# Test 2: Guard passes against current upstream (no false positive)
# ═══════════════════════════════════════════════════════════════════════════
class TestGuardPassesCurrentUpstream(unittest.TestCase):
"""``_verify_patch_targets()`` must pass cleanly against the currently
installed upstream version. Any failure here means upstream already
broke something and the guard is doing its job — but patches need
updating.
"""
@classmethod
def tearDownClass(cls) -> None:
_force_restore()
def test_verify_patch_targets_does_not_raise(self) -> None:
try:
_verify_patch_targets()
except RuntimeError as exc:
self.fail(f"_verify_patch_targets raised against current upstream: {exc}")
def test_context_manager_enter_passes_guard(self) -> None:
try:
with deepseek_compat():
pass
except RuntimeError as exc:
self.fail(f"deepseek_compat() guard failed: {exc}")
def test_guard_after_context_cycle_still_passes(self) -> None:
"""Guard should pass even after patches were applied and restored."""
with deepseek_compat():
pass
# After full apply+restore cycle, guard must still pass
try:
_verify_patch_targets()
except RuntimeError as exc:
self.fail(f"Guard failed after apply+restore cycle: {exc}")
def test_guard_after_setup_and_manual_restore_still_passes(self) -> None:
"""Guard should pass after setup_deepseek_compat() + manual restore."""
from contrib.batch_scan.runner import setup_deepseek_compat
setup_deepseek_compat()
_force_restore()
try:
_verify_patch_targets()
except RuntimeError as exc:
self.fail(f"Guard failed after setup+restore cycle: {exc}")
# ═══════════════════════════════════════════════════════════════════════════
# Test 3: Each patch guard catches its specific breakage
# ═══════════════════════════════════════════════════════════════════════════
class TestGuardPatch1Init(unittest.TestCase):
"""Guard for Patch 1: LLMAnalyzerBase.__init__(self, base_prompt, model)
AND class attribute ``response_schema`` exists."""
@classmethod
def tearDownClass(cls) -> None:
_force_restore()
def test_guard_catches_missing_base_prompt_param(self) -> None:
"""If upstream removes 'base_prompt' from __init__, guard must raise."""
original = LLMAnalyzerBase.__init__
def _broken_init(self, model):
pass
try:
LLMAnalyzerBase.__init__ = _broken_init
with self.assertRaises(RuntimeError) as ctx:
_verify_patch_targets()
self.assertIn("Patch 1", str(ctx.exception))
self.assertIn("base_prompt", str(ctx.exception))
finally:
LLMAnalyzerBase.__init__ = original
def test_guard_catches_missing_model_param(self) -> None:
"""If upstream removes 'model' from __init__, guard must raise."""
original = LLMAnalyzerBase.__init__
def _broken_init(self, base_prompt):
pass
try:
LLMAnalyzerBase.__init__ = _broken_init
with self.assertRaises(RuntimeError):
_verify_patch_targets()
finally:
LLMAnalyzerBase.__init__ = original
def test_guard_catches_missing_response_schema_attr(self) -> None:
"""If upstream removes response_schema class attr, guard must raise."""
with _TempAttributeOverride(LLMAnalyzerBase, "response_schema", delete=True):
with self.assertRaises(RuntimeError) as ctx:
_verify_patch_targets()
self.assertIn("response_schema", str(ctx.exception))
class TestGuardPatch2ParseResponse(unittest.TestCase):
"""Guard for Patch 2: LLMAnalyzerBase.parse_response + deep deps."""
@classmethod
def tearDownClass(cls) -> None:
_force_restore()
def test_guard_catches_missing_batch_param(self) -> None:
"""If parse_response no longer accepts 'batch', guard must raise."""
original = LLMAnalyzerBase.parse_response
def _broken_parse(self, response):
pass
try:
LLMAnalyzerBase.parse_response = _broken_parse
with self.assertRaises(RuntimeError) as ctx:
_verify_patch_targets()
self.assertIn("Patch 2", str(ctx.exception))
finally:
LLMAnalyzerBase.parse_response = original
def test_guard_catches_missing_model_validate(self) -> None:
"""If LLMAnalysisResult.model_validate is removed, guard must raise.
model_validate is a Pydantic metaclass-injected classmethod that
cannot be deleted via delattr. We monkey-patch builtins.hasattr
to simulate its absence.
"""
import builtins
_real_hasattr = builtins.hasattr
def _fake_hasattr(obj, name):
if obj is LLMAnalysisResult and name == "model_validate":
return False
return _real_hasattr(obj, name)
try:
builtins.hasattr = _fake_hasattr
with self.assertRaises(RuntimeError) as ctx:
_verify_patch_targets()
self.assertIn("model_validate", str(ctx.exception))
finally:
builtins.hasattr = _real_hasattr
def test_guard_catches_missing_to_finding(self) -> None:
"""If LLMFinding.to_finding is removed, guard must raise."""
with _TempAttributeOverride(LLMFinding, "to_finding", delete=True):
with self.assertRaises(RuntimeError) as ctx:
_verify_patch_targets()
self.assertIn("to_finding", str(ctx.exception))
def test_guard_catches_missing_batch_file_path_field(self) -> None:
"""If Batch.file_path field is removed, guard must raise.
Batch is a @dataclass — we test by removing the field from __dataclass_fields__.
"""
saved_fields = Batch.__dataclass_fields__.copy() # type: ignore[attr-defined]
try:
# Remove file_path from dataclass fields
Batch.__dataclass_fields__ = { # type: ignore[attr-defined]
k: v for k, v in saved_fields.items() if k != "file_path"
}
with self.assertRaises(RuntimeError) as ctx:
_verify_patch_targets()
self.assertIn("file_path", str(ctx.exception))
finally:
Batch.__dataclass_fields__ = saved_fields # type: ignore[attr-defined]
class TestGuardPatch3MetaParse(unittest.TestCase):
"""Guard for Patch 3: LLMMetaAnalyzer.parse_response + deep deps."""
@classmethod
def tearDownClass(cls) -> None:
_force_restore()
def test_guard_catches_missing_batch_param_on_meta_parse(self) -> None:
original = LLMMetaAnalyzer.parse_response
def _broken(self, response):
pass
try:
LLMMetaAnalyzer.parse_response = _broken
with self.assertRaises(RuntimeError) as ctx:
_verify_patch_targets()
self.assertIn("Patch 3", str(ctx.exception))
finally:
LLMMetaAnalyzer.parse_response = original
def test_guard_catches_missing_meta_analyzer_model_validate(self) -> None:
import builtins
_real_hasattr = builtins.hasattr
def _fake_hasattr(obj, name):
if obj is MetaAnalyzerResult and name == "model_validate":
return False
return _real_hasattr(obj, name)
try:
builtins.hasattr = _fake_hasattr
with self.assertRaises(RuntimeError) as ctx:
_verify_patch_targets()
self.assertIn("model_validate", str(ctx.exception))
finally:
builtins.hasattr = _real_hasattr
def test_guard_catches_missing_findings_field(self) -> None:
"""If MetaAnalyzerResult no longer has 'findings' field."""
saved = MetaAnalyzerResult.model_fields.copy()
try:
MetaAnalyzerResult.model_fields = {
k: v for k, v in saved.items() if k != "findings"
}
with self.assertRaises(RuntimeError) as ctx:
_verify_patch_targets()
self.assertIn("findings", str(ctx.exception))
finally:
MetaAnalyzerResult.model_fields = saved
class TestGuardPatch4BaseBuildPrompt(unittest.TestCase):
"""Guard for Patch 4: LLMAnalyzerBase.build_prompt(self, batch, **kwargs)."""
@classmethod
def tearDownClass(cls) -> None:
_force_restore()
def test_guard_catches_missing_batch_param(self) -> None:
original = LLMAnalyzerBase.build_prompt
def _broken(self):
return "prompt"
try:
LLMAnalyzerBase.build_prompt = _broken
with self.assertRaises(RuntimeError) as ctx:
_verify_patch_targets()
self.assertIn("Patch 4", str(ctx.exception))
self.assertIn("batch", str(ctx.exception))
finally:
LLMAnalyzerBase.build_prompt = original
def test_guard_catches_missing_kwargs(self) -> None:
"""If build_prompt no longer accepts **kwargs."""
original = LLMAnalyzerBase.build_prompt
def _broken(self, batch):
return "prompt"
try:
LLMAnalyzerBase.build_prompt = _broken
with self.assertRaises(RuntimeError) as ctx:
_verify_patch_targets()
self.assertIn("**kwargs", str(ctx.exception))
finally:
LLMAnalyzerBase.build_prompt = original
class TestGuardPatch5MetaBuildPrompt(unittest.TestCase):
"""Guard for Patch 5: LLMMetaAnalyzer.build_prompt(self, batch, **kwargs)."""
@classmethod
def tearDownClass(cls) -> None:
_force_restore()
def test_guard_catches_missing_batch_param(self) -> None:
original = LLMMetaAnalyzer.build_prompt
def _broken(self):
return "prompt"
try:
LLMMetaAnalyzer.build_prompt = _broken
with self.assertRaises(RuntimeError) as ctx:
_verify_patch_targets()
self.assertIn("Patch 5", str(ctx.exception))
finally:
LLMMetaAnalyzer.build_prompt = original
class TestGuardPatch7Asyncio(unittest.TestCase):
"""Guard for Patch 7: asyncio.run(main, *, debug=None, loop_factory=None)
AND deep dep: asyncio.new_event_loop is callable."""
@classmethod
def tearDownClass(cls) -> None:
_force_restore()
def test_guard_catches_missing_main_param(self) -> None:
"""If asyncio.run signature changes, guard uses saved _original_asyncio_run."""
# _verify_patch_targets inspects _original_asyncio_run (module-load snapshot),
# not asyncio.run (which may already be patched). The original always has
# 'main' — this is a structural test confirming the guard covers Patch 7.
self.assertTrue(callable(_original_asyncio_run))
# Verify the guard checks 'main' parameter on the original
sig = inspect.signature(_original_asyncio_run)
self.assertIn("main", sig.parameters,
"asyncio.run should have 'main' parameter")
def test_guard_catches_missing_new_event_loop(self) -> None:
"""If asyncio.new_event_loop is removed, guard must raise."""
with _TempAttributeOverride(asyncio, "new_event_loop", None):
with self.assertRaises(RuntimeError) as ctx:
_verify_patch_targets()
self.assertIn("new_event_loop", str(ctx.exception))
# ═══════════════════════════════════════════════════════════════════════════
# Test 4: Atomicity — guard fails → no patches applied
# ═══════════════════════════════════════════════════════════════════════════
class TestGuardAtomicity(unittest.TestCase):
"""If _verify_patch_targets raises, NO patches should be applied.
This is the "fail-closed" property: a broken upstream should result in
a loud error, not silently-malfunctioning patches.
"""
@classmethod
def tearDownClass(cls) -> None:
_force_restore()
# Ensure response_schema is restored
if hasattr(LLMAnalyzerBase, "_response_schema_original"):
LLMAnalyzerBase.response_schema = LLMAnalyzerBase._response_schema_original
def test_failed_guard_leaves_no_patches_applied(self) -> None:
"""Break response_schema, call _apply_patches, verify it raises and
no methods are patched."""
# Force-clean state
_force_restore()
with _TempAttributeOverride(LLMAnalyzerBase, "response_schema", delete=True):
# Guard should raise → _apply_patches should propagate
with self.assertRaises(RuntimeError):
_apply_patches()
# After the failed attempt, NO methods should be patched
_assert_all_restored(self)
# ═══════════════════════════════════════════════════════════════════════════
# Test 5: Original references captured at module load, not at apply-time
# ═══════════════════════════════════════════════════════════════════════════
class TestOriginalCapturedAtImportTime(unittest.TestCase):
"""Module-level original references are snapshotted when runner.py is
first imported, not when _apply_patches() runs. This ensures they are
always the true upstream originals, never a previously-patched version.
"""
def test_original_base_init_is_true_upstream(self) -> None:
self.assertTrue(
_original_base_init.__name__.startswith("__init__")
or "LLMAnalyzerBase" in str(_original_base_init),
)
def test_original_chatopenai_init_is_not_none(self) -> None:
from contrib.batch_scan.runner import _original_chatopenai_init
self.assertIsNotNone(
_original_chatopenai_init,
"_original_chatopenai_init must be captured at import time",
)
def test_original_asyncio_run_is_true_stdlib(self) -> None:
self.assertIs(_original_asyncio_run, asyncio.run,
"_original_asyncio_run should be the stdlib function (unpatched)")
# ═══════════════════════════════════════════════════════════════════════════
# Helpers (module-level reuse)
# ═══════════════════════════════════════════════════════════════════════════
def _assert_all_restored(test_case: unittest.TestCase) -> None:
"""Assert all 5 method references point to originals."""
test_case.assertIs(LLMAnalyzerBase.__init__, _original_base_init)
test_case.assertIs(LLMAnalyzerBase.parse_response, _original_base_parse)
test_case.assertIs(LLMAnalyzerBase.build_prompt, _original_base_build_prompt)
test_case.assertIs(LLMMetaAnalyzer.parse_response, _original_meta_parse)
test_case.assertIs(LLMMetaAnalyzer.build_prompt, _original_meta_build_prompt)
if __name__ == "__main__":
unittest.main()