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,282 @@
|
|
|
1
|
+
"""`foxclaw skills install` 的来源解析与落地。
|
|
2
|
+
|
|
3
|
+
全部离线:远端用 monkeypatch 替掉 `_http_get`,不打真网 —— 否则 CI 会因 ClawHub
|
|
4
|
+
抖动而红,这类测试一旦不稳就会被跳过,等于没测。
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import io
|
|
9
|
+
import json
|
|
10
|
+
import tarfile
|
|
11
|
+
import zipfile
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from foxclaw.skills import fetch as fetch_mod
|
|
16
|
+
from foxclaw.skills.archive import PackageError, has_code, read_package
|
|
17
|
+
|
|
18
|
+
MD = "---\nname: Demo Skill\ndescription: 演示用技能\nversion: 1.2.3\n---\n\n正文指示"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _zip(files: dict) -> bytes:
|
|
22
|
+
buf = io.BytesIO()
|
|
23
|
+
with zipfile.ZipFile(buf, "w") as z:
|
|
24
|
+
for name, body in files.items():
|
|
25
|
+
z.writestr(name, body)
|
|
26
|
+
return buf.getvalue()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _tgz(files: dict) -> bytes:
|
|
30
|
+
buf = io.BytesIO()
|
|
31
|
+
with tarfile.open(fileobj=buf, mode="w:gz") as t:
|
|
32
|
+
for name, text in files.items():
|
|
33
|
+
raw = text.encode("utf-8")
|
|
34
|
+
info = tarfile.TarInfo(name)
|
|
35
|
+
info.size = len(raw)
|
|
36
|
+
t.addfile(info, io.BytesIO(raw))
|
|
37
|
+
return buf.getvalue()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _mock_http(monkeypatch, routes: dict) -> None:
|
|
41
|
+
def fake(url: str, timeout: float = 25.0, max_bytes: int = 10 * 1024 * 1024):
|
|
42
|
+
for key, val in routes.items():
|
|
43
|
+
if key in url:
|
|
44
|
+
return val
|
|
45
|
+
raise PackageError(f"测试未配置这条路由: {url}")
|
|
46
|
+
|
|
47
|
+
monkeypatch.setattr(fetch_mod, "_http_get", fake)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_read_zip_root_skill_md():
|
|
51
|
+
pkg = read_package(_zip({"SKILL.md": MD, "_meta.json": '{"slug": "demo"}'}))
|
|
52
|
+
assert pkg["skill_md"] == MD
|
|
53
|
+
assert pkg["meta"]["slug"] == "demo"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_read_targz_picks_shallowest():
|
|
57
|
+
pkg = read_package(_tgz({
|
|
58
|
+
"repo-abc/SKILL.md": MD,
|
|
59
|
+
"repo-abc/docs/SKILL.md": "更深的同名副本应被忽略",
|
|
60
|
+
}))
|
|
61
|
+
assert pkg["skill_md"] == MD
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_read_package_rejects_traversal():
|
|
65
|
+
with pytest.raises(PackageError):
|
|
66
|
+
read_package(_zip({"../evil/SKILL.md": MD}))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_read_package_without_skill_md():
|
|
70
|
+
with pytest.raises(PackageError, match="未找到 SKILL.md"):
|
|
71
|
+
read_package(_zip({"README.md": "nothing here"}))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_subpath_narrows_search():
|
|
75
|
+
with pytest.raises(PackageError):
|
|
76
|
+
read_package(_tgz({"repo/a/SKILL.md": MD}), subpath="other")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_has_code_detects_scripts():
|
|
80
|
+
assert has_code(["SKILL.md", "scripts/run.py"])
|
|
81
|
+
assert has_code(["SKILL.md", "tool.sh"])
|
|
82
|
+
assert not has_code(["SKILL.md", "skill-card.md"])
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_ref_routes_scoped_name_to_clawhub(monkeypatch):
|
|
86
|
+
seen: dict = {}
|
|
87
|
+
|
|
88
|
+
def fake(owner, slug, timeout):
|
|
89
|
+
seen["id"] = (owner, slug)
|
|
90
|
+
return fetch_mod.SkillPackage(skill_md=MD, source="clawhub", origin="x")
|
|
91
|
+
|
|
92
|
+
monkeypatch.setattr(fetch_mod, "_from_clawhub", fake)
|
|
93
|
+
assert fetch_mod.fetch("@snowfox/demo").origin == "x"
|
|
94
|
+
assert seen["id"] == ("snowfox", "demo")
|
|
95
|
+
fetch_mod.fetch("snowfox/demo")
|
|
96
|
+
assert seen["id"] == ("snowfox", "demo")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_unknown_ref_rejected():
|
|
100
|
+
with pytest.raises(PackageError, match="无法识别"):
|
|
101
|
+
fetch_mod.fetch("no-such-thing-xyz")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_local_dir_reads_skill_md(tmp_path):
|
|
105
|
+
(tmp_path / "SKILL.md").write_text(MD, encoding="utf-8")
|
|
106
|
+
pkg = fetch_mod.fetch(str(tmp_path))
|
|
107
|
+
assert pkg.source == "local"
|
|
108
|
+
assert "演示用技能" in pkg.skill_md
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_local_dir_without_skill_md(tmp_path):
|
|
112
|
+
with pytest.raises(PackageError):
|
|
113
|
+
fetch_mod.fetch(str(tmp_path))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_clawhub_blocks_malware(monkeypatch):
|
|
117
|
+
_mock_http(monkeypatch, {
|
|
118
|
+
"/api/v1/skills/": (json.dumps({
|
|
119
|
+
"moderation": {"isMalwareBlocked": True, "summary": "窃取密钥"},
|
|
120
|
+
"latestVersion": {"version": "1.0.0"},
|
|
121
|
+
}).encode(), "application/json"),
|
|
122
|
+
})
|
|
123
|
+
with pytest.raises(fetch_mod.MalwareBlocked):
|
|
124
|
+
fetch_mod.fetch("@bad/skill")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_clawhub_github_handoff(monkeypatch):
|
|
128
|
+
"""download 返回 JSON 交接(GitHub 托底)时要改下 archiveUrl,而不是当 zip 解。"""
|
|
129
|
+
_mock_http(monkeypatch, {
|
|
130
|
+
"/api/v1/skills/": (json.dumps({
|
|
131
|
+
"moderation": {"isSuspicious": True},
|
|
132
|
+
"latestVersion": {"version": "2.0.0"},
|
|
133
|
+
}).encode(), "application/json"),
|
|
134
|
+
"/api/v1/download": (json.dumps({
|
|
135
|
+
"archiveUrl": "https://codeload.example/archive.tar.gz",
|
|
136
|
+
}).encode(), "application/json"),
|
|
137
|
+
"archive.tar.gz": (_tgz({"repo-main/SKILL.md": MD}), "application/gzip"),
|
|
138
|
+
})
|
|
139
|
+
pkg = fetch_mod.fetch("@snowfox/demo")
|
|
140
|
+
assert pkg.version == "2.0.0"
|
|
141
|
+
assert pkg.suspicious is True
|
|
142
|
+
assert "repo-main/SKILL.md" in pkg.files
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_clawhub_not_found(monkeypatch):
|
|
146
|
+
def boom(url, timeout=25.0, max_bytes=10 * 1024 * 1024):
|
|
147
|
+
raise fetch_mod.HttpError(404, url, "Not found")
|
|
148
|
+
|
|
149
|
+
monkeypatch.setattr(fetch_mod, "_http_get", boom)
|
|
150
|
+
with pytest.raises(PackageError, match="找不到"):
|
|
151
|
+
fetch_mod.fetch("@ghost/missing")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_github_subpath(monkeypatch):
|
|
155
|
+
_mock_http(monkeypatch, {
|
|
156
|
+
"codeload.github.com": (_tgz({
|
|
157
|
+
"repo-main/skills/demo/SKILL.md": MD,
|
|
158
|
+
"repo-main/skills/other/SKILL.md": "x",
|
|
159
|
+
}), "application/gzip"),
|
|
160
|
+
})
|
|
161
|
+
pkg = fetch_mod.fetch("github:snowfox/repo/skills/demo@main")
|
|
162
|
+
assert pkg.source == "github"
|
|
163
|
+
assert pkg.skill_md == MD
|
|
164
|
+
assert pkg.origin == "github:snowfox/repo/skills/demo@main"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _src(tmp_path):
|
|
168
|
+
src = tmp_path / "src"
|
|
169
|
+
src.mkdir()
|
|
170
|
+
(src / "SKILL.md").write_text(MD, encoding="utf-8")
|
|
171
|
+
return src
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_cli_install_writes_file_when_service_down(tmp_path, monkeypatch):
|
|
175
|
+
from foxclaw import cli
|
|
176
|
+
from foxclaw.skills import install_cmd
|
|
177
|
+
|
|
178
|
+
data = tmp_path / "data"
|
|
179
|
+
monkeypatch.setenv("FOXCLAW_DATA_DIR", str(data))
|
|
180
|
+
monkeypatch.setattr(install_cmd, "_api_alive", lambda base: False)
|
|
181
|
+
|
|
182
|
+
assert cli.main(["skills", "install", str(_src(tmp_path)), "--yes"]) == 0
|
|
183
|
+
saved = json.loads((data / "markdown_skills.json").read_text(encoding="utf-8"))
|
|
184
|
+
assert [s["id"] for s in saved["skills"]] == ["demo_skill"]
|
|
185
|
+
assert saved["skills"][0]["version"] == "1.2.3"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_cli_install_respects_explicit_id(tmp_path, monkeypatch):
|
|
189
|
+
from foxclaw import cli
|
|
190
|
+
from foxclaw.skills import install_cmd
|
|
191
|
+
|
|
192
|
+
data = tmp_path / "data"
|
|
193
|
+
monkeypatch.setenv("FOXCLAW_DATA_DIR", str(data))
|
|
194
|
+
monkeypatch.setattr(install_cmd, "_api_alive", lambda base: False)
|
|
195
|
+
|
|
196
|
+
assert cli.main(["skills", "install", str(_src(tmp_path)), "--yes",
|
|
197
|
+
"--id", "my_demo"]) == 0
|
|
198
|
+
saved = json.loads((data / "markdown_skills.json").read_text(encoding="utf-8"))
|
|
199
|
+
assert saved["skills"][0]["id"] == "my_demo"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_cli_install_refuses_without_confirmation(tmp_path, monkeypatch):
|
|
203
|
+
from foxclaw import cli
|
|
204
|
+
from foxclaw.skills import install_cmd
|
|
205
|
+
|
|
206
|
+
monkeypatch.setenv("FOXCLAW_DATA_DIR", str(tmp_path / "data"))
|
|
207
|
+
monkeypatch.setattr(install_cmd, "_api_alive", lambda base: False)
|
|
208
|
+
monkeypatch.setattr(install_cmd, "_confirm", lambda: False)
|
|
209
|
+
|
|
210
|
+
assert cli.main(["skills", "install", str(_src(tmp_path))]) == 1
|
|
211
|
+
assert not (tmp_path / "data" / "markdown_skills.json").exists()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_cli_list_reads_file_when_service_down(tmp_path, monkeypatch, capsys):
|
|
215
|
+
from foxclaw import cli
|
|
216
|
+
from foxclaw.skills import install_cmd
|
|
217
|
+
|
|
218
|
+
data = tmp_path / "data"
|
|
219
|
+
monkeypatch.setenv("FOXCLAW_DATA_DIR", str(data))
|
|
220
|
+
monkeypatch.setattr(install_cmd, "_api_alive", lambda base: False)
|
|
221
|
+
cli.main(["skills", "install", str(_src(tmp_path)), "--yes"])
|
|
222
|
+
capsys.readouterr()
|
|
223
|
+
|
|
224
|
+
assert cli.main(["skills", "list", "--json"]) == 0
|
|
225
|
+
got = json.loads(capsys.readouterr().out)
|
|
226
|
+
assert got["source"] == "file"
|
|
227
|
+
assert got["skills"][0]["id"] == "demo_skill"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_cli_remove_deletes_skill(tmp_path, monkeypatch):
|
|
231
|
+
from foxclaw import cli
|
|
232
|
+
from foxclaw.skills import install_cmd
|
|
233
|
+
|
|
234
|
+
data = tmp_path / "data"
|
|
235
|
+
monkeypatch.setenv("FOXCLAW_DATA_DIR", str(data))
|
|
236
|
+
monkeypatch.setattr(install_cmd, "_api_alive", lambda base: False)
|
|
237
|
+
cli.main(["skills", "install", str(_src(tmp_path)), "--yes"])
|
|
238
|
+
|
|
239
|
+
assert cli.main(["skills", "remove", "demo_skill"]) == 0
|
|
240
|
+
saved = json.loads((data / "markdown_skills.json").read_text(encoding="utf-8"))
|
|
241
|
+
assert saved["skills"] == []
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_cli_remove_unknown_id_fails(tmp_path, monkeypatch):
|
|
245
|
+
from foxclaw import cli
|
|
246
|
+
from foxclaw.skills import install_cmd
|
|
247
|
+
|
|
248
|
+
monkeypatch.setenv("FOXCLAW_DATA_DIR", str(tmp_path / "data"))
|
|
249
|
+
monkeypatch.setattr(install_cmd, "_api_alive", lambda base: False)
|
|
250
|
+
assert cli.main(["skills", "remove", "nope"]) == 1
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_cli_install_uses_api_when_service_alive(tmp_path, monkeypatch):
|
|
254
|
+
"""服务在跑时必须打 API:直接改 json 会被运行中注册表的下次 save 整份覆盖。"""
|
|
255
|
+
from foxclaw import cli
|
|
256
|
+
from foxclaw.skills import install_cmd
|
|
257
|
+
|
|
258
|
+
monkeypatch.setenv("FOXCLAW_DATA_DIR", str(tmp_path / "data"))
|
|
259
|
+
monkeypatch.setattr(install_cmd, "_api_alive", lambda base: True)
|
|
260
|
+
calls: list = []
|
|
261
|
+
|
|
262
|
+
def fake_json(base, method, path, payload=None):
|
|
263
|
+
calls.append((method, path, payload))
|
|
264
|
+
return {"created": True}
|
|
265
|
+
|
|
266
|
+
monkeypatch.setattr(install_cmd, "_api_json", fake_json)
|
|
267
|
+
|
|
268
|
+
assert cli.main(["skills", "install", str(_src(tmp_path)), "--yes"]) == 0
|
|
269
|
+
method, path, payload = calls[0]
|
|
270
|
+
assert (method, path) == ("POST", "/api/ahs/md-skills")
|
|
271
|
+
assert payload["acknowledged"] is True
|
|
272
|
+
assert payload["id"] == "demo_skill"
|
|
273
|
+
assert "正文指示" in payload["content"]
|
|
274
|
+
assert not (tmp_path / "data" / "markdown_skills.json").exists()
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_local_zip_package(tmp_path):
|
|
278
|
+
p = tmp_path / "demo.zip"
|
|
279
|
+
p.write_bytes(_zip({"SKILL.md": MD, "_meta.json": '{"slug": "pack"}'}))
|
|
280
|
+
pkg = fetch_mod.fetch(str(p))
|
|
281
|
+
assert pkg.source == "local"
|
|
282
|
+
assert pkg.meta["slug"] == "pack"
|
|
@@ -33,14 +33,13 @@ def _adapter_with(hmr_obj):
|
|
|
33
33
|
return s
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
# ── 1. 记忆统计 ────────────────────────────────────────────────────────────────
|
|
37
36
|
def test_stats_counts_from_memory_fs_not_vector_store():
|
|
38
37
|
"""核心回归:记忆在 memory_fs;vector_store 没有 count/size/memories 也不能让 total 归零。"""
|
|
39
38
|
mems = [_mem(["agent:supervisor"]), _mem(["agent:supervisor"]),
|
|
40
39
|
_mem(["agent:finance", "shared"]), _mem(["agent:office"])]
|
|
41
40
|
hmr = types.SimpleNamespace(
|
|
42
41
|
memory_fs=types.SimpleNamespace(list_memories=lambda: mems),
|
|
43
|
-
vector_store=types.SimpleNamespace(),
|
|
42
|
+
vector_store=types.SimpleNamespace(),
|
|
44
43
|
memory_graph=None)
|
|
45
44
|
out = HMRAdapter.stats(_adapter_with(hmr))
|
|
46
45
|
assert out["total_memories"] == 4, "必须从 memory_fs 数出真实记忆数"
|
|
@@ -75,14 +74,13 @@ def test_stats_survives_broken_hmr():
|
|
|
75
74
|
assert out["total_memories"] == 0 and out["relations"] == 0
|
|
76
75
|
|
|
77
76
|
|
|
78
|
-
# ── 1b. 记忆标题可读性 ─────────────────────────────────────────────────────────
|
|
79
77
|
def test_derive_title_from_content():
|
|
80
78
|
d = HMRAdapter._derive_title
|
|
81
79
|
assert d("对话:你好 → 你好呀!周五中午,正是一周里…") == "你好"
|
|
82
80
|
assert d("用户偏好:我喜欢简洁的回答") == "我喜欢简洁的回答"
|
|
83
81
|
assert d("把这份报告存入HMR → 好的,我现在") == "把这份报告存入HMR"
|
|
84
82
|
assert d("") == ""
|
|
85
|
-
assert len(d("很长" * 50)) <= 24
|
|
83
|
+
assert len(d("很长" * 50)) <= 24
|
|
86
84
|
|
|
87
85
|
|
|
88
86
|
def test_recent_replaces_timestamp_titles():
|
|
@@ -109,7 +107,6 @@ def test_recent_keeps_meaningful_title():
|
|
|
109
107
|
assert s.recent(5)[0]["title"] == "季度财务复盘"
|
|
110
108
|
|
|
111
109
|
|
|
112
|
-
# ── 2. 活动事件 ────────────────────────────────────────────────────────────────
|
|
113
110
|
def test_chat_publishes_activity_event(monkeypatch):
|
|
114
111
|
"""核心回归:对话完成后必须往事件脊柱发 chat.completed,否则"活动"页永远空。"""
|
|
115
112
|
monkeypatch.setenv("FOXCLAW_TOOL_USE", "1")
|
|
@@ -6,26 +6,22 @@ import pytest
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def test_streaming():
|
|
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
11
|
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
14
12
|
os.environ.setdefault("FOXCLAW_EMBEDDING_MODEL", "foxclaw-nonexistent-fast")
|
|
15
|
-
os.environ["FOXCLAW_STREAM_RATE"] = "1"
|
|
13
|
+
os.environ["FOXCLAW_STREAM_RATE"] = "1"
|
|
16
14
|
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_stream_")
|
|
17
15
|
|
|
18
16
|
print("=" * 60); print("LLM 流式输出验证"); print("=" * 60)
|
|
19
17
|
|
|
20
|
-
# 1) router.stream(stub 模式):分块拼接 == 完整文本、多块
|
|
21
18
|
from foxclaw.core.model_router import ModelRouter
|
|
22
|
-
r = ModelRouter()
|
|
19
|
+
r = ModelRouter()
|
|
23
20
|
chunks = list(r.stream("sys", [{"role": "user", "content": "你好世界这是一段用于测试流式分块的文本"}]))
|
|
24
21
|
assert len(chunks) > 1, f"应多块: {len(chunks)}"
|
|
25
22
|
assert "".join(chunks), "拼接应非空"
|
|
26
23
|
print(f"[1] router.stream stub 分块(chunk={len(chunks)}) OK")
|
|
27
24
|
|
|
28
|
-
# 2) /api/chat/stream SSE:多个 delta + done 收尾
|
|
29
25
|
from fastapi.testclient import TestClient
|
|
30
26
|
from foxclaw.api import app
|
|
31
27
|
with TestClient(app) as c:
|
|
@@ -46,7 +42,6 @@ def test_streaming():
|
|
|
46
42
|
assert "".join(deltas), "拼接应非空"
|
|
47
43
|
print(f"[2] /api/chat/stream SSE(delta={len(deltas)}, done) OK")
|
|
48
44
|
|
|
49
|
-
# 3) 限流:桶=1,上面 [2] 已消耗 → 再请求被限流
|
|
50
45
|
with c.stream("POST", "/api/chat/stream",
|
|
51
46
|
json={"task": "再来一次", "agent_id": "supervisor"}) as resp:
|
|
52
47
|
txt = "".join(resp.iter_text())
|
|
@@ -26,7 +26,7 @@ from pathlib import Path
|
|
|
26
26
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
27
27
|
|
|
28
28
|
import foxclaw.core.web_search as ws
|
|
29
|
-
ws.needs_search = lambda q: False
|
|
29
|
+
ws.needs_search = lambda q: False
|
|
30
30
|
|
|
31
31
|
from foxclaw import Foxclaw
|
|
32
32
|
from foxclaw.agents.base import CognitiveAgent, HarnessBlockedError
|
|
@@ -59,7 +59,7 @@ def main():
|
|
|
59
59
|
check(_names() == EXPECTED_STAGES, f"主管 stage 序列={EXPECTED_STAGES}(实为 {_names()})")
|
|
60
60
|
|
|
61
61
|
os_ = Foxclaw(data_dir=tempfile.mkdtemp(prefix="foxclaw_svpipe_"))
|
|
62
|
-
os_.router.provider = "stub"
|
|
62
|
+
os_.router.provider = "stub"
|
|
63
63
|
os_.router._client = None
|
|
64
64
|
sup = os_.supervisor
|
|
65
65
|
|
|
@@ -74,7 +74,7 @@ def main():
|
|
|
74
74
|
"返回体由 base 模板统一组装(agent/session_id/thought_summary)")
|
|
75
75
|
|
|
76
76
|
print("\n【3】单 Agent 分发:返回体顶层键与旧实现逐字一致")
|
|
77
|
-
r = asyncio.run(sup.run_task("帮我做个预算分析"))
|
|
77
|
+
r = asyncio.run(sup.run_task("帮我做个预算分析"))
|
|
78
78
|
res = r["result"]
|
|
79
79
|
check(res.get("dispatched_to") == "finance", f"关键词命中 → finance(实为 {res.get('dispatched_to')})")
|
|
80
80
|
for k in ("schedule", "plan", "process_id"):
|
|
@@ -91,7 +91,7 @@ def main():
|
|
|
91
91
|
print("\n【5】统一 execution.completed(文稿 Step 4)")
|
|
92
92
|
spine = os_.event_spine
|
|
93
93
|
before = len(spine.history("execution.completed"))
|
|
94
|
-
asyncio.run(sup.run_task("同时查今天的日程、分析本月理财、总结这篇论文"))
|
|
94
|
+
asyncio.run(sup.run_task("同时查今天的日程、分析本月理财、总结这篇论文"))
|
|
95
95
|
evts = spine.history("execution.completed")
|
|
96
96
|
new_evts = evts[before:]
|
|
97
97
|
check(len(new_evts) == 1,
|
|
@@ -112,7 +112,6 @@ def main():
|
|
|
112
112
|
new_evts = spine.history("execution.completed")[before:]
|
|
113
113
|
check(len(new_evts) == 1 and new_evts[0]["payload"].get("blocked") is True,
|
|
114
114
|
"被拦任务的执行事件 blocked=True → 真值链不沉淀")
|
|
115
|
-
# 还原 finance,别污染后续用例
|
|
116
115
|
del os_.finance._execute
|
|
117
116
|
|
|
118
117
|
print("\n【6】流式:run_task_stream 八 stage + 子 Agent token 传播 + 中断")
|
|
@@ -20,7 +20,7 @@ from pathlib import Path
|
|
|
20
20
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
21
21
|
|
|
22
22
|
import foxclaw.core.web_search as ws
|
|
23
|
-
ws.needs_search = lambda q: False
|
|
23
|
+
ws.needs_search = lambda q: False
|
|
24
24
|
|
|
25
25
|
from foxclaw import Foxclaw
|
|
26
26
|
|
|
@@ -31,8 +31,6 @@ def check(cond, msg):
|
|
|
31
31
|
print(f" ✓ {msg}")
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
# 分诊 prompt 的特征串。历史上 _llm_classify 用它问模型;现已无此路径,
|
|
35
|
-
# 故断言它**从不出现**,即「不再为分诊多花一次 LLM 往返」。
|
|
36
34
|
CLASSIFY_MARK = "只输出一个词"
|
|
37
35
|
|
|
38
36
|
|
|
@@ -47,19 +45,16 @@ def main():
|
|
|
47
45
|
def fake(system, messages, max_tokens=2048, agent_id=""):
|
|
48
46
|
content = str(messages[-1]["content"]) if messages else ""
|
|
49
47
|
calls.append(content)
|
|
50
|
-
if CLASSIFY_MARK in content:
|
|
48
|
+
if CLASSIFY_MARK in content:
|
|
51
49
|
return "finance"
|
|
52
50
|
return "已处理"
|
|
53
51
|
|
|
54
|
-
# 让路由认为模型可用(非 stub)
|
|
55
52
|
os_.router.provider = "deepseek"
|
|
56
53
|
os_.router._client = object()
|
|
57
54
|
os_.router.complete = fake
|
|
58
55
|
check(os_.router.is_stub is False, "router 处于'可用'状态(非 stub)")
|
|
59
56
|
|
|
60
57
|
print("\n【1】关键词未命中 + 模型可用 → 主管直接自答(不再二次分诊)")
|
|
61
|
-
# 这句不含任何领域关键词,也不像"花了钱"的报备(没有完成态动词),
|
|
62
|
-
# 所以仍然走自答 —— 花费报备现在已算作财务命中,见【1b】。
|
|
63
58
|
no_kw = "讲个笑话听听"
|
|
64
59
|
check(os_.supervisor._has_keyword_hit(no_kw) is False, f"「{no_kw[:12]}…」确无关键词命中")
|
|
65
60
|
r = asyncio.run(os_.supervisor.run_task(no_kw))
|
|
@@ -73,13 +68,12 @@ def main():
|
|
|
73
68
|
for line in ("今天买了一件衣服花了105块", "后天花了100洗车"):
|
|
74
69
|
check(os_.supervisor._has_keyword_hit(line), f"「{line[:12]}…」算作领域命中")
|
|
75
70
|
check(os_.supervisor._route(line) == "finance", f"「{line[:12]}…」→ finance")
|
|
76
|
-
# 讨论额度/比例不算报备,但仍属于财务问题 → 也交财务,而不是主管自答
|
|
77
71
|
check(os_.supervisor._route("每月房租3000占收入10000这样合理吗") == "finance",
|
|
78
72
|
"房租占比咨询 → finance")
|
|
79
73
|
|
|
80
74
|
print("\n【2】关键词命中 → 直接路由,不问 LLM 分诊(省成本)")
|
|
81
75
|
calls.clear()
|
|
82
|
-
r2 = asyncio.run(os_.supervisor.run_task("帮我做个预算分析"))
|
|
76
|
+
r2 = asyncio.run(os_.supervisor.run_task("帮我做个预算分析"))
|
|
83
77
|
d2 = r2["result"].get("dispatched_to")
|
|
84
78
|
check(d2 == "finance", f"命中'预算' → 直接 finance(实为 {d2})")
|
|
85
79
|
check(not any(CLASSIFY_MARK in c for c in calls), "关键词命中时不发出分诊 prompt")
|
|
@@ -88,7 +82,7 @@ def main():
|
|
|
88
82
|
os_.router.provider = "stub"
|
|
89
83
|
os_.router._client = None
|
|
90
84
|
calls.clear()
|
|
91
|
-
r3 = asyncio.run(os_.supervisor.run_task("解释一下什么是航天器"))
|
|
85
|
+
r3 = asyncio.run(os_.supervisor.run_task("解释一下什么是航天器"))
|
|
92
86
|
check(os_.router.is_stub is True, "router 回到 stub")
|
|
93
87
|
d3 = r3["result"].get("dispatched_to")
|
|
94
88
|
check(d3 == "supervisor", f"stub 下未命中 → 主管自身处理(实为 {d3})")
|
|
@@ -36,18 +36,15 @@ def _skill(side_effect="read", read_only=True, external=False, destructive=False
|
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
# ── 1. taint_level 分级 ───────────────────────────────────────────────
|
|
40
39
|
def test_taint_level_grading():
|
|
41
40
|
assert IG.taint_level("web_search") == IG.TAINT_EXTERNAL == 1
|
|
42
41
|
assert IG.taint_level("knowledge") == IG.TAINT_PERSONAL == 2
|
|
43
42
|
assert IG.taint_level("file") == IG.TAINT_PERSONAL == 2
|
|
44
43
|
assert IG.taint_level("memory") == IG.TAINT_SENSITIVE == 3
|
|
45
|
-
assert IG.taint_level("unknown_source") == IG.TAINT_EXTERNAL
|
|
44
|
+
assert IG.taint_level("unknown_source") == IG.TAINT_EXTERNAL
|
|
46
45
|
|
|
47
46
|
|
|
48
|
-
# ── 2. permission 污点门槛 ─────────────────────────────────────────────
|
|
49
47
|
def test_permission_taint_default_off_behavior_unchanged():
|
|
50
|
-
# 不传污点参数(默认关闭)→ 外发工具走原有 Stage 4 副作用确认
|
|
51
48
|
s = _skill(side_effect="send", read_only=False)
|
|
52
49
|
d = PERM.evaluate(s, {})
|
|
53
50
|
assert d.decision == "ask" and d.reason_code == PERM.SIDE_EFFECT_CONFIRM
|
|
@@ -77,19 +74,17 @@ def test_permission_taint_not_trigger_on_readonly():
|
|
|
77
74
|
|
|
78
75
|
def test_permission_taint_not_trigger_below_threshold():
|
|
79
76
|
s = _skill(side_effect="send", read_only=False)
|
|
80
|
-
# 污点只有 EXTERNAL(1) < 阈值 PERSONAL(2)→ 不因污点拦,走副作用确认
|
|
81
77
|
d = PERM.evaluate(s, {}, taint_level=IG.TAINT_EXTERNAL,
|
|
82
78
|
taint_threshold=IG.TAINT_PERSONAL)
|
|
83
79
|
assert d.reason_code == PERM.SIDE_EFFECT_CONFIRM
|
|
84
80
|
|
|
85
81
|
|
|
86
|
-
# ── 3. base 污点传播 ───────────────────────────────────────────────────
|
|
87
82
|
def test_taint_helpers_on_fake_agent():
|
|
88
83
|
a = types.SimpleNamespace(_think=lambda **k: None)
|
|
89
84
|
assert _taint_get(a) == 0
|
|
90
85
|
_taint_raise(a, IG.TAINT_EXTERNAL)
|
|
91
86
|
_taint_raise(a, IG.TAINT_SENSITIVE)
|
|
92
|
-
assert _taint_get(a) == IG.TAINT_SENSITIVE
|
|
87
|
+
assert _taint_get(a) == IG.TAINT_SENSITIVE
|
|
93
88
|
_taint_reset(a)
|
|
94
89
|
assert _taint_get(a) == 0
|
|
95
90
|
|
|
@@ -9,11 +9,8 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
9
9
|
def test_task_stream():
|
|
10
10
|
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
11
11
|
os.environ.setdefault("FOXCLAW_EMBEDDING_MODEL", "foxclaw-nonexistent-fast")
|
|
12
|
-
os.environ["FOXCLAW_STREAM_RATE"] = "60"
|
|
12
|
+
os.environ["FOXCLAW_STREAM_RATE"] = "60"
|
|
13
13
|
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_taskstream_")
|
|
14
|
-
# 强制 stub(离线/确定性/免费):关掉 .env,使进程环境变量成为唯一来源。
|
|
15
|
-
# (旧写法是逐个把 key 置空以依赖 load_dotenv(override=False)——但那挡不住「先 pop 再
|
|
16
|
-
# import」的路径,也挡不住 .env 里的 FOXCLAW_MCP_SERVER 等非 key 变量。见 api.py 说明。)
|
|
17
14
|
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
18
15
|
for _k in ("DEEPSEEK_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY",
|
|
19
16
|
"GOOGLE_API_KEY", "DASHSCOPE_API_KEY", "FOXCLAW_API_KEY"):
|
|
@@ -24,16 +21,11 @@ def test_task_stream():
|
|
|
24
21
|
from fastapi.testclient import TestClient
|
|
25
22
|
import foxclaw.api as _api
|
|
26
23
|
from foxclaw.api import app
|
|
27
|
-
# pytest 同进程下,test_streaming.py 可能已把模块级流式限流桶设为 rate=1(并耗尽),
|
|
28
|
-
# 而本测试要放开限流跑两条流式通路 —— 复位桶容量/速率/余量(限流语义由 test_streaming 覆盖)。
|
|
29
24
|
_api._STREAM_LIMITER.capacity = 60
|
|
30
25
|
_api._STREAM_LIMITER.rate = 1.0
|
|
31
26
|
_api._STREAM_LIMITER.tokens = 60.0
|
|
32
27
|
|
|
33
28
|
with TestClient(app) as c:
|
|
34
|
-
# 1) 正常流式:SSE 收到 stage 进度 + act delta + 恰一个 result,无 error
|
|
35
|
-
# 拓扑 5 → 3:领域 Agent 才走 DEFAULT_STAGES(含 act),主管走路由管线
|
|
36
|
-
# (recall→route→dispatch→aggregate),不会产出 act —— 故此处用 office。
|
|
37
29
|
with c.stream("POST", "/api/task/stream",
|
|
38
30
|
json={"task": "帮我分析一下本月开销", "agent_id": "office"}) as resp:
|
|
39
31
|
assert resp.status_code == 200, resp.status_code
|
|
@@ -53,9 +45,6 @@ def test_task_stream():
|
|
|
53
45
|
assert results[0].get("agent") == "office" and "result" in results[0], results[0]
|
|
54
46
|
print(f"[1] /api/task/stream SSE(stage={len(stages)} delta={len(deltas)} result=1) OK")
|
|
55
47
|
|
|
56
|
-
# 2) 【P3-3】主管不再回落领域 Agent:P1-1 pipeline 化后主管自身支持流式,
|
|
57
|
-
# stage 事件是主管管线七阶段,result.agent=supervisor,路由结论照常(stub 无关键词
|
|
58
|
-
# → 主管自身处理)。
|
|
59
48
|
with c.stream("POST", "/api/task/stream",
|
|
60
49
|
json={"task": "随便问问", "agent_id": "supervisor"}) as resp:
|
|
61
50
|
assert resp.status_code == 200, resp.status_code
|
|
@@ -15,7 +15,6 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def test_chat_stream_emits_named_thinking_event():
|
|
18
|
-
# ISOLATION_GUARD:依赖 import 期固化的单例(路由配置/限流器),须独立进程跑。
|
|
19
18
|
if "foxclaw.api" in sys.modules:
|
|
20
19
|
import pytest
|
|
21
20
|
pytest.skip("需独立进程运行(模块级单例已固化),请用 tests/run_all.py")
|
|
@@ -19,67 +19,56 @@ def test_tool_center():
|
|
|
19
19
|
with TestClient(app) as c:
|
|
20
20
|
print("=" * 60); print("工具中心 · 技能台 + 审核门 + 治理 验证"); print("=" * 60)
|
|
21
21
|
|
|
22
|
-
# 1. 安装 web_api 技能(https 只读 → allow)
|
|
23
22
|
r = c.post("/api/ahs/skills", json=WEBAPI)
|
|
24
23
|
assert r.status_code == 200, r.text
|
|
25
24
|
assert r.json()["created"] is True
|
|
26
25
|
print("[1] 安装 web_api 技能 OK")
|
|
27
26
|
|
|
28
|
-
# 2. 列表里出现且 source=user
|
|
29
27
|
r = c.get("/api/ahs/skills"); assert r.status_code == 200
|
|
30
28
|
ids = {s["id"]: s for s in r.json()["skills"]}
|
|
31
29
|
assert "wx_weather" in ids and ids["wx_weather"]["source"] == "user"
|
|
32
30
|
print("[2] 列表含新技能且 source=user OK")
|
|
33
31
|
|
|
34
|
-
# 3. 拒绝 local kind
|
|
35
32
|
r = c.post("/api/ahs/skills", json={**WEBAPI, "id": "x_local", "kind": "local"})
|
|
36
33
|
assert r.status_code == 400
|
|
37
34
|
print("[3] 拒绝 local kind(400) OK")
|
|
38
35
|
|
|
39
|
-
# 4. 拒绝非法 endpoint
|
|
40
36
|
r = c.post("/api/ahs/skills", json={**WEBAPI, "id": "x_bad", "endpoint": "ftp://x"})
|
|
41
37
|
assert r.status_code == 400
|
|
42
38
|
print("[4] 拒非法 endpoint(400) OK")
|
|
43
39
|
|
|
44
|
-
# 5. mcp 缺字段
|
|
45
40
|
r = c.post("/api/ahs/skills", json={"id": "x_mcp", "name": "m", "kind": "mcp", "side_effect": "read"})
|
|
46
41
|
assert r.status_code == 400
|
|
47
42
|
print("[5] mcp 缺 server/tool(400) OK")
|
|
48
43
|
|
|
49
|
-
# 6. 启停
|
|
50
44
|
r = c.patch("/api/ahs/skills/wx_weather", json={"enabled": False})
|
|
51
45
|
assert r.status_code == 200 and r.json()["skill"]["enabled"] is False
|
|
52
46
|
r = c.patch("/api/ahs/skills/wx_weather", json={"enabled": True})
|
|
53
47
|
assert r.json()["skill"]["enabled"] is True
|
|
54
48
|
print("[6] 启停切换 OK")
|
|
55
49
|
|
|
56
|
-
# 7. 删内置 → 409(取列表里第一个 builtin)
|
|
57
50
|
r = c.get("/api/ahs/skills")
|
|
58
51
|
builtin = next((s["id"] for s in r.json()["skills"] if s["source"] == "builtin"), None)
|
|
59
52
|
if builtin:
|
|
60
53
|
rd = c.delete(f"/api/ahs/skills/{builtin}")
|
|
61
54
|
assert rd.status_code == 409, rd.text
|
|
62
55
|
print("[7] 内置技能不可删(409) OK")
|
|
63
|
-
# 7b. 用内置 id 安装 → 拒(不能覆盖内置)
|
|
64
56
|
rc = c.post("/api/ahs/skills", json={**WEBAPI, "id": builtin})
|
|
65
57
|
assert rc.status_code == 409, rc.text
|
|
66
58
|
print("[7b] 用内置 id 安装被拒(409) OK")
|
|
67
59
|
else:
|
|
68
60
|
print("[7] 跳过(无内置技能)")
|
|
69
61
|
|
|
70
|
-
# 8. 测试调用:read 技能直接跑(返回 SkillResult 结构)
|
|
71
62
|
r = c.post("/api/ahs/skills/wx_weather/test", json={"args": {}})
|
|
72
63
|
assert r.status_code == 200 and "success" in r.json()
|
|
73
64
|
print("[8] 测试调用返回 SkillResult OK")
|
|
74
65
|
|
|
75
|
-
# 9. 安装审核门:打本地地址的 web_api 安装期被 deny(403)
|
|
76
66
|
r = c.post("/api/ahs/skills", json={"id": "ssrf_t", "name": "s", "description": "", "kind": "web_api",
|
|
77
67
|
"triggers": ["x"], "side_effect": "read", "endpoint": "http://127.0.0.1:8000/api/status"})
|
|
78
68
|
assert r.status_code == 403, r.text
|
|
79
69
|
assert r.json()["detail"]["error"] == "install_denied"
|
|
80
70
|
print("[9] 安装审核门 deny 本地地址(403) OK")
|
|
81
71
|
|
|
82
|
-
# 9b. 有副作用技能未确认 → 409 needs_ack;acknowledged 后 → 200(且持久,供步12 round-trip)
|
|
83
72
|
_send = {"id": "send_x", "name": "发信", "description": "", "kind": "web_api",
|
|
84
73
|
"triggers": ["发"], "side_effect": "send", "endpoint": "https://example.com/send"}
|
|
85
74
|
r = c.post("/api/ahs/skills", json=_send)
|
|
@@ -88,19 +77,16 @@ def test_tool_center():
|
|
|
88
77
|
assert r.status_code == 200, r.text
|
|
89
78
|
print("[9b] 副作用技能需确认(409→200) OK")
|
|
90
79
|
|
|
91
|
-
# 10. 治理端点可用
|
|
92
80
|
assert c.get("/api/ahs/breaker").status_code == 200
|
|
93
81
|
assert c.get("/api/ahs/hooks/audit").status_code == 200
|
|
94
82
|
print("[10] 治理端点(breaker/audit) OK")
|
|
95
83
|
|
|
96
|
-
# 10b. 信任分级:内置 calculator(local)=high、内置 mcp=low、用户 web_api=mid
|
|
97
84
|
sk = {s["id"]: s for s in c.get("/api/ahs/skills").json()["skills"]}
|
|
98
85
|
assert sk["calculator"]["trust"] == "high", sk["calculator"]["trust"]
|
|
99
|
-
assert sk["gmail_send"]["trust"] == "low", sk["gmail_send"]["trust"]
|
|
86
|
+
assert sk["gmail_send"]["trust"] == "low", sk["gmail_send"]["trust"]
|
|
100
87
|
assert sk["wx_weather"]["trust"] == "mid"
|
|
101
88
|
print("[10b] 信任分级 high/mid/low OK")
|
|
102
89
|
|
|
103
|
-
# 10c. deny 封杀:封杀后测试调用被拦 + blocked 标;解封恢复
|
|
104
90
|
c.post("/api/ahs/policy/deny", json={"id": "wx_weather"})
|
|
105
91
|
r = c.post("/api/ahs/skills/wx_weather/test", json={"args": {}, "confirmed": True}).json()
|
|
106
92
|
assert r["status"] == "blocked" and "封杀" in r["message"], r
|
|
@@ -111,12 +97,10 @@ def test_tool_center():
|
|
|
111
97
|
assert next(s for s in c.get("/api/ahs/skills").json()["skills"] if s["id"] == "wx_weather")["blocked"] is False
|
|
112
98
|
print("[10c] deny 封杀/解封 kill-switch OK")
|
|
113
99
|
|
|
114
|
-
# 11. 删除用户技能
|
|
115
100
|
assert c.delete("/api/ahs/skills/wx_weather").status_code == 200
|
|
116
101
|
r = c.get("/api/ahs/skills"); assert "wx_weather" not in {s["id"] for s in r.json()["skills"]}
|
|
117
102
|
print("[11] 删除用户技能 OK")
|
|
118
103
|
|
|
119
|
-
# 12. 持久化 round-trip:直建新 OS(同数据目录),已安装的 send_x 应仍在
|
|
120
104
|
from foxclaw import Foxclaw
|
|
121
105
|
os2 = Foxclaw(data_dir=_DATA)
|
|
122
106
|
assert os2.skill_registry.get("send_x") is not None, "重启后用户技能丢失"
|