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
@@ -7,7 +7,6 @@ import os, sys, tempfile, shutil
7
7
  from pathlib import Path
8
8
  sys.path.insert(0, str(Path(__file__).parent.parent))
9
9
 
10
- # ── 环境隔离:必须在 import foxclaw 之前(import 会读这些键初始化) ─────────────
11
10
  for _k in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY",
12
11
  "GOOGLE_API_KEY", "DASHSCOPE_API_KEY", "FOXCLAW_API_KEY"):
13
12
  os.environ.pop(_k, None)
@@ -15,10 +14,10 @@ os.environ["HKC_EMBEDDING"] = "stub"
15
14
  os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
16
15
  os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_att_")
17
16
  os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
18
- os.environ["FOXCLAW_PROVIDER"] = "" # 不指定 provider → 无 key 时降级 stub(确定性)
17
+ os.environ["FOXCLAW_PROVIDER"] = ""
19
18
  os.environ["FOXCLAW_MODEL"] = ""
20
19
  os.environ["FOXCLAW_MCP_SERVER"] = "0"
21
- os.environ["HF_HUB_OFFLINE"] = "1" # 离线:HMR embedding 读本地缓存,不联网
20
+ os.environ["HF_HUB_OFFLINE"] = "1"
22
21
  os.environ["TRANSFORMERS_OFFLINE"] = "1"
23
22
 
24
23
  import pytest
@@ -40,31 +39,25 @@ def store():
40
39
 
41
40
 
42
41
  def test_sanitize_attachments():
43
- # 数量上限
44
42
  many = [{"name": f"f{i}.txt", "text": "t"} for i in range(_MAX_ATTACHMENTS + 4)]
45
43
  assert len(_sanitize_attachments(many)) == _MAX_ATTACHMENTS
46
44
 
47
- # 单文件正文截断到上限
48
45
  one = _sanitize_attachments([{"name": "a.txt", "text": "x" * (_ATTACH_TEXT_MAX + 100)}])
49
46
  assert len(one[0]["text"]) == _ATTACH_TEXT_MAX
50
47
 
51
- # name 截断到 200
52
48
  assert _sanitize_attachments([{"name": "a" * 300, "text": "x"}])[0]["name"] == "a" * 200
53
49
 
54
- # 累计正文封顶:单文件先截到 20000,4×20000 累计 60000 封顶 → 第 4 个正文被清空(名字仍在)
55
50
  big = "x" * (_ATTACH_TEXT_MAX + 1000)
56
51
  four = _sanitize_attachments([{"name": f"f{i}.txt", "text": big} for i in range(4)])
57
52
  assert len(four) == 4
58
53
  assert sum(len(a["text"]) for a in four) == _ATTACH_TEXT_TOTAL
59
54
  assert four[3]["text"] == ""
60
55
 
61
- # 非列表 / 非 dict / 空 name+空 text → 全部剔除
62
56
  assert _sanitize_attachments("notalist") == []
63
57
  assert _sanitize_attachments([1, "x"]) == []
64
58
  assert _sanitize_attachments([{"name": "", "text": ""}]) == []
65
59
  assert _sanitize_attachments(None) == []
66
60
 
67
- # 字段类型清洗:size 非数字 → 0;mime/url 转 str
68
61
  out = _sanitize_attachments([{"name": "a", "size": "big", "mime": None, "url": None, "text": "t"}])
69
62
  assert out[0]["size"] == 0 and out[0]["mime"] == "" and out[0]["url"] == ""
70
63
  print("护栏(数量/单文件/累计/类型清洗/边界)OK")
@@ -78,10 +71,10 @@ def test_attachments_context():
78
71
  ctx = CognitiveAgent._attachments_context(atts)
79
72
  assert "【附件「a.txt」内容如下】" in ctx and "hello world" in ctx
80
73
  assert "【附件「b.txt」" not in ctx
81
- assert "b.pdf" in ctx and "无法提取" in ctx # 非文本降级为「文件名 + 元数据」提示
74
+ assert "b.pdf" in ctx and "无法提取" in ctx
82
75
  assert CognitiveAgent._attachments_context([]) == ""
83
76
  assert CognitiveAgent._attachments_context(None) == ""
84
- assert CognitiveAgent._attachments_context([1, "x"]) == "" # 非 dict 全跳过
77
+ assert CognitiveAgent._attachments_context([1, "x"]) == ""
85
78
  print("LLM 上下文注入(正文/降级/空)OK")
86
79
 
87
80
 
@@ -98,7 +91,6 @@ def test_sessions_append_attachments(store):
98
91
  assert len(m["attachments"]) == 2
99
92
  assert m["attachments"][0] == {"name": "a.txt", "size": 11, "mime": "text/plain", "url": "/api/attachments/abc"}
100
93
  assert "text" not in m["attachments"][0], "text 不得落盘(会话文件不膨胀)"
