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.
- 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/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/README.md +11 -2
- package/python/README_CN.md +32 -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/RELEASE.md +4 -3
- 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 +34 -152
- package/python/foxclaw/agents/base.py +65 -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 +7 -1
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +0 -6
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
- 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 +28 -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/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/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 +30 -57
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +36 -148
- 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/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 +0 -201
- package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -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 +0 -1
- 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 +0 -41
- 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 +6 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +3 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- 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_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_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_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/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/release_check.py +7 -7
- 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/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""README 与仓库实际的一致性巡检。
|
|
2
|
+
|
|
3
|
+
README 里的结构和数字曾经长期漂移:目录注释写「五智能体」而实际三个、
|
|
4
|
+
写「76 个测试文件」而实际 110、写「77 个依赖」而实际 96 条。
|
|
5
|
+
这类不一致第一眼就被识破,所以把可机器核对的部分钉在这里:
|
|
6
|
+
改了代码不改文档,这个文件会红。
|
|
7
|
+
|
|
8
|
+
刻意不钉的东西:测试文件个数、代码行数、依赖条目数——它们每次提交都在变,
|
|
9
|
+
写进 README 就是下一个「76」。需要真实数字时用命令现取,不抄文档。
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
19
|
+
README_EN = ROOT / "README.md"
|
|
20
|
+
README_CN = ROOT / "README_CN.md"
|
|
21
|
+
|
|
22
|
+
HKC_ROUTES = ROOT / "vendor" / "hkc" / "hkc_api" / "routes" / "knowledge.py"
|
|
23
|
+
KDE = ROOT / "vendor" / "hkc" / "hkc_kde" / "kde.py"
|
|
24
|
+
CHUNKER = ROOT / "vendor" / "hkc" / "hkc_kde" / "chunker.py"
|
|
25
|
+
EXTRACTOR = ROOT / "vendor" / "hkc" / "hkc_kde" / "extractor.py"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture(scope="module")
|
|
29
|
+
def docs() -> dict[str, str]:
|
|
30
|
+
return {p.name: p.read_text(encoding="utf-8") for p in (README_EN, README_CN)}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _num(path: Path, pattern: str) -> int:
|
|
34
|
+
m = re.search(pattern, path.read_text(encoding="utf-8"), re.M)
|
|
35
|
+
assert m, f"{path.relative_to(ROOT)} 中找不到模式 {pattern!r}"
|
|
36
|
+
return int(m.group(1))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_agents_are_three(docs: dict[str, str]) -> None:
|
|
40
|
+
impl = sorted(
|
|
41
|
+
p.stem
|
|
42
|
+
for p in (ROOT / "foxclaw" / "agents" / "impl").glob("*.py")
|
|
43
|
+
if not p.stem.startswith("_")
|
|
44
|
+
)
|
|
45
|
+
assert impl == ["finance", "office", "supervisor"], f"实际智能体实现: {impl}"
|
|
46
|
+
|
|
47
|
+
for name, text in docs.items():
|
|
48
|
+
assert "五智能体" not in text, f"{name} 仍写着五智能体"
|
|
49
|
+
assert "五个智能体" not in text, f"{name} 仍写着五个智能体"
|
|
50
|
+
assert "five agents" not in text.lower(), f"{name} 仍写着 five agents"
|
|
51
|
+
|
|
52
|
+
assert "Three agents" in docs["README.md"]
|
|
53
|
+
assert "三个智能体" in docs["README_CN.md"]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_channels_are_four() -> None:
|
|
57
|
+
for adapter in ("weixin", "feishu", "telegram", "discord"):
|
|
58
|
+
assert (ROOT / "foxclaw" / "channels" / f"{adapter}.py").exists(), adapter
|
|
59
|
+
assert (ROOT / "foxclaw" / "channels" / "dispatcher.py").exists()
|
|
60
|
+
|
|
61
|
+
text = README_EN.read_text(encoding="utf-8")
|
|
62
|
+
for brand in ("WeChat", "Feishu", "Telegram", "Discord"):
|
|
63
|
+
assert brand in text, brand
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_runtime_governance_modules_exist() -> None:
|
|
67
|
+
for rel in (
|
|
68
|
+
"foxclaw/core/idempotency.py",
|
|
69
|
+
"foxclaw/core/recovery.py",
|
|
70
|
+
"foxclaw/harness/loop_detection.py",
|
|
71
|
+
"foxclaw/core/compaction.py",
|
|
72
|
+
"foxclaw/core/steering_queue.py",
|
|
73
|
+
"docs/GOVERNANCE.md",
|
|
74
|
+
):
|
|
75
|
+
assert (ROOT / rel).exists(), rel
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_ingest_limits_match_docs(docs: dict[str, str]) -> None:
|
|
79
|
+
upload_mb = _num(HKC_ROUTES, r"MAX_UPLOAD_BYTES\s*=\s*(\d+)\s*\*\s*1024\s*\*\s*1024")
|
|
80
|
+
assert upload_mb == 20, f"单文件上传上限已变为 {upload_mb}MB,README 写的 20MB 要改"
|
|
81
|
+
|
|
82
|
+
assert _num(HKC_ROUTES, r"MAX_INGEST_CHARS\s*=\s*([\d_]+)") == 2_000_000
|
|
83
|
+
assert _num(KDE, r"DEFAULT_MAX_CHUNKS_PER_DOC\s*=\s*(\d+)") == 800
|
|
84
|
+
assert _num(EXTRACTOR, r'"HKC_EXTRACT_MAX_TOKENS",\s*"(\d+)"') == 8192
|
|
85
|
+
|
|
86
|
+
for name, text in docs.items():
|
|
87
|
+
assert "20 MB" in text or "20MB" in text, name
|
|
88
|
+
assert "2,000,000" in text, name
|
|
89
|
+
assert "800" in text, name
|
|
90
|
+
assert "8192" in text, name
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_chunk_geometry_matches_docs(docs: dict[str, str]) -> None:
|
|
94
|
+
max_tokens = _num(CHUNKER, r"^MAX_TOKENS\s*=\s*(\d+)")
|
|
95
|
+
overlap_tokens = _num(CHUNKER, r"^OVERLAP_TOKENS\s*=\s*(\d+)")
|
|
96
|
+
m = re.search(r"^CHARS_PER_TOKEN\s*=\s*([\d.]+)", CHUNKER.read_text(encoding="utf-8"), re.M)
|
|
97
|
+
assert m, "chunker.py 中找不到 CHARS_PER_TOKEN"
|
|
98
|
+
chars_per_token = float(m.group(1))
|
|
99
|
+
|
|
100
|
+
max_chars = int(max_tokens * chars_per_token)
|
|
101
|
+
overlap_chars = int(overlap_tokens * chars_per_token)
|
|
102
|
+
step = max_chars - overlap_chars
|
|
103
|
+
|
|
104
|
+
assert (max_chars, overlap_chars, step) == (1792, 224, 1568), (
|
|
105
|
+
f"分块几何已变为 {max_chars}/{overlap_chars}/{step},README 写的 1792/224/1568 要改"
|
|
106
|
+
)
|
|
107
|
+
for name, text in docs.items():
|
|
108
|
+
for value in ("1792", "224", "1568"):
|
|
109
|
+
assert value in text, f"{name} 未出现 {value}"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_supported_formats_match_docs(docs: dict[str, str]) -> None:
|
|
113
|
+
src = HKC_ROUTES.read_text(encoding="utf-8")
|
|
114
|
+
m = re.search(r"ALLOWED_SUFFIXES\s*=\s*\((.*?)\)", src, re.S)
|
|
115
|
+
assert m, "knowledge.py 中找不到 ALLOWED_SUFFIXES"
|
|
116
|
+
suffixes = re.findall(r'"(\.\w+)"', m.group(1))
|
|
117
|
+
assert len(suffixes) == 10, f"支持格式已变为 {suffixes}"
|
|
118
|
+
|
|
119
|
+
for name, text in docs.items():
|
|
120
|
+
for suffix in suffixes:
|
|
121
|
+
assert suffix in text, f"{name} 未列出支持格式 {suffix}"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_relative_links_exist(docs: dict[str, str]) -> None:
|
|
125
|
+
for name, text in docs.items():
|
|
126
|
+
for target in re.findall(r"\]\(([^)#:]+)\)", text):
|
|
127
|
+
if target.startswith(("http", "mailto")):
|
|
128
|
+
continue
|
|
129
|
+
assert (ROOT / target).exists(), f"{name} 的链接指向不存在的 {target}"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_directory_tree_paths_exist() -> None:
|
|
133
|
+
for rel in (
|
|
134
|
+
"foxclaw/core",
|
|
135
|
+
"foxclaw/kernel",
|
|
136
|
+
"foxclaw/pipeline",
|
|
137
|
+
"foxclaw/planning",
|
|
138
|
+
"foxclaw/cognitive",
|
|
139
|
+
"foxclaw/knowledge",
|
|
140
|
+
"foxclaw/agents",
|
|
141
|
+
"foxclaw/harness",
|
|
142
|
+
"foxclaw/finance",
|
|
143
|
+
"foxclaw/learning",
|
|
144
|
+
"foxclaw/itinerary",
|
|
145
|
+
"foxclaw/channels",
|
|
146
|
+
"foxclaw/skills",
|
|
147
|
+
"foxclaw/search",
|
|
148
|
+
"foxclaw/browser",
|
|
149
|
+
"foxclaw/capability",
|
|
150
|
+
"foxclaw/governance",
|
|
151
|
+
"vendor/hmr",
|
|
152
|
+
"vendor/hkc",
|
|
153
|
+
"examples",
|
|
154
|
+
"tests",
|
|
155
|
+
"docs",
|
|
156
|
+
):
|
|
157
|
+
assert (ROOT / rel).is_dir(), f"目录树列出的 {rel} 不存在"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_referenced_skills_exist(docs: dict[str, str]) -> None:
|
|
161
|
+
for skill in ("general-assistant", "schedule-itinerary"):
|
|
162
|
+
assert (ROOT / "skills" / skill).is_dir(), skill
|
|
163
|
+
assert skill in docs["README_CN.md"], skill
|
|
@@ -35,11 +35,8 @@ def test_reasoner_base_order():
|
|
|
35
35
|
|
|
36
36
|
def _policy():
|
|
37
37
|
from foxclaw.finance import FinanceRuntime
|
|
38
|
-
# data_path 显式隔离:默认落在 policy 同目录(系统 Temp),会与其他测试共用一份数据文件
|
|
39
38
|
p = FinanceRuntime(policy_path=tempfile.mktemp(suffix=".yaml"),
|
|
40
39
|
data_path=tempfile.mktemp(suffix=".json")).policy
|
|
41
|
-
# 显式锁定本测试的前提阈值(200,模糊带 [140,200)),**不依赖产品默认值** ——
|
|
42
|
-
# 默认阈值是给用户按自己实际调的可调项,不该让调它就牵动这些验证推理逻辑的测试。
|
|
43
40
|
for r in p.rules:
|
|
44
41
|
if r.id == "large_purchase_check":
|
|
45
42
|
r.threshold = 200.0
|
|
@@ -52,12 +49,10 @@ def test_finance_gatekeeper_reasoner():
|
|
|
52
49
|
from foxclaw.finance.reasoner import FinanceGatekeeperReasoner
|
|
53
50
|
from foxclaw.finance.models import Transaction
|
|
54
51
|
r = FinanceGatekeeperReasoner(_policy())
|
|
55
|
-
# 小额(规则内)→ approve
|
|
56
52
|
rv = r.reason((Transaction(amount=-150, category="travel", entity="高铁", account="default"), 5000))
|
|
57
53
|
assert rv.decision == "approve" and rv.rationale == "财务批准:travel 预算 150 在规则内"
|
|
58
54
|
assert rv.source == "rules" and any(e.factor == "rules_clear" for e in rv.evidence)
|
|
59
55
|
assert rv.hypotheses, "应有假设"
|
|
60
|
-
# 大额(>阈值)→ confirm,证据=large_purchase_check
|
|
61
56
|
rv2 = r.reason((Transaction(amount=-680, category="travel", entity="酒店", account="default"), 5000))
|
|
62
57
|
assert rv2.decision == "confirm" and any(e.factor == "large_purchase_check" for e in rv2.evidence)
|
|
63
58
|
print(f" ✓ 小额→approve(证据 rules_clear);大额→confirm(证据 large_purchase_check;假设={rv2.hypotheses})")
|
|
@@ -71,7 +66,7 @@ def test_context_evidence_no_decision_change():
|
|
|
71
66
|
rt = FinanceRuntime(policy_path=tempfile.mktemp(suffix=".yaml"),
|
|
72
67
|
data_path=tempfile.mktemp(suffix=".json"))
|
|
73
68
|
rt.set_budget("travel", 3000); rt.set_balance("default", 5000)
|
|
74
|
-
for _ in range(3):
|
|
69
|
+
for _ in range(3):
|
|
75
70
|
rt.add_transaction(Transaction(amount=-100, category="travel", entity="高铁",
|
|
76
71
|
account="default"), run_gatekeeper=False)
|
|
77
72
|
r = FinanceGatekeeperReasoner(rt.policy, runtime=rt)
|
|
@@ -80,7 +75,6 @@ def test_context_evidence_no_decision_change():
|
|
|
80
75
|
assert rv.decision == "approve", "规则内 → 决策仍是 approve(情境证据不改裁决)"
|
|
81
76
|
factors = {e.factor for e in rv.evidence}
|
|
82
77
|
assert {"history_category", "budget_remaining", "entity_frequency"} <= factors, factors
|
|
83
|
-
# 无 runtime → 无情境证据(向后兼容;裁决相同)
|
|
84
78
|
rv0 = FinanceGatekeeperReasoner(rt.policy).reason(
|
|
85
79
|
(Transaction(amount=-150, category="travel", entity="高铁", account="default"), 5000))
|
|
86
80
|
assert not ({"history_category", "budget_remaining", "entity_frequency"}
|
|
@@ -103,41 +97,33 @@ def test_llm_arbitration_safety():
|
|
|
103
97
|
print("\n【2c】Step3:LLM 裁决的安全不变量(只增强模糊、只能 approve→confirm、永不 reject)")
|
|
104
98
|
from foxclaw.finance.reasoner import FinanceGatekeeperReasoner
|
|
105
99
|
from foxclaw.finance.models import Transaction
|
|
106
|
-
policy = _policy()
|
|
100
|
+
policy = _policy()
|
|
107
101
|
def txn(amt): return Transaction(amount=-amt, category="travel", entity="高铁", account="default")
|
|
108
102
|
def mk(llm, enabled=True): return FinanceGatekeeperReasoner(policy, llm=llm, llm_enabled=enabled)
|
|
109
103
|
|
|
110
|
-
# 1. 规则触发(680>200)→ 规则权威,LLM 绝不被调
|
|
111
104
|
llm = _MockLLM("APPROVE"); rv = mk(llm).reason((txn(680), 5000))
|
|
112
105
|
assert rv.decision == "confirm" and rv.source == "rules" and llm.calls == 0
|
|
113
106
|
|
|
114
|
-
# 2. 模糊(150∈[140,200))+ LLM 说 CONFIRM → confirm, source=llm
|
|
115
107
|
llm = _MockLLM("CONFIRM"); rv = mk(llm).reason((txn(150), 5000))
|
|
116
108
|
assert rv.decision == "confirm" and rv.source == "llm" and llm.calls == 1
|
|
117
109
|
|
|
118
|
-
# 3. 模糊 + LLM 说 APPROVE → approve, source=llm
|
|
119
110
|
llm = _MockLLM("APPROVE"); rv = mk(llm).reason((txn(150), 5000))
|
|
120
111
|
assert rv.decision == "approve" and rv.source == "llm"
|
|
121
112
|
|
|
122
|
-
# 4. LLM 出错/超时 → confirm(fail-safe),绝不自动放行
|
|
123
113
|
llm = _MockLLM(raises=True); rv = mk(llm).reason((txn(150), 5000))
|
|
124
114
|
assert rv.decision == "confirm" and rv.source == "llm_failsafe"
|
|
125
115
|
|
|
126
|
-
# 5. 【核心不变量】LLM 乱回(含"拒绝"/REJECT/含糊)→ **绝不 reject**,fail-safe confirm
|
|
127
116
|
for bad in ["REJECT", "让我拒绝这笔", "不确定", "yes maybe"]:
|
|
128
117
|
llm = _MockLLM(bad); rv = mk(llm).reason((txn(150), 5000))
|
|
129
118
|
assert rv.decision == "confirm" and rv.source == "llm_failsafe", (bad, rv.decision)
|
|
130
119
|
assert rv.decision != "reject", "LLM 永远不能产生 reject"
|
|
131
120
|
|
|
132
|
-
# 6. 明显安全(50<140)→ 规则批准,LLM 不调(省成本)
|
|
133
121
|
llm = _MockLLM("CONFIRM"); rv = mk(llm).reason((txn(50), 5000))
|
|
134
122
|
assert rv.decision == "approve" and rv.source == "rules" and llm.calls == 0
|
|
135
123
|
|
|
136
|
-
# 7. 默认关(llm_enabled=False)→ 全走规则,LLM 不调(behavior-preserving)
|
|
137
124
|
llm = _MockLLM("CONFIRM"); rv = mk(llm, enabled=False).reason((txn(150), 5000))
|
|
138
125
|
assert rv.decision == "approve" and rv.source == "rules" and llm.calls == 0
|
|
139
126
|
|
|
140
|
-
# 8. is_stub(无真实模型)→ 视为不可用,不调 LLM
|
|
141
127
|
stub = _MockLLM("CONFIRM"); stub.is_stub = True
|
|
142
128
|
rv = FinanceGatekeeperReasoner(policy, llm=stub, llm_enabled=True).reason((txn(150), 5000))
|
|
143
129
|
assert rv.source == "rules" and stub.calls == 0
|
|
@@ -153,7 +139,6 @@ def test_real_path_evidence():
|
|
|
153
139
|
from foxclaw.orchestration import CrossAgentRequest
|
|
154
140
|
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
155
141
|
os_.finance_runtime.set_balance("default", 5000)
|
|
156
|
-
# 大额 → 需确认(裁决不变)+ 证据链附在 req 上
|
|
157
142
|
req = os_.orchestrator.route_request(CrossAgentRequest(
|
|
158
143
|
from_agent="admin", to_agent="finance", request_type="budget_request",
|
|
159
144
|
payload={"amount": 680, "category": "travel", "entity": "酒店", "account": "default"},
|
|
@@ -28,7 +28,7 @@ def _load():
|
|
|
28
28
|
spec = importlib.util.spec_from_file_location(
|
|
29
29
|
"release_check", REPO / "tools" / "release_check.py")
|
|
30
30
|
mod = importlib.util.module_from_spec(spec)
|
|
31
|
-
sys.modules["release_check"] = mod
|
|
31
|
+
sys.modules["release_check"] = mod
|
|
32
32
|
spec.loader.exec_module(mod)
|
|
33
33
|
return mod
|
|
34
34
|
|
|
@@ -36,8 +36,6 @@ def _load():
|
|
|
36
36
|
rc = _load()
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
# ── 造最小产物 ────────────────────────────────────────────
|
|
40
|
-
|
|
41
39
|
def _wheel(dist: Path, version="0.1.0", name="foxclaw", extra=(),
|
|
42
40
|
description=True) -> Path:
|
|
43
41
|
p = dist / f"{name}-{version}-py3-none-any.whl"
|
|
@@ -93,8 +91,6 @@ def _mini_repo(root: Path, version: str, npm_version: str) -> Path:
|
|
|
93
91
|
return root
|
|
94
92
|
|
|
95
93
|
|
|
96
|
-
# ── 版本与 tag ────────────────────────────────────────────
|
|
97
|
-
|
|
98
94
|
def test_repo_versions_are_consistent():
|
|
99
95
|
"""真仓库当前状态:四处一致,无 error。"""
|
|
100
96
|
assert [i for i in rc.check_versions(REPO) if i.level == "error"] == []
|
|
@@ -127,8 +123,6 @@ def test_empty_tag_is_skipped():
|
|
|
127
123
|
assert rc.check_tag("", "0.1.0") == []
|
|
128
124
|
|
|
129
125
|
|
|
130
|
-
# ── 项目页正文(long_description)────────────────────────────
|
|
131
|
-
|
|
132
126
|
def test_repo_declares_readme():
|
|
133
127
|
"""真仓库必须声明正文:0.1.1 在 PyPI 上的项目页就是空白的,这就是回归点。"""
|
|
134
128
|
assert [i for i in rc.check_metadata(REPO) if i.level == "error"] == []
|
|
@@ -157,8 +151,6 @@ def test_readme_pointing_to_missing_file_is_error(tmp_path):
|
|
|
157
151
|
assert any(i.code == "readme_missing" for i in rc.check_metadata(root))
|
|
158
152
|
|
|
159
153
|
|
|
160
|
-
# ── 归档内容 ──────────────────────────────────────────────
|
|
161
|
-
|
|
162
154
|
@pytest.mark.parametrize("entry", [
|
|
163
155
|
"foxclaw/.env",
|
|
164
156
|
"foxclaw/.env.local",
|
|
@@ -173,7 +165,7 @@ def test_forbidden_entries_detected(entry):
|
|
|
173
165
|
|
|
174
166
|
|
|
175
167
|
@pytest.mark.parametrize("entry", [
|
|
176
|
-
"foxclaw/.env.example",
|
|
168
|
+
"foxclaw/.env.example",
|
|
177
169
|
"foxclaw/cli.py",
|
|
178
170
|
"foxclaw/vendor/hmr/hmr/__init__.py",
|
|
179
171
|
])
|
|
@@ -181,8 +173,6 @@ def test_allowed_entries_pass(entry):
|
|
|
181
173
|
assert rc.forbidden_entry(entry) is None
|
|
182
174
|
|
|
183
175
|
|
|
184
|
-
# ── dist / npm ────────────────────────────────────────────
|
|
185
|
-
|
|
186
176
|
def test_check_dist_passes_with_vendor_warning_only(tmp_path):
|
|
187
177
|
dist = tmp_path / "dist"
|
|
188
178
|
dist.mkdir()
|
|
@@ -254,7 +244,6 @@ def test_run_end_to_end(tmp_path):
|
|
|
254
244
|
dist.mkdir()
|
|
255
245
|
issues = rc.run(REPO, tag="", dist=dist, npm_tgz=None)
|
|
256
246
|
assert any(i.level == "error" for i in issues), "空 dist 应报错"
|
|
257
|
-
# 版本号跟着仓库走:写死 0.1.0 会在每次改版本时假红
|
|
258
247
|
version = rc.collect_versions(REPO)["pyproject.toml:project.version"]
|
|
259
248
|
_wheel(dist, version=version)
|
|
260
249
|
_sdist(dist, version=version)
|
|
@@ -41,8 +41,6 @@ class _Rec:
|
|
|
41
41
|
self.finished.append((tid, status))
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
# ── ① 日志层 ───────────────────────────────────────────────
|
|
45
|
-
|
|
46
44
|
def test_record_and_replay_steps(tmp_path) -> None:
|
|
47
45
|
rec = RestartRecovery(str(tmp_path / "recovery.jsonl"), fsync=False)
|
|
48
46
|
rec.record_step("t1", {"kind": "stage", "name": "perceive"})
|
|
@@ -59,14 +57,12 @@ def test_compact_log_keeps_steps(tmp_path) -> None:
|
|
|
59
57
|
rec.task_started("t1", "任务一")
|
|
60
58
|
rec.record_step("t1", {"kind": "tool", "name": "web_search", "key": "k", "result": "r"})
|
|
61
59
|
rec.task_finished("t1", status="done")
|
|
62
|
-
rec.compact_log(keep_recent_days=7)
|
|
60
|
+
rec.compact_log(keep_recent_days=7)
|
|
63
61
|
assert len(rec.steps_of("t1")) == 1
|
|
64
|
-
rec.compact_log(keep_recent_days=0)
|
|
62
|
+
rec.compact_log(keep_recent_days=0)
|
|
65
63
|
assert rec.steps_of("t1") == []
|
|
66
64
|
|
|
67
65
|
|
|
68
|
-
# ── ② 指纹与成败判定 ────────────────────────────────────────
|
|
69
|
-
|
|
70
66
|
def test_step_key_is_argument_order_insensitive() -> None:
|
|
71
67
|
a = tool_exec._step_key("web_search", {"q": "x", "n": 3})
|
|
72
68
|
b = tool_exec._step_key("web_search", {"n": 3, "q": "x"})
|
|
@@ -87,8 +83,6 @@ def test_step_ok_rejects_failure_texts(text, ok) -> None:
|
|
|
87
83
|
assert tool_exec._step_ok(text) is ok
|
|
88
84
|
|
|
89
85
|
|
|
90
|
-
# ── ③ 续跑复用 ──────────────────────────────────────────────
|
|
91
|
-
|
|
92
86
|
def test_run_tool_calls_reuses_completed_steps(monkeypatch) -> None:
|
|
93
87
|
executed: list = []
|
|
94
88
|
|
|
@@ -124,16 +118,14 @@ def test_run_tool_calls_skips_recording_failures(monkeypatch) -> None:
|
|
|
124
118
|
assert rec.steps == [], "失败/超时步不能进 checkpoint(否则续跑会把失败当完成)"
|
|
125
119
|
|
|
126
120
|
|
|
127
|
-
# ── ④ 装载与生命周期钩子 ────────────────────────────────────
|
|
128
|
-
|
|
129
121
|
def test_load_resume_cache_filters_bad_steps() -> None:
|
|
130
122
|
rows = [
|
|
131
|
-
{"kind": "stage", "name": "act"},
|
|
123
|
+
{"kind": "stage", "name": "act"},
|
|
132
124
|
{"kind": "tool", "name": "web_search", "key": "k1", "result": "ok"},
|
|
133
125
|
{"kind": "tool", "name": "web_search", "key": "k2", "result": "x",
|
|
134
|
-
"truncated": True},
|
|
135
|
-
{"kind": "tool", "name": "web_search", "key": "k3", "result": " "},
|
|
136
|
-
{"kind": "tool", "name": "web_search", "result": "no-key"},
|
|
126
|
+
"truncated": True},
|
|
127
|
+
{"kind": "tool", "name": "web_search", "key": "k3", "result": " "},
|
|
128
|
+
{"kind": "tool", "name": "web_search", "result": "no-key"},
|
|
137
129
|
]
|
|
138
130
|
fake = types.SimpleNamespace(_recovery=_Rec(rows))
|
|
139
131
|
assert CognitiveAgent._load_resume_cache(fake, "t1") == {"k1": {"name": "web_search",
|
|
@@ -151,6 +143,6 @@ def test_lifecycle_hooks_record_for_any_agent() -> None:
|
|
|
151
143
|
assert rec.started[0]["channel"] == "cli"
|
|
152
144
|
assert rec.finished == [("t1", "done")]
|
|
153
145
|
|
|
154
|
-
CognitiveAgent._on_session_started(types.SimpleNamespace(), "t1", "x", {})
|
|
146
|
+
CognitiveAgent._on_session_started(types.SimpleNamespace(), "t1", "x", {})
|
|
155
147
|
CognitiveAgent._on_task_settled(types.SimpleNamespace(), "t1", "done")
|
|
156
148
|
CognitiveAgent._record_stage(types.SimpleNamespace(), "t1", "recall")
|
|
@@ -19,7 +19,6 @@ def test_routing():
|
|
|
19
19
|
|
|
20
20
|
print("=" * 60); print("任务感知模型路由 + fallback 链验证"); print("=" * 60)
|
|
21
21
|
|
|
22
|
-
# [1] 任务→档
|
|
23
22
|
assert _route_tier("supervisor", [{"role": "user", "content": "x"}]) == "fast"
|
|
24
23
|
assert _route_tier("finance", [{"role": "user", "content": "帮我做深度财务分析报告"}]) == "smart"
|
|
25
24
|
assert _route_tier("assistant", [{"role": "user", "content": "你好"}]) == "fast"
|
|
@@ -27,7 +26,6 @@ def test_routing():
|
|
|
27
26
|
assert _tier_model("deepseek", "smart") == "deepseek-reasoner"
|
|
28
27
|
print("[1] 任务→档 + 档→模型 OK")
|
|
29
28
|
|
|
30
|
-
# [2] 按任务/Agent 选档
|
|
31
29
|
os.environ["FOXCLAW_PROVIDER"] = "deepseek"; os.environ["DEEPSEEK_API_KEY"] = "x"
|
|
32
30
|
r = ModelRouter()
|
|
33
31
|
assert r.provider == "deepseek"
|
|
@@ -35,7 +33,6 @@ def test_routing():
|
|
|
35
33
|
assert r._model_for("deepseek", "finance", [{"role": "user", "content": "深度分析报告方案评估"}]) == "deepseek-reasoner"
|
|
36
34
|
print("[2] 按任务/Agent 选档(supervisor→chat,复杂→reasoner) OK")
|
|
37
35
|
|
|
38
|
-
# [3] fallback 链:主 provider 失败 → 试下一个已配 provider
|
|
39
36
|
os.environ["OPENAI_API_KEY"] = "y"
|
|
40
37
|
r2 = ModelRouter(); calls = []
|
|
41
38
|
def _fake(p, c, m, s, msgs, mt):
|
|
@@ -49,33 +46,26 @@ def test_routing():
|
|
|
49
46
|
assert "回退到 openai" in r2.fallback_reason
|
|
50
47
|
print("[3] fallback 链(deepseek 失败→openai) OK")
|
|
51
48
|
|
|
52
|
-
# [4] 锁定模型(显式 model=)不分档
|
|
53
49
|
r3 = ModelRouter(provider="deepseek", model="fixed-x")
|
|
54
50
|
assert r3._model_for("deepseek", "supervisor", [{"role": "user", "content": "x"}]) == "fixed-x"
|
|
55
51
|
print("[4] 锁定模型不分档(向后兼容) OK")
|
|
56
52
|
|
|
57
|
-
# [5] 全部 provider 失败 → 显式失败(不再降 stub 伪装成功)
|
|
58
53
|
def _allfail(*a):
|
|
59
54
|
raise RuntimeError("boom")
|
|
60
55
|
r2._call_provider = _allfail
|
|
61
56
|
out = r2.complete("sys", [{"role": "user", "content": "hi"}])
|
|
62
57
|
assert "STUB" in out or "全部模型失败" in out, out
|
|
63
|
-
# 失败语义(A 类修复):全部 provider 挂掉属**故障**,不是"未配置模型"的离线态。
|
|
64
|
-
# 旧实现会拼接 stub 文案「已完成处理:…」—— 那是把故障伪装成成功,且该文本会
|
|
65
|
-
# 经 base._store_memory 沉入长期记忆造成持续复读。此处锁死该行为不得回归。
|
|
66
58
|
assert "已完成处理" not in out, f"故障被伪装成成功: {out}"
|
|
67
59
|
assert r2.is_degraded() is True, "全部失败必须置 degraded 标志(上游据此禁写长期记忆)"
|
|
68
60
|
print("[5] 全部失败 → 显式失败(无假成功文案) + degraded OK")
|
|
69
61
|
|
|
70
|
-
# [6] switch 尊重显式模型(修回归:UI 切模型不能被 tiering 覆盖);只切 provider 清锁恢复分档
|
|
71
62
|
r4 = ModelRouter()
|
|
72
63
|
r4.switch("deepseek", "deepseek-reasoner")
|
|
73
64
|
assert r4._model_for("deepseek", "supervisor", [{"role": "user", "content": "简单"}]) == "deepseek-reasoner"
|
|
74
|
-
r4.switch("deepseek")
|
|
65
|
+
r4.switch("deepseek")
|
|
75
66
|
assert r4._model_for("deepseek", "supervisor", [{"role": "user", "content": "简单"}]) == "deepseek-chat"
|
|
76
67
|
print("[6] switch 尊重显式模型 + 清锁恢复分档 OK")
|
|
77
68
|
|
|
78
|
-
# [7] 断路器:主 provider 持续宕机,第二次调用直接跳过它(免每次等超时)
|
|
79
69
|
r5 = ModelRouter(); seq = []
|
|
80
70
|
def _fk(p, c, m, s, msgs, mt):
|
|
81
71
|
seq.append(p)
|
|
@@ -84,7 +74,7 @@ def test_routing():
|
|
|
84
74
|
return f"OK-{p}", None
|
|
85
75
|
r5._call_provider = _fk
|
|
86
76
|
r5.complete("s", [{"role": "user", "content": "hi"}]); seq.clear()
|
|
87
|
-
r5.complete("s", [{"role": "user", "content": "hi"}])
|
|
77
|
+
r5.complete("s", [{"role": "user", "content": "hi"}])
|
|
88
78
|
assert seq == ["openai"], f"断路器失效: {seq}"
|
|
89
79
|
print("[7] 断路器跳过宕机 provider OK")
|
|
90
80
|
|
|
@@ -29,11 +29,11 @@ def _run_n_concurrent(sched, n, work):
|
|
|
29
29
|
|
|
30
30
|
def test_unlimited_reaches_peak():
|
|
31
31
|
print("\n【1】无上限(默认):并发不受限,行为直通")
|
|
32
|
-
sched = Scheduler()
|
|
32
|
+
sched = Scheduler()
|
|
33
33
|
N = 6
|
|
34
|
-
barrier = threading.Barrier(N)
|
|
34
|
+
barrier = threading.Barrier(N)
|
|
35
35
|
def work():
|
|
36
|
-
barrier.wait(timeout=5)
|
|
36
|
+
barrier.wait(timeout=5)
|
|
37
37
|
time.sleep(0.01)
|
|
38
38
|
return "ok"
|
|
39
39
|
_run_n_concurrent(sched, N, work)
|
|
@@ -62,12 +62,10 @@ def test_lifecycle_events_and_accounting():
|
|
|
62
62
|
print("\n【3】生命周期事件 + 记账 + 失败传播")
|
|
63
63
|
spine = _CapSpine()
|
|
64
64
|
sched = Scheduler(spine=spine)
|
|
65
|
-
# 成功任务
|
|
66
65
|
r = sched.run(lambda: 42, agent_id="finance", task="算账")
|
|
67
66
|
assert r == 42
|
|
68
67
|
assert spine.events == ["task.lifecycle.scheduled", "task.lifecycle.running",
|
|
69
68
|
"task.lifecycle.completed"], spine.events
|
|
70
|
-
# 失败任务:异常透传 + failed 事件 + 记账
|
|
71
69
|
spine.events.clear()
|
|
72
70
|
raised = False
|
|
73
71
|
try:
|
|
@@ -90,14 +88,13 @@ def test_pm_delegation_unified_lifecycle():
|
|
|
90
88
|
spine = EventSpine()
|
|
91
89
|
seen = []
|
|
92
90
|
spine.project(lambda e: seen.append(e.topic))
|
|
93
|
-
pm = ProcessManager(bus=SystemBus(spine=spine))
|
|
94
|
-
sched = Scheduler(spine=spine, process_manager=pm)
|
|
91
|
+
pm = ProcessManager(bus=SystemBus(spine=spine))
|
|
92
|
+
sched = Scheduler(spine=spine, process_manager=pm)
|
|
95
93
|
r = sched.run(lambda: 7, agent_id="finance", task="t")
|
|
96
94
|
assert r == 7
|
|
97
95
|
life = [t for t in seen if t.startswith("task.lifecycle")]
|
|
98
96
|
assert life == ["task.lifecycle.scheduled", "task.lifecycle.running",
|
|
99
97
|
"task.lifecycle.completed"], f"应由 pm 发唯一词汇,实得 {life}"
|
|
100
|
-
# pm 里确有该进程(顶层任务也成了进程),调度器记账仍在
|
|
101
98
|
assert pm.stats()["by_state"].get("completed") == 1
|
|
102
99
|
assert sched.stats()["completed"] == 1
|
|
103
100
|
print(" ✓ pm 发 task.lifecycle.{scheduled,running,completed};顶层任务纳入进程;记账双在")
|
|
@@ -116,8 +113,8 @@ def test_priority_admission():
|
|
|
116
113
|
sched.run(lambda: order.append(name), priority=p, task=name)
|
|
117
114
|
tlo = threading.Thread(target=worker, args=(0, "lo"))
|
|
118
115
|
thi = threading.Thread(target=worker, args=(10, "hi"))
|
|
119
|
-
tlo.start(); time.sleep(0.05); thi.start(); time.sleep(0.15)
|
|
120
|
-
release.set()
|
|
116
|
+
tlo.start(); time.sleep(0.05); thi.start(); time.sleep(0.15)
|
|
117
|
+
release.set()
|
|
121
118
|
for t in (tb, tlo, thi): t.join(5)
|
|
122
119
|
assert order == ["hi", "lo"], f"高优先应先获空位,实得 {order}"
|
|
123
120
|
print(f" ✓ 释放后:高优先先获空位 → {order}(准入优先级,非抢占运行中)")
|
|
@@ -39,7 +39,6 @@ def test_empty_sources_terminates():
|
|
|
39
39
|
def test_memory_evidence():
|
|
40
40
|
print("\n【2】内部记忆证据进证据图(MEMORY 来源)")
|
|
41
41
|
rt = SearchRuntime(hmr=FakeHMR(), knowledge_port=None, external_ports=[])
|
|
42
|
-
# "分析" 是 HYBRID 触发词 → 会查内部记忆(见 planner._HYBRID_HINTS)
|
|
43
42
|
r = rt.search("分析活字印刷的历史", agent_id="office")
|
|
44
43
|
nodes = list(r.graph.nodes.values())
|
|
45
44
|
mem = [n for n in nodes if n.source == SourceType.MEMORY]
|
|
@@ -62,11 +61,9 @@ def test_via_foxclawos_lazy():
|
|
|
62
61
|
try:
|
|
63
62
|
from foxclaw import Foxclaw
|
|
64
63
|
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
65
|
-
# 【P1-3】search_runtime 由能力中枢惰性构造:未访问前未缓存
|
|
66
64
|
assert "search_runtime" not in os_.capabilities._caps, "未访问前不该构造"
|
|
67
|
-
rt = os_.search_runtime
|
|
65
|
+
rt = os_.search_runtime
|
|
68
66
|
assert rt is not None and os_.search_runtime is rt, "应缓存同一实例"
|
|
69
|
-
# 不联网:替换外部端口为空,跑一次
|
|
70
67
|
rt.external_ports = []
|
|
71
68
|
r = rt.search("测试查询", agent_id="supervisor")
|
|
72
69
|
assert r.confidence is not None
|
|
@@ -124,7 +121,6 @@ def test_agent_port_all_subsources():
|
|
|
124
121
|
assert any("thoughtchain" in o for o in origins("预算风险")), "推理链应连通"
|
|
125
122
|
assert any("capability:skill" in o for o in origins("发邮件")), "能力目录应连通"
|
|
126
123
|
assert any(":hmr:" in o for o in origins("任意")), "各Agent记忆应连通"
|
|
127
|
-
# 全部证据都标记为 AGENT 源
|
|
128
124
|
evs = port.query("星巴克", top_k=8)
|
|
129
125
|
assert all(e.source == SourceType.AGENT for e in evs)
|
|
130
126
|
print(" ✓ 财务/学习/行程/推理链/能力/各Agent记忆 六路均连通,来源标记 AGENT")
|
|
@@ -137,20 +133,16 @@ def test_bidirectional_and_internal_only():
|
|
|
137
133
|
from foxclaw import Foxclaw
|
|
138
134
|
from foxclaw.search import SourceType
|
|
139
135
|
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
140
|
-
# 往财务内核放一笔交易
|
|
141
136
|
from foxclaw.finance.models import Transaction
|
|
142
137
|
os_.finance_runtime.add_transaction(
|
|
143
138
|
Transaction(entity="蔚来汽车", amount=-260000, category="购车",
|
|
144
139
|
account="alipay", timestamp="2026-05-01T10:00:00"),
|
|
145
140
|
run_gatekeeper=False)
|
|
146
|
-
# 触发 search_runtime 装配(会把自身注入各 Agent)
|
|
147
141
|
_ = os_.search_runtime
|
|
148
|
-
# 财务 Agent 反向调用内部搜索
|
|
149
142
|
res = os_.finance.search_internal("蔚来汽车")
|
|
150
143
|
assert res is not None, "search_internal 应返回 SearchResult"
|
|
151
144
|
agent_ev = [n for n in res.graph.nodes.values() if n.source == SourceType.AGENT]
|
|
152
145
|
assert any("蔚来" in n.fact for n in agent_ev), "应从财务内核检索到该交易"
|
|
153
|
-
# internal_only:轨迹里不应出现 web 证据来源
|
|
154
146
|
assert all(n.source != SourceType.WEB for n in res.graph.nodes.values())
|
|
155
147
|
print(f" ✓ os_.finance.search_internal('蔚来汽车') → AGENT证据 {len(agent_ev)} 条,无联网")
|
|
156
148
|
asyncio.run(os_.aclose())
|
|
@@ -168,7 +160,6 @@ def test_internal_source_isolation():
|
|
|
168
160
|
from foxclaw.search.models import RawEvidence
|
|
169
161
|
return [RawEvidence(id="k1", fact=f"知识:{text}", source=SourceType.KNOWLEDGE,
|
|
170
162
|
origin="hkc:k1", confidence=0.7)]
|
|
171
|
-
# agent_port 抛异常 + memory(FakeHMR)正常 + knowledge 正常 → search 不崩,仍返回其它源证据
|
|
172
163
|
rt = SearchRuntime(hmr=FakeHMR(), knowledge_port=_OKKnow(),
|
|
173
164
|
agent_port=_BoomPort(), external_ports=[])
|
|
174
165
|
r = rt.search("测试查询", agent_id="finance", internal_only=True)
|
|
@@ -176,7 +167,6 @@ def test_internal_source_isolation():
|
|
|
176
167
|
assert any("知识" in f for f in facts), "知识源应仍返回(不受 agent 源崩溃影响)"
|
|
177
168
|
assert any("记忆" in f for f in facts), "记忆源应仍返回"
|
|
178
169
|
print(f" ✓ agent 源抛异常被隔离,其它源正常:{len(r.graph.nodes)} 节点")
|
|
179
|
-
# 全部内部源都抛异常 → search 仍不崩,返回空图(优雅降级)
|
|
180
170
|
rt2 = SearchRuntime(hmr=None, knowledge_port=_BoomPort(),
|
|
181
171
|
agent_port=_BoomPort(), external_ports=[])
|
|
182
172
|
r2 = rt2.search("测试", internal_only=True)
|
|
@@ -37,8 +37,6 @@ def no_kr(monkeypatch):
|
|
|
37
37
|
monkeypatch.setattr(secrets, "_keyring", lambda: None)
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
# ── 基础行为 ──────────────────────────────────────────────────
|
|
41
|
-
|
|
42
40
|
def test_store_takes_over_when_keyring_available(fake_kr):
|
|
43
41
|
assert secrets.store("m1", "sk-abc") is True
|
|
44
42
|
assert fake_kr.store[(secrets.SERVICE, "m1")] == "sk-abc"
|
|
@@ -76,7 +74,7 @@ def test_rename_without_source_is_noop(fake_kr):
|
|
|
76
74
|
def test_delete_is_idempotent(fake_kr):
|
|
77
75
|
secrets.store("m1", "sk-abc")
|
|
78
76
|
secrets.delete("m1")
|
|
79
|
-
secrets.delete("m1")
|
|
77
|
+
secrets.delete("m1")
|
|
80
78
|
assert secrets.load("m1") == ""
|
|
81
79
|
|
|
82
80
|
|
|
@@ -84,8 +82,6 @@ def test_delete_without_keyring_does_not_raise(no_kr):
|
|
|
84
82
|
secrets.delete("m1")
|
|
85
83
|
|
|
86
84
|
|
|
87
|
-
# ── 写入侧:JSON 里放什么 ─────────────────────────────────────
|
|
88
|
-
|
|
89
85
|
def test_save_key_keeps_only_marker_when_keyring_available(fake_kr):
|
|
90
86
|
"""有 OS 凭据库时,JSON 里不能再有任何可还原的密文。"""
|
|
91
87
|
from foxclaw.api.routes.llm_admin import _save_key
|
|
@@ -110,7 +106,7 @@ def test_save_key_empty_input_clears_both(no_kr):
|
|
|
110
106
|
|
|
111
107
|
|
|
112
108
|
@pytest.mark.parametrize("record,expected", [
|
|
113
|
-
({"api_key_kr": True, "api_key_obf": ""}, True),
|
|
109
|
+
({"api_key_kr": True, "api_key_obf": ""}, True),
|
|
114
110
|
({"api_key_kr": False, "api_key_obf": "c2stYWJj"}, True),
|
|
115
111
|
({"api_key": "sk-legacy"}, True),
|
|
116
112
|
({"api_key_kr": False, "api_key_obf": ""}, False),
|
|
@@ -122,8 +118,6 @@ def test_has_key(record, expected):
|
|
|
122
118
|
assert _has_key(record) is expected
|
|
123
119
|
|
|
124
120
|
|
|
125
|
-
# ── 回退而非拒绝保存 ─────────────────────────────────────────
|
|
126
|
-
|
|
127
121
|
def test_obf_is_reversible(no_kr):
|
|
128
122
|
"""base64 只是混淆,不要在任何地方当加密用 —— 这条测试的作用是钉住这一点。"""
|
|
129
123
|
import base64
|
|
@@ -152,8 +146,8 @@ def test_backend_probe_is_cached(monkeypatch):
|
|
|
152
146
|
|
|
153
147
|
secrets.reset_probe()
|
|
154
148
|
try:
|
|
155
|
-
assert secrets.available() is False
|
|
149
|
+
assert secrets.available() is False
|
|
156
150
|
secrets.available()
|
|
157
|
-
assert len(calls) == 1
|
|
151
|
+
assert len(calls) == 1
|
|
158
152
|
finally:
|
|
159
153
|
secrets.reset_probe()
|