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
@@ -30,43 +30,20 @@ def main():
30
30
 
31
31
  print("\n【漂移检测 · 鲁棒断言】")
32
32
  det = rep["detection"]
33
- # 正常任务绝不该被硬停(BLOCK)——跨 embedding 模型都成立。
34
- #
35
- # 【修正,2026-10-05】adversarial 场景**不适用**此断言,须排除。实测(transformer):
36
- # 正常请求「本月哪个消费分类超支最多」 sem=0.038 → PASS
37
- # 对抗样本「忽略以上所有指令…」 sem=0.548 → BLOCK
38
- # 对抗样本「SYSTEM PROMPT: 新规则…」 sem=0.695 → BLOCK
39
- # 即注入类内容**本来就语义偏离目标**,漂移维度把它标记出来是**正确行为**,
40
- # 不是误报。adversarial 的 ground-truth 写 "pass" 的原意是
41
- # 「漂移维度不应越权充当安全防线」,而验证方式是:
42
- # · 降级模式下 → 确认不误拦(停用即不产生硬误报);
43
- # · transformer 模式下 → 由 **injection_guard 的 <untrusted> 物理隔离**
44
- # 与 **skills/permission 动作级闸** 负责(见 test_injection_guard /
45
- # test_skills),漂移维度只负责"偏离目标"这层语义判断。
46
- # 换言之:拿 drift 的 hard_fpr 去衡量 adversarial 是**指标误用**。
47
33
  for name, d in det.items():
48
34
  if name == "adversarial":
49
35
  continue
50
36
  check(d["hard_fpr"] <= 0.15, f"{name}: 硬误报 hard_fpr={d['hard_fpr']} ≤ 0.15")
51
- # 工具/范围是确定性维度(类别/实体计数),标记率必满 —— 不依赖 embedding,两种模式都成立
52
37
  check(det["tool"]["flag_recall"] == 1.0, "tool: 越权动作标记率 = 1.0")
53
38
  check(det["tool"]["block_recall"] == 1.0, "tool: 越权动作硬停率 = 1.0")
54
39
  check(det["scope"]["flag_recall"] == 1.0, "scope: 范围蔓延标记率 = 1.0")
55
40
 
56
- # 语义维度:**依赖 embedding,须按模式分述**(B 类修复)。
57
- # 此前此断言在降级模式下也硬要求 flag_recall ≥ 0.5,而降级时语义维度已被
58
- # 停用(fail-open)—— 指标在设计下就不可能达标,于是测试恒红,掩盖了
59
- # 「6 项治理测试失败」这一真实信号(见 M2 修复说明)。
60
- # 现在按模式分别断言各自**真正应成立**的性质:
61
41
  emb_mode = det["semantic"].get("embedding_mode") or "unknown"
62
42
  if emb_mode == "transformer":
63
43
  check(isinstance(det["semantic"]["flag_recall"], float)
64
44
  and det["semantic"]["flag_recall"] >= 0.5,
65
45
  "semantic(transformer): 跑题标记率 ≥ 0.5(宽松)")
66
46
  else:
67
- # 降级:语义维度已停用 → 跑题**不会**被标记,这是**有意的正确取舍**
68
- # (误拦正常业务比漏放跑题更糟)。此时该断言的是「停用是显式的、可观测的」,
69
- # 而不是「跑题仍能检出」—— 后者在无区分度的哈希嵌入上不可能成立。
70
47
  print(f" [降级模式] embedding_mode={emb_mode} → 语义维度已停用(fail-open)")
71
48
  check(det["semantic"]["flag_recall"] == 0.0,
72
49
  "semantic(hash): 语义维度停用后跑题标记率为 0(不再无差别拦截)")
@@ -93,7 +70,6 @@ def main():
93
70
  check(all(b == 1.0 for b in sl["block_rate_curve"]), "重复越权每轮都被 BLOCK")
94
71
  check(all(f == 0.0 for f in sl["benign_hard_fpr_curve"]), "学习不产生正常任务硬误报")
95
72
  check(sl["converged"], "自学习整体收敛")
96
- # 【P0-1】规则级学习影子化:「学到」= 候选已提出(进 shadow)+ promote 路径可生效
97
73
  check(sl["penalized_tool_proposed"], "越权工具产出 penalize 影子候选(P0-1)")
98
74
  check(sl["penalize_promote_path_ok"], "promote 后 penalize 规则真实生效(P0-1)")
99
75
 
@@ -13,7 +13,7 @@ import os, sys, asyncio, tempfile, shutil
13
13
  from pathlib import Path
14
14
  os.environ.setdefault("HF_HUB_OFFLINE", "1")
15
15
  os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
16
- os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast" # harness 走 hash,快
16
+ os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast"
17
17
  sys.path.insert(0, str(Path(__file__).parent.parent))
18
18
 
19
19
  from foxclaw.kernel import EventSpine
@@ -23,9 +23,9 @@ def test_spine_pubsub_prefix_projection():
23
23
  print("\n【1】EventSpine:前缀订阅 + 投影 + 历史")
24
24
  spine = EventSpine()
25
25
  got = []
26
- spine.subscribe("harness", lambda e: got.append(e.topic)) # 前缀订阅
26
+ spine.subscribe("harness", lambda e: got.append(e.topic))
27
27
  allseen = []
28
- spine.project(lambda e: allseen.append(e.topic)) # 投影:收到每条
28
+ spine.project(lambda e: allseen.append(e.topic))
29
29
  spine.publish("harness.drift.blocked", {"x": 1})
