virtual-context 0.4.2__tar.gz → 0.4.4__tar.gz
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.
- {virtual_context-0.4.2 → virtual_context-0.4.4}/.gitignore +4 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/PKG-INFO +3 -1
- {virtual_context-0.4.2 → virtual_context-0.4.4}/pyproject.toml +3 -2
- virtual_context-0.4.4/tests/test_admin_backfill_cli.py +171 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_resume_e2e_postgres.py +5 -5
- virtual_context-0.4.4/tests/test_conversations_tenant_id_backfill_postgres.py +180 -0
- virtual_context-0.4.4/tests/test_conversations_tenant_id_propagation.py +186 -0
- virtual_context-0.4.4/tests/test_engine_alias_resolution.py +2232 -0
- virtual_context-0.4.4/tests/test_find_idle_deletable_conversations.py +223 -0
- virtual_context-0.4.4/tests/test_import_adapters.py +236 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_postgres_store.py +56 -26
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_proxy.py +3 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_proxy_session.py +98 -9
- virtual_context-0.4.4/tests/test_tag_summary_backfill.py +637 -0
- virtual_context-0.4.4/tests/test_vcattach.py +1321 -0
- virtual_context-0.4.4/tests/test_vcmerge_body_method.py +1795 -0
- virtual_context-0.4.4/tests/test_vcmerge_body_method_postgres.py +524 -0
- virtual_context-0.4.4/tests/test_vcmerge_composite_forwarders.py +433 -0
- virtual_context-0.4.4/tests/test_vcmerge_dual_handler_refuse.py +363 -0
- virtual_context-0.4.4/tests/test_vcmerge_schema.py +397 -0
- virtual_context-0.4.4/tests/test_vcmerge_schema_postgres.py +163 -0
- virtual_context-0.4.4/tests/test_vcmerge_storage.py +379 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/__init__.py +1 -1
- virtual_context-0.4.4/virtual_context/cli/import_cmd.py +75 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/cli/main.py +201 -3
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/config.py +5 -0
- virtual_context-0.4.4/virtual_context/core/alias_resolution.py +162 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/compaction_pipeline.py +186 -103
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/composite_store.py +224 -8
- virtual_context-0.4.4/virtual_context/core/exceptions.py +180 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/protocols.py +15 -2
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/store.py +37 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/tagging_pipeline.py +2 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/engine.py +673 -16
- virtual_context-0.4.4/virtual_context/import_adapters/__init__.py +32 -0
- virtual_context-0.4.4/virtual_context/import_adapters/base.py +48 -0
- virtual_context-0.4.4/virtual_context/import_adapters/chatgpt.py +63 -0
- virtual_context-0.4.4/virtual_context/import_adapters/claude.py +47 -0
- virtual_context-0.4.4/virtual_context/import_adapters/grok.py +61 -0
- virtual_context-0.4.4/virtual_context/import_adapters/loader.py +65 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/handlers.py +395 -25
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/server.py +125 -27
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/state.py +47 -26
- virtual_context-0.4.4/virtual_context/proxy/vcattach.py +203 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/storage/filesystem.py +13 -0
- virtual_context-0.4.4/virtual_context/storage/postgres.py +6864 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/storage/sqlite.py +1354 -60
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/types.py +179 -6
- virtual_context-0.4.2/tests/test_vcattach.py +0 -413
- virtual_context-0.4.2/virtual_context/proxy/vcattach.py +0 -108
- virtual_context-0.4.2/virtual_context/storage/postgres.py +0 -5302
- {virtual_context-0.4.2 → virtual_context-0.4.4}/LICENSE +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/README.md +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/assets/dashboard.png +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/assets/hero.png +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/models.yaml +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/scripts/install.ps1 +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/scripts/install.sh +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/REGRESSION_MAP.md +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/conftest.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/docker-compose.test.yml +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/haiku/__init__.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/haiku/conftest.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/haiku/test_compaction.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/haiku/test_retrieval.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/haiku/test_tagging.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/ollama/__init__.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/ollama/conftest.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/ollama/test_compactor.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/ollama/test_pipeline.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/ollama/test_provider.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/ollama/test_tag_generator.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/proxy/__init__.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/proxy/test_dashboard_cors.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/proxy/test_metrics.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_active_compaction_op_invariants.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_assembler.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_backend_integration.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_beam_judge.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_budget_enforcement.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_canonical_row_epoch_guards.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_canonical_row_epoch_guards_postgres.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_canonical_turn_grouping.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_canonical_turns_schema.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_canonical_turns_schema_postgres.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_claim_compaction_lease_signature.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_claim_compaction_lease_signature_postgres.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_cleanup_abandoned_compaction.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_cleanup_abandoned_compaction_postgres.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_cli_init.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compacting_phase_takeover.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_commit_prune.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_heartbeat_sidecar.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_lease_lost_abort.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_lifecycle.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_lifecycle_contract.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_lifecycle_wiring.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_operation.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_operation_crud.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_operation_crud_postgres.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_operation_id_uuid.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_operation_postgres.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_ownership_guards.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_progress_event.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_resume_schema_migration.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_resume_schema_migration_postgres.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compactor.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compactor_concurrent.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_composite_store.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_config.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_context_bleed.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_conversation_coverage_report.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_conversation_identity.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_conversation_lifecycle.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_conversation_schema.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_conversation_schema_postgres.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_conversation_scoping.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_deferred_payload_compaction.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_delete_resurrect_epoch_races.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_embedding_tag_generator.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_empty_turn_skip.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_engine_event_bus.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_engine_integration.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_engine_lifecycle_epoch.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_engine_lookback.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_engine_state.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_engine_sync_turns.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_fact_enrichment.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_fact_graph_integration.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_fact_link_checker.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_fact_link_query.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_fact_link_types.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_fact_links_sqlite.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_fact_redesign.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_filesystem_store_compaction_compat.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_fill_pass.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_find_quote.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_find_stale_lease_helpers.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_format_agnostic.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_handle_prepare_payload.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_headless.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_history_filter.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_idf_retrieval.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingest_index_integrity.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingest_reconciler_append_fastpath.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingest_reconciler_epoch.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingestion_compaction_full_cycle.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingestion_db_sweep_catchup.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingestion_episode.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingestion_episode_crud.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingestion_episode_crud_postgres.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingestion_episode_postgres.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingestion_finalize_self_heal.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingestion_progress_event.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingestion_regression_resume.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_legacy_ingestion_lease_ownership.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_lifecycle_epoch.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_lifecycle_epoch_store.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_lifecycle_epoch_store_postgres.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_longmemeval_auth.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_mark_canonical_turns_compacted_all_halves.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_mcp_server.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_media.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_message_filter.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_metrics_persistence.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_model_catalog.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_model_limits.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_monitor.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_multi_instance.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_multi_worker_sliding_window.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_neo4j_falkordb_compaction_compat.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_noop_fact_link_store.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_openrouter_provider.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_orphan_compacting_repair.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_paging.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_passthrough_filter.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_phase_transition_events.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_presets.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_prev_context_leak.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_progress_event_bus.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_progress_events.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_progress_snapshot.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_progress_snapshot_postgres.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_provider_adapters.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_proxy_dashboard.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_proxy_formats.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_proxy_message_filter.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_proxy_streaming.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_raw_content.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_recall_all.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_refresh_compaction_heartbeat.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_registry_lifecycle.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_request_captures_persistence.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_request_metadata.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_request_metadata_postgres.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_retriever.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_rrf_scoring.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_segmenter.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_semantic_search.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_sender_identity.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_session_cache.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_session_date.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_session_state.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_sqlite_schema_add_column_race.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_storage_protocols.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_store_recovery.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_store_sqlite.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_stub_turn_handling.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_supersession.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_supersession_migration.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_tag_canonicalizer.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_tag_consolidator.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_tag_generator.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_tag_splitter.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_tagger_loop.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_telemetry.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_telemetry_integration.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_temporal_resolver.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_tool_loop.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_tool_output_interceptor.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_tool_result_filter.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_tool_tags.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_tui.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_turn_grouping.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_turn_tag_index.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_unified_budget.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_upstream_trim.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_verb_expansion.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual-context.yaml.example +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/cli/__init__.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/conversation_identity.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/__init__.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/assembler.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/canonical_turns.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/compactor.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/conversation_store.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/coverage_report.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/embedding_provider.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/embedding_tag_generator.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/engine_utils.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/event_bus.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/fact_query.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/fts_preprocessor.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/hint_builder.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/ingest_reconciler.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/lifecycle_epoch.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/llm_utils.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/math_utils.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/model_catalog.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/monitor.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/paging_manager.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/progress_events.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/progress_snapshot.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/provider_adapters.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/quote_search.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/retrieval_assembler.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/retrieval_scoring.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/retriever.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/search_engine.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/segmenter.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/semantic_search.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/tag_canonicalizer.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/tag_consolidator.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/tag_generator.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/tag_scoring.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/tag_splitter.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/telemetry.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/temporal_resolver.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/tool_loop.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/tool_query.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/turn_tag_index.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/data/anthropic-tokenizer/tokenizer.json +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/data/models.yaml +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/ingest/__init__.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/ingest/curator.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/ingest/date_resolver.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/ingest/parsers.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/ingest/supersession.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/mcp/__init__.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/mcp/server.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/model_limits.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/openclaw/virtual-context.mjs +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/patterns.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/presets/__init__.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/presets/agentic.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/presets/base.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/presets/coding.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/providers/__init__.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/providers/anthropic.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/providers/base.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/providers/generic_openai.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/providers/ollama_native.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/__init__.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/_envelope.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/dashboard.html +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/dashboard.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/formats.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/helpers.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/media.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/message_filter.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/metrics.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/multi.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/registry.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/session_cache.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/session_state.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/static/android-chrome-192x192.png +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/static/android-chrome-512x512.png +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/static/apple-touch-icon.png +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/static/favicon-16x16.png +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/static/favicon-32x32.png +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/static/favicon.ico +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/static/site.webmanifest +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/tool_output_interceptor.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/storage/__init__.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/storage/falkordb.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/storage/helpers.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/storage/neo4j.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/storage/noop_fact_link_store.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/token_counter.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/__init__.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/app.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/chat.tcss +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/chat_provider.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/headless.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/modals/__init__.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/modals/turn_inspector.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/state.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/widgets/__init__.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/widgets/budget_bar.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/widgets/chat_view.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/widgets/input_box.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/widgets/tag_panel.py +0 -0
- {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/widgets/turn_list.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: virtual-context
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: OS-style virtual memory for LLM session context management
|
|
5
5
|
Project-URL: Homepage, https://virtual-context.com
|
|
6
6
|
Project-URL: Repository, https://github.com/virtual-context/virtual-context
|
|
@@ -28,6 +28,7 @@ Requires-Dist: tiktoken>=0.7
|
|
|
28
28
|
Requires-Dist: torch>=2.0
|
|
29
29
|
Requires-Dist: uvicorn>=0.30
|
|
30
30
|
Provides-Extra: all
|
|
31
|
+
Requires-Dist: psycopg-pool>=3.1; extra == 'all'
|
|
31
32
|
Requires-Dist: psycopg[binary]>=3.1; extra == 'all'
|
|
32
33
|
Requires-Dist: redis>=5.0; extra == 'all'
|
|
33
34
|
Provides-Extra: dev
|
|
@@ -40,6 +41,7 @@ Requires-Dist: falkordb>=1.0; extra == 'falkordb'
|
|
|
40
41
|
Provides-Extra: neo4j
|
|
41
42
|
Requires-Dist: neo4j>=5.0; extra == 'neo4j'
|
|
42
43
|
Provides-Extra: postgres
|
|
44
|
+
Requires-Dist: psycopg-pool>=3.1; extra == 'postgres'
|
|
43
45
|
Requires-Dist: psycopg[binary]>=3.1; extra == 'postgres'
|
|
44
46
|
Provides-Extra: redis
|
|
45
47
|
Requires-Dist: redis>=5.0; extra == 'redis'
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "virtual-context"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.4"
|
|
8
8
|
description = "OS-style virtual memory for LLM session context management"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "AGPL-3.0-or-later"
|
|
@@ -34,12 +34,13 @@ dependencies = [
|
|
|
34
34
|
]
|
|
35
35
|
|
|
36
36
|
[project.optional-dependencies]
|
|
37
|
-
postgres = ["psycopg[binary]>=3.1"]
|
|
37
|
+
postgres = ["psycopg[binary]>=3.1", "psycopg-pool>=3.1"]
|
|
38
38
|
neo4j = ["neo4j>=5.0"]
|
|
39
39
|
falkordb = ["falkordb>=1.0"]
|
|
40
40
|
redis = ["redis>=5.0"]
|
|
41
41
|
all = [
|
|
42
42
|
"psycopg[binary]>=3.1",
|
|
43
|
+
"psycopg-pool>=3.1",
|
|
43
44
|
"redis>=5.0",
|
|
44
45
|
]
|
|
45
46
|
dev = [
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Tests for the ``admin backfill-tag-summaries`` CLI subcommand.
|
|
2
|
+
|
|
3
|
+
Covers the storage-override flags + ``DATABASE_URL`` env fallback
|
|
4
|
+
introduced so the subcommand runs inside containers that don't have a
|
|
5
|
+
config file mounted at a standard path.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE")
|
|
13
|
+
|
|
14
|
+
from types import SimpleNamespace
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
from virtual_context.cli.main import _apply_storage_overrides
|
|
19
|
+
from virtual_context.types import StorageConfig, VirtualContextConfig
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _make_config(*, backend="sqlite", postgres_dsn="", sqlite_path=".db"):
|
|
23
|
+
return VirtualContextConfig(
|
|
24
|
+
storage=StorageConfig(
|
|
25
|
+
backend=backend,
|
|
26
|
+
postgres_dsn=postgres_dsn,
|
|
27
|
+
sqlite_path=sqlite_path,
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_storage_override_explicit_flag_wins(monkeypatch):
|
|
33
|
+
"""Explicit ``--postgres-dsn`` overrides whatever config carried."""
|
|
34
|
+
monkeypatch.delenv("DATABASE_URL", raising=False)
|
|
35
|
+
config = _make_config(backend="sqlite", postgres_dsn="from-config")
|
|
36
|
+
args = SimpleNamespace(
|
|
37
|
+
config=None,
|
|
38
|
+
storage_backend="postgres",
|
|
39
|
+
postgres_dsn="postgresql://override/db",
|
|
40
|
+
sqlite_path=None,
|
|
41
|
+
)
|
|
42
|
+
_apply_storage_overrides(config, args)
|
|
43
|
+
assert config.storage.backend == "postgres"
|
|
44
|
+
assert config.storage.postgres_dsn == "postgresql://override/db"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_storage_override_sqlite_path_flag(monkeypatch):
|
|
48
|
+
"""``--sqlite-path`` overrides ``storage.sqlite_path``."""
|
|
49
|
+
monkeypatch.delenv("DATABASE_URL", raising=False)
|
|
50
|
+
config = _make_config(backend="sqlite", sqlite_path=".db")
|
|
51
|
+
args = SimpleNamespace(
|
|
52
|
+
config=None,
|
|
53
|
+
storage_backend=None,
|
|
54
|
+
postgres_dsn=None,
|
|
55
|
+
sqlite_path="/tmp/override.db",
|
|
56
|
+
)
|
|
57
|
+
_apply_storage_overrides(config, args)
|
|
58
|
+
assert config.storage.sqlite_path == "/tmp/override.db"
|
|
59
|
+
# Backend unchanged because no --storage-backend flag.
|
|
60
|
+
assert config.storage.backend == "sqlite"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_storage_override_database_url_env_fallback(monkeypatch):
|
|
64
|
+
"""When no flag and no ``-c`` are given, ``DATABASE_URL`` env
|
|
65
|
+
promotes the engine to Postgres with that DSN. Covers the
|
|
66
|
+
container-based invocation shape where ops just runs the
|
|
67
|
+
subcommand with the env already set by the container runtime."""
|
|
68
|
+
monkeypatch.setenv("DATABASE_URL", "postgresql://from-env/db")
|
|
69
|
+
config = _make_config(backend="sqlite", postgres_dsn="")
|
|
70
|
+
args = SimpleNamespace(
|
|
71
|
+
config=None,
|
|
72
|
+
storage_backend=None,
|
|
73
|
+
postgres_dsn=None,
|
|
74
|
+
sqlite_path=None,
|
|
75
|
+
)
|
|
76
|
+
_apply_storage_overrides(config, args)
|
|
77
|
+
assert config.storage.backend == "postgres"
|
|
78
|
+
assert config.storage.postgres_dsn == "postgresql://from-env/db"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_storage_override_database_url_skipped_when_config_provided(monkeypatch):
|
|
82
|
+
"""When the caller passes ``-c``, the env fallback DOES NOT fire;
|
|
83
|
+
the config file is treated as authoritative."""
|
|
84
|
+
monkeypatch.setenv("DATABASE_URL", "postgresql://from-env/db")
|
|
85
|
+
config = _make_config(backend="sqlite", postgres_dsn="")
|
|
86
|
+
args = SimpleNamespace(
|
|
87
|
+
config="/some/config.yaml",
|
|
88
|
+
storage_backend=None,
|
|
89
|
+
postgres_dsn=None,
|
|
90
|
+
sqlite_path=None,
|
|
91
|
+
)
|
|
92
|
+
_apply_storage_overrides(config, args)
|
|
93
|
+
# Backend + DSN unchanged because ``-c`` was given.
|
|
94
|
+
assert config.storage.backend == "sqlite"
|
|
95
|
+
assert config.storage.postgres_dsn == ""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_storage_override_database_url_skipped_when_any_flag_set(monkeypatch):
|
|
99
|
+
"""When ANY storage flag is set, the env fallback does NOT fire
|
|
100
|
+
(explicit flags are authoritative)."""
|
|
101
|
+
monkeypatch.setenv("DATABASE_URL", "postgresql://from-env/db")
|
|
102
|
+
config = _make_config(backend="sqlite")
|
|
103
|
+
args = SimpleNamespace(
|
|
104
|
+
config=None,
|
|
105
|
+
storage_backend=None,
|
|
106
|
+
postgres_dsn=None,
|
|
107
|
+
sqlite_path="/tmp/explicit.db",
|
|
108
|
+
)
|
|
109
|
+
_apply_storage_overrides(config, args)
|
|
110
|
+
# Only the sqlite_path flag applies; env fallback is skipped.
|
|
111
|
+
assert config.storage.sqlite_path == "/tmp/explicit.db"
|
|
112
|
+
assert config.storage.backend == "sqlite"
|
|
113
|
+
assert config.storage.postgres_dsn == ""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_storage_override_no_change_when_no_flags_and_no_env(monkeypatch):
|
|
117
|
+
"""Defaults intact when neither flags, ``-c``, nor ``DATABASE_URL``
|
|
118
|
+
are present. (``-c`` absent means ``load_config()`` already returned
|
|
119
|
+
defaults; this assertion guards against accidental mutation.)"""
|
|
120
|
+
monkeypatch.delenv("DATABASE_URL", raising=False)
|
|
121
|
+
config = _make_config(backend="sqlite", sqlite_path=".db")
|
|
122
|
+
args = SimpleNamespace(
|
|
123
|
+
config=None,
|
|
124
|
+
storage_backend=None,
|
|
125
|
+
postgres_dsn=None,
|
|
126
|
+
sqlite_path=None,
|
|
127
|
+
)
|
|
128
|
+
_apply_storage_overrides(config, args)
|
|
129
|
+
assert config.storage.backend == "sqlite"
|
|
130
|
+
assert config.storage.postgres_dsn == ""
|
|
131
|
+
assert config.storage.sqlite_path == ".db"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_cli_parser_accepts_new_storage_flags():
|
|
135
|
+
"""End-to-end parser smoke test: the ``admin backfill-tag-summaries``
|
|
136
|
+
subparser accepts the three new flags + ``DATABASE_URL`` precedence
|
|
137
|
+
metadata in the help text."""
|
|
138
|
+
from virtual_context.cli.main import main as _main
|
|
139
|
+
import argparse
|
|
140
|
+
|
|
141
|
+
# Re-build the parser the way ``main()`` does, then parse a sample
|
|
142
|
+
# invocation. (Doing a full ``main()`` call would actually run the
|
|
143
|
+
# backfill; we only need argument parsing here.)
|
|
144
|
+
parser = argparse.ArgumentParser(prog="virtual-context")
|
|
145
|
+
parser.add_argument("--config", "-c")
|
|
146
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
147
|
+
admin_parser = subparsers.add_parser("admin")
|
|
148
|
+
admin_sub = admin_parser.add_subparsers(dest="admin_command")
|
|
149
|
+
backfill_parser = admin_sub.add_parser("backfill-tag-summaries")
|
|
150
|
+
backfill_parser.add_argument("conversation_id")
|
|
151
|
+
backfill_parser.add_argument("--tenant-id", default="")
|
|
152
|
+
backfill_parser.add_argument("--force-rebuild", action="store_true")
|
|
153
|
+
backfill_parser.add_argument(
|
|
154
|
+
"--storage-backend",
|
|
155
|
+
choices=("sqlite", "postgres", "filesystem"),
|
|
156
|
+
)
|
|
157
|
+
backfill_parser.add_argument("--postgres-dsn")
|
|
158
|
+
backfill_parser.add_argument("--sqlite-path")
|
|
159
|
+
|
|
160
|
+
args = parser.parse_args([
|
|
161
|
+
"admin", "backfill-tag-summaries", "conv-xyz",
|
|
162
|
+
"--tenant-id", "t1",
|
|
163
|
+
"--storage-backend", "postgres",
|
|
164
|
+
"--postgres-dsn", "postgresql://h/db",
|
|
165
|
+
])
|
|
166
|
+
assert args.conversation_id == "conv-xyz"
|
|
167
|
+
assert args.tenant_id == "t1"
|
|
168
|
+
assert args.storage_backend == "postgres"
|
|
169
|
+
assert args.postgres_dsn == "postgresql://h/db"
|
|
170
|
+
assert args.sqlite_path is None
|
|
171
|
+
assert args.force_rebuild is False
|
{virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_resume_e2e_postgres.py
RENAMED
|
@@ -167,7 +167,7 @@ def _age_heartbeat(pg_store, op_id: str, age_s: float = 600) -> None:
|
|
|
167
167
|
def test_compaction_resume_e2e_postgres():
|
|
168
168
|
"""Full takeover flow against real Postgres.
|
|
169
169
|
|
|
170
|
-
Worker-death simulation: simple heartbeat back-dating (plan
|
|
170
|
+
Worker-death simulation: simple heartbeat back-dating (plan "go with
|
|
171
171
|
the simple approach"). No subprocess spawning required.
|
|
172
172
|
|
|
173
173
|
LLM stubbing: compact_if_needed is replaced with a MagicMock that
|
|
@@ -314,10 +314,10 @@ def test_compaction_resume_e2e_postgres():
|
|
|
314
314
|
)
|
|
315
315
|
|
|
316
316
|
# 7d. ALL canonical_turns have compacted_at NOT NULL
|
|
317
|
-
#
|
|
318
|
-
#
|
|
319
|
-
#
|
|
320
|
-
#
|
|
317
|
+
# and compaction_operation_id = new_op
|
|
318
|
+
# (compact_if_needed is mocked to return None so no mark_canonical
|
|
319
|
+
# call fires inside the pipeline — skip these assertions when
|
|
320
|
+
# the mock swallows them, but assert the zero-dead-op invariant)
|
|
321
321
|
dead_ct_count = conn.execute(
|
|
322
322
|
"""
|
|
323
323
|
SELECT COUNT(*) AS cnt FROM canonical_turns
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""PG smoke for conversations.tenant_id backfill from cloud_conversations.
|
|
2
|
+
|
|
3
|
+
Skipped when DATABASE_URL is absent (the standard project pattern at
|
|
4
|
+
tests/test_vcmerge_schema_postgres.py:28). Verifies the one-time
|
|
5
|
+
backfill UPDATE that ``PostgresStore._ensure_schema`` runs on bootstrap
|
|
6
|
+
correctly populates ``conversations.tenant_id`` from
|
|
7
|
+
``cloud_conversations.tenant_id`` for rows that existed before the
|
|
8
|
+
insertion-path fix landed.
|
|
9
|
+
|
|
10
|
+
Per prod blocker: 14 existing user convs in prod tenant
|
|
11
|
+
``43bd6d7f0f8d6798`` had ``conversations.tenant_id = ''`` (engine
|
|
12
|
+
predecessor passed empty placeholder); cloud_conversations was correctly
|
|
13
|
+
populated by TenantMiddleware. The migration needs to be idempotent so
|
|
14
|
+
re-running schema bootstrap is safe.
|
|
15
|
+
|
|
16
|
+
Marked @pytest.mark.regression("VCATTACH-DATALOSS-2026-04-26").
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import uuid
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
|
|
25
|
+
import pytest
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
pytestmark = [
|
|
29
|
+
pytest.mark.skipif(
|
|
30
|
+
not os.environ.get("DATABASE_URL"),
|
|
31
|
+
reason="DATABASE_URL not set: Postgres smoke skipped",
|
|
32
|
+
),
|
|
33
|
+
pytest.mark.regression("VCATTACH-DATALOSS-2026-04-26"),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
def pg_store():
|
|
39
|
+
"""Build a PostgresStore against DATABASE_URL. Cleanup teardown drops
|
|
40
|
+
test rows under the test tenant prefix."""
|
|
41
|
+
from virtual_context.storage.postgres import PostgresStore
|
|
42
|
+
dsn = os.environ["DATABASE_URL"]
|
|
43
|
+
store = PostgresStore(dsn)
|
|
44
|
+
yield store
|
|
45
|
+
try:
|
|
46
|
+
conn = store._get_conn()
|
|
47
|
+
conn.execute("DELETE FROM conversations WHERE tenant_id LIKE 'pgbf-%' "
|
|
48
|
+
"OR conversation_id LIKE 'pgbf-%'")
|
|
49
|
+
try:
|
|
50
|
+
conn.execute("DELETE FROM cloud_conversations WHERE tenant_id LIKE 'pgbf-%' "
|
|
51
|
+
"OR conversation_id LIKE 'pgbf-%'")
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _now():
|
|
59
|
+
return datetime.now(timezone.utc)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _ensure_cloud_conversations_table(conn):
|
|
63
|
+
"""If the test environment's PG doesn't have cloud_conversations
|
|
64
|
+
(pure-engine deploys), create a minimal version sufficient for the
|
|
65
|
+
backfill smoke. The real cloud schema is richer; this stub just has
|
|
66
|
+
the columns the backfill UPDATE references.
|
|
67
|
+
"""
|
|
68
|
+
conn.execute(
|
|
69
|
+
"""
|
|
70
|
+
CREATE TABLE IF NOT EXISTS cloud_conversations (
|
|
71
|
+
conversation_id TEXT NOT NULL,
|
|
72
|
+
tenant_id TEXT NOT NULL DEFAULT '',
|
|
73
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
74
|
+
)
|
|
75
|
+
"""
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_pg_backfill_populates_empty_tenant_id_from_cloud_conversations(pg_store):
|
|
80
|
+
""": simulate the prod state (conversations row with empty
|
|
81
|
+
tenant_id; matching cloud_conversations row with the right tenant_id).
|
|
82
|
+
Run schema bootstrap; assert backfill populated the empty cell.
|
|
83
|
+
"""
|
|
84
|
+
conn = pg_store._get_conn()
|
|
85
|
+
_ensure_cloud_conversations_table(conn)
|
|
86
|
+
|
|
87
|
+
tid = f"pgbf-{uuid.uuid4().hex[:8]}"
|
|
88
|
+
cid = f"pgbf-conv-{uuid.uuid4().hex[:8]}"
|
|
89
|
+
# Pre-fold prod state: empty tenant_id on conversations.
|
|
90
|
+
conn.execute(
|
|
91
|
+
"INSERT INTO conversations (conversation_id, tenant_id, phase, "
|
|
92
|
+
"created_at, updated_at) VALUES (%s, '', 'active', %s, %s)",
|
|
93
|
+
(cid, _now(), _now()),
|
|
94
|
+
)
|
|
95
|
+
# Cloud's wrapper has the right tenant_id.
|
|
96
|
+
conn.execute(
|
|
97
|
+
"INSERT INTO cloud_conversations (conversation_id, tenant_id) "
|
|
98
|
+
"VALUES (%s, %s)",
|
|
99
|
+
(cid, tid),
|
|
100
|
+
)
|
|
101
|
+
# Re-trigger schema bootstrap (which runs the backfill UPDATE).
|
|
102
|
+
pg_store._ensure_schema()
|
|
103
|
+
# Backfill should have populated the empty cell from cloud_conversations.
|
|
104
|
+
row = conn.execute(
|
|
105
|
+
"SELECT tenant_id FROM conversations WHERE conversation_id = %s",
|
|
106
|
+
(cid,),
|
|
107
|
+
).fetchone()
|
|
108
|
+
assert row["tenant_id"] == tid, (
|
|
109
|
+
f"Backfill should have set conversations.tenant_id to {tid!r}, "
|
|
110
|
+
f"got {row['tenant_id']!r}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_pg_backfill_is_idempotent(pg_store):
|
|
115
|
+
""": second bootstrap run is a no-op (the WHERE filter on
|
|
116
|
+
empty target makes the migration idempotent)."""
|
|
117
|
+
conn = pg_store._get_conn()
|
|
118
|
+
_ensure_cloud_conversations_table(conn)
|
|
119
|
+
|
|
120
|
+
tid = f"pgbf-{uuid.uuid4().hex[:8]}"
|
|
121
|
+
cid = f"pgbf-conv-{uuid.uuid4().hex[:8]}"
|
|
122
|
+
conn.execute(
|
|
123
|
+
"INSERT INTO conversations (conversation_id, tenant_id, phase, "
|
|
124
|
+
"created_at, updated_at) VALUES (%s, '', 'active', %s, %s)",
|
|
125
|
+
(cid, _now(), _now()),
|
|
126
|
+
)
|
|
127
|
+
conn.execute(
|
|
128
|
+
"INSERT INTO cloud_conversations (conversation_id, tenant_id) "
|
|
129
|
+
"VALUES (%s, %s)",
|
|
130
|
+
(cid, tid),
|
|
131
|
+
)
|
|
132
|
+
pg_store._ensure_schema() # First backfill
|
|
133
|
+
# Manually mutate cloud_conversations to a different tenant; bootstrap
|
|
134
|
+
# again. The WHERE clause filters on empty target so the row's
|
|
135
|
+
# already-populated tenant_id should NOT be overwritten.
|
|
136
|
+
conn.execute(
|
|
137
|
+
"UPDATE cloud_conversations SET tenant_id = %s WHERE conversation_id = %s",
|
|
138
|
+
(f"{tid}-mutated", cid),
|
|
139
|
+
)
|
|
140
|
+
pg_store._ensure_schema() # Second run; should be no-op for this row
|
|
141
|
+
final = conn.execute(
|
|
142
|
+
"SELECT tenant_id FROM conversations WHERE conversation_id = %s",
|
|
143
|
+
(cid,),
|
|
144
|
+
).fetchone()["tenant_id"]
|
|
145
|
+
assert final == tid, (
|
|
146
|
+
"Backfill should be idempotent; populated row's tenant_id must "
|
|
147
|
+
"NOT be overwritten on subsequent bootstraps"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_pg_backfill_skips_when_cloud_conversations_absent(pg_store, tmp_path):
|
|
152
|
+
""": when ``cloud_conversations`` is absent (engine-only deploy),
|
|
153
|
+
the backfill is silently skipped via the UndefinedTable narrow-catch.
|
|
154
|
+
Bootstrap completes without raising.
|
|
155
|
+
|
|
156
|
+
Validates the SAVEPOINT-via-nested-transaction pattern around the
|
|
157
|
+
backfill UPDATE: the missing-table raise rolls back ONLY the savepoint
|
|
158
|
+
and leaves the outer schema bootstrap alive.
|
|
159
|
+
"""
|
|
160
|
+
conn = pg_store._get_conn()
|
|
161
|
+
# Drop cloud_conversations if it exists (simulating engine-only deploy).
|
|
162
|
+
try:
|
|
163
|
+
conn.execute("DROP TABLE IF EXISTS cloud_conversations")
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
166
|
+
# Schema bootstrap must NOT raise.
|
|
167
|
+
pg_store._ensure_schema()
|
|
168
|
+
# And bootstrap should still create (or leave intact) other engine
|
|
169
|
+
# tables, evidenced by being able to insert a conversations row.
|
|
170
|
+
cid = f"pgbf-skipped-{uuid.uuid4().hex[:8]}"
|
|
171
|
+
conn.execute(
|
|
172
|
+
"INSERT INTO conversations (conversation_id, tenant_id, phase, "
|
|
173
|
+
"created_at, updated_at) VALUES (%s, '', 'active', %s, %s)",
|
|
174
|
+
(cid, _now(), _now()),
|
|
175
|
+
)
|
|
176
|
+
row = conn.execute(
|
|
177
|
+
"SELECT 1 FROM conversations WHERE conversation_id = %s",
|
|
178
|
+
(cid,),
|
|
179
|
+
).fetchone()
|
|
180
|
+
assert row is not None
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Tenant_id propagation tests for the conversations table ( prod fold).
|
|
2
|
+
|
|
3
|
+
Per prod blocker (cloud E2E surfaced: every existing user conv
|
|
4
|
+
plus every freshly-created conv had ``conversations.tenant_id = ''``,
|
|
5
|
+
which broke VCMERGE Layer C tenant validation with HTTP 403):
|
|
6
|
+
|
|
7
|
+
1. ``VirtualContextConfig.tenant_id`` field carries the request-scope
|
|
8
|
+
tenant context to every engine-internal ``upsert_conversation`` call.
|
|
9
|
+
2. The engine's ``_load_lifecycle_epoch_into_engine_state`` and
|
|
10
|
+
``sync_turns_from_payload`` paths now pass ``self.config.tenant_id``
|
|
11
|
+
instead of the empty-string placeholder.
|
|
12
|
+
3. PostgreSQL ``_ensure_schema`` runs a one-time backfill UPDATE from
|
|
13
|
+
``cloud_conversations.tenant_id`` on bootstrap (DATABASE_URL gated;
|
|
14
|
+
skipped silently when ``cloud_conversations`` is absent).
|
|
15
|
+
|
|
16
|
+
Tests pin the structural invariants behaviorally where possible and via
|
|
17
|
+
inspection where the cloud/PG cross-table join can't be exercised on
|
|
18
|
+
SQLite alone.
|
|
19
|
+
|
|
20
|
+
Marked @pytest.mark.regression("VCATTACH-DATALOSS-2026-04-26") so the
|
|
21
|
+
entire backstory is searchable.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
import uuid
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
|
|
30
|
+
import pytest
|
|
31
|
+
|
|
32
|
+
from virtual_context.storage.sqlite import SQLiteStore
|
|
33
|
+
from virtual_context.types import VirtualContextConfig
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
pytestmark = pytest.mark.regression("VCATTACH-DATALOSS-2026-04-26")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Config field: VirtualContextConfig.tenant_id is a string; defaults to ''.
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
def test_config_tenant_id_field_defaults_to_empty_string():
|
|
44
|
+
cfg = VirtualContextConfig()
|
|
45
|
+
assert hasattr(cfg, "tenant_id")
|
|
46
|
+
assert cfg.tenant_id == ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_config_tenant_id_field_round_trips_via_yaml(tmp_path):
|
|
50
|
+
"""Config builder accepts ``tenant_id`` in raw yaml dict."""
|
|
51
|
+
from virtual_context.config import _build_config
|
|
52
|
+
cfg = _build_config({"tenant_id": "tenant-xyz"})
|
|
53
|
+
assert cfg.tenant_id == "tenant-xyz"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_config_tenant_id_optional_in_yaml(tmp_path):
|
|
57
|
+
"""No ``tenant_id`` key in yaml means empty default (single-user dev)."""
|
|
58
|
+
from virtual_context.config import _build_config
|
|
59
|
+
cfg = _build_config({})
|
|
60
|
+
assert cfg.tenant_id == ""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Engine populates conversations.tenant_id from config when creating rows.
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
def test_upsert_conversation_writes_tenant_id_from_caller(tmp_path):
|
|
68
|
+
"""``Store.upsert_conversation`` writes the caller-supplied tenant_id
|
|
69
|
+
into the conversations row (the storage primitive has always supported
|
|
70
|
+
this; the bug was at engine call sites that passed '')."""
|
|
71
|
+
store = SQLiteStore(tmp_path / "store.db")
|
|
72
|
+
conv_id = "test-conv-" + uuid.uuid4().hex[:8]
|
|
73
|
+
store.upsert_conversation(tenant_id="tenant-A", conversation_id=conv_id)
|
|
74
|
+
conn = store._get_conn()
|
|
75
|
+
row = conn.execute(
|
|
76
|
+
"SELECT tenant_id FROM conversations WHERE conversation_id = ?",
|
|
77
|
+
(conv_id,),
|
|
78
|
+
).fetchone()
|
|
79
|
+
assert row["tenant_id"] == "tenant-A"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_engine_lifecycle_load_uses_config_tenant_id(tmp_path):
|
|
83
|
+
"""Structural pin: ``Engine._load_lifecycle_epoch_into_engine_state``
|
|
84
|
+
no longer hard-codes ``tenant_id=""`` in its upsert_conversation call;
|
|
85
|
+
the call sources tenant_id from ``self.config.tenant_id``.
|
|
86
|
+
"""
|
|
87
|
+
import inspect
|
|
88
|
+
from virtual_context.engine import VirtualContextEngine
|
|
89
|
+
src = inspect.getsource(
|
|
90
|
+
VirtualContextEngine._load_lifecycle_epoch_into_engine_state
|
|
91
|
+
)
|
|
92
|
+
# The literal hard-coded empty placeholder is gone.
|
|
93
|
+
assert 'tenant_id=""' not in src, (
|
|
94
|
+
"tenant_id-propagation: Engine._load_lifecycle_epoch_into_engine_state still "
|
|
95
|
+
"hard-codes tenant_id=\"\"; tenant scoping regression"
|
|
96
|
+
)
|
|
97
|
+
# The config-derived form is present.
|
|
98
|
+
assert "self.config.tenant_id" in src or "tenant_id=tenant_id" in src, (
|
|
99
|
+
"tenant_id-propagation: Engine._load_lifecycle_epoch_into_engine_state should "
|
|
100
|
+
"source tenant_id from self.config.tenant_id"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_engine_sync_turns_uses_config_tenant_id(tmp_path):
|
|
105
|
+
"""Structural pin: ``Engine.sync_turns_from_payload`` no
|
|
106
|
+
longer hard-codes ``tenant_id=""`` in its upsert_conversation call.
|
|
107
|
+
"""
|
|
108
|
+
import inspect
|
|
109
|
+
from virtual_context.engine import VirtualContextEngine
|
|
110
|
+
src = inspect.getsource(VirtualContextEngine.sync_turns_from_payload)
|
|
111
|
+
assert 'tenant_id=""' not in src, (
|
|
112
|
+
"tenant_id-propagation: Engine.sync_turns_from_payload still hard-codes "
|
|
113
|
+
"tenant_id=\"\"; tenant scoping regression"
|
|
114
|
+
)
|
|
115
|
+
assert "self.config.tenant_id" in src, (
|
|
116
|
+
"tenant_id-propagation: Engine.sync_turns_from_payload should source tenant_id "
|
|
117
|
+
"from self.config.tenant_id"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_engine_creates_conversations_row_with_config_tenant_id(tmp_path):
|
|
122
|
+
"""End-to-end: instantiate engine with ``cfg.tenant_id`` set; the
|
|
123
|
+
bootstrap-created conversations row carries that tenant_id (was '' in
|
|
124
|
+
pre-fold prod, breaking VCMERGE tenant scoping)."""
|
|
125
|
+
from virtual_context.engine import VirtualContextEngine
|
|
126
|
+
cfg = VirtualContextConfig(
|
|
127
|
+
tenant_id="tenant-prod-fix",
|
|
128
|
+
conversation_id="conv-test-" + uuid.uuid4().hex[:8],
|
|
129
|
+
)
|
|
130
|
+
cfg.storage.backend = "sqlite"
|
|
131
|
+
cfg.storage.sqlite_path = str(tmp_path / "store.db")
|
|
132
|
+
engine = VirtualContextEngine(config=cfg)
|
|
133
|
+
try:
|
|
134
|
+
store = engine._store
|
|
135
|
+
# Drill into composite/inner stores to reach the SQLite path.
|
|
136
|
+
inner = getattr(store, "_segments", None) or getattr(store, "_store", None) or store
|
|
137
|
+
conn = inner._get_conn()
|
|
138
|
+
row = conn.execute(
|
|
139
|
+
"SELECT tenant_id FROM conversations WHERE conversation_id = ?",
|
|
140
|
+
(cfg.conversation_id,),
|
|
141
|
+
).fetchone()
|
|
142
|
+
assert row is not None, "engine bootstrap did not create conversations row"
|
|
143
|
+
assert row["tenant_id"] == "tenant-prod-fix", (
|
|
144
|
+
f"conversations.tenant_id should be 'tenant-prod-fix' but got "
|
|
145
|
+
f"{row['tenant_id']!r}; pre-fold regression"
|
|
146
|
+
)
|
|
147
|
+
finally:
|
|
148
|
+
engine.close()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
# backfill behavior: source tenant_id from cloud_conversations.
|
|
153
|
+
# Structural-only on SQLite (no cloud_conversations table); behavioral
|
|
154
|
+
# in tests/test_conversations_tenant_id_backfill_postgres.py against PG.
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
def test_pg_backfill_sql_present_in_ensure_schema():
|
|
158
|
+
""" structural pin: ``PostgresStore._ensure_schema`` must contain
|
|
159
|
+
the cloud_conversations backfill UPDATE. A future regression that drops
|
|
160
|
+
the migration would fail this test before reaching staging."""
|
|
161
|
+
import inspect
|
|
162
|
+
from virtual_context.storage import postgres as postgres_mod
|
|
163
|
+
src = inspect.getsource(postgres_mod.PostgresStore._ensure_schema)
|
|
164
|
+
# The backfill SQL fingerprint: "FROM cloud_conversations" + tenant_id update target.
|
|
165
|
+
assert "FROM cloud_conversations" in src, (
|
|
166
|
+
"tenant_id-propagation: PostgresStore._ensure_schema missing cloud_conversations "
|
|
167
|
+
"backfill UPDATE; existing prod convs would stay with empty tenant_id"
|
|
168
|
+
)
|
|
169
|
+
assert "UPDATE conversations" in src, (
|
|
170
|
+
"tenant_id-propagation: PostgresStore._ensure_schema backfill missing UPDATE "
|
|
171
|
+
"conversations target"
|
|
172
|
+
)
|
|
173
|
+
# Idempotency safeguard: WHERE filters on empty target.
|
|
174
|
+
assert (
|
|
175
|
+
"conversations.tenant_id = ''" in src
|
|
176
|
+
or 'conversations.tenant_id IS NULL' in src
|
|
177
|
+
), (
|
|
178
|
+
"tenant_id-propagation: backfill UPDATE missing the empty-source idempotency "
|
|
179
|
+
"filter; would re-write tenant_id on every schema bootstrap"
|
|
180
|
+
)
|
|
181
|
+
# Best-effort gate: UndefinedTable swallowed.
|
|
182
|
+
assert "UndefinedTable" in src, (
|
|
183
|
+
"tenant_id-propagation: backfill should swallow only psycopg.errors.UndefinedTable "
|
|
184
|
+
"(missing cloud_conversations on engine-only deploys); other "
|
|
185
|
+
"exceptions must propagate"
|
|
186
|
+
)
|