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,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
统一规划原语(P1-2:三个 Planner 合并)测试 —— 全离线。
|
|
3
|
+
|
|
4
|
+
覆盖:
|
|
5
|
+
1. 共享原语:is_negated / split_on / keyword_hits(否定开关)
|
|
6
|
+
2. trigger_route 引擎:否定过滤、打分、兜底、near-ties→needs_composition、确定性平局
|
|
7
|
+
3. 三处委托同一引擎(单一真源):
|
|
8
|
+
· AHS pipeline.route 否定生效(不选被否定的 skill)
|
|
9
|
+
· search QueryPlanner._decompose 用共享拆分
|
|
10
|
+
· 主管关键词命中走 keyword_hits
|
|
11
|
+
4. 结构:pipeline 不再持有本地 _negated/_split_on(已删除,证明去重)
|
|
12
|
+
"""
|
|
13
|
+
import os, sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
os.environ.setdefault("HF_HUB_OFFLINE", "1")
|
|
16
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
17
|
+
|
|
18
|
+
from foxclaw.planning import planner as P
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_primitives():
|
|
22
|
+
print("\n【1】共享原语:否定 / 拆分 / 关键词命中")
|
|
23
|
+
assert P.is_negated("不要计算这个", "计算") is True
|
|
24
|
+
assert P.is_negated("请计算这个", "计算") is False
|
|
25
|
+
assert P.split_on("查北京,查上海、查广州", P.LIST_SEPS) == ["查北京", "查上海", "查广州"]
|
|
26
|
+
assert P.split_on("先做A 然后 做B", P.SPLIT_MARKERS) == ["先做A", "做B"]
|
|
27
|
+
assert P.keyword_hits("分析财务收支", ["财务", "收支", "行程"]) == 2
|
|
28
|
+
assert P.keyword_hits("不要分析财务", ["财务"], negation=True) == 0
|
|
29
|
+
assert P.keyword_hits("不要分析财务", ["财务"], negation=False) == 1
|
|
30
|
+
print(" ✓ is_negated / split_on / keyword_hits(否定开关) 均正确")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_trigger_route_engine():
|
|
34
|
+
print("\n【2】trigger_route 引擎:否定/打分/兜底/near-ties/确定性")
|
|
35
|
+
C = P.ScoredCandidate
|
|
36
|
+
cands = [
|
|
37
|
+
C("calculator", ["计算", "算"], "四则运算 计算器"),
|
|
38
|
+
C("web", ["搜索", "查"], "联网 搜索 网页"),
|
|
39
|
+
C("email", ["发邮件", "email"], "发送 邮件"),
|
|
40
|
+
]
|
|
41
|
+
# 否定:"不要计算" → calculator 触发被否定 → 不选它
|
|
42
|
+
r = P.trigger_route("不要计算一下", cands)
|
|
43
|
+
assert r.primary != "calculator", f"否定应排除 calculator,实得 {r.primary}"
|
|
44
|
+
# 命中:"帮我搜索北京" → web
|
|
45
|
+
r2 = P.trigger_route("帮我搜索北京", cands)
|
|
46
|
+
assert r2.primary == "web" and r2.confidence >= 0.15
|
|
47
|
+
# 兜底:完全不相关 → 用最后一个候选兜底,低置信
|
|
48
|
+
r3 = P.trigger_route("xyzzy 无关内容", cands)
|
|
49
|
+
assert r3.primary == cands[-1].id and r3.confidence < 0.15
|
|
50
|
+
# 确定性:空候选
|
|
51
|
+
assert P.trigger_route("x", []).primary is None
|
|
52
|
+
print(f" ✓ 否定排除 / 命中选中 / 兜底(={r3.primary}) / 空候选")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_three_sites_delegate():
|
|
56
|
+
print("\n【3】三处委托同一引擎(单一真源)")
|
|
57
|
+
# AHS
|
|
58
|
+
from foxclaw.skills import build_default_registry
|
|
59
|
+
from foxclaw.skills.pipeline import route, plan
|
|
60
|
+
reg = build_default_registry()
|
|
61
|
+
assert route("不要计算", reg).primary != "calculator", "AHS route 否定生效"
|
|
62
|
+
assert len(plan("先算 5+5 然后再算 9*9", reg).tasks) == 2, "AHS plan 拆解"
|
|
63
|
+
# search
|
|
64
|
+
from foxclaw.search.planner import QueryPlanner
|
|
65
|
+
sub = QueryPlanner()._decompose("查A,查B、查C")
|
|
66
|
+
assert len(sub) == 3, "search 拆解走共享原语"
|
|
67
|
+
# supervisor(用引擎的 keyword_hits;此处直接验证原语等价)
|
|
68
|
+
assert P.keyword_hits("买基金理财", ["理财", "基金", "收支"]) == 2
|
|
69
|
+
print(" ✓ AHS / search / 主管 三处均由 foxclaw.planning 单一真源驱动")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_dedup_structural():
|
|
73
|
+
print("\n【4】结构去重:pipeline 不再持有本地 _negated/_split_on")
|
|
74
|
+
import foxclaw.skills.pipeline as pipe
|
|
75
|
+
assert not hasattr(pipe, "_negated"), "pipeline 应删除本地 _negated(改用共享)"
|
|
76
|
+
assert not hasattr(pipe, "_split_on"), "pipeline 应删除本地 _split_on(改用共享)"
|
|
77
|
+
assert not hasattr(pipe, "_NEGATIONS"), "pipeline 应删除本地 _NEGATIONS 常量"
|
|
78
|
+
print(" ✓ pipeline 的重复否定/拆分实现已删除,收敛到 foxclaw.planning")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main():
|
|
82
|
+
print("=" * 60)
|
|
83
|
+
print("统一规划原语 · 三个 Planner 合并(P1-2)")
|
|
84
|
+
print("=" * 60)
|
|
85
|
+
test_primitives()
|
|
86
|
+
test_trigger_route_engine()
|
|
87
|
+
test_three_sites_delegate()
|
|
88
|
+
test_dedup_structural()
|
|
89
|
+
print("\n" + "=" * 60)
|
|
90
|
+
print("✓ P1-2 验证完成:一个规划引擎,主管/AHS/search 三处委托")
|
|
91
|
+
print("=" * 60)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
main()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
统一准入门(PolicyGate, v2 ① Step4)测试 —— 全离线。
|
|
3
|
+
|
|
4
|
+
覆盖:
|
|
5
|
+
1. 推理准入归一化:Harness PASS/WARN/BLOCK → Verdict allow/warn/deny(raw 透传同一 HarnessResult)
|
|
6
|
+
2. 动作准入归一化:permission allow/ask/deny → Verdict allow/confirm/deny(raw 透传);
|
|
7
|
+
非 allow 发统一 policy.decision.* 事件,allow 不发(降噪)
|
|
8
|
+
3. 真实集成:经 Foxclaw,技能路径(副作用 skill 需确认)发出 policy.decision 事件
|
|
9
|
+
"""
|
|
10
|
+
import os, sys, asyncio, tempfile, shutil
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
os.environ.setdefault("HF_HUB_OFFLINE", "1")
|
|
13
|
+
os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
|
|
14
|
+
os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast"
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
16
|
+
|
|
17
|
+
from foxclaw.kernel import PolicyGate
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _HR: # 假 HarnessResult
|
|
21
|
+
def __init__(self, blocked=False, warned=False, reason=""):
|
|
22
|
+
self._b, self._w, self.reason = blocked, warned, reason
|
|
23
|
+
@property
|
|
24
|
+
def is_blocked(self): return self._b
|
|
25
|
+
@property
|
|
26
|
+
def is_warned(self): return self._w
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _Harness:
|
|
30
|
+
def __init__(self, hr): self.hr = hr; self.calls = 0
|
|
31
|
+
def check(self, node): self.calls += 1; return self.hr
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _PD: # 假 PermissionDecision
|
|
35
|
+
def __init__(self, decision, reason=""):
|
|
36
|
+
self.decision, self.stage, self.reason, self.trust_downgrade = decision, 2, reason, False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _CapSpine:
|
|
40
|
+
def __init__(self): self.events = []
|
|
41
|
+
def publish(self, topic, payload=None, source="", **kw): self.events.append(topic)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _Skill:
|
|
45
|
+
id = "gmail_send"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_reasoning_normalization():
|
|
49
|
+
print("\n【1】推理准入归一化(Harness → Verdict;raw 透传)")
|
|
50
|
+
gate = PolicyGate()
|
|
51
|
+
h = _Harness(_HR())
|
|
52
|
+
v = gate.check_reasoning(h, None)
|
|
53
|
+
assert v.level == "allow" and v.allowed and v.raw is h.hr and h.calls == 1
|
|
54
|
+
v = gate.check_reasoning(_Harness(_HR(warned=True)), None)
|
|
55
|
+
assert v.level == "warn" and v.allowed
|
|
56
|
+
v = gate.check_reasoning(_Harness(_HR(blocked=True, reason="越界")), None)
|
|
57
|
+
assert v.level == "deny" and not v.allowed and v.reason == "越界"
|
|
58
|
+
print(" ✓ PASS→allow / WARN→warn / BLOCK→deny;raw 仍是同一 HarnessResult(行为一致)")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_action_normalization_and_event():
|
|
62
|
+
print("\n【2】动作准入归一化 + 非 allow 发 policy.decision")
|
|
63
|
+
for decision, level, event in [("allow", "allow", None),
|
|
64
|
+
("ask", "confirm", "policy.decision.confirm"),
|
|
65
|
+
("deny", "deny", "policy.decision.deny")]:
|
|
66
|
+
spine = _CapSpine()
|
|
67
|
+
pd = _PD(decision, reason=f"r-{decision}")
|
|
68
|
+
gate = PolicyGate(spine=spine, action_evaluator=lambda *a, _pd=pd, **k: _pd)
|
|
69
|
+
v = gate.check_action(_Skill(), {})
|
|
70
|
+
assert v.level == level and v.raw is pd, f"{decision}→{v.level}"
|
|
71
|
+
assert v.allowed == (decision == "allow")
|
|
72
|
+
assert spine.events == ([event] if event else []), (decision, spine.events)
|
|
73
|
+
print(" ✓ allow(不发) / ask→confirm(发) / deny(发);raw 透传 PermissionDecision")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_integration_skill_path():
|
|
77
|
+
print("\n【3】真实集成:技能路径经统一门,副作用 skill 发 policy.decision")
|
|
78
|
+
d = tempfile.mkdtemp(prefix="pg_")
|
|
79
|
+
try:
|
|
80
|
+
from foxclaw import Foxclaw
|
|
81
|
+
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
82
|
+
seen = []
|
|
83
|
+
os_.event_spine.project(lambda e: seen.append(e.topic))
|
|
84
|
+
# 发邮件是副作用 skill → 需确认 → 经 PolicyGate 发 policy.decision.confirm
|
|
85
|
+
os_.supervisor.use_skill("帮我发邮件给客户",
|
|
86
|
+
args={"to": "a@b.com", "subject": "x", "body": "y"})
|
|
87
|
+
assert any(t.startswith("policy.decision") for t in seen), \
|
|
88
|
+
f"技能准入应发 policy.decision,实得 {[t for t in seen if 'policy' in t]}"
|
|
89
|
+
assert os_.capabilities.get("policy_gate") is os_.policy_gate
|
|
90
|
+
print(f" ✓ use_skill(副作用)→ 发 {[t for t in seen if t.startswith('policy')]}")
|
|
91
|
+
asyncio.run(os_.aclose())
|
|
92
|
+
finally:
|
|
93
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main():
|
|
97
|
+
print("=" * 60)
|
|
98
|
+
print("统一准入门 · PolicyGate(v2 ① Step4)")
|
|
99
|
+
print("=" * 60)
|
|
100
|
+
test_reasoning_normalization()
|
|
101
|
+
test_action_normalization_and_event()
|
|
102
|
+
test_integration_skill_path()
|
|
103
|
+
print("\n" + "=" * 60)
|
|
104
|
+
print("✓ Step4 验证完成:两个门统一到 Verdict 词汇 + 一个接口 + 统一审计")
|
|
105
|
+
print("=" * 60)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
main()
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
领域认知化(v2 ③ Step1:DomainReasoner + 财务守门人形式化)测试 —— 全离线。
|
|
3
|
+
|
|
4
|
+
覆盖:
|
|
5
|
+
1. DomainReasoner 基座:reason() 按序 observe→hypothesize→gather_evidence→arbitrate
|
|
6
|
+
2. FinanceGatekeeperReasoner:小额→approve、大额→confirm、低余额→reject(freeze);
|
|
7
|
+
裁决与原守门人逐字一致 + 显式证据链(source=rules) + 假设
|
|
8
|
+
3. 真实路径:orchestrator.route_request 走认知化守门人,req 附证据链(可审计)
|
|
9
|
+
"""
|
|
10
|
+
import os, sys, asyncio, tempfile, shutil
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
os.environ.setdefault("HF_HUB_OFFLINE", "1")
|
|
13
|
+
os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
|
|
14
|
+
os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast"
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
16
|
+
|
|
17
|
+
from foxclaw.cognitive.reasoner import DomainReasoner, ReasonedVerdict, Evidence
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_reasoner_base_order():
|
|
21
|
+
print("\n【1】DomainReasoner 基座:四步按序执行")
|
|
22
|
+
order = []
|
|
23
|
+
class R(DomainReasoner):
|
|
24
|
+
def observe(self, s): order.append("observe"); return {"x": s}
|
|
25
|
+
def hypothesize(self, o): order.append("hypothesize"); return ["h"]
|
|
26
|
+
def gather_evidence(self, s, o): order.append("evidence"); return [Evidence("f", "d")]
|
|
27
|
+
def arbitrate(self, s, o, h, e):
|
|
28
|
+
order.append("arbitrate")
|
|
29
|
+
return ReasonedVerdict("approve", "ok", e, h, "rules")
|
|
30
|
+
rv = R().reason(42)
|
|
31
|
+
assert order == ["observe", "hypothesize", "evidence", "arbitrate"], order
|
|
32
|
+
assert rv.decision == "approve" and rv.hypotheses == ["h"] and len(rv.evidence) == 1
|
|
33
|
+
print(" ✓ observe→hypothesize→gather_evidence→arbitrate;证据链随裁决返回")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _policy():
|
|
37
|
+
from foxclaw.finance import FinanceRuntime
|
|
38
|
+
# data_path 显式隔离:默认落在 policy 同目录(系统 Temp),会与其他测试共用一份数据文件
|
|
39
|
+
p = FinanceRuntime(policy_path=tempfile.mktemp(suffix=".yaml"),
|
|
40
|
+
data_path=tempfile.mktemp(suffix=".json")).policy
|
|
41
|
+
# 显式锁定本测试的前提阈值(200,模糊带 [140,200)),**不依赖产品默认值** ——
|
|
42
|
+
# 默认阈值是给用户按自己实际调的可调项,不该让调它就牵动这些验证推理逻辑的测试。
|
|
43
|
+
for r in p.rules:
|
|
44
|
+
if r.id == "large_purchase_check":
|
|
45
|
+
r.threshold = 200.0
|
|
46
|
+
r.condition = "单笔支出 > 200"
|
|
47
|
+
return p
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_finance_gatekeeper_reasoner():
|
|
51
|
+
print("\n【2】财务守门人认知化:裁决逐字一致 + 证据链 + 假设")
|
|
52
|
+
from foxclaw.finance.reasoner import FinanceGatekeeperReasoner
|
|
53
|
+
from foxclaw.finance.models import Transaction
|
|
54
|
+
r = FinanceGatekeeperReasoner(_policy())
|
|
55
|
+
# 小额(规则内)→ approve
|
|
56
|
+
rv = r.reason((Transaction(amount=-150, category="travel", entity="高铁", account="default"), 5000))
|
|
57
|
+
assert rv.decision == "approve" and rv.rationale == "财务批准:travel 预算 150 在规则内"
|
|
58
|
+
assert rv.source == "rules" and any(e.factor == "rules_clear" for e in rv.evidence)
|
|
59
|
+
assert rv.hypotheses, "应有假设"
|
|
60
|
+
# 大额(>阈值)→ confirm,证据=large_purchase_check
|
|
61
|
+
rv2 = r.reason((Transaction(amount=-680, category="travel", entity="酒店", account="default"), 5000))
|
|
62
|
+
assert rv2.decision == "confirm" and any(e.factor == "large_purchase_check" for e in rv2.evidence)
|
|
63
|
+
print(f" ✓ 小额→approve(证据 rules_clear);大额→confirm(证据 large_purchase_check;假设={rv2.hypotheses})")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_context_evidence_no_decision_change():
|
|
67
|
+
print("\n【2b】Step2:情境证据(历史/预算/频率)丰满证据链,但**决策不变**")
|
|
68
|
+
from foxclaw.finance import FinanceRuntime
|
|
69
|
+
from foxclaw.finance.reasoner import FinanceGatekeeperReasoner
|
|
70
|
+
from foxclaw.finance.models import Transaction
|
|
71
|
+
rt = FinanceRuntime(policy_path=tempfile.mktemp(suffix=".yaml"),
|
|
72
|
+
data_path=tempfile.mktemp(suffix=".json"))
|
|
73
|
+
rt.set_budget("travel", 3000); rt.set_balance("default", 5000)
|
|
74
|
+
for _ in range(3): # 3 笔历史同类交易(同对手方)
|
|
75
|
+
rt.add_transaction(Transaction(amount=-100, category="travel", entity="高铁",
|
|
76
|
+
account="default"), run_gatekeeper=False)
|
|
77
|
+
r = FinanceGatekeeperReasoner(rt.policy, runtime=rt)
|
|
78
|
+
rv = r.reason((Transaction(amount=-150, category="travel", entity="高铁",
|
|
79
|
+
account="default"), 5000))
|
|
80
|
+
assert rv.decision == "approve", "规则内 → 决策仍是 approve(情境证据不改裁决)"
|
|
81
|
+
factors = {e.factor for e in rv.evidence}
|
|
82
|
+
assert {"history_category", "budget_remaining", "entity_frequency"} <= factors, factors
|
|
83
|
+
# 无 runtime → 无情境证据(向后兼容;裁决相同)
|
|
84
|
+
rv0 = FinanceGatekeeperReasoner(rt.policy).reason(
|
|
85
|
+
(Transaction(amount=-150, category="travel", entity="高铁", account="default"), 5000))
|
|
86
|
+
assert not ({"history_category", "budget_remaining", "entity_frequency"}
|
|
87
|
+
& {e.factor for e in rv0.evidence})
|
|
88
|
+
assert rv0.decision == rv.decision == "approve"
|
|
89
|
+
print(f" ✓ 决策 approve 不变;证据链含 {sorted(factors & {'history_category','budget_remaining','entity_frequency'})}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class _MockLLM:
|
|
93
|
+
def __init__(self, resp="APPROVE", raises=False):
|
|
94
|
+
self.resp, self.raises, self.calls, self.is_stub = resp, raises, 0, False
|
|
95
|
+
def complete(self, system, messages, max_tokens=2048, agent_id=""):
|
|
96
|
+
self.calls += 1
|
|
97
|
+
if self.raises:
|
|
98
|
+
raise RuntimeError("llm down")
|
|
99
|
+
return self.resp
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_llm_arbitration_safety():
|
|
103
|
+
print("\n【2c】Step3:LLM 裁决的安全不变量(只增强模糊、只能 approve→confirm、永不 reject)")
|
|
104
|
+
from foxclaw.finance.reasoner import FinanceGatekeeperReasoner
|
|
105
|
+
from foxclaw.finance.models import Transaction
|
|
106
|
+
policy = _policy() # large_purchase_check 阈值 200;模糊带 [140,200)
|
|
107
|
+
def txn(amt): return Transaction(amount=-amt, category="travel", entity="高铁", account="default")
|
|
108
|
+
def mk(llm, enabled=True): return FinanceGatekeeperReasoner(policy, llm=llm, llm_enabled=enabled)
|
|
109
|
+
|
|
110
|
+
# 1. 规则触发(680>200)→ 规则权威,LLM 绝不被调
|
|
111
|
+
llm = _MockLLM("APPROVE"); rv = mk(llm).reason((txn(680), 5000))
|
|
112
|
+
assert rv.decision == "confirm" and rv.source == "rules" and llm.calls == 0
|
|
113
|
+
|
|
114
|
+
# 2. 模糊(150∈[140,200))+ LLM 说 CONFIRM → confirm, source=llm
|
|
115
|
+
llm = _MockLLM("CONFIRM"); rv = mk(llm).reason((txn(150), 5000))
|
|
116
|
+
assert rv.decision == "confirm" and rv.source == "llm" and llm.calls == 1
|
|
117
|
+
|
|
118
|
+
# 3. 模糊 + LLM 说 APPROVE → approve, source=llm
|
|
119
|
+
llm = _MockLLM("APPROVE"); rv = mk(llm).reason((txn(150), 5000))
|
|
120
|
+
assert rv.decision == "approve" and rv.source == "llm"
|
|
121
|
+
|
|
122
|
+
# 4. LLM 出错/超时 → confirm(fail-safe),绝不自动放行
|
|
123
|
+
llm = _MockLLM(raises=True); rv = mk(llm).reason((txn(150), 5000))
|
|
124
|
+
assert rv.decision == "confirm" and rv.source == "llm_failsafe"
|
|
125
|
+
|
|
126
|
+
# 5. 【核心不变量】LLM 乱回(含"拒绝"/REJECT/含糊)→ **绝不 reject**,fail-safe confirm
|
|
127
|
+
for bad in ["REJECT", "让我拒绝这笔", "不确定", "yes maybe"]:
|
|
128
|
+
llm = _MockLLM(bad); rv = mk(llm).reason((txn(150), 5000))
|
|
129
|
+
assert rv.decision == "confirm" and rv.source == "llm_failsafe", (bad, rv.decision)
|
|
130
|
+
assert rv.decision != "reject", "LLM 永远不能产生 reject"
|
|
131
|
+
|
|
132
|
+
# 6. 明显安全(50<140)→ 规则批准,LLM 不调(省成本)
|
|
133
|
+
llm = _MockLLM("CONFIRM"); rv = mk(llm).reason((txn(50), 5000))
|
|
134
|
+
assert rv.decision == "approve" and rv.source == "rules" and llm.calls == 0
|
|
135
|
+
|
|
136
|
+
# 7. 默认关(llm_enabled=False)→ 全走规则,LLM 不调(behavior-preserving)
|
|
137
|
+
llm = _MockLLM("CONFIRM"); rv = mk(llm, enabled=False).reason((txn(150), 5000))
|
|
138
|
+
assert rv.decision == "approve" and rv.source == "rules" and llm.calls == 0
|
|
139
|
+
|
|
140
|
+
# 8. is_stub(无真实模型)→ 视为不可用,不调 LLM
|
|
141
|
+
stub = _MockLLM("CONFIRM"); stub.is_stub = True
|
|
142
|
+
rv = FinanceGatekeeperReasoner(policy, llm=stub, llm_enabled=True).reason((txn(150), 5000))
|
|
143
|
+
assert rv.source == "rules" and stub.calls == 0
|
|
144
|
+
print(" ✓ 规则权威不问LLM · 模糊只 approve/confirm · 出错/乱回 fail-safe confirm · "
|
|
145
|
+
"永不 reject · 明显安全/默认关/stub 不调")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_real_path_evidence():
|
|
149
|
+
print("\n【3】真实路径:认知化守门人 + req 附证据链")
|
|
150
|
+
d = tempfile.mkdtemp(prefix="reasoner_")
|
|
151
|
+
try:
|
|
152
|
+
from foxclaw import Foxclaw
|
|
153
|
+
from foxclaw.orchestration import CrossAgentRequest
|
|
154
|
+
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
155
|
+
os_.finance_runtime.set_balance("default", 5000)
|
|
156
|
+
# 大额 → 需确认(裁决不变)+ 证据链附在 req 上
|
|
157
|
+
req = os_.orchestrator.route_request(CrossAgentRequest(
|
|
158
|
+
from_agent="admin", to_agent="finance", request_type="budget_request",
|
|
159
|
+
payload={"amount": 680, "category": "travel", "entity": "酒店", "account": "default"},
|
|
160
|
+
reason="大额"))
|
|
161
|
+
assert req.status == "needs_confirmation", req.status
|
|
162
|
+
assert hasattr(req, "evidence") and req.evidence, "守门人应附证据链"
|
|
163
|
+
assert req.reason_source == "rules"
|
|
164
|
+
print(f" ✓ status={req.status}(裁决不变);evidence={req.evidence};source={req.reason_source}")
|
|
165
|
+
asyncio.run(os_.aclose())
|
|
166
|
+
finally:
|
|
167
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def main():
|
|
171
|
+
print("=" * 60)
|
|
172
|
+
print("领域认知化 · DomainReasoner + 财务守门人(v2 ③ Step1)")
|
|
173
|
+
print("=" * 60)
|
|
174
|
+
test_reasoner_base_order()
|
|
175
|
+
test_finance_gatekeeper_reasoner()
|
|
176
|
+
test_context_evidence_no_decision_change()
|
|
177
|
+
test_llm_arbitration_safety()
|
|
178
|
+
test_real_path_evidence()
|
|
179
|
+
print("\n" + "=" * 60)
|
|
180
|
+
print("✓ Step1 验证完成:观察→假设→证据→裁决 形式化;规则实现、裁决不变、证据链可审计")
|
|
181
|
+
print("=" * 60)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
main()
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""发布前校验脚本(tools/release_check.py)的规则测试。
|
|
2
|
+
|
|
3
|
+
锁住三件事:
|
|
4
|
+
1. 四处版本号必须一致 —— 只改一处的发布物会让用户装到对不上的版本;
|
|
5
|
+
2. tag 必须指向本次版本 —— 发布不可撤回,yank 也仍占号;
|
|
6
|
+
3. 产物里不能有 .env / docs/internal / 运行期数据,缺 vendor 只算提示(占位包现状)。
|
|
7
|
+
|
|
8
|
+
脚本放在 tools/ 不在包里,故按文件路径加载;测试只构造最小归档,不跑真构建(秒级)。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import io
|
|
15
|
+
import json
|
|
16
|
+
import shutil
|
|
17
|
+
import sys
|
|
18
|
+
import tarfile
|
|
19
|
+
import zipfile
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
23
|
+
|
|
24
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _load():
|
|
28
|
+
spec = importlib.util.spec_from_file_location(
|
|
29
|
+
"release_check", REPO / "tools" / "release_check.py")
|
|
30
|
+
mod = importlib.util.module_from_spec(spec)
|
|
31
|
+
sys.modules["release_check"] = mod # dataclass 需要能按模块名回查,否则 exec 时炸
|
|
32
|
+
spec.loader.exec_module(mod)
|
|
33
|
+
return mod
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
rc = _load()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ── 造最小产物 ────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
def _wheel(dist: Path, version="0.1.0", name="foxclaw", extra=(),
|
|
42
|
+
description=True) -> Path:
|
|
43
|
+
p = dist / f"{name}-{version}-py3-none-any.whl"
|
|
44
|
+
body = ("\n\n" + "long description body. " * 12) if description else ""
|
|
45
|
+
with zipfile.ZipFile(p, "w") as z:
|
|
46
|
+
z.writestr(f"{name}/__init__.py", "__version__ = '0.1.0'\n")
|
|
47
|
+
z.writestr(f"{name}/cli.py", "def main(): pass\n")
|
|
48
|
+
z.writestr(f"{name}-{version}.dist-info/METADATA",
|
|
49
|
+
f"Name: {name}\nVersion: {version}{body}\n")
|
|
50
|
+
for e in extra:
|
|
51
|
+
z.writestr(e, "x")
|
|
52
|
+
return p
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _sdist(dist: Path, version="0.1.0", name="foxclaw", extra=()) -> Path:
|
|
56
|
+
p = dist / f"{name}-{version}.tar.gz"
|
|
57
|
+
with tarfile.open(p, "w:gz") as t:
|
|
58
|
+
for arc in (f"{name}-{version}/foxclaw/cli.py",
|
|
59
|
+
f"{name}-{version}/PKG-INFO", *extra):
|
|
60
|
+
data = b"x"
|
|
61
|
+
info = tarfile.TarInfo(arc)
|
|
62
|
+
info.size = len(data)
|
|
63
|
+
t.addfile(info, io.BytesIO(data))
|
|
64
|
+
return p
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _tgz(dist: Path, version="0.1.0", name="foxclaw", pkg=None, extra=()) -> Path:
|
|
68
|
+
p = dist / f"{name}-{version}.tgz"
|
|
69
|
+
payload = json.dumps(pkg if pkg is not None else
|
|
70
|
+
{"name": name, "version": version}).encode()
|
|
71
|
+
with tarfile.open(p, "w:gz") as t:
|
|
72
|
+
for arc, data in (("package/package.json", payload),
|
|
73
|
+
("package/index.js", b"export default {}"),
|
|
74
|
+
*[(e, b"x") for e in extra]):
|
|
75
|
+
info = tarfile.TarInfo(arc)
|
|
76
|
+
info.size = len(data)
|
|
77
|
+
t.addfile(info, io.BytesIO(data))
|
|
78
|
+
return p
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _mini_repo(root: Path, version: str, npm_version: str) -> Path:
|
|
82
|
+
"""只含四处版本来源的最小仓库,用来测"漏改一处"。"""
|
|
83
|
+
(root / "foxclaw").mkdir(parents=True)
|
|
84
|
+
(root / "npm").mkdir()
|
|
85
|
+
(root / "pyproject.toml").write_text(
|
|
86
|
+
f'[project]\nname = "foxclaw"\nversion = "{version}"\n', encoding="utf-8")
|
|
87
|
+
(root / "foxclaw" / "__init__.py").write_text(
|
|
88
|
+
f'__version__ = "{version}"\n', encoding="utf-8")
|
|
89
|
+
(root / "npm" / "package.json").write_text(
|
|
90
|
+
json.dumps({"name": "foxclaw", "version": npm_version}), encoding="utf-8")
|
|
91
|
+
(root / "npm" / "index.js").write_text(
|
|
92
|
+
f'export const version = "{npm_version}";\n', encoding="utf-8")
|
|
93
|
+
return root
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ── 版本与 tag ────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
def test_repo_versions_are_consistent():
|
|
99
|
+
"""真仓库当前状态:四处一致,无 error。"""
|
|
100
|
+
assert [i for i in rc.check_versions(REPO) if i.level == "error"] == []
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_single_missed_source_is_an_error(tmp_path):
|
|
104
|
+
root = _mini_repo(tmp_path, "0.2.0", npm_version="0.1.0")
|
|
105
|
+
errs = [i for i in rc.check_versions(root) if i.level == "error"]
|
|
106
|
+
assert errs and any("npm" in i.msg for i in errs), errs
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_unreadable_source_is_an_error(tmp_path):
|
|
110
|
+
root = _mini_repo(tmp_path, "0.1.0", "0.1.0")
|
|
111
|
+
(root / "npm" / "index.js").write_text("// 没有导出\n", encoding="utf-8")
|
|
112
|
+
assert any(i.code == "unreadable" for i in rc.check_versions(root))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@pytest.mark.parametrize("tag", ["v0.1.0", "0.1.0"])
|
|
116
|
+
def test_tag_matching_version_passes(tag):
|
|
117
|
+
assert rc.check_tag(tag, "0.1.0") == []
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.mark.parametrize("tag", ["v0.2.0", "v0.1.0-rc1"])
|
|
121
|
+
def test_tag_drift_is_an_error(tag):
|
|
122
|
+
errs = rc.check_tag(tag, "0.1.0")
|
|
123
|
+
assert errs and errs[0].code == "tag_mismatch"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_empty_tag_is_skipped():
|
|
127
|
+
assert rc.check_tag("", "0.1.0") == []
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ── 项目页正文(long_description)────────────────────────────
|
|
131
|
+
|
|
132
|
+
def test_repo_declares_readme():
|
|
133
|
+
"""真仓库必须声明正文:0.1.1 在 PyPI 上的项目页就是空白的,这就是回归点。"""
|
|
134
|
+
assert [i for i in rc.check_metadata(REPO) if i.level == "error"] == []
|
|
135
|
+
assert rc.readme_target(REPO).endswith(".md")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_missing_readme_is_error(tmp_path):
|
|
139
|
+
root = _mini_repo(tmp_path, "0.1.0", "0.1.0")
|
|
140
|
+
assert any(i.code == "no_readme" for i in rc.check_metadata(root))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_short_readme_is_error(tmp_path):
|
|
144
|
+
root = _mini_repo(tmp_path, "0.1.0", "0.1.0")
|
|
145
|
+
(root / "pyproject.toml").write_text(
|
|
146
|
+
'[project]\nname = "foxclaw"\nversion = "0.1.0"\nreadme = "README.md"\n',
|
|
147
|
+
encoding="utf-8")
|
|
148
|
+
(root / "README.md").write_text("短\n", encoding="utf-8")
|
|
149
|
+
assert any(i.code == "readme_too_short" for i in rc.check_metadata(root))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_readme_pointing_to_missing_file_is_error(tmp_path):
|
|
153
|
+
root = _mini_repo(tmp_path, "0.1.0", "0.1.0")
|
|
154
|
+
(root / "pyproject.toml").write_text(
|
|
155
|
+
'[project]\nname = "foxclaw"\nversion = "0.1.0"\nreadme = "NOPE.md"\n',
|
|
156
|
+
encoding="utf-8")
|
|
157
|
+
assert any(i.code == "readme_missing" for i in rc.check_metadata(root))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ── 归档内容 ──────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
@pytest.mark.parametrize("entry", [
|
|
163
|
+
"foxclaw/.env",
|
|
164
|
+
"foxclaw/.env.local",
|
|
165
|
+
"foxclaw-0.1.0/docs/internal/secret.md",
|
|
166
|
+
"foxclaw/foxclaw_data/sessions.db",
|
|
167
|
+
"foxclaw/old.zip",
|
|
168
|
+
"foxclaw/__pycache__/x.pyc",
|
|
169
|
+
"foxclaw/.venv/lib/x.py",
|
|
170
|
+
])
|
|
171
|
+
def test_forbidden_entries_detected(entry):
|
|
172
|
+
assert rc.forbidden_entry(entry), entry
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@pytest.mark.parametrize("entry", [
|
|
176
|
+
"foxclaw/.env.example", # 模板必须能进包
|
|
177
|
+
"foxclaw/cli.py",
|
|
178
|
+
"foxclaw/vendor/hmr/hmr/__init__.py",
|
|
179
|
+
])
|
|
180
|
+
def test_allowed_entries_pass(entry):
|
|
181
|
+
assert rc.forbidden_entry(entry) is None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ── dist / npm ────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
def test_check_dist_passes_with_vendor_warning_only(tmp_path):
|
|
187
|
+
dist = tmp_path / "dist"
|
|
188
|
+
dist.mkdir()
|
|
189
|
+
_wheel(dist)
|
|
190
|
+
_sdist(dist)
|
|
191
|
+
issues = rc.check_dist(dist, "foxclaw", "0.1.0")
|
|
192
|
+
assert [i for i in issues if i.level == "error"] == []
|
|
193
|
+
assert any(i.code == "no_vendor" for i in issues), "缺 vendor 应显式提示"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_check_dist_flags_missing_wheel(tmp_path):
|
|
197
|
+
dist = tmp_path / "dist"
|
|
198
|
+
dist.mkdir()
|
|
199
|
+
_sdist(dist)
|
|
200
|
+
assert any(i.code == "wheel_count" for i in rc.check_dist(dist, "foxclaw", "0.1.0"))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_check_dist_flags_version_drift_in_filename(tmp_path):
|
|
204
|
+
dist = tmp_path / "dist"
|
|
205
|
+
dist.mkdir()
|
|
206
|
+
_wheel(dist, version="0.2.0")
|
|
207
|
+
_sdist(dist, version="0.1.0")
|
|
208
|
+
codes = {i.code for i in rc.check_dist(dist, "foxclaw", "0.1.0")}
|
|
209
|
+
assert "wheel_name" in codes or "meta_mismatch" in codes, codes
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_check_dist_flags_secret_inside_archive(tmp_path):
|
|
213
|
+
dist = tmp_path / "dist"
|
|
214
|
+
dist.mkdir()
|
|
215
|
+
_wheel(dist, extra=(".env",))
|
|
216
|
+
_sdist(dist)
|
|
217
|
+
msgs = [i.msg for i in rc.check_dist(dist, "foxclaw", "0.1.0")
|
|
218
|
+
if i.code == "forbidden_entry"]
|
|
219
|
+
assert msgs and any(".env" in m for m in msgs), msgs
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_check_dist_flags_empty_long_description(tmp_path):
|
|
223
|
+
dist = tmp_path / "dist"
|
|
224
|
+
dist.mkdir()
|
|
225
|
+
_wheel(dist, description=False)
|
|
226
|
+
_sdist(dist)
|
|
227
|
+
codes = {i.code for i in rc.check_dist(dist, "foxclaw", "0.1.0")}
|
|
228
|
+
assert "no_long_description" in codes, codes
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_check_dist_missing_dir_is_error(tmp_path):
|
|
232
|
+
assert any(i.code == "no_dist"
|
|
233
|
+
for i in rc.check_dist(tmp_path / "nope", "foxclaw", "0.1.0"))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_npm_tgz_ok(tmp_path):
|
|
237
|
+
assert rc.check_npm_tgz(_tgz(tmp_path), "foxclaw", "0.1.0") == []
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_npm_tgz_name_drift_detected(tmp_path):
|
|
241
|
+
tgz = _tgz(tmp_path, pkg={"name": "foxclaw", "version": "0.2.0"})
|
|
242
|
+
codes = {i.code for i in rc.check_npm_tgz(tgz, "foxclaw", "0.1.0")}
|
|
243
|
+
assert "npm_meta" in codes, codes
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_npm_tgz_filename_drift_detected(tmp_path):
|
|
247
|
+
tgz = _tgz(tmp_path, version="0.2.0")
|
|
248
|
+
assert any(i.code == "npm_name" for i in rc.check_npm_tgz(tgz, "foxclaw", "0.1.0"))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_run_end_to_end(tmp_path):
|
|
252
|
+
"""run() 串起来:真仓库 + 构造的 dist/tgz,无 tag 也应通过。"""
|
|
253
|
+
dist = tmp_path / "dist"
|
|
254
|
+
dist.mkdir()
|
|
255
|
+
issues = rc.run(REPO, tag="", dist=dist, npm_tgz=None)
|
|
256
|
+
assert any(i.level == "error" for i in issues), "空 dist 应报错"
|
|
257
|
+
# 版本号跟着仓库走:写死 0.1.0 会在每次改版本时假红
|
|
258
|
+
version = rc.collect_versions(REPO)["pyproject.toml:project.version"]
|
|
259
|
+
_wheel(dist, version=version)
|
|
260
|
+
_sdist(dist, version=version)
|
|
261
|
+
tgz = _tgz(dist, version=version)
|
|
262
|
+
assert [i for i in rc.run(REPO, f"v{version}", dist, tgz) if i.level == "error"] == []
|
|
263
|
+
shutil.rmtree(dist)
|