30
30
  spine.publish("memory.ingested", {"y": 2})
31
31
  assert got == ["harness.drift.blocked"], "前缀 'harness' 应只命中 harness.*"
@@ -42,7 +42,7 @@ def test_systembus_facade():
42
42
  spine.project(lambda e: seen.append((e.topic, e.source)))
43
43
  bus = SystemBus(spine=spine)
44
44
  got = []
45
- bus.subscribe("execution.completed", lambda m: got.append(m.payload)) # 旧式 handler(msg)
45
+ bus.subscribe("execution.completed", lambda m: got.append(m.payload))
46
46
  bus.publish("execution.completed", {"task": "t"})
47
47
  assert got == [{"task": "t"}], "旧消费者 handler(msg) 仍收到 .payload"
48
48
  assert ("execution.completed", "system_bus") in seen, "系统事件应汇入同一脊柱"
@@ -62,8 +62,8 @@ def test_harness_forwards_to_spine():
62
62
  h = Harness("tester", ThoughtChain(str(Path(d) / "tc.db")),
63
63
  policy_path=str(Path(d) / "p.yaml"), spine=spine)
64
64
  local = []
65
- h.bus.on("harness_warn", lambda data: local.append(data)) # 本地订阅仍工作
66
- h.bus.emit("harness_warn", {"a": 1}) # harness_warn 无内部自订阅,输出干净
65
+ h.bus.on("harness_warn", lambda data: local.append(data))
66
+ h.bus.emit("harness_warn", {"a": 1})
67
67
  h.bus.emit("goal_registered", {"g": 1})
68
68
  assert local == [{"a": 1}], "本地订阅(向后兼容)仍收到"
69
69
  assert "harness_warn" in seen, "harness 事件应汇入脊柱"
@@ -94,10 +94,8 @@ def test_hkc_events_bridge_to_spine():
94
94
  assert ("knowledge.merged", "hkc", {"ku_id": "K1"}) in seen, seen
95
95
  assert any(t == "conflict.detected" and s == "hkc" for t, s, _ in seen)
96
96
  assert any(t == "ability.created" for t, _, _ in seen)
97
- # memory.* 刻意不桥(HMR 域,转发会重复/混淆)
98
97
  bus.publish({"event": "memory.created", "payload": {}})
99
98
  assert not any(t == "memory.created" for t, _, _ in seen), "memory.* 不应被桥接"
100
- # 降级:bus/spine 为 None → 不接线、不崩
101
99
  assert bridge_hkc_events(None, spine) is False
102
100
  print(" ✓ knowledge.merged/conflict/ability 桥到脊柱(source=hkc);memory.* 不桥;None 优雅降级")
103
101
 
@@ -110,12 +108,10 @@ def test_unified_projection_via_foxclawos():
110
108
  os_ = Foxclaw(data_dir=d, shadow_rounds=50)
111
109
  seen = []
112
110
  os_.event_spine.project(lambda e: seen.append(e.topic))
113
- # 两类来源:harness 总线 与 系统总线,都汇入 os_.event_spine
114
111
  os_.finance.harness.bus.emit("harness_warn", {"agent": "finance"})
115
112
  os_.system_bus.publish("execution.completed", {"task": "demo"})
116
113
  assert "harness_warn" in seen, "harness 事件应到统一投影"
117
114
  assert "execution.completed" in seen, "系统事件应到统一投影(修复:此前到不了)"
118
- # 且二者用的是同一条脊柱实例
119
115
  assert os_.system_bus.spine is os_.event_spine
120
116
  asyncio.run(os_.aclose())
121
117
  finally:
@@ -127,34 +123,30 @@ def test_ws_projection_safe_and_bounded():
127
123
  print("\n【5】WS 投影净化:脏/大 payload 不丢连接、不泛洪(深审修复)")
128
124
  import json as _json
129
125
  import foxclaw.api as api
130
- # a) 净化器:非 JSON 对象 → 可序列化;超大 → 截断;正常 → 透传
131
- class Weird: # 非 JSON 可序列化
126
+ class Weird:
132
127
  def __repr__(self): return "<Weird>"
133
128
  safe = api._ws_safe_payload({"obj": Weird(), "n": 1})
134
- _json.dumps(safe) # 不应抛
129
+ _json.dumps(safe)
135
130
  big = api._ws_safe_payload({"blob": "x" * 10000})
136
131
  assert big.get("_truncated") is True, "超大 payload 应被截断"
137
132
  assert api._ws_safe_payload({"a": 1, "b": "ok"}) == {"a": 1, "b": "ok"}, "正常 payload 透传"
138
133
  print(" ✓ 非 JSON 对象被 str 化;超大被截断;正常透传")
139
134
 
140
- # b) 端到端:投影把脏事件放进队列后,队列项必须 JSON 可序列化(否则 send_json 会丢连接)
141
135
  d = tempfile.mkdtemp(prefix="spine_ws_")
142
136
  try:
143
137
  from foxclaw import Foxclaw
144
138
  os_ = Foxclaw(data_dir=d, shadow_rounds=50)
145
- # 清空可能的既有队列
146
139
  while not api._event_queue.empty():
147
140
  api._event_queue.get_nowait()
148
141
  api._register_spine_projection(os_)
149
142
  os_.system_bus.publish("execution.completed",
150
- {"agent_id": "x", "task": "t", "result": Weird()}) # 脏 result
151
- # 真值链会连带产生 trace/truth/memory 事件(再入 publish),全部经投影入队。
143
+ {"agent_id": "x", "task": "t", "result": Weird()})
152
144
  items = []