101
- # 不带附件 → 无 attachments 字段(向后兼容)
102
94
  assert store.append(s["id"], "assistant", "好")
103
95
  assert "attachments" not in store.get(s["id"])["messages"][1]
104
96
  print("会话落盘(轻字段/不存 text/向后兼容)OK")
@@ -106,42 +98,35 @@ def test_sessions_append_attachments(store):
106
98
 
107
99
  def test_upload_and_download():
108
100
  with TestClient(app) as c:
109
- # 上传文本文件 → 提取正文
110
101
  r = c.post("/api/upload", files={"file": ("笔记.txt", "今天学了三件事".encode(), "text/plain")})
111
102
  assert r.status_code == 200, r.text
112
103
  a = r.json()
113
104
  assert a["name"] == "笔记.txt" and a["text"] == "今天学了三件事"
114
105
  assert a["url"].startswith("/api/attachments/") and a["id"] and a["size"] > 0
115
106
 
116
- # 下载 → 同一内容
117
107
  r2 = c.get(a["url"])
118
108
  assert r2.status_code == 200
119
109
  assert r2.content.decode("utf-8") == "今天学了三件事"
120
110
 
121
- # 二进制(不可提取)→ text 为空,降级
122
111
  r3 = c.post("/api/upload", files={"file": ("img.bin", b"\x00\x01\x02", "application/octet-stream")})
123
112
  assert r3.status_code == 200 and r3.json()["text"] == ""
124
113
 
125
- # 空文件 → 400;超大 → 413
126
114
  assert c.post("/api/upload", files={"file": ("e.txt", b"", "text/plain")}).status_code == 400
127
115
  big = b"x" * (21 * 1024 * 1024)
128
116
  assert c.post("/api/upload", files={"file": ("big.txt", big, "text/plain")}).status_code == 413
129
117
 
130
- # 路径穿越:HTTP 层规范化后到不了附件路由(404);非法 id 字符 → 400;合法但不存在 → 404
131
- assert c.get("/api/attachments/../../etc/passwd").status_code == 404 # .. 被 URL 规范化,不匹配路由
132
- assert c.get("/api/attachments/a.b").status_code == 400 # . 不在 id 白名单 [_A-Za-z0-9_-]
133
- assert c.get("/api/attachments/deadbeef0000").status_code == 404 # 合法 id 但文件不存在
118
+ assert c.get("/api/attachments/../../etc/passwd").status_code == 404
119
+ assert c.get("/api/attachments/a.b").status_code == 400
120
+ assert c.get("/api/attachments/deadbeef0000").status_code == 404
134
121
  print("上传/下载(正文提取/二进制降级/空/超大/穿越/404)OK")
135
122
 
136
123
 
137
124
  def test_chat_stream_with_attachments():
138
125
  import foxclaw.api as _api
139
126
  with TestClient(app) as c:
140
- # 先上传一个带正文的附件,拿到引用
141
127
  up = c.post("/api/upload", files={"file": ("要点.txt", "预算 100 万".encode(), "text/plain")})
142
128
  att = up.json()
143
- # 会话须先存在,append 才会落盘(前端 ensureSession 同款:POST /api/sessions → 拿 id)
144
- sid = _api._os_instance.sessions.create()["id"]
129
+ sid = _api.os_instance().sessions.create()["id"]
145
130
  payload = {
146
131
  "task": "帮我总结要点",
147
132
  "agent_id": "supervisor",
@@ -153,8 +138,7 @@ def test_chat_stream_with_attachments():
153
138
  assert r.status_code == 200, r.text[:200]
154
139
  assert "data:" in r.text and '"error"' not in r.text, r.text[:300]
155
140
 
156
- # 会话已落盘用户消息 + 附件元数据(不含 text)
157
- d = _api._os_instance.sessions.get(sid)
141
+ d = _api.os_instance().sessions.get(sid)
158
142
  assert d is not None, "会话未落盘"
159
143
  um = d["messages"][0]
160
144
  assert um["role"] == "user" and um["content"] == "帮我总结要点"
@@ -24,8 +24,6 @@ def _write_chain(path: Path, records) -> None:
24
24
  path.write_text("\n".join(lines) + "\n", encoding="utf-8")
25
25
 
26
26
 
27
- # ── 链 ─────────────────────────────────────────────────────────────
28
-
29
27
  def test_chain_passes(tmp_path):
30
28
  from foxclaw.core import audit_chain
31
29
 
@@ -42,7 +40,7 @@ def test_tampered_line_detected(tmp_path):
42
40
  _write_chain(p, [{"verdict": "deny"}, {"verdict": "allow"}])
43
41
  lines = p.read_text(encoding="utf-8").splitlines()
44
42
  rec = json.loads(lines[0])
45
- rec["verdict"] = "allow" # 把一次拒绝抹成放行
43
+ rec["verdict"] = "allow"
46
44
  lines[0] = json.dumps(rec, ensure_ascii=False)
47
45
  p.write_text("\n".join(lines) + "\n", encoding="utf-8")
48
46
 
@@ -69,7 +67,7 @@ def test_resume_after_restart(tmp_path):
69
67
  p = tmp_path / "governance.jsonl"
70
68
  GovernanceAuditor(sink_path=p).record(
71
69
  layer="skill", actor="office", action="write_note", verdict="success")
72
- aud = GovernanceAuditor(sink_path=p) # 模拟重启
70
+ aud = GovernanceAuditor(sink_path=p)
73
71
  aud.record(layer="skill", actor="office", action="write_note", verdict="success")
74
72
 
75
73
  ok, n, msg = aud.verify()
@@ -96,8 +94,6 @@ def test_tail_hash_ignores_torn_line(tmp_path):
96
94
  assert audit_chain.tail_hash(p) == "h1"
97
95
 
98
96
 
99
- # ── recovery 脱敏 ──────────────────────────────────────────────────
100
-
101
97
  def test_recovery_step_args_and_result_redacted():
102
98
  from types import SimpleNamespace
103
99
 
@@ -117,5 +113,4 @@ def test_recovery_step_args_and_result_redacted():
117
113
 
118
114
  assert "sk-" not in json.dumps(got["args"]), got["args"]
119
115
  assert "[已脱敏]" in got["result"], got["result"]
120
- # key 必须用未脱敏的 args 算,否则同一步骤前后 key 不一致(缓存/回放对不上)
121
116
  assert got["key"] == _step_key("http", {"api_key": key})
@@ -42,7 +42,6 @@ def test_unit():
42
42
  "reason": "office 的职责域不含 ['type:decision','finance.transfer']",
43
43
  "mode": "audit", "content_sha": "0123456789abcdef",
44
44
  "content_preview": "财务应给办公转账1000元"}, source="office")
