gitinstall 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. gitinstall/__init__.py +61 -0
  2. gitinstall/_sdk.py +541 -0
  3. gitinstall/academic.py +831 -0
  4. gitinstall/admin.html +327 -0
  5. gitinstall/auto_update.py +384 -0
  6. gitinstall/autopilot.py +349 -0
  7. gitinstall/badge.py +476 -0
  8. gitinstall/checkpoint.py +330 -0
  9. gitinstall/cicd.py +499 -0
  10. gitinstall/clawhub.html +718 -0
  11. gitinstall/config_schema.py +353 -0
  12. gitinstall/db.py +984 -0
  13. gitinstall/db_backend.py +445 -0
  14. gitinstall/dep_chain.py +337 -0
  15. gitinstall/dependency_audit.py +1153 -0
  16. gitinstall/detector.py +542 -0
  17. gitinstall/doctor.py +493 -0
  18. gitinstall/education.py +869 -0
  19. gitinstall/enterprise.py +802 -0
  20. gitinstall/error_fixer.py +953 -0
  21. gitinstall/event_bus.py +251 -0
  22. gitinstall/executor.py +577 -0
  23. gitinstall/feature_flags.py +138 -0
  24. gitinstall/fetcher.py +921 -0
  25. gitinstall/huggingface.py +922 -0
  26. gitinstall/hw_detect.py +988 -0
  27. gitinstall/i18n.py +664 -0
  28. gitinstall/installer_registry.py +362 -0
  29. gitinstall/knowledge_base.py +379 -0
  30. gitinstall/license_check.py +605 -0
  31. gitinstall/llm.py +569 -0
  32. gitinstall/log.py +236 -0
  33. gitinstall/main.py +1408 -0
  34. gitinstall/mcp_agent.py +841 -0
  35. gitinstall/mcp_server.py +386 -0
  36. gitinstall/monorepo.py +810 -0
  37. gitinstall/multi_source.py +425 -0
  38. gitinstall/onboard.py +276 -0
  39. gitinstall/planner.py +222 -0
  40. gitinstall/planner_helpers.py +323 -0
  41. gitinstall/planner_known_projects.py +1010 -0
  42. gitinstall/planner_templates.py +996 -0
  43. gitinstall/remote_gpu.py +633 -0
  44. gitinstall/resilience.py +608 -0
  45. gitinstall/run_tests.py +572 -0
  46. gitinstall/skills.py +476 -0
  47. gitinstall/tool_schemas.py +324 -0
  48. gitinstall/trending.py +279 -0
  49. gitinstall/uninstaller.py +415 -0
  50. gitinstall/validate_top100.py +607 -0
  51. gitinstall/watchdog.py +180 -0
  52. gitinstall/web.py +1277 -0
  53. gitinstall/web_ui.html +2277 -0
  54. gitinstall-1.1.0.dist-info/METADATA +275 -0
  55. gitinstall-1.1.0.dist-info/RECORD +59 -0
  56. gitinstall-1.1.0.dist-info/WHEEL +5 -0
  57. gitinstall-1.1.0.dist-info/entry_points.txt +3 -0
  58. gitinstall-1.1.0.dist-info/licenses/LICENSE +21 -0
  59. gitinstall-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,572 @@
