foxclaw 0.1.2 → 0.1.3

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 (344) hide show
  1. package/index.js +1 -8
  2. package/package.json +1 -1
  3. package/python/.env.example +8 -0
  4. package/python/.github/workflows/ci.yml +47 -2
  5. package/python/CHANGELOG.md +57 -0
  6. package/python/CHANGELOG_CN.md +57 -0
  7. package/python/README.md +11 -2
  8. package/python/README_CN.md +32 -20
  9. package/python/bench/bm25_audit.py +1 -14
  10. package/python/bench/bm25_regression.py +0 -1
  11. package/python/bench/bm25_stress.py +0 -4
  12. package/python/bench/cd_audit.py +0 -23
  13. package/python/bench/chaos.py +6 -8
  14. package/python/bench/e2e_real_models.py +0 -1
  15. package/python/bench/llm_retry_test.py +2 -9
  16. package/python/bench/load_test.py +5 -8
  17. package/python/bench/vector_ivf_upgrade.py +1 -7
  18. package/python/docs/RELEASE.md +4 -3
  19. package/python/docs/RUN_CN.md +2 -2
  20. package/python/evals/governance/cases.py +0 -37
  21. package/python/evals/governance/run_evals.py +5 -37
  22. package/python/examples/governance_middleware_demo.py +1 -6
  23. package/python/examples/langgraph_governance_demo.py +7 -11
  24. package/python/foxclaw/__init__.py +34 -152
  25. package/python/foxclaw/agents/base.py +65 -136
  26. package/python/foxclaw/agents/impl/_helpers.py +2 -10
  27. package/python/foxclaw/agents/impl/finance.py +4 -21
  28. package/python/foxclaw/agents/impl/office.py +7 -24
  29. package/python/foxclaw/agents/impl/supervisor.py +12 -79
  30. package/python/foxclaw/agents/mixins/_context.py +0 -23
  31. package/python/foxclaw/agents/mixins/augment.py +10 -16
  32. package/python/foxclaw/agents/mixins/llm.py +7 -6
  33. package/python/foxclaw/agents/mixins/memory.py +7 -1
  34. package/python/foxclaw/agents/mixins/preference.py +58 -8
  35. package/python/foxclaw/agents/mixins/prompt.py +0 -6
  36. package/python/foxclaw/agents/mixins/skillbox.py +6 -6
  37. package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
  38. package/python/foxclaw/api/__init__.py +18 -77
  39. package/python/foxclaw/api/_state.py +43 -0
  40. package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
  41. package/python/foxclaw/api/middleware.py +5 -28
  42. package/python/foxclaw/api/routes/ahs.py +60 -73
  43. package/python/foxclaw/api/routes/attachments.py +5 -6
  44. package/python/foxclaw/api/routes/browser.py +7 -11
  45. package/python/foxclaw/api/routes/domain.py +18 -26
  46. package/python/foxclaw/api/routes/knowledge.py +3 -21
  47. package/python/foxclaw/api/routes/llm_admin.py +21 -33
  48. package/python/foxclaw/api/routes/memory.py +28 -12
  49. package/python/foxclaw/api/routes/orchestration.py +3 -5
  50. package/python/foxclaw/api/routes/recovery.py +4 -3
  51. package/python/foxclaw/api/routes/review.py +2 -11
  52. package/python/foxclaw/api/routes/search.py +3 -7
  53. package/python/foxclaw/api/routes/system.py +33 -49
  54. package/python/foxclaw/api/routes/task.py +31 -49
  55. package/python/foxclaw/api/routes/ws.py +2 -6
  56. package/python/foxclaw/api/spine.py +1 -4
  57. package/python/foxclaw/browser/action/action_executor.py +1 -5
  58. package/python/foxclaw/browser/action/action_types.py +0 -1
  59. package/python/foxclaw/browser/action/url_policy.py +1 -5
  60. package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
  61. package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
  62. package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
  63. package/python/foxclaw/browser/perception/element_indexer.py +1 -2
  64. package/python/foxclaw/browser/service.py +7 -11
  65. package/python/foxclaw/capability/embedding_service.py +4 -6
  66. package/python/foxclaw/channels/__init__.py +9 -11
  67. package/python/foxclaw/channels/discord.py +4 -13
  68. package/python/foxclaw/channels/dispatcher.py +2 -27
  69. package/python/foxclaw/channels/feishu.py +10 -20
  70. package/python/foxclaw/channels/openclaw_bridge.py +4 -9
  71. package/python/foxclaw/channels/signing.py +9 -9
  72. package/python/foxclaw/channels/telegram.py +3 -4
  73. package/python/foxclaw/channels/weixin.py +7 -17
  74. package/python/foxclaw/cli.py +42 -13
  75. package/python/foxclaw/cognitive/reasoner.py +9 -10
  76. package/python/foxclaw/cognitive/state.py +18 -24
  77. package/python/foxclaw/core/audit_chain.py +1 -3
  78. package/python/foxclaw/core/audit_sink.py +3 -3
  79. package/python/foxclaw/core/compaction.py +8 -26
  80. package/python/foxclaw/core/config.py +2 -2
  81. package/python/foxclaw/core/confirm_nonce.py +6 -12
  82. package/python/foxclaw/core/envfile.py +2 -2
  83. package/python/foxclaw/core/failure_policy.py +17 -26
  84. package/python/foxclaw/core/gateway.py +6 -10
  85. package/python/foxclaw/core/governance_audit.py +3 -7
  86. package/python/foxclaw/core/idempotency.py +15 -22
  87. package/python/foxclaw/core/injection_guard.py +10 -29
  88. package/python/foxclaw/core/memory_adapter.py +30 -57
  89. package/python/foxclaw/core/metrics.py +4 -5
  90. package/python/foxclaw/core/model_router.py +36 -148
  91. package/python/foxclaw/core/paths.py +1 -1
  92. package/python/foxclaw/core/recovery.py +21 -34
  93. package/python/foxclaw/core/scheduler.py +23 -26
  94. package/python/foxclaw/core/secrets.py +0 -3
  95. package/python/foxclaw/core/sessions.py +4 -6
  96. package/python/foxclaw/core/settings.py +16 -32
  97. package/python/foxclaw/core/shared_write.py +11 -23
  98. package/python/foxclaw/core/steering_queue.py +12 -11
  99. package/python/foxclaw/core/system_bus.py +1 -1
  100. package/python/foxclaw/core/thought_chain.py +30 -32
  101. package/python/foxclaw/core/token_meter.py +9 -13
  102. package/python/foxclaw/core/tools.py +4 -12
  103. package/python/foxclaw/core/tracing.py +25 -40
  104. package/python/foxclaw/core/truth_pipeline.py +5 -9
  105. package/python/foxclaw/core/update_check.py +149 -0
  106. package/python/foxclaw/core/vec_recall_adapter.py +11 -37
  107. package/python/foxclaw/core/verify.py +6 -14
  108. package/python/foxclaw/core/web_search.py +4 -28
  109. package/python/foxclaw/finance/evidence.py +14 -18
  110. package/python/foxclaw/finance/fx.py +1 -3
  111. package/python/foxclaw/finance/graph.py +9 -18
  112. package/python/foxclaw/finance/models.py +20 -24
  113. package/python/foxclaw/finance/nlparse.py +12 -72
  114. package/python/foxclaw/finance/policy.py +8 -36
  115. package/python/foxclaw/finance/reasoner.py +3 -11
  116. package/python/foxclaw/finance/reasoning.py +13 -34
  117. package/python/foxclaw/finance/reconcile.py +5 -10
  118. package/python/foxclaw/finance/runtime.py +19 -46
  119. package/python/foxclaw/governance/middleware.py +13 -21
  120. package/python/foxclaw/harness/_base.py +2 -8
  121. package/python/foxclaw/harness/_policy.py +19 -38
  122. package/python/foxclaw/harness/drift.py +1 -13
  123. package/python/foxclaw/harness/event_bus.py +2 -5
  124. package/python/foxclaw/harness/harness.py +9 -97
  125. package/python/foxclaw/harness/loop_detection.py +11 -18
  126. package/python/foxclaw/itinerary/amap_client.py +2 -5
  127. package/python/foxclaw/itinerary/models.py +7 -7
  128. package/python/foxclaw/itinerary/reasoner.py +1 -6
  129. package/python/foxclaw/itinerary/reasoning.py +15 -21
  130. package/python/foxclaw/itinerary/runtime.py +8 -12
  131. package/python/foxclaw/kernel/capabilities.py +5 -6
  132. package/python/foxclaw/kernel/policy_gate.py +4 -10
  133. package/python/foxclaw/kernel/process_manager.py +17 -36
  134. package/python/foxclaw/kernel/scheduler.py +3 -9
  135. package/python/foxclaw/kernel/sentinel.py +13 -23
  136. package/python/foxclaw/kernel/sentinel_client.py +12 -15
  137. package/python/foxclaw/kernel/spine.py +9 -9
  138. package/python/foxclaw/knowledge/__init__.py +4 -20
  139. package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
  140. package/python/foxclaw/learning/knowledge_graph.py +4 -9
  141. package/python/foxclaw/learning/models.py +13 -13
  142. package/python/foxclaw/learning/reasoner.py +1 -4
  143. package/python/foxclaw/learning/runtime.py +19 -31
  144. package/python/foxclaw/learning/spaced_repetition.py +2 -7
  145. package/python/foxclaw/mcp_server.py +2 -3
  146. package/python/foxclaw/orchestration/__init__.py +16 -33
  147. package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
  148. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  149. package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
  150. package/python/foxclaw/planning/llm_planner.py +3 -3
  151. package/python/foxclaw/planning/planner.py +6 -11
  152. package/python/foxclaw/search/agent_port.py +7 -11
  153. package/python/foxclaw/search/evidence.py +0 -4
  154. package/python/foxclaw/search/internal_search.py +3 -5
  155. package/python/foxclaw/search/models.py +11 -13
  156. package/python/foxclaw/search/planner.py +3 -3
  157. package/python/foxclaw/search/retriever.py +1 -1
  158. package/python/foxclaw/search/runtime.py +1 -8
  159. package/python/foxclaw/skills/__init__.py +46 -100
  160. package/python/foxclaw/skills/archive.py +111 -0
  161. package/python/foxclaw/skills/fetch.py +226 -0
  162. package/python/foxclaw/skills/hooks_spec.py +21 -25
  163. package/python/foxclaw/skills/install_cmd.py +216 -0
  164. package/python/foxclaw/skills/lifecycle.py +16 -42
  165. package/python/foxclaw/skills/markdown_skill.py +10 -9
  166. package/python/foxclaw/skills/mcp_client.py +5 -11
  167. package/python/foxclaw/skills/mcp_store.py +0 -3
  168. package/python/foxclaw/skills/permission.py +13 -48
  169. package/python/foxclaw/skills/pipeline.py +21 -53
  170. package/python/foxclaw/skills/policy.py +4 -10
  171. package/python/foxclaw/skills/review.py +3 -3
  172. package/python/frontend/deck/auth.js +0 -7
  173. package/python/frontend/deck/chat_model_picker.js +0 -54
  174. package/python/frontend/deck/domain.js +0 -20
  175. package/python/frontend/deck/main.js +0 -144
  176. package/python/frontend/deck/markdown.js +0 -4
  177. package/python/frontend/deck/settings.js +1 -18
  178. package/python/frontend/deck/state.js +0 -1
  179. package/python/frontend/deck/util.js +0 -2
  180. package/python/frontend/deck/visuals.js +0 -22
  181. package/python/frontend/planet/planet.js +0 -201
  182. package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
  183. package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
  184. package/python/frontend/v3/index.html +3 -3
  185. package/python/frontend/widget_runtime.js +0 -43
  186. package/python/frontend/widgets.js +0 -57
  187. package/python/frontend_src/index.html +1 -1
  188. package/python/frontend_src/package-lock.json +2 -2
  189. package/python/frontend_src/package.json +1 -1
  190. package/python/frontend_src/src/App.vue +3 -7
  191. package/python/frontend_src/src/api.js +0 -7
  192. package/python/frontend_src/src/components/ChatDock.vue +27 -39
  193. package/python/frontend_src/src/components/DocViewer.vue +0 -9
  194. package/python/frontend_src/src/components/Inspector.vue +0 -6
  195. package/python/frontend_src/src/components/LeftRail.vue +60 -39
  196. package/python/frontend_src/src/components/MenuPopover.vue +9 -11
  197. package/python/frontend_src/src/components/MessageStream.vue +18 -30
  198. package/python/frontend_src/src/components/ModelPicker.vue +0 -5
  199. package/python/frontend_src/src/components/QuickIngest.vue +0 -6
  200. package/python/frontend_src/src/components/Stage.vue +71 -25
  201. package/python/frontend_src/src/components/ToolMenu.vue +0 -3
  202. package/python/frontend_src/src/components/TopBar.vue +38 -10
  203. package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
  204. package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
  205. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
  206. package/python/frontend_src/src/components/panels/MemoryPanel.vue +0 -1
  207. package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
  208. package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
  209. package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
  210. package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
  211. package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
  212. package/python/frontend_src/src/lib/fmt.js +0 -4
  213. package/python/frontend_src/src/lib/i18n.js +53 -0
  214. package/python/frontend_src/src/lib/locales/en.js +173 -0
  215. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  216. package/python/frontend_src/src/lib/md.js +0 -14
  217. package/python/frontend_src/src/main.js +2 -0
  218. package/python/frontend_src/src/stores/app.js +17 -32
  219. package/python/frontend_src/src/stores/chat.js +0 -34
  220. package/python/frontend_src/src/stores/model.js +0 -8
  221. package/python/frontend_src/src/stores/panels.js +0 -41
  222. package/python/frontend_src/src/stores/profile.js +31 -0
  223. package/python/frontend_src/src/stores/system.js +25 -0
  224. package/python/frontend_src/vite.config.js +0 -7
  225. package/python/pyproject.toml +6 -3
  226. package/python/requirements.txt +311 -0
  227. package/python/tests/conftest.py +0 -22
  228. package/python/tests/run_all.py +3 -66
  229. package/python/tests/test_agent_lifecycle.py +4 -38
  230. package/python/tests/test_ahs_p2.py +4 -25
  231. package/python/tests/test_anthropic_tools.py +1 -15
  232. package/python/tests/test_api.py +19 -9
  233. package/python/tests/test_api_auth.py +1 -5
  234. package/python/tests/test_attachments.py +9 -25
  235. package/python/tests/test_audit_chain.py +2 -7
  236. package/python/tests/test_audit_sink.py +0 -6
  237. package/python/tests/test_browser.py +2 -6
  238. package/python/tests/test_browser_request_guard.py +0 -4
  239. package/python/tests/test_capabilities.py +0 -6
  240. package/python/tests/test_channel_signing.py +2 -9
  241. package/python/tests/test_channels.py +1 -11
  242. package/python/tests/test_config_layering.py +0 -4
  243. package/python/tests/test_confirm_nonce.py +1 -12
  244. package/python/tests/test_context_compaction_wiring.py +1 -7
  245. package/python/tests/test_dispatch.py +0 -6
  246. package/python/tests/test_domain_evals.py +11 -80
  247. package/python/tests/test_domain_import.py +0 -6
  248. package/python/tests/test_domain_reasoners.py +0 -14
  249. package/python/tests/test_e2e.py +0 -17
  250. package/python/tests/test_emb_cache.py +7 -17
  251. package/python/tests/test_embedding_singleton.py +1 -3
  252. package/python/tests/test_evals_smoke.py +0 -24
  253. package/python/tests/test_event_spine.py +10 -18
  254. package/python/tests/test_evidence_semantic.py +1 -14
  255. package/python/tests/test_evidence_sse.py +3 -6
  256. package/python/tests/test_file_ingest.py +0 -11
  257. package/python/tests/test_finance.py +0 -9
  258. package/python/tests/test_finance_ledger.py +5 -12
  259. package/python/tests/test_finance_persist.py +8 -12
  260. package/python/tests/test_gateway.py +3 -22
  261. package/python/tests/test_governance.py +5 -52
  262. package/python/tests/test_governance_audit.py +4 -39
  263. package/python/tests/test_governance_middleware.py +1 -7
  264. package/python/tests/test_hkc.py +6 -24
  265. package/python/tests/test_hkc_index_build_manager.py +0 -33
  266. package/python/tests/test_hkc_manifest_reuse.py +4 -27
  267. package/python/tests/test_hkc_search_event_sync.py +3 -54
  268. package/python/tests/test_import_path_guard.py +0 -6
  269. package/python/tests/test_injection_guard.py +18 -36
  270. package/python/tests/test_integration.py +5 -29
  271. package/python/tests/test_itinerary.py +0 -3
  272. package/python/tests/test_kee_semantic.py +0 -5
  273. package/python/tests/test_kernel.py +1 -11
  274. package/python/tests/test_learning.py +3 -12
  275. package/python/tests/test_llm_admin.py +4 -19
  276. package/python/tests/test_llm_planner.py +1 -23
  277. package/python/tests/test_markdown_skills.py +3 -14
  278. package/python/tests/test_markdown_skills_openclaw.py +4 -9
  279. package/python/tests/test_mcp_connectors.py +3 -15
  280. package/python/tests/test_mcp_guardian_discovery.py +2 -6
  281. package/python/tests/test_mcp_no_memory.py +3 -3
  282. package/python/tests/test_mcp_policy_gate.py +0 -1
  283. package/python/tests/test_mcp_server.py +6 -9
  284. package/python/tests/test_memory_recall.py +1 -4
  285. package/python/tests/test_metrics.py +0 -8
  286. package/python/tests/test_multi_chain.py +1 -13
  287. package/python/tests/test_multimodal.py +5 -18
  288. package/python/tests/test_negotiation.py +3 -13
  289. package/python/tests/test_orchestration.py +1 -11
  290. package/python/tests/test_orchestration_decoupled.py +0 -6
  291. package/python/tests/test_p3_p5_hardening.py +3 -17
  292. package/python/tests/test_parallel_dispatch.py +4 -12
  293. package/python/tests/test_pipeline.py +17 -58
  294. package/python/tests/test_plan_visible.py +4 -4
  295. package/python/tests/test_planner.py +0 -7
  296. package/python/tests/test_policy_gate.py +2 -3
  297. package/python/tests/test_profile.py +67 -0
  298. package/python/tests/test_readme_claims.py +163 -0
  299. package/python/tests/test_reasoner.py +2 -17
  300. package/python/tests/test_release_check.py +2 -13
  301. package/python/tests/test_resume_checkpoint.py +7 -15
  302. package/python/tests/test_routing.py +2 -12
  303. package/python/tests/test_scheduler.py +7 -10
  304. package/python/tests/test_search.py +1 -11
  305. package/python/tests/test_secrets.py +4 -10
  306. package/python/tests/test_semantic.py +1 -10
  307. package/python/tests/test_sessions.py +3 -4
  308. package/python/tests/test_settings.py +2 -18
  309. package/python/tests/test_shadow_learning.py +3 -43
  310. package/python/tests/test_shared_write.py +0 -7
  311. package/python/tests/test_skill_taint_gate.py +1 -10
  312. package/python/tests/test_skills.py +2 -18
  313. package/python/tests/test_skills_fetch.py +282 -0
  314. package/python/tests/test_stats_and_events.py +2 -5
  315. package/python/tests/test_streaming.py +2 -7
  316. package/python/tests/test_supervisor_pipeline.py +4 -5
  317. package/python/tests/test_supervisor_triage.py +4 -10
  318. package/python/tests/test_taint.py +2 -7
  319. package/python/tests/test_task_stream.py +1 -12
  320. package/python/tests/test_thinking_sse.py +0 -1
  321. package/python/tests/test_tool_center.py +1 -17
  322. package/python/tests/test_tool_parallel.py +2 -2
  323. package/python/tests/test_tool_params.py +1 -5
  324. package/python/tests/test_tool_use.py +0 -13
  325. package/python/tests/test_toplevel_concurrency.py +3 -4
  326. package/python/tests/test_tracing.py +7 -41
  327. package/python/tests/test_truth_crystallizer.py +4 -6
  328. package/python/tests/test_truth_flow.py +2 -7
  329. package/python/tests/test_update_check.py +95 -0
  330. package/python/tests/test_verify_evidence.py +8 -21
  331. package/python/tests/test_version_sync.py +35 -6
  332. package/python/tests/test_web_trigger.py +0 -17
  333. package/python/tools/check_faiss_type.py +0 -1
  334. package/python/tools/check_log.ps1 +0 -8
  335. package/python/tools/mypy_baseline.txt +12 -4
  336. package/python/tools/mypy_ratchet.py +52 -6
  337. package/python/tools/pack.py +0 -5
  338. package/python/tools/release_check.py +7 -7
  339. package/python/tools/restart_foxclaw.ps1 +5 -8
  340. package/python/tools/shared_write_audit.py +1 -15
  341. package/python/tools/size_check.py +1 -1
  342. package/python/tools/strip_comments.py +228 -0
  343. package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
  344. package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
