foxclaw 0.1.0 → 0.1.2

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 (495) hide show
  1. package/README.md +54 -29
  2. package/bin/foxclaw.cjs +51 -0
  3. package/index.js +11 -10
  4. package/package.json +46 -38
  5. package/python/.dockerignore +35 -0
  6. package/python/.env.example +165 -0
  7. package/python/.gitattributes +46 -0
  8. package/python/.github/workflows/ci.yml +122 -0
  9. package/python/.github/workflows/release.yml +183 -0
  10. package/python/Dockerfile +64 -0
  11. package/python/LICENSE +21 -0
  12. package/python/MANIFEST.in +14 -0
  13. package/python/README.md +192 -0
  14. package/python/README_CN.md +324 -0
  15. package/python/bench/bm25_audit.py +199 -0
  16. package/python/bench/bm25_regression.py +178 -0
  17. package/python/bench/bm25_stress.py +159 -0
  18. package/python/bench/cd_audit.py +238 -0
  19. package/python/bench/chaos.py +142 -0
  20. package/python/bench/concurrency.py +103 -0
  21. package/python/bench/e2e_real_models.py +95 -0
  22. package/python/bench/llm_retry_test.py +248 -0
  23. package/python/bench/load_test.py +196 -0
  24. package/python/bench/vector_ivf_upgrade.py +134 -0
  25. package/python/docs/CHANNELS.md +195 -0
  26. package/python/docs/GOVERNANCE.md +164 -0
  27. package/python/docs/RELEASE.md +109 -0
  28. package/python/docs/RUN.md +75 -0
  29. package/python/docs/RUN_CN.md +121 -0
  30. package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
  31. package/python/evals/__init__.py +0 -0
  32. package/python/evals/governance/README.md +61 -0
  33. package/python/evals/governance/__init__.py +0 -0
  34. package/python/evals/governance/cases.py +152 -0
  35. package/python/evals/governance/compare_embeddings.py +94 -0
  36. package/python/evals/governance/convergence.svg +11 -0
  37. package/python/evals/governance/report.json +371 -0
  38. package/python/evals/governance/run_evals.py +400 -0
  39. package/python/examples/governance_middleware_demo.py +77 -0
  40. package/python/examples/langgraph_governance_demo.py +108 -0
  41. package/python/examples/sample_itinerary.csv +6 -0
  42. package/python/examples/sample_knowledge.csv +9 -0
  43. package/python/examples/sample_transactions.csv +14 -0
  44. package/python/foxclaw/__init__.py +526 -0
  45. package/python/foxclaw/agents/__init__.py +12 -0
  46. package/python/foxclaw/agents/agents.py +35 -0
  47. package/python/foxclaw/agents/base.py +865 -0
  48. package/python/foxclaw/agents/impl/__init__.py +1 -0
  49. package/python/foxclaw/agents/impl/_helpers.py +78 -0
  50. package/python/foxclaw/agents/impl/finance.py +248 -0
  51. package/python/foxclaw/agents/impl/office.py +367 -0
  52. package/python/foxclaw/agents/impl/supervisor.py +659 -0
  53. package/python/foxclaw/agents/mixins/_context.py +151 -0
  54. package/python/foxclaw/agents/mixins/augment.py +149 -0
  55. package/python/foxclaw/agents/mixins/llm.py +66 -0
  56. package/python/foxclaw/agents/mixins/memory.py +35 -0
  57. package/python/foxclaw/agents/mixins/preference.py +81 -0
  58. package/python/foxclaw/agents/mixins/prompt.py +77 -0
  59. package/python/foxclaw/agents/mixins/skillbox.py +58 -0
  60. package/python/foxclaw/agents/mixins/tool_exec.py +443 -0
  61. package/python/foxclaw/api/__init__.py +283 -0
  62. package/python/foxclaw/api/app.py +123 -0
  63. package/python/foxclaw/api/middleware.py +317 -0
  64. package/python/foxclaw/api/routes/ahs.py +417 -0
  65. package/python/foxclaw/api/routes/attachments.py +117 -0
  66. package/python/foxclaw/api/routes/browser.py +39 -0
  67. package/python/foxclaw/api/routes/domain.py +314 -0
  68. package/python/foxclaw/api/routes/knowledge.py +163 -0
  69. package/python/foxclaw/api/routes/llm_admin.py +461 -0
  70. package/python/foxclaw/api/routes/memory.py +89 -0
  71. package/python/foxclaw/api/routes/orchestration.py +41 -0
  72. package/python/foxclaw/api/routes/recovery.py +43 -0
  73. package/python/foxclaw/api/routes/review.py +275 -0
  74. package/python/foxclaw/api/routes/search.py +37 -0
  75. package/python/foxclaw/api/routes/system.py +327 -0
  76. package/python/foxclaw/api/routes/task.py +281 -0
  77. package/python/foxclaw/api/routes/ws.py +68 -0
  78. package/python/foxclaw/api/spine.py +55 -0
  79. package/python/foxclaw/browser/__init__.py +13 -0
  80. package/python/foxclaw/browser/action/__init__.py +1 -0
  81. package/python/foxclaw/browser/action/action_executor.py +54 -0
  82. package/python/foxclaw/browser/action/action_types.py +45 -0
  83. package/python/foxclaw/browser/action/action_verifier.py +68 -0
  84. package/python/foxclaw/browser/action/url_policy.py +147 -0
  85. package/python/foxclaw/browser/adapter/__init__.py +1 -0
  86. package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
  87. package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
  88. package/python/foxclaw/browser/driver/__init__.py +1 -0
  89. package/python/foxclaw/browser/driver/base_driver.py +61 -0
  90. package/python/foxclaw/browser/driver/playwright_driver.py +166 -0
  91. package/python/foxclaw/browser/perception/__init__.py +1 -0
  92. package/python/foxclaw/browser/perception/dom_extractor.py +43 -0
  93. package/python/foxclaw/browser/perception/element_indexer.py +57 -0
  94. package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
  95. package/python/foxclaw/browser/perception/snapshot.py +29 -0
  96. package/python/foxclaw/browser/service.py +148 -0
  97. package/python/foxclaw/browser/session/__init__.py +1 -0
  98. package/python/foxclaw/browser/session/session_manager.py +57 -0
  99. package/python/foxclaw/capability/__init__.py +12 -0
  100. package/python/foxclaw/capability/embedding_service.py +67 -0
  101. package/python/foxclaw/channels/__init__.py +140 -0
  102. package/python/foxclaw/channels/discord.py +184 -0
  103. package/python/foxclaw/channels/dispatcher.py +204 -0
  104. package/python/foxclaw/channels/feishu.py +225 -0
  105. package/python/foxclaw/channels/openclaw_bridge.py +156 -0
  106. package/python/foxclaw/channels/signing.py +187 -0
  107. package/python/foxclaw/channels/telegram.py +121 -0
  108. package/python/foxclaw/channels/weixin.py +225 -0
  109. package/python/foxclaw/cli.py +154 -0
  110. package/python/foxclaw/cognitive/__init__.py +4 -0
  111. package/python/foxclaw/cognitive/reasoner.py +82 -0
  112. package/python/foxclaw/cognitive/state.py +147 -0
  113. package/python/foxclaw/core/__init__.py +9 -0
  114. package/python/foxclaw/core/audit_chain.py +133 -0
  115. package/python/foxclaw/core/audit_sink.py +100 -0
  116. package/python/foxclaw/core/compaction.py +283 -0
  117. package/python/foxclaw/core/config.py +65 -0
  118. package/python/foxclaw/core/confirm_nonce.py +157 -0
  119. package/python/foxclaw/core/dispatch.py +27 -0
  120. package/python/foxclaw/core/envfile.py +48 -0
  121. package/python/foxclaw/core/failure_policy.py +151 -0
  122. package/python/foxclaw/core/gateway.py +85 -0
  123. package/python/foxclaw/core/governance_audit.py +175 -0
  124. package/python/foxclaw/core/idempotency.py +289 -0
  125. package/python/foxclaw/core/injection_guard.py +221 -0
  126. package/python/foxclaw/core/memory_adapter.py +516 -0
  127. package/python/foxclaw/core/metrics.py +89 -0
  128. package/python/foxclaw/core/model_router.py +1097 -0
  129. package/python/foxclaw/core/paths.py +103 -0
  130. package/python/foxclaw/core/recovery.py +362 -0
  131. package/python/foxclaw/core/scheduler.py +144 -0
  132. package/python/foxclaw/core/secrets.py +134 -0
  133. package/python/foxclaw/core/sessions.py +157 -0
  134. package/python/foxclaw/core/settings.py +430 -0
  135. package/python/foxclaw/core/shared_write.py +198 -0
  136. package/python/foxclaw/core/steering_queue.py +153 -0
  137. package/python/foxclaw/core/system_bus.py +84 -0
  138. package/python/foxclaw/core/thought_chain.py +318 -0
  139. package/python/foxclaw/core/token_meter.py +235 -0
  140. package/python/foxclaw/core/tools.py +172 -0
  141. package/python/foxclaw/core/tracing.py +390 -0
  142. package/python/foxclaw/core/truth_pipeline.py +114 -0
  143. package/python/foxclaw/core/vec_recall_adapter.py +503 -0
  144. package/python/foxclaw/core/verify.py +181 -0
  145. package/python/foxclaw/core/web_search.py +242 -0
  146. package/python/foxclaw/finance/__init__.py +15 -0
  147. package/python/foxclaw/finance/evidence.py +153 -0
  148. package/python/foxclaw/finance/fx.py +100 -0
  149. package/python/foxclaw/finance/graph.py +188 -0
  150. package/python/foxclaw/finance/models.py +150 -0
  151. package/python/foxclaw/finance/nlparse.py +575 -0
  152. package/python/foxclaw/finance/policy.py +329 -0
  153. package/python/foxclaw/finance/reasoner.py +129 -0
  154. package/python/foxclaw/finance/reasoning.py +340 -0
  155. package/python/foxclaw/finance/reconcile.py +180 -0
  156. package/python/foxclaw/finance/runtime.py +527 -0
  157. package/python/foxclaw/governance/__init__.py +24 -0
  158. package/python/foxclaw/governance/middleware.py +184 -0
  159. package/python/foxclaw/harness/__init__.py +17 -0
  160. package/python/foxclaw/harness/_base.py +72 -0
  161. package/python/foxclaw/harness/_policy.py +359 -0
  162. package/python/foxclaw/harness/drift.py +89 -0
  163. package/python/foxclaw/harness/event_bus.py +31 -0
  164. package/python/foxclaw/harness/harness.py +446 -0
  165. package/python/foxclaw/harness/loop_detection.py +152 -0
  166. package/python/foxclaw/itinerary/__init__.py +10 -0
  167. package/python/foxclaw/itinerary/amap_client.py +117 -0
  168. package/python/foxclaw/itinerary/models.py +80 -0
  169. package/python/foxclaw/itinerary/reasoner.py +101 -0
  170. package/python/foxclaw/itinerary/reasoning.py +174 -0
  171. package/python/foxclaw/itinerary/runtime.py +148 -0
  172. package/python/foxclaw/kernel/__init__.py +13 -0
  173. package/python/foxclaw/kernel/capabilities.py +73 -0
  174. package/python/foxclaw/kernel/policy_gate.py +114 -0
  175. package/python/foxclaw/kernel/process_manager.py +205 -0
  176. package/python/foxclaw/kernel/scheduler.py +131 -0
  177. package/python/foxclaw/kernel/sentinel.py +453 -0
  178. package/python/foxclaw/kernel/sentinel_client.py +246 -0
  179. package/python/foxclaw/kernel/spine.py +97 -0
  180. package/python/foxclaw/knowledge/__init__.py +376 -0
  181. package/python/foxclaw/knowledge/truth_crystallizer.py +92 -0
  182. package/python/foxclaw/learning/__init__.py +12 -0
  183. package/python/foxclaw/learning/knowledge_graph.py +149 -0
  184. package/python/foxclaw/learning/models.py +93 -0
  185. package/python/foxclaw/learning/reasoner.py +70 -0
  186. package/python/foxclaw/learning/runtime.py +363 -0
  187. package/python/foxclaw/learning/spaced_repetition.py +96 -0
  188. package/python/foxclaw/mcp_server.py +189 -0
  189. package/python/foxclaw/orchestration/__init__.py +184 -0
  190. package/python/foxclaw/pipeline/__init__.py +23 -0
  191. package/python/foxclaw/pipeline/cognitive_pipeline.py +481 -0
  192. package/python/foxclaw/pipeline/supervisor_stages.py +230 -0
  193. package/python/foxclaw/planning/__init__.py +17 -0
  194. package/python/foxclaw/planning/llm_planner.py +232 -0
  195. package/python/foxclaw/planning/planner.py +99 -0
  196. package/python/foxclaw/search/__init__.py +24 -0
  197. package/python/foxclaw/search/agent_port.py +194 -0
  198. package/python/foxclaw/search/evidence.py +113 -0
  199. package/python/foxclaw/search/external_search.py +50 -0
  200. package/python/foxclaw/search/hkc_read_port.py +60 -0
  201. package/python/foxclaw/search/internal_search.py +73 -0
  202. package/python/foxclaw/search/models.py +129 -0
  203. package/python/foxclaw/search/planner.py +44 -0
  204. package/python/foxclaw/search/retriever.py +39 -0
  205. package/python/foxclaw/search/runtime.py +134 -0
  206. package/python/foxclaw/search/strategy.py +44 -0
  207. package/python/foxclaw/skills/__init__.py +613 -0
  208. package/python/foxclaw/skills/hooks_spec.py +73 -0
  209. package/python/foxclaw/skills/lifecycle.py +278 -0
  210. package/python/foxclaw/skills/markdown_skill.py +174 -0
  211. package/python/foxclaw/skills/markdown_store.py +39 -0
  212. package/python/foxclaw/skills/mcp_client.py +190 -0
  213. package/python/foxclaw/skills/mcp_store.py +43 -0
  214. package/python/foxclaw/skills/permission.py +180 -0
  215. package/python/foxclaw/skills/pipeline.py +386 -0
  216. package/python/foxclaw/skills/policy.py +80 -0
  217. package/python/foxclaw/skills/review.py +72 -0
  218. package/python/foxclaw/skills/store.py +57 -0
  219. package/python/frontend/assets/core_bg.jpg +0 -0
  220. package/python/frontend/deck/auth.js +32 -0
  221. package/python/frontend/deck/chat_model_picker.js +537 -0
  222. package/python/frontend/deck/domain.js +296 -0
  223. package/python/frontend/deck/main.js +1950 -0
  224. package/python/frontend/deck/markdown.js +48 -0
  225. package/python/frontend/deck/settings.js +324 -0
  226. package/python/frontend/deck/state.js +6 -0
  227. package/python/frontend/deck/util.js +6 -0
  228. package/python/frontend/deck/visuals.js +212 -0
  229. package/python/frontend/deck.css +855 -0
  230. package/python/frontend/deck.html +686 -0
  231. package/python/frontend/deck_v2.html +341 -0
  232. package/python/frontend/planet/index.html +266 -0
  233. package/python/frontend/planet/planet.css +579 -0
  234. package/python/frontend/planet/planet.js +1637 -0
  235. package/python/frontend/planet/vendor/three.core.min.js +6 -0
  236. package/python/frontend/planet/vendor/three.module.min.js +6 -0
  237. package/python/frontend/v3/assets/index-BD7VPbMV.js +33 -0
  238. package/python/frontend/v3/assets/index-CAvnYWGH.css +1 -0
  239. package/python/frontend/v3/index.html +15 -0
  240. package/python/frontend/widget_runtime.js +295 -0
  241. package/python/frontend/widgets.js +696 -0
  242. package/python/frontend_src/index.html +14 -0
  243. package/python/frontend_src/package-lock.json +1376 -0
  244. package/python/frontend_src/package.json +19 -0
  245. package/python/frontend_src/src/App.vue +62 -0
  246. package/python/frontend_src/src/api.js +89 -0
  247. package/python/frontend_src/src/components/ChatDock.vue +377 -0
  248. package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
  249. package/python/frontend_src/src/components/DocViewer.vue +171 -0
  250. package/python/frontend_src/src/components/Inspector.vue +241 -0
  251. package/python/frontend_src/src/components/LeftRail.vue +394 -0
  252. package/python/frontend_src/src/components/MenuPopover.vue +130 -0
  253. package/python/frontend_src/src/components/MessageStream.vue +401 -0
  254. package/python/frontend_src/src/components/ModelPicker.vue +355 -0
  255. package/python/frontend_src/src/components/QuickIngest.vue +118 -0
  256. package/python/frontend_src/src/components/Stage.vue +226 -0
  257. package/python/frontend_src/src/components/ToolMenu.vue +144 -0
  258. package/python/frontend_src/src/components/TopBar.vue +90 -0
  259. package/python/frontend_src/src/components/panels/ActivityPanel.vue +84 -0
  260. package/python/frontend_src/src/components/panels/AgentsPanel.vue +86 -0
  261. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +441 -0
  262. package/python/frontend_src/src/components/panels/MemoryPanel.vue +118 -0
  263. package/python/frontend_src/src/components/panels/PanelBox.vue +51 -0
  264. package/python/frontend_src/src/components/panels/ReviewPanel.vue +308 -0
  265. package/python/frontend_src/src/components/panels/SettingsPanel.vue +254 -0
  266. package/python/frontend_src/src/components/panels/TasksPanel.vue +74 -0
  267. package/python/frontend_src/src/components/panels/ToolsPanel.vue +640 -0
  268. package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
  269. package/python/frontend_src/src/lib/fmt.js +48 -0
  270. package/python/frontend_src/src/lib/md.js +204 -0
  271. package/python/frontend_src/src/main.js +7 -0
  272. package/python/frontend_src/src/stores/app.js +106 -0
  273. package/python/frontend_src/src/stores/chat.js +484 -0
  274. package/python/frontend_src/src/stores/model.js +104 -0
  275. package/python/frontend_src/src/stores/panels.js +330 -0
  276. package/python/frontend_src/src/styles/base.css +138 -0
  277. package/python/frontend_src/src/styles/panel.css +117 -0
  278. package/python/frontend_src/src/styles/tokens.css +78 -0
  279. package/python/frontend_src/vite.config.js +46 -0
  280. package/python/pyproject.toml +265 -0
  281. package/python/skills/README.md +123 -0
  282. package/python/skills/execution-contract/SKILL.md +30 -0
  283. package/python/skills/general-assistant/SKILL.md +34 -0
  284. package/python/skills/planner/SKILL.md +33 -0
  285. package/python/skills/schedule-itinerary/SKILL.md +35 -0
  286. package/python/skills/skill-composition/SKILL.md +31 -0
  287. package/python/skills/skill-guard/SKILL.md +67 -0
  288. package/python/skills/skill-memory/SKILL.md +50 -0
  289. package/python/skills/skill-router/SKILL.md +35 -0
  290. package/python/tests/conftest.py +76 -0
  291. package/python/tests/run_all.py +263 -0
  292. package/python/tests/test_agent_lifecycle.py +360 -0
  293. package/python/tests/test_ahs_p2.py +414 -0
  294. package/python/tests/test_anthropic_tools.py +206 -0
  295. package/python/tests/test_api.py +87 -0
  296. package/python/tests/test_api_auth.py +83 -0
  297. package/python/tests/test_api_host_guard.py +69 -0
  298. package/python/tests/test_attachments.py +167 -0
  299. package/python/tests/test_audit_chain.py +121 -0
  300. package/python/tests/test_audit_sink.py +136 -0
  301. package/python/tests/test_browser.py +153 -0
  302. package/python/tests/test_browser_request_guard.py +184 -0
  303. package/python/tests/test_capabilities.py +158 -0
  304. package/python/tests/test_channel_signing.py +192 -0
  305. package/python/tests/test_channel_trust.py +109 -0
  306. package/python/tests/test_channels.py +184 -0
  307. package/python/tests/test_cli.py +46 -0
  308. package/python/tests/test_compaction_toolpair.py +119 -0
  309. package/python/tests/test_config_layering.py +65 -0
  310. package/python/tests/test_confirm_nonce.py +197 -0
  311. package/python/tests/test_context_compaction_wiring.py +130 -0
  312. package/python/tests/test_dispatch.py +95 -0
  313. package/python/tests/test_domain_evals.py +403 -0
  314. package/python/tests/test_domain_import.py +78 -0
  315. package/python/tests/test_domain_reasoners.py +148 -0
  316. package/python/tests/test_e2e.py +178 -0
  317. package/python/tests/test_emb_cache.py +107 -0
  318. package/python/tests/test_embedding_singleton.py +89 -0
  319. package/python/tests/test_envfile.py +83 -0
  320. package/python/tests/test_evals_smoke.py +114 -0
  321. package/python/tests/test_event_spine.py +182 -0
  322. package/python/tests/test_evidence_semantic.py +101 -0
  323. package/python/tests/test_evidence_sse.py +97 -0
  324. package/python/tests/test_file_ingest.py +146 -0
  325. package/python/tests/test_finance.py +153 -0
  326. package/python/tests/test_finance_ledger.py +252 -0
  327. package/python/tests/test_finance_persist.py +178 -0
  328. package/python/tests/test_gateway.py +144 -0
  329. package/python/tests/test_governance.py +468 -0
  330. package/python/tests/test_governance_audit.py +359 -0
  331. package/python/tests/test_governance_middleware.py +143 -0
  332. package/python/tests/test_hkc.py +286 -0
  333. package/python/tests/test_hkc_index_build_manager.py +339 -0
  334. package/python/tests/test_hkc_manifest_reuse.py +317 -0
  335. package/python/tests/test_hkc_search_event_sync.py +533 -0
  336. package/python/tests/test_import_path_guard.py +161 -0
  337. package/python/tests/test_injection_guard.py +211 -0
  338. package/python/tests/test_integration.py +141 -0
  339. package/python/tests/test_itinerary.py +103 -0
  340. package/python/tests/test_kee_semantic.py +89 -0
  341. package/python/tests/test_kernel.py +222 -0
  342. package/python/tests/test_learning.py +110 -0
  343. package/python/tests/test_llm_admin.py +160 -0
  344. package/python/tests/test_llm_planner.py +366 -0
  345. package/python/tests/test_markdown_skills.py +73 -0
  346. package/python/tests/test_markdown_skills_openclaw.py +71 -0
  347. package/python/tests/test_mcp_connectors.py +109 -0
  348. package/python/tests/test_mcp_guardian_discovery.py +91 -0
  349. package/python/tests/test_mcp_no_memory.py +43 -0
  350. package/python/tests/test_mcp_policy_gate.py +77 -0
  351. package/python/tests/test_mcp_server.py +95 -0
  352. package/python/tests/test_memory_recall.py +95 -0
  353. package/python/tests/test_metrics.py +66 -0
  354. package/python/tests/test_multi_chain.py +102 -0
  355. package/python/tests/test_multimodal.py +135 -0
  356. package/python/tests/test_negotiation.py +146 -0
  357. package/python/tests/test_orchestration.py +119 -0
  358. package/python/tests/test_orchestration_decoupled.py +109 -0
  359. package/python/tests/test_p3_p5_hardening.py +338 -0
  360. package/python/tests/test_parallel_dispatch.py +215 -0
  361. package/python/tests/test_pipeline.py +533 -0
  362. package/python/tests/test_plan_visible.py +86 -0
  363. package/python/tests/test_planner.py +95 -0
  364. package/python/tests/test_policy_gate.py +109 -0
  365. package/python/tests/test_reasoner.py +185 -0
  366. package/python/tests/test_release_check.py +263 -0
  367. package/python/tests/test_resume_checkpoint.py +156 -0
  368. package/python/tests/test_routing.py +95 -0
  369. package/python/tests/test_scheduler.py +141 -0
  370. package/python/tests/test_search.py +204 -0
  371. package/python/tests/test_secrets.py +159 -0
  372. package/python/tests/test_semantic.py +78 -0
  373. package/python/tests/test_sessions.py +91 -0
  374. package/python/tests/test_settings.py +281 -0
  375. package/python/tests/test_shadow_learning.py +330 -0
  376. package/python/tests/test_shared_write.py +207 -0
  377. package/python/tests/test_skill_taint_gate.py +154 -0
  378. package/python/tests/test_skills.py +133 -0
  379. package/python/tests/test_stats_and_events.py +165 -0
  380. package/python/tests/test_streaming.py +60 -0
  381. package/python/tests/test_supervisor_pipeline.py +177 -0
  382. package/python/tests/test_supervisor_triage.py +108 -0
  383. package/python/tests/test_taint.py +117 -0
  384. package/python/tests/test_task_stream.py +85 -0
  385. package/python/tests/test_thinking_sse.py +67 -0
  386. package/python/tests/test_thinking_stream.py +76 -0
  387. package/python/tests/test_tool_center.py +129 -0
  388. package/python/tests/test_tool_parallel.py +113 -0
  389. package/python/tests/test_tool_params.py +103 -0
  390. package/python/tests/test_tool_use.py +242 -0
  391. package/python/tests/test_toplevel_concurrency.py +117 -0
  392. package/python/tests/test_tracing.py +290 -0
  393. package/python/tests/test_truth_crystallizer.py +127 -0
  394. package/python/tests/test_truth_flow.py +93 -0
  395. package/python/tests/test_url_policy.py +53 -0
  396. package/python/tests/test_verify_evidence.py +225 -0
  397. package/python/tests/test_version_sync.py +71 -0
  398. package/python/tests/test_web_trigger.py +99 -0
  399. package/python/tools/check_faiss_type.py +31 -0
  400. package/python/tools/check_log.ps1 +46 -0
  401. package/python/tools/ivf_status.py +72 -0
  402. package/python/tools/mypy_baseline.txt +5 -0
  403. package/python/tools/mypy_ratchet.py +97 -0
  404. package/python/tools/pack.py +178 -0
  405. package/python/tools/release_check.py +349 -0
  406. package/python/tools/restart_foxclaw.ps1 +74 -0
  407. package/python/tools/shared_write_audit.py +204 -0
  408. package/python/tools/size_baseline.txt +28 -0
  409. package/python/tools/size_check.py +102 -0
  410. package/python/tools/verify_env.bat +2 -0
  411. package/python/tools/verify_env.py +25 -0
  412. package/python/vendor/HMR_LICENSE +37 -0
  413. package/python/vendor/HMR_README.md +65 -0
  414. package/python/vendor/hkc/LICENSE +37 -0
  415. package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
  416. package/python/vendor/hkc/hkc_ace/ace.py +457 -0
  417. package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
  418. package/python/vendor/hkc/hkc_api/__init__.py +0 -0
  419. package/python/vendor/hkc/hkc_api/container.py +416 -0
  420. package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
  421. package/python/vendor/hkc/hkc_api/main.py +157 -0
  422. package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
  423. package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
  424. package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
  425. package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
  426. package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
  427. package/python/vendor/hkc/hkc_api/schemas.py +153 -0
  428. package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
  429. package/python/vendor/hkc/hkc_core/__init__.py +0 -0
  430. package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
  431. package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
  432. package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
  433. package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
  434. package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
  435. package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
  436. package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
  437. package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
  438. package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
  439. package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
  440. package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
  441. package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
  442. package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
  443. package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
  444. package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
  445. package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
  446. package/python/vendor/hkc/hkc_kde/kde.py +298 -0
  447. package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
  448. package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
  449. package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
  450. package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
  451. package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
  452. package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
  453. package/python/vendor/hkc/hkc_kde/models.py +74 -0
  454. package/python/vendor/hkc/hkc_kde/packager.py +329 -0
  455. package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
  456. package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
  457. package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
  458. package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
  459. package/python/vendor/hkc/hkc_kee/kee.py +541 -0
  460. package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
  461. package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
  462. package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
  463. package/python/vendor/hkc/hkc_sdk/base.py +151 -0
  464. package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
  465. package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
  466. package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
  467. package/python/vendor/hkc/hkc_sdk/models.py +171 -0
  468. package/python/vendor/hkc/hkc_search/__init__.py +0 -0
  469. package/python/vendor/hkc/hkc_search/bm25.py +325 -0
  470. package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
  471. package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
  472. package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
  473. package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
  474. package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
  475. package/python/vendor/hmr/__init__.py +0 -0
  476. package/python/vendor/hmr/core/__init__.py +0 -0
  477. package/python/vendor/hmr/core/hmr.py +684 -0
  478. package/python/vendor/hmr/core/models.py +310 -0
  479. package/python/vendor/hmr/engines/__init__.py +0 -0
  480. package/python/vendor/hmr/engines/evolution.py +710 -0
  481. package/python/vendor/hmr/engines/jit_compiler.py +472 -0
  482. package/python/vendor/hmr/engines/lifecycle.py +264 -0
  483. package/python/vendor/hmr/engines/policy.py +625 -0
  484. package/python/vendor/hmr/engines/recall.py +260 -0
  485. package/python/vendor/hmr/engines/runtime_state.py +158 -0
  486. package/python/vendor/hmr/engines/scheduler.py +323 -0
  487. package/python/vendor/hmr/engines/semantic.py +150 -0
  488. package/python/vendor/hmr/engines/temporal.py +261 -0
  489. package/python/vendor/hmr/engines/thought_chain.py +635 -0
  490. package/python/vendor/hmr/graph/__init__.py +0 -0
  491. package/python/vendor/hmr/graph/cwg.py +338 -0
  492. package/python/vendor/hmr/graph/memory_graph.py +530 -0
  493. package/python/vendor/hmr/storage/__init__.py +0 -0
  494. package/python/vendor/hmr/storage/memory_fs.py +300 -0
  495. package/python/vendor/hmr/storage/vector_store.py +520 -0