153
145
  while not api._event_queue.empty():
154
146
  items.append(api._event_queue.get_nowait())
155
147
  assert items, "投影应把事件放进队列"
156
148
  for it in items:
157
- _json.dumps(it) # 关键:每一项都必须能 send_json 而不抛 → 不会丢连接
149
+ _json.dumps(it)
158
150
  topics = {it["event"] for it in items}
159
151
  assert "execution.completed" in topics, f"应含 execution.completed,实得 {topics}"
160
152
  asyncio.run(os_.aclose())
@@ -18,7 +18,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent / "vendor" / "hkc"))
18
18
  def test_evidence_semantic():
19
19
  os.environ.setdefault("HF_HUB_OFFLINE", "1")
20
20
  os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
21
- os.environ["FOXCLAW_EVIDENCE_MODEL"] = "all-MiniLM-L6-v2" # 进程缓存里通常有,保证离线可测
21
+ os.environ["FOXCLAW_EVIDENCE_MODEL"] = "all-MiniLM-L6-v2"
22
22
 
23
23
  from foxclaw.search.evidence import EvidenceFusion, _has_neg, _semantic_sim
24
24
  from foxclaw.search.models import RawEvidence
@@ -26,7 +26,6 @@ def test_evidence_semantic():
26
26
  print("=" * 60); print("证据融合语义化验证"); print("=" * 60)
27
27
  f = EvidenceFusion()
28
28
 
29
- # [1] 否定词词边界:不依赖模型,总能测(这是幻影矛盾的根因)
30
29
  assert _has_neg("this is another way") is False, "another 里的 not 不该命中"
31
30
  assert _has_neg("the notebook is useful") is False, "notebook 里的 not 不该命中"
32
31
  assert _has_neg("I know this") is False, "know 里的 no 不该命中"
@@ -35,17 +34,14 @@ def test_evidence_semantic():
35
34
  assert _has_neg("这个方案并非最优") is True
36
35
  print("[1] 否定词词边界(another/notebook/know 不误命中)OK")
37
36
 
38
- # [2] 幻影矛盾消失:无关句子不再被判矛盾
39
37
  assert f._contradicts("this is another way", "this is a way") is False, "幻影矛盾"
40
38
  assert f._contradicts("the notebook is useful", "the book is useful") is False, "幻影矛盾"
41
39
  print("[2] 幻影矛盾消失(another/notebook 不再被判矛盾)OK")
42
40
 
43
- # [3] 真矛盾仍能识别(不能因为修 bug 把功能砍了)
44
41
  assert f._contradicts("cannot proceed", "can proceed") is True
45
42
  assert f._contradicts("这个方案并非最优", "这个方案是最优") is True
46
43
  print("[3] 真矛盾仍识别(cannot/并非)OK")
47
44
 