@@ -0,0 +1,282 @@
1
+ """`foxclaw skills install` 的来源解析与落地。
2
+
3
+ 全部离线:远端用 monkeypatch 替掉 `_http_get`,不打真网 —— 否则 CI 会因 ClawHub
4
+ 抖动而红,这类测试一旦不稳就会被跳过,等于没测。
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import io
9
+ import json
10
+ import tarfile
11
+ import zipfile
12
+
13
+ import pytest
14
+
15
+ from foxclaw.skills import fetch as fetch_mod
16
+ from foxclaw.skills.archive import PackageError, has_code, read_package
17
+
18
+ MD = "---\nname: Demo Skill\ndescription: 演示用技能\nversion: 1.2.3\n---\n\n正文指示"
19
+
20
+
21
+ def _zip(files: dict) -> bytes:
22
+ buf = io.BytesIO()
23
+ with zipfile.ZipFile(buf, "w") as z:
24
+ for name, body in files.items():
25
+ z.writestr(name, body)
26
+ return buf.getvalue()
27
+
28
+
29
+ def _tgz(files: dict) -> bytes:
30
+ buf = io.BytesIO()
31
+ with tarfile.open(fileobj=buf, mode="w:gz") as t:
32
+ for name, text in files.items():
33
+ raw = text.encode("utf-8")
34
+ info = tarfile.TarInfo(name)
35
+ info.size = len(raw)
36
+ t.addfile(info, io.BytesIO(raw))
37
+ return buf.getvalue()
38
+
39
+
40
+ def _mock_http(monkeypatch, routes: dict) -> None:
41
+ def fake(url: str, timeout: float = 25.0, max_bytes: int = 10 * 1024 * 1024):
42
+ for key, val in routes.items():
43
+ if key in url:
44
+ return val
45
+ raise PackageError(f"测试未配置这条路由: {url}")
46
+
47
+ monkeypatch.setattr(fetch_mod, "_http_get", fake)
48
+
49
+
50
+ def test_read_zip_root_skill_md():
51
+ pkg = read_package(_zip({"SKILL.md": MD, "_meta.json": '{"slug": "demo"}'}))
52
+ assert pkg["skill_md"] == MD
53
+ assert pkg["meta"]["slug"] == "demo"
54
+
55
+
56
+ def test_read_targz_picks_shallowest():
57
+ pkg = read_package(_tgz({
58
+ "repo-abc/SKILL.md": MD,
59
+ "repo-abc/docs/SKILL.md": "更深的同名副本应被忽略",
60
+ }))
61
+ assert pkg["skill_md"] == MD
62
+
63
+
64
+ def test_read_package_rejects_traversal():
65
+ with pytest.raises(PackageError):
66
+ read_package(_zip({"../evil/SKILL.md": MD}))
67
+
68
+
69
+ def test_read_package_without_skill_md():
70
+ with pytest.raises(PackageError, match="未找到 SKILL.md"):
71
+ read_package(_zip({"README.md": "nothing here"}))
72
+
73
+
74
+ def test_subpath_narrows_search():
75
+ with pytest.raises(PackageError):
76
+ read_package(_tgz({"repo/a/SKILL.md": MD}), subpath="other")
77
+
78
+
79
+ def test_has_code_detects_scripts():
80
+ assert has_code(["SKILL.md", "scripts/run.py"])
81
+ assert has_code(["SKILL.md", "tool.sh"])
82
+ assert not has_code(["SKILL.md", "skill-card.md"])
83
+
84
+
85
+ def test_ref_routes_scoped_name_to_clawhub(monkeypatch):
86
+ seen: dict = {}
87
+
88
+ def fake(owner, slug, timeout):
89
+ seen["id"] = (owner, slug)
90
+ return fetch_mod.SkillPackage(skill_md=MD, source="clawhub", origin="x")
91
+
92
+ monkeypatch.setattr(fetch_mod, "_from_clawhub", fake)
93
+ assert fetch_mod.fetch("@snowfox/demo").origin == "x"
94
+ assert seen["id"] == ("snowfox", "demo")
95
+ fetch_mod.fetch("snowfox/demo")
96
+ assert seen["id"] == ("snowfox", "demo")
97
+
98
+
99
+ def test_unknown_ref_rejected():
100
+ with pytest.raises(PackageError, match="无法识别"):
101
+ fetch_mod.fetch("no-such-thing-xyz")
102
+
103
+
104
+ def test_local_dir_reads_skill_md(tmp_path):
105
+ (tmp_path / "SKILL.md").write_text(MD, encoding="utf-8")
106
+ pkg = fetch_mod.fetch(str(tmp_path))
107
+ assert pkg.source == "local"
108
+ assert "演示用技能" in pkg.skill_md
109
+
110
+
111
+ def test_local_dir_without_skill_md(tmp_path):
112
+ with pytest.raises(PackageError):
113
+ fetch_mod.fetch(str(tmp_path))
114
+
115
+
116
+ def test_clawhub_blocks_malware(monkeypatch):
117
+ _mock_http(monkeypatch, {
118
+ "/api/v1/skills/": (json.dumps({
119
+ "moderation": {"isMalwareBlocked": True, "summary": "窃取密钥"},
120
+ "latestVersion": {"version": "1.0.0"},
121
+ }).encode(), "application/json"),
122
+ })
123
+ with pytest.raises(fetch_mod.MalwareBlocked):
124
+ fetch_mod.fetch("@bad/skill")
125
+
126
+
127
+ def test_clawhub_github_handoff(monkeypatch):
128
+ """download 返回 JSON 交接(GitHub 托底)时要改下 archiveUrl,而不是当 zip 解。"""
129
+ _mock_http(monkeypatch, {
130
+ "/api/v1/skills/": (json.dumps({
131
+ "moderation": {"isSuspicious": True},
132
+ "latestVersion": {"version": "2.0.0"},
133
+ }).encode(), "application/json"),
134
+ "/api/v1/download": (json.dumps({
135
+ "archiveUrl": "https://codeload.example/archive.tar.gz",
136
+ }).encode(), "application/json"),
137
+ "archive.tar.gz": (_tgz({"repo-main/SKILL.md": MD}), "application/gzip"),
138
+ })
139
+ pkg = fetch_mod.fetch("@snowfox/demo")
140
+ assert pkg.version == "2.0.0"
141
+ assert pkg.suspicious is True
142
+ assert "repo-main/SKILL.md" in pkg.files
143
+
144
+
145
+ def test_clawhub_not_found(monkeypatch):
146
+ def boom(url, timeout=25.0, max_bytes=10 * 1024 * 1024):
147
+ raise fetch_mod.HttpError(404, url, "Not found")
148
+
149
+ monkeypatch.setattr(fetch_mod, "_http_get", boom)
150
+ with pytest.raises(PackageError, match="找不到"):
151
+ fetch_mod.fetch("@ghost/missing")
152
+
153
+
154
+ def test_github_subpath(monkeypatch):
155
+ _mock_http(monkeypatch, {
156
+ "codeload.github.com": (_tgz({
157
+ "repo-main/skills/demo/SKILL.md": MD,
158
+ "repo-main/skills/other/SKILL.md": "x",
159
+ }), "application/gzip"),
160
+ })
161
+ pkg = fetch_mod.fetch("github:snowfox/repo/skills/demo@main")
162
+ assert pkg.source == "github"
163
+ assert pkg.skill_md == MD
164
+ assert pkg.origin == "github:snowfox/repo/skills/demo@main"
165
+
166
+
167
+ def _src(tmp_path):
168
+ src = tmp_path / "src"
169
+ src.mkdir()
170
+ (src / "SKILL.md").write_text(MD, encoding="utf-8")
171
+ return src
172
+
173
+
174
+ def test_cli_install_writes_file_when_service_down(tmp_path, monkeypatch):
175
+ from foxclaw import cli
176
+ from foxclaw.skills import install_cmd
177
+
178
+ data = tmp_path / "data"
179
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(data))
180
+ monkeypatch.setattr(install_cmd, "_api_alive", lambda base: False)
181
+
182
+ assert cli.main(["skills", "install", str(_src(tmp_path)), "--yes"]) == 0
183
+ saved = json.loads((data / "markdown_skills.json").read_text(encoding="utf-8"))
184
+ assert [s["id"] for s in saved["skills"]] == ["demo_skill"]
185
+ assert saved["skills"][0]["version"] == "1.2.3"
186
+
187
+
188
+ def test_cli_install_respects_explicit_id(tmp_path, monkeypatch):
189
+ from foxclaw import cli
190
+ from foxclaw.skills import install_cmd
191
+
192
+ data = tmp_path / "data"
193
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(data))
194
+ monkeypatch.setattr(install_cmd, "_api_alive", lambda base: False)
195
+
196
+ assert cli.main(["skills", "install", str(_src(tmp_path)), "--yes",
197
+ "--id", "my_demo"]) == 0
198
+ saved = json.loads((data / "markdown_skills.json").read_text(encoding="utf-8"))
199
+ assert saved["skills"][0]["id"] == "my_demo"
200
+
201
+
202
+ def test_cli_install_refuses_without_confirmation(tmp_path, monkeypatch):
203
+ from foxclaw import cli
204
+ from foxclaw.skills import install_cmd
205
+
206
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(tmp_path / "data"))
207
+ monkeypatch.setattr(install_cmd, "_api_alive", lambda base: False)
208
+ monkeypatch.setattr(install_cmd, "_confirm", lambda: False)
209
+
210
+ assert cli.main(["skills", "install", str(_src(tmp_path))]) == 1
211
+ assert not (tmp_path / "data" / "markdown_skills.json").exists()
212
+
213
+
214
+ def test_cli_list_reads_file_when_service_down(tmp_path, monkeypatch, capsys):
215
+ from foxclaw import cli
216
+ from foxclaw.skills import install_cmd
217
+
218
+ data = tmp_path / "data"
219
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(data))
220
+ monkeypatch.setattr(install_cmd, "_api_alive", lambda base: False)
221
+ cli.main(["skills", "install", str(_src(tmp_path)), "--yes"])
222
+ capsys.readouterr()
223
+
224
+ assert cli.main(["skills", "list", "--json"]) == 0
225
+ got = json.loads(capsys.readouterr().out)
226
+ assert got["source"] == "file"
227
+ assert got["skills"][0]["id"] == "demo_skill"
228
+
229
+
230
+ def test_cli_remove_deletes_skill(tmp_path, monkeypatch):
231
+ from foxclaw import cli
232
+ from foxclaw.skills import install_cmd
233
+
234
+ data = tmp_path / "data"
235
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(data))
236
+ monkeypatch.setattr(install_cmd, "_api_alive", lambda base: False)
237
+ cli.main(["skills", "install", str(_src(tmp_path)), "--yes"])
238
+
239
+ assert cli.main(["skills", "remove", "demo_skill"]) == 0
240
+ saved = json.loads((data / "markdown_skills.json").read_text(encoding="utf-8"))
241
+ assert saved["skills"] == []
242
+
243
+
244
+ def test_cli_remove_unknown_id_fails(tmp_path, monkeypatch):
245
+ from foxclaw import cli
246
+ from foxclaw.skills import install_cmd
247
+
248
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(tmp_path / "data"))
249
+ monkeypatch.setattr(install_cmd, "_api_alive", lambda base: False)
250
+ assert cli.main(["skills", "remove", "nope"]) == 1
251
+
252
+
253
+ def test_cli_install_uses_api_when_service_alive(tmp_path, monkeypatch):
254
+ """服务在跑时必须打 API:直接改 json 会被运行中注册表的下次 save 整份覆盖。"""
255
+ from foxclaw import cli
256
+ from foxclaw.skills import install_cmd
257
+
258
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(tmp_path / "data"))
259
+ monkeypatch.setattr(install_cmd, "_api_alive", lambda base: True)
260
+ calls: list = []
261
+
262
+ def fake_json(base, method, path, payload=None):
263
+ calls.append((method, path, payload))
264
+ return {"created": True}
265
+
266
+ monkeypatch.setattr(install_cmd, "_api_json", fake_json)
267
+
268
+ assert cli.main(["skills", "install", str(_src(tmp_path)), "--yes"]) == 0
269
+ method, path, payload = calls[0]
270
+ assert (method, path) == ("POST", "/api/ahs/md-skills")
271
+ assert payload["acknowledged"] is True
272
+ assert payload["id"] == "demo_skill"
273
+ assert "正文指示" in payload["content"]
274
+ assert not (tmp_path / "data" / "markdown_skills.json").exists()
275
+
276
+
277
+ def test_local_zip_package(tmp_path):
278
+ p = tmp_path / "demo.zip"
279
+ p.write_bytes(_zip({"SKILL.md": MD, "_meta.json": '{"slug": "pack"}'}))
280
+ pkg = fetch_mod.fetch(str(p))
281
+ assert pkg.source == "local"
282
+ assert pkg.meta["slug"] == "pack"
@@ -33,14 +33,13 @@ def _adapter_with(hmr_obj):
33
33
  return s
34
34
 
35
35
 
36
- # ── 1. 记忆统计 ────────────────────────────────────────────────────────────────
37
36
  def test_stats_counts_from_memory_fs_not_vector_store():
38
37
  """核心回归:记忆在 memory_fs;vector_store 没有 count/size/memories 也不能让 total 归零。"""
39
38
  mems = [_mem(["agent:supervisor"]), _mem(["agent:supervisor"]),
40
39
  _mem(["agent:finance", "shared"]), _mem(["agent:office"])]
41
40
  hmr = types.SimpleNamespace(
42
41
  memory_fs=types.SimpleNamespace(list_memories=lambda: mems),
43
- vector_store=types.SimpleNamespace(), # 故意:没有 count/size/memories(复现旧 bug 条件)
42
+ vector_store=types.SimpleNamespace(),
44
43
  memory_graph=None)
45
44
  out = HMRAdapter.stats(_adapter_with(hmr))
46
45
  assert out["total_memories"] == 4, "必须从 memory_fs 数出真实记忆数"
@@ -75,14 +74,13 @@ def test_stats_survives_broken_hmr():
75
74
  assert out["total_memories"] == 0 and out["relations"] == 0
76
75
 
77
76
 
78
- # ── 1b. 记忆标题可读性 ─────────────────────────────────────────────────────────
79
77
  def test_derive_title_from_content():
80
78
  d = HMRAdapter._derive_title
81
79
  assert d("对话:你好 → 你好呀!周五中午,正是一周里…") == "你好"
82
80
  assert d("用户偏好:我喜欢简洁的回答") == "我喜欢简洁的回答"
83
81
  assert d("把这份报告存入HMR → 好的,我现在") == "把这份报告存入HMR"
84
82
  assert d("") == ""
85
- assert len(d("很长" * 50)) <= 24 # 限长
83
+ assert len(d("很长" * 50)) <= 24
86
84
 
87
85
 
88
86
  def test_recent_replaces_timestamp_titles():
@@ -109,7 +107,6 @@ def test_recent_keeps_meaningful_title():
109
107
  assert s.recent(5)[0]["title"] == "季度财务复盘"
110
108
 
111
109
 
112
- # ── 2. 活动事件 ────────────────────────────────────────────────────────────────
113
110
  def test_chat_publishes_activity_event(monkeypatch):
114
111
  """核心回归:对话完成后必须往事件脊柱发 chat.completed,否则"活动"页永远空。"""
115
112
  monkeypatch.setenv("FOXCLAW_TOOL_USE", "1")
@@ -6,26 +6,22 @@ import pytest
6
6
 
7
7
 
8
8
  def test_streaming():
9
- # ISOLATION_GUARD:本用例依赖 import 时固化的模块级单例(网关/限流器/路由配置按 env 在 import 时定型),单进程全量跑会被前序用例污染 →
10
- # 假红。全量请用 tests/run_all.py(每文件独立子进程)。
11
9
  if "foxclaw.api" in sys.modules:
12
10
  pytest.skip("需独立进程运行(模块级单例已固化),请用 tests/run_all.py")
13
11
  os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
14
12
  os.environ.setdefault("FOXCLAW_EMBEDDING_MODEL", "foxclaw-nonexistent-fast")
15
- os.environ["FOXCLAW_STREAM_RATE"] = "1" # 令牌桶=1,测限流
13
+ os.environ["FOXCLAW_STREAM_RATE"] = "1"
16
14
  os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_stream_")
17
15
 
18
16
  print("=" * 60); print("LLM 流式输出验证"); print("=" * 60)
19
17
 
20
- # 1) router.stream(stub 模式):分块拼接 == 完整文本、多块
21
18
  from foxclaw.core.model_router import ModelRouter
22
- r = ModelRouter() # 无 key → stub
19
+ r = ModelRouter()
23
20
  chunks = list(r.stream("sys", [{"role": "user", "content": "你好世界这是一段用于测试流式分块的文本"}]))
24
21
  assert len(chunks) > 1, f"应多块: {len(chunks)}"
25
22
  assert "".join(chunks), "拼接应非空"
26
23
  print(f"[1] router.stream stub 分块(chunk={len(chunks)}) OK")
27
24
 
28
- # 2) /api/chat/stream SSE:多个 delta + done 收尾
29
25
  from fastapi.testclient import TestClient
30
26
  from foxclaw.api import app
31
27
  with TestClient(app) as c:
@@ -46,7 +42,6 @@ def test_streaming():
46
42
  assert "".join(deltas), "拼接应非空"
47
43
  print(f"[2] /api/chat/stream SSE(delta={len(deltas)}, done) OK")
48
44
 
49
- # 3) 限流:桶=1,上面 [2] 已消耗 → 再请求被限流
50
45
  with c.stream("POST", "/api/chat/stream",
51
46
  json={"task": "再来一次", "agent_id": "supervisor"}) as resp:
52
47
  txt = "".join(resp.iter_text())
@@ -26,7 +26,7 @@ from pathlib import Path
26
26
  sys.path.insert(0, str(Path(__file__).parent.parent))
27
27
 
28
28
  import foxclaw.core.web_search as ws
29
- ws.needs_search = lambda q: False # 自答/兜底路径不触发联网,保持离线
29
+ ws.needs_search = lambda q: False
30
30
 
31
31
  from foxclaw import Foxclaw
32
32
  from foxclaw.agents.base import CognitiveAgent, HarnessBlockedError
@@ -59,7 +59,7 @@ def main():
59
59
  check(_names() == EXPECTED_STAGES, f"主管 stage 序列={EXPECTED_STAGES}(实为 {_names()})")
60
60
 
61
61
  os_ = Foxclaw(data_dir=tempfile.mkdtemp(prefix="foxclaw_svpipe_"))
62
- os_.router.provider = "stub" # 共享 router,五 Agent 同源 → 全离线确定性
62
+ os_.router.provider = "stub"
63
63
  os_.router._client = None
64
64
  sup = os_.supervisor
65
65
 
@@ -74,7 +74,7 @@ def main():
74
74
  "返回体由 base 模板统一组装(agent/session_id/thought_summary)")
75
75
 
76
76
  print("\n【3】单 Agent 分发:返回体顶层键与旧实现逐字一致")
77
- r = asyncio.run(sup.run_task("帮我做个预算分析")) # "预算" 命中 finance
77
+ r = asyncio.run(sup.run_task("帮我做个预算分析"))
78
78
  res = r["result"]
79
79
  check(res.get("dispatched_to") == "finance", f"关键词命中 → finance(实为 {res.get('dispatched_to')})")
80
80
  for k in ("schedule", "plan", "process_id"):
@@ -91,7 +91,7 @@ def main():
91
91
  print("\n【5】统一 execution.completed(文稿 Step 4)")
92
92
  spine = os_.event_spine
93
93
  before = len(spine.history("execution.completed"))
94
- asyncio.run(sup.run_task("同时查今天的日程、分析本月理财、总结这篇论文")) # 并行 3 子任务
94
+ asyncio.run(sup.run_task("同时查今天的日程、分析本月理财、总结这篇论文"))
95
95
  evts = spine.history("execution.completed")
96
96
  new_evts = evts[before:]
97
97
  check(len(new_evts) == 1,
@@ -112,7 +112,6 @@ def main():
112
112
  new_evts = spine.history("execution.completed")[before:]
113
113
  check(len(new_evts) == 1 and new_evts[0]["payload"].get("blocked") is True,
114
114
  "被拦任务的执行事件 blocked=True → 真值链不沉淀")
115
- # 还原 finance,别污染后续用例
116
115
  del os_.finance._execute
117
116
 
118
117
  print("\n【6】流式:run_task_stream 八 stage + 子 Agent token 传播 + 中断")
@@ -20,7 +20,7 @@ from pathlib import Path
20
20
  sys.path.insert(0, str(Path(__file__).parent.parent))
21
21
 
22
22
  import foxclaw.core.web_search as ws
23
- ws.needs_search = lambda q: False # 自答路径不触发联网,保持离线
23
+ ws.needs_search = lambda q: False
24
24
 
25
25
  from foxclaw import Foxclaw
26
26
 
@@ -31,8 +31,6 @@ def check(cond, msg):
31
31
  print(f" ✓ {msg}")
32
32
 
33
33
 
34
- # 分诊 prompt 的特征串。历史上 _llm_classify 用它问模型;现已无此路径,
35
- # 故断言它**从不出现**,即「不再为分诊多花一次 LLM 往返」。
36
34
  CLASSIFY_MARK = "只输出一个词"
37
35
 
38
36
 
@@ -47,19 +45,16 @@ def main():
47
45
  def fake(system, messages, max_tokens=2048, agent_id=""):
48
46
  content = str(messages[-1]["content"]) if messages else ""
49
47
  calls.append(content)
50
- if CLASSIFY_MARK in content: # 若真有人恢复分诊,这里会暴露
48
+ if CLASSIFY_MARK in content:
51
49
  return "finance"
52
50
  return "已处理"
53
51
 
54
- # 让路由认为模型可用(非 stub)
55
52
  os_.router.provider = "deepseek"
56
53
  os_.router._client = object()
57
54
  os_.router.complete = fake
58
55
  check(os_.router.is_stub is False, "router 处于'可用'状态(非 stub)")
59
56
 
60
57
  print("\n【1】关键词未命中 + 模型可用 → 主管直接自答(不再二次分诊)")
61
- # 这句不含任何领域关键词,也不像"花了钱"的报备(没有完成态动词),
62
- # 所以仍然走自答 —— 花费报备现在已算作财务命中,见【1b】。
63
58
  no_kw = "讲个笑话听听"
64
59
  check(os_.supervisor._has_keyword_hit(no_kw) is False, f"「{no_kw[:12]}…」确无关键词命中")
65
60
  r = asyncio.run(os_.supervisor.run_task(no_kw))
@@ -73,13 +68,12 @@ def main():
73
68
  for line in ("今天买了一件衣服花了105块", "后天花了100洗车"):
74
69
  check(os_.supervisor._has_keyword_hit(line), f"「{line[:12]}…」算作领域命中")
75
70
  check(os_.supervisor._route(line) == "finance", f"「{line[:12]}…」→ finance")
76
- # 讨论额度/比例不算报备,但仍属于财务问题 → 也交财务,而不是主管自答
77
71
  check(os_.supervisor._route("每月房租3000占收入10000这样合理吗") == "finance",
78
72
  "房租占比咨询 → finance")
79
73
 
80
74
  print("\n【2】关键词命中 → 直接路由,不问 LLM 分诊(省成本)")
81
75
  calls.clear()
82
- r2 = asyncio.run(os_.supervisor.run_task("帮我做个预算分析")) # "预算" 命中 finance
76
+ r2 = asyncio.run(os_.supervisor.run_task("帮我做个预算分析"))
83
77
  d2 = r2["result"].get("dispatched_to")
84
78
  check(d2 == "finance", f"命中'预算' → 直接 finance(实为 {d2})")
85
79
  check(not any(CLASSIFY_MARK in c for c in calls), "关键词命中时不发出分诊 prompt")
@@ -88,7 +82,7 @@ def main():
88
82
  os_.router.provider = "stub"
89
83
  os_.router._client = None
90
84
  calls.clear()
91
- r3 = asyncio.run(os_.supervisor.run_task("解释一下什么是航天器")) # 无关键词
85
+ r3 = asyncio.run(os_.supervisor.run_task("解释一下什么是航天器"))
92
86
  check(os_.router.is_stub is True, "router 回到 stub")
93
87
  d3 = r3["result"].get("dispatched_to")
94
88
  check(d3 == "supervisor", f"stub 下未命中 → 主管自身处理(实为 {d3})")
@@ -36,18 +36,15 @@ def _skill(side_effect="read", read_only=True, external=False, destructive=False
36
36
  )
37
37
 
38
38
 
39
- # ── 1. taint_level 分级 ───────────────────────────────────────────────
40
39
  def test_taint_level_grading():
41
40
  assert IG.taint_level("web_search") == IG.TAINT_EXTERNAL == 1
42
41
  assert IG.taint_level("knowledge") == IG.TAINT_PERSONAL == 2
43
42
  assert IG.taint_level("file") == IG.TAINT_PERSONAL == 2
44
43
  assert IG.taint_level("memory") == IG.TAINT_SENSITIVE == 3
45
- assert IG.taint_level("unknown_source") == IG.TAINT_EXTERNAL # 未知来源不低估
44
+ assert IG.taint_level("unknown_source") == IG.TAINT_EXTERNAL
46
45
 
47
46
 
48
- # ── 2. permission 污点门槛 ─────────────────────────────────────────────
49
47
  def test_permission_taint_default_off_behavior_unchanged():
50
- # 不传污点参数(默认关闭)→ 外发工具走原有 Stage 4 副作用确认
51
48
  s = _skill(side_effect="send", read_only=False)
52
49
  d = PERM.evaluate(s, {})
53
50
  assert d.decision == "ask" and d.reason_code == PERM.SIDE_EFFECT_CONFIRM
@@ -77,19 +74,17 @@ def test_permission_taint_not_trigger_on_readonly():
77
74
 
78
75
  def test_permission_taint_not_trigger_below_threshold():
79
76
  s = _skill(side_effect="send", read_only=False)
80
- # 污点只有 EXTERNAL(1) < 阈值 PERSONAL(2)→ 不因污点拦,走副作用确认
81
77
  d = PERM.evaluate(s, {}, taint_level=IG.TAINT_EXTERNAL,
82
78
  taint_threshold=IG.TAINT_PERSONAL)
83
79
  assert d.reason_code == PERM.SIDE_EFFECT_CONFIRM
84
80
 
85
81
 
86
- # ── 3. base 污点传播 ───────────────────────────────────────────────────
87
82
  def test_taint_helpers_on_fake_agent():
88
83
  a = types.SimpleNamespace(_think=lambda **k: None)
89
84
  assert _taint_get(a) == 0
90
85
  _taint_raise(a, IG.TAINT_EXTERNAL)
91
86
  _taint_raise(a, IG.TAINT_SENSITIVE)
92
- assert _taint_get(a) == IG.TAINT_SENSITIVE # 单调不减
87
+ assert _taint_get(a) == IG.TAINT_SENSITIVE
93
88
  _taint_reset(a)
94
89
  assert _taint_get(a) == 0
95
90
 
@@ -9,11 +9,8 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
9
9
  def test_task_stream():
10
10
  os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
11
11
  os.environ.setdefault("FOXCLAW_EMBEDDING_MODEL", "foxclaw-nonexistent-fast")
12
- os.environ["FOXCLAW_STREAM_RATE"] = "60" # 放开限流(限流由 test_streaming 覆盖),这里只验通路
12
+ os.environ["FOXCLAW_STREAM_RATE"] = "60"
13
13
  os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_taskstream_")
14
- # 强制 stub(离线/确定性/免费):关掉 .env,使进程环境变量成为唯一来源。
15
- # (旧写法是逐个把 key 置空以依赖 load_dotenv(override=False)——但那挡不住「先 pop 再
16
- # import」的路径,也挡不住 .env 里的 FOXCLAW_MCP_SERVER 等非 key 变量。见 api.py 说明。)
17
14
  os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
18
15
  for _k in ("DEEPSEEK_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY",
19
16
  "GOOGLE_API_KEY", "DASHSCOPE_API_KEY", "FOXCLAW_API_KEY"):
@@ -24,16 +21,11 @@ def test_task_stream():
24
21
  from fastapi.testclient import TestClient
25
22
  import foxclaw.api as _api
26
23
  from foxclaw.api import app
27
- # pytest 同进程下,test_streaming.py 可能已把模块级流式限流桶设为 rate=1(并耗尽),
28
- # 而本测试要放开限流跑两条流式通路 —— 复位桶容量/速率/余量(限流语义由 test_streaming 覆盖)。
29
24
  _api._STREAM_LIMITER.capacity = 60
30
25
  _api._STREAM_LIMITER.rate = 1.0
31
26
  _api._STREAM_LIMITER.tokens = 60.0
32
27
 
33
28
  with TestClient(app) as c:
34
- # 1) 正常流式:SSE 收到 stage 进度 + act delta + 恰一个 result,无 error
35
- # 拓扑 5 → 3:领域 Agent 才走 DEFAULT_STAGES(含 act),主管走路由管线
36
- # (recall→route→dispatch→aggregate),不会产出 act —— 故此处用 office。
37
29
  with c.stream("POST", "/api/task/stream",
38
30
  json={"task": "帮我分析一下本月开销", "agent_id": "office"}) as resp:
39
31
  assert resp.status_code == 200, resp.status_code
@@ -53,9 +45,6 @@ def test_task_stream():
53
45
  assert results[0].get("agent") == "office" and "result" in results[0], results[0]
54
46
  print(f"[1] /api/task/stream SSE(stage={len(stages)} delta={len(deltas)} result=1) OK")
55
47
 
56
- # 2) 【P3-3】主管不再回落领域 Agent:P1-1 pipeline 化后主管自身支持流式,
57
- # stage 事件是主管管线七阶段,result.agent=supervisor,路由结论照常(stub 无关键词
58
- # → 主管自身处理)。
59
48
  with c.stream("POST", "/api/task/stream",
60
49
  json={"task": "随便问问", "agent_id": "supervisor"}) as resp:
61
50
  assert resp.status_code == 200, resp.status_code
@@ -15,7 +15,6 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
15
15
 
16
16
 
17
17
  def test_chat_stream_emits_named_thinking_event():
18
- # ISOLATION_GUARD:依赖 import 期固化的单例(路由配置/限流器),须独立进程跑。
19
18
  if "foxclaw.api" in sys.modules:
20
19
  import pytest
21
20
  pytest.skip("需独立进程运行(模块级单例已固化),请用 tests/run_all.py")
@@ -19,67 +19,56 @@ def test_tool_center():
19
19
  with TestClient(app) as c:
20
20
  print("=" * 60); print("工具中心 · 技能台 + 审核门 + 治理 验证"); print("=" * 60)
21
21
 
22
- # 1. 安装 web_api 技能(https 只读 → allow)
23
22
  r = c.post("/api/ahs/skills", json=WEBAPI)
24
23
  assert r.status_code == 200, r.text
25
24
  assert r.json()["created"] is True
26
25
  print("[1] 安装 web_api 技能 OK")
27
26
 
28
- # 2. 列表里出现且 source=user
29
27
  r = c.get("/api/ahs/skills"); assert r.status_code == 200
30
28
  ids = {s["id"]: s for s in r.json()["skills"]}
31
29
  assert "wx_weather" in ids and ids["wx_weather"]["source"] == "user"
32
30
  print("[2] 列表含新技能且 source=user OK")
33
31
 
34
- # 3. 拒绝 local kind
35
32
  r = c.post("/api/ahs/skills", json={**WEBAPI, "id": "x_local", "kind": "local"})
36
33
  assert r.status_code == 400
37
34
  print("[3] 拒绝 local kind(400) OK")
38
35
 
39
- # 4. 拒绝非法 endpoint
40
36
  r = c.post("/api/ahs/skills", json={**WEBAPI, "id": "x_bad", "endpoint": "ftp://x"})
41
37
  assert r.status_code == 400
42
38
  print("[4] 拒非法 endpoint(400) OK")
43
39
 
44
- # 5. mcp 缺字段
45
40
  r = c.post("/api/ahs/skills", json={"id": "x_mcp", "name": "m", "kind": "mcp", "side_effect": "read"})
46
41
  assert r.status_code == 400
47
42
  print("[5] mcp 缺 server/tool(400) OK")
48
43
 
49
- # 6. 启停
50
44
  r = c.patch("/api/ahs/skills/wx_weather", json={"enabled": False})
51
45
  assert r.status_code == 200 and r.json()["skill"]["enabled"] is False
52
46
  r = c.patch("/api/ahs/skills/wx_weather", json={"enabled": True})
53
47
  assert r.json()["skill"]["enabled"] is True
54
48
  print("[6] 启停切换 OK")
55
49
 
56
- # 7. 删内置 → 409(取列表里第一个 builtin)
57
50
  r = c.get("/api/ahs/skills")
58
51
  builtin = next((s["id"] for s in r.json()["skills"] if s["source"] == "builtin"), None)
59
52
  if builtin:
60
53
  rd = c.delete(f"/api/ahs/skills/{builtin}")
61
54
  assert rd.status_code == 409, rd.text
62
55
  print("[7] 内置技能不可删(409) OK")
63
- # 7b. 用内置 id 安装 → 拒(不能覆盖内置)
64
56
  rc = c.post("/api/ahs/skills", json={**WEBAPI, "id": builtin})
65
57
  assert rc.status_code == 409, rc.text
66
58
  print("[7b] 用内置 id 安装被拒(409) OK")
67
59
  else:
68
60
  print("[7] 跳过(无内置技能)")
69
61
 
70
- # 8. 测试调用:read 技能直接跑(返回 SkillResult 结构)
71
62
  r = c.post("/api/ahs/skills/wx_weather/test", json={"args": {}})
72
63
  assert r.status_code == 200 and "success" in r.json()
73
64
  print("[8] 测试调用返回 SkillResult OK")
74
65
 
75
- # 9. 安装审核门:打本地地址的 web_api 安装期被 deny(403)
76
66
  r = c.post("/api/ahs/skills", json={"id": "ssrf_t", "name": "s", "description": "", "kind": "web_api",
77
67
  "triggers": ["x"], "side_effect": "read", "endpoint": "http://127.0.0.1:8000/api/status"})
78
68
  assert r.status_code == 403, r.text
79
69
  assert r.json()["detail"]["error"] == "install_denied"
80
70
  print("[9] 安装审核门 deny 本地地址(403) OK")
81
71
 
82
- # 9b. 有副作用技能未确认 → 409 needs_ack;acknowledged 后 → 200(且持久,供步12 round-trip)
83
72
  _send = {"id": "send_x", "name": "发信", "description": "", "kind": "web_api",
84
73
  "triggers": ["发"], "side_effect": "send", "endpoint": "https://example.com/send"}
85
74
  r = c.post("/api/ahs/skills", json=_send)
@@ -88,19 +77,16 @@ def test_tool_center():
88
77
  assert r.status_code == 200, r.text
89
78
  print("[9b] 副作用技能需确认(409→200) OK")
90
79
 
91
- # 10. 治理端点可用
92
80
  assert c.get("/api/ahs/breaker").status_code == 200
93
81
  assert c.get("/api/ahs/hooks/audit").status_code == 200
94
82
  print("[10] 治理端点(breaker/audit) OK")
95
83
 
96
- # 10b. 信任分级:内置 calculator(local)=high、内置 mcp=low、用户 web_api=mid
97
84
  sk = {s["id"]: s for s in c.get("/api/ahs/skills").json()["skills"]}
98
85
  assert sk["calculator"]["trust"] == "high", sk["calculator"]["trust"]
99
- assert sk["gmail_send"]["trust"] == "low", sk["gmail_send"]["trust"] # 内置但 mcp→低
86
+ assert sk["gmail_send"]["trust"] == "low", sk["gmail_send"]["trust"]
100
87
  assert sk["wx_weather"]["trust"] == "mid"
101
88
  print("[10b] 信任分级 high/mid/low OK")
102
89
 
103
- # 10c. deny 封杀:封杀后测试调用被拦 + blocked 标;解封恢复
104
90
  c.post("/api/ahs/policy/deny", json={"id": "wx_weather"})
105
91
  r = c.post("/api/ahs/skills/wx_weather/test", json={"args": {}, "confirmed": True}).json()
106
92
  assert r["status"] == "blocked" and "封杀" in r["message"], r
@@ -111,12 +97,10 @@ def test_tool_center():
111
97
  assert next(s for s in c.get("/api/ahs/skills").json()["skills"] if s["id"] == "wx_weather")["blocked"] is False
112
98
  print("[10c] deny 封杀/解封 kill-switch OK")
113
99
 
114
- # 11. 删除用户技能
115
100
  assert c.delete("/api/ahs/skills/wx_weather").status_code == 200
116
101
  r = c.get("/api/ahs/skills"); assert "wx_weather" not in {s["id"] for s in r.json()["skills"]}
117
102
  print("[11] 删除用户技能 OK")
118
103
 
119
- # 12. 持久化 round-trip:直建新 OS(同数据目录),已安装的 send_x 应仍在
120
104
  from foxclaw import Foxclaw
121
105
  os2 = Foxclaw(data_dir=_DATA)
122
106
  assert os2.skill_registry.get("send_x") is not None, "重启后用户技能丢失"