@@ -0,0 +1,159 @@
1
+ """本地密钥存储:keyring 优先、base64 回退、改名/删除不留孤儿。
2
+
3
+ 不碰真 OS 凭据库(测试机器上可能没有后端,也可能有用户真实数据),
4
+ 用 fake 替换 `secrets._keyring`。
5
+ """
6
+
7
+ import pytest
8
+
9
+ from foxclaw.core import secrets
10
+
11
+
12
+ class _FakeKeyring:
13
+ def __init__(self):
14
+ self.store = {}
15
+
16
+ def set_password(self, service, user, value):
17
+ self.store[(service, user)] = value
18
+
19
+ def get_password(self, service, user):
20
+ return self.store.get((service, user))
21
+
22
+ def delete_password(self, service, user):
23
+ if (service, user) not in self.store:
24
+ raise KeyError(user)
25
+ del self.store[(service, user)]
26
+
27
+
28
+ @pytest.fixture
29
+ def fake_kr(monkeypatch):
30
+ kr = _FakeKeyring()
31
+ monkeypatch.setattr(secrets, "_keyring", lambda: kr)
32
+ return kr
33
+
34
+
35
+ @pytest.fixture
36
+ def no_kr(monkeypatch):
37
+ monkeypatch.setattr(secrets, "_keyring", lambda: None)
38
+
39
+
40
+ # ── 基础行为 ──────────────────────────────────────────────────
41
+
42
+ def test_store_takes_over_when_keyring_available(fake_kr):
43
+ assert secrets.store("m1", "sk-abc") is True
44
+ assert fake_kr.store[(secrets.SERVICE, "m1")] == "sk-abc"
45
+
46
+
47
+ def test_store_reports_failure_without_keyring(no_kr):
48
+ assert secrets.store("m1", "sk-abc") is False
49
+
50
+
51
+ def test_load_prefers_keyring_over_legacy_ciphertext(fake_kr):
52
+ fake_kr.store[(secrets.SERVICE, "m1")] = "sk-new"
53
+ assert secrets.load("m1", secrets.obf("sk-old")) == "sk-new"
54
+
55
+
56
+ def test_load_falls_back_to_legacy_ciphertext(no_kr):
57
+ assert secrets.load("m1", secrets.obf("sk-old")) == "sk-old"
58
+
59
+
60
+ def test_load_returns_empty_when_nothing_stored(no_kr):
61
+ assert secrets.load("m1") == ""
62
+
63
+
64
+ def test_rename_moves_credential(fake_kr):
65
+ """改名不搬迁 → load(新名) 取不到,key 静默丢失。"""
66
+ secrets.store("old", "sk-abc")
67
+ assert secrets.rename("old", "new") is True
68
+ assert secrets.load("new") == "sk-abc"
69
+ assert fake_kr.get_password(secrets.SERVICE, "old") is None
70
+
71
+
72
+ def test_rename_without_source_is_noop(fake_kr):
73
+ assert secrets.rename("missing", "new") is False
74
+
75
+
76
+ def test_delete_is_idempotent(fake_kr):
77
+ secrets.store("m1", "sk-abc")
78
+ secrets.delete("m1")
79
+ secrets.delete("m1") # 再删一次不能抛
80
+ assert secrets.load("m1") == ""
81
+
82
+
83
+ def test_delete_without_keyring_does_not_raise(no_kr):
84
+ secrets.delete("m1")
85
+
86
+
87
+ # ── 写入侧:JSON 里放什么 ─────────────────────────────────────
88
+
89
+ def test_save_key_keeps_only_marker_when_keyring_available(fake_kr):
90
+ """有 OS 凭据库时,JSON 里不能再有任何可还原的密文。"""
91
+ from foxclaw.api.routes.llm_admin import _save_key
92
+
93
+ fields = _save_key("m1", " sk-abc ")
94
+ assert fields == {"api_key_kr": True, "api_key_obf": ""}
95
+ assert fake_kr.store[(secrets.SERVICE, "m1")] == "sk-abc"
96
+
97
+
98
+ def test_save_key_falls_back_to_ciphertext(no_kr):
99
+ from foxclaw.api.routes.llm_admin import _save_key
100
+
101
+ fields = _save_key("m1", "sk-abc")
102
+ assert fields["api_key_kr"] is False
103
+ assert secrets.deobf(fields["api_key_obf"]) == "sk-abc"
104
+
105
+
106
+ def test_save_key_empty_input_clears_both(no_kr):
107
+ from foxclaw.api.routes.llm_admin import _save_key
108
+
109
+ assert _save_key("m1", "") == {"api_key_kr": False, "api_key_obf": ""}
110
+
111
+
112
+ @pytest.mark.parametrize("record,expected", [
113
+ ({"api_key_kr": True, "api_key_obf": ""}, True), # 存 keyring
114
+ ({"api_key_kr": False, "api_key_obf": "c2stYWJj"}, True),
115
+ ({"api_key": "sk-legacy"}, True),
116
+ ({"api_key_kr": False, "api_key_obf": ""}, False),
117
+ ({}, False),
118
+ ])
119
+ def test_has_key(record, expected):
120
+ from foxclaw.api.routes.llm_admin import _has_key
121
+
122
+ assert _has_key(record) is expected
123
+
124
+
125
+ # ── 回退而非拒绝保存 ─────────────────────────────────────────
126
+
127
+ def test_obf_is_reversible(no_kr):
128
+ """base64 只是混淆,不要在任何地方当加密用 —— 这条测试的作用是钉住这一点。"""
129
+ import base64
130
+
131
+ assert secrets.deobf(secrets.obf("sk-secret")) == "sk-secret"
132
+ assert base64.b64decode(secrets.obf("sk-secret")).decode() == "sk-secret"
133
+
134
+
135
+ def test_backend_probe_is_cached(monkeypatch):
136
+ import sys
137
+ import types
138
+
139
+ calls = []
140
+
141
+ class _Fail:
142
+ pass
143
+
144
+ fake = types.ModuleType("keyring")
145
+ fake.get_keyring = lambda: calls.append(1) or _Fail()
146
+ backends = types.ModuleType("keyring.backends")
147
+ fail_mod = types.ModuleType("keyring.backends.fail")
148
+ fail_mod.Keyring = _Fail
149
+ monkeypatch.setitem(sys.modules, "keyring", fake)
150
+ monkeypatch.setitem(sys.modules, "keyring.backends", backends)
151
+ monkeypatch.setitem(sys.modules, "keyring.backends.fail", fail_mod)
152
+
153
+ secrets.reset_probe()
154
+ try:
155
+ assert secrets.available() is False # Fail 后端等同「没有可用凭据库」
156
+ secrets.available()
157
+ assert len(calls) == 1 # 探测结果被缓存,不反复 import
158
+ finally:
159
+ secrets.reset_probe()
@@ -0,0 +1,78 @@
1
+ """真语义检索 + KEE 语义化验证。用 HMR 已缓存的 all-MiniLM-L6-v2 证明机制(离线可用);
2
+ 模型未缓存则优雅跳过(退出0,不 FAIL)——真机上装了多语言模型即走真语义,否则 auto/keyword 降级。"""
3
+ import os, sys
4
+ from pathlib import Path
5
+ sys.path.insert(0, str(Path(__file__).parent.parent))
6
+ sys.path.insert(0, str(Path(__file__).parent.parent / "vendor" / "hkc"))
7
+
8
+ def test_semantic():
9
+ os.environ.setdefault("HF_HUB_OFFLINE", "1")
10
+ os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
11
+
12
+ MODEL = "all-MiniLM-L6-v2" # HMR 在用,进程缓存里通常有;保证离线可测
13
+ from hkc_search.embedding_backends import get_st_model
14
+
15
+ print("=" * 60); print("真语义检索 + KEE 语义化验证"); print("=" * 60)
16
+
17
+ # [0] 否定词不再子串误命中(不依赖模型,总能测):badge⊅bad-word, know⊅no-word
18
+ from hkc_kee.kee import _SimpleEmbedder as _SE
19
+ assert _SE._has_neg("this is a badge") is False, "badge 误命中否定"
20
+ assert _SE._has_neg("I know this") is False, "know 误命中否定"
21
+ assert _SE._has_neg("this is not good") is True
22
+ assert _SE._has_neg("cannot proceed") is True
23
+ assert _SE._has_neg("这个方案不好") is True
24
+ print("[0] KEE 否定词词边界(badge/know 不误命中) OK")
25
+
26
+ # [0b] 语义方向阈值接线:默认 0.30(与既有硬编码一致,behavior-preserving)+ 可经构造参数配置
27
+ from hkc_kee.kee import KnowledgeEvolutionEngine
28
+ class _Bus:
29
+ def subscribe(self, *a, **k): pass
30
+ class _Store: pass
31
+ class _Gen: pass
32
+ eng = KnowledgeEvolutionEngine(_Store(), _Bus(), _Gen())
33
+ assert abs(eng.similarity_threshold - 0.30) < 1e-9, "默认阈值应 0.30(与既有硬编码一致)"
34
+ eng2 = KnowledgeEvolutionEngine(_Store(), _Bus(), _Gen(), similarity_threshold=0.90)
35
+ assert abs(eng2.similarity_threshold - 0.90) < 1e-9, "阈值应可经构造参数配置"
36
+ print("[0b] KEE 语义方向阈值接线(默认 0.30 / 可配置) OK")
37
+
38
+ m = get_st_model(MODEL)
39
+ if m is None:
40
+ print("[skip] all-MiniLM 未缓存,跳过真语义验证(生产靠 auto/keyword 降级兜底)")
41
+ # 两种运行方式都要正确退出(M7):
42
+ # · 被 pytest 收集 → pytest.skip(allow_module_level) 是标准做法
43
+ # · 被 run_all.py 当脚本直跑 → pytest.skip 抛 Skipped 会让脚本非零退出,
44
+ # 反而把「跳过」变成「失败」。故按 __name__ 区分。
45
+ if __name__ != "__main__":
46
+ import pytest
47
+ pytest.skip("all-MiniLM 未缓存 → 真语义验证跳过(auto/keyword 降级兜底)",
48
+ allow_module_level=True)
49
+ sys.exit(0)
50
+
51
+ # 1) 共享缓存单例:同名只加载一次
52
+ assert get_st_model(MODEL) is m, "同名应共享同一实例(单例)"
53
+ print("[1] get_st_model 同名单例 OK")
54
+
55
+ # 2) LocalSTBackend 真余弦:相近 > 无关
56
+ import numpy as np
57
+ from hkc_search.embedding_backends import LocalSTBackend
58
+ b = LocalSTBackend(model_name=MODEL)
59
+ va, vb, vc = b.encode(["I am very happy today", "Feeling great today", "The stock market crashed"])
60
+ sim_close = float(np.dot(va, vb)); sim_far = float(np.dot(va, vc))
61
+ assert sim_close > sim_far, f"相近应更高: {sim_close} vs {sim_far}"
62
+ print(f"[2] LocalST 真余弦:相近({sim_close:.2f}) > 无关({sim_far:.2f}) OK")
63
+
64
+ # 3) KEE _SimpleEmbedder 语义方向(指向缓存模型)
65
+ from hkc_kee.kee import _SimpleEmbedder
66
+ _SimpleEmbedder._MODEL_NAME = MODEL
67
+ emb = _SimpleEmbedder()
68
+ s_same = emb.score("this plan is good", "this plan is nice")
69
+ s_unrel = emb.score("this plan is good", "it will rain tomorrow")
70
+ s_contra = emb.score("this plan is good", "this plan is not good")
71
+ assert s_same > s_unrel, f"相近应 > 无关: {s_same} vs {s_unrel}"
72
+ assert s_contra < s_same, f"矛盾应 < 同向: {s_contra} vs {s_same}"
73
+ print(f"[3] KEE score:同向({s_same:.2f}) > 无关({s_unrel:.2f}),矛盾({s_contra:.2f})<同向 OK")
74
+
75
+ print("\n真语义检索 + KEE 语义化验证通过")
76
+
77
+ if __name__ == "__main__":
78
+ test_semantic()
@@ -0,0 +1,91 @@
1
+ """
2
+ 会话存储回归测试(全离线)。
3
+
4
+ 覆盖:建/列/读/追加/改名/删、标题自动提炼、按更新时间倒序、
5
+ 列表不含 messages(负载)、路径穿越防护、并发追加(对话跑在后台线程)。
6
+ """
7
+ import sys, tempfile, shutil, threading
8
+ from pathlib import Path
9
+ sys.path.insert(0, str(Path(__file__).parent.parent))
10
+
11
+ from foxclaw.core.sessions import SessionStore, derive_title
12
+ import pytest
13
+
14
+
15
+ @pytest.fixture()
16
+ def store():
17
+ d = tempfile.mkdtemp(prefix="sess_")
18
+ try:
19
+ yield SessionStore(Path(d) / "sessions")
20
+ finally:
21
+ shutil.rmtree(d, ignore_errors=True)
22
+
23
+
24
+ def test_create_list_get(store):
25
+ s = store.create()
26
+ assert s["id"] and s["title"] == "新会话" and s["message_count"] == 0
27
+ assert [x["id"] for x in store.list()] == [s["id"]]
28
+ assert store.get(s["id"])["messages"] == []
29
+
30
+
31
+ def test_append_and_title_derivation(store):
32
+ s = store.create()
33
+ assert store.append(s["id"], "user", "帮我分析本月支出情况。还有别的")
34
+ assert store.append(s["id"], "assistant", "好的,本月支出如下…")
35
+ d = store.get(s["id"])
36
+ assert len(d["messages"]) == 2
37
+ assert d["messages"][0]["role"] == "user"
38
+ # 标题从首条用户消息提炼(截到句读)
39
+ assert d["title"] == "帮我分析本月支出情况"
40
+
41
+
42
+ def test_append_rejects_bad_role_and_missing_session(store):
43
+ s = store.create()
44
+ assert store.append(s["id"], "system", "x") is False # 非法角色
45
+ assert store.append(s["id"], "user", "") is False # 空内容
46
+ assert store.append("deadbeef1234", "user", "x") is False # 会话不存在
47
+
48
+
49
+ def test_list_is_light_and_sorted(store):
50
+ a = store.create(); store.append(a["id"], "user", "第一个会话")
51
+ b = store.create(); store.append(b["id"], "user", "第二个会话")
52
+ lst = store.list()
53
+ assert lst[0]["id"] == b["id"], "应按 updated_at 倒序"
54
+ assert "messages" not in lst[0], "列表不得带 messages(负载)"
55
+ assert lst[0]["message_count"] == 1 and lst[0]["last"] == "第二个会话"
56
+
57
+
58
+ def test_rename_and_delete(store):
59
+ s = store.create()
60
+ assert store.rename(s["id"], "季度复盘")
61
+ assert store.get(s["id"])["title"] == "季度复盘"
62
+ assert store.delete(s["id"])
63
+ assert store.get(s["id"]) is None
64
+ assert store.delete(s["id"]) is False
65
+
66
+
67
+ def test_path_traversal_blocked(store):
68
+ """id 只允许安全字符,杜绝 ../ 路径穿越。"""
69
+ assert store.get("../../etc/passwd") is None
70
+ assert store.delete("../../x") is False
71
+ assert store.append("../../x", "user", "hi") is False
72
+
73
+
74
+ def test_concurrent_append_no_loss(store):
75
+ """对话跑在后台线程,追加必须线程安全、不丢消息。"""
76
+ s = store.create()
77
+ def w(i):
78
+ store.append(s["id"], "user", f"消息{i}")
79
+ ts = [threading.Thread(target=w, args=(i,)) for i in range(20)]
80
+ [t.start() for t in ts]; [t.join() for t in ts]
81
+ assert len(store.get(s["id"])["messages"]) == 20
82
+
83
+
84
+ def test_derive_title():
85
+ assert derive_title("你好呀!今天天气") == "你好呀"
86
+ assert derive_title("") == "新会话"
87
+ assert len(derive_title("长" * 100)) <= 24
88
+
89
+
90
+ if __name__ == "__main__":
91
+ pytest.main([__file__, "-v"])
@@ -0,0 +1,281 @@
1
+ """配置校验(core/settings.py)的行为测试。
2
+
3
+ 立这些档的目的是让"配置错时的提示"本身成为被测试的对象:它比崩溃更容易腐坏 ——
4
+ 提示错了没人会发现,只会让用户在错误的方向上改。
5
+ """
6
+ import json
7
+
8
+ import pytest
9
+
10
+ from foxclaw import cli
11
+ from foxclaw.core import settings
12
+
13
+
14
+ @pytest.fixture(autouse=True)
15
+ def _reset_warned(monkeypatch):
16
+ # _warned 是进程级的"只喊一次"账本,测试之间必须清,否则 get_safe 的日志断言会串
17
+ monkeypatch.setattr(settings, "_warned", set())
18
+
19
+
20
+ # ── 类型化读取 ───────────────────────────────────────────────────────────
21
+ def test_get_int_reads_value(monkeypatch):
22
+ monkeypatch.setenv("FOXCLAW_PORT", "9001")
23
+ assert settings.get_int("FOXCLAW_PORT") == 9001
24
+
25
+
26
+ def test_get_int_blank_means_default(monkeypatch):
27
+ # `.env` 里写 `FOXCLAW_PORT=` 曾经会 int("") 崩掉;空串按未设置处理
28
+ monkeypatch.setenv("FOXCLAW_PORT", " ")
29
+ assert settings.get_int("FOXCLAW_PORT") == 8000
30
+
31
+
32
+ def test_get_int_unset_means_default(monkeypatch):
33
+ monkeypatch.delenv("FOXCLAW_PORT", raising=False)
34
+ assert settings.get_int("FOXCLAW_PORT") == 8000
35
+
36
+
37
+ def test_get_int_bad_value_names_variable_and_caller(monkeypatch):
38
+ monkeypatch.setenv("FOXCLAW_PORT", "80oo")
39
+ with pytest.raises(settings.ConfigError) as ei:
40
+ settings.get_int("FOXCLAW_PORT")
41
+ msg = str(ei.value)
42
+ assert "FOXCLAW_PORT" in msg and "80oo" in msg
43
+ assert "期望" in msg and "8000" in msg
44
+ # 调用点必须是本测试文件,否则用户还是不知道该去哪改
45
+ assert "test_settings.py" in msg
46
+
47
+
48
+ def test_get_int_below_min(monkeypatch):
49
+ monkeypatch.setenv("FOXCLAW_GATEWAY_RATE", "-5")
50
+ with pytest.raises(settings.ConfigError):
51
+ settings.get_int("FOXCLAW_GATEWAY_RATE")
52
+
53
+
54
+ def test_fractional_timeout_allowed(monkeypatch):
55
+ # 工具超时可以小于 1 秒(tests/test_tool_parallel.py 就用 0.3 模拟慢工具)
56
+ monkeypatch.setenv("FOXCLAW_TOOL_TIMEOUT", "0.3")
57
+ assert settings.get_float("FOXCLAW_TOOL_TIMEOUT") == pytest.approx(0.3)
58
+
59
+
60
+ def test_get_float_accepts_int_text(monkeypatch):
61
+ monkeypatch.setenv("FOXCLAW_LLM_COOLDOWN", "30")
62
+ assert settings.get_float("FOXCLAW_LLM_COOLDOWN") == 30.0
63
+
64
+
65
+ def test_get_safe_degrades_and_warns_once(monkeypatch, caplog):
66
+ monkeypatch.setenv("FOXCLAW_TRACE_SLOW_MS", "慢")
67
+ with caplog.at_level("WARNING", logger="foxclaw.config"):
68
+ assert settings.get_safe("FOXCLAW_TRACE_SLOW_MS") == 1000.0
69
+ assert settings.get_safe("FOXCLAW_TRACE_SLOW_MS") == 1000.0 # 第二次不重复喊
70
+ assert sum("FOXCLAW_TRACE_SLOW_MS" in r.message for r in caplog.records) == 1
71
+
72
+
73
+ def test_get_safe_unknown_name_returns_default(monkeypatch):
74
+ monkeypatch.setenv("SOME_UNDECLARED_KNOB", "abc")
75
+ assert settings.get_safe("SOME_UNDECLARED_KNOB", "dflt") == "abc" # str 恒可解析
76
+ assert settings.get_safe("NOT_SET_AT_ALL_X", 7) == 7
77
+
78
+
79
+ # ── check():一次汇总所有问题 ─────────────────────────────────────────────
80
+ def _clean(monkeypatch, *keys):
81
+ """清掉一组变量,避免宿主机上真实存在的键干扰断言。"""
82
+ for k in keys:
83
+ monkeypatch.delenv(k, raising=False)
84
+
85
+
86
+ def test_check_flags_type_error_without_raising(monkeypatch):
87
+ monkeypatch.setenv("FOXCLAW_PORT", "80oo")
88
+ issues = settings.check({"FOXCLAW_PORT": "80oo", "DEEPSEEK_API_KEY": "x"})
89
+ errs = [i for i in issues if i.level == "error"]
90
+ assert len(errs) == 1 and errs[0].code == "type" and errs[0].name == "FOXCLAW_PORT"
91
+ # 提示不该与消息重复(Exception 里已写"期望")
92
+ assert errs[0].hint == ""
93
+
94
+
95
+ def test_check_collects_multiple_errors(monkeypatch):
96
+ env = {"FOXCLAW_PORT": "x", "FOXCLAW_GATEWAY_RATE": "y",
97
+ "DEEPSEEK_API_KEY": "k"}
98
+ names = {i.name for i in settings.check(env) if i.level == "error"}
99
+ assert {"FOXCLAW_PORT", "FOXCLAW_GATEWAY_RATE"} <= names
100
+
101
+
102
+ def test_check_choice_error(monkeypatch):
103
+ issues = settings.check({"LLM_PROVIDER": "deepsek", "DEEPSEEK_API_KEY": "k"})
104
+ codes = {(i.code, i.name) for i in issues if i.level == "error"}
105
+ assert ("choice", "LLM_PROVIDER") in codes
106
+
107
+
108
+ def test_check_clean_env_has_no_error():
109
+ assert [i for i in settings.check({"DEEPSEEK_API_KEY": "k"}) if i.level == "error"] == []
110
+
111
+
112
+ def test_check_secret_value_is_masked_in_message():
113
+ # 密钥写错时不能把原文打进日志/终端
114
+ issues = settings.check({"LLM_PROVIDER": "openai", "OPENAI_API_KEY": " sk-live-1234567890 "})
115
+ msg = " ".join(i.message for i in issues)
116
+ assert "sk-live-1234567890" not in msg
117
+
118
+
119
+ # ── 语义检查:选了 provider 却没 key ────────────────────────────────────
120
+ def test_provider_without_key_is_error():
121
+ issues = settings.check({"LLM_PROVIDER": "deepseek", "ANTHROPIC_API_KEY": ""})
122
+ hit = [i for i in issues if i.code == "provider_key"]
123
+ assert hit and hit[0].level == "error" and hit[0].name == "DEEPSEEK_API_KEY"
124
+
125
+
126
+ def test_provider_with_key_is_fine():
127
+ issues = settings.check({"LLM_PROVIDER": "deepseek", "DEEPSEEK_API_KEY": "k"})
128
+ assert not [i for i in issues if i.code == "provider_key"]
129
+
130
+
131
+ def test_legacy_provider_name_also_checked():
132
+ issues = settings.check({"FOXCLAW_PROVIDER": "openai"})
133
+ assert [i for i in issues if i.code == "provider_key"]
134
+
135
+
136
+ def test_no_key_at_all_is_warn_not_error():
137
+ issues = settings.check({})
138
+ hit = [i for i in issues if i.code == "no_llm"]
139
+ assert hit and hit[0].level == "warn"
140
+
141
+
142
+ # ── 未知键:拼错的键必须被点名 + 给出正确写法 ────────────────────────────
143
+ def test_typo_key_is_flagged_with_suggestion():
144
+ issues = settings.check({"DEEPSEEK_API_KEY": "k", "FOXCLAW_TOOL_TIMEOUTS": "60"})
145
+ hit = [i for i in issues if i.name == "FOXCLAW_TOOL_TIMEOUTS"]
146
+ assert hit and hit[0].code == "unknown_key"
147
+ assert "FOXCLAW_TOOL_TIMEOUT" in hit[0].hint
148
+
149
+
150
+ def test_known_key_is_not_flagged():
151
+ issues = settings.check({"FOXCLAW_TOOL_TIMEOUT": "60", "DEEPSEEK_API_KEY": "k"})
152
+ assert not [i for i in issues if i.name == "FOXCLAW_TOOL_TIMEOUT"]
153
+
154
+
155
+ def test_system_env_vars_are_not_flagged():
156
+ # 不是我们的命名空间就不该管(PATH/HOME 之类)
157
+ issues = settings.check({"PATH": "/usr/bin", "DEEPSEEK_API_KEY": "k"})
158
+ assert not [i for i in issues if i.name == "PATH"]
159
+
160
+
161
+ def test_known_keys_covers_real_usage():
162
+ known = settings.known_keys()
163
+ for name in ("FOXCLAW_PORT", "FOXCLAW_TOOL_TIMEOUT", "DEEPSEEK_API_KEY",
164
+ "FOXCLAW_CHANNEL_SIGNING_SECRET"):
165
+ assert name in known
166
+ assert "NOT_A_REAL_KEY_NAME" not in known
167
+
168
+
169
+ # ── 组合矛盾 ────────────────────────────────────────────────────────────
170
+ def test_signing_key_id_without_secret_warns():
171
+ issues = settings.check({"DEEPSEEK_API_KEY": "k",
172
+ "FOXCLAW_CHANNEL_SIGNING_KEY_ID": "k2"})
173
+ assert [i for i in issues if i.code == "signing_pair"]
174
+
175
+
176
+ def test_short_signing_secret_warns():
177
+ issues = settings.check({"DEEPSEEK_API_KEY": "k",
178
+ "FOXCLAW_CHANNEL_SIGNING_SECRET": "short"})
179
+ assert [i for i in issues if i.code == "weak_secret"]
180
+
181
+
182
+ def test_open_cors_without_api_key_warns():
183
+ issues = settings.check({"DEEPSEEK_API_KEY": "k", "FOXCLAW_CORS_ORIGINS": "*"})
184
+ assert [i for i in issues if i.code == "open_cors"]
185
+
186
+
187
+ def test_gateway_off_with_api_key_warns():
188
+ issues = settings.check({"DEEPSEEK_API_KEY": "k", "FOXCLAW_API_KEY": "s3cret",
189
+ "FOXCLAW_GATEWAY": "0"})
190
+ assert [i for i in issues if i.code == "gateway_off"]
191
+
192
+
193
+ # ── 输出 / 脱敏 ─────────────────────────────────────────────────────────
194
+ @pytest.mark.parametrize("value,expect", [("", "(空)"), ("abcd", "****"),
195
+ ("sk-1234567890", "sk******90")])
196
+ def test_mask(value, expect):
197
+ assert settings.mask(value) == expect
198
+
199
+
200
+ def test_is_secret_heuristics():
201
+ assert settings.is_secret("DEEPSEEK_API_KEY")
202
+ assert settings.is_secret("SOME_TOKEN_X")
203
+ assert settings.is_secret("AMAP_JS_SCODE")
204
+ assert not settings.is_secret("FOXCLAW_PORT")
205
+
206
+
207
+ def test_format_issues_counts_and_limit():
208
+ issues = [settings.Issue("error", "type", f"K{i}", "m") for i in range(5)]
209
+ out = settings.format_issues(issues, limit=2)
210
+ assert "5 个错误" in out and "另有 3 条" in out
211
+
212
+
213
+ def test_format_issues_clean():
214
+ assert "通过" in settings.format_issues([])
215
+
216
+
217
+ def test_fatal_issues_only_type_and_choice():
218
+ issues = [settings.Issue("error", "type", "A", "m"),
219
+ settings.Issue("error", "provider_key", "B", "m"),
220
+ settings.Issue("warn", "type", "C", "m")]
221
+ assert [i.name for i in settings.fatal_issues(issues)] == ["A"]
222
+
223
+
224
+ # ── CLI ─────────────────────────────────────────────────────────────────
225
+ def test_cli_config_check_exit_code_on_error(monkeypatch, capsys):
226
+ monkeypatch.setenv("FOXCLAW_PORT", "80oo")
227
+ assert cli.main(["config", "check"]) == 1
228
+ out = capsys.readouterr().out
229
+ assert "FOXCLAW_PORT" in out and "80oo" in out
230
+
231
+
232
+ def test_cli_config_check_clean_exit_zero(monkeypatch, capsys):
233
+ monkeypatch.setenv("DEEPSEEK_API_KEY", "k")
234
+ monkeypatch.delenv("FOXCLAW_PORT", raising=False)
235
+ assert cli.main(["config", "check"]) == 0
236
+ assert "配置检查" in capsys.readouterr().out
237
+
238
+
239
+ def test_cli_config_check_json(monkeypatch, capsys):
240
+ monkeypatch.setenv("FOXCLAW_PORT", "80oo")
241
+ cli.main(["config", "check", "--json"])
242
+ data = json.loads(capsys.readouterr().out)
243
+ assert isinstance(data, list) and data[0]["name"] == "FOXCLAW_PORT"
244
+
245
+
246
+ def test_cli_config_without_action_is_usage_error(capsys):
247
+ assert cli.main(["config"]) == 2
248
+ assert "config check" in capsys.readouterr().out
249
+
250
+
251
+ def test_cli_config_show_masks_secrets(monkeypatch, capsys):
252
+ monkeypatch.setenv("DEEPSEEK_API_KEY", "sk-abcdef123456")
253
+ cli.main(["config", "show"])
254
+ out = capsys.readouterr().out
255
+ assert "sk-abcdef123456" not in out and "DEEPSEEK_API_KEY" in out
256
+
257
+
258
+ def test_cli_config_show_reveal(monkeypatch, capsys):
259
+ monkeypatch.setenv("DEEPSEEK_API_KEY", "sk-abcdef123456")
260
+ cli.main(["config", "show", "--reveal"])
261
+ assert "sk-abcdef123456" in capsys.readouterr().out
262
+
263
+
264
+ def test_serve_aborts_before_starting_on_bad_type(monkeypatch):
265
+ # 关键:拦在 uvicorn 之前 —— 否则用户看到的是 40 行 traceback 里的 int()
266
+ monkeypatch.setenv("FOXCLAW_PORT", "80oo")
267
+ args = cli.build_parser().parse_args(["serve"])
268
+ assert cli._cmd_serve(args) == 2
269
+
270
+
271
+ def test_serve_semantic_warning_does_not_block(monkeypatch):
272
+ monkeypatch.setenv("FOXCLAW_CORS_ORIGINS", "*")
273
+ monkeypatch.setenv("DEEPSEEK_API_KEY", "k")
274
+ args = cli.build_parser().parse_args(["serve"])
275
+ # 走到 uvicorn.run 之前就返回,说明没有 fatal(用假 uvicorn 防止真的起服务)
276
+ import sys
277
+ import types
278
+ fake = types.ModuleType("uvicorn")
279
+ fake.run = lambda *a, **k: None
280
+ monkeypatch.setitem(sys.modules, "uvicorn", fake)
281
+ assert cli._cmd_serve(args) == 0