foxclaw 0.1.2 → 0.1.4
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.
- package/index.js +1 -8
- package/package.json +1 -1
- package/python/.env.example +8 -0
- package/python/.github/workflows/ci.yml +47 -2
- package/python/.github/workflows/release.yml +5 -0
- package/python/CHANGELOG.md +100 -0
- package/python/CHANGELOG_CN.md +100 -0
- package/python/README.md +17 -2
- package/python/README_CN.md +38 -20
- package/python/bench/bm25_audit.py +1 -14
- package/python/bench/bm25_regression.py +0 -1
- package/python/bench/bm25_stress.py +0 -4
- package/python/bench/cd_audit.py +0 -23
- package/python/bench/chaos.py +6 -8
- package/python/bench/e2e_real_models.py +0 -1
- package/python/bench/llm_retry_test.py +2 -9
- package/python/bench/load_test.py +5 -8
- package/python/bench/vector_ivf_upgrade.py +1 -7
- package/python/docs/CONTENT_GUARD.md +189 -0
- package/python/docs/CONTENT_GUARD_CN.md +189 -0
- package/python/docs/RELEASE.md +78 -37
- package/python/docs/RUN_CN.md +2 -2
- package/python/evals/governance/cases.py +0 -37
- package/python/evals/governance/run_evals.py +5 -37
- package/python/examples/governance_middleware_demo.py +1 -6
- package/python/examples/langgraph_governance_demo.py +7 -11
- package/python/foxclaw/__init__.py +74 -152
- package/python/foxclaw/agents/base.py +82 -136
- package/python/foxclaw/agents/impl/_helpers.py +2 -10
- package/python/foxclaw/agents/impl/finance.py +4 -21
- package/python/foxclaw/agents/impl/office.py +7 -24
- package/python/foxclaw/agents/impl/supervisor.py +12 -79
- package/python/foxclaw/agents/mixins/_context.py +0 -23
- package/python/foxclaw/agents/mixins/augment.py +10 -16
- package/python/foxclaw/agents/mixins/llm.py +7 -6
- package/python/foxclaw/agents/mixins/memory.py +23 -3
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +59 -12
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/agents/rules/finance/RULES.md +131 -0
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/bootstrap.py +114 -0
- package/python/foxclaw/api/middleware.py +5 -28
- package/python/foxclaw/api/routes/ahs.py +60 -73
- package/python/foxclaw/api/routes/attachments.py +5 -6
- package/python/foxclaw/api/routes/browser.py +7 -11
- package/python/foxclaw/api/routes/domain.py +18 -26
- package/python/foxclaw/api/routes/knowledge.py +3 -21
- package/python/foxclaw/api/routes/llm_admin.py +21 -33
- package/python/foxclaw/api/routes/memory.py +50 -12
- package/python/foxclaw/api/routes/orchestration.py +3 -5
- package/python/foxclaw/api/routes/recovery.py +4 -3
- package/python/foxclaw/api/routes/review.py +2 -11
- package/python/foxclaw/api/routes/search.py +3 -7
- package/python/foxclaw/api/routes/system.py +33 -49
- package/python/foxclaw/api/routes/task.py +31 -49
- package/python/foxclaw/api/routes/ws.py +2 -6
- package/python/foxclaw/api/spine.py +1 -4
- package/python/foxclaw/behavior/__init__.py +20 -0
- package/python/foxclaw/behavior/behavior.yaml +62 -0
- package/python/foxclaw/behavior/spec.py +287 -0
- package/python/foxclaw/browser/action/action_executor.py +1 -5
- package/python/foxclaw/browser/action/action_types.py +0 -1
- package/python/foxclaw/browser/action/url_policy.py +1 -5
- package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
- package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
- package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
- package/python/foxclaw/browser/perception/element_indexer.py +1 -2
- package/python/foxclaw/browser/service.py +7 -11
- package/python/foxclaw/capability/embedding_service.py +4 -6
- package/python/foxclaw/channels/__init__.py +9 -11
- package/python/foxclaw/channels/discord.py +4 -13
- package/python/foxclaw/channels/dispatcher.py +2 -27
- package/python/foxclaw/channels/feishu.py +10 -20
- package/python/foxclaw/channels/openclaw_bridge.py +4 -9
- package/python/foxclaw/channels/signing.py +9 -9
- package/python/foxclaw/channels/telegram.py +3 -4
- package/python/foxclaw/channels/weixin.py +7 -17
- package/python/foxclaw/cli.py +42 -13
- package/python/foxclaw/cognitive/reasoner.py +9 -10
- package/python/foxclaw/cognitive/state.py +18 -24
- package/python/foxclaw/core/audit_chain.py +1 -3
- package/python/foxclaw/core/audit_sink.py +3 -3
- package/python/foxclaw/core/compaction.py +8 -26
- package/python/foxclaw/core/config.py +2 -2
- package/python/foxclaw/core/confirm_nonce.py +6 -12
- package/python/foxclaw/core/content_guard.py +341 -0
- package/python/foxclaw/core/envfile.py +2 -2
- package/python/foxclaw/core/failure_policy.py +17 -26
- package/python/foxclaw/core/gateway.py +6 -10
- package/python/foxclaw/core/governance_audit.py +3 -7
- package/python/foxclaw/core/idempotency.py +15 -22
- package/python/foxclaw/core/injection_guard.py +10 -29
- package/python/foxclaw/core/memory_adapter.py +53 -60
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +74 -150
- package/python/foxclaw/core/paths.py +1 -1
- package/python/foxclaw/core/recovery.py +21 -34
- package/python/foxclaw/core/scheduler.py +23 -26
- package/python/foxclaw/core/secrets.py +0 -3
- package/python/foxclaw/core/sensitive_write.py +283 -0
- package/python/foxclaw/core/sessions.py +4 -6
- package/python/foxclaw/core/settings.py +16 -32
- package/python/foxclaw/core/shared_write.py +11 -23
- package/python/foxclaw/core/steering_queue.py +12 -11
- package/python/foxclaw/core/system_bus.py +1 -1
- package/python/foxclaw/core/thought_chain.py +30 -32
- package/python/foxclaw/core/token_meter.py +9 -13
- package/python/foxclaw/core/tools.py +4 -12
- package/python/foxclaw/core/tracing.py +25 -40
- package/python/foxclaw/core/truth_pipeline.py +5 -9
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +11 -37
- package/python/foxclaw/core/verify.py +6 -14
- package/python/foxclaw/core/web_search.py +4 -28
- package/python/foxclaw/finance/evidence.py +14 -18
- package/python/foxclaw/finance/fx.py +1 -3
- package/python/foxclaw/finance/graph.py +9 -18
- package/python/foxclaw/finance/models.py +20 -24
- package/python/foxclaw/finance/nlparse.py +12 -72
- package/python/foxclaw/finance/policy.py +8 -36
- package/python/foxclaw/finance/reasoner.py +3 -11
- package/python/foxclaw/finance/reasoning.py +13 -34
- package/python/foxclaw/finance/reconcile.py +5 -10
- package/python/foxclaw/finance/runtime.py +19 -46
- package/python/foxclaw/governance/middleware.py +13 -21
- package/python/foxclaw/harness/_base.py +2 -8
- package/python/foxclaw/harness/_policy.py +19 -38
- package/python/foxclaw/harness/drift.py +1 -13
- package/python/foxclaw/harness/event_bus.py +2 -5
- package/python/foxclaw/harness/harness.py +9 -97
- package/python/foxclaw/harness/loop_detection.py +11 -18
- package/python/foxclaw/itinerary/amap_client.py +2 -5
- package/python/foxclaw/itinerary/models.py +7 -7
- package/python/foxclaw/itinerary/reasoner.py +1 -6
- package/python/foxclaw/itinerary/reasoning.py +15 -21
- package/python/foxclaw/itinerary/runtime.py +8 -12
- package/python/foxclaw/kernel/capabilities.py +5 -6
- package/python/foxclaw/kernel/policy_gate.py +4 -10
- package/python/foxclaw/kernel/process_manager.py +17 -36
- package/python/foxclaw/kernel/scheduler.py +3 -9
- package/python/foxclaw/kernel/sentinel.py +13 -23
- package/python/foxclaw/kernel/sentinel_client.py +12 -15
- package/python/foxclaw/kernel/spine.py +9 -9
- package/python/foxclaw/knowledge/__init__.py +4 -20
- package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
- package/python/foxclaw/learning/knowledge_graph.py +4 -9
- package/python/foxclaw/learning/models.py +13 -13
- package/python/foxclaw/learning/reasoner.py +1 -4
- package/python/foxclaw/learning/runtime.py +19 -31
- package/python/foxclaw/learning/spaced_repetition.py +2 -7
- package/python/foxclaw/mcp_server.py +2 -3
- package/python/foxclaw/orchestration/__init__.py +16 -33
- package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
- package/python/foxclaw/planning/llm_planner.py +3 -3
- package/python/foxclaw/planning/planner.py +6 -11
- package/python/foxclaw/search/agent_port.py +7 -11
- package/python/foxclaw/search/evidence.py +0 -4
- package/python/foxclaw/search/internal_search.py +3 -5
- package/python/foxclaw/search/models.py +11 -13
- package/python/foxclaw/search/planner.py +3 -3
- package/python/foxclaw/search/retriever.py +1 -1
- package/python/foxclaw/search/runtime.py +1 -8
- package/python/foxclaw/skills/__init__.py +46 -100
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +21 -25
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +16 -42
- package/python/foxclaw/skills/markdown_skill.py +10 -9
- package/python/foxclaw/skills/mcp_client.py +5 -11
- package/python/foxclaw/skills/mcp_store.py +0 -3
- package/python/foxclaw/skills/permission.py +13 -48
- package/python/foxclaw/skills/pipeline.py +21 -53
- package/python/foxclaw/skills/policy.py +4 -10
- package/python/foxclaw/skills/review.py +3 -3
- package/python/frontend/deck/auth.js +0 -7
- package/python/frontend/deck/chat_model_picker.js +0 -54
- package/python/frontend/deck/domain.js +0 -20
- package/python/frontend/deck/main.js +0 -144
- package/python/frontend/deck/markdown.js +0 -4
- package/python/frontend/deck/settings.js +1 -18
- package/python/frontend/deck/state.js +0 -1
- package/python/frontend/deck/util.js +0 -2
- package/python/frontend/deck/visuals.js +0 -22
- package/python/frontend/planet/planet.js +18 -201
- package/python/frontend/v3/assets/index-JKfHlb-a.css +1 -0
- package/python/frontend/v3/assets/index-tN90dpUH.js +34 -0
- package/python/frontend/v3/index.html +3 -3
- package/python/frontend/widget_runtime.js +0 -43
- package/python/frontend/widgets.js +0 -57
- package/python/frontend_src/index.html +1 -1
- package/python/frontend_src/package-lock.json +2 -2
- package/python/frontend_src/package.json +1 -1
- package/python/frontend_src/src/App.vue +3 -7
- package/python/frontend_src/src/api.js +0 -7
- package/python/frontend_src/src/components/ChatDock.vue +27 -39
- package/python/frontend_src/src/components/DocViewer.vue +0 -9
- package/python/frontend_src/src/components/Inspector.vue +0 -6
- package/python/frontend_src/src/components/LeftRail.vue +60 -39
- package/python/frontend_src/src/components/MenuPopover.vue +9 -11
- package/python/frontend_src/src/components/MessageStream.vue +18 -30
- package/python/frontend_src/src/components/ModelPicker.vue +0 -5
- package/python/frontend_src/src/components/QuickIngest.vue +0 -6
- package/python/frontend_src/src/components/Stage.vue +71 -25
- package/python/frontend_src/src/components/ToolMenu.vue +0 -3
- package/python/frontend_src/src/components/TopBar.vue +38 -10
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +42 -3
- package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
- package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
- package/python/frontend_src/src/lib/fmt.js +0 -4
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +0 -14
- package/python/frontend_src/src/main.js +2 -0
- package/python/frontend_src/src/stores/app.js +17 -32
- package/python/frontend_src/src/stores/chat.js +0 -34
- package/python/frontend_src/src/stores/model.js +0 -8
- package/python/frontend_src/src/stores/panels.js +16 -44
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/vite.config.js +0 -7
- package/python/pyproject.toml +17 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +4 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- package/python/tests/test_agent_rules.py +81 -0
- package/python/tests/test_ahs_p2.py +4 -25
- package/python/tests/test_anthropic_tools.py +1 -15
- package/python/tests/test_api.py +19 -9
- package/python/tests/test_api_auth.py +1 -5
- package/python/tests/test_attachments.py +9 -25
- package/python/tests/test_audit_chain.py +2 -7
- package/python/tests/test_audit_sink.py +0 -6
- package/python/tests/test_behavior_spec.py +198 -0
- package/python/tests/test_browser.py +2 -6
- package/python/tests/test_browser_request_guard.py +0 -4
- package/python/tests/test_capabilities.py +0 -6
- package/python/tests/test_channel_signing.py +2 -9
- package/python/tests/test_channels.py +1 -11
- package/python/tests/test_config_layering.py +0 -4
- package/python/tests/test_confirm_nonce.py +1 -12
- package/python/tests/test_conflict_dismiss.py +143 -0
- package/python/tests/test_content_guard.py +177 -0
- package/python/tests/test_context_compaction_wiring.py +1 -7
- package/python/tests/test_dispatch.py +0 -6
- package/python/tests/test_domain_evals.py +11 -80
- package/python/tests/test_domain_import.py +0 -6
- package/python/tests/test_domain_reasoners.py +0 -14
- package/python/tests/test_e2e.py +0 -17
- package/python/tests/test_emb_cache.py +7 -17
- package/python/tests/test_embedding_singleton.py +1 -3
- package/python/tests/test_evals_smoke.py +0 -24
- package/python/tests/test_event_spine.py +10 -18
- package/python/tests/test_evidence_semantic.py +1 -14
- package/python/tests/test_evidence_sse.py +3 -6
- package/python/tests/test_file_ingest.py +0 -11
- package/python/tests/test_finance.py +0 -9
- package/python/tests/test_finance_ledger.py +5 -12
- package/python/tests/test_finance_persist.py +8 -12
- package/python/tests/test_gateway.py +3 -22
- package/python/tests/test_governance.py +5 -52
- package/python/tests/test_governance_audit.py +4 -39
- package/python/tests/test_governance_middleware.py +1 -7
- package/python/tests/test_hkc.py +6 -24
- package/python/tests/test_hkc_index_build_manager.py +0 -33
- package/python/tests/test_hkc_manifest_reuse.py +4 -27
- package/python/tests/test_hkc_search_event_sync.py +3 -54
- package/python/tests/test_import_path_guard.py +0 -6
- package/python/tests/test_injection_guard.py +18 -36
- package/python/tests/test_integration.py +5 -29
- package/python/tests/test_itinerary.py +0 -3
- package/python/tests/test_kee_semantic.py +0 -5
- package/python/tests/test_kernel.py +1 -11
- package/python/tests/test_learning.py +3 -12
- package/python/tests/test_llm_admin.py +4 -19
- package/python/tests/test_llm_planner.py +1 -23
- package/python/tests/test_markdown_skills.py +3 -14
- package/python/tests/test_markdown_skills_openclaw.py +4 -9
- package/python/tests/test_mcp_connectors.py +3 -15
- package/python/tests/test_mcp_guardian_discovery.py +2 -6
- package/python/tests/test_mcp_no_memory.py +3 -3
- package/python/tests/test_mcp_policy_gate.py +0 -1
- package/python/tests/test_mcp_server.py +6 -9
- package/python/tests/test_memory_recall.py +1 -4
- package/python/tests/test_metrics.py +0 -8
- package/python/tests/test_multi_chain.py +1 -13
- package/python/tests/test_multimodal.py +5 -18
- package/python/tests/test_negotiation.py +3 -13
- package/python/tests/test_orchestration.py +1 -11
- package/python/tests/test_orchestration_decoupled.py +0 -6
- package/python/tests/test_p3_p5_hardening.py +3 -17
- package/python/tests/test_parallel_dispatch.py +4 -12
- package/python/tests/test_pipeline.py +17 -58
- package/python/tests/test_plan_visible.py +4 -4
- package/python/tests/test_planner.py +0 -7
- package/python/tests/test_policy_gate.py +2 -3
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +2 -17
- package/python/tests/test_release_check.py +2 -13
- package/python/tests/test_resume_checkpoint.py +7 -15
- package/python/tests/test_routing.py +2 -12
- package/python/tests/test_scheduler.py +7 -10
- package/python/tests/test_search.py +1 -11
- package/python/tests/test_secrets.py +4 -10
- package/python/tests/test_semantic.py +1 -10
- package/python/tests/test_sensitive_write.py +181 -0
- package/python/tests/test_sessions.py +3 -4
- package/python/tests/test_settings.py +2 -18
- package/python/tests/test_shadow_learning.py +3 -43
- package/python/tests/test_shared_write.py +0 -7
- package/python/tests/test_skill_taint_gate.py +1 -10
- package/python/tests/test_skills.py +2 -18
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +2 -5
- package/python/tests/test_streaming.py +2 -7
- package/python/tests/test_supervisor_pipeline.py +4 -5
- package/python/tests/test_supervisor_triage.py +4 -10
- package/python/tests/test_taint.py +2 -7
- package/python/tests/test_task_stream.py +1 -12
- package/python/tests/test_thinking_sse.py +0 -1
- package/python/tests/test_tool_center.py +1 -17
- package/python/tests/test_tool_parallel.py +2 -2
- package/python/tests/test_tool_params.py +1 -5
- package/python/tests/test_tool_use.py +0 -13
- package/python/tests/test_toplevel_concurrency.py +3 -4
- package/python/tests/test_tracing.py +7 -41
- package/python/tests/test_truth_crystallizer.py +4 -6
- package/python/tests/test_truth_flow.py +2 -7
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_verify_evidence.py +8 -21
- package/python/tests/test_version_sync.py +35 -6
- package/python/tests/test_web_trigger.py +0 -17
- package/python/tools/build_content_lexicon.py +218 -0
- package/python/tools/bundle_web.py +98 -0
- package/python/tools/check_faiss_type.py +0 -1
- package/python/tools/check_log.ps1 +0 -8
- package/python/tools/mypy_baseline.txt +12 -4
- package/python/tools/mypy_ratchet.py +52 -6
- package/python/tools/pack.py +0 -5
- package/python/tools/recheck_conflicts.py +156 -0
- package/python/tools/release_check.py +24 -15
- package/python/tools/restart_foxclaw.ps1 +5 -8
- package/python/tools/shared_write_audit.py +1 -15
- package/python/tools/size_check.py +1 -1
- package/python/tools/strip_comments.py +228 -0
- package/python/vendor/hkc/hkc_api/routes/ability.py +31 -0
- package/python/vendor/hkc/hkc_api/schemas.py +11 -0
- package/python/vendor/hkc/hkc_kee/kee.py +124 -3
- package/python/vendor/hkc/hkc_sdk/base.py +22 -0
- package/python/vendor/hkc/hkc_sdk/direct_client.py +10 -0
- package/python/vendor/hkc/hkc_sdk/http_client.py +19 -0
- package/python/foxclaw/api/app.py +0 -123
- package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- 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"
|
|
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))
|
|
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})
|
|
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
|
-
|
|
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()})
|
|
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)
|
|
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
|
|
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美元")
|
|
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()
|
|
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"},
|
|
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"
|
|
13
|
+
os.environ["FOXCLAW_GATEWAY_RATE"] = "5"
|
|
20
14
|
os.environ["FOXCLAW_GATEWAY_QUOTA"] = "1000"
|
|
21
|
-
os.environ["FOXCLAW_MAX_BODY_MB"] = "1"
|
|
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)
|
|
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"
|