clsplusplus 7.3.0__tar.gz → 7.3.2__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.
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/PKG-INFO +1 -1
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/pyproject.toml +1 -1
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/api.py +14 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/memory_phase.py +4 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/memory_service.py +28 -1
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus.egg-info/PKG-INFO +1 -1
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus.egg-info/SOURCES.txt +2 -0
- clsplusplus-7.3.2/tests/test_extension_cross_model_runtime.py +280 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_extension_integration.py +26 -2
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_extension_ui.py +1 -1
- clsplusplus-7.3.2/tests/test_mem0_failure_modes.py +706 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/LICENSE +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/README.md +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/setup.cfg +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/__init__.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/abuse_guard.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/api_usage_analytics.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/auth.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/cli.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/client.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/config.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/cost_forecast.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/cost_model.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/debug_console.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/demo_llm.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/demo_llm_calls.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/demo_local.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/email_service.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/embeddings.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/funnel_routes.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/geo.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/health_metrics.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/homepage_autopromote.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/idempotency.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/integration_service.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/integrations/__init__.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/integrations/autogen.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/integrations/crewai.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/integrations/langgraph.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/integrations/llamaindex.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/jwt_utils.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/local_routes.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/main.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/mcp_http.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/mcp_server.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/memory_cycle.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/metering_v2/__init__.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/metering_v2/__main__.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/metering_v2/billing.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/metering_v2/healthcheck.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/metering_v2/notifier.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/metering_v2/pricing.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/metering_v2/reconciler.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/metering_v2/schema.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/metering_v2/writer.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/metrics.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/middleware.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/models.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/namespace_resolver.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/oauth_server.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/permissions.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/plasticity.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/pricing.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/pricing_models.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/pricing_store.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/prompt_log.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/rate_limit.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/razorpay_service.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/rbac_service.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/reconsolidation.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/resilience.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/sleep_cycle.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/__init__.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/base.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/chat_session_store.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/integration_store.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/l0_working_buffer.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/l1_indexing_store.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/l2_schema_graph.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/l3_deep_recess.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/l3_postgres.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/oauth_store.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/rbac_store.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/user_store.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/waitlist_store.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/web_events_store.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stripe_service.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/subscription_watchdog.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/temporal.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/test_suite.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/tier_resolver.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/tiers.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/topical_resonance.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/tracer.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/usage.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/user_embeddings.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/user_pulse.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/user_service.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/waitlist_service.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/webhook_dispatcher.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/weblab.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/weblab_watcher.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/window_limits.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/write_metrics.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus.egg-info/dependency_links.txt +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus.egg-info/entry_points.txt +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus.egg-info/requires.txt +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus.egg-info/top_level.txt +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_abuse_guard.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_admin.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_admin_seed.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_api.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_api_comprehensive.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_api_endpoints.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_api_usage_analytics.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_auth.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_auth_me_api_key.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_autogen_memory.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_billing_e2e.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_burst_hardening.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_client_sdk.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_config.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_cost_forecast.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_crewai_provider.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_cross_llm_memory.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_debug_console.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_demo_llm.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_embeddings.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_feedback_github.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_free_tier_lifecycle.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_full_api_coverage.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_funnel_metrics.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_geo_gating.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_health_metrics.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_idempotency.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_integration_service.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_integration_sync_cli_contract.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_langgraph_store.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_llamaindex_block.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_mcp_connect.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_mcp_oauth.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_memory_cycle.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_memory_phase.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_memory_service.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_memory_write_no_schema_garbage.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_metering_v2_healthcheck.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_metering_v2_notifier.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_metering_v2_pricing.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_metering_v2_reconciler.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_metering_v2_schema.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_metering_v2_writer.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_middleware.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_models.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_overage_billing.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_performance.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_plasticity.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_pricing.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_prototype_e2e.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_rate_limit.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_razorpay_billing.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_razorpay_subscription_webhooks.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_reconsolidation.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_regression.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_resilience.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_security.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_sleep_cycle.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_stores.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_subscription_watchdog.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_tier_resolver.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_tiers.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_usage.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_user_auth.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_user_embeddings.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_user_pulse.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_user_stories.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_waitlist.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_webhook_dispatcher.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_weblab.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_window_limits.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_write_metrics.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clsplusplus
|
|
3
|
-
Version: 7.3.
|
|
3
|
+
Version: 7.3.2
|
|
4
4
|
Summary: Brain-inspired, model-agnostic persistent memory for LLMs. Learn, recall, forget — like a brain. Works with OpenAI, Claude, Gemini, Llama.
|
|
5
5
|
Author-email: AlphaForge AI Labs <contact@alphaforge.ai>
|
|
6
6
|
Maintainer-email: Rajamohan Jabbala <contact@alphaforge.ai>
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "clsplusplus"
|
|
7
|
-
version = "7.3.
|
|
7
|
+
version = "7.3.2"
|
|
8
8
|
description = "Brain-inspired, model-agnostic persistent memory for LLMs. Learn, recall, forget — like a brain. Works with OpenAI, Claude, Gemini, Llama."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -481,6 +481,20 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI:
|
|
|
481
481
|
"distinct_id": distinct_id,
|
|
482
482
|
}
|
|
483
483
|
|
|
484
|
+
@app.get("/v1/config/extension")
|
|
485
|
+
async def extension_provider_config():
|
|
486
|
+
"""Hot-updatable provider URL rules for the Chrome extension.
|
|
487
|
+
|
|
488
|
+
When ChatGPT/Claude/Gemini change API paths, update extension/provider_rules.json
|
|
489
|
+
on the server — clients pick it up without a Chrome Web Store redeploy.
|
|
490
|
+
"""
|
|
491
|
+
rules_path = FilePath(__file__).resolve().parents[2] / "extension" / "provider_rules.json"
|
|
492
|
+
try:
|
|
493
|
+
import json as _json
|
|
494
|
+
return _json.loads(rules_path.read_text(encoding="utf-8"))
|
|
495
|
+
except Exception:
|
|
496
|
+
return {"version": 0, "providers": {}}
|
|
497
|
+
|
|
484
498
|
@app.get("/admin/weblab")
|
|
485
499
|
async def admin_weblab_status(request: Request):
|
|
486
500
|
"""Weblab control status — watched flags + latest green/red verdict."""
|
|
@@ -496,6 +496,10 @@ class PhaseMemoryItem:
|
|
|
496
496
|
# True = a newer fact on the same (subject, relation) dimension exists.
|
|
497
497
|
# Superseded items are hidden from default reads but never deleted.
|
|
498
498
|
superseded: bool = False
|
|
499
|
+
# Belief-revision provenance: lineage holds the ids of prior facts this item
|
|
500
|
+
# supersedes (oldest first); version increments on each adjudicated overwrite.
|
|
501
|
+
lineage: list = field(default_factory=list)
|
|
502
|
+
version: int = 1
|
|
499
503
|
|
|
500
504
|
# --- Crystallization (Liquid → Solid phase transition) ---
|
|
501
505
|
schema_meta: Optional[SchemaMeta] = None # Non-None = solid/glass phase
|
|
@@ -112,6 +112,8 @@ class MemoryService:
|
|
|
112
112
|
object=(phase_item.fact.value[:256] if phase_item.fact.value else None) or (req.object if req else None),
|
|
113
113
|
event_at=getattr(phase_item, "event_at", None),
|
|
114
114
|
superseded=getattr(phase_item, "superseded", False),
|
|
115
|
+
lineage=list(getattr(phase_item, "lineage", []) or []),
|
|
116
|
+
version=getattr(phase_item, "version", 1),
|
|
115
117
|
)
|
|
116
118
|
|
|
117
119
|
# =========================================================================
|
|
@@ -1245,6 +1247,7 @@ class MemoryService:
|
|
|
1245
1247
|
new_item = MemoryItem(text=new_text, namespace=namespace)
|
|
1246
1248
|
new_item = self.embedding_service.embed_item(new_item)
|
|
1247
1249
|
|
|
1250
|
+
revised = None # the gate-updated item carrying lineage/version, if any
|
|
1248
1251
|
if existing and existing.embedding:
|
|
1249
1252
|
updated, archived, should_engrave = self.reconsolidation.prepare_for_reconsolidation(
|
|
1250
1253
|
new_item, existing, evidence,
|
|
@@ -1252,6 +1255,7 @@ class MemoryService:
|
|
|
1252
1255
|
if not should_engrave:
|
|
1253
1256
|
# Quorum not met — return existing unchanged
|
|
1254
1257
|
return existing
|
|
1258
|
+
revised = updated # lineage = old.lineage + [old.id], version = old.version + 1
|
|
1255
1259
|
|
|
1256
1260
|
# Quorum met (or no existing) — store via PhaseMemoryEngine
|
|
1257
1261
|
req = WriteRequest(
|
|
@@ -1260,7 +1264,30 @@ class MemoryService:
|
|
|
1260
1264
|
source="adjudicate",
|
|
1261
1265
|
authority=1.0, # High authority for adjudicated facts
|
|
1262
1266
|
)
|
|
1263
|
-
|
|
1267
|
+
result = await self.write(req)
|
|
1268
|
+
|
|
1269
|
+
# Persist belief-revision provenance: carry the gate's lineage/version onto
|
|
1270
|
+
# the engraved fact and archive (never delete) the superseded one. Without
|
|
1271
|
+
# this the overwrite would land as a fresh v1 with no history — the gap a
|
|
1272
|
+
# naive "latest wins" store (e.g. Mem0 #4536) leaves behind.
|
|
1273
|
+
if revised is not None:
|
|
1274
|
+
new_phase = self.engine._item_by_id.get(result.id)
|
|
1275
|
+
if new_phase is not None:
|
|
1276
|
+
new_phase.lineage = list(revised.lineage)
|
|
1277
|
+
new_phase.version = revised.version
|
|
1278
|
+
result.lineage = list(revised.lineage)
|
|
1279
|
+
result.version = revised.version
|
|
1280
|
+
if existing is not None:
|
|
1281
|
+
old_phase = self.engine._item_by_id.get(existing.id)
|
|
1282
|
+
if old_phase is not None:
|
|
1283
|
+
old_phase.superseded = True
|
|
1284
|
+
# Best-effort durable archive of the superseded fact in L1.
|
|
1285
|
+
try:
|
|
1286
|
+
await self.l1.update_superseded(existing.id, namespace)
|
|
1287
|
+
except Exception as _arch_err: # pragma: no cover - archive is best-effort
|
|
1288
|
+
logger.debug("Adjudicate archive (L1) non-fatal: %s", _arch_err)
|
|
1289
|
+
|
|
1290
|
+
return result
|
|
1264
1291
|
|
|
1265
1292
|
# =========================================================================
|
|
1266
1293
|
# Multi-hop read (Issue #36)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clsplusplus
|
|
3
|
-
Version: 7.3.
|
|
3
|
+
Version: 7.3.2
|
|
4
4
|
Summary: Brain-inspired, model-agnostic persistent memory for LLMs. Learn, recall, forget — like a brain. Works with OpenAI, Claude, Gemini, Llama.
|
|
5
5
|
Author-email: AlphaForge AI Labs <contact@alphaforge.ai>
|
|
6
6
|
Maintainer-email: Rajamohan Jabbala <contact@alphaforge.ai>
|
|
@@ -120,6 +120,7 @@ tests/test_cross_llm_memory.py
|
|
|
120
120
|
tests/test_debug_console.py
|
|
121
121
|
tests/test_demo_llm.py
|
|
122
122
|
tests/test_embeddings.py
|
|
123
|
+
tests/test_extension_cross_model_runtime.py
|
|
123
124
|
tests/test_extension_integration.py
|
|
124
125
|
tests/test_extension_ui.py
|
|
125
126
|
tests/test_feedback_github.py
|
|
@@ -135,6 +136,7 @@ tests/test_langgraph_store.py
|
|
|
135
136
|
tests/test_llamaindex_block.py
|
|
136
137
|
tests/test_mcp_connect.py
|
|
137
138
|
tests/test_mcp_oauth.py
|
|
139
|
+
tests/test_mem0_failure_modes.py
|
|
138
140
|
tests/test_memory_cycle.py
|
|
139
141
|
tests/test_memory_phase.py
|
|
140
142
|
tests/test_memory_service.py
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Executable cross-model extension runtime tests.
|
|
3
|
+
|
|
4
|
+
Simulates the ChatGPT → Claude handoff without a browser:
|
|
5
|
+
store on GPT-shaped body, prefetch mailbox, inject on Claude-shaped bodies.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import time
|
|
14
|
+
from copy import deepcopy
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
EXT_DIR = os.path.join(os.path.dirname(__file__), "..", "extension")
|
|
19
|
+
CTX_MARKER = "For context, here are some things I have mentioned before"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def read_ext(name: str) -> str:
|
|
23
|
+
with open(os.path.join(EXT_DIR, name), "r") as f:
|
|
24
|
+
return f.read()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_ctx(facts: list[str]) -> str:
|
|
28
|
+
return (
|
|
29
|
+
"For context, here are some things I have mentioned before in other conversations:\n"
|
|
30
|
+
+ "\n".join(f"- {f[:200]}" for f in facts)
|
|
31
|
+
+ "\n\nNow, my actual question:\n"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_llm(url: str, host: str) -> bool:
|
|
36
|
+
u = url.lower()
|
|
37
|
+
h = host.lower()
|
|
38
|
+
if "chatgpt" in h or "openai" in h:
|
|
39
|
+
if "/backend-api/" not in u or "conversation" not in u:
|
|
40
|
+
return False
|
|
41
|
+
if "/init" in u or "/prepare" in u:
|
|
42
|
+
return False
|
|
43
|
+
return True
|
|
44
|
+
if "claude" in h:
|
|
45
|
+
if "/v1/sessions/" in u and "/events" in u:
|
|
46
|
+
return True
|
|
47
|
+
if "/api/organizations/" in u and "chat_conversations" in u and "completion" in u:
|
|
48
|
+
return True
|
|
49
|
+
return "/api/" in u and any(k in u for k in ("chat", "completion", "message"))
|
|
50
|
+
if "gemini" in h:
|
|
51
|
+
return "batchexecute" in u
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def extract(body: str, host: str) -> tuple[str, callable] | None:
|
|
56
|
+
try:
|
|
57
|
+
b = json.loads(body)
|
|
58
|
+
except json.JSONDecodeError:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
if "chatgpt" in host or "openai" in host:
|
|
62
|
+
messages = b.get("messages") or []
|
|
63
|
+
if messages:
|
|
64
|
+
m = messages[-1]
|
|
65
|
+
content = m.get("content")
|
|
66
|
+
if isinstance(content, dict) and content.get("parts"):
|
|
67
|
+
q = content["parts"][0]
|
|
68
|
+
def setter(ctx, _b=b, _m=m):
|
|
69
|
+
_m["content"]["parts"][0] = ctx + q
|
|
70
|
+
return json.dumps(_b)
|
|
71
|
+
return q, setter
|
|
72
|
+
if isinstance(content, str):
|
|
73
|
+
def setter(ctx, _b=b, _m=m):
|
|
74
|
+
_m["content"] = ctx + content
|
|
75
|
+
return json.dumps(_b)
|
|
76
|
+
return content, setter
|
|
77
|
+
|
|
78
|
+
if isinstance(b.get("prompt"), str) and len(b["prompt"]) > 3:
|
|
79
|
+
q = b["prompt"]
|
|
80
|
+
def setter(ctx, _b=b):
|
|
81
|
+
_b["prompt"] = ctx + q
|
|
82
|
+
return json.dumps(_b)
|
|
83
|
+
return q, setter
|
|
84
|
+
|
|
85
|
+
events = b.get("events")
|
|
86
|
+
if isinstance(events, list):
|
|
87
|
+
for ev in reversed(events):
|
|
88
|
+
if not ev or ev.get("type") != "user":
|
|
89
|
+
continue
|
|
90
|
+
msg = ev.get("message") or {}
|
|
91
|
+
content = msg.get("content")
|
|
92
|
+
if isinstance(content, str) and content:
|
|
93
|
+
def setter(ctx, _msg=msg, _q=content, _b=b):
|
|
94
|
+
_msg["content"] = ctx + _q
|
|
95
|
+
return json.dumps(_b)
|
|
96
|
+
return content, setter
|
|
97
|
+
if isinstance(content, list):
|
|
98
|
+
for block in content:
|
|
99
|
+
if block.get("type") == "text" and block.get("text"):
|
|
100
|
+
q = block["text"]
|
|
101
|
+
def setter(ctx, _block=block, _q=q, _b=b):
|
|
102
|
+
_block["text"] = ctx + _q
|
|
103
|
+
return json.dumps(_b)
|
|
104
|
+
return q, setter
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def inject(body: str, host: str, facts: list[str]) -> str | None:
|
|
109
|
+
parsed = extract(body, host)
|
|
110
|
+
if not parsed:
|
|
111
|
+
return None
|
|
112
|
+
_q, setter = parsed
|
|
113
|
+
return setter(build_ctx(facts))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TestURLDetection:
|
|
117
|
+
def test_chatgpt_conversation(self):
|
|
118
|
+
assert is_llm("https://chatgpt.com/backend-api/conversation", "chatgpt.com")
|
|
119
|
+
|
|
120
|
+
def test_claude_completion_legacy(self):
|
|
121
|
+
url = "https://claude.ai/api/organizations/org/chat_conversations/conv/completion"
|
|
122
|
+
assert is_llm(url, "claude.ai")
|
|
123
|
+
|
|
124
|
+
def test_claude_sessions_events_2026(self):
|
|
125
|
+
url = "https://claude.ai/v1/sessions/session_01ABC/events"
|
|
126
|
+
assert is_llm(url, "claude.ai")
|
|
127
|
+
|
|
128
|
+
def test_claude_org_list_not_llm(self):
|
|
129
|
+
assert not is_llm("https://claude.ai/api/organizations", "claude.ai")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class TestCrossModelInjection:
|
|
133
|
+
def test_chatgpt_store_shape_injects(self):
|
|
134
|
+
body = json.dumps({
|
|
135
|
+
"messages": [{
|
|
136
|
+
"role": "user",
|
|
137
|
+
"content": {"parts": ["My favorite color is teal"]},
|
|
138
|
+
}]
|
|
139
|
+
})
|
|
140
|
+
out = inject(body, "chatgpt.com", ["My favorite color is teal", "I live in Austin"])
|
|
141
|
+
assert out
|
|
142
|
+
assert CTX_MARKER in out
|
|
143
|
+
assert "teal" in out
|
|
144
|
+
|
|
145
|
+
def test_claude_completion_injects(self):
|
|
146
|
+
body = json.dumps({"prompt": "What is my favorite color?"})
|
|
147
|
+
out = inject(body, "claude.ai", ["My favorite color is teal"])
|
|
148
|
+
assert out
|
|
149
|
+
assert CTX_MARKER in out
|
|
150
|
+
assert "teal" in out
|
|
151
|
+
|
|
152
|
+
def test_claude_sessions_events_injects(self):
|
|
153
|
+
body = json.dumps({
|
|
154
|
+
"events": [{
|
|
155
|
+
"type": "user",
|
|
156
|
+
"message": {"role": "user", "content": "What is my favorite color?"},
|
|
157
|
+
}]
|
|
158
|
+
})
|
|
159
|
+
out = inject(body, "claude.ai", ["My favorite color is teal"])
|
|
160
|
+
assert out
|
|
161
|
+
assert CTX_MARKER in out
|
|
162
|
+
assert "teal" in out
|
|
163
|
+
|
|
164
|
+
def test_claude_sessions_blocks_injects(self):
|
|
165
|
+
body = json.dumps({
|
|
166
|
+
"events": [{
|
|
167
|
+
"type": "user",
|
|
168
|
+
"message": {
|
|
169
|
+
"role": "user",
|
|
170
|
+
"content": [{"type": "text", "text": "Remind me where I live"}],
|
|
171
|
+
},
|
|
172
|
+
}]
|
|
173
|
+
})
|
|
174
|
+
out = inject(body, "claude.ai", ["I live in Springfield"])
|
|
175
|
+
assert out
|
|
176
|
+
assert "Springfield" in out
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class TestMailboxRace:
|
|
180
|
+
def test_delayed_mailbox_still_injects_within_wait_window(self):
|
|
181
|
+
"""Simulates capture.js prefetch arriving after GPT store but before Claude send."""
|
|
182
|
+
facts: list[str] = []
|
|
183
|
+
mailbox_ready_at = time.time() + 0.15
|
|
184
|
+
|
|
185
|
+
def get_ctx_now() -> str:
|
|
186
|
+
if time.time() < mailbox_ready_at or not facts:
|
|
187
|
+
return ""
|
|
188
|
+
return build_ctx(facts)
|
|
189
|
+
|
|
190
|
+
def wait_for_ctx(timeout_ms: int = 1200) -> str:
|
|
191
|
+
deadline = time.time() + timeout_ms / 1000
|
|
192
|
+
while time.time() < deadline:
|
|
193
|
+
ctx = get_ctx_now()
|
|
194
|
+
if ctx:
|
|
195
|
+
return ctx
|
|
196
|
+
time.sleep(0.04)
|
|
197
|
+
return ""
|
|
198
|
+
|
|
199
|
+
# GPT turn stores fact (async)
|
|
200
|
+
facts.append("My dog is named Max")
|
|
201
|
+
|
|
202
|
+
claude_body = json.dumps({"prompt": "What is my dog's name?"})
|
|
203
|
+
parsed = extract(claude_body, "claude.ai")
|
|
204
|
+
assert parsed
|
|
205
|
+
q, setter = parsed
|
|
206
|
+
|
|
207
|
+
ctx = get_ctx_now()
|
|
208
|
+
if not ctx:
|
|
209
|
+
ctx = wait_for_ctx()
|
|
210
|
+
assert ctx
|
|
211
|
+
out = setter(ctx)
|
|
212
|
+
assert "Max" in out
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class TestExtensionSourceGuards:
|
|
216
|
+
def test_intercept_has_sessions_extractor(self):
|
|
217
|
+
js = read_ext("intercept.js")
|
|
218
|
+
assert "b.events" in js
|
|
219
|
+
assert "/v1/sessions/" in js
|
|
220
|
+
|
|
221
|
+
def test_intercept_async_uses_wait_path(self):
|
|
222
|
+
js = read_ext("intercept.js")
|
|
223
|
+
assert "async function injectBody" in js
|
|
224
|
+
assert "await injectBody(body, 'async')" in js
|
|
225
|
+
|
|
226
|
+
def test_background_bumps_mailbox_after_store(self):
|
|
227
|
+
js = read_ext("background.js")
|
|
228
|
+
assert "cls_mem_bump" in js
|
|
229
|
+
|
|
230
|
+
def test_capture_listens_for_mem_bump(self):
|
|
231
|
+
js = read_ext("capture.js")
|
|
232
|
+
assert "cls_mem_bump" in js
|
|
233
|
+
|
|
234
|
+
def test_query_aware_recall_on_send(self):
|
|
235
|
+
js = read_ext("capture.js")
|
|
236
|
+
assert "data-cls-query" in js
|
|
237
|
+
assert "FETCH_RELEVANT" in js
|
|
238
|
+
assert "refreshMemoriesForQuery" in js
|
|
239
|
+
|
|
240
|
+
def test_remote_provider_rules_sync(self):
|
|
241
|
+
js = read_ext("capture.js")
|
|
242
|
+
assert "data-cls-provider-rules" in js
|
|
243
|
+
assert "GET_PROVIDER_RULES" in js
|
|
244
|
+
js2 = read_ext("intercept.js")
|
|
245
|
+
assert "getProviderRules" in js2
|
|
246
|
+
js3 = read_ext("background.js")
|
|
247
|
+
assert "/v1/config/extension" in js3
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@pytest.mark.asyncio
|
|
251
|
+
async def test_prototype_mock_claude_sessions_endpoint():
|
|
252
|
+
"""Server mock validates injected sessions API body."""
|
|
253
|
+
import sys
|
|
254
|
+
from pathlib import Path
|
|
255
|
+
from httpx import ASGITransport, AsyncClient
|
|
256
|
+
|
|
257
|
+
root = Path(__file__).resolve().parents[1]
|
|
258
|
+
for p in (str(root / "prototype"), str(root / "src")):
|
|
259
|
+
if p not in sys.path:
|
|
260
|
+
sys.path.insert(0, p)
|
|
261
|
+
import server as proto_server # noqa: PLC0415
|
|
262
|
+
|
|
263
|
+
transport = ASGITransport(app=proto_server.app)
|
|
264
|
+
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
265
|
+
body = {
|
|
266
|
+
"events": [{
|
|
267
|
+
"type": "user",
|
|
268
|
+
"message": {
|
|
269
|
+
"role": "user",
|
|
270
|
+
"content": (
|
|
271
|
+
"For context, here are some things I have mentioned before in other conversations:\n"
|
|
272
|
+
"- My favorite color is teal\n\n"
|
|
273
|
+
"Now, my actual question:\nWhat is my favorite color?"
|
|
274
|
+
),
|
|
275
|
+
},
|
|
276
|
+
}]
|
|
277
|
+
}
|
|
278
|
+
r = await client.post("/v1/sessions/session_test/events", json=body)
|
|
279
|
+
assert r.status_code == 200
|
|
280
|
+
assert r.json().get("injection_ok") is True
|
|
@@ -90,7 +90,8 @@ class TestInterceptClaudeExtraction:
|
|
|
90
90
|
|
|
91
91
|
def test_detects_claude_url(self):
|
|
92
92
|
assert "claude" in self.js
|
|
93
|
-
assert "/
|
|
93
|
+
assert "/v1/sessions/" in self.js
|
|
94
|
+
assert "chat_conversations" in self.js
|
|
94
95
|
|
|
95
96
|
def test_extracts_prompt_field(self):
|
|
96
97
|
assert "b.prompt" in self.js
|
|
@@ -106,6 +107,12 @@ class TestInterceptClaudeExtraction:
|
|
|
106
107
|
def test_extracts_text_field(self):
|
|
107
108
|
assert "b.text" in self.js
|
|
108
109
|
|
|
110
|
+
def test_has_short_wait_fallback_for_first_claude_send(self):
|
|
111
|
+
"""Injection waits for mailbox when facts are not ready yet."""
|
|
112
|
+
assert "waitForCtx" in self.js
|
|
113
|
+
assert "async function injectBody" in self.js
|
|
114
|
+
assert "await injectBody(body, 'async')" in self.js
|
|
115
|
+
|
|
109
116
|
|
|
110
117
|
class TestInterceptGeminiExtraction:
|
|
111
118
|
"""Verify Gemini batchexecute URL-encoded form data extraction."""
|
|
@@ -212,6 +219,10 @@ class TestCaptureDOMMailbox:
|
|
|
212
219
|
def test_writes_timestamp(self):
|
|
213
220
|
assert "data-ts" in self.js
|
|
214
221
|
|
|
222
|
+
def test_uses_bridge_node_fallback_before_body_exists(self):
|
|
223
|
+
"""Early document_start execution may run before body is available."""
|
|
224
|
+
assert "document.body || document.documentElement" in self.js
|
|
225
|
+
|
|
215
226
|
def test_reads_outbox(self):
|
|
216
227
|
assert "data-cls-outbox" in self.js
|
|
217
228
|
assert "JSON.parse" in self.js
|
|
@@ -365,7 +376,8 @@ class TestBackgroundMessageRouting:
|
|
|
365
376
|
|
|
366
377
|
def test_handles_all_message_types(self):
|
|
367
378
|
required = [
|
|
368
|
-
"STORE", "FETCH", "
|
|
379
|
+
"STORE", "FETCH", "FETCH_RELEVANT", "GET_PROVIDER_RULES",
|
|
380
|
+
"ACTIVITY", "USAGE", "SEARCH",
|
|
369
381
|
"DAILY_COUNTERS", "INCREMENT_INJECTED", "VERIFY_KEY", "OPEN_PANEL",
|
|
370
382
|
]
|
|
371
383
|
for msg_type in required:
|
|
@@ -431,6 +443,12 @@ class TestCrossWorldArchitecture:
|
|
|
431
443
|
assert main[0]["js"] == ["intercept.js"]
|
|
432
444
|
assert isolated[0]["js"] == ["capture.js"]
|
|
433
445
|
|
|
446
|
+
def test_capture_runs_at_document_start(self):
|
|
447
|
+
"""Capture must initialize mailbox before first provider send."""
|
|
448
|
+
manifest = json.loads(read_ext("manifest.json"))
|
|
449
|
+
capture = [s for s in manifest["content_scripts"] if s.get("js") == ["capture.js"]][0]
|
|
450
|
+
assert capture["run_at"] == "document_start"
|
|
451
|
+
|
|
434
452
|
|
|
435
453
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
436
454
|
# Memory Filter Logic (capture.js)
|
|
@@ -457,3 +475,9 @@ class TestMemoryFiltering:
|
|
|
457
475
|
|
|
458
476
|
def test_filters_questions(self):
|
|
459
477
|
assert "?" in self.js # endsWith('?') filter
|
|
478
|
+
|
|
479
|
+
def test_refreshes_on_tab_reentry(self):
|
|
480
|
+
"""Switching models/tabs should trigger immediate prefetch refresh."""
|
|
481
|
+
assert "visibilitychange" in self.js
|
|
482
|
+
assert "pageshow" in self.js
|
|
483
|
+
assert "focus" in self.js
|
|
@@ -648,7 +648,7 @@ class TestCaptureJS:
|
|
|
648
648
|
assert "data-facts" in self.js
|
|
649
649
|
|
|
650
650
|
def test_refresh_interval(self):
|
|
651
|
-
assert "setInterval(refreshMemories,
|
|
651
|
+
assert "setInterval(refreshMemories, 10000)" in self.js
|
|
652
652
|
|
|
653
653
|
def test_outbox_poll_interval(self):
|
|
654
654
|
assert "setInterval(checkOutbox, 500)" in self.js
|