45
- # 非本主题的事件不应落盘(subscribe 前缀精确命中)
46
45
  spine.publish("memory.updated", {"x": 1})
47
46
 
48
47
  check(path.exists(), "审计文件已创建(含 audit/ 目录)")
@@ -55,7 +54,6 @@ def test_unit():
55
54
  check("ts" in r and "T" in r["ts"], "ts 为 ISO 时间(人类可读)")
56
55
  check(bool(r["correlation_id"]), "correlation_id 已记录(可与脊柱 history 对账)")
57
56
 
58
- # 追加语义:第二条接在后面,顺序保持
59
57
  spine.publish("memory.shared.write", {"agent_id": "system", "type": "execution",
60
58
  "tags": ["truth_pipeline"], "allowed": True,
61
59
  "reason": "命中全局允许 facet", "mode": "audit",
@@ -65,12 +63,10 @@ def test_unit():
65
63
  check(len(recs) == 2 and recs[1]["payload"]["agent_id"] == "system",
66
64
  "追加写不覆盖旧行,顺序保持")
67
65
 
68
- # 坏行容错:夹一行垃圾,read() 跳过而不是崩
69
66
  with open(path, "a", encoding="utf-8") as f:
70
67
  f.write("{这不是JSON}\n")
71
68
  check(len(sink.read()) == 2, "坏行被跳过,读取不崩")
72
69
 
73
- # 开关:环境变量置 0 时 attach 不挂
74
70
  old = os.environ.get(ENV_SWITCH)
75
71
  os.environ[ENV_SWITCH] = "0"
76
72
  try:
@@ -99,7 +95,6 @@ def test_end_to_end():
99
95
  audit_path = Path(tmp) / "audit" / "shared_write.jsonl"
100
96
  check(audit_path.parent.exists(), "audit/ 目录已建在 data_dir 下")
101
97
 
102
- # 一条合法 + 一条越界(audit 模式不拦截,只记录)——真实闸门口径
103
98
  osi.hmr.ingest("用户偏好:喜欢表格", type="concept", agent_id="finance",
104
99
  tags=["preference"], shared=True)
105
100
  osi.hmr.ingest("财务应给办公转账1000元", type="decision", agent_id="office",
@@ -112,7 +107,6 @@ def test_end_to_end():
112
107
  denied = [r for r in recs if not r["payload"]["allowed"]]
113
108
  check(bool(denied) and "office" in denied[0]["payload"]["reason"] or bool(denied),
114
109
  "越界那条可从文件直接判读")
115
- # 与脊柱 history 口径一致:文件条数 == 脊柱里该主题条数
116
110
  spine_n = len(osi.event_spine.history("memory.shared.write", 100))
117
111
  check(spine_n == len(recs), f"文件条数与脊柱 history 一致({len(recs)}=={spine_n})")
118
112
  finally:
@@ -91,10 +91,8 @@ async def test_service_wrapper():
91
91
  print("\n【3】BrowserService async 包装(注入假 session,经专用工作线程)")
92
92
  svc = BrowserService()
93
93
  fake = FakeSession()
94
- # 绕过 playwright:预置会话,则真实 _ensure_session_sync 因 _session 非 None 直接返回它,
95
- # 不构造 SessionManager/不 launch chromium。强制 available=True 走真实工作线程路径。
96
94
  svc._session = fake
97
- svc.available = lambda: True # 实例属性覆盖类 staticmethod
95
+ svc.available = lambda: True
98
96
 
99
97
  p = await svc.perceive()
100
98
  assert p["ok"] and p["semantic_state"]["title"] == "P1"
@@ -104,16 +102,14 @@ async def test_service_wrapper():
104
102
  assert r["ok"] and r["verified"]["success"]
105
103
  print(" ✓ act(navigate) 返回校验后结果 verified.success=True")
106
104
 
107
- bad = await svc.act({"target": "3"}) # 缺 type
105
+ bad = await svc.act({"target": "3"})
108
106
  assert bad["ok"] is False and "非法动作" in bad["error"]
109
107
  print(" ✓ 非法动作(缺 type)被结构化拒绝")
110
108
 
111
109
  await svc.close()
112
110
  assert fake.closed, "close() 必须在工作线程上释放会话"
113
111
  print(" ✓ close() 释放了会话")
114
- # 幂等:再次 close 不报错
115
112
  await svc.close()
116
- # 关停后调用被拒(executor 已关)
117
113
  after_close = await svc.perceive()
118
114
  assert after_close["ok"] is False
119
115
  print(" ✓ close() 幂等 + 关停后调用被安全拒绝")
@@ -10,8 +10,6 @@ from foxclaw.browser.action import url_policy
10
10
  from foxclaw.browser.driver.playwright_driver import PlaywrightDriver
11
11
 
12
12
 
13
- # ── 判定层 ────────────────────────────────────────────────────
14
-
15
13
  @pytest.mark.parametrize("url", [
16
14
  "data:image/png;base64,iVBORw0KGgo=",
17
15
  "blob:https://example.com/2f8c-4a",
@@ -112,8 +110,6 @@ def test_short_ipv6_with_scope_is_not_mistaken(monkeypatch):
112
110
  url_policy._resolve_cache.clear()
113
111
 
114
112
 
115
- # ── 路由回调 ──────────────────────────────────────────────────
116
-
117
113
  class _FakeRoute:
118
114
  def __init__(self, url):
119
115
  self.request = type("Req", (), {"url": url})()
@@ -43,10 +43,8 @@ def test_cycles_broken_structural():
43
43
  try:
44
44
  from foxclaw import Foxclaw
45
45
  os_ = Foxclaw(data_dir=d, shadow_rounds=50)
46
- # 环1:Agent 不再直接持 search_runtime
47
46
  assert not hasattr(os_.finance, "_search_runtime"), "Agent 不应有 _search_runtime 直接引用"
48
47
  assert os_.finance._caps is os_.capabilities, "Agent 改持能力中枢"
49
- # 环2:Orchestrator 不再把 agents 存为实例字段(改为经中枢解析的 property)
50
48
  assert "agents" not in os_.orchestrator.__dict__, "orchestrator 不应把 agents 存为实例字段"
51
49
  assert os_.orchestrator._caps is os_.capabilities
52
50
  assert os_.orchestrator.agents is os_._agents, "orchestrator.agents 经中枢解析"
@@ -63,17 +61,14 @@ def test_behavior_preserved():
63
61
  try:
64
62
  from foxclaw import Foxclaw
65
63
  os_ = Foxclaw(data_dir=d, shadow_rounds=50)
66
- # 跨 Agent 编排(orchestrator 经中枢解析 finance)
67
64
  r = os_.supervisor.cross_request(
68
65
  from_agent="admin", to_agent="finance", request_type="budget_request",
69
66
  payload={"amount": 150, "category": "travel", "entity": "高铁", "account": "default"},
70
67
  reason="出差")
71
68
  assert r["status"] in ("approved", "confirm_required", "rejected"), f"裁决异常 {r}"
72
69
  print(f" ✓ cross_request 经中枢解析 agents → 状态 {r['status']}")
73
- # Agent 反向调用内部搜索(经中枢解析 search_runtime)
74
70
  res = os_.finance.search_internal("测试查询")
75
71
  assert res is not None, "search_internal 应经中枢解析到 search_runtime"
76
- # 中枢缓存:两次解析同一实例
77
72
  assert os_.capabilities.get("search_runtime") is os_.search_runtime
78
73
  print(" ✓ search_internal 经中枢解析 search_runtime(缓存一致)")
79
74
  asyncio.run(os_.aclose())
@@ -101,7 +96,6 @@ def test_lazy_build_once_concurrent():
101
96
  for t in ts: t.join()
102
97
  assert calls["n"] == 1, f"并发下惰性应只构造 1 次,实得 {calls['n']}"
103
98
  assert len({id(r) for r in results}) == 1, "12 线程应拿到同一实例"
104
- # 不同能力不互相阻塞(构造锁 per-name;此处只验证正确性:各自构造一次)
105
99
  reg.register_lazy("y", lambda: object())
106
100
  assert reg.get("y") is reg.get("y")
107
101
  print(" ✓ 12 线程并发 get 同一惰性 → 只构造 1 次、同一实例;不同能力独立")
@@ -23,8 +23,6 @@ def _headers(secret: str = SECRET, body: bytes = b'{"a":1}', ts: float = 1_700_0
23
23
  return signer.headers(body, nonce="n0nce", now=ts)
24
24
 
25
25
 
26
- # ── canonical / 签名 ────────────────────────────────────────
27
-
28
26
  def test_canonical_format_is_locked() -> None:
29
27
  """canonical 是两端的协议:字段顺序/分隔符一改,对方全部验签失败。"""
30
28
  line = S.canonical("1700000000", "abc", b"body").decode()
@@ -63,7 +61,6 @@ def test_replay_guard_rejects_second_use() -> None:
63
61
  guard = S.ReplayGuard(ttl=300)
64
62
  assert S.verify(h, body, SECRET, replay=guard, now=1_700_000_000) == (True, "ok")
65
63
  assert S.verify(h, body, SECRET, replay=guard, now=1_700_000_000) == (False, "replay")
66
- # 换一个 nonce(正常新请求)→ 通过
67
64
  h2 = S.OutboundSigner(SECRET).headers(body, nonce="other", now=1_700_000_000)
68
65
  assert S.verify(h2, body, SECRET, replay=guard, now=1_700_000_000) == (True, "ok")
69
66
 
@@ -82,12 +79,10 @@ def test_verify_reason_codes() -> None:
82
79
 
83
80
  def test_header_lookup_is_case_insensitive() -> None:
84
81
  body = b"{}"
85
- h = {k.lower(): v for k, v in _headers(body=body).items()} # ASGI 常给小写
82
+ h = {k.lower(): v for k, v in _headers(body=body).items()}
86
83
  assert S.verify(h, body, SECRET, now=1_700_000_000) == (True, "ok")
87
84
 
88
85
 
89
- # ── signer ─────────────────────────────────────────────────
90
-
91
86
  def test_signer_optional_key_id_and_disabled_state() -> None:
92
87
  h = S.OutboundSigner(SECRET, key_id="k9").headers(b"{}")
93
88
  assert h[S.KEY_ID_HEADER] == "k9"
@@ -105,8 +100,6 @@ def test_from_env_opt_in() -> None:
105
100
  assert S.from_env({S.ENV_SECRET: " "}) is None
106
101
 
107
102
 
108
- # ── 桥接层 ─────────────────────────────────────────────────
109
-
110
103
  class _Resp:
111
104
  status_code = 200
112
105
  text = ""
@@ -155,7 +148,7 @@ def test_bridge_signs_the_bytes_it_sends(fake_http) -> None:
155
148
  assert isinstance(body, bytes) and json.loads(body)["text"] == "hi"
156
149
  assert cap["headers"]["Content-Type"] == "application/json"
157
150
  assert cap["headers"]["Authorization"] == "Bearer t"
158
- ok, why = S.verify(cap["headers"], body, SECRET) # 网关侧照此验签
151
+ ok, why = S.verify(cap["headers"], body, SECRET)
159
152
  assert (ok, why) == (True, "ok")
160
153
 
161
154
 
@@ -23,7 +23,6 @@ def check(cond, msg):
23
23
  def test_abstraction():
24
24
  print("\n【1】渠道抽象层")
25
25
  from foxclaw.channels import ChannelAdapter, InboundMessage, SendResult, ChannelRegistry, OutboundMessage
26
- # handle_raw 异常隔离:on_message 抛异常不冒泡
27
26
  class A(ChannelAdapter):
28
27
  CHANNEL = "t"
29
28
  def normalize(self, raw):
@@ -38,7 +37,6 @@ def test_abstraction():
38
37
  for txt in ["a", "bad", "c"]:
39
38
  ad.handle_raw({"text": txt})
40
39
  check(processed == ["a", "c"], "handle_raw 异常隔离(单条失败不影响其他)")
41
- # 注册表
42
40
  reg = ChannelRegistry(); reg.register(ad)
43
41
  check(reg.get("t") is ad, "渠道注册表")
44
42
  check(not reg.send("nope", OutboundMessage("c", "x")).ok, "未注册渠道发送失败")
@@ -69,14 +67,12 @@ def test_weixin():
69
67
  check(h["Authorization"].startswith("Bearer "), "Bearer token")
70
68
  check(int(base64.b64decode(h["X-WECHAT-UIN"]).decode()) >= 0, "X-WECHAT-UIN 格式")
71
69
  check(wx._build_client_version("2.4.3") == 132099, "ClientVersion 计算")
72
- # 标准化 + context_token 记忆
73
70
  msg = {"message_id": 1, "from_user_id": "u1", "session_id": "s",
74
71
  "message_type": MSG_FROM_USER, "context_token": "ctx1",
75
72
  "item_list": [{"type": ITEM_TEXT, "text_item": {"text": "你好"}}]}
76
73
  inb = wx._normalize_msg(msg)
77
74
  check(inb.text == "你好", "消息标准化")
78
75
  check(wx._context_tokens.get("u1") == "ctx1", "记住 context_token")
79
- # bot 回显忽略
80
76
  check(wx._normalize_msg({"message_type": 2, "item_list": []}) is None, "bot 回显忽略")
81
77
 
82
78
 
@@ -84,10 +80,8 @@ def test_feishu():
84
80
  print("\n【4】飞书")
85
81
  from foxclaw.channels.feishu import FeishuAdapter
86
82
  fs = FeishuAdapter(app_id="x", app_secret="y", encrypt_key="my_secret_key_123")
87
- # URL 验证
88
83
  r = fs.handle_event({"type": "url_verification", "challenge": "c1"})
89
84
  check(r.get("challenge") == "c1", "URL 验证 challenge")
90
- # 加密解密端到端
91
85
  def encrypt(plaintext, key_str):
92
86
  from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
93
87
  key = hashlib.sha256(key_str.encode()).digest()
@@ -100,10 +94,8 @@ def test_feishu():
100
94
  original = {"type": "event", "x": "测试"}
101
95
  dec = fs.decrypt_event(encrypt(json.dumps(original), "my_secret_key_123"))
102
96
  check(dec == original, "AES 加密解密端到端")
103
- # 错误密钥明确报错
104
97
  fs2 = FeishuAdapter(app_id="x", app_secret="y", encrypt_key="wrong")
105
98
  check(fs2.decrypt_event(encrypt(json.dumps(original), "my_secret_key_123")) is None, "错误密钥解密失败")
106
- # 消息标准化
107
99
  event = {"header": {"event_type": "im.message.receive_v1"},
108
100
  "event": {"message": {"message_id": "om1", "chat_id": "oc1", "chat_type": "p2p",
109
101
  "content": json.dumps({"text": "查日程"}), "create_time": "1718900000000"},
@@ -141,7 +133,7 @@ def test_closed_loop():
141
133
 
142
134
  import tempfile, shutil
143
135
  data_dir = tempfile.mkdtemp(prefix="foxclaw_ch_test_")
144
- shutil.rmtree(data_dir, ignore_errors=True) # 确保干净起步(防残留幂等记录污染)
136
+ shutil.rmtree(data_dir, ignore_errors=True)
145
137
  os_ = Foxclaw(data_dir=data_dir, shadow_rounds=50)
146
138
  disp = ChannelDispatcher(os_.supervisor)
147
139
  mock = Mock(); disp.register_channel(mock)
@@ -151,12 +143,10 @@ def test_closed_loop():
151
143
  sender_id="u1", conversation_id="c1")
152
144
  r1 = await disp.handle(msg)
153
145
  check(r1["replied"], "闭环:收→主管→回")
154
- # 幂等:同消息重发只处理一次
155
146
  r2 = await disp.handle(msg)
156
147
  check(r2.get("idempotent_skip"), "重发消息被幂等跳过")
157
148
  check(len(mock.sent) == 1, "重发只回复一次")
158
149
  asyncio.run(go())
159
- # 清理
160
150
  import shutil
161
151
  shutil.rmtree(data_dir, ignore_errors=True)
162
152
 
@@ -9,7 +9,6 @@ def test_config_layering():
9
9
 
10
10
  from foxclaw.core.config import LayeredConfig, load_policy_layer
11
11
 
12
- # 1) LayeredConfig:标量高层覆盖;集合三层并集(只增不减);policy_locked
13
12
  cfg = LayeredConfig(policy={"model": "P", "deny": ["a"]},
14
13
  user={"model": "U", "deny": ["b"]},
15
14
  runtime={"model": "R", "deny": ["c"], "x": 1})
@@ -19,7 +18,6 @@ def test_config_layering():
19
18
  assert cfg.policy_locked("deny") == {"a"}, "policy 层锁定项"
20
19
  print("[1] LayeredConfig 标量覆盖 + 集合并集 + policy_locked OK")
21
20
 
22
- # 2) load_policy_layer:env + policy.json 合并;默认空
23
21
  d = tempfile.mkdtemp(prefix="e1_")
24
22
  try:
25
23
  os.environ["FOXCLAW_POLICY_DENY"] = "gmail_send, danger_tool"
@@ -28,7 +26,6 @@ def test_config_layering():
28
26
  assert set(pl["deny"]) == {"gmail_send", "danger_tool", "file_delete"}, pl
29
27
  print(f"[2] load_policy_layer(env + policy.json) = {pl['deny']} OK")
30
28
 
31
- # 3) SkillPolicy:policy 层 deny 生效 + undeny 撤不掉 + 从盘 user deny 生效(引用未断) + user 可撤
32
29
  (Path(d) / "skill_policy.json").write_text(
33
30
  json.dumps({"deny": ["user_denied"], "allow": []}), encoding="utf-8")
34
31
  from foxclaw.skills.policy import SkillPolicy
@@ -45,7 +42,6 @@ def test_config_layering():
45
42
  os.environ.pop("FOXCLAW_POLICY_DENY", None)
46
43
  shutil.rmtree(d, ignore_errors=True)
47
44
 
48
- # 4) behavior-preserving:无 policy 层(无 env、无 policy.json)→ SkillPolicy 行为 = 现状
49
45
  d2 = tempfile.mkdtemp(prefix="e1b_")
50
46
  try:
51
47
  from foxclaw.skills.policy import SkillPolicy
@@ -25,15 +25,12 @@ from foxclaw.skills import pipeline as sk_pipeline
25
25
  SESSION = "sess-1"
26
26
 
27
27
 
28
- # ── broker 单元 ────────────────────────────────────────────
29
-
30
28
  def test_issue_consume_single_use() -> None:
31
29
  b = ConfirmationBroker(ttl=300, clock=lambda: 1000.0)
32
30
  ch = b.issue(SESSION, "skill:mail_send", {"to": "a@x"})
33
31
  assert ch["nonce"] and ch["expires_at"] == 1300.0
34
32
  assert b.consume(SESSION, ch["nonce"], "skill:mail_send", {"to": "a@x"},
35
33
  now=1000.0) == (True, "ok")
36
- # 第二次:已消费 → 拒绝(单次使用)
37
34
  assert b.consume(SESSION, ch["nonce"], "skill:mail_send", {"to": "a@x"},
38
35
  now=1000.0) == (False, "unknown_nonce")
39
36
 
@@ -48,12 +45,10 @@ def test_expired_nonce_rejected() -> None:
48
45
  def test_args_and_action_drift_rejected() -> None:
49
46
  b = ConfirmationBroker(clock=lambda: 1000.0)
50
47
  ch = b.issue(SESSION, "skill:mail_send", {"to": "a@x"})
51
- # 确认"发给 A"却执行"发给 B" → 指纹不匹配
52
48
  assert b.consume(SESSION, ch["nonce"], "skill:mail_send", {"to": "b@x"},
53
49
  now=1000.0) == (False, "args_mismatch")
54
50
  assert b.consume(SESSION, ch["nonce"], "skill:pay", {"to": "a@x"},
55
51
  now=1000.0) == (False, "action_mismatch")
56
- # 原动作原参数仍可用(前两次失败没有消费掉它)
57
52
  assert b.consume(SESSION, ch["nonce"], "skill:mail_send", {"to": "a@x"},
58
53
  now=1000.0) == (True, "ok")
59
54
 
@@ -83,8 +78,6 @@ def test_pending_is_bounded() -> None:
83
78
  assert b.pending("nobody") == []
84
79
 
85
80
 
86
- # ── adapter 集成 ───────────────────────────────────────────
87
-
88
81
  class _Calls:
89
82
  def __init__(self):
90
83
  self.n = 0
@@ -118,7 +111,6 @@ def test_send_requires_nonce_even_when_confirmed():
118
111
  assert first.confirm_nonce and first.confirm_expires_at > 0
119
112
  assert calls.n == 0
120
113
 
121
- # 只有 confirmed=True 没有确认码 → 不放行(退回待确认,换发新码)
122
114
  no_nonce = adapter.invoke(skill, args, confirmed=True)
123
115
  assert no_nonce.status == "needs_confirmation" and calls.n == 0
124
116
  assert "missing_nonce" in no_nonce.message
@@ -128,7 +120,6 @@ def test_send_requires_nonce_even_when_confirmed():
128
120
  confirm_nonce=no_nonce.confirm_nonce)
129
121
  assert ok.success and calls.n == 1
130
122
 
131
- # 重放同一个码 → 拒绝,不执行
132
123
  replay = adapter.invoke(skill, args, confirmed=True,
133
124
  confirm_nonce=no_nonce.confirm_nonce)
134
125
  assert replay.status == "needs_confirmation" and calls.n == 1
@@ -156,13 +147,11 @@ def test_read_skill_and_no_broker_unaffected():
156
147
  adapter, reg, calls = _build(ConfirmationBroker())
157
148
  assert adapter.invoke(reg.get("calc"), {}, confirmed=True).success
158
149
 
159
- plain = SkillAdapter(reg) # 未注入 broker → 旧语义
150
+ plain = SkillAdapter(reg)
160
151
  assert plain.invoke(reg.get("mail_send"), {"to": "a@x"}, confirmed=True).success
161
152
  assert calls.n >= 1
162
153
 
163
154
 
164
- # ── pipeline 兼容 ──────────────────────────────────────────
165
-
166
155
  def test_pipeline_invoke_tolerates_old_adapter():
167
156
  """测试/第三方 adapter 只实现 4 参 invoke 时自动降级(签名探测,不吞内部 TypeError)。"""
168
157
  seen = {}
@@ -26,8 +26,6 @@ def _long_history(n: int = 12, chars: int = 400) -> list:
26
26
  for i in range(n)]
27
27
 
28
28
 
29
- # ── 1. Compactor.maybe_compact 契约 ─────────────────────────────
30
-
31
29
  def test_maybe_compact_noop_when_under_budget() -> None:
32
30
  msgs = [{"role": "user", "content": "短问题"}]
33
31
  out, res = Compactor(router=None, max_tokens=6000).maybe_compact(msgs)
@@ -57,8 +55,6 @@ def test_maybe_compact_never_raises_on_garbage() -> None:
57
55
  assert isinstance(out, list)
58
56
 
59
57
 
60
- # ── 2. CognitiveAgent._compact_context 接入层 ────────────────────────
61
-
62
58
  def _stub_agent(thoughts: list) -> SimpleNamespace:
63
59
  return SimpleNamespace(router=None,
64
60
  _think=lambda **kw: thoughts.append(kw.get("content", "")))
@@ -84,10 +80,8 @@ def test_compact_context_swallows_errors(monkeypatch) -> None:
84
80
  monkeypatch.setenv("FOXCLAW_CONTEXT_MAX_TOKENS", "100")
85
81
  msgs = _long_history()
86
82
  out = CognitiveAgent._compact_context(SimpleNamespace(router=None), msgs, "chat")
87
- assert isinstance(out, list) # _think 缺失 → 内部异常被吞,不影响主流程
88
-
83
+ assert isinstance(out, list)
89
84
 
90
- # ── 3. 工具调用循环里确实过闸 ───────────────────────────────────
91
85
 
92
86
  class _NoToolRouter:
93
87
  """supports_tools=True 但模型直接给答案:只跑一轮,便于断言压缩闸被调用。"""
@@ -27,12 +27,10 @@ async def main():
27
27
  print("=" * 60)
28
28
  os_ = Foxclaw(data_dir=tempfile.mkdtemp(prefix="foxclaw_dispatch_"), shadow_rounds=20)
29
29
 
30
- # 1. 主管调度不同类型任务,验证路由
31
30
  print("\n【1】主管真实调度(关键词路由)")
32
31
  routing_cases = [
33
32
  ("分析本月家庭支出预算", "finance"),
34
33
  ("学习一篇关于 RAG 的论文", "office"),
35
- # 行政并入办公(日程/会议走 office);"整理邮件"不再有助手 Agent,归办公
36
34
  ("安排周三下午的项目会议", "office"),
37
35
  ("帮我整理今天的邮件要点", "office"),
38
36
  ]
@@ -46,19 +44,16 @@ async def main():
46
44
  print(f" {'✓' if ok else '✗'} 「{task[:18]}」→ {dispatched} (预期 {expected})")
47
45
  check(not bad_routes, f"关键词路由 4/4 命中(失配: {bad_routes or '无'})")
48
46
 
49
- # 2. 结果汇聚
50
47
  print("\n【2】结果汇聚")
51
48
  r = await os_.supervisor.run_task("评估我的投资组合风险")
52
49
  res = r.get("result", {})
53
50
  print(f" 派给: {res.get('dispatched_to')}")
54
51
  print(f" 汇聚: {str(res.get('sub_result'))[:50]}...")
55
52
  print(f" 主管摘要: {res.get('summary', '')}")
56
- # 主管必须真的派出去并拿回子结果,而不是空转返回一句套话
57
53
  check(res.get("dispatched_to") == "finance",
58
54
  f"投资组合风险任务派给 finance(实为 {res.get('dispatched_to')})")
59
55
  check(bool(str(res.get("sub_result") or "").strip()), "子 Agent 结果已汇聚回主管(非空)")
60
56
 
61
- # 3. 共享记忆:其他 Agent 能召回主管的调度记录
62
57
  print("\n【3】共享记忆层(所有 Agent 可见)")
63
58
  empty_shared = []
64
59
  for viewer in ["office", "finance", "supervisor"]:
@@ -69,7 +64,6 @@ async def main():
69
64
  print(f" {viewer:<10} 召回共享调度记录: {len(shared_hits)} 条")
70
65
  check(not empty_shared, f"三个 Agent 都能召回共享调度记录(召回为空: {empty_shared or '无'})")
71
66
 
72
- # 4. 隔离仍成立:私有记忆不跨 Agent
73
67
  print("\n【4】私有记忆隔离(共享层不打破隔离)")
74
68
  await os_.finance.run_task("记录我的私人理财偏好")
75
69
  office_view = os_.hmr.recall(query="理财 偏好", agent_id="office", top_k=10)