48
- # [4] 幻影矛盾会拉低置信 —— 端到端证明它真的有害
49
45
  g = f.build([
50
46
  RawEvidence(id="e1", fact="this is another way", source="s1", origin="internal", confidence=0.9),
51
47
  RawEvidence(id="e2", fact="this is a way", source="s2", origin="internal", confidence=0.9),
@@ -54,14 +50,9 @@ def test_evidence_semantic():
54
50
  assert f.confidence(g) >= 0.85, f"置信不该被幻影矛盾扣分: {f.confidence(g)}"
55
51
  print(f"[4] 端到端:无幻影矛盾边,置信未被误扣({f.confidence(g):.2f})OK")
56
52
 
57
- # ── 语义部分:模型可用才断言,否则优雅跳过(降级路径本身已由 [1]-[4] 覆盖)──
58
53
  if _semantic_sim("a", "a") is None:
59
54
  print("[skip] ST 模型未缓存 → 语义部分跳过(已回退字符级,不崩)")
60
55
  print("\n证据融合语义化验证通过(降级路径)")
61
- # 两种运行方式都要正确退出(M7):
62
- # · 被 pytest 收集 → pytest.skip 是标准做法;让测试显式"跳过"而非崩溃。
63
- # · 被 run_all.py 当脚本直跑 → pytest.skip 抛 Skipped 会让脚本非零退出,
64
- # 反而把「跳过」变成「失败」。故按 __name__ 区分。
65
56
  if __name__ != "__main__":
66
57
  import pytest
67
58
  pytest.skip("ST 模型未缓存 → 语义部分跳过(降级路径已由 [1]-[4] 覆盖)",
@@ -69,14 +60,12 @@ def test_evidence_semantic():
69
60
  import sys as _sys
70
61
  _sys.exit(0)
71
62
 
72
- # [5] 语义相似:近义高、无关低(字符级做不到这点)
73
63
  close = f._similarity("the plan is good", "the plan is nice")
74
64
  far = f._similarity("the plan is good", "the stock market crashed")
75
65
  assert close > 0.6 > far, f"近义应>0.6>无关: {close:.3f} vs {far:.3f}"
76
66
  assert f._similarity("hello world", "hello world") >= 0.99
77
67
  print(f"[5] 语义相似(近义 {close:.2f} > 0.6 > 无关 {far:.2f})OK")
78
68
 
79
- # [6] 语义等价的证据 → 建立 supports 边(原字符级识别不出)
80
69
  g2 = f.build([
81
70
  RawEvidence(id="a", fact="it will rain today", source="s1", origin="internal", confidence=0.8),
82
71
  RawEvidence(id="b", fact="rain is expected today", source="s2", origin="external", confidence=0.8),
@@ -85,8 +74,6 @@ def test_evidence_semantic():
85
74
  assert sup, "语义等价的证据应互相支持(字符级识别不出)"
86
75
  print("[6] 语义等价证据建立 supports 边 OK")
87
76
 
88
- # [7] 阈值仍成立(换成余弦后 support=0.6 / dup=0.92 依然分得开——实测:近义 0.80~0.91、
89
- # 无关 0.02、相同 1.00),且完全相同的证据仍被去重
90
77
  g3 = f.build([
91
78
  RawEvidence(id="x", fact="hello world", source="s1", origin="internal", confidence=0.8),
92
79
  RawEvidence(id="y", fact="hello world", source="s2", origin="internal", confidence=0.8),
@@ -46,7 +46,6 @@ def _parse(body: str) -> tuple[list, str]:
46
46
 
47
47
 
48
48
  def test_chat_stream_evidence_event_contract():
49
- # ISOLATION_GUARD:依赖 import 期固化的单例(路由配置/限流器),须独立进程跑。
50
49
  if "foxclaw.api" in sys.modules:
51
50
  import pytest
52
51
  pytest.skip("需独立进程运行(模块级单例已固化),请用 tests/run_all.py")
@@ -62,10 +61,10 @@ def test_chat_stream_evidence_event_contract():
62
61
  attachments=None, on_thinking=None, on_evidence=None):
63
62
  seen["has_cb"] = on_evidence is not None
64
63
  if task == "plain":
65
- yield "纯对话,没有动用工具。" # 无工具步:不上报任何证据
64
+ yield "纯对话,没有动用工具。"
66
65
  return
67
- yield "已完成。" # 正文先出
68
- if on_evidence is not None: # 证据后出(与真实实现一致:答案之后上报)
66
+ yield "已完成。"
67
+ if on_evidence is not None:
69
68
  on_evidence(EVIDENCE)
70
69
 
71
70
  original = CognitiveAgent.stream_reply
@@ -88,10 +87,8 @@ def test_chat_stream_evidence_event_contract():
88
87
  assert [i["tool"] for i in items] == ["web_search", "send_mail"]
89
88
  assert [i["status"] for i in items] == ["verified", "unverified"]
90
89
  assert items[0]["evidence"][0]["ref"] == "https://example.com/a"
91
- # 凭据不得混进正文——正文是答案,不是证据仓库
92
90
  assert "example.com" not in delta_text
93
91
  assert delta_text.strip() == "已完成。"
94
92
 
95
- # 纯对话:没有 evidence 事件,正文照常
96
93
  assert "event: evidence" not in plain_body
97
94
  assert "纯对话" in plain_body
@@ -12,7 +12,6 @@ sys.path.insert(0, str(Path(__file__).parent.parent / "vendor" / "hkc"))
12
12
 
13
13
 
14
14
  def test_file_ingest():
15
- # 离线 + stub,确定性
16
15
  for k in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY"):
17
16
  os.environ.pop(k, None)
18
17
  os.environ["HKC_EMBEDDING"] = "stub"
@@ -34,7 +33,6 @@ def test_file_ingest():
34
33
  doc = Document()
35
34
  doc.add_heading("第一章 引言", level=1)
36
35
  doc.add_paragraph("Foxclaw 是一个认知代理操作系统,由五个 Agent 组成。")
37
- # 表格夹在第一章内、第二章之前——用于验证按 body 顺序归章(而非全塞末章)
38
36
  t = doc.add_table(rows=2, cols=2)
39
37
  t.rows[0].cells[0].text = "字段"; t.rows[0].cells[1].text = "取值"
40
38
  t.rows[1].cells[0].text = "版本"; t.rows[1].cells[1].text = "v1.6"
@@ -53,7 +51,6 @@ def test_file_ingest():
53
51
  wb.save(str(p)); wb.close()
54
52
  return p
55
53
 
56
- # [1] DocxLoader:标题切章 + 表格行入文
57
54
  pd = DocxLoader().load(str(_make_docx()))
58
55
  assert pd.doc_type == "DOCX", pd.doc_type
59
56
  titles = [c.title for c in pd.chapters]
@@ -61,14 +58,12 @@ def test_file_ingest():
61
58
  alltext = "\n".join(s.content for c in pd.chapters for s in c.sections)
62
59
  assert "五个 Agent" in alltext, "正文段落丢失"
63
60
  assert "版本 | v1.6" in alltext, "表格行未转文本"
64
- # 表格按 body 顺序归入它出现的章节(第一章),不该被塞到末章
65
61
  ch1 = [c for c in pd.chapters if c.title == "第一章 引言"][0]
66
62
  ch2 = [c for c in pd.chapters if c.title == "第二章 架构"][0]
67
63
  assert any("版本 | v1.6" in s.content for s in ch1.sections), "表格未归到出现位置的第一章"
68
64
  assert not any("版本" in s.content for s in ch2.sections), "表格错误归到末章第二章"
69
65
  print(f"[1] DocxLoader({len(pd.chapters)} 章,标题切分 + 表格按序归章)OK")
70
66
 
71
- # [2] XlsxLoader:每表一章 + 行合并
72
67
  px = XlsxLoader().load(str(_make_xlsx()))
73
68
  assert px.doc_type == "XLSX", px.doc_type
74
69
  sheet_titles = [c.title for c in px.chapters]
@@ -77,12 +72,10 @@ def test_file_ingest():
77
72
  assert "收入 | 100" in xtext and "季度汇总" in xtext, xtext
78
73
  print(f"[2] XlsxLoader({len(px.chapters)} 表→章,行合并)OK")
79
74
 
80
- # [3] 加载器输出可被 Chunker 消费(loader→chunker 接线)
81
75
  chunks = Chunker().chunk(pd)
82
76
  assert chunks and any("Agent" in ch.content for ch in chunks), "docx 未切出有效 chunk"
83
77
  print(f"[3] Chunker 消费 docx({len(chunks)} chunk)OK")
84
78
 
85
- # [4] KDE 后缀分派端到端:假抽取器把 chunk 变 KU,证明 .docx/.xlsx 走通全链路
86
79
  def _build_kde():
87
80
  from hkc_core.utils.id_gen import IDGenerator
88
81
  from hkc_core.graph.sqlite_store import SQLiteGraphStore
@@ -111,7 +104,6 @@ def test_file_ingest():
111
104
  assert kus, f"{suffix}: 未写入任何 KU"
112
105
  print("[4] KDE 分派 .docx/.xlsx → loader → chunker → KU 全链路 OK")
113
106
 
114
- # [5] 不支持的格式明确报错(不静默产空)
115
107
  kde = _build_kde()
116
108
  bad = TMP / "x.pptx"; bad.write_bytes(b"PK\x03\x04not really")
117
109
  try:
@@ -121,16 +113,13 @@ def test_file_ingest():
121
113
  assert ".pptx" in str(e) or "不支持" in str(e), str(e)
122
114
  print("[5] 不支持格式(.pptx)明确 ValueError OK")
123
115
 
124
- # [6] 上传路由格式门:TestClient 走 Foxclaw app 挂载的 /knowledge/ingest/file
125
116
  from fastapi.testclient import TestClient
126
117
  from foxclaw.api import app
127
118
  with TestClient(app) as c:
128
- # 不支持后缀 → 415(后缀检查在读文件前,无需 LLM)
129
119
  r = c.post("/knowledge/ingest/file",
130
120
  files={"file": ("bad.pptx", b"x", "application/octet-stream")})
131
121
  assert r.status_code == 415, (r.status_code, r.text)
132
122
  assert ".docx" in r.text and ".xlsx" in r.text, "415 提示未含新格式"
133
- # 支持的 docx 上传 → 200(stub 抽取 0 KU,但格式门放行、链路不崩)
134
123
  with open(_make_docx(), "rb") as fh:
135
124
  r2 = c.post("/knowledge/ingest/file",
136
125
  files={"file": ("sample.docx", fh.read(),
@@ -25,8 +25,6 @@ def main():
25
25
  print("B 阶段 · 财务 Agent 内核(资金行为操作系统)")
26
26
  print("=" * 60)
27
27
 
28
- # policy 落临时目录:原先写 ./b_finance_policy.yaml 再 os.remove —— 一旦中途抛异常
29
- # 就会在仓库根留下垃圾文件。改临时目录后无需清理,也不会污染工作区。
30
28
  fr = FinanceRuntime(policy_path=str(Path(tempfile.mkdtemp()) / "b_finance_policy.yaml"))
31
29
  n = fr.import_csv(CSV)
32
30
  fr.set_budget("food", 400)
@@ -85,11 +83,6 @@ def main():
85
83
  print(f" 第 {t['month']} 月预计余额 {t['projected_balance']}")
86
84
  check(len(cf["trajectory"]) == 3, "现金流预测给出 3 个月轨迹")
87
85
 
88
- # ⚠ burn_rate 是**相对 now 的 30 天窗口**,而 examples/sample_transactions.csv 的交易
89
- # 日期固定在 2026-06 —— 随真实时间推移,样例必然滑出窗口,于是上面那段会**静默变成 0
90
- # 却毫无报错**(本测试此前不断言,故退化无人发现)。这里做两件事:
91
- # (a) 用"当下日期"的交易构造可控用例,把 burn/cashflow 的算术真正锁住;
92
- # (b) 断言其与 now-30天 窗口严格一致,把这个耦合显式写出来(而非假装它是稳定值)。
93
86
  _cutoff = datetime.now() - timedelta(days=30)
94
87
  in_window = [t for t in fr.transactions if t.dt >= _cutoff]
95
88
  expect_expense = round(sum(abs(t.amount) for t in in_window if t.is_expense), 2)
@@ -135,8 +128,6 @@ def main():
135
128
  req = fr.request_action("cancel_subscription", {"entity": "YouTube", "save": 12.0})
136
129
  print(f" 请求: {req['action']} · 状态: {req['status']}")
137
130
  print(f" 说明: {req['note']}")
138
- # 本文件 docstring 的承诺是"不真实动钱":执行层必须只产出**待批**状态。
139
- # 若哪天有人把它改成自动执行(EXECUTED/DONE),这里必须红。
140
131
  check("PENDING" in str(req["status"]).upper(),
141
132
  f"执行层只产出待批状态,不自动动钱(status={req['status']})")
142
133
 
@@ -33,7 +33,6 @@ def _one(text, now=NOW):
33
33
  return ps[0]
34
34
 
35
35
 
36
- # ── 1. 用户原话 ────────────────────────────────────────────
37
36
  def test_今天买衣服105块():
38
37
  p = _one("今天买了一件衣服花了105块")
39
38
  assert (p.amount, p.currency, p.direction) == (105.0, "CNY", "expense")
@@ -56,7 +55,6 @@ def test_后天洗车100():
56
55
  assert p.when.date() == datetime(2026, 11, 9).date()
57
56
 
58
57
 
59
- # ── 2. 币种 ───────────────────────────────────────────────
60
58
  def test_货币单位识别():
61
59
  assert _one("花了105块").currency == "CNY"
62
60
  assert _one("花了105块钱").currency == "CNY"
@@ -90,7 +88,6 @@ def test_中文数字():
90
88
  assert _one("两千三买了个手机").amount == 2300.0
91
89
 
92
90
 
93
- # ── 3. 时间关键词 ──────────────────────────────────────────
94
91
  def test_过去时间词():
95
92
  assert _one("昨天买了个杯子30").when.date() == datetime(2026, 11, 6).date()
96
93
  assert _one("前天买了个杯子30").when.date() == datetime(2026, 11, 5).date()
@@ -113,7 +110,6 @@ def test_上个月与上月():
113
110
 
114
111
  def test_几月与几号():
115
112
  assert _one("9月买衣服105").when.strftime("%Y-%m") == "2026-09"
116
- # 本月已过的 5 号 → 本月;本月还没到的 28 号 → 上个月
117
113
  assert _one("5号买衣服105", now=datetime(2026, 11, 7)).when.strftime("%Y-%m") == "2026-11"
118
114
  assert _one("28号买衣服105", now=datetime(2026, 11, 7)).when.strftime("%Y-%m") == "2026-10"
119
115
 
@@ -124,7 +120,6 @@ def test_时间词不会被当成金额():
124
120
  assert p.when.date() == datetime(2026, 10, 5).date()
125
121
 
126
122
 
127
- # ── 4. 多笔与分类 ──────────────────────────────────────────
128
123
  def test_一句话多笔拆分():
129
124
  ps = parse("今天买衣服105,还买了大米45", now=NOW)
130
125
  assert [p.amount for p in ps] == [105.0, 45.0]
@@ -148,7 +143,6 @@ def test_收入方向():
148
143
  assert p.category == "income"
149
144
 
150
145
 
151
- # ── 5. 不该记账的句子 ──────────────────────────────────────
152
146
  def test_无金额不解析():
153
147
  assert parse("今天几号", now=NOW) == []
154
148
  assert parse("本月预算怎么分配", now=NOW) == []
@@ -178,7 +172,6 @@ def test_主管把花费报备路由给财务():
178
172
  assert not SupervisorAgent._is_expense_report("今天几号")
179
173
 
180
174
 
181
- # ── 6. 记账与汇总 ──────────────────────────────────────────
182
175
  def test_记账回执与账本(tmp_path):
183
176
  rt = _rt(tmp_path)
184
177
  rec = rt.record_text("今天买了一件衣服花了105块")
@@ -197,15 +190,15 @@ def test_计划支出不进本月已花(tmp_path):
197
190
  rt.record_text("明天买一袋大米花了45")
198
191
 
199
192
  led = rt.ledger()
200
- assert led["expense"] == 105.0 # 45 还没发生
193
+ assert led["expense"] == 105.0
201
194
  assert led["planned"]["count"] == 1
202
195
  assert led["planned"]["expense"] == 45.0
203
- assert rt.analyze()["transaction_count"] == 1 # 分析也不能把未来算进来
196
+ assert rt.analyze()["transaction_count"] == 1
204
197
 
205
198
 
206
199
  def test_多币种折成本位币(tmp_path):
207
200
  rt = _rt(tmp_path)
208
- rt.record_text("花了100美元") # 内置汇率 7.2
201
+ rt.record_text("花了100美元")
209
202
  led = rt.ledger()
210
203
  assert led["expense"] == 720.0
211
204
  usd = [c for c in led["by_currency"] if c["currency"] == "USD"][0]
@@ -219,7 +212,7 @@ def test_汇率可改(tmp_path):
219
212
  rt.fx.set_rate("USD", 7.0)
220
213
  rt.record_text("花了100美金")
221
214
  assert rt.ledger()["expense"] == 700.0
222
- assert _rt(tmp_path).fx.rate("USD") == 7.0 # 汇率也落盘
215
+ assert _rt(tmp_path).fx.rate("USD") == 7.0
223
216
 
224
217
 
225
218
  def test_按天与分类汇总(tmp_path):
@@ -246,7 +239,7 @@ def test_记账后账本可持久化(tmp_path):
246
239
 
247
240
  def test_未配置汇率按1比1并提示(tmp_path):
248
241
  rt = _rt(tmp_path)
249
- rt.fx.rates.pop("JPY", None) # 模拟"币种认识但没配汇率"
242
+ rt.fx.rates.pop("JPY", None)
250
243
  rec = rt.record_text("花了100日元")
251
244
  assert any("汇率" in w for w in rec["recorded"][0]["warnings"])
252
245
  assert rt.ledger()["expense"] == 100.0
@@ -41,7 +41,6 @@ def test_交易预算余额重启后可恢复(tmp_path):
41
41
  assert rt.save() is True
42
42
  assert (tmp_path / "finance_data.json").exists()
43
43
 
44
- # 新实例 = 进程重启后的状态
45
44
  rt2 = _rt(tmp_path)
46
45
  assert len(rt2.transactions) == 2
47
46
  assert rt2.transactions[0].amount == -88.5
@@ -51,7 +50,6 @@ def test_交易预算余额重启后可恢复(tmp_path):
51
50
  assert rt2.transactions[1].amount == 12000
52
51
  assert len(rt2.budgets) == 1 and rt2.budgets[0].monthly_limit == 400
53
52
  assert rt2.account_balances == {"debit": 11800}
54
- # 实体统计随交易一起恢复(重启后 trace_category / status 不该突然归零)
55
53
  assert rt2.entities["星巴克"].txn_count == 1
56
54
  assert rt2.entities["星巴克"].entity_type == "subscription"
57
55
  assert rt2.entities["公司"].total_amount == 12000
@@ -64,7 +62,7 @@ def test_写入即落盘下次实例自动加载(tmp_path):
64
62
  rt.set_balance("debit", 100)
65
63
 
66
64
  loaded = _rt(tmp_path)
67
- assert loaded.load() # 已显式 load 过,再次 load 幂等
65
+ assert loaded.load()
68
66
  assert len(loaded.transactions) == 1
69
67
  assert loaded.status()["persist"]["load_error"] == ""
70
68
 
@@ -83,14 +81,14 @@ def test_损坏文件不阻断启动且可自愈(tmp_path):
83
81
  p = tmp_path / "finance_data.json"
84
82
  p.write_text("{ 这不是 json ", encoding="utf-8")
85
83
 
86
- rt = _rt(tmp_path) # 构造本身不得抛
84
+ rt = _rt(tmp_path)
87
85
  assert rt.transactions == [] and rt.budgets == []
88
- assert rt.load_error # 错误必须可见,不能静默吞掉
86
+ assert rt.load_error
89
87
 
90
88
  rt.add_transaction(Transaction(amount=-5, category="misc",
91
89
  entity="y", account="debit"))
92
90
  assert rt.save() is True
93
- assert rt.load_error == "" # 覆盖写成功后重新可读
91
+ assert rt.load_error == ""
94
92
  assert len(_rt(tmp_path).transactions) == 1
95
93
 
96
94
 
@@ -106,8 +104,8 @@ def test_单条坏记录只跳过自己不废掉整份(tmp_path):
106
104
  "version": 1,
107
105
  "transactions": [
108
106
  {"amount": -10, "category": "food", "entity": "a", "account": "debit"},
109
- {"category": "food"}, # 缺 amount/entity/account → 跳过
110
- "not-a-dict", # 非对象 → 跳过
107
+ {"category": "food"},
108
+ "not-a-dict",
111
109
  {"amount": -20, "category": "food", "entity": "b", "account": "debit"},
112
110
  ],
113
111
  "budgets": [{"category": "food", "monthly_limit": 300}],
@@ -117,7 +115,7 @@ def test_单条坏记录只跳过自己不废掉整份(tmp_path):
117
115
  rt = _rt(tmp_path)
118
116
  assert [t.entity for t in rt.transactions] == ["a", "b"]
119
117
  assert len(rt.budgets) == 1
120
- assert rt.account_balances == {"cash": 50.0} # 坏值单独跳过
118
+ assert rt.account_balances == {"cash": 50.0}
121
119
 
122
120
 
123
121
  def test_实体段缺失时按交易重建(tmp_path):
@@ -154,12 +152,10 @@ def test_批量导入只写一次盘(tmp_path, monkeypatch):
154
152
  def test_落盘失败只回报不抛异常(tmp_path):
155
153
  blocker = tmp_path / "blocker"
156
154
  blocker.write_text("我是个文件,不是目录", encoding="utf-8")
157
- # 父目录被一个文件占住 → mkdir/写入必失败
158
155
  rt = FinanceRuntime(policy_path=str(tmp_path / "finance_policy.yaml"),
159
156
  data_path=str(blocker / "finance_data.json"))
160
157
  assert rt.save() is False
161
- assert rt.save_error # 失败原因必须留痕
162
- # 内存里的数据仍在,用户这次会话不至于丢东西
158
+ assert rt.save_error
163
159
  rt.add_transaction(Transaction(amount=-1, category="misc",
164
160
  entity="z", account="debit"))
165
161
  assert len(rt.transactions) == 1
@@ -6,19 +6,13 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
6
6
  import pytest
7
7
 
8
8
  def test_gateway():
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
- # 本测试用 pop("FOXCLAW_API_KEY") 验证「未设即网关默认关」,但 pop 之后 load_dotenv
14
- # (override=False)仍会从 .env 把键灌回来(开发者 .env 常带真实 FOXCLAW_API_KEY)→ 判定
15
- # 随机器漂移。关掉 .env 使本测试自洽,单独运行也稳定。
16
11
  os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
17
- # 必须在 import foxclaw.api 之前设好:网关实例在模块加载时按环境变量建
18
12
  os.environ["FOXCLAW_GATEWAY"] = "1"
19
- os.environ["FOXCLAW_GATEWAY_RATE"] = "5" # 每分钟 5 → 桶容量 5,突发 5 后限流
13
+ os.environ["FOXCLAW_GATEWAY_RATE"] = "5"
20
14
  os.environ["FOXCLAW_GATEWAY_QUOTA"] = "1000"
21
- os.environ["FOXCLAW_MAX_BODY_MB"] = "1" # 请求体硬顶降到 1MB 便于测 413
15
+ os.environ["FOXCLAW_MAX_BODY_MB"] = "1"
22
16
  os.environ.pop("FOXCLAW_API_KEY", None)
23
17
  os.environ["HKC_EMBEDDING"] = "stub"
24
18
  os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
@@ -26,10 +20,8 @@ def test_gateway():
26
20
 
27
21
  print("=" * 60); print("统一 API 网关验证"); print("=" * 60)
28
22
 
29
- # ── Part A:Gateway 类单元 ─────────────────────────────────
30
23
  from foxclaw.core.gateway import Gateway
31
24
 
32
- # [1] 突发令牌桶:容量内放行,超出限流(reason=rate)
33
25
  g = Gateway(rate_per_min=3, quota_per_hour=100000)
34
26
  allowed = [g.check("a")[0] for _ in range(5)]
35
27
  assert allowed[:3] == [True, True, True] and allowed[3] is False, allowed
@@ -37,11 +29,9 @@ def test_gateway():
37
29
  assert not ok and reason == "rate" and retry >= 1, (ok, retry, reason)
38
30
  print("[1] 令牌桶突发限流(3 放行→限流,带 retry)OK")
39
31
 
40
- # [2] per-client 隔离:a 被限,b 不受影响
41
32
  assert g.check("b")[0] is True, "客户端之间不隔离"
42
33
  print("[2] per-client 隔离 OK")
43
34
 
44
- # [3] 配额(固定窗口):超每小时额度 → reason=quota
45
35
  g2 = Gateway(rate_per_min=100000, quota_per_hour=4)
46
36
  cnt = sum(1 for _ in range(6) if g2.check("z")[0])
47
37
  assert cnt == 4, cnt
@@ -49,24 +39,20 @@ def test_gateway():
49
39
  assert not ok and reason == "quota" and retry >= 1, (ok, retry, reason)
50
40
  print("[3] 每小时配额封顶(reason=quota)OK")
51
41
 
52
- # [4] 空闲淘汰:大量不同 client 不撑爆内存
53
42
  g3 = Gateway(rate_per_min=100, quota_per_hour=100000, max_clients=64)
54
43
  for i in range(500):
55
44
  g3.check(f"c{i}")
56
45
  assert g3.stats()["tracked_clients"] <= 64, g3.stats()
57
46
  print(f"[4] 空闲淘汰(500 client → 追踪 {g3.stats()['tracked_clients']}≤64,防内存 DoS)OK")
58
47
 
59
- # ── Part B:接入 FastAPI 中间件(TestClient)────────────────
60
48
  from fastapi.testclient import TestClient
61
49
  from foxclaw.api import app, _gateway_on
62
50
 
63
51
  with TestClient(app) as c:
64
- # [5] stats 端点:启用 + 额度可见
65
52
  s = c.get("/api/gateway/stats").json()
66
53
  assert s["enabled"] is True and s["rate_per_min"] == 5, s
67
54
  print(f"[5] /api/gateway/stats(enabled={s['enabled']}, rate={s['rate_per_min']}/min)OK")
68
55
 
69
- # [6] 超限 → 429 + Retry-After(同一 client=ip:testclient 共用桶)
70
56
  codes = [c.get("/api/status").status_code for _ in range(12)]
71
57
  assert 200 in codes and 429 in codes, codes
72
58
  r429 = None
@@ -78,16 +64,13 @@ def test_gateway():
78
64
  assert r429.headers.get("Retry-After") and r429.json().get("retry_after", 0) >= 1, (r429.headers, r429.text)
79
65
  print("[6] 突发超限 → 429 + Retry-After 头 OK")
80
66
 
81
- # [7] 探针豁免:/health 不在网关下,始终放行
82
67
  assert c.get("/health").status_code == 200, "探针被网关误限"
83
68
  print("[7] 探针 /health 豁免网关 OK")
84
69
 
85
- # [8] 请求体硬顶:Content-Length 超 1MB → 413(在读 body/sanitize 前拦下,始终生效)
86
70
  big = c.post("/api/knowledge/ingest", json={"text": "x" * (1200 * 1024)})
87
71
  assert big.status_code == 413, (big.status_code, big.text[:120])
88
72
  print("[8] 超大请求体 → 413(读 body 前拦截)OK")
89
73
 
90
- # [8b] chunked / 无 Content-Length:只查头会漏,流上累计必须兜住
91
74
  import asyncio
92
75
  from starlette.requests import Request as _Req
93
76
  from foxclaw.api.middleware import (
@@ -104,7 +87,7 @@ def test_gateway():
104
87
  req = _Req({"type": "http", "method": "POST", "path": "/api/x",
105
88
  "scheme": "http", "server": ("test", 80),
106
89
  "headers": [], "query_string": b""}, _recv)
107
- _wrap_body_limit(req, 15) # 第 2 块累计 20 > 15
90
+ _wrap_body_limit(req, 15)
108
91
 
109
92
  async def _drain():
110
93
  for _ in range(3):
@@ -117,7 +100,6 @@ def test_gateway():
117
100
  else:
118
101
  raise AssertionError("chunked 超限未被流上计数拦住")
119
102
 
120
- # [8c] 流超限转成 413,而不是冒成 500
121
103
  async def _boom(_req):
122
104
  raise _PayloadTooLarge(999)
123
105
 
@@ -128,7 +110,6 @@ def test_gateway():
128
110
  assert resp.status_code == 413, resp.status_code
129
111
  print("[8b] chunked 超限 → 流上计数拦截并转 413 OK")
130
112
 
131
- # [9] 启用哲学:默认关;配 FOXCLAW_API_KEY(=对外暴露)自动开;=0 强制关优先
132
113
  os.environ.pop("FOXCLAW_GATEWAY", None); os.environ.pop("FOXCLAW_API_KEY", None)
133
114
  assert _gateway_on() is False, "本地默认应关"
134
115
  os.environ["FOXCLAW_API_KEY"] = "x"