feat(analysis): prevent pipeline crash on single file analysis failure

Catch Gemini API errors (e.g., HTTP 429) and summarize upstream messages
Generate fallback markdown for failed files instead of aborting
Append analysis failures to pipeline warnings
This commit is contained in:
2026-04-02 10:59:04 +08:00
parent 0bdebd5368
commit c73767073e
4 changed files with 149 additions and 25 deletions

View File

@@ -14,7 +14,7 @@ fake_dotenv.load_dotenv = lambda *args, **kwargs: None
sys.modules.setdefault("dotenv", fake_dotenv)
from uipath_explainator.config import Settings
from uipath_explainator.gemini import GeminiAnalyzer
from uipath_explainator.gemini import AnalysisError, GeminiAnalyzer
class FakeHttpOptions:
@@ -159,6 +159,33 @@ class GeminiAnalyzerTests(unittest.TestCase):
self.assertIn("先讲这个文件在整个流程中的定位", prompt)
self.assertIn("判断逻辑、调用链、输入输出、关键变量、外部依赖", prompt)
def test_analyze_wraps_rate_limit_error_with_clear_message(self) -> None:
fake_types = SimpleNamespace(
HttpOptions=FakeHttpOptions,
GenerateContentConfig=FakeGenerateContentConfig,
)
fake_genai = ModuleType("google.genai")
fake_genai.Client = FakeClient
fake_genai.types = fake_types
fake_google = ModuleType("google")
fake_google.genai = fake_genai
with patch.dict(sys.modules, {"google": fake_google, "google.genai": fake_genai}):
analyzer = GeminiAnalyzer(Settings(api_key="test-key", base_url=None, model="gemini-test"))
error = RuntimeError("quota exceeded")
error.status_code = 429
error.response_json = {"error": {"message": "Resource has been exhausted (e.g. check quota)."}}
analyzer._client.models.generate_content = lambda **_: (_ for _ in ()).throw(error)
with self.assertRaises(AnalysisError) as captured:
analyzer.analyze(Path("main.xaml"), "<Sequence />")
self.assertIn("HTTP 429", str(captured.exception))
self.assertIn("Resource has been exhausted", str(captured.exception))
self.assertIn("--skip-analysis", str(captured.exception))
if __name__ == "__main__":
unittest.main()

View File

@@ -57,6 +57,13 @@ class StubAnalyzer:
return f"# {relative_path.as_posix()}\n\n{len(content)}"
class FlakyAnalyzer:
def analyze(self, relative_path: Path, content: str) -> str:
if relative_path.name == "Active.xaml":
raise RuntimeError("HTTP 429 quota exhausted")
return f"# {relative_path.as_posix()}\n\n{len(content)}"
class PipelineTests(unittest.TestCase):
def test_strip_comment_out_blocks_removes_nested_blocks(self) -> None:
source = "<root><ui:CommentOut><x/><ui:CommentOut><y/></ui:CommentOut></ui:CommentOut><z/></root>"
@@ -145,6 +152,27 @@ class PipelineTests(unittest.TestCase):
self.assertIn("Final scan complete:", combined)
self.assertIn("Pipeline completed in", combined)
def test_pipeline_keeps_running_when_single_analysis_fails(self) -> None:
with TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
project_root = tmp_path / "project"
output_root = tmp_path / "workspace"
docs_root = output_root / "docs"
(project_root / "Flows").mkdir(parents=True)
(project_root / "Flows" / "Active.xaml").write_text(ACTIVE_XAML, encoding="utf-8")
(project_root / "Scripts").mkdir()
(project_root / "Scripts" / "Keep.bas").write_text("Sub Keep()\nEnd Sub", encoding="utf-8")
(project_root / "main.xaml").write_text(MAIN_XAML, encoding="utf-8")
report = ProjectPipeline(project_root, output_root, "main.xaml", force=True).run(FlakyAnalyzer())
self.assertTrue((docs_root / "Flows" / "Active.xaml.analysis.md").exists())
fallback = (docs_root / "Flows" / "Active.xaml.analysis.md").read_text(encoding="utf-8")
self.assertIn("Gemini 分析失败", fallback)
self.assertIn("HTTP 429 quota exhausted", fallback)
self.assertTrue((docs_root / "Scripts" / "Keep.bas.analysis.md").exists())
self.assertTrue(any("Analysis failed for Flows/Active.xaml" in item for item in report.warnings))
if __name__ == "__main__":
unittest.main()