foxclaw 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -8
- package/package.json +1 -1
- package/python/.env.example +8 -0
- package/python/.github/workflows/ci.yml +47 -2
- package/python/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/README.md +11 -2
- package/python/README_CN.md +32 -20
- package/python/bench/bm25_audit.py +1 -14
- package/python/bench/bm25_regression.py +0 -1
- package/python/bench/bm25_stress.py +0 -4
- package/python/bench/cd_audit.py +0 -23
- package/python/bench/chaos.py +6 -8
- package/python/bench/e2e_real_models.py +0 -1
- package/python/bench/llm_retry_test.py +2 -9
- package/python/bench/load_test.py +5 -8
- package/python/bench/vector_ivf_upgrade.py +1 -7
- package/python/docs/RELEASE.md +4 -3
- package/python/docs/RUN_CN.md +2 -2
- package/python/evals/governance/cases.py +0 -37
- package/python/evals/governance/run_evals.py +5 -37
- package/python/examples/governance_middleware_demo.py +1 -6
- package/python/examples/langgraph_governance_demo.py +7 -11
- package/python/foxclaw/__init__.py +34 -152
- package/python/foxclaw/agents/base.py +65 -136
- package/python/foxclaw/agents/impl/_helpers.py +2 -10
- package/python/foxclaw/agents/impl/finance.py +4 -21
- package/python/foxclaw/agents/impl/office.py +7 -24
- package/python/foxclaw/agents/impl/supervisor.py +12 -79
- package/python/foxclaw/agents/mixins/_context.py +0 -23
- package/python/foxclaw/agents/mixins/augment.py +10 -16
- package/python/foxclaw/agents/mixins/llm.py +7 -6
- package/python/foxclaw/agents/mixins/memory.py +7 -1
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +0 -6
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
- package/python/foxclaw/api/middleware.py +5 -28
- package/python/foxclaw/api/routes/ahs.py +60 -73
- package/python/foxclaw/api/routes/attachments.py +5 -6
- package/python/foxclaw/api/routes/browser.py +7 -11
- package/python/foxclaw/api/routes/domain.py +18 -26
- package/python/foxclaw/api/routes/knowledge.py +3 -21
- package/python/foxclaw/api/routes/llm_admin.py +21 -33
- package/python/foxclaw/api/routes/memory.py +28 -12
- package/python/foxclaw/api/routes/orchestration.py +3 -5
- package/python/foxclaw/api/routes/recovery.py +4 -3
- package/python/foxclaw/api/routes/review.py +2 -11
- package/python/foxclaw/api/routes/search.py +3 -7
- package/python/foxclaw/api/routes/system.py +33 -49
- package/python/foxclaw/api/routes/task.py +31 -49
- package/python/foxclaw/api/routes/ws.py +2 -6
- package/python/foxclaw/api/spine.py +1 -4
- package/python/foxclaw/browser/action/action_executor.py +1 -5
- package/python/foxclaw/browser/action/action_types.py +0 -1
- package/python/foxclaw/browser/action/url_policy.py +1 -5
- package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
- package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
- package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
- package/python/foxclaw/browser/perception/element_indexer.py +1 -2
- package/python/foxclaw/browser/service.py +7 -11
- package/python/foxclaw/capability/embedding_service.py +4 -6
- package/python/foxclaw/channels/__init__.py +9 -11
- package/python/foxclaw/channels/discord.py +4 -13
- package/python/foxclaw/channels/dispatcher.py +2 -27
- package/python/foxclaw/channels/feishu.py +10 -20
- package/python/foxclaw/channels/openclaw_bridge.py +4 -9
- package/python/foxclaw/channels/signing.py +9 -9
- package/python/foxclaw/channels/telegram.py +3 -4
- package/python/foxclaw/channels/weixin.py +7 -17
- package/python/foxclaw/cli.py +42 -13
- package/python/foxclaw/cognitive/reasoner.py +9 -10
- package/python/foxclaw/cognitive/state.py +18 -24
- package/python/foxclaw/core/audit_chain.py +1 -3
- package/python/foxclaw/core/audit_sink.py +3 -3
- package/python/foxclaw/core/compaction.py +8 -26
- package/python/foxclaw/core/config.py +2 -2
- package/python/foxclaw/core/confirm_nonce.py +6 -12
- package/python/foxclaw/core/envfile.py +2 -2
- package/python/foxclaw/core/failure_policy.py +17 -26
- package/python/foxclaw/core/gateway.py +6 -10
- package/python/foxclaw/core/governance_audit.py +3 -7
- package/python/foxclaw/core/idempotency.py +15 -22
- package/python/foxclaw/core/injection_guard.py +10 -29
- package/python/foxclaw/core/memory_adapter.py +30 -57
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +36 -148
- package/python/foxclaw/core/paths.py +1 -1
- package/python/foxclaw/core/recovery.py +21 -34
- package/python/foxclaw/core/scheduler.py +23 -26
- package/python/foxclaw/core/secrets.py +0 -3
- package/python/foxclaw/core/sessions.py +4 -6
- package/python/foxclaw/core/settings.py +16 -32
- package/python/foxclaw/core/shared_write.py +11 -23
- package/python/foxclaw/core/steering_queue.py +12 -11
- package/python/foxclaw/core/system_bus.py +1 -1
- package/python/foxclaw/core/thought_chain.py +30 -32
- package/python/foxclaw/core/token_meter.py +9 -13
- package/python/foxclaw/core/tools.py +4 -12
- package/python/foxclaw/core/tracing.py +25 -40
- package/python/foxclaw/core/truth_pipeline.py +5 -9
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +11 -37
- package/python/foxclaw/core/verify.py +6 -14
- package/python/foxclaw/core/web_search.py +4 -28
- package/python/foxclaw/finance/evidence.py +14 -18
- package/python/foxclaw/finance/fx.py +1 -3
- package/python/foxclaw/finance/graph.py +9 -18
- package/python/foxclaw/finance/models.py +20 -24
- package/python/foxclaw/finance/nlparse.py +12 -72
- package/python/foxclaw/finance/policy.py +8 -36
- package/python/foxclaw/finance/reasoner.py +3 -11
- package/python/foxclaw/finance/reasoning.py +13 -34
- package/python/foxclaw/finance/reconcile.py +5 -10
- package/python/foxclaw/finance/runtime.py +19 -46
- package/python/foxclaw/governance/middleware.py +13 -21
- package/python/foxclaw/harness/_base.py +2 -8
- package/python/foxclaw/harness/_policy.py +19 -38
- package/python/foxclaw/harness/drift.py +1 -13
- package/python/foxclaw/harness/event_bus.py +2 -5
- package/python/foxclaw/harness/harness.py +9 -97
- package/python/foxclaw/harness/loop_detection.py +11 -18
- package/python/foxclaw/itinerary/amap_client.py +2 -5
- package/python/foxclaw/itinerary/models.py +7 -7
- package/python/foxclaw/itinerary/reasoner.py +1 -6
- package/python/foxclaw/itinerary/reasoning.py +15 -21
- package/python/foxclaw/itinerary/runtime.py +8 -12
- package/python/foxclaw/kernel/capabilities.py +5 -6
- package/python/foxclaw/kernel/policy_gate.py +4 -10
- package/python/foxclaw/kernel/process_manager.py +17 -36
- package/python/foxclaw/kernel/scheduler.py +3 -9
- package/python/foxclaw/kernel/sentinel.py +13 -23
- package/python/foxclaw/kernel/sentinel_client.py +12 -15
- package/python/foxclaw/kernel/spine.py +9 -9
- package/python/foxclaw/knowledge/__init__.py +4 -20
- package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
- package/python/foxclaw/learning/knowledge_graph.py +4 -9
- package/python/foxclaw/learning/models.py +13 -13
- package/python/foxclaw/learning/reasoner.py +1 -4
- package/python/foxclaw/learning/runtime.py +19 -31
- package/python/foxclaw/learning/spaced_repetition.py +2 -7
- package/python/foxclaw/mcp_server.py +2 -3
- package/python/foxclaw/orchestration/__init__.py +16 -33
- package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
- package/python/foxclaw/planning/llm_planner.py +3 -3
- package/python/foxclaw/planning/planner.py +6 -11
- package/python/foxclaw/search/agent_port.py +7 -11
- package/python/foxclaw/search/evidence.py +0 -4
- package/python/foxclaw/search/internal_search.py +3 -5
- package/python/foxclaw/search/models.py +11 -13
- package/python/foxclaw/search/planner.py +3 -3
- package/python/foxclaw/search/retriever.py +1 -1
- package/python/foxclaw/search/runtime.py +1 -8
- package/python/foxclaw/skills/__init__.py +46 -100
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +21 -25
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +16 -42
- package/python/foxclaw/skills/markdown_skill.py +10 -9
- package/python/foxclaw/skills/mcp_client.py +5 -11
- package/python/foxclaw/skills/mcp_store.py +0 -3
- package/python/foxclaw/skills/permission.py +13 -48
- package/python/foxclaw/skills/pipeline.py +21 -53
- package/python/foxclaw/skills/policy.py +4 -10
- package/python/foxclaw/skills/review.py +3 -3
- package/python/frontend/deck/auth.js +0 -7
- package/python/frontend/deck/chat_model_picker.js +0 -54
- package/python/frontend/deck/domain.js +0 -20
- package/python/frontend/deck/main.js +0 -144
- package/python/frontend/deck/markdown.js +0 -4
- package/python/frontend/deck/settings.js +1 -18
- package/python/frontend/deck/state.js +0 -1
- package/python/frontend/deck/util.js +0 -2
- package/python/frontend/deck/visuals.js +0 -22
- package/python/frontend/planet/planet.js +0 -201
- package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
- package/python/frontend/v3/index.html +3 -3
- package/python/frontend/widget_runtime.js +0 -43
- package/python/frontend/widgets.js +0 -57
- package/python/frontend_src/index.html +1 -1
- package/python/frontend_src/package-lock.json +2 -2
- package/python/frontend_src/package.json +1 -1
- package/python/frontend_src/src/App.vue +3 -7
- package/python/frontend_src/src/api.js +0 -7
- package/python/frontend_src/src/components/ChatDock.vue +27 -39
- package/python/frontend_src/src/components/DocViewer.vue +0 -9
- package/python/frontend_src/src/components/Inspector.vue +0 -6
- package/python/frontend_src/src/components/LeftRail.vue +60 -39
- package/python/frontend_src/src/components/MenuPopover.vue +9 -11
- package/python/frontend_src/src/components/MessageStream.vue +18 -30
- package/python/frontend_src/src/components/ModelPicker.vue +0 -5
- package/python/frontend_src/src/components/QuickIngest.vue +0 -6
- package/python/frontend_src/src/components/Stage.vue +71 -25
- package/python/frontend_src/src/components/ToolMenu.vue +0 -3
- package/python/frontend_src/src/components/TopBar.vue +38 -10
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
- package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
- package/python/frontend_src/src/lib/fmt.js +0 -4
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +0 -14
- package/python/frontend_src/src/main.js +2 -0
- package/python/frontend_src/src/stores/app.js +17 -32
- package/python/frontend_src/src/stores/chat.js +0 -34
- package/python/frontend_src/src/stores/model.js +0 -8
- package/python/frontend_src/src/stores/panels.js +0 -41
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/vite.config.js +0 -7
- package/python/pyproject.toml +6 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +3 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- package/python/tests/test_ahs_p2.py +4 -25
- package/python/tests/test_anthropic_tools.py +1 -15
- package/python/tests/test_api.py +19 -9
- package/python/tests/test_api_auth.py +1 -5
- package/python/tests/test_attachments.py +9 -25
- package/python/tests/test_audit_chain.py +2 -7
- package/python/tests/test_audit_sink.py +0 -6
- package/python/tests/test_browser.py +2 -6
- package/python/tests/test_browser_request_guard.py +0 -4
- package/python/tests/test_capabilities.py +0 -6
- package/python/tests/test_channel_signing.py +2 -9
- package/python/tests/test_channels.py +1 -11
- package/python/tests/test_config_layering.py +0 -4
- package/python/tests/test_confirm_nonce.py +1 -12
- package/python/tests/test_context_compaction_wiring.py +1 -7
- package/python/tests/test_dispatch.py +0 -6
- package/python/tests/test_domain_evals.py +11 -80
- package/python/tests/test_domain_import.py +0 -6
- package/python/tests/test_domain_reasoners.py +0 -14
- package/python/tests/test_e2e.py +0 -17
- package/python/tests/test_emb_cache.py +7 -17
- package/python/tests/test_embedding_singleton.py +1 -3
- package/python/tests/test_evals_smoke.py +0 -24
- package/python/tests/test_event_spine.py +10 -18
- package/python/tests/test_evidence_semantic.py +1 -14
- package/python/tests/test_evidence_sse.py +3 -6
- package/python/tests/test_file_ingest.py +0 -11
- package/python/tests/test_finance.py +0 -9
- package/python/tests/test_finance_ledger.py +5 -12
- package/python/tests/test_finance_persist.py +8 -12
- package/python/tests/test_gateway.py +3 -22
- package/python/tests/test_governance.py +5 -52
- package/python/tests/test_governance_audit.py +4 -39
- package/python/tests/test_governance_middleware.py +1 -7
- package/python/tests/test_hkc.py +6 -24
- package/python/tests/test_hkc_index_build_manager.py +0 -33
- package/python/tests/test_hkc_manifest_reuse.py +4 -27
- package/python/tests/test_hkc_search_event_sync.py +3 -54
- package/python/tests/test_import_path_guard.py +0 -6
- package/python/tests/test_injection_guard.py +18 -36
- package/python/tests/test_integration.py +5 -29
- package/python/tests/test_itinerary.py +0 -3
- package/python/tests/test_kee_semantic.py +0 -5
- package/python/tests/test_kernel.py +1 -11
- package/python/tests/test_learning.py +3 -12
- package/python/tests/test_llm_admin.py +4 -19
- package/python/tests/test_llm_planner.py +1 -23
- package/python/tests/test_markdown_skills.py +3 -14
- package/python/tests/test_markdown_skills_openclaw.py +4 -9
- package/python/tests/test_mcp_connectors.py +3 -15
- package/python/tests/test_mcp_guardian_discovery.py +2 -6
- package/python/tests/test_mcp_no_memory.py +3 -3
- package/python/tests/test_mcp_policy_gate.py +0 -1
- package/python/tests/test_mcp_server.py +6 -9
- package/python/tests/test_memory_recall.py +1 -4
- package/python/tests/test_metrics.py +0 -8
- package/python/tests/test_multi_chain.py +1 -13
- package/python/tests/test_multimodal.py +5 -18
- package/python/tests/test_negotiation.py +3 -13
- package/python/tests/test_orchestration.py +1 -11
- package/python/tests/test_orchestration_decoupled.py +0 -6
- package/python/tests/test_p3_p5_hardening.py +3 -17
- package/python/tests/test_parallel_dispatch.py +4 -12
- package/python/tests/test_pipeline.py +17 -58
- package/python/tests/test_plan_visible.py +4 -4
- package/python/tests/test_planner.py +0 -7
- package/python/tests/test_policy_gate.py +2 -3
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +2 -17
- package/python/tests/test_release_check.py +2 -13
- package/python/tests/test_resume_checkpoint.py +7 -15
- package/python/tests/test_routing.py +2 -12
- package/python/tests/test_scheduler.py +7 -10
- package/python/tests/test_search.py +1 -11
- package/python/tests/test_secrets.py +4 -10
- package/python/tests/test_semantic.py +1 -10
- package/python/tests/test_sessions.py +3 -4
- package/python/tests/test_settings.py +2 -18
- package/python/tests/test_shadow_learning.py +3 -43
- package/python/tests/test_shared_write.py +0 -7
- package/python/tests/test_skill_taint_gate.py +1 -10
- package/python/tests/test_skills.py +2 -18
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +2 -5
- package/python/tests/test_streaming.py +2 -7
- package/python/tests/test_supervisor_pipeline.py +4 -5
- package/python/tests/test_supervisor_triage.py +4 -10
- package/python/tests/test_taint.py +2 -7
- package/python/tests/test_task_stream.py +1 -12
- package/python/tests/test_thinking_sse.py +0 -1
- package/python/tests/test_tool_center.py +1 -17
- package/python/tests/test_tool_parallel.py +2 -2
- package/python/tests/test_tool_params.py +1 -5
- package/python/tests/test_tool_use.py +0 -13
- package/python/tests/test_toplevel_concurrency.py +3 -4
- package/python/tests/test_tracing.py +7 -41
- package/python/tests/test_truth_crystallizer.py +4 -6
- package/python/tests/test_truth_flow.py +2 -7
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_verify_evidence.py +8 -21
- package/python/tests/test_version_sync.py +35 -6
- package/python/tests/test_web_trigger.py +0 -17
- package/python/tools/check_faiss_type.py +0 -1
- package/python/tools/check_log.ps1 +0 -8
- package/python/tools/mypy_baseline.txt +12 -4
- package/python/tools/mypy_ratchet.py +52 -6
- package/python/tools/pack.py +0 -5
- package/python/tools/release_check.py +7 -7
- package/python/tools/restart_foxclaw.ps1 +5 -8
- package/python/tools/shared_write_audit.py +1 -15
- package/python/tools/size_check.py +1 -1
- package/python/tools/strip_comments.py +228 -0
- package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
package/python/tests/test_hkc.py
CHANGED
|
@@ -35,11 +35,10 @@ def test_assemble_search_close():
|
|
|
35
35
|
print(f" ✓ stats: total_kus={s.get('total_kus')} embedding={s.get('embedding')!r}")
|
|
36
36
|
|
|
37
37
|
svc.close()
|
|
38
|
-
svc.close()
|
|
38
|
+
svc.close()
|
|
39
39
|
print(" ✓ close() 幂等")
|
|
40
40
|
finally:
|
|
41
|
-
|
|
42
|
-
shutil.rmtree(d) # 不 ignore_errors:泄漏则抛 OSError → 测试失败
|
|
41
|
+
shutil.rmtree(d)
|
|
43
42
|
ok_rmtree = True
|
|
44
43
|
assert ok_rmtree
|
|
45
44
|
print(" ✓ close 后 rmtree 成功 → H1 资源泄漏修复生效(无 WinError 32)")
|
|
@@ -51,11 +50,10 @@ def test_via_foxclawos_lazy_teardown():
|
|
|
51
50
|
try:
|
|
52
51
|
from foxclaw import Foxclaw
|
|
53
52
|
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
54
|
-
# 惰性:未访问 .hkc 前不该装配(不开句柄)
|
|
55
53
|
assert os_._hkc_service is None and os_._hkc_inited is False
|
|
56
54
|
print(" ✓ 构造 OS 后未装配 HKC(惰性,未开 db 句柄)")
|
|
57
55
|
|
|
58
|
-
hkc = os_.hkc
|
|
56
|
+
hkc = os_.hkc
|
|
59
57
|
assert os_._hkc_inited is True
|
|
60
58
|
assert hkc is not None and hkc.available
|
|
61
59
|
print(" ✓ 首次访问 os_.hkc 触发装配 available=True")
|
|
@@ -67,7 +65,7 @@ def test_via_foxclawos_lazy_teardown():
|
|
|
67
65
|
asyncio.run(os_.aclose())
|
|
68
66
|
print(" ✓ aclose() 释放浏览器 + HKC 资源")
|
|
69
67
|
finally:
|
|
70
|
-
shutil.rmtree(d)
|
|
68
|
+
shutil.rmtree(d)
|
|
71
69
|
print(" ✓ teardown 后 rmtree 成功(HKC 句柄经 aclose 释放)")
|
|
72
70
|
|
|
73
71
|
|
|
@@ -79,11 +77,9 @@ def test_ability_readonly_surface():
|
|
|
79
77
|
svc = init_hkc(d, api_key=None)
|
|
80
78
|
assert svc is not None and svc.available
|
|
81
79
|
from hkc_sdk.exceptions import HKCInsufficientCoverage, HKCNotFoundError
|
|
82
|
-
# list_abilities:返回 taxonomy 里定义的能力 key(静态,无需摄入)
|
|
83
80
|
keys = svc.client.list_abilities()
|
|
84
81
|
assert isinstance(keys, list) and len(keys) > 0, f"应有内置能力定义,实得 {keys}"
|
|
85
82
|
print(f" ✓ list_abilities 返回 {len(keys)} 个能力: {keys[:4]}...")
|
|
86
|
-
# compile_ability:空知识库 → 覆盖不足 → 证据 gate(抛 HKCInsufficientCoverage,带 missing/coverage)
|
|
87
83
|
gated = False
|
|
88
84
|
try:
|
|
89
85
|
svc.client.compile_ability(keys[0])
|
|
@@ -92,9 +88,6 @@ def test_ability_readonly_surface():
|
|
|
92
88
|
assert hasattr(e, "missing_skills") and hasattr(e, "coverage")
|
|
93
89
|
assert gated, "空知识库编译能力应被证据 gate 拦(覆盖不足)"
|
|
94
90
|
print(" ✓ 空库 compile_ability 被证据 gate 拦(覆盖不足→能力不予授予)")
|
|
95
|
-
# neighbors:不存在 KU → SDK 抛 HKCNotFoundError(契约)。注:/api/knowledge/ku/{id}/neighbors
|
|
96
|
-
# 端点已在 V3 接口清理删除(前端零用 + HKC router 的 /knowledge/ku/{id}/neighbors 有等价),
|
|
97
|
-
# 此处只验 SDK 层契约;SDK neighbors 仍被 ACE 能力/图查询内部使用,保留测试。
|
|
98
91
|
raised = False
|
|
99
92
|
try:
|
|
100
93
|
svc.client.neighbors("KU_nonexistent", max_depth=2, top_k=10)
|
|
@@ -116,14 +109,12 @@ def test_agent_available_abilities():
|
|
|
116
109
|
try:
|
|
117
110
|
from foxclaw import Foxclaw
|
|
118
111
|
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
119
|
-
# 空知识库:所有能力都应 sufficient=False(证据 gate:没知识不予授予)
|
|
120
112
|
abils = os_.finance.available_abilities()
|
|
121
113
|
assert isinstance(abils, list) and len(abils) > 0, f"应枚举出能力,实得 {abils}"
|
|
122
114
|
assert all(a["sufficient"] is False for a in abils), \
|
|
123
115
|
f"空库→全部证据 gate 拦(sufficient=False),实得 {[a['sufficient'] for a in abils]}"
|
|
124
116
|
assert all("missing_skills" in a for a in abils), "未达标应附 missing_skills"
|
|
125
117
|
print(f" ✓ 空库自省:{len(abils)} 个能力全 sufficient=False(证据 gate)")
|
|
126
|
-
# 各 Agent 看到一致(知识全局共享)
|
|
127
118
|
assert len(os_.office.available_abilities()) == len(abils)
|
|
128
119
|
print(" ✓ 各 Agent 一致(HKC 知识全局共享)")
|
|
129
120
|
asyncio.run(os_.aclose())
|
|
@@ -143,14 +134,11 @@ def test_ability_context_injection():
|
|
|
143
134
|
ag = os_.finance
|
|
144
135
|
base = [{"role": "user", "content": "原始问题"}]
|
|
145
136
|
|
|
146
|
-
# (a) 默认关:不注入(行为逐字不变)
|
|
147
137
|
assert ag._ability_augment("帮我做写作专家分析", list(base)) == base
|
|
148
138
|
|
|
149
|
-
# (b) 启用但无达标能力(真实空 HKC)→ no-op
|
|
150
139
|
ag.ABILITY_AUGMENT = True
|
|
151
140
|
assert ag._ability_augment("帮我做写作专家分析", list(base)) == base
|
|
152
141
|
|
|
153
|
-
# (c) 启用 + 注册假 HKC(有达标能力 + 检索命中)→ 注入知识上下文到 messages 前
|
|
154
142
|
class _C:
|
|
155
143
|
def list_abilities(self): return ["writing_expert"]
|
|
156
144
|
def compile_ability(self, k):
|
|
@@ -165,7 +153,6 @@ def test_ability_context_injection():
|
|
|
165
153
|
assert out[1] == base[0], "原消息应保留在注入内容之后"
|
|
166
154
|
print(" ✓ 默认关不注入;空库 no-op;启用+达标+命中→注入知识上下文")
|
|
167
155
|
|
|
168
|
-
# (d) fail-safe:能力增强内部异常(search 抛错)→ 原样返回,不影响主流程
|
|
169
156
|
class _Cbad(_C):
|
|
170
157
|
def search(self, *a, **k): raise RuntimeError("boom")
|
|
171
158
|
os_.capabilities.register("hkc", SimpleNamespace(available=True, client=_Cbad()))
|
|
@@ -187,7 +174,7 @@ def test_ability_as_skill_step4():
|
|
|
187
174
|
def list_abilities(self): return ["writing_expert", "quant_analyst"]
|
|
188
175
|
def compile_ability(self, k):
|
|
189
176
|
if k == "quant_analyst":
|
|
190
|
-
raise RuntimeError("coverage 不足")
|
|
177
|
+
raise RuntimeError("coverage 不足")
|
|
191
178
|
return SimpleNamespace(ability_key=k, display_name="写作专家",
|
|
192
179
|
domain="writing", coverage={"修辞": 0.9})
|
|
193
180
|
def search(self, q, mode="hybrid", top_k=10, domain=""):
|
|
@@ -197,19 +184,16 @@ def test_ability_as_skill_step4():
|
|
|
197
184
|
hkc = SimpleNamespace(available=True, client=_C())
|
|
198
185
|
reg = SkillRegistry()
|
|
199
186
|
ids = register_abilities_as_skills(reg, hkc)
|
|
200
|
-
# 证据 gate:只注册达标的 writing_expert;覆盖不足的 quant_analyst 不注册
|
|
201
187
|
assert ids == ["ability:writing_expert"], ids
|
|
202
188
|
assert reg.get("ability:quant_analyst") is None, "覆盖不足→不注册(证据 gate)"
|
|
203
189
|
spec = reg.get("ability:writing_expert")
|
|
204
190
|
assert spec is not None and spec.side_effect == "read", "能力 skill 为 read(无外部副作用)"
|
|
205
191
|
|
|
206
|
-
# 经 SkillAdapter(→PolicyGate 四阶段)调用:read → 放行 → handler 返回 domain 内知识
|
|
207
192
|
res = SkillAdapter(reg).invoke(spec, {"query": "写作风格"})
|
|
208
193
|
assert res.success and res.output.get("ability") == "writing_expert", res.to_dict()
|
|
209
194
|
assert res.output["knowledge"] and res.output["knowledge"][0]["name"] == "修辞"
|
|
210
195
|
print(" ✓ 证据 gate:达标能力注册为 read skill、覆盖不足不注册;经 PolicyGate 放行返回知识")
|
|
211
196
|
|
|
212
|
-
# 降级:HKC 不可用 / registry 缺失 → 不注册、不崩
|
|
213
197
|
assert register_abilities_as_skills(SkillRegistry(), None) == []
|
|
214
198
|
assert register_abilities_as_skills(None, hkc) == []
|
|
215
199
|
print(" ✓ HKC 不可用/registry 缺失 → 优雅返回 []")
|
|
@@ -222,7 +206,6 @@ def test_ability_skill_prune_and_concurrency():
|
|
|
222
206
|
from foxclaw.knowledge import register_abilities_as_skills
|
|
223
207
|
from foxclaw.skills import SkillRegistry, SkillSpec
|
|
224
208
|
|
|
225
|
-
# (a) 陈旧清理:先达标注册 → 知识掉了不再达标 → re-sync 移除陈旧 ability skill
|
|
226
209
|
class _Cdrop:
|
|
227
210
|
def __init__(self): self.ok = True
|
|
228
211
|
def list_abilities(self): return ["writing_expert"]
|
|
@@ -237,12 +220,11 @@ def test_ability_skill_prune_and_concurrency():
|
|
|
237
220
|
reg = SkillRegistry()
|
|
238
221
|
assert register_abilities_as_skills(reg, hkc) == ["ability:writing_expert"]
|
|
239
222
|
assert reg.get("ability:writing_expert") is not None
|
|
240
|
-
c.ok = False
|
|
223
|
+
c.ok = False
|
|
241
224
|
assert register_abilities_as_skills(reg, hkc) == []
|
|
242
225
|
assert reg.get("ability:writing_expert") is None, "陈旧 ability skill 应在 re-sync 时被移除"
|
|
243
226
|
print(" ✓ re-sync:覆盖掉了的 ability skill 被清理(不残留)")
|
|
244
227
|
|
|
245
|
-
# (b) 线程安全:并发 register + match/all/catalog 无 "dict changed size" 异常
|
|
246
228
|
reg2 = SkillRegistry()
|
|
247
229
|
errs = []
|
|
248
230
|
def reg_worker():
|
|
@@ -24,7 +24,6 @@ import time
|
|
|
24
24
|
from pathlib import Path
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
# ── vendor 隔离硬阻断 ────────────────────────────────────────────
|
|
28
27
|
from hkc_search.index_build_manager import IndexBuildManager, _BUILD_STATES
|
|
29
28
|
from hkc_search.vector_search import VectorIndex
|
|
30
29
|
from hkc_search.embedding_backends import StubBackend
|
|
@@ -55,8 +54,6 @@ for _cls_name, _cls in [
|
|
|
55
54
|
_assert_vendor_path(_cls, _cls_name)
|
|
56
55
|
|
|
57
56
|
|
|
58
|
-
# ── Helpers ─────────────────────────────────────────────────────
|
|
59
|
-
|
|
60
57
|
def _seed_kus(c: HKCContainer, ids: list[str]) -> None:
|
|
61
58
|
"""直接 put 进 store,不发出 KNOWLEDGE_CREATED 事件。
|
|
62
59
|
用于构造 '库有数据但 vector 没条' 的初始状态,触发 manager 全量构建。
|
|
@@ -94,16 +91,12 @@ def _seed_and_start_build(c: HKCContainer, ids: list[str]) -> None:
|
|
|
94
91
|
"""
|
|
95
92
|
_seed_kus(c, ids)
|
|
96
93
|
r = c.index_build_manager.start()
|
|
97
|
-
# size>0 不应返 None(已加载的 faiss 有数据但库无相同数据时可能返 None;此测试场景是库空)
|
|
98
|
-
# 这里 size=0 + source>0 → 必起构建 → start 返 task_id
|
|
99
94
|
assert r is not None, (
|
|
100
95
|
f"_seed_and_start_build 应触发新构建,但 start() 返 None。"
|
|
101
96
|
f"当前 stats: {c.index_build_manager.stats()}"
|
|
102
97
|
)
|
|
103
98
|
|
|
104
99
|
|
|
105
|
-
# ── S1:无索引启动,manager 后台跑全量构建 ───────────────────
|
|
106
|
-
|
|
107
100
|
class TestStartNoIndexManagerBuildsBackground:
|
|
108
101
|
def test_assemble_returns_fast_manager_builds_then_ready(self, tmp_path):
|
|
109
102
|
"""库有 5 KU 但磁盘无索引 → seed + start → manager 后台构建;
|
|
@@ -121,7 +114,6 @@ class TestStartNoIndexManagerBuildsBackground:
|
|
|
121
114
|
assert final["source_total"] == 5, final
|
|
122
115
|
assert final["ready"] == 5, final
|
|
123
116
|
assert final["failed"] == 0, final
|
|
124
|
-
# 内存索引真存在
|
|
125
117
|
assert c.search.vector.size() == 5
|
|
126
118
|
finally:
|
|
127
119
|
c.close()
|
|
@@ -133,7 +125,6 @@ class TestStartNoIndexManagerBuildsBackground:
|
|
|
133
125
|
data_dir = str(tmp_path)
|
|
134
126
|
c = init_container(data_dir=data_dir, embedding_kind="stub")
|
|
135
127
|
try:
|
|
136
|
-
# 装配已经返回(库空,manager 立即 idle)。seed + 重 start 让它编码。
|
|
137
128
|
_seed_and_start_build(c, [f"KU_FAST_{i}" for i in range(2)])
|
|
138
129
|
s = c.index_build_manager.stats()
|
|
139
130
|
assert s["index_state"] in _BUILD_STATES
|
|
@@ -145,14 +136,11 @@ class TestStartNoIndexManagerBuildsBackground:
|
|
|
145
136
|
reset_container()
|
|
146
137
|
|
|
147
138
|
|
|
148
|
-
# ── S2:旧快照启动(磁盘 faiss 已完整) → 不重起构建 ──────────
|
|
149
|
-
|
|
150
139
|
class TestStartWithPersistedSnapshotNoRebuild:
|
|
151
140
|
def test_manager_skips_when_disk_snapshot_covers_source(self, tmp_path):
|
|
152
141
|
"""先启动一次让 manager 构建完 → close;重启 → manager 看到 faiss 已非空
|
|
153
142
|
→ start() 返 None,不再构建。"""
|
|
154
143
|
data_dir = str(tmp_path)
|
|
155
|
-
# 第一轮:seed + start → manager 构建 → 落盘
|
|
156
144
|
c = init_container(data_dir=data_dir, embedding_kind="stub")
|
|
157
145
|
try:
|
|
158
146
|
_seed_and_start_build(c, [f"KU_S2A_{i}" for i in range(4)])
|
|
@@ -162,7 +150,6 @@ class TestStartWithPersistedSnapshotNoRebuild:
|
|
|
162
150
|
c.close()
|
|
163
151
|
reset_container()
|
|
164
152
|
|
|
165
|
-
# 第二轮:simulate 重启 —— 装配后,manager.start() 应返 None (已有快照)
|
|
166
153
|
c2 = init_container(data_dir=data_dir, embedding_kind="stub")
|
|
167
154
|
try:
|
|
168
155
|
s = c2.index_build_manager.stats()
|
|
@@ -172,7 +159,6 @@ class TestStartWithPersistedSnapshotNoRebuild:
|
|
|
172
159
|
assert s["vector_size"] == 4, (
|
|
173
160
|
f"vector_size 应从磁盘恢复,实际 {s}"
|
|
174
161
|
)
|
|
175
|
-
# 显式再调 start() 也返 None(幂等)
|
|
176
162
|
r = c2.index_build_manager.start()
|
|
177
163
|
assert r is None, (
|
|
178
164
|
f"已有快照时 start() 应返 None,实际 {r!r}"
|
|
@@ -182,8 +168,6 @@ class TestStartWithPersistedSnapshotNoRebuild:
|
|
|
182
168
|
reset_container()
|
|
183
169
|
|
|
184
170
|
|
|
185
|
-
# ── S3:构建中重启 → 续跑 ───────────────────────────────
|
|
186
|
-
|
|
187
171
|
class TestMidBuildRestartPicksUp:
|
|
188
172
|
def test_resume_after_disk_vector_partial(self, tmp_path):
|
|
189
173
|
"""模拟 '上次构建到一半被杀掉':磁盘 faiss 有 5 条全量,manager 看到库空
|
|
@@ -192,7 +176,6 @@ class TestMidBuildRestartPicksUp:
|
|
|
192
176
|
但因为 manager 的 start() 总是基于「当前 vector vs 当前库」做决策,
|
|
193
177
|
磁盘有 5 条 + 库 5 条相同 → 跳过(等价于 S2 的 happy path)。"""
|
|
194
178
|
data_dir = str(tmp_path)
|
|
195
|
-
# 第一轮:build 5 条 → save → close
|
|
196
179
|
c = init_container(data_dir=data_dir, embedding_kind="stub")
|
|
197
180
|
try:
|
|
198
181
|
_seed_and_start_build(c, [f"KU_S3_{i}" for i in range(5)])
|
|
@@ -202,16 +185,12 @@ class TestMidBuildRestartPicksUp:
|
|
|
202
185
|
c.close()
|
|
203
186
|
reset_container()
|
|
204
187
|
|
|
205
|
-
# 模拟"上次只跑了部分,faiss 是部分加载的" — 删 vector 文件(变 0 条)
|
|
206
|
-
# manager 装配时会 _load_persisted_index_and_bm25 → load 失败(因为已被我们删)→ 0 条
|
|
207
|
-
# 然后 manager.start() 看到 size=0 < source=5 → 起新构建
|
|
208
188
|
vec_path = tmp_path / "search_vector"
|
|
209
189
|
if (vec_path.with_suffix(".faiss")).exists():
|
|
210
190
|
(vec_path.with_suffix(".faiss")).unlink()
|
|
211
191
|
if (vec_path.with_suffix(".meta")).exists():
|
|
212
192
|
(vec_path.with_suffix(".meta")).unlink()
|
|
213
193
|
|
|
214
|
-
# 写 state.json 模拟"上次未完成",确认 manager.start 决策时能读到历史
|
|
215
194
|
import json as _json
|
|
216
195
|
state = {
|
|
217
196
|
"task_id": "build-deadbeef",
|
|
@@ -232,10 +211,8 @@ class TestMidBuildRestartPicksUp:
|
|
|
232
211
|
encoding="utf-8",
|
|
233
212
|
)
|
|
234
213
|
|
|
235
|
-
# 第二轮:重启
|
|
236
214
|
c2 = init_container(data_dir=data_dir, embedding_kind="stub")
|
|
237
215
|
try:
|
|
238
|
-
# 装配已自动 start → 应启构建(因为 faiss 已删)
|
|
239
216
|
final = _wait_idle(c2, timeout=30)
|
|
240
217
|
assert final["index_state"] == "ready", (
|
|
241
218
|
f"重启后应到 ready,实际 {final}"
|
|
@@ -243,7 +220,6 @@ class TestMidBuildRestartPicksUp:
|
|
|
243
220
|
assert c2.search.vector.size() == 5, (
|
|
244
221
|
f"重启后 vector_size 应=5,实际 {c2.search.vector.size()}"
|
|
245
222
|
)
|
|
246
|
-
# 跑出了新 task_id(不是历史的 deadbeef)
|
|
247
223
|
assert final["task_id"] != "build-deadbeef", (
|
|
248
224
|
f"应起新 task_id,实际仍用历史的 {final['task_id']!r}"
|
|
249
225
|
)
|
|
@@ -252,8 +228,6 @@ class TestMidBuildRestartPicksUp:
|
|
|
252
228
|
reset_container()
|
|
253
229
|
|
|
254
230
|
|
|
255
|
-
# ── S4:构建中查询 ──────────────────────────────────────────
|
|
256
|
-
|
|
257
231
|
class TestSearchDuringBuild:
|
|
258
232
|
def test_concurrent_search_does_not_crash(self, tmp_path):
|
|
259
233
|
"""manager 后台跑编码的同时,前调 c.search.search() → 不抛错;
|
|
@@ -262,13 +236,11 @@ class TestSearchDuringBuild:
|
|
|
262
236
|
c = init_container(data_dir=data_dir, embedding_kind="stub")
|
|
263
237
|
try:
|
|
264
238
|
_seed_and_start_build(c, [f"KU_S4_{i}" for i in range(6)])
|
|
265
|
-
# 不等;立即并发调 search
|
|
266
239
|
results = c.search.search("ku_1 描述", mode="hybrid", top_k=3)
|
|
267
240
|
assert isinstance(results, list)
|
|
268
241
|
for r in results:
|
|
269
242
|
assert hasattr(r, "ku_id")
|
|
270
243
|
assert hasattr(r, "score")
|
|
271
|
-
# 等构建完再做一次 search 确认 6 条都被 indexed
|
|
272
244
|
final = _wait_idle(c, timeout=30)
|
|
273
245
|
assert final["index_state"] == "ready"
|
|
274
246
|
assert c.search.vector.size() == 6
|
|
@@ -279,8 +251,6 @@ class TestSearchDuringBuild:
|
|
|
279
251
|
reset_container()
|
|
280
252
|
|
|
281
253
|
|
|
282
|
-
# ── 单元测:IndexBuildManager 状态机字段 ─────────────────
|
|
283
|
-
|
|
284
254
|
class TestManagerStats:
|
|
285
255
|
def test_stats_keys_present(self, tmp_path):
|
|
286
256
|
"""stats 字典字段稳定性(对外观测前端要靠这些 key)。"""
|
|
@@ -307,14 +277,11 @@ class TestManagerStats:
|
|
|
307
277
|
)
|
|
308
278
|
mgr.start()
|
|
309
279
|
mgr.join(timeout=5)
|
|
310
|
-
# 空 store 扫描到 0 条 → 直接 _mark_ready
|
|
311
280
|
s = mgr.stats()
|
|
312
281
|
assert s["index_state"] in ("ready", "cancelled", "failed"), s
|
|
313
282
|
assert s["source_total"] == 0, s
|
|
314
283
|
|
|
315
284
|
|
|
316
|
-
# ── Fakes(单元测用) ─────────────────────────────────────
|
|
317
|
-
|
|
318
285
|
class _FakeHybrid:
|
|
319
286
|
"""空 hybrid:vector_size=0,_ku_ids 空列表,_lock 是 RLock,vector 假实例。"""
|
|
320
287
|
def __init__(self):
|
|
@@ -35,7 +35,6 @@ import faiss
|
|
|
35
35
|
import numpy as np
|
|
36
36
|
import pytest
|
|
37
37
|
|
|
38
|
-
# ── vendor 隔离硬阻断(等价于 test_vendor_path_isolation.py 闸门 2-4) ────────
|
|
39
38
|
from hkc_search.vector_search import (
|
|
40
39
|
VectorIndex,
|
|
41
40
|
_MANIFEST_FORMAT_VERSION,
|
|
@@ -68,8 +67,6 @@ for _cls_name, _cls in [
|
|
|
68
67
|
_assert_vendor_path(_cls, _cls_name)
|
|
69
68
|
|
|
70
69
|
|
|
71
|
-
# ── Fixtures ────────────────────────────────────────────────────────
|
|
72
|
-
|
|
73
70
|
@pytest.fixture
|
|
74
71
|
def stub_vector(tmp_path) -> VectorIndex:
|
|
75
72
|
"""已 build 3 条 KU 的 vector,save 后留磁盘文件。"""
|
|
@@ -96,23 +93,18 @@ def _ku_text(ku) -> str:
|
|
|
96
93
|
return " ".join(parts)
|
|
97
94
|
|
|
98
95
|
|
|
99
|
-
# ── M1: roundtrip(全过) ───────────────────────────────────────────
|
|
100
|
-
|
|
101
96
|
class TestManifestRoundtrip:
|
|
102
97
|
def test_save_load_roundtrip(self, tmp_path, stub_vector):
|
|
103
98
|
"""save → load 完整,数据一致(ku_ids/names/summaries/text_hashes 全部保留)。"""
|
|
104
99
|
vec_path = str(tmp_path / "search_vector")
|
|
105
100
|
stub_vector.save(vec_path)
|
|
106
|
-
# 落盘了 v2 manifest
|
|
107
101
|
meta = pickle.loads((Path(vec_path + ".meta")).read_bytes())
|
|
108
102
|
assert meta["format_version"] == _MANIFEST_FORMAT_VERSION == 2
|
|
109
103
|
assert meta["ku_text_version"] == _KU_TEXT_FORMAT_VERSION == 1
|
|
110
|
-
# backend 字段以"Stub("开头(StubBackend.name 形如 "Stub(dim=384)")
|
|
111
104
|
assert meta["backend"].startswith("Stub("), meta["backend"]
|
|
112
105
|
assert meta["dim"] == stub_vector.dim
|
|
113
106
|
assert len(meta["ku_ids"]) == 3
|
|
114
107
|
assert len(meta["text_hashes"]) == 3
|
|
115
|
-
# 加载
|
|
116
108
|
fresh = VectorIndex(backend=StubBackend())
|
|
117
109
|
assert fresh.load(vec_path) is True
|
|
118
110
|
assert fresh.size() == 3
|
|
@@ -138,13 +130,11 @@ class TestManifestRoundtrip:
|
|
|
138
130
|
"""同文本 → 同 hash(阶段 B 跨进程比对、阶段 C 续跑检测都要靠这个稳定性)。"""
|
|
139
131
|
a = _hash_text("alpha 一种希腊字母")
|
|
140
132
|
b = _hash_text("alpha 一种希腊字母")
|
|
141
|
-
c = _hash_text("alpha 一种希腊字母 ")
|
|
133
|
+
c = _hash_text("alpha 一种希腊字母 ")
|
|
142
134
|
assert a == b
|
|
143
135
|
assert a != c
|
|
144
136
|
|
|
145
137
|
|
|
146
|
-
# ── M2~M9: 8 步校验各失败路径 ───────────────────────────────────
|
|
147
|
-
|
|
148
138
|
class TestManifestValidation:
|
|
149
139
|
def _fresh_v2_disk(self, tmp_path) -> str:
|
|
150
140
|
"""构造一份合法的 v2 磁盘,返回 prefix path。"""
|
|
@@ -159,14 +149,11 @@ class TestManifestValidation:
|
|
|
159
149
|
def test_v1_manifest_no_format_version_resets(self, tmp_path):
|
|
160
150
|
"""M2:旧 v1 manifest(无 format_version 字段)→ _reset。"""
|
|
161
151
|
vec_path = self._fresh_v2_disk(tmp_path)
|
|
162
|
-
# 改写 meta 为 v1:仅 ku_ids/names/summaries/backend,无 format_version/dim/text_hashes
|
|
163
152
|
with open(vec_path + ".meta", "wb") as f:
|
|
164
153
|
pickle.dump({"ku_ids": ["X"], "names": ["x"],
|
|
165
154
|
"summaries": ["x"], "backend": "stub"}, f)
|
|
166
155
|
v = VectorIndex(backend=StubBackend())
|
|
167
|
-
# 加载必失败 → 返 False
|
|
168
156
|
assert v.load(vec_path) is False
|
|
169
|
-
# _reset 已清空
|
|
170
157
|
assert v.size() == 0
|
|
171
158
|
assert v._ku_ids == []
|
|
172
159
|
assert v._text_hashes == []
|
|
@@ -224,7 +211,7 @@ class TestManifestValidation:
|
|
|
224
211
|
vec_path = self._fresh_v2_disk(tmp_path)
|
|
225
212
|
with open(vec_path + ".meta", "rb") as f:
|
|
226
213
|
meta = pickle.load(f)
|
|
227
|
-
meta["text_hashes"] = meta["text_hashes"][:1]
|
|
214
|
+
meta["text_hashes"] = meta["text_hashes"][:1]
|
|
228
215
|
with open(vec_path + ".meta", "wb") as f:
|
|
229
216
|
pickle.dump(meta, f)
|
|
230
217
|
v = VectorIndex(backend=StubBackend())
|
|
@@ -233,8 +220,7 @@ class TestManifestValidation:
|
|
|
233
220
|
def test_faiss_dim_mismatch_resets(self, tmp_path):
|
|
234
221
|
"""M8:faiss 实际维度与 meta["dim"] 不一致(切后端后常见)→ 校验失败。"""
|
|
235
222
|
vec_path = self._fresh_v2_disk(tmp_path)
|
|
236
|
-
|
|
237
|
-
wrong_idx = faiss.IndexFlatIP(999) # 错维度
|
|
223
|
+
wrong_idx = faiss.IndexFlatIP(999)
|
|
238
224
|
wrong_idx.add(np.random.rand(2, 999).astype("float32"))
|
|
239
225
|
faiss.write_index(wrong_idx, vec_path + ".faiss")
|
|
240
226
|
v = VectorIndex(backend=StubBackend())
|
|
@@ -244,7 +230,6 @@ class TestManifestValidation:
|
|
|
244
230
|
def test_faiss_ntotal_mismatch_resets(self, tmp_path):
|
|
245
231
|
"""M9:faiss 实际 ntotal 与 meta len(ku_ids) 不一致 → 校验失败。"""
|
|
246
232
|
vec_path = self._fresh_v2_disk(tmp_path)
|
|
247
|
-
# 重建一个 ntotal=99 的 faiss(其他字段不动)
|
|
248
233
|
wrong_idx = faiss.IndexFlatIP(stub_vector_dim := VectorIndex(backend=StubBackend()).dim)
|
|
249
234
|
wrong_idx.add(np.random.rand(99, stub_vector_dim).astype("float32"))
|
|
250
235
|
faiss.write_index(wrong_idx, vec_path + ".faiss")
|
|
@@ -266,8 +251,7 @@ class TestManifestValidation:
|
|
|
266
251
|
v = VectorIndex(backend=StubBackend())
|
|
267
252
|
kus = [ConceptKU(ku_id="KU_A", name="a", summary="a", domain="d")]
|
|
268
253
|
v.build(kus)
|
|
269
|
-
|
|
270
|
-
v._text_hashes = v._text_hashes[:0] # 现在 _text_hashes=[] 但 _ku_ids=[KU_A]
|
|
254
|
+
v._text_hashes = v._text_hashes[:0]
|
|
271
255
|
with pytest.raises(AssertionError):
|
|
272
256
|
v.save(str(tmp_path / "search_vector"))
|
|
273
257
|
|
|
@@ -280,12 +264,6 @@ class TestManifestValidation:
|
|
|
280
264
|
assert "foo msg" in str(e)
|
|
281
265
|
|
|
282
266
|
|
|
283
|
-
# ── B1~B4: bm25 0 corpus 保护 ────────────────────────────────────
|
|
284
|
-
# B 阶段把 BM25Index 重写为自实现倒排索引(不再包装 rank_bm25),旧私有属性
|
|
285
|
-
# `_bm25` / `_ku_ids` / `_names` / `_summaries` / `_corpus` / `_rebuild_bm25`
|
|
286
|
-
# 均已不存在。故断言改为验证**对外行为**(size / search),不再耦合私有结构 ——
|
|
287
|
-
# 旧测试直接拼私有 list 再调私有 rebuild,是它在此次重构后失效的根因。
|
|
288
|
-
|
|
289
267
|
class TestBM25EmptyCorpus:
|
|
290
268
|
def test_build_empty_list(self):
|
|
291
269
|
"""B1:build([]) → 索引为空,search 返 []。"""
|
|
@@ -313,5 +291,4 @@ class TestBM25EmptyCorpus:
|
|
|
313
291
|
def test_search_with_no_bm25(self):
|
|
314
292
|
"""B4:未 build 时 search 返 []。"""
|
|
315
293
|
b = BM25Index()
|
|
316
|
-
# 不 build,直接 search
|
|
317
294
|
assert b.search("anything") == []
|