foxclaw 0.1.0 → 0.1.2
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/README.md +54 -29
- package/bin/foxclaw.cjs +51 -0
- package/index.js +11 -10
- package/package.json +46 -38
- package/python/.dockerignore +35 -0
- package/python/.env.example +165 -0
- package/python/.gitattributes +46 -0
- package/python/.github/workflows/ci.yml +122 -0
- package/python/.github/workflows/release.yml +183 -0
- package/python/Dockerfile +64 -0
- package/python/LICENSE +21 -0
- package/python/MANIFEST.in +14 -0
- package/python/README.md +192 -0
- package/python/README_CN.md +324 -0
- package/python/bench/bm25_audit.py +199 -0
- package/python/bench/bm25_regression.py +178 -0
- package/python/bench/bm25_stress.py +159 -0
- package/python/bench/cd_audit.py +238 -0
- package/python/bench/chaos.py +142 -0
- package/python/bench/concurrency.py +103 -0
- package/python/bench/e2e_real_models.py +95 -0
- package/python/bench/llm_retry_test.py +248 -0
- package/python/bench/load_test.py +196 -0
- package/python/bench/vector_ivf_upgrade.py +134 -0
- package/python/docs/CHANNELS.md +195 -0
- package/python/docs/GOVERNANCE.md +164 -0
- package/python/docs/RELEASE.md +109 -0
- package/python/docs/RUN.md +75 -0
- package/python/docs/RUN_CN.md +121 -0
- package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
- package/python/evals/__init__.py +0 -0
- package/python/evals/governance/README.md +61 -0
- package/python/evals/governance/__init__.py +0 -0
- package/python/evals/governance/cases.py +152 -0
- package/python/evals/governance/compare_embeddings.py +94 -0
- package/python/evals/governance/convergence.svg +11 -0
- package/python/evals/governance/report.json +371 -0
- package/python/evals/governance/run_evals.py +400 -0
- package/python/examples/governance_middleware_demo.py +77 -0
- package/python/examples/langgraph_governance_demo.py +108 -0
- package/python/examples/sample_itinerary.csv +6 -0
- package/python/examples/sample_knowledge.csv +9 -0
- package/python/examples/sample_transactions.csv +14 -0
- package/python/foxclaw/__init__.py +526 -0
- package/python/foxclaw/agents/__init__.py +12 -0
- package/python/foxclaw/agents/agents.py +35 -0
- package/python/foxclaw/agents/base.py +865 -0
- package/python/foxclaw/agents/impl/__init__.py +1 -0
- package/python/foxclaw/agents/impl/_helpers.py +78 -0
- package/python/foxclaw/agents/impl/finance.py +248 -0
- package/python/foxclaw/agents/impl/office.py +367 -0
- package/python/foxclaw/agents/impl/supervisor.py +659 -0
- package/python/foxclaw/agents/mixins/_context.py +151 -0
- package/python/foxclaw/agents/mixins/augment.py +149 -0
- package/python/foxclaw/agents/mixins/llm.py +66 -0
- package/python/foxclaw/agents/mixins/memory.py +35 -0
- package/python/foxclaw/agents/mixins/preference.py +81 -0
- package/python/foxclaw/agents/mixins/prompt.py +77 -0
- package/python/foxclaw/agents/mixins/skillbox.py +58 -0
- package/python/foxclaw/agents/mixins/tool_exec.py +443 -0
- package/python/foxclaw/api/__init__.py +283 -0
- package/python/foxclaw/api/app.py +123 -0
- package/python/foxclaw/api/middleware.py +317 -0
- package/python/foxclaw/api/routes/ahs.py +417 -0
- package/python/foxclaw/api/routes/attachments.py +117 -0
- package/python/foxclaw/api/routes/browser.py +39 -0
- package/python/foxclaw/api/routes/domain.py +314 -0
- package/python/foxclaw/api/routes/knowledge.py +163 -0
- package/python/foxclaw/api/routes/llm_admin.py +461 -0
- package/python/foxclaw/api/routes/memory.py +89 -0
- package/python/foxclaw/api/routes/orchestration.py +41 -0
- package/python/foxclaw/api/routes/recovery.py +43 -0
- package/python/foxclaw/api/routes/review.py +275 -0
- package/python/foxclaw/api/routes/search.py +37 -0
- package/python/foxclaw/api/routes/system.py +327 -0
- package/python/foxclaw/api/routes/task.py +281 -0
- package/python/foxclaw/api/routes/ws.py +68 -0
- package/python/foxclaw/api/spine.py +55 -0
- package/python/foxclaw/browser/__init__.py +13 -0
- package/python/foxclaw/browser/action/__init__.py +1 -0
- package/python/foxclaw/browser/action/action_executor.py +54 -0
- package/python/foxclaw/browser/action/action_types.py +45 -0
- package/python/foxclaw/browser/action/action_verifier.py +68 -0
- package/python/foxclaw/browser/action/url_policy.py +147 -0
- package/python/foxclaw/browser/adapter/__init__.py +1 -0
- package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
- package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
- package/python/foxclaw/browser/driver/__init__.py +1 -0
- package/python/foxclaw/browser/driver/base_driver.py +61 -0
- package/python/foxclaw/browser/driver/playwright_driver.py +166 -0
- package/python/foxclaw/browser/perception/__init__.py +1 -0
- package/python/foxclaw/browser/perception/dom_extractor.py +43 -0
- package/python/foxclaw/browser/perception/element_indexer.py +57 -0
- package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
- package/python/foxclaw/browser/perception/snapshot.py +29 -0
- package/python/foxclaw/browser/service.py +148 -0
- package/python/foxclaw/browser/session/__init__.py +1 -0
- package/python/foxclaw/browser/session/session_manager.py +57 -0
- package/python/foxclaw/capability/__init__.py +12 -0
- package/python/foxclaw/capability/embedding_service.py +67 -0
- package/python/foxclaw/channels/__init__.py +140 -0
- package/python/foxclaw/channels/discord.py +184 -0
- package/python/foxclaw/channels/dispatcher.py +204 -0
- package/python/foxclaw/channels/feishu.py +225 -0
- package/python/foxclaw/channels/openclaw_bridge.py +156 -0
- package/python/foxclaw/channels/signing.py +187 -0
- package/python/foxclaw/channels/telegram.py +121 -0
- package/python/foxclaw/channels/weixin.py +225 -0
- package/python/foxclaw/cli.py +154 -0
- package/python/foxclaw/cognitive/__init__.py +4 -0
- package/python/foxclaw/cognitive/reasoner.py +82 -0
- package/python/foxclaw/cognitive/state.py +147 -0
- package/python/foxclaw/core/__init__.py +9 -0
- package/python/foxclaw/core/audit_chain.py +133 -0
- package/python/foxclaw/core/audit_sink.py +100 -0
- package/python/foxclaw/core/compaction.py +283 -0
- package/python/foxclaw/core/config.py +65 -0
- package/python/foxclaw/core/confirm_nonce.py +157 -0
- package/python/foxclaw/core/dispatch.py +27 -0
- package/python/foxclaw/core/envfile.py +48 -0
- package/python/foxclaw/core/failure_policy.py +151 -0
- package/python/foxclaw/core/gateway.py +85 -0
- package/python/foxclaw/core/governance_audit.py +175 -0
- package/python/foxclaw/core/idempotency.py +289 -0
- package/python/foxclaw/core/injection_guard.py +221 -0
- package/python/foxclaw/core/memory_adapter.py +516 -0
- package/python/foxclaw/core/metrics.py +89 -0
- package/python/foxclaw/core/model_router.py +1097 -0
- package/python/foxclaw/core/paths.py +103 -0
- package/python/foxclaw/core/recovery.py +362 -0
- package/python/foxclaw/core/scheduler.py +144 -0
- package/python/foxclaw/core/secrets.py +134 -0
- package/python/foxclaw/core/sessions.py +157 -0
- package/python/foxclaw/core/settings.py +430 -0
- package/python/foxclaw/core/shared_write.py +198 -0
- package/python/foxclaw/core/steering_queue.py +153 -0
- package/python/foxclaw/core/system_bus.py +84 -0
- package/python/foxclaw/core/thought_chain.py +318 -0
- package/python/foxclaw/core/token_meter.py +235 -0
- package/python/foxclaw/core/tools.py +172 -0
- package/python/foxclaw/core/tracing.py +390 -0
- package/python/foxclaw/core/truth_pipeline.py +114 -0
- package/python/foxclaw/core/vec_recall_adapter.py +503 -0
- package/python/foxclaw/core/verify.py +181 -0
- package/python/foxclaw/core/web_search.py +242 -0
- package/python/foxclaw/finance/__init__.py +15 -0
- package/python/foxclaw/finance/evidence.py +153 -0
- package/python/foxclaw/finance/fx.py +100 -0
- package/python/foxclaw/finance/graph.py +188 -0
- package/python/foxclaw/finance/models.py +150 -0
- package/python/foxclaw/finance/nlparse.py +575 -0
- package/python/foxclaw/finance/policy.py +329 -0
- package/python/foxclaw/finance/reasoner.py +129 -0
- package/python/foxclaw/finance/reasoning.py +340 -0
- package/python/foxclaw/finance/reconcile.py +180 -0
- package/python/foxclaw/finance/runtime.py +527 -0
- package/python/foxclaw/governance/__init__.py +24 -0
- package/python/foxclaw/governance/middleware.py +184 -0
- package/python/foxclaw/harness/__init__.py +17 -0
- package/python/foxclaw/harness/_base.py +72 -0
- package/python/foxclaw/harness/_policy.py +359 -0
- package/python/foxclaw/harness/drift.py +89 -0
- package/python/foxclaw/harness/event_bus.py +31 -0
- package/python/foxclaw/harness/harness.py +446 -0
- package/python/foxclaw/harness/loop_detection.py +152 -0
- package/python/foxclaw/itinerary/__init__.py +10 -0
- package/python/foxclaw/itinerary/amap_client.py +117 -0
- package/python/foxclaw/itinerary/models.py +80 -0
- package/python/foxclaw/itinerary/reasoner.py +101 -0
- package/python/foxclaw/itinerary/reasoning.py +174 -0
- package/python/foxclaw/itinerary/runtime.py +148 -0
- package/python/foxclaw/kernel/__init__.py +13 -0
- package/python/foxclaw/kernel/capabilities.py +73 -0
- package/python/foxclaw/kernel/policy_gate.py +114 -0
- package/python/foxclaw/kernel/process_manager.py +205 -0
- package/python/foxclaw/kernel/scheduler.py +131 -0
- package/python/foxclaw/kernel/sentinel.py +453 -0
- package/python/foxclaw/kernel/sentinel_client.py +246 -0
- package/python/foxclaw/kernel/spine.py +97 -0
- package/python/foxclaw/knowledge/__init__.py +376 -0
- package/python/foxclaw/knowledge/truth_crystallizer.py +92 -0
- package/python/foxclaw/learning/__init__.py +12 -0
- package/python/foxclaw/learning/knowledge_graph.py +149 -0
- package/python/foxclaw/learning/models.py +93 -0
- package/python/foxclaw/learning/reasoner.py +70 -0
- package/python/foxclaw/learning/runtime.py +363 -0
- package/python/foxclaw/learning/spaced_repetition.py +96 -0
- package/python/foxclaw/mcp_server.py +189 -0
- package/python/foxclaw/orchestration/__init__.py +184 -0
- package/python/foxclaw/pipeline/__init__.py +23 -0
- package/python/foxclaw/pipeline/cognitive_pipeline.py +481 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +230 -0
- package/python/foxclaw/planning/__init__.py +17 -0
- package/python/foxclaw/planning/llm_planner.py +232 -0
- package/python/foxclaw/planning/planner.py +99 -0
- package/python/foxclaw/search/__init__.py +24 -0
- package/python/foxclaw/search/agent_port.py +194 -0
- package/python/foxclaw/search/evidence.py +113 -0
- package/python/foxclaw/search/external_search.py +50 -0
- package/python/foxclaw/search/hkc_read_port.py +60 -0
- package/python/foxclaw/search/internal_search.py +73 -0
- package/python/foxclaw/search/models.py +129 -0
- package/python/foxclaw/search/planner.py +44 -0
- package/python/foxclaw/search/retriever.py +39 -0
- package/python/foxclaw/search/runtime.py +134 -0
- package/python/foxclaw/search/strategy.py +44 -0
- package/python/foxclaw/skills/__init__.py +613 -0
- package/python/foxclaw/skills/hooks_spec.py +73 -0
- package/python/foxclaw/skills/lifecycle.py +278 -0
- package/python/foxclaw/skills/markdown_skill.py +174 -0
- package/python/foxclaw/skills/markdown_store.py +39 -0
- package/python/foxclaw/skills/mcp_client.py +190 -0
- package/python/foxclaw/skills/mcp_store.py +43 -0
- package/python/foxclaw/skills/permission.py +180 -0
- package/python/foxclaw/skills/pipeline.py +386 -0
- package/python/foxclaw/skills/policy.py +80 -0
- package/python/foxclaw/skills/review.py +72 -0
- package/python/foxclaw/skills/store.py +57 -0
- package/python/frontend/assets/core_bg.jpg +0 -0
- package/python/frontend/deck/auth.js +32 -0
- package/python/frontend/deck/chat_model_picker.js +537 -0
- package/python/frontend/deck/domain.js +296 -0
- package/python/frontend/deck/main.js +1950 -0
- package/python/frontend/deck/markdown.js +48 -0
- package/python/frontend/deck/settings.js +324 -0
- package/python/frontend/deck/state.js +6 -0
- package/python/frontend/deck/util.js +6 -0
- package/python/frontend/deck/visuals.js +212 -0
- package/python/frontend/deck.css +855 -0
- package/python/frontend/deck.html +686 -0
- package/python/frontend/deck_v2.html +341 -0
- package/python/frontend/planet/index.html +266 -0
- package/python/frontend/planet/planet.css +579 -0
- package/python/frontend/planet/planet.js +1637 -0
- package/python/frontend/planet/vendor/three.core.min.js +6 -0
- package/python/frontend/planet/vendor/three.module.min.js +6 -0
- package/python/frontend/v3/assets/index-BD7VPbMV.js +33 -0
- package/python/frontend/v3/assets/index-CAvnYWGH.css +1 -0
- package/python/frontend/v3/index.html +15 -0
- package/python/frontend/widget_runtime.js +295 -0
- package/python/frontend/widgets.js +696 -0
- package/python/frontend_src/index.html +14 -0
- package/python/frontend_src/package-lock.json +1376 -0
- package/python/frontend_src/package.json +19 -0
- package/python/frontend_src/src/App.vue +62 -0
- package/python/frontend_src/src/api.js +89 -0
- package/python/frontend_src/src/components/ChatDock.vue +377 -0
- package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
- package/python/frontend_src/src/components/DocViewer.vue +171 -0
- package/python/frontend_src/src/components/Inspector.vue +241 -0
- package/python/frontend_src/src/components/LeftRail.vue +394 -0
- package/python/frontend_src/src/components/MenuPopover.vue +130 -0
- package/python/frontend_src/src/components/MessageStream.vue +401 -0
- package/python/frontend_src/src/components/ModelPicker.vue +355 -0
- package/python/frontend_src/src/components/QuickIngest.vue +118 -0
- package/python/frontend_src/src/components/Stage.vue +226 -0
- package/python/frontend_src/src/components/ToolMenu.vue +144 -0
- package/python/frontend_src/src/components/TopBar.vue +90 -0
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +84 -0
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +86 -0
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +441 -0
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +118 -0
- package/python/frontend_src/src/components/panels/PanelBox.vue +51 -0
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +308 -0
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +254 -0
- package/python/frontend_src/src/components/panels/TasksPanel.vue +74 -0
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +640 -0
- package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
- package/python/frontend_src/src/lib/fmt.js +48 -0
- package/python/frontend_src/src/lib/md.js +204 -0
- package/python/frontend_src/src/main.js +7 -0
- package/python/frontend_src/src/stores/app.js +106 -0
- package/python/frontend_src/src/stores/chat.js +484 -0
- package/python/frontend_src/src/stores/model.js +104 -0
- package/python/frontend_src/src/stores/panels.js +330 -0
- package/python/frontend_src/src/styles/base.css +138 -0
- package/python/frontend_src/src/styles/panel.css +117 -0
- package/python/frontend_src/src/styles/tokens.css +78 -0
- package/python/frontend_src/vite.config.js +46 -0
- package/python/pyproject.toml +265 -0
- package/python/skills/README.md +123 -0
- package/python/skills/execution-contract/SKILL.md +30 -0
- package/python/skills/general-assistant/SKILL.md +34 -0
- package/python/skills/planner/SKILL.md +33 -0
- package/python/skills/schedule-itinerary/SKILL.md +35 -0
- package/python/skills/skill-composition/SKILL.md +31 -0
- package/python/skills/skill-guard/SKILL.md +67 -0
- package/python/skills/skill-memory/SKILL.md +50 -0
- package/python/skills/skill-router/SKILL.md +35 -0
- package/python/tests/conftest.py +76 -0
- package/python/tests/run_all.py +263 -0
- package/python/tests/test_agent_lifecycle.py +360 -0
- package/python/tests/test_ahs_p2.py +414 -0
- package/python/tests/test_anthropic_tools.py +206 -0
- package/python/tests/test_api.py +87 -0
- package/python/tests/test_api_auth.py +83 -0
- package/python/tests/test_api_host_guard.py +69 -0
- package/python/tests/test_attachments.py +167 -0
- package/python/tests/test_audit_chain.py +121 -0
- package/python/tests/test_audit_sink.py +136 -0
- package/python/tests/test_browser.py +153 -0
- package/python/tests/test_browser_request_guard.py +184 -0
- package/python/tests/test_capabilities.py +158 -0
- package/python/tests/test_channel_signing.py +192 -0
- package/python/tests/test_channel_trust.py +109 -0
- package/python/tests/test_channels.py +184 -0
- package/python/tests/test_cli.py +46 -0
- package/python/tests/test_compaction_toolpair.py +119 -0
- package/python/tests/test_config_layering.py +65 -0
- package/python/tests/test_confirm_nonce.py +197 -0
- package/python/tests/test_context_compaction_wiring.py +130 -0
- package/python/tests/test_dispatch.py +95 -0
- package/python/tests/test_domain_evals.py +403 -0
- package/python/tests/test_domain_import.py +78 -0
- package/python/tests/test_domain_reasoners.py +148 -0
- package/python/tests/test_e2e.py +178 -0
- package/python/tests/test_emb_cache.py +107 -0
- package/python/tests/test_embedding_singleton.py +89 -0
- package/python/tests/test_envfile.py +83 -0
- package/python/tests/test_evals_smoke.py +114 -0
- package/python/tests/test_event_spine.py +182 -0
- package/python/tests/test_evidence_semantic.py +101 -0
- package/python/tests/test_evidence_sse.py +97 -0
- package/python/tests/test_file_ingest.py +146 -0
- package/python/tests/test_finance.py +153 -0
- package/python/tests/test_finance_ledger.py +252 -0
- package/python/tests/test_finance_persist.py +178 -0
- package/python/tests/test_gateway.py +144 -0
- package/python/tests/test_governance.py +468 -0
- package/python/tests/test_governance_audit.py +359 -0
- package/python/tests/test_governance_middleware.py +143 -0
- package/python/tests/test_hkc.py +286 -0
- package/python/tests/test_hkc_index_build_manager.py +339 -0
- package/python/tests/test_hkc_manifest_reuse.py +317 -0
- package/python/tests/test_hkc_search_event_sync.py +533 -0
- package/python/tests/test_import_path_guard.py +161 -0
- package/python/tests/test_injection_guard.py +211 -0
- package/python/tests/test_integration.py +141 -0
- package/python/tests/test_itinerary.py +103 -0
- package/python/tests/test_kee_semantic.py +89 -0
- package/python/tests/test_kernel.py +222 -0
- package/python/tests/test_learning.py +110 -0
- package/python/tests/test_llm_admin.py +160 -0
- package/python/tests/test_llm_planner.py +366 -0
- package/python/tests/test_markdown_skills.py +73 -0
- package/python/tests/test_markdown_skills_openclaw.py +71 -0
- package/python/tests/test_mcp_connectors.py +109 -0
- package/python/tests/test_mcp_guardian_discovery.py +91 -0
- package/python/tests/test_mcp_no_memory.py +43 -0
- package/python/tests/test_mcp_policy_gate.py +77 -0
- package/python/tests/test_mcp_server.py +95 -0
- package/python/tests/test_memory_recall.py +95 -0
- package/python/tests/test_metrics.py +66 -0
- package/python/tests/test_multi_chain.py +102 -0
- package/python/tests/test_multimodal.py +135 -0
- package/python/tests/test_negotiation.py +146 -0
- package/python/tests/test_orchestration.py +119 -0
- package/python/tests/test_orchestration_decoupled.py +109 -0
- package/python/tests/test_p3_p5_hardening.py +338 -0
- package/python/tests/test_parallel_dispatch.py +215 -0
- package/python/tests/test_pipeline.py +533 -0
- package/python/tests/test_plan_visible.py +86 -0
- package/python/tests/test_planner.py +95 -0
- package/python/tests/test_policy_gate.py +109 -0
- package/python/tests/test_reasoner.py +185 -0
- package/python/tests/test_release_check.py +263 -0
- package/python/tests/test_resume_checkpoint.py +156 -0
- package/python/tests/test_routing.py +95 -0
- package/python/tests/test_scheduler.py +141 -0
- package/python/tests/test_search.py +204 -0
- package/python/tests/test_secrets.py +159 -0
- package/python/tests/test_semantic.py +78 -0
- package/python/tests/test_sessions.py +91 -0
- package/python/tests/test_settings.py +281 -0
- package/python/tests/test_shadow_learning.py +330 -0
- package/python/tests/test_shared_write.py +207 -0
- package/python/tests/test_skill_taint_gate.py +154 -0
- package/python/tests/test_skills.py +133 -0
- package/python/tests/test_stats_and_events.py +165 -0
- package/python/tests/test_streaming.py +60 -0
- package/python/tests/test_supervisor_pipeline.py +177 -0
- package/python/tests/test_supervisor_triage.py +108 -0
- package/python/tests/test_taint.py +117 -0
- package/python/tests/test_task_stream.py +85 -0
- package/python/tests/test_thinking_sse.py +67 -0
- package/python/tests/test_thinking_stream.py +76 -0
- package/python/tests/test_tool_center.py +129 -0
- package/python/tests/test_tool_parallel.py +113 -0
- package/python/tests/test_tool_params.py +103 -0
- package/python/tests/test_tool_use.py +242 -0
- package/python/tests/test_toplevel_concurrency.py +117 -0
- package/python/tests/test_tracing.py +290 -0
- package/python/tests/test_truth_crystallizer.py +127 -0
- package/python/tests/test_truth_flow.py +93 -0
- package/python/tests/test_url_policy.py +53 -0
- package/python/tests/test_verify_evidence.py +225 -0
- package/python/tests/test_version_sync.py +71 -0
- package/python/tests/test_web_trigger.py +99 -0
- package/python/tools/check_faiss_type.py +31 -0
- package/python/tools/check_log.ps1 +46 -0
- package/python/tools/ivf_status.py +72 -0
- package/python/tools/mypy_baseline.txt +5 -0
- package/python/tools/mypy_ratchet.py +97 -0
- package/python/tools/pack.py +178 -0
- package/python/tools/release_check.py +349 -0
- package/python/tools/restart_foxclaw.ps1 +74 -0
- package/python/tools/shared_write_audit.py +204 -0
- package/python/tools/size_baseline.txt +28 -0
- package/python/tools/size_check.py +102 -0
- package/python/tools/verify_env.bat +2 -0
- package/python/tools/verify_env.py +25 -0
- package/python/vendor/HMR_LICENSE +37 -0
- package/python/vendor/HMR_README.md +65 -0
- package/python/vendor/hkc/LICENSE +37 -0
- package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
- package/python/vendor/hkc/hkc_ace/ace.py +457 -0
- package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
- package/python/vendor/hkc/hkc_api/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/container.py +416 -0
- package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
- package/python/vendor/hkc/hkc_api/main.py +157 -0
- package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
- package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
- package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
- package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
- package/python/vendor/hkc/hkc_api/schemas.py +153 -0
- package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
- package/python/vendor/hkc/hkc_core/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
- package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
- package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
- package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
- package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
- package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
- package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
- package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
- package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
- package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
- package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
- package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
- package/python/vendor/hkc/hkc_kde/kde.py +298 -0
- package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
- package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
- package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
- package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
- package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
- package/python/vendor/hkc/hkc_kde/models.py +74 -0
- package/python/vendor/hkc/hkc_kde/packager.py +329 -0
- package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
- package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
- package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
- package/python/vendor/hkc/hkc_kee/kee.py +541 -0
- package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
- package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
- package/python/vendor/hkc/hkc_sdk/base.py +151 -0
- package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
- package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
- package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
- package/python/vendor/hkc/hkc_sdk/models.py +171 -0
- package/python/vendor/hkc/hkc_search/__init__.py +0 -0
- package/python/vendor/hkc/hkc_search/bm25.py +325 -0
- package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
- package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
- package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
- package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
- package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
- package/python/vendor/hmr/__init__.py +0 -0
- package/python/vendor/hmr/core/__init__.py +0 -0
- package/python/vendor/hmr/core/hmr.py +684 -0
- package/python/vendor/hmr/core/models.py +310 -0
- package/python/vendor/hmr/engines/__init__.py +0 -0
- package/python/vendor/hmr/engines/evolution.py +710 -0
- package/python/vendor/hmr/engines/jit_compiler.py +472 -0
- package/python/vendor/hmr/engines/lifecycle.py +264 -0
- package/python/vendor/hmr/engines/policy.py +625 -0
- package/python/vendor/hmr/engines/recall.py +260 -0
- package/python/vendor/hmr/engines/runtime_state.py +158 -0
- package/python/vendor/hmr/engines/scheduler.py +323 -0
- package/python/vendor/hmr/engines/semantic.py +150 -0
- package/python/vendor/hmr/engines/temporal.py +261 -0
- package/python/vendor/hmr/engines/thought_chain.py +635 -0
- package/python/vendor/hmr/graph/__init__.py +0 -0
- package/python/vendor/hmr/graph/cwg.py +338 -0
- package/python/vendor/hmr/graph/memory_graph.py +530 -0
- package/python/vendor/hmr/storage/__init__.py +0 -0
- package/python/vendor/hmr/storage/memory_fs.py +300 -0
- package/python/vendor/hmr/storage/vector_store.py +520 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""统一 API 网关:per-client 令牌桶 + 配额 + 空闲淘汰 + 429/Retry-After + 启用哲学。
|
|
2
|
+
全离线(TestClient)。网关默认关,本测试显式 FOXCLAW_GATEWAY=1 + 低额度触发限流。"""
|
|
3
|
+
import os, sys, tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
def test_gateway():
|
|
9
|
+
# ISOLATION_GUARD:本用例依赖 import 时固化的模块级单例(网关/限流器/路由配置按 env 在 import 时定型),单进程全量跑会被前序用例污染 →
|
|
10
|
+
# 假红。全量请用 tests/run_all.py(每文件独立子进程)。
|
|
11
|
+
if "foxclaw.api" in sys.modules:
|
|
12
|
+
pytest.skip("需独立进程运行(模块级单例已固化),请用 tests/run_all.py")
|
|
13
|
+
# 本测试用 pop("FOXCLAW_API_KEY") 验证「未设即网关默认关」,但 pop 之后 load_dotenv
|
|
14
|
+
# (override=False)仍会从 .env 把键灌回来(开发者 .env 常带真实 FOXCLAW_API_KEY)→ 判定
|
|
15
|
+
# 随机器漂移。关掉 .env 使本测试自洽,单独运行也稳定。
|
|
16
|
+
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
17
|
+
# 必须在 import foxclaw.api 之前设好:网关实例在模块加载时按环境变量建
|
|
18
|
+
os.environ["FOXCLAW_GATEWAY"] = "1"
|
|
19
|
+
os.environ["FOXCLAW_GATEWAY_RATE"] = "5" # 每分钟 5 → 桶容量 5,突发 5 后限流
|
|
20
|
+
os.environ["FOXCLAW_GATEWAY_QUOTA"] = "1000"
|
|
21
|
+
os.environ["FOXCLAW_MAX_BODY_MB"] = "1" # 请求体硬顶降到 1MB 便于测 413
|
|
22
|
+
os.environ.pop("FOXCLAW_API_KEY", None)
|
|
23
|
+
os.environ["HKC_EMBEDDING"] = "stub"
|
|
24
|
+
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
25
|
+
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_gw_")
|
|
26
|
+
|
|
27
|
+
print("=" * 60); print("统一 API 网关验证"); print("=" * 60)
|
|
28
|
+
|
|
29
|
+
# ── Part A:Gateway 类单元 ─────────────────────────────────
|
|
30
|
+
from foxclaw.core.gateway import Gateway
|
|
31
|
+
|
|
32
|
+
# [1] 突发令牌桶:容量内放行,超出限流(reason=rate)
|
|
33
|
+
g = Gateway(rate_per_min=3, quota_per_hour=100000)
|
|
34
|
+
allowed = [g.check("a")[0] for _ in range(5)]
|
|
35
|
+
assert allowed[:3] == [True, True, True] and allowed[3] is False, allowed
|
|
36
|
+
ok, retry, reason = g.check("a")
|
|
37
|
+
assert not ok and reason == "rate" and retry >= 1, (ok, retry, reason)
|
|
38
|
+
print("[1] 令牌桶突发限流(3 放行→限流,带 retry)OK")
|
|
39
|
+
|
|
40
|
+
# [2] per-client 隔离:a 被限,b 不受影响
|
|
41
|
+
assert g.check("b")[0] is True, "客户端之间不隔离"
|
|
42
|
+
print("[2] per-client 隔离 OK")
|
|
43
|
+
|
|
44
|
+
# [3] 配额(固定窗口):超每小时额度 → reason=quota
|
|
45
|
+
g2 = Gateway(rate_per_min=100000, quota_per_hour=4)
|
|
46
|
+
cnt = sum(1 for _ in range(6) if g2.check("z")[0])
|
|
47
|
+
assert cnt == 4, cnt
|
|
48
|
+
ok, retry, reason = g2.check("z")
|
|
49
|
+
assert not ok and reason == "quota" and retry >= 1, (ok, retry, reason)
|
|
50
|
+
print("[3] 每小时配额封顶(reason=quota)OK")
|
|
51
|
+
|
|
52
|
+
# [4] 空闲淘汰:大量不同 client 不撑爆内存
|
|
53
|
+
g3 = Gateway(rate_per_min=100, quota_per_hour=100000, max_clients=64)
|
|
54
|
+
for i in range(500):
|
|
55
|
+
g3.check(f"c{i}")
|
|
56
|
+
assert g3.stats()["tracked_clients"] <= 64, g3.stats()
|
|
57
|
+
print(f"[4] 空闲淘汰(500 client → 追踪 {g3.stats()['tracked_clients']}≤64,防内存 DoS)OK")
|
|
58
|
+
|
|
59
|
+
# ── Part B:接入 FastAPI 中间件(TestClient)────────────────
|
|
60
|
+
from fastapi.testclient import TestClient
|
|
61
|
+
from foxclaw.api import app, _gateway_on
|
|
62
|
+
|
|
63
|
+
with TestClient(app) as c:
|
|
64
|
+
# [5] stats 端点:启用 + 额度可见
|
|
65
|
+
s = c.get("/api/gateway/stats").json()
|
|
66
|
+
assert s["enabled"] is True and s["rate_per_min"] == 5, s
|
|
67
|
+
print(f"[5] /api/gateway/stats(enabled={s['enabled']}, rate={s['rate_per_min']}/min)OK")
|
|
68
|
+
|
|
69
|
+
# [6] 超限 → 429 + Retry-After(同一 client=ip:testclient 共用桶)
|
|
70
|
+
codes = [c.get("/api/status").status_code for _ in range(12)]
|
|
71
|
+
assert 200 in codes and 429 in codes, codes
|
|
72
|
+
r429 = None
|
|
73
|
+
for _ in range(15):
|
|
74
|
+
r = c.get("/api/status")
|
|
75
|
+
if r.status_code == 429:
|
|
76
|
+
r429 = r; break
|
|
77
|
+
assert r429 is not None, "未触发 429"
|
|
78
|
+
assert r429.headers.get("Retry-After") and r429.json().get("retry_after", 0) >= 1, (r429.headers, r429.text)
|
|
79
|
+
print("[6] 突发超限 → 429 + Retry-After 头 OK")
|
|
80
|
+
|
|
81
|
+
# [7] 探针豁免:/health 不在网关下,始终放行
|
|
82
|
+
assert c.get("/health").status_code == 200, "探针被网关误限"
|
|
83
|
+
print("[7] 探针 /health 豁免网关 OK")
|
|
84
|
+
|
|
85
|
+
# [8] 请求体硬顶:Content-Length 超 1MB → 413(在读 body/sanitize 前拦下,始终生效)
|
|
86
|
+
big = c.post("/api/knowledge/ingest", json={"text": "x" * (1200 * 1024)})
|
|
87
|
+
assert big.status_code == 413, (big.status_code, big.text[:120])
|
|
88
|
+
print("[8] 超大请求体 → 413(读 body 前拦截)OK")
|
|
89
|
+
|
|
90
|
+
# [8b] chunked / 无 Content-Length:只查头会漏,流上累计必须兜住
|
|
91
|
+
import asyncio
|
|
92
|
+
from starlette.requests import Request as _Req
|
|
93
|
+
from foxclaw.api.middleware import (
|
|
94
|
+
_gateway_mw, _PayloadTooLarge, _wrap_body_limit)
|
|
95
|
+
|
|
96
|
+
pending = [b"x" * 10, b"y" * 10, b"z" * 10]
|
|
97
|
+
|
|
98
|
+
async def _recv():
|
|
99
|
+
if pending:
|
|
100
|
+
return {"type": "http.request", "body": pending.pop(0),
|
|
101
|
+
"more_body": bool(pending)}
|
|
102
|
+
return {"type": "http.disconnect"}
|
|
103
|
+
|
|
104
|
+
req = _Req({"type": "http", "method": "POST", "path": "/api/x",
|
|
105
|
+
"scheme": "http", "server": ("test", 80),
|
|
106
|
+
"headers": [], "query_string": b""}, _recv)
|
|
107
|
+
_wrap_body_limit(req, 15) # 第 2 块累计 20 > 15
|
|
108
|
+
|
|
109
|
+
async def _drain():
|
|
110
|
+
for _ in range(3):
|
|
111
|
+
await req.receive()
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
asyncio.run(_drain())
|
|
115
|
+
except _PayloadTooLarge:
|
|
116
|
+
pass
|
|
117
|
+
else:
|
|
118
|
+
raise AssertionError("chunked 超限未被流上计数拦住")
|
|
119
|
+
|
|
120
|
+
# [8c] 流超限转成 413,而不是冒成 500
|
|
121
|
+
async def _boom(_req):
|
|
122
|
+
raise _PayloadTooLarge(999)
|
|
123
|
+
|
|
124
|
+
req2 = _Req({"type": "http", "method": "POST", "path": "/api/x",
|
|
125
|
+
"scheme": "http", "server": ("test", 80),
|
|
126
|
+
"headers": [], "query_string": b""})
|
|
127
|
+
resp = asyncio.run(_gateway_mw(req2, _boom))
|
|
128
|
+
assert resp.status_code == 413, resp.status_code
|
|
129
|
+
print("[8b] chunked 超限 → 流上计数拦截并转 413 OK")
|
|
130
|
+
|
|
131
|
+
# [9] 启用哲学:默认关;配 FOXCLAW_API_KEY(=对外暴露)自动开;=0 强制关优先
|
|
132
|
+
os.environ.pop("FOXCLAW_GATEWAY", None); os.environ.pop("FOXCLAW_API_KEY", None)
|
|
133
|
+
assert _gateway_on() is False, "本地默认应关"
|
|
134
|
+
os.environ["FOXCLAW_API_KEY"] = "x"
|
|
135
|
+
assert _gateway_on() is True, "配鉴权应自动开"
|
|
136
|
+
os.environ["FOXCLAW_GATEWAY"] = "0"
|
|
137
|
+
assert _gateway_on() is False, "FOXCLAW_GATEWAY=0 应强制关"
|
|
138
|
+
os.environ.pop("FOXCLAW_GATEWAY", None); os.environ.pop("FOXCLAW_API_KEY", None)
|
|
139
|
+
print("[9] 启用哲学(默认关 / 配 key 自动开 / =0 强制关)OK")
|
|
140
|
+
|
|
141
|
+
print("\n统一 API 网关验证全部通过")
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
test_gateway()
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"""
|
|
2
|
+
运行时治理回归测试 — 覆盖借鉴 OpenClaw 的六项能力
|
|
3
|
+
|
|
4
|
+
验证:
|
|
5
|
+
1. 工具循环检测(repeat/ping_pong/no_progress + 多级阈值)
|
|
6
|
+
2. 上下文压缩(渐进降级 + 标识符保留 + 无效压缩放弃)
|
|
7
|
+
3. 钩子标准化(向后兼容 + 阻断权 + 非法名校验)
|
|
8
|
+
4. 幂等守卫(原子查重 + 并发 + TTL + 跨重启)
|
|
9
|
+
5. 重启恢复(boot_id 防 pid 复用 + 中断识别)
|
|
10
|
+
6. 干预队列(租约不重复 + 过期重入队 + drain 尊重租约)
|
|
11
|
+
"""
|
|
12
|
+
import sys, os, time, threading, tempfile
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def check(cond, msg):
|
|
18
|
+
if not cond:
|
|
19
|
+
raise AssertionError(f"✗ {msg}")
|
|
20
|
+
print(f" ✓ {msg}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_loop_detection():
|
|
24
|
+
print("\n【1】工具循环检测")
|
|
25
|
+
from foxclaw.harness.loop_detection import ToolLoopDetector
|
|
26
|
+
# repeat
|
|
27
|
+
d = ToolLoopDetector(warning=3, critical=5, circuit_break=8)
|
|
28
|
+
for _ in range(8):
|
|
29
|
+
v = d.record("web_search", {"q": "天气"})
|
|
30
|
+
check(v.stuck and v.detector == "repeat", "重复调用检测为 repeat")
|
|
31
|
+
# ping_pong
|
|
32
|
+
d2 = ToolLoopDetector(warning=4, critical=6, circuit_break=10)
|
|
33
|
+
for i in range(6):
|
|
34
|
+
v2 = d2.record("search" if i % 2 == 0 else "fetch", {"x": i % 2})
|
|
35
|
+
check(v2.stuck and v2.detector == "ping_pong", "交替调用检测为 ping_pong")
|
|
36
|
+
# no_progress
|
|
37
|
+
d3 = ToolLoopDetector(warning=3, critical=5, circuit_break=8)
|
|
38
|
+
for _ in range(5):
|
|
39
|
+
v3 = d3.record("poll", {"id": "j"}, result={"status": "pending"})
|
|
40
|
+
check(v3.stuck and v3.detector == "no_progress", "轮询无变化检测为 no_progress")
|
|
41
|
+
# 正常不误报
|
|
42
|
+
d4 = ToolLoopDetector(warning=3)
|
|
43
|
+
for q in ["a", "b", "c", "d", "e"]:
|
|
44
|
+
v4 = d4.record("search", {"q": q})
|
|
45
|
+
check(not v4.stuck, "多样调用不误报")
|
|
46
|
+
# 熔断
|
|
47
|
+
check(d.should_abort(), "达阈值 should_abort")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_loop_per_session():
|
|
51
|
+
print("\n【1b】循环检测 per-session 隔离(阶段四·步骤13,消除跨会话污染)")
|
|
52
|
+
from foxclaw.skills import SkillAdapter, set_loop_session, reset_loop_session, _loop_session
|
|
53
|
+
adp = SkillAdapter(None) # _detector_for 不依赖 registry,构造最小实例即可
|
|
54
|
+
|
|
55
|
+
# 会话 A 连续同工具 8 次(达熔断)
|
|
56
|
+
tok = set_loop_session("sess_A")
|
|
57
|
+
try:
|
|
58
|
+
for _ in range(8):
|
|
59
|
+
va = adp._detector_for(_loop_session.get()).record("web_search", {"q": "天气"})
|
|
60
|
+
finally:
|
|
61
|
+
reset_loop_session(tok)
|
|
62
|
+
check(va.stuck, "会话 A 连续同工具达熔断")
|
|
63
|
+
|
|
64
|
+
# 切到会话 B:历史应从零,同工具仅 1 次不应被 A 污染成 stuck
|
|
65
|
+
tok = set_loop_session("sess_B")
|
|
66
|
+
try:
|
|
67
|
+
vb = adp._detector_for(_loop_session.get()).record("web_search", {"q": "天气"})
|
|
68
|
+
finally:
|
|
69
|
+
reset_loop_session(tok)
|
|
70
|
+
check(not vb.stuck, "会话 B 不受会话 A 污染(独立历史)")
|
|
71
|
+
check(not adp._detector_for("sess_B").should_abort(), "会话 B 未达熔断")
|
|
72
|
+
|
|
73
|
+
# 不同会话持有独立检测器实例;无会话时用共享默认槽(向后兼容)
|
|
74
|
+
check(adp._detector_for("sess_A") is not adp._detector_for("sess_B"), "不同会话独立检测器实例")
|
|
75
|
+
check(adp._detector_for(None) is adp._detector_for(None), "无会话共用默认槽(稳定)")
|
|
76
|
+
check(_loop_session.get() is None, "退出后 contextvar 已复位(无残留)")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_compaction():
|
|
80
|
+
print("\n【2】上下文压缩")
|
|
81
|
+
from foxclaw.core.compaction import Compactor, extract_identifiers, estimate_messages_tokens
|
|
82
|
+
# token 估算(压缩触发的判据)
|
|
83
|
+
check(estimate_messages_tokens([{"role": "user", "content": "你好世界"}]) > 0, "token 估算非零")
|
|
84
|
+
# 标识符提取(含 ¥金额)
|
|
85
|
+
ids = extract_identifiers([{"role": "user", "content": "订单 txn_abc12345 金额 ¥1,200 流水 202606210001"}])
|
|
86
|
+
check("txn_abc12345" in ids and any("¥" in i for i in ids), "关键标识符提取(含金额)")
|
|
87
|
+
# 截断兜底(无 router)
|
|
88
|
+
msgs = [{"role": "system", "content": "你是助手"}]
|
|
89
|
+
for i in range(10):
|
|
90
|
+
msgs.append({"role": "user", "content": f"长消息{i}" * 20})
|
|
91
|
+
msgs.append({"role": "assistant", "content": f"长回复{i}" * 20})
|
|
92
|
+
c = Compactor(router=None, max_tokens=50, keep_recent=2)
|
|
93
|
+
r = c.compact(msgs)
|
|
94
|
+
check(r.method == "truncate" and r.compacted_tokens < r.original_tokens, "无 router 截断兜底有效")
|
|
95
|
+
check(r.messages[-1] == msgs[-1], "最近消息保留")
|
|
96
|
+
# 巨大系统消息 → 放弃无效压缩
|
|
97
|
+
huge = [{"role": "system", "content": "x" * 8000}]
|
|
98
|
+
for i in range(6):
|
|
99
|
+
huge.append({"role": "user", "content": f"m{i}"})
|
|
100
|
+
c2 = Compactor(router=None, max_tokens=100, keep_recent=2)
|
|
101
|
+
r2 = c2.compact(huge)
|
|
102
|
+
check(r2.compacted_tokens <= r2.original_tokens, "无效压缩时不会变大")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_hooks():
|
|
106
|
+
print("\n【3】钩子标准化")
|
|
107
|
+
from foxclaw.skills.lifecycle import HookManager
|
|
108
|
+
from foxclaw.skills.hooks_spec import Hooks, normalize_hook, is_valid_hook
|
|
109
|
+
check(normalize_hook("pre_skill") == Hooks.BEFORE_TOOL_CALL, "旧名规整为标准名")
|
|
110
|
+
check(is_valid_hook("on_loop"), "on_loop 是合法钩子")
|
|
111
|
+
# 向后兼容互通
|
|
112
|
+
hm = HookManager()
|
|
113
|
+
fired = []
|
|
114
|
+
hm.register("pre_skill", lambda ctx: fired.append("x") or {})
|
|
115
|
+
hm.fire("before_tool_call", "calc", {})
|
|
116
|
+
check("x" in fired, "旧名注册 + 标准名触发 互通")
|
|
117
|
+
# 阻断权
|
|
118
|
+
hm2 = HookManager()
|
|
119
|
+
hm2.register("before_tool_call", lambda ctx: {"block": True, "reason": "test"})
|
|
120
|
+
check(hm2.fire("before_tool_call", "calc", {}).blocked, "before_tool_call 可阻断")
|
|
121
|
+
hm3 = HookManager()
|
|
122
|
+
hm3.register("after_tool_call", lambda ctx: {"block": True})
|
|
123
|
+
check(not hm3.fire("after_tool_call", "calc", {}).blocked, "after_tool_call 无阻断权")
|
|
124
|
+
# 非法名警告
|
|
125
|
+
hm4 = HookManager()
|
|
126
|
+
hm4.register("typo_hook", lambda ctx: {})
|
|
127
|
+
check(any("未知钩子" in a.get("error", "") for a in hm4.audit_log), "拼错钩子名有警告")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_idempotency():
|
|
131
|
+
print("\n【4】幂等守卫")
|
|
132
|
+
from foxclaw.core.idempotency import IdempotencyGuard, make_key
|
|
133
|
+
k1 = make_key("message", "telegram", "msg1", "u1")
|
|
134
|
+
k2 = make_key("message", "telegram", "msg1", "u1")
|
|
135
|
+
check(k1 == k2, "相同输入得相同 key")
|
|
136
|
+
g = IdempotencyGuard(ttl_seconds=3600)
|
|
137
|
+
check(g.check_and_mark(k1) and not g.check_and_mark(k1), "原子查重:首次True重复False")
|
|
138
|
+
# 并发只1个成功
|
|
139
|
+
g2 = IdempotencyGuard(ttl_seconds=3600)
|
|
140
|
+
results, lock = [], threading.Lock()
|
|
141
|
+
def worker():
|
|
142
|
+
r = g2.check_and_mark("contended")
|
|
143
|
+
with lock: results.append(r)
|
|
144
|
+
ts = [threading.Thread(target=worker) for _ in range(50)]
|
|
145
|
+
for t in ts: t.start()
|
|
146
|
+
for t in ts: t.join()
|
|
147
|
+
check(sum(results) == 1, "50 线程抢同 key 恰好 1 个成功")
|
|
148
|
+
# TTL
|
|
149
|
+
g3 = IdempotencyGuard(ttl_seconds=0.3)
|
|
150
|
+
g3.check_and_mark("tmp"); time.sleep(0.4)
|
|
151
|
+
check(g3.check_and_mark("tmp"), "TTL 过期后可重新执行")
|
|
152
|
+
# 跨重启
|
|
153
|
+
p = tempfile.mktemp(suffix=".json")
|
|
154
|
+
ga = IdempotencyGuard(persist_path=p); ga.check_and_mark("pk")
|
|
155
|
+
gb = IdempotencyGuard(persist_path=p)
|
|
156
|
+
check(gb.seen("pk"), "持久化跨重启")
|
|
157
|
+
os.remove(p) if os.path.exists(p) else None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_recovery():
|
|
161
|
+
print("\n【5】重启恢复")
|
|
162
|
+
from foxclaw.core.recovery import RestartRecovery, _pid_alive
|
|
163
|
+
log = tempfile.mktemp(suffix=".jsonl")
|
|
164
|
+
# boot_id 防 pid 复用
|
|
165
|
+
rA = RestartRecovery(log_path=log, boot_id="bootA")
|
|
166
|
+
rA.task_started("t1", "中断任务")
|
|
167
|
+
# 即使 pid 是存活的(复用),boot_id 不同也应识别中断
|
|
168
|
+
rB = RestartRecovery(log_path=log, boot_id="bootB", current_pid=os.getpid())
|
|
169
|
+
interrupted = rB.scan_interrupted()
|
|
170
|
+
check(len(interrupted) == 1 and interrupted[0].task_id == "t1", "boot_id 识别中断(防 pid 复用)")
|
|
171
|
+
# 本次启动自己的任务不误判
|
|
172
|
+
rB.task_started("t2", "本次任务")
|
|
173
|
+
check(not any(t.task_id == "t2" for t in rB.scan_interrupted()), "不误判本次启动的任务")
|
|
174
|
+
# 完成的不算中断
|
|
175
|
+
rB.task_finished("t2")
|
|
176
|
+
check(not any(t.task_id == "t2" for t in rB.scan_interrupted()), "已完成任务不算中断")
|
|
177
|
+
check(_pid_alive(os.getpid()) and not _pid_alive(999999), "pid 存活检测准确")
|
|
178
|
+
# 危险路径回归:探测【外部活进程】必须 True 且【绝不杀死】它
|
|
179
|
+
# (旧实现在 Windows 上 os.kill(pid,0) 会 TerminateProcess 误杀,本用例专抓此坑)
|
|
180
|
+
import subprocess
|
|
181
|
+
proc = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(10)"])
|
|
182
|
+
try:
|
|
183
|
+
alive = _pid_alive(proc.pid)
|
|
184
|
+
time.sleep(0.4)
|
|
185
|
+
not_killed = proc.poll() is None
|
|
186
|
+
check(alive and not_killed, "探测外部活进程:判活且非破坏性(不误杀)")
|
|
187
|
+
finally:
|
|
188
|
+
proc.kill()
|
|
189
|
+
os.remove(log) if os.path.exists(log) else None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_steering_queue():
|
|
193
|
+
print("\n【6】干预队列")
|
|
194
|
+
from foxclaw.core.steering_queue import SteeringQueue
|
|
195
|
+
# 基本流
|
|
196
|
+
q = SteeringQueue(lease_seconds=30)
|
|
197
|
+
q.publish("m1", "finance", {"r": "A"})
|
|
198
|
+
leased = q.lease("m1")
|
|
199
|
+
check(len(leased) == 1, "publish→lease")
|
|
200
|
+
check(q.ack(leased[0].item_id, leased[0].lease_id), "ack 成功移除")
|
|
201
|
+
# 并发不重复租用
|
|
202
|
+
q2 = SteeringQueue(lease_seconds=30)
|
|
203
|
+
for i in range(20):
|
|
204
|
+
q2.publish("m2", "a", {"i": i})
|
|
205
|
+
got, lock = [], threading.Lock()
|
|
206
|
+
def consumer():
|
|
207
|
+
items = q2.lease("m2", max_items=20)
|
|
208
|
+
with lock: got.extend(it.item_id for it in items)
|
|
209
|
+
ts = [threading.Thread(target=consumer) for _ in range(5)]
|
|
210
|
+
for t in ts: t.start()
|
|
211
|
+
for t in ts: t.join()
|
|
212
|
+
check(len(got) == len(set(got)) == 20, "并发租取不重复")
|
|
213
|
+
# 租约过期重入队
|
|
214
|
+
q3 = SteeringQueue(lease_seconds=0.3)
|
|
215
|
+
q3.publish("m3", "a", {}); q3.lease("m3"); time.sleep(0.4)
|
|
216
|
+
check(q3.pending_count() == 1, "租约过期自动重入队(不丢)")
|
|
217
|
+
# drain 尊重租约
|
|
218
|
+
q4 = SteeringQueue(lease_seconds=30)
|
|
219
|
+
q4.publish("m4", "a", {"r": 1}); q4.publish("m4", "a", {"r": 2})
|
|
220
|
+
q4.lease("m4", max_items=1)
|
|
221
|
+
check(len(q4.drain("m4")) == 1, "drain 跳过被租用项")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_token_meter():
|
|
225
|
+
print("\n【7】Token 计量器")
|
|
226
|
+
from foxclaw.core.token_meter import TokenMeter, estimate_tokens
|
|
227
|
+
# 估算非零
|
|
228
|
+
check(estimate_tokens("你好世界hello") > 0, "token 估算非零")
|
|
229
|
+
check(estimate_tokens("") == 0, "空文本 0 token")
|
|
230
|
+
m = TokenMeter()
|
|
231
|
+
# 真实 usage 优先(estimated=False)
|
|
232
|
+
r1 = m.measure_call(system="s", messages=[{"role": "user", "content": "q"}],
|
|
233
|
+
output="a", provider="deepseek", model="deepseek-chat",
|
|
234
|
+
agent_id="finance", usage={"input_tokens": 100, "output_tokens": 200})
|
|
235
|
+
check(not r1.estimated and r1.total_tokens == 300, "真实 usage 优先,total 正确")
|
|
236
|
+
# 无 usage 走估算(estimated=True)
|
|
237
|
+
r2 = m.measure_call(system="你是主管", messages=[{"role": "user", "content": "分析理财预算"}],
|
|
238
|
+
output="已完成分析", provider="deepseek", model="deepseek-chat",
|
|
239
|
+
agent_id="supervisor", usage=None)
|
|
240
|
+
check(r2.estimated and r2.total_tokens > 0, "无 usage 走估算")
|
|
241
|
+
# 按 agent 聚合(agent_id 正确)
|
|
242
|
+
agg = m.by_agent()
|
|
243
|
+
check("finance" in agg and "supervisor" in agg, "按 Agent 聚合(非 unknown)")
|
|
244
|
+
check(agg["finance"]["total_tokens"] == 300, "Agent 聚合数值正确")
|
|
245
|
+
# 报告不崩
|
|
246
|
+
check(len(m.report()) > 0, "文本报告生成")
|
|
247
|
+
# 成本估算
|
|
248
|
+
cost = m.estimate_cost(price_per_1k_input=0.001, price_per_1k_output=0.002)
|
|
249
|
+
check(cost["estimated_cost"] >= 0, "成本估算")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_thoughtchain_concurrency():
|
|
253
|
+
print("\n【8】ThoughtChain 并发写(共享审计存储)")
|
|
254
|
+
from foxclaw.core.thought_chain import ThoughtChain, ThoughtNode
|
|
255
|
+
tc = ThoughtChain(db_path=tempfile.mktemp(suffix=".db"))
|
|
256
|
+
# WAL 已启用?(并发加固的核心)
|
|
257
|
+
with tc._conn() as c:
|
|
258
|
+
mode = c.execute("PRAGMA journal_mode").fetchone()[0]
|
|
259
|
+
check(mode.lower() == "wal", f"WAL 日志模式已启用(实为 {mode})")
|
|
260
|
+
# N 线程 × M 节点并发写:默认 busy_timeout=0 时这会 database is locked
|
|
261
|
+
N, M, errors = 8, 25, []
|
|
262
|
+
def writer(aid):
|
|
263
|
+
try:
|
|
264
|
+
for i in range(M):
|
|
265
|
+
node = ThoughtNode.create(session_id=f"s{aid}", agent_id=f"a{aid}",
|
|
266
|
+
type="reasoning", content=f"n{i}", conf=0.9)
|
|
267
|
+
tc.add_node(node)
|
|
268
|
+
tc.update_harness(node.id, "PASS")
|
|
269
|
+
except Exception as e:
|
|
270
|
+
errors.append(repr(e))
|
|
271
|
+
ts = [threading.Thread(target=writer, args=(k,)) for k in range(N)]
|
|
272
|
+
for t in ts: t.start()
|
|
273
|
+
for t in ts: t.join()
|
|
274
|
+
check(not errors, f"{N} 线程并发写无异常(否则: {errors[:2]})")
|
|
275
|
+
with tc._conn() as c:
|
|
276
|
+
total = c.execute("SELECT COUNT(*) FROM thought_nodes").fetchone()[0]
|
|
277
|
+
check(total == N * M, f"并发写不丢:{total}/{N*M} 条落库")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def test_harness_action_wiring():
|
|
281
|
+
print("\n【9】Harness 学习规则 action 级生效(penalize_tool / restrict_scope)")
|
|
282
|
+
from foxclaw.core.thought_chain import ThoughtChain, ThoughtNode
|
|
283
|
+
from foxclaw.harness.harness import Harness
|
|
284
|
+
tc = ThoughtChain(db_path=tempfile.mktemp(suffix=".db"))
|
|
285
|
+
|
|
286
|
+
def fresh(baseline_entities):
|
|
287
|
+
h = Harness(agent_id="t", thought_chain=tc,
|
|
288
|
+
policy_path=tempfile.mktemp(suffix=".yaml"), shadow_rounds=0)
|
|
289
|
+
h.register_goal("分析财务预算", allowed_tool_categories={"read", "finance"},
|
|
290
|
+
baseline_entities=baseline_entities)
|
|
291
|
+
h._shadow_mode = False # 关观测期,直接进入实判
|
|
292
|
+
h._block_threshold, h._warn_threshold = 0.25, 0.18
|
|
293
|
+
return h
|
|
294
|
+
|
|
295
|
+
def node(tool=None, entities=None):
|
|
296
|
+
n = ThoughtNode.create(session_id="s", agent_id="t", type="tool_intent",
|
|
297
|
+
content="继续分析预算", conf=0.9,
|
|
298
|
+
tool_name=tool, entities=entities or [])
|
|
299
|
+
tc.add_node(n)
|
|
300
|
+
return n
|
|
301
|
+
|
|
302
|
+
# —— penalize_tool:get_balance 本在 finance 允许集,基线判 0.0 ——
|
|
303
|
+
h1 = fresh({"预算", "收支"})
|
|
304
|
+
check(h1.check(node(tool="get_balance")).scores["tool"] == 0.0,
|
|
305
|
+
"基线:允许的工具 get_balance 工具分 = 0.0")
|
|
306
|
+
# 注入一条 penalize_tool 学习规则(模拟之前 BLOCK 学到的)→ 应立即改变判定
|
|
307
|
+
h1._policy.setdefault("rules", []).append({
|
|
308
|
+
"id": "r1", "source": "harness_learned",
|
|
309
|
+
"action": {"type": "penalize_tool", "tool_pattern": "get_balance"}})
|
|
310
|
+
h1._reindex_learned_actions()
|
|
311
|
+
r1 = h1.check(node(tool="get_balance"))
|
|
312
|
+
check(r1.scores["tool"] == 1.0 and r1.is_blocked,
|
|
313
|
+
"penalize_tool 生效:同一工具被抬到 1.0 并 BLOCK")
|
|
314
|
+
|
|
315
|
+
# —— restrict_scope:大基线下加 2 个新实体,基线分低(0.2),规则应抬到 1.0 ——
|
|
316
|
+
big = {f"e{i}" for i in range(10)}
|
|
317
|
+
h2 = fresh(big)
|
|
318
|
+
base_scope = h2.check(node(entities=["e0", "x1", "x2"])).scores["scope"]
|
|
319
|
+
check(abs(base_scope - 0.2) < 1e-6, f"基线:2 个新实体 scope≈0.2(实为 {base_scope})")
|
|
320
|
+
h2._policy.setdefault("rules", []).append({
|
|
321
|
+
"id": "r2", "source": "harness_learned",
|
|
322
|
+
"action": {"type": "restrict_scope", "entity_expansion_limit": 1}})
|
|
323
|
+
h2._reindex_learned_actions()
|
|
324
|
+
r2 = h2.check(node(entities=["e0", "x1", "x2"]))
|
|
325
|
+
check(r2.scores["scope"] == 1.0, "restrict_scope 生效:超上限 scope 抬到 1.0")
|
|
326
|
+
|
|
327
|
+
# —— 端到端:一次真实 BLOCK → 【P0-1 新行为】规则进**影子**(不生效),不再立即重建索引 ——
|
|
328
|
+
h3 = fresh({"预算"})
|
|
329
|
+
h3.check(node(tool="delete_everything")) # delete 类越权 → BLOCK → 影子候选
|
|
330
|
+
check(not h3._is_penalized_tool("delete_everything"),
|
|
331
|
+
"P0-1:BLOCK 后规则进影子,**不立即生效**(未重建索引)")
|
|
332
|
+
shadow = h3._policy.get("shadow_rules", [])
|
|
333
|
+
check(len(shadow) == 1 and shadow[0]["action"]["type"] == "penalize_tool"
|
|
334
|
+
and shadow[0]["shadow"]["status"] == "observing",
|
|
335
|
+
"P0-1:影子规则已入库且处于 observing")
|
|
336
|
+
# 手工 promote 后应立即生效(升迁路径与自动升迁同构)
|
|
337
|
+
h3._promote_rule(shadow[0])
|
|
338
|
+
check(h3._is_penalized_tool("delete_everything"),
|
|
339
|
+
"P0-1:promote 后规则生效(索引已重建)")
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def test_harness_thread_safety():
|
|
343
|
+
print("\n【10】Harness 累积态线程安全(为请求 offload 到线程池铺路)")
|
|
344
|
+
from foxclaw.core.thought_chain import ThoughtChain, ThoughtNode
|
|
345
|
+
from foxclaw.harness.harness import Harness
|
|
346
|
+
tc = ThoughtChain(db_path=tempfile.mktemp(suffix=".db"))
|
|
347
|
+
h = Harness(agent_id="t", thought_chain=tc,
|
|
348
|
+
policy_path=tempfile.mktemp(suffix=".yaml"), shadow_rounds=10**9)
|
|
349
|
+
N, M, errors = 8, 30, []
|
|
350
|
+
|
|
351
|
+
def worker(k):
|
|
352
|
+
try:
|
|
353
|
+
# 每线程在自己的 context 里注册基线(contextvar 线程独立),再并发 check
|
|
354
|
+
h.register_goal(f"目标{k}", allowed_tool_categories={"read"},
|
|
355
|
+
baseline_entities={"e"})
|
|
356
|
+
for i in range(M):
|
|
357
|
+
node = ThoughtNode.create(session_id=f"s{k}", agent_id="t",
|
|
358
|
+
type="reasoning", content=f"n{k}_{i}", conf=0.9)
|
|
359
|
+
tc.add_node(node)
|
|
360
|
+
h.check(node)
|
|
361
|
+
except Exception as e:
|
|
362
|
+
errors.append(repr(e))
|
|
363
|
+
|
|
364
|
+
ts = [threading.Thread(target=worker, args=(k,)) for k in range(N)]
|
|
365
|
+
for t in ts: t.start()
|
|
366
|
+
for t in ts: t.join()
|
|
367
|
+
|
|
368
|
+
check(not errors, f"{N} 线程并发 check 无异常(否则: {errors[:2]})")
|
|
369
|
+
# 锁保护下 _run_count 无丢增量(无锁时 += 竞态会丢)
|
|
370
|
+
check(h._run_count == N * M, f"_run_count 无丢增量({h._run_count}=={N*M})")
|
|
371
|
+
check(len(h._delta_history) == N * M, f"_delta_history 无丢({len(h._delta_history)}=={N*M})")
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def test_harness_delta_history_bounded():
|
|
375
|
+
print("\n【11】深审修复:_delta_history 校准后停止增长(长跑不无界泄漏)")
|
|
376
|
+
from foxclaw.core.thought_chain import ThoughtChain, ThoughtNode
|
|
377
|
+
from foxclaw.harness.harness import Harness
|
|
378
|
+
tc = ThoughtChain(db_path=tempfile.mktemp(suffix=".db"))
|
|
379
|
+
rounds = 5
|
|
380
|
+
h = Harness(agent_id="t2", thought_chain=tc,
|
|
381
|
+
policy_path=tempfile.mktemp(suffix=".yaml"), shadow_rounds=rounds)
|
|
382
|
+
h.register_goal("目标", allowed_tool_categories={"read"}, baseline_entities={"e"})
|
|
383
|
+
for i in range(rounds * 20): # 远超 shadow_rounds
|
|
384
|
+
node = ThoughtNode.create(session_id="s", agent_id="t2",
|
|
385
|
+
type="reasoning", content=f"n{i}", conf=0.9)
|
|
386
|
+
tc.add_node(node); h.check(node)
|
|
387
|
+
# 仅 shadow 期(前 rounds 次)append,校准后不再 append → 上限=rounds,不随 run_count 无界增长
|
|
388
|
+
check(len(h._delta_history) <= rounds,
|
|
389
|
+
f"_delta_history 有界(≤{rounds}),实得 {len(h._delta_history)}")
|
|
390
|
+
check(h._run_count == rounds * 20, "run_count 仍完整计数(每次 check 都记)")
|
|
391
|
+
check(not h._shadow_mode, "shadow 已结束(已校准)")
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def test_orchestration_request_log_bounded():
|
|
395
|
+
print("\n【12】深审修复:orchestrator.request_log 有界(长跑不泄漏)")
|
|
396
|
+
import tempfile as _tf, shutil as _sh, asyncio as _aio
|
|
397
|
+
from foxclaw import Foxclaw
|
|
398
|
+
from foxclaw.orchestration import CrossAgentRequest
|
|
399
|
+
d = _tf.mkdtemp(prefix="reqlog_")
|
|
400
|
+
try:
|
|
401
|
+
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
402
|
+
os_.finance_runtime.set_balance("default", 100000)
|
|
403
|
+
for _ in range(1200): # 远超 maxlen=1000
|
|
404
|
+
os_.orchestrator.route_request(CrossAgentRequest(
|
|
405
|
+
from_agent="admin", to_agent="finance", request_type="budget_request",
|
|
406
|
+
payload={"amount": 10, "category": "travel", "account": "default"},
|
|
407
|
+
reason="x"))
|
|
408
|
+
check(len(os_.orchestrator.request_log) <= 1000,
|
|
409
|
+
f"request_log 有界(≤1000),实得 {len(os_.orchestrator.request_log)}")
|
|
410
|
+
_aio.run(os_.aclose())
|
|
411
|
+
finally:
|
|
412
|
+
_sh.rmtree(d, ignore_errors=True)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def test_hmr_adapter_stats_lock():
|
|
416
|
+
print("\n【13】深审修复:HMR 适配层 stats/status 补齐锁覆盖(与 ingest/recall 互斥)")
|
|
417
|
+
import threading, tempfile, time
|
|
418
|
+
from foxclaw.core.memory_adapter import create_memory_backend
|
|
419
|
+
hmr = create_memory_backend(storage_path=tempfile.mkdtemp(prefix="hmrlock_"))
|
|
420
|
+
if not hasattr(hmr, "_lock"):
|
|
421
|
+
print(" ~ MockHMR(无 vendor/hmr)无 _lock,跳过"); return
|
|
422
|
+
# 白盒:主线程持锁 → 另一线程的 stats() 必须等锁释放才完成(证明 stats 已进锁)。
|
|
423
|
+
# 补锁前 stats 不持锁 → 会在持锁期间直接完成,order 顺序相反。
|
|
424
|
+
order = []
|
|
425
|
+
hmr._lock.acquire()
|
|
426
|
+
def call_stats():
|
|
427
|
+
hmr.stats(); order.append("stats_done")
|
|
428
|
+
t = threading.Thread(target=call_stats); t.start()
|
|
429
|
+
time.sleep(0.15); order.append("released"); hmr._lock.release()
|
|
430
|
+
t.join(timeout=5)
|
|
431
|
+
check(order == ["released", "stats_done"],
|
|
432
|
+
f"stats() 应等 _lock 释放才完成(证明补锁生效),实得 {order}")
|
|
433
|
+
# get_system_status 同样补了锁
|
|
434
|
+
hmr._lock.acquire()
|
|
435
|
+
order2 = []
|
|
436
|
+
def call_status():
|
|
437
|
+
hmr.get_system_status(); order2.append("status_done")
|
|
438
|
+
t2 = threading.Thread(target=call_status); t2.start()
|
|
439
|
+
time.sleep(0.15); order2.append("released"); hmr._lock.release()
|
|
440
|
+
t2.join(timeout=5)
|
|
441
|
+
check(order2 == ["released", "status_done"], f"get_system_status 应持锁,实得 {order2}")
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def main():
|
|
445
|
+
print("=" * 56)
|
|
446
|
+
print("运行时治理回归测试")
|
|
447
|
+
print("=" * 56)
|
|
448
|
+
tests = [test_loop_detection, test_compaction, test_hooks,
|
|
449
|
+
test_idempotency, test_recovery, test_steering_queue,
|
|
450
|
+
test_token_meter, test_thoughtchain_concurrency,
|
|
451
|
+
test_harness_action_wiring, test_harness_thread_safety,
|
|
452
|
+
test_harness_delta_history_bounded, test_orchestration_request_log_bounded,
|
|
453
|
+
test_hmr_adapter_stats_lock]
|
|
454
|
+
failed = 0
|
|
455
|
+
for t in tests:
|
|
456
|
+
try:
|
|
457
|
+
t()
|
|
458
|
+
except Exception as e:
|
|
459
|
+
print(f" ✗ {t.__name__} 失败: {e}")
|
|
460
|
+
failed += 1
|
|
461
|
+
print("\n" + "=" * 56)
|
|
462
|
+
print(f"结果:{len(tests) - failed}/{len(tests)} 组通过")
|
|
463
|
+
print("=" * 56)
|
|
464
|
+
sys.exit(1 if failed else 0)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
if __name__ == "__main__":
|
|
468
|
+
main()
|