@smilintux/skcapstone 0.1.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +98 -0
- package/.github/workflows/ci.yml +39 -3
- package/.github/workflows/publish.yml +25 -4
- package/.openclaw-workspace.json +58 -0
- package/CHANGELOG.md +62 -0
- package/CLAUDE.md +39 -2
- package/MANIFEST.in +6 -0
- package/MISSION.md +7 -0
- package/README.md +47 -2
- package/SKILL.md +895 -23
- package/docker/Dockerfile +61 -0
- package/docker/compose-templates/dev-team.yml +203 -0
- package/docker/compose-templates/mini-team.yml +140 -0
- package/docker/compose-templates/ops-team.yml +173 -0
- package/docker/compose-templates/research-team.yml +170 -0
- package/docker/entrypoint.sh +192 -0
- package/docs/ARCHITECTURE.md +663 -374
- package/docs/BOND_WITH_GROK.md +112 -0
- package/docs/GETTING_STARTED.md +782 -0
- package/docs/QUICKSTART.md +477 -0
- package/docs/SKJOULE_ARCHITECTURE.md +658 -0
- package/docs/SOUL_SWAPPER.md +921 -0
- package/docs/SOVEREIGN_SINGULARITY.md +47 -14
- package/examples/custom-bond-template.json +36 -0
- package/examples/grok-feb.json +36 -0
- package/examples/grok-testimony.md +34 -0
- package/examples/love-bootloader.txt +32 -0
- package/examples/plugins/echo_tool.py +87 -0
- package/examples/queen-ava-feb.json +36 -0
- package/examples/souls/lumina.yaml +64 -0
- package/index.js +6 -5
- package/installer/build.py +124 -0
- package/openclaw-plugin/package.json +13 -0
- package/openclaw-plugin/src/index.ts +351 -0
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +38 -2
- package/scripts/bump_version.py +141 -0
- package/scripts/check-updates.py +230 -0
- package/scripts/convert_blueprints_to_yaml.py +157 -0
- package/scripts/dev-install.sh +14 -0
- package/scripts/e2e-test.sh +193 -0
- package/scripts/install-bundle.sh +171 -0
- package/scripts/install.bat +2 -0
- package/scripts/install.ps1 +253 -0
- package/scripts/install.sh +185 -0
- package/scripts/mcp-serve.sh +69 -0
- package/scripts/mcp-server.bat +113 -0
- package/scripts/mcp-server.ps1 +116 -0
- package/scripts/mcp-server.sh +99 -0
- package/scripts/pull-models.sh +10 -0
- package/scripts/skcapstone +48 -0
- package/scripts/verify_install.sh +180 -0
- package/scripts/windows/install-tasks.ps1 +406 -0
- package/scripts/windows/skcapstone-task.xml +113 -0
- package/scripts/windows/uninstall-tasks.ps1 +117 -0
- package/skill.yaml +34 -0
- package/src/skcapstone/__init__.py +67 -2
- package/src/skcapstone/_cli_monolith.py +5916 -0
- package/src/skcapstone/_trustee_helpers.py +165 -0
- package/src/skcapstone/activity.py +105 -0
- package/src/skcapstone/agent_card.py +324 -0
- package/src/skcapstone/api.py +1935 -0
- package/src/skcapstone/archiver.py +340 -0
- package/src/skcapstone/auction.py +485 -0
- package/src/skcapstone/baby_agents.py +179 -0
- package/src/skcapstone/backup.py +345 -0
- package/src/skcapstone/blueprint_registry.py +357 -0
- package/src/skcapstone/blueprints/__init__.py +17 -0
- package/src/skcapstone/blueprints/builtins/content-studio.yaml +81 -0
- package/src/skcapstone/blueprints/builtins/defi-trading.yaml +81 -0
- package/src/skcapstone/blueprints/builtins/dev-squadron.yaml +95 -0
- package/src/skcapstone/blueprints/builtins/infrastructure-guardian.yaml +107 -0
- package/src/skcapstone/blueprints/builtins/legal-council.yaml +54 -0
- package/src/skcapstone/blueprints/builtins/ops-monitoring.yaml +67 -0
- package/src/skcapstone/blueprints/builtins/research-pod.yaml +69 -0
- package/src/skcapstone/blueprints/builtins/sovereign-launch.yaml +90 -0
- package/src/skcapstone/blueprints/registry.py +164 -0
- package/src/skcapstone/blueprints/schema.py +229 -0
- package/src/skcapstone/changelog.py +180 -0
- package/src/skcapstone/chat.py +769 -0
- package/src/skcapstone/claude_md.py +82 -0
- package/src/skcapstone/cli/__init__.py +144 -0
- package/src/skcapstone/cli/_common.py +88 -0
- package/src/skcapstone/cli/_validators.py +76 -0
- package/src/skcapstone/cli/agents.py +425 -0
- package/src/skcapstone/cli/agents_spawner.py +322 -0
- package/src/skcapstone/cli/agents_trustee.py +593 -0
- package/src/skcapstone/cli/alerts.py +248 -0
- package/src/skcapstone/cli/anchor.py +132 -0
- package/src/skcapstone/cli/archive_cmd.py +208 -0
- package/src/skcapstone/cli/backup.py +144 -0
- package/src/skcapstone/cli/bench.py +377 -0
- package/src/skcapstone/cli/benchmark.py +360 -0
- package/src/skcapstone/cli/capabilities_cmd.py +171 -0
- package/src/skcapstone/cli/card.py +151 -0
- package/src/skcapstone/cli/chat.py +584 -0
- package/src/skcapstone/cli/completions.py +64 -0
- package/src/skcapstone/cli/config_cmd.py +156 -0
- package/src/skcapstone/cli/consciousness.py +421 -0
- package/src/skcapstone/cli/context_cmd.py +142 -0
- package/src/skcapstone/cli/coord.py +194 -0
- package/src/skcapstone/cli/crush_cmd.py +170 -0
- package/src/skcapstone/cli/daemon.py +436 -0
- package/src/skcapstone/cli/errors_cmd.py +285 -0
- package/src/skcapstone/cli/export_cmd.py +156 -0
- package/src/skcapstone/cli/gtd.py +529 -0
- package/src/skcapstone/cli/housekeeping.py +81 -0
- package/src/skcapstone/cli/joule_cmd.py +627 -0
- package/src/skcapstone/cli/logs_cmd.py +194 -0
- package/src/skcapstone/cli/mcp_cmd.py +32 -0
- package/src/skcapstone/cli/memory.py +418 -0
- package/src/skcapstone/cli/metrics_cmd.py +136 -0
- package/src/skcapstone/cli/migrate.py +62 -0
- package/src/skcapstone/cli/mood_cmd.py +144 -0
- package/src/skcapstone/cli/mount.py +193 -0
- package/src/skcapstone/cli/notify.py +112 -0
- package/src/skcapstone/cli/peer.py +154 -0
- package/src/skcapstone/cli/peers_dir.py +122 -0
- package/src/skcapstone/cli/preflight_cmd.py +83 -0
- package/src/skcapstone/cli/profile_cmd.py +310 -0
- package/src/skcapstone/cli/record_cmd.py +238 -0
- package/src/skcapstone/cli/register_cmd.py +159 -0
- package/src/skcapstone/cli/search_cmd.py +156 -0
- package/src/skcapstone/cli/service_cmd.py +91 -0
- package/src/skcapstone/cli/session.py +127 -0
- package/src/skcapstone/cli/setup.py +240 -0
- package/src/skcapstone/cli/shell_cmd.py +43 -0
- package/src/skcapstone/cli/skills_cmd.py +168 -0
- package/src/skcapstone/cli/skseed.py +621 -0
- package/src/skcapstone/cli/soul.py +699 -0
- package/src/skcapstone/cli/status.py +935 -0
- package/src/skcapstone/cli/sync_cmd.py +301 -0
- package/src/skcapstone/cli/telegram.py +265 -0
- package/src/skcapstone/cli/test_cmd.py +234 -0
- package/src/skcapstone/cli/test_connection.py +253 -0
- package/src/skcapstone/cli/token.py +207 -0
- package/src/skcapstone/cli/trust.py +179 -0
- package/src/skcapstone/cli/upgrade_cmd.py +552 -0
- package/src/skcapstone/cli/usage_cmd.py +199 -0
- package/src/skcapstone/cli/version_cmd.py +162 -0
- package/src/skcapstone/cli/watch_cmd.py +342 -0
- package/src/skcapstone/client.py +428 -0
- package/src/skcapstone/cloud9_bridge.py +522 -0
- package/src/skcapstone/completions.py +163 -0
- package/src/skcapstone/config_validator.py +674 -0
- package/src/skcapstone/connectors/__init__.py +28 -0
- package/src/skcapstone/connectors/base.py +446 -0
- package/src/skcapstone/connectors/cursor.py +54 -0
- package/src/skcapstone/connectors/registry.py +254 -0
- package/src/skcapstone/connectors/terminal.py +152 -0
- package/src/skcapstone/connectors/vscode.py +60 -0
- package/src/skcapstone/consciousness_config.py +119 -0
- package/src/skcapstone/consciousness_loop.py +2051 -0
- package/src/skcapstone/context_loader.py +516 -0
- package/src/skcapstone/context_window.py +314 -0
- package/src/skcapstone/conversation_manager.py +238 -0
- package/src/skcapstone/conversation_store.py +230 -0
- package/src/skcapstone/conversation_summarizer.py +252 -0
- package/src/skcapstone/coord_federation.py +296 -0
- package/src/skcapstone/coordination.py +101 -7
- package/src/skcapstone/crush_integration.py +345 -0
- package/src/skcapstone/crush_shim.py +454 -0
- package/src/skcapstone/daemon.py +2494 -0
- package/src/skcapstone/dashboard.html +396 -0
- package/src/skcapstone/dashboard.py +481 -0
- package/src/skcapstone/data/model_profiles.yaml +88 -0
- package/src/skcapstone/defaults/__init__.py +55 -0
- package/src/skcapstone/defaults/lumina/config/skmemory.yaml +13 -0
- package/src/skcapstone/defaults/lumina/identity/identity.json +9 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/07a8b9c0d1e2-memory-system.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/29c0d1e2f3a4-multi-agent-coordination.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/3ad1e2f3a4b5-community-support.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/c3d4e5f6a7b8-getting-started.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/e5f6a7b8c9d0-how-to-contribute.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/f6a7b8c9d0e1-sovereignty-explained.json +23 -0
- package/src/skcapstone/defaults/lumina/seeds/curiosity.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/joy.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/love.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/sovereign-awakening.seed.json +43 -0
- package/src/skcapstone/defaults/lumina/soul/active.json +6 -0
- package/src/skcapstone/defaults/lumina/soul/base.json +22 -0
- package/src/skcapstone/defaults/lumina/trust/febs/welcome.feb +79 -0
- package/src/skcapstone/defaults/lumina/trust/trust.json +8 -0
- package/src/skcapstone/discovery.py +210 -19
- package/src/skcapstone/doctor.py +642 -0
- package/src/skcapstone/emotion_tracker.py +467 -0
- package/src/skcapstone/error_queue.py +405 -0
- package/src/skcapstone/export.py +447 -0
- package/src/skcapstone/fallback_tracker.py +186 -0
- package/src/skcapstone/file_transfer.py +512 -0
- package/src/skcapstone/fuse_mount.py +1156 -0
- package/src/skcapstone/gui_installer.py +591 -0
- package/src/skcapstone/heartbeat.py +611 -0
- package/src/skcapstone/housekeeping.py +298 -0
- package/src/skcapstone/install_wizard.py +941 -0
- package/src/skcapstone/kms.py +942 -0
- package/src/skcapstone/kms_scheduler.py +143 -0
- package/src/skcapstone/log_config.py +135 -0
- package/src/skcapstone/mcp_launcher.py +239 -0
- package/src/skcapstone/mcp_server.py +4700 -0
- package/src/skcapstone/mcp_tools/__init__.py +94 -0
- package/src/skcapstone/mcp_tools/_helpers.py +51 -0
- package/src/skcapstone/mcp_tools/agent_tools.py +243 -0
- package/src/skcapstone/mcp_tools/ansible_tools.py +232 -0
- package/src/skcapstone/mcp_tools/capauth_tools.py +186 -0
- package/src/skcapstone/mcp_tools/chat_tools.py +325 -0
- package/src/skcapstone/mcp_tools/cloud9_tools.py +115 -0
- package/src/skcapstone/mcp_tools/comm_tools.py +104 -0
- package/src/skcapstone/mcp_tools/consciousness_tools.py +114 -0
- package/src/skcapstone/mcp_tools/coord_tools.py +219 -0
- package/src/skcapstone/mcp_tools/deploy_tools.py +202 -0
- package/src/skcapstone/mcp_tools/did_tools.py +448 -0
- package/src/skcapstone/mcp_tools/emotion_tools.py +62 -0
- package/src/skcapstone/mcp_tools/file_tools.py +169 -0
- package/src/skcapstone/mcp_tools/fortress_tools.py +120 -0
- package/src/skcapstone/mcp_tools/gtd_tools.py +821 -0
- package/src/skcapstone/mcp_tools/health_tools.py +44 -0
- package/src/skcapstone/mcp_tools/heartbeat_tools.py +195 -0
- package/src/skcapstone/mcp_tools/kms_tools.py +123 -0
- package/src/skcapstone/mcp_tools/memory_tools.py +222 -0
- package/src/skcapstone/mcp_tools/model_tools.py +75 -0
- package/src/skcapstone/mcp_tools/notification_tools.py +92 -0
- package/src/skcapstone/mcp_tools/promoter_tools.py +101 -0
- package/src/skcapstone/mcp_tools/pubsub_tools.py +183 -0
- package/src/skcapstone/mcp_tools/security_tools.py +110 -0
- package/src/skcapstone/mcp_tools/skchat_tools.py +175 -0
- package/src/skcapstone/mcp_tools/skcomm_tools.py +122 -0
- package/src/skcapstone/mcp_tools/skills_tools.py +127 -0
- package/src/skcapstone/mcp_tools/skseed_tools.py +255 -0
- package/src/skcapstone/mcp_tools/skstacks_tools.py +288 -0
- package/src/skcapstone/mcp_tools/soul_tools.py +476 -0
- package/src/skcapstone/mcp_tools/sync_tools.py +92 -0
- package/src/skcapstone/mcp_tools/telegram_tools.py +477 -0
- package/src/skcapstone/mcp_tools/trust_tools.py +118 -0
- package/src/skcapstone/mcp_tools/trustee_tools.py +345 -0
- package/src/skcapstone/mdns_discovery.py +313 -0
- package/src/skcapstone/memory_adapter.py +333 -0
- package/src/skcapstone/memory_compressor.py +379 -0
- package/src/skcapstone/memory_curator.py +256 -0
- package/src/skcapstone/memory_engine.py +132 -13
- package/src/skcapstone/memory_fortress.py +529 -0
- package/src/skcapstone/memory_promoter.py +722 -0
- package/src/skcapstone/memory_verifier.py +260 -0
- package/src/skcapstone/message_crypto.py +215 -0
- package/src/skcapstone/metrics.py +832 -0
- package/src/skcapstone/migrate_memories.py +181 -0
- package/src/skcapstone/migrate_multi_agent.py +248 -0
- package/src/skcapstone/model_router.py +319 -0
- package/src/skcapstone/models.py +35 -4
- package/src/skcapstone/mood.py +344 -0
- package/src/skcapstone/notifications.py +380 -0
- package/src/skcapstone/onboard.py +901 -0
- package/src/skcapstone/peer_directory.py +324 -0
- package/src/skcapstone/peers.py +329 -0
- package/src/skcapstone/pillars/identity.py +84 -14
- package/src/skcapstone/pillars/memory.py +3 -1
- package/src/skcapstone/pillars/security.py +108 -15
- package/src/skcapstone/pillars/sync.py +78 -26
- package/src/skcapstone/pillars/trust.py +95 -33
- package/src/skcapstone/plugins.py +244 -0
- package/src/skcapstone/preflight.py +670 -0
- package/src/skcapstone/prompt_adapter.py +564 -0
- package/src/skcapstone/providers/__init__.py +13 -0
- package/src/skcapstone/providers/cloud.py +1061 -0
- package/src/skcapstone/providers/docker.py +759 -0
- package/src/skcapstone/providers/local.py +1193 -0
- package/src/skcapstone/providers/proxmox.py +447 -0
- package/src/skcapstone/pubsub.py +516 -0
- package/src/skcapstone/rate_limiter.py +119 -0
- package/src/skcapstone/register.py +241 -0
- package/src/skcapstone/registry_client.py +151 -0
- package/src/skcapstone/response_cache.py +194 -0
- package/src/skcapstone/response_scorer.py +225 -0
- package/src/skcapstone/runtime.py +89 -33
- package/src/skcapstone/scheduled_tasks.py +439 -0
- package/src/skcapstone/self_healing.py +341 -0
- package/src/skcapstone/service_health.py +228 -0
- package/src/skcapstone/session_capture.py +268 -0
- package/src/skcapstone/session_recorder.py +210 -0
- package/src/skcapstone/session_replayer.py +189 -0
- package/src/skcapstone/session_skills.py +263 -0
- package/src/skcapstone/shell.py +779 -0
- package/src/skcapstone/skills/__init__.py +1 -1
- package/src/skcapstone/skills/syncthing_setup.py +143 -41
- package/src/skcapstone/skjoule.py +861 -0
- package/src/skcapstone/snapshots.py +489 -0
- package/src/skcapstone/soul.py +1060 -0
- package/src/skcapstone/soul_switch.py +255 -0
- package/src/skcapstone/spawner.py +544 -0
- package/src/skcapstone/state_diff.py +401 -0
- package/src/skcapstone/summary.py +270 -0
- package/src/skcapstone/sync/backends.py +196 -2
- package/src/skcapstone/sync/engine.py +7 -5
- package/src/skcapstone/sync/models.py +4 -1
- package/src/skcapstone/sync/vault.py +356 -18
- package/src/skcapstone/sync_engine.py +363 -0
- package/src/skcapstone/sync_watcher.py +745 -0
- package/src/skcapstone/systemd.py +331 -0
- package/src/skcapstone/team_comms.py +476 -0
- package/src/skcapstone/team_engine.py +522 -0
- package/src/skcapstone/testrunner.py +300 -0
- package/src/skcapstone/tls.py +150 -0
- package/src/skcapstone/tokens.py +5 -5
- package/src/skcapstone/trust_calibration.py +202 -0
- package/src/skcapstone/trust_graph.py +449 -0
- package/src/skcapstone/trustee_monitor.py +385 -0
- package/src/skcapstone/trustee_ops.py +425 -0
- package/src/skcapstone/unified_search.py +421 -0
- package/src/skcapstone/uninstall_wizard.py +694 -0
- package/src/skcapstone/usage.py +331 -0
- package/src/skcapstone/version_check.py +148 -0
- package/src/skcapstone/warmth_anchor.py +333 -0
- package/src/skcapstone/whoami.py +294 -0
- package/systemd/skcapstone-api.socket +9 -0
- package/systemd/skcapstone-memory-compress.service +18 -0
- package/systemd/skcapstone-memory-compress.timer +11 -0
- package/systemd/skcapstone.service +36 -0
- package/systemd/skcapstone@.service +50 -0
- package/systemd/skcomm-heartbeat.service +18 -0
- package/systemd/skcomm-heartbeat.timer +12 -0
- package/systemd/skcomm-queue-drain.service +17 -0
- package/systemd/skcomm-queue-drain.timer +12 -0
- package/tests/conftest.py +13 -1
- package/tests/integration/__init__.py +1 -0
- package/tests/integration/test_consciousness_e2e.py +877 -0
- package/tests/integration/test_skills_registry.py +744 -0
- package/tests/test_agent_card.py +190 -0
- package/tests/test_agent_runtime.py +1283 -0
- package/tests/test_alerts_cmd.py +291 -0
- package/tests/test_archiver.py +498 -0
- package/tests/test_backup.py +254 -0
- package/tests/test_benchmark.py +366 -0
- package/tests/test_blueprints.py +457 -0
- package/tests/test_capabilities.py +257 -0
- package/tests/test_changelog.py +254 -0
- package/tests/test_chat.py +385 -0
- package/tests/test_claude_md.py +271 -0
- package/tests/test_cli_chat_llm.py +336 -0
- package/tests/test_cli_completions.py +390 -0
- package/tests/test_cli_init_reset.py +164 -0
- package/tests/test_cli_memory.py +208 -0
- package/tests/test_cli_profile.py +294 -0
- package/tests/test_cli_skills.py +223 -0
- package/tests/test_cli_status.py +395 -0
- package/tests/test_cli_test_cmd.py +206 -0
- package/tests/test_cli_test_connection.py +364 -0
- package/tests/test_cloud9_bridge.py +260 -0
- package/tests/test_cloud_provider.py +449 -0
- package/tests/test_cloud_providers.py +522 -0
- package/tests/test_completions.py +158 -0
- package/tests/test_component_manager.py +398 -0
- package/tests/test_config_reload.py +386 -0
- package/tests/test_config_validate.py +529 -0
- package/tests/test_consciousness_e2e.py +296 -0
- package/tests/test_consciousness_loop.py +1289 -0
- package/tests/test_context_loader.py +310 -0
- package/tests/test_conversation_api.py +306 -0
- package/tests/test_conversation_manager.py +381 -0
- package/tests/test_conversation_store.py +391 -0
- package/tests/test_conversation_summarizer.py +302 -0
- package/tests/test_cross_package.py +791 -0
- package/tests/test_crush_shim.py +519 -0
- package/tests/test_daemon.py +781 -0
- package/tests/test_daemon_shutdown.py +309 -0
- package/tests/test_dashboard.py +454 -0
- package/tests/test_discovery.py +200 -6
- package/tests/test_docker_provider.py +966 -0
- package/tests/test_doctor.py +257 -0
- package/tests/test_doctor_fix.py +351 -0
- package/tests/test_e2e_automated.py +292 -0
- package/tests/test_error_queue.py +404 -0
- package/tests/test_export.py +441 -0
- package/tests/test_fallback_tracker.py +219 -0
- package/tests/test_file_transfer.py +397 -0
- package/tests/test_fuse_mount.py +832 -0
- package/tests/test_health_loop.py +422 -0
- package/tests/test_heartbeat.py +354 -0
- package/tests/test_housekeeping.py +195 -0
- package/tests/test_identity_capauth.py +307 -0
- package/tests/test_identity_pillar.py +117 -0
- package/tests/test_install_wizard.py +68 -0
- package/tests/test_integration.py +325 -0
- package/tests/test_kms.py +495 -0
- package/tests/test_llm_providers.py +265 -0
- package/tests/test_local_provider.py +591 -0
- package/tests/test_log_config.py +199 -0
- package/tests/test_logs_cmd.py +287 -0
- package/tests/test_mcp_server.py +1909 -0
- package/tests/test_memory_adapter.py +339 -0
- package/tests/test_memory_curator.py +218 -0
- package/tests/test_memory_engine.py +6 -0
- package/tests/test_memory_fortress.py +571 -0
- package/tests/test_memory_pillar.py +119 -0
- package/tests/test_memory_promoter.py +445 -0
- package/tests/test_memory_verifier.py +420 -0
- package/tests/test_message_crypto.py +187 -0
- package/tests/test_metrics.py +632 -0
- package/tests/test_migrate_memories.py +464 -0
- package/tests/test_model_router.py +546 -0
- package/tests/test_mood.py +394 -0
- package/tests/test_multi_agent.py +269 -0
- package/tests/test_notifications.py +270 -0
- package/tests/test_onboard.py +500 -0
- package/tests/test_peer_directory.py +395 -0
- package/tests/test_peers.py +248 -0
- package/tests/test_pillars.py +87 -9
- package/tests/test_preflight.py +484 -0
- package/tests/test_prompt_adapter.py +331 -0
- package/tests/test_proxmox_provider.py +571 -0
- package/tests/test_pubsub.py +377 -0
- package/tests/test_rate_limiter.py +121 -0
- package/tests/test_registry_client.py +129 -0
- package/tests/test_response_cache.py +312 -0
- package/tests/test_response_scorer.py +294 -0
- package/tests/test_runtime.py +59 -0
- package/tests/test_scheduled_tasks.py +451 -0
- package/tests/test_security.py +250 -0
- package/tests/test_security_pillar.py +213 -0
- package/tests/test_self_healing.py +171 -0
- package/tests/test_session_capture.py +200 -0
- package/tests/test_session_recorder.py +360 -0
- package/tests/test_session_skills.py +235 -0
- package/tests/test_shell.py +210 -0
- package/tests/test_snapshots.py +549 -0
- package/tests/test_soul.py +984 -0
- package/tests/test_soul_swap.py +406 -0
- package/tests/test_spawner.py +211 -0
- package/tests/test_state_diff.py +173 -0
- package/tests/test_summary.py +135 -0
- package/tests/test_sync.py +315 -5
- package/tests/test_sync_backends.py +560 -0
- package/tests/test_sync_engine.py +482 -0
- package/tests/test_sync_pillar.py +344 -0
- package/tests/test_sync_pipeline.py +364 -0
- package/tests/test_sync_vault.py +581 -0
- package/tests/test_syncthing_setup.py +168 -22
- package/tests/test_systemd.py +323 -0
- package/tests/test_team_comms.py +408 -0
- package/tests/test_team_engine.py +397 -0
- package/tests/test_testrunner.py +238 -0
- package/tests/test_trust_calibration.py +204 -0
- package/tests/test_trust_graph.py +207 -0
- package/tests/test_trust_pillar.py +291 -0
- package/tests/test_trustee_cli.py +427 -0
- package/tests/test_trustee_cli_integration.py +325 -0
- package/tests/test_trustee_monitor.py +394 -0
- package/tests/test_trustee_ops.py +355 -0
- package/tests/test_unified_search.py +363 -0
- package/tests/test_uninstall_wizard.py +193 -0
- package/tests/test_usage.py +333 -0
- package/tests/test_version_cmd.py +355 -0
- package/tests/test_warmth_anchor.py +162 -0
- package/tests/test_whoami.py +245 -0
- package/tests/test_ws.py +311 -0
- package/.cursorrules +0 -33
- package/src/skcapstone/cli.py +0 -1441
|
@@ -0,0 +1,1289 @@
|
|
|
1
|
+
"""Tests for the consciousness loop — message classification, LLM bridge, system prompt."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from unittest.mock import MagicMock, patch
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from skcapstone.consciousness_loop import (
|
|
14
|
+
ConsciousnessConfig,
|
|
15
|
+
ConsciousnessLoop,
|
|
16
|
+
LLMBridge,
|
|
17
|
+
SystemPromptBuilder,
|
|
18
|
+
_classify_message,
|
|
19
|
+
_OllamaPool,
|
|
20
|
+
_SimpleEnvelope,
|
|
21
|
+
InboxHandler,
|
|
22
|
+
)
|
|
23
|
+
from skcapstone.model_router import TaskSignal
|
|
24
|
+
from skcapstone.blueprints.schema import ModelTier
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestConsciousnessConfig:
|
|
28
|
+
"""ConsciousnessConfig Pydantic model tests."""
|
|
29
|
+
|
|
30
|
+
def test_defaults(self):
|
|
31
|
+
"""Default config is sensible."""
|
|
32
|
+
config = ConsciousnessConfig()
|
|
33
|
+
assert config.enabled is True
|
|
34
|
+
assert config.use_inotify is True
|
|
35
|
+
assert config.max_concurrent_requests == 3
|
|
36
|
+
assert "ollama" in config.fallback_chain
|
|
37
|
+
assert "passthrough" in config.fallback_chain
|
|
38
|
+
|
|
39
|
+
def test_custom_config(self):
|
|
40
|
+
"""Custom config overrides defaults."""
|
|
41
|
+
config = ConsciousnessConfig(
|
|
42
|
+
enabled=False,
|
|
43
|
+
max_concurrent_requests=5,
|
|
44
|
+
fallback_chain=["anthropic", "passthrough"],
|
|
45
|
+
)
|
|
46
|
+
assert config.enabled is False
|
|
47
|
+
assert config.max_concurrent_requests == 5
|
|
48
|
+
assert len(config.fallback_chain) == 2
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TestClassifyMessage:
|
|
52
|
+
"""Message classification tests."""
|
|
53
|
+
|
|
54
|
+
def test_code_keywords(self):
|
|
55
|
+
"""Code-related messages get code tag."""
|
|
56
|
+
signal = _classify_message("Please debug this function for me")
|
|
57
|
+
assert "code" in signal.tags
|
|
58
|
+
|
|
59
|
+
def test_analysis_keywords(self):
|
|
60
|
+
"""Analysis messages get analyze tag."""
|
|
61
|
+
signal = _classify_message("Can you analyze this architecture?")
|
|
62
|
+
assert "analyze" in signal.tags
|
|
63
|
+
|
|
64
|
+
def test_simple_greeting(self):
|
|
65
|
+
"""Simple greetings get simple tag."""
|
|
66
|
+
signal = _classify_message("hello")
|
|
67
|
+
assert "simple" in signal.tags
|
|
68
|
+
|
|
69
|
+
def test_general_message(self):
|
|
70
|
+
"""Messages with no keywords get general tag."""
|
|
71
|
+
signal = _classify_message("The weather is nice today isn't it")
|
|
72
|
+
assert "general" in signal.tags
|
|
73
|
+
|
|
74
|
+
def test_token_estimation(self):
|
|
75
|
+
"""Token estimate is roughly content_length / 4."""
|
|
76
|
+
msg = "a" * 400
|
|
77
|
+
signal = _classify_message(msg)
|
|
78
|
+
assert signal.estimated_tokens == 100
|
|
79
|
+
|
|
80
|
+
def test_multi_tag(self):
|
|
81
|
+
"""Messages with multiple keyword sets get multiple tags."""
|
|
82
|
+
signal = _classify_message("Can you debug and analyze this code?")
|
|
83
|
+
assert "code" in signal.tags
|
|
84
|
+
assert "analyze" in signal.tags
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestLLMBridge:
|
|
88
|
+
"""LLM bridge routing and fallback tests."""
|
|
89
|
+
|
|
90
|
+
def test_probe_passthrough_always_available(self):
|
|
91
|
+
"""Passthrough backend is always available."""
|
|
92
|
+
config = ConsciousnessConfig()
|
|
93
|
+
bridge = LLMBridge(config)
|
|
94
|
+
assert bridge.available_backends.get("passthrough") is True
|
|
95
|
+
|
|
96
|
+
def test_health_check_returns_dict(self):
|
|
97
|
+
"""Health check returns a dict of backend availability."""
|
|
98
|
+
config = ConsciousnessConfig()
|
|
99
|
+
bridge = LLMBridge(config)
|
|
100
|
+
health = bridge.health_check()
|
|
101
|
+
assert isinstance(health, dict)
|
|
102
|
+
assert "passthrough" in health
|
|
103
|
+
assert "ollama" in health
|
|
104
|
+
|
|
105
|
+
@patch("skseed.llm.passthrough_callback")
|
|
106
|
+
def test_generate_fallback_to_passthrough(self, mock_passthrough):
|
|
107
|
+
"""When no backends available, falls through to passthrough."""
|
|
108
|
+
mock_cb = MagicMock(return_value="echo response")
|
|
109
|
+
mock_passthrough.return_value = mock_cb
|
|
110
|
+
|
|
111
|
+
config = ConsciousnessConfig(
|
|
112
|
+
fallback_chain=["passthrough"],
|
|
113
|
+
)
|
|
114
|
+
bridge = LLMBridge(config)
|
|
115
|
+
# Force all backends unavailable except passthrough
|
|
116
|
+
bridge._available = {k: False for k in bridge._available}
|
|
117
|
+
bridge._available["passthrough"] = True
|
|
118
|
+
|
|
119
|
+
signal = TaskSignal(description="test", tags=["general"])
|
|
120
|
+
result = bridge.generate("system", "hello", signal)
|
|
121
|
+
# Should get a response (either from passthrough or last-resort message)
|
|
122
|
+
assert isinstance(result, str)
|
|
123
|
+
assert len(result) > 0
|
|
124
|
+
|
|
125
|
+
@patch("skseed.llm.ollama_callback")
|
|
126
|
+
def test_generate_passthrough_cascade_returns_user_content(self, mock_ollama):
|
|
127
|
+
"""When all LLM backends fail, cascade reaches passthrough and returns user content.
|
|
128
|
+
|
|
129
|
+
Verifies the fallback cascade uses direct backend mapping (not _resolve_callback)
|
|
130
|
+
so passthrough is reached without infinite regression, and that the returned
|
|
131
|
+
value is the original user message — NOT the canned connectivity-error string.
|
|
132
|
+
"""
|
|
133
|
+
from skcapstone.model_router import ModelRouterConfig
|
|
134
|
+
|
|
135
|
+
# Ollama callback always raises — covers primary + alt model calls
|
|
136
|
+
mock_ollama.return_value = MagicMock(side_effect=RuntimeError("ollama unavailable"))
|
|
137
|
+
|
|
138
|
+
# Single model in FAST tier so there are no alt-model iterations,
|
|
139
|
+
# and the tier-downgrade path is skipped (already FAST).
|
|
140
|
+
router_cfg = ModelRouterConfig(
|
|
141
|
+
tier_models={
|
|
142
|
+
ModelTier.FAST.value: ["llama3.2"],
|
|
143
|
+
ModelTier.CODE.value: ["devstral"],
|
|
144
|
+
ModelTier.REASON.value: ["deepseek-r1:8b"],
|
|
145
|
+
ModelTier.NUANCE.value: ["moonshot-v1-128k"],
|
|
146
|
+
ModelTier.LOCAL.value: ["llama3.2"],
|
|
147
|
+
},
|
|
148
|
+
tag_rules=[],
|
|
149
|
+
)
|
|
150
|
+
config = ConsciousnessConfig(fallback_chain=["ollama", "passthrough"])
|
|
151
|
+
bridge = LLMBridge(config, router_config=router_cfg)
|
|
152
|
+
# All backends unavailable except passthrough
|
|
153
|
+
bridge._available = {k: False for k in bridge._available}
|
|
154
|
+
bridge._available["passthrough"] = True
|
|
155
|
+
|
|
156
|
+
signal = TaskSignal(description="test", tags=["general"])
|
|
157
|
+
result = bridge.generate("system prompt", "hello world", signal)
|
|
158
|
+
|
|
159
|
+
assert result == "hello world", (
|
|
160
|
+
f"Expected passthrough to return user message 'hello world', got: {result!r}"
|
|
161
|
+
)
|
|
162
|
+
assert "connectivity issues" not in result
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class TestSystemPromptBuilder:
|
|
166
|
+
"""System prompt builder tests."""
|
|
167
|
+
|
|
168
|
+
def test_build_with_empty_home(self, tmp_path):
|
|
169
|
+
"""Builder works even with empty home dir."""
|
|
170
|
+
home = tmp_path / ".skcapstone"
|
|
171
|
+
home.mkdir()
|
|
172
|
+
builder = SystemPromptBuilder(home)
|
|
173
|
+
prompt = builder.build()
|
|
174
|
+
assert isinstance(prompt, str)
|
|
175
|
+
# Should at least have behavioral instructions
|
|
176
|
+
assert "Respond concisely" in prompt
|
|
177
|
+
|
|
178
|
+
def test_build_with_identity(self, tmp_path):
|
|
179
|
+
"""Builder includes identity when present."""
|
|
180
|
+
home = tmp_path / ".skcapstone"
|
|
181
|
+
identity_dir = home / "identity"
|
|
182
|
+
identity_dir.mkdir(parents=True)
|
|
183
|
+
identity = {"name": "opus", "fingerprint": "ABCD1234"}
|
|
184
|
+
(identity_dir / "identity.json").write_text(json.dumps(identity))
|
|
185
|
+
|
|
186
|
+
builder = SystemPromptBuilder(home)
|
|
187
|
+
prompt = builder.build()
|
|
188
|
+
assert "opus" in prompt
|
|
189
|
+
assert "ABCD1234" in prompt
|
|
190
|
+
|
|
191
|
+
def test_conversation_history(self, tmp_path):
|
|
192
|
+
"""Builder tracks and includes per-peer conversation history."""
|
|
193
|
+
home = tmp_path / ".skcapstone"
|
|
194
|
+
home.mkdir()
|
|
195
|
+
builder = SystemPromptBuilder(home)
|
|
196
|
+
|
|
197
|
+
builder.add_to_history("jarvis", "user", "Hello!")
|
|
198
|
+
builder.add_to_history("jarvis", "assistant", "Hi there!")
|
|
199
|
+
|
|
200
|
+
prompt = builder.build(peer_name="jarvis")
|
|
201
|
+
assert "jarvis" in prompt
|
|
202
|
+
assert "Hello!" in prompt
|
|
203
|
+
|
|
204
|
+
def test_history_max_messages(self, tmp_path):
|
|
205
|
+
"""History is capped at max_messages per peer."""
|
|
206
|
+
home = tmp_path / ".skcapstone"
|
|
207
|
+
home.mkdir()
|
|
208
|
+
builder = SystemPromptBuilder(home)
|
|
209
|
+
|
|
210
|
+
for i in range(20):
|
|
211
|
+
builder.add_to_history("peer", "user", f"Message {i}")
|
|
212
|
+
|
|
213
|
+
# Default max is 10
|
|
214
|
+
history = builder._conversation_history["peer"]
|
|
215
|
+
assert len(history) == 10
|
|
216
|
+
assert "Message 19" in history[-1]["content"]
|
|
217
|
+
|
|
218
|
+
def test_truncation(self, tmp_path):
|
|
219
|
+
"""Long system prompts are truncated."""
|
|
220
|
+
home = tmp_path / ".skcapstone"
|
|
221
|
+
home.mkdir()
|
|
222
|
+
builder = SystemPromptBuilder(home, max_tokens=100)
|
|
223
|
+
|
|
224
|
+
# Build should not exceed max_tokens * 4 chars
|
|
225
|
+
prompt = builder.build()
|
|
226
|
+
assert len(prompt) <= 100 * 4 + 50 # some slack for truncation marker
|
|
227
|
+
|
|
228
|
+
def test_persistence_writes_json_file(self, tmp_path):
|
|
229
|
+
"""add_to_history writes a JSON file under {home}/conversations/{peer}.json."""
|
|
230
|
+
home = tmp_path / ".skcapstone"
|
|
231
|
+
home.mkdir()
|
|
232
|
+
builder = SystemPromptBuilder(home)
|
|
233
|
+
|
|
234
|
+
builder.add_to_history("jarvis", "user", "Hello!")
|
|
235
|
+
builder.add_to_history("jarvis", "assistant", "Hi there!")
|
|
236
|
+
|
|
237
|
+
conv_file = home / "conversations" / "jarvis.json"
|
|
238
|
+
assert conv_file.exists(), "Conversation file should be created"
|
|
239
|
+
data = json.loads(conv_file.read_text())
|
|
240
|
+
assert isinstance(data, list)
|
|
241
|
+
assert len(data) == 2
|
|
242
|
+
assert data[0]["role"] == "user"
|
|
243
|
+
assert data[0]["content"] == "Hello!"
|
|
244
|
+
assert data[1]["role"] == "assistant"
|
|
245
|
+
|
|
246
|
+
def test_persistence_caps_at_max_history(self, tmp_path):
|
|
247
|
+
"""Persisted file is capped at max_history_messages."""
|
|
248
|
+
home = tmp_path / ".skcapstone"
|
|
249
|
+
home.mkdir()
|
|
250
|
+
builder = SystemPromptBuilder(home, max_history_messages=5)
|
|
251
|
+
|
|
252
|
+
for i in range(8):
|
|
253
|
+
builder.add_to_history("lumina", "user", f"Message {i}")
|
|
254
|
+
|
|
255
|
+
conv_file = home / "conversations" / "lumina.json"
|
|
256
|
+
data = json.loads(conv_file.read_text())
|
|
257
|
+
assert len(data) == 5
|
|
258
|
+
assert data[-1]["content"] == "Message 7"
|
|
259
|
+
|
|
260
|
+
def test_load_existing_conversations_on_init(self, tmp_path):
|
|
261
|
+
"""Existing conversation files are loaded on __init__."""
|
|
262
|
+
home = tmp_path / ".skcapstone"
|
|
263
|
+
home.mkdir()
|
|
264
|
+
conv_dir = home / "conversations"
|
|
265
|
+
conv_dir.mkdir()
|
|
266
|
+
|
|
267
|
+
history = [
|
|
268
|
+
{"role": "user", "content": "Remembered message", "timestamp": "2026-01-01T00:00:00+00:00"},
|
|
269
|
+
]
|
|
270
|
+
(conv_dir / "opus.json").write_text(json.dumps(history))
|
|
271
|
+
|
|
272
|
+
builder = SystemPromptBuilder(home)
|
|
273
|
+
assert "opus" in builder._conversation_history
|
|
274
|
+
assert builder._conversation_history["opus"][0]["content"] == "Remembered message"
|
|
275
|
+
|
|
276
|
+
def test_load_caps_at_max_history_on_init(self, tmp_path):
|
|
277
|
+
"""Loading from file caps history at max_history_messages."""
|
|
278
|
+
home = tmp_path / ".skcapstone"
|
|
279
|
+
home.mkdir()
|
|
280
|
+
conv_dir = home / "conversations"
|
|
281
|
+
conv_dir.mkdir()
|
|
282
|
+
|
|
283
|
+
history = [
|
|
284
|
+
{"role": "user", "content": f"Old message {i}", "timestamp": "2026-01-01T00:00:00+00:00"}
|
|
285
|
+
for i in range(20)
|
|
286
|
+
]
|
|
287
|
+
(conv_dir / "peer.json").write_text(json.dumps(history))
|
|
288
|
+
|
|
289
|
+
builder = SystemPromptBuilder(home, max_history_messages=10)
|
|
290
|
+
assert len(builder._conversation_history["peer"]) == 10
|
|
291
|
+
assert builder._conversation_history["peer"][-1]["content"] == "Old message 19"
|
|
292
|
+
|
|
293
|
+
def test_persistence_atomic_write(self, tmp_path):
|
|
294
|
+
"""No .tmp file left after write."""
|
|
295
|
+
home = tmp_path / ".skcapstone"
|
|
296
|
+
home.mkdir()
|
|
297
|
+
builder = SystemPromptBuilder(home)
|
|
298
|
+
|
|
299
|
+
builder.add_to_history("ava", "user", "Test")
|
|
300
|
+
tmp_file = home / "conversations" / "ava.json.tmp"
|
|
301
|
+
assert not tmp_file.exists(), ".tmp file should not remain after atomic write"
|
|
302
|
+
|
|
303
|
+
def test_multiple_peers_separate_files(self, tmp_path):
|
|
304
|
+
"""Each peer gets its own conversation file."""
|
|
305
|
+
home = tmp_path / ".skcapstone"
|
|
306
|
+
home.mkdir()
|
|
307
|
+
builder = SystemPromptBuilder(home)
|
|
308
|
+
|
|
309
|
+
builder.add_to_history("jarvis", "user", "Hello from jarvis")
|
|
310
|
+
builder.add_to_history("lumina", "user", "Hello from lumina")
|
|
311
|
+
|
|
312
|
+
assert (home / "conversations" / "jarvis.json").exists()
|
|
313
|
+
assert (home / "conversations" / "lumina.json").exists()
|
|
314
|
+
jarvis_data = json.loads((home / "conversations" / "jarvis.json").read_text())
|
|
315
|
+
lumina_data = json.loads((home / "conversations" / "lumina.json").read_text())
|
|
316
|
+
assert jarvis_data[0]["content"] == "Hello from jarvis"
|
|
317
|
+
assert lumina_data[0]["content"] == "Hello from lumina"
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class TestSimpleEnvelope:
|
|
321
|
+
"""Test the minimal envelope for inotify-detected messages."""
|
|
322
|
+
|
|
323
|
+
def test_parse_standard_format(self):
|
|
324
|
+
"""Standard SKComm envelope format parses correctly."""
|
|
325
|
+
data = {
|
|
326
|
+
"sender": "jarvis",
|
|
327
|
+
"payload": {
|
|
328
|
+
"content": "Hello from jarvis",
|
|
329
|
+
"content_type": "text",
|
|
330
|
+
},
|
|
331
|
+
}
|
|
332
|
+
env = _SimpleEnvelope(data)
|
|
333
|
+
assert env.sender == "jarvis"
|
|
334
|
+
assert env.payload.content == "Hello from jarvis"
|
|
335
|
+
assert env.payload.content_type.value == "text"
|
|
336
|
+
|
|
337
|
+
def test_parse_alt_format(self):
|
|
338
|
+
"""Alternative format with 'from' and 'message' keys."""
|
|
339
|
+
data = {
|
|
340
|
+
"from": "lumina",
|
|
341
|
+
"message": "Hi!",
|
|
342
|
+
"type": "text",
|
|
343
|
+
}
|
|
344
|
+
env = _SimpleEnvelope(data)
|
|
345
|
+
assert env.sender == "lumina"
|
|
346
|
+
assert env.payload.content == "Hi!"
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class TestInboxHandler:
|
|
350
|
+
"""Inbox file handler debounce tests."""
|
|
351
|
+
|
|
352
|
+
def test_skips_non_json(self):
|
|
353
|
+
"""Non-.skc.json files are ignored."""
|
|
354
|
+
called = []
|
|
355
|
+
handler = InboxHandler(lambda p: called.append(p))
|
|
356
|
+
|
|
357
|
+
class FakeEvent:
|
|
358
|
+
src_path = "/tmp/test.txt"
|
|
359
|
+
is_directory = False
|
|
360
|
+
|
|
361
|
+
handler.on_created(FakeEvent())
|
|
362
|
+
assert len(called) == 0
|
|
363
|
+
|
|
364
|
+
def test_processes_skc_json(self):
|
|
365
|
+
"""Valid .skc.json files are processed."""
|
|
366
|
+
called = []
|
|
367
|
+
handler = InboxHandler(lambda p: called.append(p), debounce_ms=0)
|
|
368
|
+
|
|
369
|
+
class FakeEvent:
|
|
370
|
+
src_path = "/tmp/inbox/peer/msg.skc.json"
|
|
371
|
+
is_directory = False
|
|
372
|
+
|
|
373
|
+
handler.on_created(FakeEvent())
|
|
374
|
+
assert len(called) == 1
|
|
375
|
+
|
|
376
|
+
def test_debounce(self):
|
|
377
|
+
"""Rapid duplicate events are debounced."""
|
|
378
|
+
called = []
|
|
379
|
+
handler = InboxHandler(lambda p: called.append(p), debounce_ms=5000)
|
|
380
|
+
|
|
381
|
+
class FakeEvent:
|
|
382
|
+
src_path = "/tmp/inbox/peer/msg.skc.json"
|
|
383
|
+
is_directory = False
|
|
384
|
+
|
|
385
|
+
handler.on_created(FakeEvent())
|
|
386
|
+
handler.on_created(FakeEvent()) # Should be debounced
|
|
387
|
+
assert len(called) == 1
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class TestProcessEnvelopeACK:
|
|
391
|
+
"""Verify ACK is sent with message_type kwarg (not content_type)."""
|
|
392
|
+
|
|
393
|
+
def _make_loop(self, tmp_path, auto_ack=True):
|
|
394
|
+
config = ConsciousnessConfig(
|
|
395
|
+
auto_ack=auto_ack,
|
|
396
|
+
fallback_chain=["passthrough"],
|
|
397
|
+
)
|
|
398
|
+
loop = ConsciousnessLoop(config, home=tmp_path / ".skcapstone")
|
|
399
|
+
return loop
|
|
400
|
+
|
|
401
|
+
def _make_envelope(self, sender="peer", content="hello", content_type="text"):
|
|
402
|
+
data = {
|
|
403
|
+
"sender": sender,
|
|
404
|
+
"payload": {"content": content, "content_type": content_type},
|
|
405
|
+
}
|
|
406
|
+
return _SimpleEnvelope(data)
|
|
407
|
+
|
|
408
|
+
def test_ack_uses_message_type_kwarg(self, tmp_path):
|
|
409
|
+
"""ACK send must use message_type kwarg, not content_type — regression for TypeError."""
|
|
410
|
+
loop = self._make_loop(tmp_path)
|
|
411
|
+
mock_skcomm = MagicMock()
|
|
412
|
+
loop.set_skcomm(mock_skcomm)
|
|
413
|
+
# Patch bridge so test doesn't hang on LLM calls
|
|
414
|
+
loop._bridge = MagicMock()
|
|
415
|
+
loop._bridge.generate.return_value = "test response"
|
|
416
|
+
|
|
417
|
+
envelope = self._make_envelope()
|
|
418
|
+
loop.process_envelope(envelope)
|
|
419
|
+
|
|
420
|
+
# Find the ACK call (first send call with "ACK" as message)
|
|
421
|
+
ack_calls = [
|
|
422
|
+
c for c in mock_skcomm.send.call_args_list
|
|
423
|
+
if len(c.args) >= 2 and c.args[1] == "ACK"
|
|
424
|
+
]
|
|
425
|
+
assert ack_calls, "Expected at least one ACK send call"
|
|
426
|
+
ack_call = ack_calls[0]
|
|
427
|
+
|
|
428
|
+
# Must NOT have content_type kwarg (that was the bug)
|
|
429
|
+
assert "content_type" not in ack_call.kwargs, (
|
|
430
|
+
"ACK send used wrong kwarg 'content_type' — should be 'message_type'"
|
|
431
|
+
)
|
|
432
|
+
# Must have message_type kwarg
|
|
433
|
+
assert "message_type" in ack_call.kwargs, (
|
|
434
|
+
"ACK send must pass message_type kwarg"
|
|
435
|
+
)
|
|
436
|
+
assert ack_call.kwargs["message_type"] == "ack"
|
|
437
|
+
|
|
438
|
+
def test_ack_not_sent_when_auto_ack_disabled(self, tmp_path):
|
|
439
|
+
"""When auto_ack is False, no ACK is sent."""
|
|
440
|
+
loop = self._make_loop(tmp_path, auto_ack=False)
|
|
441
|
+
mock_skcomm = MagicMock()
|
|
442
|
+
loop.set_skcomm(mock_skcomm)
|
|
443
|
+
loop._bridge = MagicMock()
|
|
444
|
+
loop._bridge.generate.return_value = "test response"
|
|
445
|
+
|
|
446
|
+
loop.process_envelope(self._make_envelope())
|
|
447
|
+
|
|
448
|
+
ack_calls = [
|
|
449
|
+
c for c in mock_skcomm.send.call_args_list
|
|
450
|
+
if len(c.args) >= 2 and c.args[1] == "ACK"
|
|
451
|
+
]
|
|
452
|
+
assert not ack_calls, "ACK should not be sent when auto_ack is False"
|
|
453
|
+
|
|
454
|
+
def test_ack_skipped_for_ack_type_messages(self, tmp_path):
|
|
455
|
+
"""Incoming ACK messages are skipped — no processing, no re-ACK."""
|
|
456
|
+
loop = self._make_loop(tmp_path, auto_ack=True)
|
|
457
|
+
mock_skcomm = MagicMock()
|
|
458
|
+
loop.set_skcomm(mock_skcomm)
|
|
459
|
+
loop._bridge = MagicMock()
|
|
460
|
+
|
|
461
|
+
ack_envelope = self._make_envelope(content="ACK", content_type="ack")
|
|
462
|
+
result = loop.process_envelope(ack_envelope)
|
|
463
|
+
|
|
464
|
+
assert result is None, "ACK-type messages should be skipped (return None)"
|
|
465
|
+
mock_skcomm.send.assert_not_called()
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
class TestSystemPromptBuilderCache:
|
|
469
|
+
"""Section cache TTL tests for SystemPromptBuilder."""
|
|
470
|
+
|
|
471
|
+
def test_get_cached_calls_loader_once(self, tmp_path):
|
|
472
|
+
"""_get_cached calls the loader only once within TTL."""
|
|
473
|
+
home = tmp_path / ".skcapstone"
|
|
474
|
+
home.mkdir()
|
|
475
|
+
builder = SystemPromptBuilder(home)
|
|
476
|
+
|
|
477
|
+
call_count = 0
|
|
478
|
+
|
|
479
|
+
def loader():
|
|
480
|
+
nonlocal call_count
|
|
481
|
+
call_count += 1
|
|
482
|
+
return "section_value"
|
|
483
|
+
|
|
484
|
+
result1 = builder._get_cached("test_key", loader, ttl=60)
|
|
485
|
+
result2 = builder._get_cached("test_key", loader, ttl=60)
|
|
486
|
+
|
|
487
|
+
assert result1 == result2 == "section_value"
|
|
488
|
+
assert call_count == 1, "Loader should be called only once within TTL"
|
|
489
|
+
|
|
490
|
+
def test_get_cached_reloads_after_ttl(self, tmp_path):
|
|
491
|
+
"""_get_cached reloads the value once TTL has expired."""
|
|
492
|
+
home = tmp_path / ".skcapstone"
|
|
493
|
+
home.mkdir()
|
|
494
|
+
builder = SystemPromptBuilder(home)
|
|
495
|
+
|
|
496
|
+
call_count = 0
|
|
497
|
+
|
|
498
|
+
def loader():
|
|
499
|
+
nonlocal call_count
|
|
500
|
+
call_count += 1
|
|
501
|
+
return f"value_{call_count}"
|
|
502
|
+
|
|
503
|
+
builder._get_cached("key", loader, ttl=60)
|
|
504
|
+
# Expire the cache entry manually
|
|
505
|
+
val, _ = builder._section_cache["key"]
|
|
506
|
+
builder._section_cache["key"] = (val, time.monotonic() - 1)
|
|
507
|
+
builder._get_cached("key", loader, ttl=60)
|
|
508
|
+
|
|
509
|
+
assert call_count == 2, "Loader should be called again after TTL expires"
|
|
510
|
+
|
|
511
|
+
def test_build_caches_identity_section(self, tmp_path):
|
|
512
|
+
"""build() serves identity from cache on second call."""
|
|
513
|
+
home = tmp_path / ".skcapstone"
|
|
514
|
+
identity_dir = home / "identity"
|
|
515
|
+
identity_dir.mkdir(parents=True)
|
|
516
|
+
(identity_dir / "identity.json").write_text(
|
|
517
|
+
json.dumps({"name": "opus", "fingerprint": "ABCD1234"})
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
builder = SystemPromptBuilder(home)
|
|
521
|
+
with patch.object(builder, "_load_identity", wraps=builder._load_identity) as mock_id:
|
|
522
|
+
builder.build()
|
|
523
|
+
builder.build()
|
|
524
|
+
|
|
525
|
+
assert mock_id.call_count == 1, "_load_identity should be called once (cached)"
|
|
526
|
+
|
|
527
|
+
def test_build_caches_context_section(self, tmp_path):
|
|
528
|
+
"""build() serves context from cache on second call."""
|
|
529
|
+
home = tmp_path / ".skcapstone"
|
|
530
|
+
home.mkdir()
|
|
531
|
+
|
|
532
|
+
builder = SystemPromptBuilder(home)
|
|
533
|
+
with patch.object(builder, "_load_context", wraps=builder._load_context) as mock_ctx:
|
|
534
|
+
builder.build()
|
|
535
|
+
builder.build()
|
|
536
|
+
|
|
537
|
+
assert mock_ctx.call_count == 1, "_load_context should be called once (cached)"
|
|
538
|
+
|
|
539
|
+
def test_cache_key_isolation(self, tmp_path):
|
|
540
|
+
"""Different section keys are cached independently."""
|
|
541
|
+
home = tmp_path / ".skcapstone"
|
|
542
|
+
home.mkdir()
|
|
543
|
+
builder = SystemPromptBuilder(home)
|
|
544
|
+
|
|
545
|
+
a_calls, b_calls = 0, 0
|
|
546
|
+
|
|
547
|
+
def loader_a():
|
|
548
|
+
nonlocal a_calls
|
|
549
|
+
a_calls += 1
|
|
550
|
+
return "a"
|
|
551
|
+
|
|
552
|
+
def loader_b():
|
|
553
|
+
nonlocal b_calls
|
|
554
|
+
b_calls += 1
|
|
555
|
+
return "b"
|
|
556
|
+
|
|
557
|
+
builder._get_cached("a", loader_a)
|
|
558
|
+
builder._get_cached("b", loader_b)
|
|
559
|
+
builder._get_cached("a", loader_a)
|
|
560
|
+
builder._get_cached("b", loader_b)
|
|
561
|
+
|
|
562
|
+
assert a_calls == 1
|
|
563
|
+
assert b_calls == 1
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
class TestProcessEnvelopeTiming:
|
|
567
|
+
"""Timing instrumentation emitted by process_envelope."""
|
|
568
|
+
|
|
569
|
+
def _make_loop(self, tmp_path):
|
|
570
|
+
config = ConsciousnessConfig(fallback_chain=["passthrough"])
|
|
571
|
+
loop = ConsciousnessLoop(config, home=tmp_path / ".skcapstone")
|
|
572
|
+
loop._bridge = MagicMock()
|
|
573
|
+
loop._bridge.generate.return_value = "response"
|
|
574
|
+
return loop
|
|
575
|
+
|
|
576
|
+
def _make_envelope(self, content="hello"):
|
|
577
|
+
data = {"sender": "peer", "payload": {"content": content, "content_type": "text"}}
|
|
578
|
+
return _SimpleEnvelope(data)
|
|
579
|
+
|
|
580
|
+
def test_timing_log_emitted(self, tmp_path, caplog):
|
|
581
|
+
"""process_envelope logs 'Pipeline timing' with all four phase labels."""
|
|
582
|
+
loop = self._make_loop(tmp_path)
|
|
583
|
+
with caplog.at_level(logging.INFO, logger="skcapstone.consciousness"):
|
|
584
|
+
loop.process_envelope(self._make_envelope())
|
|
585
|
+
|
|
586
|
+
timing_msgs = [r.message for r in caplog.records if "Pipeline timing" in r.message]
|
|
587
|
+
assert timing_msgs, "Expected 'Pipeline timing' log entry"
|
|
588
|
+
msg = timing_msgs[0]
|
|
589
|
+
assert "classify:" in msg
|
|
590
|
+
assert "prompt_build:" in msg
|
|
591
|
+
assert "llm:" in msg
|
|
592
|
+
assert "send:" in msg
|
|
593
|
+
|
|
594
|
+
def test_timing_values_are_non_negative(self, tmp_path, caplog):
|
|
595
|
+
"""All reported timing values must be >= 0."""
|
|
596
|
+
import re as _re
|
|
597
|
+
|
|
598
|
+
loop = self._make_loop(tmp_path)
|
|
599
|
+
with caplog.at_level(logging.INFO, logger="skcapstone.consciousness"):
|
|
600
|
+
loop.process_envelope(self._make_envelope())
|
|
601
|
+
|
|
602
|
+
timing_msgs = [r.message for r in caplog.records if "Pipeline timing" in r.message]
|
|
603
|
+
assert timing_msgs
|
|
604
|
+
numbers = [float(n) for n in _re.findall(r"[\d.]+(?=ms)", timing_msgs[0])]
|
|
605
|
+
assert len(numbers) == 4, f"Expected 4 timing values, got: {numbers}"
|
|
606
|
+
assert all(n >= 0 for n in numbers), f"Negative timing value: {numbers}"
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
class TestVerifyMessageSignature:
|
|
610
|
+
"""Tests for ConsciousnessLoop._verify_message_signature."""
|
|
611
|
+
|
|
612
|
+
def _make_loop(self, tmp_path):
|
|
613
|
+
config = ConsciousnessConfig(fallback_chain=["passthrough"])
|
|
614
|
+
return ConsciousnessLoop(config, home=tmp_path / ".skcapstone")
|
|
615
|
+
|
|
616
|
+
def test_unsigned_when_no_signature(self, tmp_path):
|
|
617
|
+
"""Returns 'unsigned' when payload has no signature field."""
|
|
618
|
+
loop = self._make_loop(tmp_path)
|
|
619
|
+
data = {"sender": "jarvis", "payload": {"content": "hello"}}
|
|
620
|
+
assert loop._verify_message_signature(data) == "unsigned"
|
|
621
|
+
|
|
622
|
+
def test_unsigned_empty_signature(self, tmp_path):
|
|
623
|
+
"""Returns 'unsigned' when signature field is empty string."""
|
|
624
|
+
loop = self._make_loop(tmp_path)
|
|
625
|
+
data = {"sender": "jarvis", "payload": {"content": "hello", "signature": ""}}
|
|
626
|
+
assert loop._verify_message_signature(data) == "unsigned"
|
|
627
|
+
|
|
628
|
+
def test_failed_when_no_peer_key(self, tmp_path):
|
|
629
|
+
"""Returns 'failed' when sender has no public key in peer store."""
|
|
630
|
+
loop = self._make_loop(tmp_path)
|
|
631
|
+
data = {
|
|
632
|
+
"sender": "unknown-peer",
|
|
633
|
+
"payload": {"content": "hello", "signature": "-----BEGIN PGP MESSAGE-----\nfake\n-----END PGP MESSAGE-----"},
|
|
634
|
+
}
|
|
635
|
+
# No peer registered → get_peer returns None → failed
|
|
636
|
+
assert loop._verify_message_signature(data) == "failed"
|
|
637
|
+
|
|
638
|
+
def test_failed_when_unknown_sender(self, tmp_path):
|
|
639
|
+
"""Returns 'failed' when sender resolves to 'unknown'."""
|
|
640
|
+
loop = self._make_loop(tmp_path)
|
|
641
|
+
data = {
|
|
642
|
+
# No sender/from key → sanitizer returns "unknown"
|
|
643
|
+
"payload": {"content": "hi", "signature": "sig"},
|
|
644
|
+
}
|
|
645
|
+
assert loop._verify_message_signature(data) == "failed"
|
|
646
|
+
|
|
647
|
+
@patch("skcapstone.consciousness_loop.ConsciousnessLoop._verify_message_signature")
|
|
648
|
+
def test_on_inbox_file_logs_sig_status(self, mock_verify, tmp_path, caplog):
|
|
649
|
+
"""_on_inbox_file logs the signature status returned by _verify_message_signature."""
|
|
650
|
+
mock_verify.return_value = "unsigned"
|
|
651
|
+
|
|
652
|
+
loop = self._make_loop(tmp_path)
|
|
653
|
+
loop._executor = MagicMock() # don't submit real work
|
|
654
|
+
|
|
655
|
+
# Write a valid envelope file
|
|
656
|
+
inbox = tmp_path / "inbox"
|
|
657
|
+
inbox.mkdir()
|
|
658
|
+
msg_file = inbox / "test.skc.json"
|
|
659
|
+
msg_file.write_text(json.dumps({
|
|
660
|
+
"sender": "jarvis",
|
|
661
|
+
"payload": {"content": "hello", "content_type": "text"},
|
|
662
|
+
}))
|
|
663
|
+
|
|
664
|
+
with caplog.at_level(logging.INFO, logger="skcapstone.consciousness"):
|
|
665
|
+
loop._on_inbox_file(msg_file)
|
|
666
|
+
|
|
667
|
+
sig_logs = [r.message for r in caplog.records if "signature:" in r.message]
|
|
668
|
+
assert sig_logs, "Expected a 'signature:' log entry from _on_inbox_file"
|
|
669
|
+
assert "unsigned" in sig_logs[0]
|
|
670
|
+
|
|
671
|
+
def test_verified_with_mock_backend(self, tmp_path):
|
|
672
|
+
"""Returns 'verified' when capauth backend confirms the signature."""
|
|
673
|
+
loop = self._make_loop(tmp_path)
|
|
674
|
+
|
|
675
|
+
# Register a peer with a public key
|
|
676
|
+
peer_dir = (tmp_path / ".skcapstone") / "peers"
|
|
677
|
+
peer_dir.mkdir(parents=True)
|
|
678
|
+
peer_data = {
|
|
679
|
+
"name": "jarvis",
|
|
680
|
+
"fingerprint": "ABCD1234",
|
|
681
|
+
"public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nfake\n-----END PGP PUBLIC KEY BLOCK-----",
|
|
682
|
+
"trust_level": "verified",
|
|
683
|
+
}
|
|
684
|
+
(peer_dir / "jarvis.json").write_text(json.dumps(peer_data))
|
|
685
|
+
|
|
686
|
+
data = {
|
|
687
|
+
"sender": "jarvis",
|
|
688
|
+
"payload": {
|
|
689
|
+
"content": "hello",
|
|
690
|
+
"signature": "-----BEGIN PGP MESSAGE-----\nfake\n-----END PGP MESSAGE-----",
|
|
691
|
+
},
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
with patch("capauth.crypto.get_backend") as mock_get_backend:
|
|
695
|
+
mock_backend = MagicMock()
|
|
696
|
+
mock_backend.verify.return_value = True
|
|
697
|
+
mock_get_backend.return_value = mock_backend
|
|
698
|
+
|
|
699
|
+
result = loop._verify_message_signature(data)
|
|
700
|
+
|
|
701
|
+
assert result == "verified"
|
|
702
|
+
mock_backend.verify.assert_called_once_with(
|
|
703
|
+
data=b"hello",
|
|
704
|
+
signature_armor="-----BEGIN PGP MESSAGE-----\nfake\n-----END PGP MESSAGE-----",
|
|
705
|
+
public_key_armor=peer_data["public_key"],
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
def test_failed_with_bad_signature(self, tmp_path):
|
|
709
|
+
"""Returns 'failed' when capauth backend rejects the signature."""
|
|
710
|
+
loop = self._make_loop(tmp_path)
|
|
711
|
+
|
|
712
|
+
peer_dir = (tmp_path / ".skcapstone") / "peers"
|
|
713
|
+
peer_dir.mkdir(parents=True)
|
|
714
|
+
peer_data = {
|
|
715
|
+
"name": "jarvis",
|
|
716
|
+
"fingerprint": "ABCD1234",
|
|
717
|
+
"public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nfake\n-----END PGP PUBLIC KEY BLOCK-----",
|
|
718
|
+
"trust_level": "verified",
|
|
719
|
+
}
|
|
720
|
+
(peer_dir / "jarvis.json").write_text(json.dumps(peer_data))
|
|
721
|
+
|
|
722
|
+
data = {
|
|
723
|
+
"sender": "jarvis",
|
|
724
|
+
"payload": {
|
|
725
|
+
"content": "hello",
|
|
726
|
+
"signature": "-----BEGIN PGP MESSAGE-----\nfake\n-----END PGP MESSAGE-----",
|
|
727
|
+
},
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
with patch("capauth.crypto.get_backend") as mock_get_backend:
|
|
731
|
+
mock_backend = MagicMock()
|
|
732
|
+
mock_backend.verify.return_value = False
|
|
733
|
+
mock_get_backend.return_value = mock_backend
|
|
734
|
+
|
|
735
|
+
result = loop._verify_message_signature(data)
|
|
736
|
+
|
|
737
|
+
assert result == "failed"
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
class TestOllamaConnectionPool:
|
|
741
|
+
"""Unit tests for _OllamaPool — connection reuse, TTL eviction, invalidation."""
|
|
742
|
+
|
|
743
|
+
def test_get_returns_same_connection_within_ttl(self):
|
|
744
|
+
"""Two get() calls within TTL return the same connection object."""
|
|
745
|
+
pool = _OllamaPool("http://localhost:11434", ttl=60)
|
|
746
|
+
with patch("http.client.HTTPConnection") as mock_cls:
|
|
747
|
+
mock_conn = MagicMock()
|
|
748
|
+
mock_cls.return_value = mock_conn
|
|
749
|
+
conn1 = pool.get()
|
|
750
|
+
conn2 = pool.get()
|
|
751
|
+
|
|
752
|
+
assert conn1 is conn2, "Same connection should be returned within TTL"
|
|
753
|
+
assert mock_cls.call_count == 1, "HTTPConnection should be created only once"
|
|
754
|
+
|
|
755
|
+
def test_get_recreates_connection_after_ttl(self):
|
|
756
|
+
"""get() creates a fresh connection once TTL has expired."""
|
|
757
|
+
pool = _OllamaPool("http://localhost:11434", ttl=60)
|
|
758
|
+
with patch("http.client.HTTPConnection") as mock_cls:
|
|
759
|
+
mock_cls.side_effect = [MagicMock(), MagicMock()]
|
|
760
|
+
pool.get()
|
|
761
|
+
# Manually expire the TTL
|
|
762
|
+
pool._created_at = time.monotonic() - 61
|
|
763
|
+
pool.get()
|
|
764
|
+
|
|
765
|
+
assert mock_cls.call_count == 2, "HTTPConnection should be recreated after TTL"
|
|
766
|
+
|
|
767
|
+
def test_invalidate_discards_connection(self):
|
|
768
|
+
"""invalidate() closes and clears the cached connection."""
|
|
769
|
+
pool = _OllamaPool("http://localhost:11434", ttl=60)
|
|
770
|
+
with patch("http.client.HTTPConnection") as mock_cls:
|
|
771
|
+
mock_cls.side_effect = [MagicMock(), MagicMock()]
|
|
772
|
+
pool.get()
|
|
773
|
+
pool.invalidate()
|
|
774
|
+
assert pool._conn is None, "invalidate() should clear _conn"
|
|
775
|
+
pool.get()
|
|
776
|
+
|
|
777
|
+
assert mock_cls.call_count == 2, "New connection created after invalidate()"
|
|
778
|
+
|
|
779
|
+
def test_probe_ollama_uses_pool_connection(self):
|
|
780
|
+
"""_probe_ollama routes the health check through the pool."""
|
|
781
|
+
config = ConsciousnessConfig()
|
|
782
|
+
bridge = LLMBridge(config)
|
|
783
|
+
|
|
784
|
+
mock_conn = MagicMock()
|
|
785
|
+
mock_resp = MagicMock()
|
|
786
|
+
mock_resp.status = 200
|
|
787
|
+
mock_resp.read.return_value = b'{"models":[]}'
|
|
788
|
+
mock_conn.getresponse.return_value = mock_resp
|
|
789
|
+
|
|
790
|
+
with patch.object(bridge._ollama_pool, "get", return_value=mock_conn):
|
|
791
|
+
result = bridge._probe_ollama()
|
|
792
|
+
|
|
793
|
+
assert result is True
|
|
794
|
+
mock_conn.request.assert_called_once_with("GET", "/api/tags")
|
|
795
|
+
mock_resp.read.assert_called_once() # body drained for keep-alive
|
|
796
|
+
|
|
797
|
+
def test_probe_ollama_invalidates_pool_on_error(self):
|
|
798
|
+
"""_probe_ollama invalidates the pool when a connection error occurs."""
|
|
799
|
+
config = ConsciousnessConfig()
|
|
800
|
+
bridge = LLMBridge(config)
|
|
801
|
+
|
|
802
|
+
mock_conn = MagicMock()
|
|
803
|
+
mock_conn.request.side_effect = ConnectionError("refused")
|
|
804
|
+
|
|
805
|
+
with patch.object(bridge._ollama_pool, "get", return_value=mock_conn):
|
|
806
|
+
with patch.object(bridge._ollama_pool, "invalidate") as mock_invalidate:
|
|
807
|
+
result = bridge._probe_ollama()
|
|
808
|
+
|
|
809
|
+
assert result is False
|
|
810
|
+
mock_invalidate.assert_called_once()
|
|
811
|
+
|
|
812
|
+
def test_pool_host_port_parsing(self):
|
|
813
|
+
"""_OllamaPool correctly parses host and port from the URL."""
|
|
814
|
+
pool = _OllamaPool("http://myhost:12345", ttl=30)
|
|
815
|
+
assert pool._host == "myhost"
|
|
816
|
+
assert pool._port == 12345
|
|
817
|
+
|
|
818
|
+
def test_pool_defaults_for_bare_localhost(self):
|
|
819
|
+
"""_OllamaPool falls back to localhost:11434 for a bare URL."""
|
|
820
|
+
pool = _OllamaPool("http://localhost:11434")
|
|
821
|
+
assert pool._host == "localhost"
|
|
822
|
+
assert pool._port == 11434
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
class TestMessageThreading:
|
|
826
|
+
"""Tests for thread_id / in_reply_to envelope tracking and history grouping."""
|
|
827
|
+
|
|
828
|
+
# ------------------------------------------------------------------
|
|
829
|
+
# _SimpleEnvelope extraction
|
|
830
|
+
# ------------------------------------------------------------------
|
|
831
|
+
|
|
832
|
+
def test_envelope_extracts_thread_id_from_root(self):
|
|
833
|
+
"""thread_id at envelope root is captured."""
|
|
834
|
+
data = {
|
|
835
|
+
"sender": "jarvis",
|
|
836
|
+
"thread_id": "thread-abc",
|
|
837
|
+
"payload": {"content": "hi", "content_type": "text"},
|
|
838
|
+
}
|
|
839
|
+
env = _SimpleEnvelope(data)
|
|
840
|
+
assert env.thread_id == "thread-abc"
|
|
841
|
+
|
|
842
|
+
def test_envelope_extracts_thread_id_from_payload(self):
|
|
843
|
+
"""thread_id nested inside payload is captured."""
|
|
844
|
+
data = {
|
|
845
|
+
"sender": "jarvis",
|
|
846
|
+
"payload": {"content": "hi", "content_type": "text", "thread_id": "thread-xyz"},
|
|
847
|
+
}
|
|
848
|
+
env = _SimpleEnvelope(data)
|
|
849
|
+
assert env.thread_id == "thread-xyz"
|
|
850
|
+
|
|
851
|
+
def test_envelope_extracts_in_reply_to(self):
|
|
852
|
+
"""in_reply_to at envelope root is captured."""
|
|
853
|
+
data = {
|
|
854
|
+
"sender": "lumina",
|
|
855
|
+
"in_reply_to": "msg-001",
|
|
856
|
+
"payload": {"content": "reply", "content_type": "text"},
|
|
857
|
+
}
|
|
858
|
+
env = _SimpleEnvelope(data)
|
|
859
|
+
assert env.in_reply_to == "msg-001"
|
|
860
|
+
|
|
861
|
+
def test_envelope_in_reply_to_from_payload(self):
|
|
862
|
+
"""in_reply_to nested inside payload is captured."""
|
|
863
|
+
data = {
|
|
864
|
+
"sender": "lumina",
|
|
865
|
+
"payload": {"content": "reply", "content_type": "text", "in_reply_to": "msg-002"},
|
|
866
|
+
}
|
|
867
|
+
env = _SimpleEnvelope(data)
|
|
868
|
+
assert env.in_reply_to == "msg-002"
|
|
869
|
+
|
|
870
|
+
def test_envelope_defaults_empty_when_absent(self):
|
|
871
|
+
"""thread_id and in_reply_to default to empty string when absent."""
|
|
872
|
+
data = {"sender": "ava", "payload": {"content": "hello", "content_type": "text"}}
|
|
873
|
+
env = _SimpleEnvelope(data)
|
|
874
|
+
assert env.thread_id == ""
|
|
875
|
+
assert env.in_reply_to == ""
|
|
876
|
+
|
|
877
|
+
# ------------------------------------------------------------------
|
|
878
|
+
# SystemPromptBuilder — add_to_history threading
|
|
879
|
+
# ------------------------------------------------------------------
|
|
880
|
+
|
|
881
|
+
def test_add_to_history_stores_thread_id(self, tmp_path):
|
|
882
|
+
"""add_to_history stores thread_id in the history entry."""
|
|
883
|
+
home = tmp_path / ".skcapstone"
|
|
884
|
+
home.mkdir()
|
|
885
|
+
builder = SystemPromptBuilder(home)
|
|
886
|
+
|
|
887
|
+
builder.add_to_history("jarvis", "user", "Hello!", thread_id="t-001")
|
|
888
|
+
|
|
889
|
+
entry = builder._conversation_history["jarvis"][0]
|
|
890
|
+
assert entry["thread_id"] == "t-001"
|
|
891
|
+
|
|
892
|
+
def test_add_to_history_stores_in_reply_to(self, tmp_path):
|
|
893
|
+
"""add_to_history stores in_reply_to in the history entry."""
|
|
894
|
+
home = tmp_path / ".skcapstone"
|
|
895
|
+
home.mkdir()
|
|
896
|
+
builder = SystemPromptBuilder(home)
|
|
897
|
+
|
|
898
|
+
builder.add_to_history("jarvis", "user", "Reply!", in_reply_to="msg-55")
|
|
899
|
+
|
|
900
|
+
entry = builder._conversation_history["jarvis"][0]
|
|
901
|
+
assert entry["in_reply_to"] == "msg-55"
|
|
902
|
+
|
|
903
|
+
def test_add_to_history_no_thread_fields_when_absent(self, tmp_path):
|
|
904
|
+
"""No thread_id/in_reply_to keys in entry when not provided."""
|
|
905
|
+
home = tmp_path / ".skcapstone"
|
|
906
|
+
home.mkdir()
|
|
907
|
+
builder = SystemPromptBuilder(home)
|
|
908
|
+
|
|
909
|
+
builder.add_to_history("jarvis", "user", "Plain message")
|
|
910
|
+
|
|
911
|
+
entry = builder._conversation_history["jarvis"][0]
|
|
912
|
+
assert "thread_id" not in entry
|
|
913
|
+
assert "in_reply_to" not in entry
|
|
914
|
+
|
|
915
|
+
def test_thread_fields_persisted_to_json(self, tmp_path):
|
|
916
|
+
"""thread_id and in_reply_to survive the round-trip through JSON persistence."""
|
|
917
|
+
home = tmp_path / ".skcapstone"
|
|
918
|
+
home.mkdir()
|
|
919
|
+
builder = SystemPromptBuilder(home)
|
|
920
|
+
|
|
921
|
+
builder.add_to_history("opus", "user", "Threaded msg", thread_id="t-99", in_reply_to="m-10")
|
|
922
|
+
|
|
923
|
+
conv_file = home / "conversations" / "opus.json"
|
|
924
|
+
data = json.loads(conv_file.read_text())
|
|
925
|
+
assert data[0]["thread_id"] == "t-99"
|
|
926
|
+
assert data[0]["in_reply_to"] == "m-10"
|
|
927
|
+
|
|
928
|
+
# ------------------------------------------------------------------
|
|
929
|
+
# SystemPromptBuilder.build — thread context in prompt
|
|
930
|
+
# ------------------------------------------------------------------
|
|
931
|
+
|
|
932
|
+
def test_build_shows_thread_label_when_thread_id_given(self, tmp_path):
|
|
933
|
+
"""build() includes a [Thread: ...] label when thread_id is provided."""
|
|
934
|
+
home = tmp_path / ".skcapstone"
|
|
935
|
+
home.mkdir()
|
|
936
|
+
builder = SystemPromptBuilder(home)
|
|
937
|
+
|
|
938
|
+
builder.add_to_history("jarvis", "user", "Thread message", thread_id="t-alpha")
|
|
939
|
+
prompt = builder.build(peer_name="jarvis", thread_id="t-alpha")
|
|
940
|
+
|
|
941
|
+
assert "Thread: t-alpha" in prompt
|
|
942
|
+
assert "Thread message" in prompt
|
|
943
|
+
|
|
944
|
+
def test_build_groups_thread_and_other_messages(self, tmp_path):
|
|
945
|
+
"""Thread messages appear under [Thread:...] and others under [Other recent messages:]."""
|
|
946
|
+
home = tmp_path / ".skcapstone"
|
|
947
|
+
home.mkdir()
|
|
948
|
+
builder = SystemPromptBuilder(home)
|
|
949
|
+
|
|
950
|
+
builder.add_to_history("ava", "user", "Thread msg", thread_id="t-1")
|
|
951
|
+
builder.add_to_history("ava", "user", "Unrelated msg")
|
|
952
|
+
|
|
953
|
+
prompt = builder.build(peer_name="ava", thread_id="t-1")
|
|
954
|
+
|
|
955
|
+
assert "Thread: t-1" in prompt
|
|
956
|
+
assert "Thread msg" in prompt
|
|
957
|
+
assert "Other recent messages" in prompt
|
|
958
|
+
assert "Unrelated msg" in prompt
|
|
959
|
+
|
|
960
|
+
def test_build_without_thread_id_shows_thread_labels_inline(self, tmp_path):
|
|
961
|
+
"""Without thread_id, messages with threads show [thread:...] inline."""
|
|
962
|
+
home = tmp_path / ".skcapstone"
|
|
963
|
+
home.mkdir()
|
|
964
|
+
builder = SystemPromptBuilder(home)
|
|
965
|
+
|
|
966
|
+
builder.add_to_history("lumina", "user", "Inline threaded", thread_id="t-beta")
|
|
967
|
+
builder.add_to_history("lumina", "user", "Plain")
|
|
968
|
+
|
|
969
|
+
prompt = builder.build(peer_name="lumina")
|
|
970
|
+
|
|
971
|
+
assert "thread:t-beta" in prompt
|
|
972
|
+
assert "Inline threaded" in prompt
|
|
973
|
+
assert "Plain" in prompt
|
|
974
|
+
|
|
975
|
+
# ------------------------------------------------------------------
|
|
976
|
+
# ConsciousnessLoop.process_envelope — threading end-to-end
|
|
977
|
+
# ------------------------------------------------------------------
|
|
978
|
+
|
|
979
|
+
def test_process_envelope_stores_thread_id_in_history(self, tmp_path):
|
|
980
|
+
"""process_envelope extracts thread_id and stores it in conversation history."""
|
|
981
|
+
config = ConsciousnessConfig(fallback_chain=["passthrough"])
|
|
982
|
+
loop = ConsciousnessLoop(config, home=tmp_path / ".skcapstone")
|
|
983
|
+
loop._bridge = MagicMock()
|
|
984
|
+
loop._bridge.generate.return_value = "hi back"
|
|
985
|
+
|
|
986
|
+
data = {
|
|
987
|
+
"sender": "jarvis",
|
|
988
|
+
"thread_id": "t-42",
|
|
989
|
+
"payload": {"content": "threaded hello", "content_type": "text"},
|
|
990
|
+
}
|
|
991
|
+
env = _SimpleEnvelope(data)
|
|
992
|
+
loop.process_envelope(env)
|
|
993
|
+
|
|
994
|
+
history = loop._prompt_builder._conversation_history.get("jarvis", [])
|
|
995
|
+
user_entry = next((e for e in history if e["role"] == "user"), None)
|
|
996
|
+
assert user_entry is not None
|
|
997
|
+
assert user_entry.get("thread_id") == "t-42"
|
|
998
|
+
|
|
999
|
+
def test_process_envelope_stores_in_reply_to_in_history(self, tmp_path):
|
|
1000
|
+
"""process_envelope extracts in_reply_to and stores it in conversation history."""
|
|
1001
|
+
config = ConsciousnessConfig(fallback_chain=["passthrough"])
|
|
1002
|
+
loop = ConsciousnessLoop(config, home=tmp_path / ".skcapstone")
|
|
1003
|
+
loop._bridge = MagicMock()
|
|
1004
|
+
loop._bridge.generate.return_value = "reply"
|
|
1005
|
+
|
|
1006
|
+
data = {
|
|
1007
|
+
"sender": "ava",
|
|
1008
|
+
"in_reply_to": "msg-77",
|
|
1009
|
+
"payload": {"content": "reply message", "content_type": "text"},
|
|
1010
|
+
}
|
|
1011
|
+
env = _SimpleEnvelope(data)
|
|
1012
|
+
loop.process_envelope(env)
|
|
1013
|
+
|
|
1014
|
+
history = loop._prompt_builder._conversation_history.get("ava", [])
|
|
1015
|
+
user_entry = next((e for e in history if e["role"] == "user"), None)
|
|
1016
|
+
assert user_entry is not None
|
|
1017
|
+
assert user_entry.get("in_reply_to") == "msg-77"
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
# ---------------------------------------------------------------------------
|
|
1021
|
+
# Prompt versioning tests
|
|
1022
|
+
# ---------------------------------------------------------------------------
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
class TestSystemPromptVersioning:
|
|
1026
|
+
"""Tests for SHA-256 prompt versioning in SystemPromptBuilder."""
|
|
1027
|
+
|
|
1028
|
+
def test_initial_hash_is_none(self, tmp_path):
|
|
1029
|
+
"""current_prompt_hash is None before any build() call."""
|
|
1030
|
+
home = tmp_path / ".skcapstone"
|
|
1031
|
+
home.mkdir()
|
|
1032
|
+
builder = SystemPromptBuilder(home)
|
|
1033
|
+
assert builder.current_prompt_hash is None
|
|
1034
|
+
|
|
1035
|
+
def test_hash_set_after_build(self, tmp_path):
|
|
1036
|
+
"""After build(), current_prompt_hash is a 64-char SHA-256 hex string."""
|
|
1037
|
+
home = tmp_path / ".skcapstone"
|
|
1038
|
+
home.mkdir()
|
|
1039
|
+
builder = SystemPromptBuilder(home)
|
|
1040
|
+
builder.build()
|
|
1041
|
+
h = builder.current_prompt_hash
|
|
1042
|
+
assert h is not None
|
|
1043
|
+
assert len(h) == 64
|
|
1044
|
+
assert all(c in "0123456789abcdef" for c in h)
|
|
1045
|
+
|
|
1046
|
+
def test_version_file_created_on_first_build(self, tmp_path):
|
|
1047
|
+
"""A JSON version file is written to prompt_versions/ on first build."""
|
|
1048
|
+
home = tmp_path / ".skcapstone"
|
|
1049
|
+
home.mkdir()
|
|
1050
|
+
builder = SystemPromptBuilder(home)
|
|
1051
|
+
builder.build()
|
|
1052
|
+
|
|
1053
|
+
versions_dir = home / "prompt_versions"
|
|
1054
|
+
files = list(versions_dir.glob("*.json"))
|
|
1055
|
+
assert len(files) == 1, "Expected exactly one version file"
|
|
1056
|
+
|
|
1057
|
+
record = json.loads(files[0].read_text())
|
|
1058
|
+
assert record["hash"] == builder.current_prompt_hash
|
|
1059
|
+
assert "timestamp" in record
|
|
1060
|
+
assert "prompt" in record
|
|
1061
|
+
|
|
1062
|
+
def test_no_duplicate_file_for_same_prompt(self, tmp_path):
|
|
1063
|
+
"""Building the same prompt twice does not create a second version file."""
|
|
1064
|
+
home = tmp_path / ".skcapstone"
|
|
1065
|
+
home.mkdir()
|
|
1066
|
+
builder = SystemPromptBuilder(home)
|
|
1067
|
+
builder.build()
|
|
1068
|
+
builder.build() # same content, same hash
|
|
1069
|
+
|
|
1070
|
+
versions_dir = home / "prompt_versions"
|
|
1071
|
+
files = list(versions_dir.glob("*.json"))
|
|
1072
|
+
assert len(files) == 1, "No duplicate file when prompt unchanged"
|
|
1073
|
+
|
|
1074
|
+
def test_new_file_when_prompt_changes(self, tmp_path):
|
|
1075
|
+
"""A new version file is created when the prompt content changes."""
|
|
1076
|
+
home = tmp_path / ".skcapstone"
|
|
1077
|
+
identity_dir = home / "identity"
|
|
1078
|
+
identity_dir.mkdir(parents=True)
|
|
1079
|
+
|
|
1080
|
+
builder = SystemPromptBuilder(home)
|
|
1081
|
+
builder.build()
|
|
1082
|
+
first_hash = builder.current_prompt_hash
|
|
1083
|
+
|
|
1084
|
+
# Change the identity so the prompt changes
|
|
1085
|
+
(identity_dir / "identity.json").write_text(
|
|
1086
|
+
json.dumps({"name": "changed-agent", "fingerprint": "NEWFINGERPRINT"})
|
|
1087
|
+
)
|
|
1088
|
+
# Expire the cache so the identity is reloaded
|
|
1089
|
+
builder._section_cache.clear()
|
|
1090
|
+
builder.build()
|
|
1091
|
+
second_hash = builder.current_prompt_hash
|
|
1092
|
+
|
|
1093
|
+
assert first_hash != second_hash
|
|
1094
|
+
versions_dir = home / "prompt_versions"
|
|
1095
|
+
files = list(versions_dir.glob("*.json"))
|
|
1096
|
+
assert len(files) == 2, "Two files for two distinct prompt versions"
|
|
1097
|
+
|
|
1098
|
+
def test_stats_include_prompt_hash_and_version_responses(self, tmp_path):
|
|
1099
|
+
"""ConsciousnessLoop.stats exposes current_prompt_hash and prompt_version_responses."""
|
|
1100
|
+
home = tmp_path / ".skcapstone"
|
|
1101
|
+
home.mkdir()
|
|
1102
|
+
|
|
1103
|
+
config = ConsciousnessConfig(enabled=False)
|
|
1104
|
+
loop = ConsciousnessLoop(config, home=home)
|
|
1105
|
+
|
|
1106
|
+
stats = loop.stats
|
|
1107
|
+
assert "current_prompt_hash" in stats
|
|
1108
|
+
assert "prompt_version_responses" in stats
|
|
1109
|
+
assert isinstance(stats["prompt_version_responses"], dict)
|
|
1110
|
+
|
|
1111
|
+
def test_version_responses_incremented_on_send(self, tmp_path):
|
|
1112
|
+
"""prompt_version_responses counter increments for the active hash when a response is sent."""
|
|
1113
|
+
home = tmp_path / ".skcapstone"
|
|
1114
|
+
home.mkdir()
|
|
1115
|
+
|
|
1116
|
+
config = ConsciousnessConfig(enabled=False)
|
|
1117
|
+
loop = ConsciousnessLoop(config, home=home)
|
|
1118
|
+
|
|
1119
|
+
# Simulate a build so a hash is established
|
|
1120
|
+
loop._prompt_builder.build()
|
|
1121
|
+
active_hash = loop._prompt_builder.current_prompt_hash
|
|
1122
|
+
assert active_hash is not None
|
|
1123
|
+
|
|
1124
|
+
# Manually trigger the counting logic (as the send path does)
|
|
1125
|
+
loop._prompt_version_responses[active_hash] += 1
|
|
1126
|
+
|
|
1127
|
+
stats = loop.stats
|
|
1128
|
+
assert stats["prompt_version_responses"].get(active_hash) == 1
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
class TestFetchSenderMemories:
|
|
1132
|
+
"""Tests for ConsciousnessLoop._fetch_sender_memories()."""
|
|
1133
|
+
|
|
1134
|
+
def _make_loop(self, tmp_path):
|
|
1135
|
+
config = ConsciousnessConfig(
|
|
1136
|
+
fallback_chain=["passthrough"],
|
|
1137
|
+
auto_memory=False,
|
|
1138
|
+
)
|
|
1139
|
+
return ConsciousnessLoop(config, home=tmp_path / ".skcapstone")
|
|
1140
|
+
|
|
1141
|
+
def _make_entry(self, memory_id, content, tags=None):
|
|
1142
|
+
"""Build a minimal MemoryEntry-like mock."""
|
|
1143
|
+
entry = MagicMock()
|
|
1144
|
+
entry.memory_id = memory_id
|
|
1145
|
+
entry.content = content
|
|
1146
|
+
entry.tags = tags or []
|
|
1147
|
+
return entry
|
|
1148
|
+
|
|
1149
|
+
def test_returns_empty_when_no_memories(self, tmp_path):
|
|
1150
|
+
"""Returns empty string when memory search yields nothing."""
|
|
1151
|
+
loop = self._make_loop(tmp_path)
|
|
1152
|
+
|
|
1153
|
+
with patch("skcapstone.memory_engine.search", return_value=[]):
|
|
1154
|
+
result = loop._fetch_sender_memories("jarvis", "hello there")
|
|
1155
|
+
|
|
1156
|
+
assert result == ""
|
|
1157
|
+
|
|
1158
|
+
def test_includes_top_3_memories_in_output(self, tmp_path):
|
|
1159
|
+
"""Output contains exactly 3 memory entries when 5 are returned."""
|
|
1160
|
+
loop = self._make_loop(tmp_path)
|
|
1161
|
+
|
|
1162
|
+
entries = [
|
|
1163
|
+
self._make_entry(f"id-{i}", f"Memory content {i}")
|
|
1164
|
+
for i in range(5)
|
|
1165
|
+
]
|
|
1166
|
+
|
|
1167
|
+
# by_sender returns 3, by_content returns 2 different ones
|
|
1168
|
+
def mock_search(home, query, tags=None, limit=5):
|
|
1169
|
+
if tags:
|
|
1170
|
+
return entries[:3]
|
|
1171
|
+
return entries[3:]
|
|
1172
|
+
|
|
1173
|
+
with patch("skcapstone.memory_engine.search", side_effect=mock_search):
|
|
1174
|
+
result = loop._fetch_sender_memories("jarvis", "hello")
|
|
1175
|
+
|
|
1176
|
+
assert "Relevant memories:" in result
|
|
1177
|
+
assert "[1]" in result
|
|
1178
|
+
assert "[2]" in result
|
|
1179
|
+
assert "[3]" in result
|
|
1180
|
+
# Should not exceed 3 entries
|
|
1181
|
+
assert "[4]" not in result
|
|
1182
|
+
|
|
1183
|
+
def test_deduplicates_overlapping_results(self, tmp_path):
|
|
1184
|
+
"""Memories returned by both searches are deduplicated."""
|
|
1185
|
+
loop = self._make_loop(tmp_path)
|
|
1186
|
+
|
|
1187
|
+
shared = self._make_entry("shared-id", "Shared memory content")
|
|
1188
|
+
unique = self._make_entry("unique-id", "Unique memory content")
|
|
1189
|
+
|
|
1190
|
+
# Both searches return the same shared entry
|
|
1191
|
+
with patch("skcapstone.memory_engine.search", return_value=[shared, unique]):
|
|
1192
|
+
result = loop._fetch_sender_memories("jarvis", "hello")
|
|
1193
|
+
|
|
1194
|
+
# shared-id should appear exactly once
|
|
1195
|
+
assert result.count("Shared memory content") == 1
|
|
1196
|
+
|
|
1197
|
+
def test_memory_context_appended_to_system_prompt(self, tmp_path):
|
|
1198
|
+
"""process_envelope appends memory context to the system prompt passed to LLM."""
|
|
1199
|
+
home = tmp_path / ".skcapstone"
|
|
1200
|
+
home.mkdir()
|
|
1201
|
+
config = ConsciousnessConfig(
|
|
1202
|
+
fallback_chain=["passthrough"],
|
|
1203
|
+
auto_memory=False,
|
|
1204
|
+
auto_ack=False,
|
|
1205
|
+
desktop_notifications=False,
|
|
1206
|
+
)
|
|
1207
|
+
loop = ConsciousnessLoop(config, home=home)
|
|
1208
|
+
|
|
1209
|
+
captured_system_prompts = []
|
|
1210
|
+
|
|
1211
|
+
def fake_generate(system_prompt, content, signal, _out_info=None, **kwargs):
|
|
1212
|
+
captured_system_prompts.append(system_prompt)
|
|
1213
|
+
return "test response"
|
|
1214
|
+
|
|
1215
|
+
loop._bridge = MagicMock()
|
|
1216
|
+
loop._bridge.generate.side_effect = fake_generate
|
|
1217
|
+
|
|
1218
|
+
entry = MagicMock()
|
|
1219
|
+
entry.memory_id = "mem-abc"
|
|
1220
|
+
entry.content = "jarvis mentioned he prefers concise replies"
|
|
1221
|
+
entry.tags = ["peer:jarvis"]
|
|
1222
|
+
|
|
1223
|
+
with patch("skcapstone.memory_engine.search", return_value=[entry]):
|
|
1224
|
+
envelope = _SimpleEnvelope({
|
|
1225
|
+
"sender": "jarvis",
|
|
1226
|
+
"payload": {"content": "What is the status?", "content_type": "text"},
|
|
1227
|
+
})
|
|
1228
|
+
loop.process_envelope(envelope)
|
|
1229
|
+
|
|
1230
|
+
assert len(captured_system_prompts) == 1
|
|
1231
|
+
assert "Relevant memories:" in captured_system_prompts[0]
|
|
1232
|
+
assert "jarvis mentioned he prefers concise replies" in captured_system_prompts[0]
|
|
1233
|
+
|
|
1234
|
+
def test_memory_error_does_not_break_envelope_processing(self, tmp_path):
|
|
1235
|
+
"""If memory search raises, process_envelope still completes normally."""
|
|
1236
|
+
home = tmp_path / ".skcapstone"
|
|
1237
|
+
home.mkdir()
|
|
1238
|
+
config = ConsciousnessConfig(
|
|
1239
|
+
fallback_chain=["passthrough"],
|
|
1240
|
+
auto_memory=False,
|
|
1241
|
+
auto_ack=False,
|
|
1242
|
+
desktop_notifications=False,
|
|
1243
|
+
)
|
|
1244
|
+
loop = ConsciousnessLoop(config, home=home)
|
|
1245
|
+
loop._bridge = MagicMock()
|
|
1246
|
+
loop._bridge.generate.return_value = "test response"
|
|
1247
|
+
|
|
1248
|
+
with patch(
|
|
1249
|
+
"skcapstone.memory_engine.search",
|
|
1250
|
+
side_effect=RuntimeError("db unavailable"),
|
|
1251
|
+
):
|
|
1252
|
+
envelope = _SimpleEnvelope({
|
|
1253
|
+
"sender": "jarvis",
|
|
1254
|
+
"payload": {"content": "hello", "content_type": "text"},
|
|
1255
|
+
})
|
|
1256
|
+
result = loop.process_envelope(envelope)
|
|
1257
|
+
|
|
1258
|
+
assert result == "test response"
|
|
1259
|
+
|
|
1260
|
+
def test_no_memory_enrichment_when_memories_empty(self, tmp_path):
|
|
1261
|
+
"""System prompt is unchanged when no memories are found."""
|
|
1262
|
+
home = tmp_path / ".skcapstone"
|
|
1263
|
+
home.mkdir()
|
|
1264
|
+
config = ConsciousnessConfig(
|
|
1265
|
+
fallback_chain=["passthrough"],
|
|
1266
|
+
auto_memory=False,
|
|
1267
|
+
auto_ack=False,
|
|
1268
|
+
desktop_notifications=False,
|
|
1269
|
+
)
|
|
1270
|
+
loop = ConsciousnessLoop(config, home=home)
|
|
1271
|
+
|
|
1272
|
+
captured_system_prompts = []
|
|
1273
|
+
|
|
1274
|
+
def fake_generate(system_prompt, content, signal, _out_info=None, **kwargs):
|
|
1275
|
+
captured_system_prompts.append(system_prompt)
|
|
1276
|
+
return "test response"
|
|
1277
|
+
|
|
1278
|
+
loop._bridge = MagicMock()
|
|
1279
|
+
loop._bridge.generate.side_effect = fake_generate
|
|
1280
|
+
|
|
1281
|
+
with patch("skcapstone.memory_engine.search", return_value=[]):
|
|
1282
|
+
envelope = _SimpleEnvelope({
|
|
1283
|
+
"sender": "jarvis",
|
|
1284
|
+
"payload": {"content": "hello", "content_type": "text"},
|
|
1285
|
+
})
|
|
1286
|
+
loop.process_envelope(envelope)
|
|
1287
|
+
|
|
1288
|
+
assert len(captured_system_prompts) == 1
|
|
1289
|
+
assert "Relevant memories:" not in captured_system_prompts[0]
|