1
+ """
2
+ 完整功能测试套件
3
+ 运行方式:python3 run_tests.py
4
+ """
5
+ import sys
6
+ import json
7
+ import os
8
+ import traceback
9
+
10
+ sys.path.insert(0, os.path.dirname(__file__))
11
+
12
+ PASS = "✅"
13
+ FAIL = "❌"
14
+ WARN = "⚠️ "
15
+ results = []
16
+
17
+ def test(name, fn):
18
+ try:
19
+ fn()
20
+ results.append((PASS, name))
21
+ print(f" {PASS} {name}")
22
+ except AssertionError as e:
23
+ results.append((FAIL, name, str(e)))
24
+ print(f" {FAIL} {name}: {e}")
25
+ except Exception as e:
26
+ results.append((FAIL, name, traceback.format_exc()))
27
+ print(f" {FAIL} {name}: {e}")
28
+
29
+
30
+ # ──────────────────────────────────────────────
31
+ # 1. detector.py
32
+ # ──────────────────────────────────────────────
33
+ print("\n【1/7】 detector.py - 环境检测")
34
+
35
+ from detector import EnvironmentDetector, format_env_summary
36
+
37
+ def t_detect_os():
38
+ env = EnvironmentDetector().detect()
39
+ assert "os" in env, "缺少 os 字段"
40
+ assert env["os"]["type"] in ("macos", "linux", "windows"), f"未知 os.type: {env['os']['type']}"
41
+
42
+ def t_detect_hardware():
43
+ env = EnvironmentDetector().detect()
44
+ hw = env["hardware"]
45
+ assert hw["cpu_count"] > 0, "cpu_count <= 0"
46
+ assert hw["ram_gb"] > 0, "ram_gb <= 0"
47
+
48
+ def t_detect_gpu():
49
+ env = EnvironmentDetector().detect()
50
+ gpu = env["gpu"]
51
+ assert "type" in gpu, "缺少 gpu.type"
52
+ assert gpu["type"] in ("apple_mps", "mps", "cuda", "rocm", "cpu_only"), f"未知 gpu.type: {gpu['type']}"
53
+
54
+ def t_detect_disk():
55
+ env = EnvironmentDetector().detect()
56
+ disk = env["disk"]
57
+ assert disk["free_gb"] > 0, "free_gb <= 0"
58
+
59
+ def t_detect_network():
60
+ env = EnvironmentDetector().detect()
61
+ net = env["network"]
62
+ assert "github" in net, "缺少 network.github"
63
+ assert "pypi" in net, "缺少 network.pypi"
64
+
65
+ def t_format_summary():
66
+ env = EnvironmentDetector().detect()
67
+ summary = format_env_summary(env)
68
+ assert len(summary) > 50, "summary 太短"
69
+ assert "OS" in summary or "macOS" in summary or "Linux" in summary or "Windows" in summary, \
70
+ "summary 不含平台信息"
71
+
72
+ test("detect OS 类型", t_detect_os)
73
+ test("detect 硬件 (CPU/RAM)", t_detect_hardware)
74
+ test("detect GPU 类型", t_detect_gpu)
75
+ test("detect 磁盘空间", t_detect_disk)
76
+ test("detect 网络可达性", t_detect_network)
77
+ test("format_env_summary 输出", t_format_summary)
78
+
79
+
80
+ # ──────────────────────────────────────────────
81
+ # 2. llm.py
82
+ # ──────────────────────────────────────────────
83
+ print("\n【2/7】 llm.py - LLM 适配器")
84
+
85
+ from llm import create_provider, HeuristicProvider, BaseLLMProvider, INSTALL_SYSTEM_PROMPT, ERROR_FIX_SYSTEM_PROMPT
86
+
87
+ def t_heuristic_always_available():
88
+ p = create_provider(force="none")
89
+ assert isinstance(p, HeuristicProvider), "force=none 应返回 HeuristicProvider"
90
+
91
+ def t_heuristic_name():
92
+ p = HeuristicProvider()
93
+ assert len(p.name) > 0, "name 不能为空"
94
+
95
+ def t_heuristic_extract_pip():
96
+ p = HeuristicProvider()
97
+ fake_readme = "```bash\npip install torch\npip install transformers\n```"
98
+ result = p.complete("", fake_readme)
99
+ data = json.loads(result)
100
+ cmds = [s["command"] for s in data.get("steps", [])]
101
+ assert any("pip install torch" in c for c in cmds), f"未提取到 pip install torch, cmds={cmds}"
102
+
103
+ def t_heuristic_extract_git_clone():
104
+ p = HeuristicProvider()
105
+ fake = "```bash\ngit clone https://github.com/foo/bar.git\ncd bar\n```"
106
+ result = p.complete("", fake)
107
+ data = json.loads(result)
108
+ cmds = [s["command"] for s in data.get("steps", [])]
109
+ assert any("git clone" in c for c in cmds), f"未提取到 git clone, cmds={cmds}"
110
+
111
+ def t_heuristic_blocks_dangerous():
112
+ p = HeuristicProvider()
113
+ fake = "```bash\nrm -rf /\ngit clone https://github.com/foo/bar.git\n```"
114
+ result = p.complete("", fake)
115
+ data = json.loads(result)
116
+ cmds = [s["command"] for s in data.get("steps", [])]
117
+ assert not any("rm -rf /" in c for c in cmds), "危险命令未被过滤"
118
+
119
+ def t_heuristic_no_steps_message():
120
+ p = HeuristicProvider()
121
+ result = p.complete("", "这是一段没有任何代码块的纯文字 README。")
122
+ data = json.loads(result)
123
+ assert data["status"] == "insufficient_data", f"无命令时 status 应为 insufficient_data, 得 {data['status']}"
124
+
125
+ def t_create_provider_fallback():
126
+ # 不论环境如何,create_provider 永远返回一个可用的 Provider
127
+ p = create_provider(force="none")
128
+ assert isinstance(p, BaseLLMProvider), "必须是 BaseLLMProvider 子类"
129
+ # 调用 complete 不崩溃
130
+ r = p.complete("system", "user")
131
+ assert isinstance(r, str), "complete() 应返回 str"
132
+
133
+ def t_system_prompts_nonempty():
134
+ assert len(INSTALL_SYSTEM_PROMPT) > 100, "INSTALL_SYSTEM_PROMPT 太短"
135
+ assert len(ERROR_FIX_SYSTEM_PROMPT) > 50, "ERROR_FIX_SYSTEM_PROMPT 太短"
136
+
137
+ test("HeuristicProvider 永远可用", t_heuristic_always_available)
138
+ test("HeuristicProvider.name 非空", t_heuristic_name)
139
+ test("提取 pip install 命令", t_heuristic_extract_pip)
140
+ test("提取 git clone 命令", t_heuristic_extract_git_clone)
141
+ test("过滤 rm -rf / 危险命令", t_heuristic_blocks_dangerous)
142
+ test("无代码块时返回 insufficient_data", t_heuristic_no_steps_message)
143
+ test("create_provider 降级到 HeuristicProvider", t_create_provider_fallback)
144
+ test("System Prompt 非空", t_system_prompts_nonempty)
145
+
146
+
147
+ # ──────────────────────────────────────────────
148
+ # 3. planner.py
149
+ # ──────────────────────────────────────────────
150
+ print("\n【3/7】 planner.py - SmartPlanner")
151
+
152
+ from planner import SmartPlanner, _torch_install_cmd, _venv_activate, _node_pm
153
+
154
+ ENV_MAC_M3 = {
155
+ "os": {"type": "macos", "arch": "arm64", "chip": "M3", "is_apple_silicon": True},
156
+ "gpu": {"type": "apple_mps"},
157
+ "package_managers": {"pip": {}, "pip3": {}, "brew": {}, "npm": {}, "pnpm": {}},
158
+ "runtimes": {"python3": {"available": True}, "git": {"available": True}},
159
+ }
160
+ ENV_LINUX_CUDA12 = {
161
+ "os": {"type": "linux", "distro": "ubuntu"},
162
+ "gpu": {"type": "cuda", "cuda_version": "12.1"},
163
+ "package_managers": {"pip3": {}, "apt": {}},
164
+ "runtimes": {"python3": {}, "git": {}},
165
+ }
166
+ ENV_WIN_CPU = {
167
+ "os": {"type": "windows"},
168
+ "gpu": {"type": "cpu_only"},
169
+ "package_managers": {"pip": {}, "winget": {}},
170
+ "runtimes": {"python": {}, "git": {}},
171
+ }
172
+
173
+ def t_torch_mps():
174
+ cmd = _torch_install_cmd(ENV_MAC_M3)
175
+ assert "cu1" not in cmd and "rocm" not in cmd, f"MPS 不应包含 CUDA/ROCm: {cmd}"
176
+ assert "torch" in cmd, "应包含 torch"
177
+
178
+ def t_torch_cuda12():
179
+ cmd = _torch_install_cmd(ENV_LINUX_CUDA12)
180
+ assert "cu121" in cmd, f"CUDA12 应用 cu121, 得: {cmd}"
181
+
182
+ def t_torch_windows_cpu():
183
+ cmd = _torch_install_cmd(ENV_WIN_CPU)
184
+ assert "cpu" in cmd.lower(), f"CPU only 应包含 cpu 索引: {cmd}"
185
+
186
+ def t_venv_activate_unix():
187
+ cmd = _venv_activate(ENV_MAC_M3)
188
+ assert cmd.startswith("source"), f"macOS venv activate 应用 source, 得: {cmd}"
189
+
190
+ def t_venv_activate_windows():
191
+ cmd = _venv_activate(ENV_WIN_CPU)
192
+ assert "Scripts" in cmd or "activate" in cmd, f"Windows activate 路径不对: {cmd}"
193
+
194
+ def t_node_pm_pnpm():
195
+ install, dev = _node_pm(ENV_MAC_M3) # ENV_MAC_M3 有 pnpm
196
+ assert install.startswith("pnpm"), f"有 pnpm 时应优先 pnpm, 得: {install}"
197
+
198
+ def t_known_project_comfyui():
199
+ p = SmartPlanner()
200
+ plan = p.generate_plan("comfyanonymous", "ComfyUI", ENV_MAC_M3, ["python", "pytorch"], {}, "")
201
+ assert plan["confidence"] == "high", f"ComfyUI 应为 high, 得: {plan['confidence']}"
202
+ assert plan["strategy"] == "known_project"
203
+ cmds = [s["command"] for s in plan["steps"]]
204
+ # 应包含 git clone
205
+ assert any("git clone" in c for c in cmds), f"缺少 git clone: {cmds}"
206
+ # MPS 环境 torch 不应有 cu1xx
207
+ torch_cmds = [c for c in cmds if "torch" in c]
208
+ assert torch_cmds, "没有 torch 安装命令"
209
+ assert all("cu1" not in c for c in torch_cmds), f"MPS 环境不应含 CUDA index: {torch_cmds}"
210
+
211
+ def t_known_project_ollama_linux():
212
+ p = SmartPlanner()
213
+ plan = p.generate_plan("ollama", "ollama", ENV_LINUX_CUDA12, [], {}, "")
214
+ assert plan["confidence"] == "high"
215
+ cmds = [s["command"] for s in plan["steps"]]
216
+ assert any("curl" in c for c in cmds), "Linux Ollama 应用 curl 安装"
217
+
218
+ def t_unknown_ml_project():
219
+ p = SmartPlanner()
220
+ plan = p.generate_plan(
221
+ "unknown-user", "my-ai-tool",
222
+ ENV_LINUX_CUDA12,
223
+ ["python", "pytorch", "diffusers"],
224
+ {"requirements.txt": "torch\ndiffusers"},
225
+ ""
226
+ )
227
+ assert plan["strategy"] == "type_template_python_ml"
228
+ cmds = [s["command"] for s in plan["steps"]]
229
+ cuda_cmds = [c for c in cmds if "cu121" in c]
230
+ assert cuda_cmds, f"CUDA12 Linux 应有 cu121 安装命令, cmds={cmds}"
231
+
232
+ def t_node_project():
233
+ p = SmartPlanner()
234
+ plan = p.generate_plan("user", "my-front", ENV_MAC_M3, ["node"], {"package.json": "{}"}, "")
235
+ assert "node" in plan["strategy"]
236
+ assert plan["launch_command"] in ("pnpm dev", "yarn dev", "npm run dev")
237
+
238
+ def t_rust_project():
239
+ p = SmartPlanner()
240
+ plan = p.generate_plan("user", "my-cli", ENV_MAC_M3, ["rust"], {"Cargo.toml": ""}, "")
241
+ assert "rust" in plan["strategy"]
242
+ cmds = [s["command"] for s in plan["steps"]]
243
+ assert any("cargo" in c for c in cmds), "Rust 项目应有 cargo 命令"
244
+
245
+ def t_dangerous_cmd_filtered_in_plan():
246
+ p = SmartPlanner()
247
+ # 手动构造含危险命令的 README
248
+ bad_readme = "```bash\nrm -rf /\ngit clone https://github.com/foo/bar.git\n```"
249
+ plan = p.generate_plan("foo", "bar", ENV_MAC_M3, [], {}, bad_readme)
250
+ cmds = [s["command"] for s in plan["steps"]]
251
+ assert not any("rm -rf /" in c for c in cmds), "危险命令不应出现在计划中"
252
+
253
+ def t_plan_steps_have_required_fields():
254
+ p = SmartPlanner()
255
+ plan = p.generate_plan("comfyanonymous", "ComfyUI", ENV_MAC_M3, ["python"], {}, "")
256
+ for i, step in enumerate(plan["steps"]):
257
+ assert "command" in step, f"step[{i}] 缺少 command"
258
+ assert "description" in step, f"step[{i}] 缺少 description"
259
+ assert "_warning" in step, f"step[{i}] 缺少 _warning 字段"
260
+
261
+ test("_torch_install_cmd: Apple MPS 无 CUDA", t_torch_mps)
262
+ test("_torch_install_cmd: CUDA 12 → cu121", t_torch_cuda12)
263
+ test("_torch_install_cmd: Windows CPU only", t_torch_windows_cpu)
264
+ test("_venv_activate: macOS 用 source", t_venv_activate_unix)
265
+ test("_venv_activate: Windows 用 Scripts", t_venv_activate_windows)
266
+ test("_node_pm: pnpm 优先于 npm", t_node_pm_pnpm)
267
+ test("已知项目 ComfyUI(M3)→ high confidence + MPS torch", t_known_project_comfyui)
268
+ test("已知项目 Ollama(Linux)→ curl 安装", t_known_project_ollama_linux)
269
+ test("未知 ML 项目(CUDA12)→ 类型模板 + cu121", t_unknown_ml_project)
270
+ test("Node.js 项目模板", t_node_project)
271
+ test("Rust 项目模板", t_rust_project)
272
+ test("README 提取时过滤危险命令", t_dangerous_cmd_filtered_in_plan)
273
+ test("所有 step 含 command/description/_warning", t_plan_steps_have_required_fields)
274
+
275
+
276
+ # ──────────────────────────────────────────────
277
+ # 4. executor.py - 安全检查(不真正执行)
278
+ # ──────────────────────────────────────────────
279
+ print("\n【4/7】 executor.py - 安全检查")
280
+
281
+ from executor import check_command_safety, BLOCKED_PATTERNS, WARN_PATTERNS
282
+
283
+ def t_block_rm_rf_root():
284
+ safe, msg = check_command_safety("rm -rf /")
285
+ assert not safe, "rm -rf / 应被拒绝"
286
+
287
+ def t_block_fork_bomb():
288
+ safe, msg = check_command_safety(":(){ :|:& };:")
289
+ assert not safe, "fork bomb 应被拒绝"
290
+
291
+ def t_block_mkfs():
292
+ safe, msg = check_command_safety("mkfs.ext4 /dev/sda")
293
+ assert not safe, "mkfs 应被拒绝"
294
+
295
+ def t_block_dd():
296
+ safe, msg = check_command_safety("dd if=/dev/zero of=/dev/sda")
297
+ assert not safe, "dd 覆盖磁盘应被拒绝"
298
+
299
+ def t_warn_sudo():
300
+ safe, msg = check_command_safety("sudo apt install python3")
301
+ assert safe, "sudo apt 应允许(带警告)"
302
+ assert len(msg) > 0, "sudo 应有警告信息"
303
+
304
+ def t_warn_curl_pipe():
305
+ safe, msg = check_command_safety("curl -fsSL https://example.com/install.sh | sh")
306
+ assert safe, "curl|sh 应允许(带警告)"
307
+ assert len(msg) > 0, "curl|sh 应有警告"
308
+
309
+ def t_safe_pip_install():
310
+ safe, msg = check_command_safety("pip install torch")
311
+ assert safe, "pip install 应安全"
312
+ assert msg == "", f"pip install 不应有警告, 得: {msg}"
313
+
314
+ def t_safe_git_clone():
315
+ safe, msg = check_command_safety("git clone https://github.com/foo/bar.git")
316
+ assert safe, "git clone 应安全"
317
+
318
+ def t_safe_python_run():
319
+ safe, msg = check_command_safety("python main.py --listen")
320
+ assert safe, "python main.py 应安全"
321
+
322
+ test("拦截 rm -rf /", t_block_rm_rf_root)
323
+ test("拦截 fork bomb", t_block_fork_bomb)
324
+ test("拦截 mkfs.*", t_block_mkfs)
325
+ test("拦截 dd if=/dev/...", t_block_dd)
326
+ test("警告 sudo(但允许)", t_warn_sudo)
327
+ test("警告 curl|sh(但允许)", t_warn_curl_pipe)
328
+ test("pip install 安全无警告", t_safe_pip_install)
329
+ test("git clone 安全", t_safe_git_clone)
330
+ test("python main.py 安全", t_safe_python_run)
331
+
332
+
333
+ # ──────────────────────────────────────────────
334
+ # 5. fetcher.py - 解析逻辑(不请求网络)
335
+ # ──────────────────────────────────────────────
336
+ print("\n【5/7】 fetcher.py - 项目标识解析")
337
+
338
+ from fetcher import parse_repo_identifier, detect_project_types
339
+
340
+ def t_parse_full_url():
341
+ owner, repo = parse_repo_identifier("https://github.com/comfyanonymous/ComfyUI")
342
+ assert owner == "comfyanonymous", f"owner={owner}"
343
+ assert repo == "ComfyUI", f"repo={repo}"
344
+
345
+ def t_parse_owner_slash_repo():
346
+ owner, repo = parse_repo_identifier("ollama/ollama")
347
+ assert owner == "ollama"
348
+ assert repo == "ollama"
349
+
350
+ def t_parse_url_with_git():
351
+ owner, repo = parse_repo_identifier("https://github.com/hiyouga/LLaMA-Factory.git")
352
+ assert owner == "hiyouga"
353
+ assert repo == "LLaMA-Factory"
354
+
355
+ def t_parse_url_with_trailing_slash():
356
+ owner, repo = parse_repo_identifier("https://github.com/AUTOMATIC1111/stable-diffusion-webui/")
357
+ assert owner == "AUTOMATIC1111"
358
+ assert repo == "stable-diffusion-webui"
359
+
360
+ def t_detect_python():
361
+ types = detect_project_types(
362
+ {"language": "Python"},
363
+ "This is a Python project",
364
+ {"requirements.txt": "torch\n"}
365
+ )
366
+ assert "python" in types, f"应检测到 python: {types}"
367
+
368
+ def t_detect_pytorch():
369
+ types = detect_project_types(
370
+ {"language": "Python"},
371
+ "pip install torch\nThis uses pytorch",
372
+ {}
373
+ )
374
+ assert "pytorch" in types, f"应检测到 pytorch: {types}"
375
+
376
+ def t_detect_docker():
377
+ types = detect_project_types(
378
+ {"language": "Python"},
379
+ "docker run -it foo/bar",
380
+ {"Dockerfile": "FROM python:3.11"}
381
+ )
382
+ assert "docker" in types, f"应检测到 docker: {types}"
383
+
384
+ def t_detect_node():
385
+ types = detect_project_types(
386
+ {"language": "JavaScript"},
387
+ "npm install",
388
+ {"package.json": "{}"}
389
+ )
390
+ assert "node" in types, f"应检测到 node: {types}"
391
+
392
+ test("解析完整 GitHub URL", t_parse_full_url)
393
+ test("解析 owner/repo 格式", t_parse_owner_slash_repo)
394
+ test("解析 .git 后缀 URL", t_parse_url_with_git)
395
+ test("解析末尾带 / 的 URL", t_parse_url_with_trailing_slash)
396
+ test("检测 Python 项目类型", t_detect_python)
397
+ test("检测 PyTorch 关键词", t_detect_pytorch)
398
+ test("检测 Docker 类型", t_detect_docker)
399
+ test("检测 Node.js 类型", t_detect_node)
400
+
401
+
402
+ # ──────────────────────────────────────────────
403
+ # 6. main.py - cmd_plan 逻辑(Mock 网络)
404
+ # ──────────────────────────────────────────────
405
+ print("\n【6/7】 main.py - cmd_plan 逻辑(模拟数据)")
406
+
407
+ import main as _main
408
+ from planner import SmartPlanner
409
+ from unittest.mock import patch, MagicMock
410
+ from fetcher import RepoInfo
411
+
412
+ def _make_fake_info(owner="comfyanonymous", repo="ComfyUI", project_type=None):
413
+ return RepoInfo(
414
+ owner=owner, repo=repo,
415
+ full_name=f"{owner}/{repo}",
416
+ description="test",
417
+ stars=1000,
418
+ language="Python",
419
+ license="GPL",
420
+ default_branch="main",
421
+ readme="# ComfyUI\n```bash\ngit clone https://github.com/comfyanonymous/ComfyUI.git\n```",
422
+ project_type=project_type or ["python", "pytorch"],
423
+ dependency_files={"requirements.txt": "torch\n"},
424
+ clone_url=f"https://github.com/{owner}/{repo}.git",
425
+ homepage=f"https://github.com/{owner}/{repo}",
426
+ )
427
+
428
+ def t_cmd_plan_known_project_no_llm():
429
+ """ComfyUI 是已知项目,应走 SmartPlanner 不调用 LLM"""
430
+ fake_info = _make_fake_info()
431
+ llm_called = []
432
+
433
+ with patch("main.fetch_project", return_value=fake_info):
434
+ with patch("main.EnvironmentDetector") as MockDet:
435
+ MockDet.return_value.detect.return_value = {
436
+ "os": {"type": "macos", "arch": "arm64", "is_apple_silicon": True},
437
+ "gpu": {"type": "apple_mps"},
438
+ "package_managers": {"pip": {}},
439
+ "runtimes": {},
440
+ }
441
+ # 监视 LLM 是否被调用
442
+ orig_complete = _main.HeuristicProvider.complete
443
+ def spy_complete(self, *args, **kwargs):
444
+ llm_called.append("heuristic_called")
445
+ return orig_complete(self, *args, **kwargs)
446
+
447
+ result = _main.cmd_plan("comfyanonymous/ComfyUI", llm_force="none")
448
+
449
+ assert result["status"] == "ok", f"应返回 ok, 得: {result}"
450
+ assert result["confidence"] == "high", "已知项目应 high confidence"
451
+ assert len(result["plan"]["steps"]) > 0, "应有安装步骤"
452
+
453
+ def t_cmd_plan_error_handling():
454
+ """fetch 失败时应返回 error"""
455
+ with patch("main.fetch_project", side_effect=FileNotFoundError("项目不存在")):
456
+ result = _main.cmd_plan("nonexistent/norepo999xyz")
457
+ assert result["status"] == "error", f"应返回 error, 得: {result}"
458
+ assert "message" in result
459
+
460
+ def t_cmd_plan_steps_safe():
461
+ """所有步骤都应通过安全检查"""
462
+ fake_info = _make_fake_info()
463
+ with patch("main.fetch_project", return_value=fake_info):
464
+ with patch("main.EnvironmentDetector") as MockDet:
465
+ MockDet.return_value.detect.return_value = {
466
+ "os": {"type": "macos", "arch": "arm64", "is_apple_silicon": True},
467
+ "gpu": {"type": "apple_mps"},
468
+ "package_managers": {"pip": {}},
469
+ "runtimes": {},
470
+ }
471
+ result = _main.cmd_plan("comfyanonymous/ComfyUI", llm_force="none")
472
+
473
+ for step in result["plan"]["steps"]:
474
+ assert step.get("_safe") is not False, f"步骤不安全: {step['command']}"
475
+
476
+ def t_cmd_detect_returns_env():
477
+ env_result = _main.cmd_detect()
478
+ assert env_result["status"] == "ok"
479
+ assert "env" in env_result
480
+ assert "os" in env_result["env"]
481
+
482
+ test("cmd_plan 已知项目 → high confidence", t_cmd_plan_known_project_no_llm)
483
+ test("cmd_plan 项目不存在 → error", t_cmd_plan_error_handling)
484
+ test("cmd_plan 所有步骤通过安全检查", t_cmd_plan_steps_safe)
485
+ test("cmd_detect 返回完整 env", t_cmd_detect_returns_env)
486
+
487
+
488
+ # ──────────────────────────────────────────────
489
+ # 7. 边界场景测试
490
+ # ──────────────────────────────────────────────
491
+ print("\n【7/7】 边界场景")
492
+
493
+ def t_empty_readme_graceful():
494
+ """空 README 不应崩溃"""
495
+ p = SmartPlanner()
496
+ plan = p.generate_plan("foo", "bar-unknown-xyz", {
497
+ "os": {"type": "linux"},
498
+ "gpu": {"type": "cpu_only"},
499
+ "package_managers": {},
500
+ "runtimes": {},
501
+ }, [], {}, "")
502
+ assert "steps" in plan
503
+ assert "status" in plan or "confidence" in plan
504
+
505
+ def t_unknown_os_graceful():
506
+ """未知 OS 类型不应崩溃"""
507
+ from planner import _os_type, _python_cmd, _pip_cmd
508
+ env_weird = {"os": {"type": "freebsd"}, "gpu": {"type": "cpu_only"},
509
+ "package_managers": {}, "runtimes": {}}
510
+ # 这些函数不应抛出
511
+ _os_type(env_weird)
512
+ _python_cmd(env_weird)
513
+ _pip_cmd(env_weird)
514
+
515
+ def t_plan_no_steps_not_crash():
516
+ """README 无安装命令时返回合理结果"""
517
+ p = SmartPlanner()
518
+ plan = p.generate_plan("foo", "totally-unknown-xyz-abc", {
519
+ "os": {"type": "linux"}, "gpu": {"type": "cpu_only"},
520
+ "package_managers": {}, "runtimes": {},
521
+ }, [], {}, "This is just documentation.\nNo install commands here.")
522
+ assert isinstance(plan["steps"], list) # 即使是空列表也可以
523
+
524
+ def t_heuristic_multiblock():
525
+ """多个代码块不重复提取同一命令"""
526
+ p = HeuristicProvider()
527
+ readme = (
528
+ "```bash\npip install torch\n```\n"
529
+ "Some text\n"
530
+ "```bash\npip install torch\n```" # 重复
531
+ )
532
+ result = json.loads(p.complete("", readme))
533
+ cmds = [s["command"] for s in result.get("steps", [])]
534
+ torch_cmds = [c for c in cmds if c == "pip install torch"]
535
+ assert len(torch_cmds) <= 1, f"相同命令不应重复出现: {cmds}"
536
+
537
+ def t_parse_identifier_just_reponame():
538
+ """只给项目名(无 owner)"""
539
+ owner, repo = parse_repo_identifier("ComfyUI")
540
+ # owner 为空,交给搜索处理
541
+ assert repo == "ComfyUI", f"repo={repo}"
542
+
543
+ test("空 README 不崩溃", t_empty_readme_graceful)
544
+ test("未知 OS 类型不崩溃", t_unknown_os_graceful)
545
+ test("无安装命令时返回空步骤列表", t_plan_no_steps_not_crash)
546
+ test("重复代码块命令去重", t_heuristic_multiblock)
547
+ test("只给项目名(无 owner)graceful", t_parse_identifier_just_reponame)
548
+
549
+
550
+ # ──────────────────────────────────────────────
551
+ # 汇总
552
+ # ──────────────────────────────────────────────
553
+ print("\n" + "═" * 50)
554
+ total = len(results)
555
+ passed = sum(1 for r in results if r[0] == PASS)
556
+ failed = total - passed
557
+
558
+ print(f"测试结果:{passed}/{total} 通过", end="")
559
+ if failed:
560
+ print(f",{failed} 失败")
561
+ print("\n失败详情:")
562
+ for r in results:
563
+ if r[0] == FAIL:
564
+ print(f" {FAIL} {r[1]}")
565
+ if len(r) > 2:
566
+ # 只打印第一行
567
+ print(f" {r[2].splitlines()[-1]}")
568
+ else:
569
+ print(" 🎉")
570
+
571
+ print("═" * 50)
572
+ sys.exit(0 if failed == 0 else 1)