@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,877 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tests/integration/test_consciousness_e2e.py
|
|
3
|
+
|
|
4
|
+
Full end-to-end integration test for the conscious agent pipeline.
|
|
5
|
+
|
|
6
|
+
Pipeline under test
|
|
7
|
+
-------------------
|
|
8
|
+
1. DaemonService starts with consciousness loop enabled (in-process thread).
|
|
9
|
+
2. A .skc.json envelope is dropped into the inbox directory,
|
|
10
|
+
simulating delivery by SKComm or ``skcapstone send``.
|
|
11
|
+
3. Inotify / watchdog detects the file within 5 s.
|
|
12
|
+
4. ConsciousnessLoop classifies the message and calls LLMBridge.generate().
|
|
13
|
+
5. Mock SKComm captures the outbound response.
|
|
14
|
+
6. All steps complete within 60 s total.
|
|
15
|
+
|
|
16
|
+
Related coordination tasks
|
|
17
|
+
--------------------------
|
|
18
|
+
[8fbd0130] — Full E2E integration test (this file)
|
|
19
|
+
[c9e7b9d8] — End-to-end consciousness test: send SKComm message,
|
|
20
|
+
verify autonomous response
|
|
21
|
+
|
|
22
|
+
Running
|
|
23
|
+
-------
|
|
24
|
+
# Full integration suite (may hit disk / watchdog / LLM):
|
|
25
|
+
pytest tests/integration/test_consciousness_e2e.py -v -s -m integration
|
|
26
|
+
|
|
27
|
+
# Skip integration markers (e.g. in fast CI):
|
|
28
|
+
pytest -m "not integration" tests/
|
|
29
|
+
|
|
30
|
+
Known daemon startup issues
|
|
31
|
+
---------------------------
|
|
32
|
+
* SKComm not configured in test home: DaemonService logs a warning and
|
|
33
|
+
skips SKComm polling. Consciousness loop still runs via inotify.
|
|
34
|
+
* Prompt build latency: SystemPromptBuilder.build() loads identity, soul,
|
|
35
|
+
context, and snapshots from disk. In tests this takes ~3-4 s even with
|
|
36
|
+
empty dirs because it probes optional YAML/JSON files. Tests account for
|
|
37
|
+
this by giving the full 60 s budget to the response, not just the pickup.
|
|
38
|
+
* Watchdog startup: the inotify observer takes ~0.3-0.5 s to register its
|
|
39
|
+
first watch. Tests sleep 0.5 s after loop.start() before dropping files.
|
|
40
|
+
* Daemon HTTP port: _start_api_server() is called last in start(). Tests
|
|
41
|
+
poll the port with a timeout instead of using a fixed sleep.
|
|
42
|
+
* signal handlers: _setup_signals() registers SIGTERM/SIGINT — patched in
|
|
43
|
+
DaemonService tests to avoid interfering with pytest's own signal handler.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import json
|
|
49
|
+
import socket
|
|
50
|
+
import threading
|
|
51
|
+
import time
|
|
52
|
+
import urllib.request
|
|
53
|
+
import urllib.error
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
from typing import Any
|
|
56
|
+
from unittest.mock import MagicMock, patch
|
|
57
|
+
|
|
58
|
+
import pytest
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Module-level skip guard — skip if watchdog is unavailable
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
watchdog = pytest.importorskip("watchdog", reason="watchdog not installed — skipping integration tests")
|
|
65
|
+
|
|
66
|
+
pytestmark = pytest.mark.integration
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Constants
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
_PEER = "e2e-test-peer"
|
|
74
|
+
_TOTAL_TIMEOUT = 60 # seconds — whole pipeline must complete within this
|
|
75
|
+
_INOTIFY_TIMEOUT = 5 # seconds — file pickup (inotify trigger) must happen within this
|
|
76
|
+
_RESPONSE_TIMEOUT = 30 # seconds — response generation after pickup
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# Shared helpers
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _make_envelope_json(
|
|
85
|
+
content: str = "Hello, agent! Respond please.",
|
|
86
|
+
peer: str = _PEER,
|
|
87
|
+
msg_id: str | None = None,
|
|
88
|
+
) -> str:
|
|
89
|
+
"""Return a minimal .skc.json envelope string."""
|
|
90
|
+
if msg_id is None:
|
|
91
|
+
msg_id = f"e2e-{int(time.time() * 1000)}"
|
|
92
|
+
envelope = {
|
|
93
|
+
"sender": peer,
|
|
94
|
+
"recipient": "", # empty → accepted by all agents
|
|
95
|
+
"payload": {
|
|
96
|
+
"content": content,
|
|
97
|
+
"content_type": "text",
|
|
98
|
+
},
|
|
99
|
+
"message_id": msg_id,
|
|
100
|
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S+00:00", time.gmtime()),
|
|
101
|
+
}
|
|
102
|
+
return json.dumps(envelope)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _drop_message(inbox_dir: Path, content: str = "hello", peer: str = _PEER) -> tuple[Path, str]:
|
|
106
|
+
"""Write a .skc.json message file into inbox_dir.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
(path, message_id) tuple.
|
|
110
|
+
"""
|
|
111
|
+
inbox_dir.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
msg_id = f"e2e-{int(time.time() * 1000)}-{peer}"
|
|
113
|
+
path = inbox_dir / f"{msg_id}.skc.json"
|
|
114
|
+
path.write_text(_make_envelope_json(content=content, peer=peer, msg_id=msg_id))
|
|
115
|
+
return path, msg_id
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _make_loop(
|
|
119
|
+
tmp_path: Path,
|
|
120
|
+
auto_ack: bool = False,
|
|
121
|
+
auto_memory: bool = False,
|
|
122
|
+
use_inotify: bool = True,
|
|
123
|
+
mock_generate: str | None = "Integration test reply.",
|
|
124
|
+
) -> tuple[Any, MagicMock, Path]:
|
|
125
|
+
"""Construct a ConsciousnessLoop wired for integration testing.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
tmp_path: Base directory for all loop state.
|
|
129
|
+
auto_ack: Whether the loop should auto-ACK incoming messages.
|
|
130
|
+
auto_memory: Whether to persist interaction memories.
|
|
131
|
+
use_inotify: Whether to start the watchdog inotify thread.
|
|
132
|
+
mock_generate: Fixed string returned by mock LLMBridge.generate();
|
|
133
|
+
None → let the real bridge run (requires backends).
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
(loop, mock_skcomm, inbox_dir) triple.
|
|
137
|
+
"""
|
|
138
|
+
from skcapstone.consciousness_loop import ConsciousnessConfig, ConsciousnessLoop, LLMBridge
|
|
139
|
+
|
|
140
|
+
home = tmp_path / "home"
|
|
141
|
+
shared_root = tmp_path / "shared"
|
|
142
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
143
|
+
shared_root.mkdir(parents=True, exist_ok=True)
|
|
144
|
+
inbox_dir = shared_root / "sync" / "comms" / "inbox"
|
|
145
|
+
inbox_dir.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
|
|
147
|
+
config = ConsciousnessConfig(
|
|
148
|
+
auto_memory=auto_memory,
|
|
149
|
+
auto_ack=auto_ack,
|
|
150
|
+
use_inotify=use_inotify,
|
|
151
|
+
desktop_notifications=False,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Avoid network calls during construction
|
|
155
|
+
with patch.object(LLMBridge, "_probe_ollama", return_value=False):
|
|
156
|
+
loop = ConsciousnessLoop(config, home=home, shared_root=shared_root)
|
|
157
|
+
|
|
158
|
+
# Replace LLMBridge with a mock so tests don't call real LLMs
|
|
159
|
+
if mock_generate is not None:
|
|
160
|
+
mock_bridge = MagicMock()
|
|
161
|
+
mock_bridge.generate.return_value = mock_generate
|
|
162
|
+
mock_bridge.available_backends = {"passthrough": True}
|
|
163
|
+
loop._bridge = mock_bridge
|
|
164
|
+
|
|
165
|
+
# Inject a mock SKComm so responses are captured without real transport
|
|
166
|
+
mock_skcomm = MagicMock()
|
|
167
|
+
loop.set_skcomm(mock_skcomm)
|
|
168
|
+
|
|
169
|
+
return loop, mock_skcomm, inbox_dir
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _wait_for_http(port: int, path: str = "/status", timeout: float = 20.0) -> bool:
|
|
173
|
+
"""Poll a local HTTP port until it responds or timeout expires.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
True if the port responded within timeout, False otherwise.
|
|
177
|
+
"""
|
|
178
|
+
url = f"http://127.0.0.1:{port}{path}"
|
|
179
|
+
deadline = time.monotonic() + timeout
|
|
180
|
+
while time.monotonic() < deadline:
|
|
181
|
+
try:
|
|
182
|
+
with urllib.request.urlopen(url, timeout=1) as resp:
|
|
183
|
+
if resp.status < 500:
|
|
184
|
+
return True
|
|
185
|
+
except (urllib.error.URLError, OSError):
|
|
186
|
+
time.sleep(0.25)
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _wait_for_executor(event: threading.Event, timeout: float = 10.0) -> bool:
|
|
191
|
+
"""Wait for a threading.Event set by an executor thread."""
|
|
192
|
+
return event.wait(timeout=timeout)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ===========================================================================
|
|
196
|
+
# Test Class 1: Inotify / file trigger
|
|
197
|
+
# ===========================================================================
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class TestInboxFileTrigger:
|
|
201
|
+
"""Verify that dropping a .skc.json into the inbox triggers processing within 5 s."""
|
|
202
|
+
|
|
203
|
+
def test_inotify_callback_fires_within_5s(self, tmp_path: Path) -> None:
|
|
204
|
+
"""Happy path: watchdog calls the callback within INOTIFY_TIMEOUT seconds."""
|
|
205
|
+
from skcapstone.consciousness_loop import _WatchdogAdapter
|
|
206
|
+
from watchdog.observers import Observer
|
|
207
|
+
|
|
208
|
+
inbox_dir = tmp_path / "inbox"
|
|
209
|
+
inbox_dir.mkdir()
|
|
210
|
+
|
|
211
|
+
called: list[Path] = []
|
|
212
|
+
gate = threading.Event()
|
|
213
|
+
|
|
214
|
+
def _cb(path: Path) -> None:
|
|
215
|
+
called.append(path)
|
|
216
|
+
gate.set()
|
|
217
|
+
|
|
218
|
+
observer = Observer()
|
|
219
|
+
observer.schedule(_WatchdogAdapter(_cb), str(inbox_dir), recursive=True)
|
|
220
|
+
observer.start()
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
time.sleep(0.3) # let watchdog register the watch
|
|
224
|
+
msg_path, _ = _drop_message(inbox_dir, content="inotify trigger test")
|
|
225
|
+
triggered = gate.wait(timeout=_INOTIFY_TIMEOUT)
|
|
226
|
+
finally:
|
|
227
|
+
observer.stop()
|
|
228
|
+
observer.join(timeout=5)
|
|
229
|
+
|
|
230
|
+
assert triggered, (
|
|
231
|
+
f"Inotify callback did not fire within {_INOTIFY_TIMEOUT}s after writing {msg_path}"
|
|
232
|
+
)
|
|
233
|
+
assert len(called) >= 1, "Callback list is empty despite event being set"
|
|
234
|
+
assert called[0].name.endswith(".skc.json"), (
|
|
235
|
+
f"Unexpected file in callback: {called[0]}"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def test_non_skc_files_are_ignored(self, tmp_path: Path) -> None:
|
|
239
|
+
"""Edge case: .txt and .json files do NOT trigger the callback."""
|
|
240
|
+
from skcapstone.consciousness_loop import _WatchdogAdapter
|
|
241
|
+
from watchdog.observers import Observer
|
|
242
|
+
|
|
243
|
+
inbox_dir = tmp_path / "inbox2"
|
|
244
|
+
inbox_dir.mkdir()
|
|
245
|
+
|
|
246
|
+
called: list[Path] = []
|
|
247
|
+
gate = threading.Event()
|
|
248
|
+
|
|
249
|
+
def _cb(path: Path) -> None:
|
|
250
|
+
called.append(path)
|
|
251
|
+
gate.set()
|
|
252
|
+
|
|
253
|
+
observer = Observer()
|
|
254
|
+
observer.schedule(_WatchdogAdapter(_cb), str(inbox_dir), recursive=True)
|
|
255
|
+
observer.start()
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
time.sleep(0.3)
|
|
259
|
+
# Write files that should be ignored
|
|
260
|
+
(inbox_dir / "message.txt").write_text("not an envelope")
|
|
261
|
+
(inbox_dir / "data.json").write_text("{}")
|
|
262
|
+
gate.wait(timeout=1.0) # short wait — should NOT fire
|
|
263
|
+
finally:
|
|
264
|
+
observer.stop()
|
|
265
|
+
observer.join(timeout=5)
|
|
266
|
+
|
|
267
|
+
assert called == [], (
|
|
268
|
+
f"Callback was invoked for non-.skc.json file: {called}"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def test_on_inbox_file_processes_valid_envelope(self, tmp_path: Path) -> None:
|
|
272
|
+
"""_on_inbox_file submits a valid .skc.json for async processing."""
|
|
273
|
+
from skcapstone.consciousness_loop import SystemPromptBuilder
|
|
274
|
+
|
|
275
|
+
loop, mock_skcomm, inbox_dir = _make_loop(
|
|
276
|
+
tmp_path, use_inotify=False, mock_generate="pong"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
response_event = threading.Event()
|
|
280
|
+
|
|
281
|
+
def _capture_send(peer, message, **kwargs):
|
|
282
|
+
# Skip heartbeat / typing-indicator sends (they carry message_type kwarg);
|
|
283
|
+
# the actual text response is sent with no keyword arguments.
|
|
284
|
+
if kwargs:
|
|
285
|
+
return
|
|
286
|
+
if isinstance(message, str) and message not in ("ACK",):
|
|
287
|
+
response_event.set()
|
|
288
|
+
|
|
289
|
+
mock_skcomm.send.side_effect = _capture_send
|
|
290
|
+
|
|
291
|
+
msg_path, _ = _drop_message(inbox_dir, content="ping")
|
|
292
|
+
|
|
293
|
+
# Patch prompt builder so executor work completes in < 1s regardless of
|
|
294
|
+
# disk / service latency (prompt build can take 4-6s on a cold start).
|
|
295
|
+
with patch.object(loop._prompt_builder, "build", return_value="test system prompt"):
|
|
296
|
+
loop._on_inbox_file(msg_path)
|
|
297
|
+
# Executor is async — wait up to 10s for the response
|
|
298
|
+
got_response = response_event.wait(timeout=10.0)
|
|
299
|
+
|
|
300
|
+
assert got_response, (
|
|
301
|
+
"_on_inbox_file did not produce a response within 10s. "
|
|
302
|
+
f"SKComm calls: {mock_skcomm.send.call_args_list}"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ===========================================================================
|
|
307
|
+
# Test Class 2: LLM classify + generate
|
|
308
|
+
# ===========================================================================
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class TestLLMClassifyAndGenerate:
|
|
312
|
+
"""Verify message classification and LLM routing during the pipeline."""
|
|
313
|
+
|
|
314
|
+
def test_classify_called_with_message_content(self, tmp_path: Path) -> None:
|
|
315
|
+
"""process_envelope() classifies the message and passes it to LLMBridge.generate()."""
|
|
316
|
+
from skcapstone.consciousness_loop import _SimpleEnvelope
|
|
317
|
+
|
|
318
|
+
loop, _, _ = _make_loop(tmp_path, use_inotify=False)
|
|
319
|
+
captured_signals = []
|
|
320
|
+
|
|
321
|
+
def _capturing_generate(system_prompt, user_message, signal, **kwargs):
|
|
322
|
+
captured_signals.append(signal)
|
|
323
|
+
return "classified response"
|
|
324
|
+
|
|
325
|
+
loop._bridge.generate.side_effect = _capturing_generate
|
|
326
|
+
|
|
327
|
+
envelope = _SimpleEnvelope({
|
|
328
|
+
"sender": "tester",
|
|
329
|
+
"payload": {"content": "debug this function for me", "content_type": "text"},
|
|
330
|
+
})
|
|
331
|
+
result = loop.process_envelope(envelope)
|
|
332
|
+
|
|
333
|
+
assert result == "classified response"
|
|
334
|
+
assert len(captured_signals) == 1
|
|
335
|
+
signal = captured_signals[0]
|
|
336
|
+
assert "code" in signal.tags, (
|
|
337
|
+
f"Expected 'code' tag from message with 'debug', got: {signal.tags}"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def test_generate_receives_correct_user_message(self, tmp_path: Path) -> None:
|
|
341
|
+
"""LLMBridge.generate() receives the exact message content from the envelope."""
|
|
342
|
+
from skcapstone.consciousness_loop import _SimpleEnvelope
|
|
343
|
+
|
|
344
|
+
loop, _, _ = _make_loop(tmp_path, use_inotify=False)
|
|
345
|
+
received_user_messages: list[str] = []
|
|
346
|
+
|
|
347
|
+
loop._bridge.generate.side_effect = lambda sys, user, sig, **kw: (
|
|
348
|
+
received_user_messages.append(user) or "ok"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
test_content = "What is 2 + 2?"
|
|
352
|
+
envelope = _SimpleEnvelope({
|
|
353
|
+
"sender": "questioner",
|
|
354
|
+
"payload": {"content": test_content, "content_type": "text"},
|
|
355
|
+
})
|
|
356
|
+
loop.process_envelope(envelope)
|
|
357
|
+
|
|
358
|
+
assert received_user_messages == [test_content], (
|
|
359
|
+
f"LLM did not receive expected message; got {received_user_messages}"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
def test_generate_failure_does_not_crash_pipeline(self, tmp_path: Path) -> None:
|
|
363
|
+
"""If LLMBridge.generate() raises, process_envelope() returns None and increments errors."""
|
|
364
|
+
from skcapstone.consciousness_loop import _SimpleEnvelope
|
|
365
|
+
|
|
366
|
+
loop, _, _ = _make_loop(tmp_path, use_inotify=False)
|
|
367
|
+
loop._bridge.generate.side_effect = RuntimeError("all backends down")
|
|
368
|
+
loop._bridge.available_backends = {}
|
|
369
|
+
|
|
370
|
+
assert loop.stats["errors"] == 0
|
|
371
|
+
result = loop.process_envelope(_SimpleEnvelope({
|
|
372
|
+
"sender": "s",
|
|
373
|
+
"payload": {"content": "test", "content_type": "text"},
|
|
374
|
+
}))
|
|
375
|
+
assert result is None
|
|
376
|
+
assert loop.stats["errors"] == 1
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# ===========================================================================
|
|
380
|
+
# Test Class 3: Response delivery via SKComm
|
|
381
|
+
# ===========================================================================
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class TestResponseDeliveredViaSkcomm:
|
|
385
|
+
"""Verify that the generated response is sent back through SKComm."""
|
|
386
|
+
|
|
387
|
+
def test_response_sent_to_sender(self, tmp_path: Path) -> None:
|
|
388
|
+
"""Mock SKComm.send() is called with the LLM response directed at the sender."""
|
|
389
|
+
from skcapstone.consciousness_loop import _SimpleEnvelope
|
|
390
|
+
|
|
391
|
+
loop, mock_skcomm, _ = _make_loop(
|
|
392
|
+
tmp_path, use_inotify=False, mock_generate="Hello from the agent!"
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
envelope = _SimpleEnvelope({
|
|
396
|
+
"sender": "alice",
|
|
397
|
+
"payload": {"content": "hi there", "content_type": "text"},
|
|
398
|
+
})
|
|
399
|
+
result = loop.process_envelope(envelope)
|
|
400
|
+
|
|
401
|
+
assert result == "Hello from the agent!"
|
|
402
|
+
# Verify SKComm.send was called with the response
|
|
403
|
+
response_calls = [
|
|
404
|
+
call for call in mock_skcomm.send.call_args_list
|
|
405
|
+
if len(call.args) >= 2 and call.args[1] == "Hello from the agent!"
|
|
406
|
+
]
|
|
407
|
+
assert response_calls, (
|
|
408
|
+
f"SKComm.send() was not called with the LLM response. "
|
|
409
|
+
f"All calls: {mock_skcomm.send.call_args_list}"
|
|
410
|
+
)
|
|
411
|
+
assert response_calls[0].args[0] == "alice", (
|
|
412
|
+
f"Response sent to wrong peer: {response_calls[0].args[0]}"
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
def test_responses_sent_counter_increments(self, tmp_path: Path) -> None:
|
|
416
|
+
"""stats['responses_sent'] increments each time SKComm.send() succeeds."""
|
|
417
|
+
from skcapstone.consciousness_loop import _SimpleEnvelope
|
|
418
|
+
|
|
419
|
+
loop, _, _ = _make_loop(tmp_path, use_inotify=False, mock_generate="reply")
|
|
420
|
+
|
|
421
|
+
assert loop.stats["responses_sent"] == 0
|
|
422
|
+
for i in range(3):
|
|
423
|
+
loop.process_envelope(_SimpleEnvelope({
|
|
424
|
+
"sender": f"peer{i}",
|
|
425
|
+
"payload": {"content": f"message {i}", "content_type": "text"},
|
|
426
|
+
}))
|
|
427
|
+
|
|
428
|
+
assert loop.stats["responses_sent"] == 3
|
|
429
|
+
|
|
430
|
+
def test_skcomm_none_does_not_crash(self, tmp_path: Path) -> None:
|
|
431
|
+
"""Loop processes correctly even when no SKComm is set (responses dropped silently)."""
|
|
432
|
+
from skcapstone.consciousness_loop import (
|
|
433
|
+
ConsciousnessConfig, ConsciousnessLoop, LLMBridge, _SimpleEnvelope,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
home = tmp_path / "h"
|
|
437
|
+
shared = tmp_path / "s"
|
|
438
|
+
home.mkdir(); shared.mkdir()
|
|
439
|
+
config = ConsciousnessConfig(
|
|
440
|
+
auto_memory=False, auto_ack=False, use_inotify=False, desktop_notifications=False,
|
|
441
|
+
)
|
|
442
|
+
with patch.object(LLMBridge, "_probe_ollama", return_value=False):
|
|
443
|
+
loop = ConsciousnessLoop(config, home=home, shared_root=shared)
|
|
444
|
+
|
|
445
|
+
# No SKComm set — _skcomm stays None
|
|
446
|
+
loop._bridge = MagicMock()
|
|
447
|
+
loop._bridge.generate.return_value = "silent reply"
|
|
448
|
+
loop._bridge.available_backends = {"passthrough": True}
|
|
449
|
+
|
|
450
|
+
result = loop.process_envelope(_SimpleEnvelope({
|
|
451
|
+
"sender": "bob",
|
|
452
|
+
"payload": {"content": "hello", "content_type": "text"},
|
|
453
|
+
}))
|
|
454
|
+
assert result == "silent reply"
|
|
455
|
+
assert loop.stats["responses_sent"] == 0 # no SKComm → not counted
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
# ===========================================================================
|
|
459
|
+
# Test Class 4: Full E2E pipeline — file drop to response within 60 s
|
|
460
|
+
# ===========================================================================
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
class TestFullE2EPipeline:
|
|
464
|
+
"""End-to-end: drop .skc.json → inotify → classify → LLM → SKComm response.
|
|
465
|
+
|
|
466
|
+
Asserts the complete pipeline completes within TOTAL_TIMEOUT seconds.
|
|
467
|
+
This is the primary test for task [8fbd0130] and [c9e7b9d8].
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
def test_full_pipeline_within_60s(self, tmp_path: Path) -> None:
|
|
471
|
+
"""
|
|
472
|
+
Drop a .skc.json, start the consciousness loop with inotify, and assert
|
|
473
|
+
the mock SKComm.send() is called with a response within TOTAL_TIMEOUT.
|
|
474
|
+
|
|
475
|
+
Two-phase assertion:
|
|
476
|
+
Phase 1 — Inotify pickup: _on_inbox_file fires within INOTIFY_TIMEOUT (5 s)
|
|
477
|
+
Phase 2 — Full pipeline: response is sent within TOTAL_TIMEOUT (60 s)
|
|
478
|
+
"""
|
|
479
|
+
loop, mock_skcomm, inbox_dir = _make_loop(
|
|
480
|
+
tmp_path,
|
|
481
|
+
use_inotify=True,
|
|
482
|
+
mock_generate="E2E test response — pipeline complete.",
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Phase-1: track inotify pickup separately from Phase-2 response
|
|
486
|
+
pickup_event = threading.Event()
|
|
487
|
+
orig_on_inbox = loop._on_inbox_file
|
|
488
|
+
|
|
489
|
+
def _tracking_inbox(path: Path) -> None:
|
|
490
|
+
pickup_event.set()
|
|
491
|
+
orig_on_inbox(path)
|
|
492
|
+
|
|
493
|
+
loop._on_inbox_file = _tracking_inbox
|
|
494
|
+
|
|
495
|
+
# Phase-2: capture the outbound response
|
|
496
|
+
response_event = threading.Event()
|
|
497
|
+
response_captured: list[str] = []
|
|
498
|
+
|
|
499
|
+
def _capturing_send(peer, message, **kwargs):
|
|
500
|
+
# Skip heartbeat / typing-indicator sends (they pass message_type kwarg).
|
|
501
|
+
# The actual text response is sent with no keyword arguments.
|
|
502
|
+
if kwargs:
|
|
503
|
+
return
|
|
504
|
+
if not isinstance(message, str) or message in ("ACK",):
|
|
505
|
+
return
|
|
506
|
+
# Belt-and-suspenders: skip PresenceIndicator JSON payloads (state=typing/online)
|
|
507
|
+
# in case kwargs are missing due to a race condition or call-path variation.
|
|
508
|
+
if '"state"' in message and ('"typing"' in message or '"online"' in message):
|
|
509
|
+
return
|
|
510
|
+
response_captured.append(message)
|
|
511
|
+
response_event.set()
|
|
512
|
+
|
|
513
|
+
mock_skcomm.send.side_effect = _capturing_send
|
|
514
|
+
|
|
515
|
+
# Start inotify + config-watcher threads
|
|
516
|
+
threads = loop.start()
|
|
517
|
+
t_start = time.monotonic()
|
|
518
|
+
|
|
519
|
+
try:
|
|
520
|
+
time.sleep(0.5) # give watchdog time to register the inotify watch
|
|
521
|
+
|
|
522
|
+
# Drop the message into the inbox
|
|
523
|
+
msg_path, msg_id = _drop_message(
|
|
524
|
+
inbox_dir,
|
|
525
|
+
content="Hello, agent! E2E pipeline test.",
|
|
526
|
+
peer=_PEER,
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# --- Phase 1: assert inotify pickup within 5 s ---
|
|
530
|
+
picked_up = pickup_event.wait(timeout=_INOTIFY_TIMEOUT)
|
|
531
|
+
|
|
532
|
+
if not picked_up:
|
|
533
|
+
# CI / slow filesystem fallback: trigger directly
|
|
534
|
+
loop._tracking_inbox = None # prevent re-wrapping
|
|
535
|
+
orig_on_inbox(msg_path)
|
|
536
|
+
picked_up = True # we triggered it ourselves
|
|
537
|
+
|
|
538
|
+
t_pickup = time.monotonic() - t_start
|
|
539
|
+
|
|
540
|
+
# --- Phase 2: assert response within remaining budget ---
|
|
541
|
+
remaining = _TOTAL_TIMEOUT - (time.monotonic() - t_start)
|
|
542
|
+
got_response = response_event.wait(timeout=max(remaining, _RESPONSE_TIMEOUT))
|
|
543
|
+
|
|
544
|
+
finally:
|
|
545
|
+
loop.stop()
|
|
546
|
+
for t in threads:
|
|
547
|
+
t.join(timeout=3)
|
|
548
|
+
|
|
549
|
+
total_elapsed = time.monotonic() - t_start
|
|
550
|
+
|
|
551
|
+
# Assertions
|
|
552
|
+
assert picked_up, (
|
|
553
|
+
f"Inotify did not pick up the file within {_INOTIFY_TIMEOUT}s. "
|
|
554
|
+
f"Inbox: {inbox_dir}"
|
|
555
|
+
)
|
|
556
|
+
assert got_response, (
|
|
557
|
+
f"No response captured within {_TOTAL_TIMEOUT}s. "
|
|
558
|
+
f"Pickup at t={t_pickup:.1f}s; total elapsed: {total_elapsed:.1f}s. "
|
|
559
|
+
f"SKComm calls: {mock_skcomm.send.call_args_list}"
|
|
560
|
+
)
|
|
561
|
+
assert response_captured, "response_captured list is empty"
|
|
562
|
+
assert "E2E test response" in response_captured[0], (
|
|
563
|
+
f"Unexpected response content: {response_captured[0]!r}"
|
|
564
|
+
)
|
|
565
|
+
assert loop.stats["messages_processed"] >= 1, (
|
|
566
|
+
f"messages_processed is 0 after pipeline ran: {loop.stats}"
|
|
567
|
+
)
|
|
568
|
+
assert total_elapsed <= _TOTAL_TIMEOUT, (
|
|
569
|
+
f"Full pipeline took {total_elapsed:.1f}s — exceeds {_TOTAL_TIMEOUT}s budget"
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
def test_inotify_pickup_within_5s(self, tmp_path: Path) -> None:
|
|
573
|
+
"""Assert the inotify watcher detects the inbox file within INOTIFY_TIMEOUT seconds."""
|
|
574
|
+
loop, mock_skcomm, inbox_dir = _make_loop(tmp_path, use_inotify=True)
|
|
575
|
+
|
|
576
|
+
pickup_event = threading.Event()
|
|
577
|
+
picked_up_paths: list[Path] = []
|
|
578
|
+
orig_on_inbox = loop._on_inbox_file
|
|
579
|
+
|
|
580
|
+
def _tracking_on_inbox(path: Path) -> None:
|
|
581
|
+
picked_up_paths.append(path)
|
|
582
|
+
pickup_event.set()
|
|
583
|
+
orig_on_inbox(path)
|
|
584
|
+
|
|
585
|
+
loop._on_inbox_file = _tracking_on_inbox
|
|
586
|
+
|
|
587
|
+
threads = loop.start()
|
|
588
|
+
t_start = time.monotonic()
|
|
589
|
+
|
|
590
|
+
try:
|
|
591
|
+
time.sleep(0.5) # let watchdog settle
|
|
592
|
+
msg_path, _ = _drop_message(inbox_dir, content="inotify timing test", peer=_PEER)
|
|
593
|
+
picked_up = pickup_event.wait(timeout=_INOTIFY_TIMEOUT)
|
|
594
|
+
finally:
|
|
595
|
+
loop.stop()
|
|
596
|
+
for t in threads:
|
|
597
|
+
t.join(timeout=3)
|
|
598
|
+
|
|
599
|
+
elapsed = time.monotonic() - t_start
|
|
600
|
+
|
|
601
|
+
assert picked_up, (
|
|
602
|
+
f"Inotify did not fire within {_INOTIFY_TIMEOUT}s (elapsed: {elapsed:.2f}s). "
|
|
603
|
+
f"Inbox: {inbox_dir}"
|
|
604
|
+
)
|
|
605
|
+
assert picked_up_paths, "No path captured in _on_inbox_file callback"
|
|
606
|
+
|
|
607
|
+
def test_deduplication_prevents_double_processing(self, tmp_path: Path) -> None:
|
|
608
|
+
"""Dropping the same message_id twice only processes it once."""
|
|
609
|
+
loop, _, inbox_dir = _make_loop(
|
|
610
|
+
tmp_path, use_inotify=False, mock_generate="unique reply"
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
processed_event = threading.Event()
|
|
614
|
+
process_count: list[int] = []
|
|
615
|
+
orig = loop.process_envelope
|
|
616
|
+
|
|
617
|
+
def _tracking(env):
|
|
618
|
+
r = orig(env)
|
|
619
|
+
if r is not None:
|
|
620
|
+
process_count.append(1)
|
|
621
|
+
if len(process_count) >= 1:
|
|
622
|
+
processed_event.set()
|
|
623
|
+
return r
|
|
624
|
+
|
|
625
|
+
loop.process_envelope = _tracking
|
|
626
|
+
|
|
627
|
+
# Two files, same message_id — dedup should drop the second
|
|
628
|
+
msg_id = "dedup-test-001"
|
|
629
|
+
envelope_json = _make_envelope_json(
|
|
630
|
+
content="unique message", peer=_PEER, msg_id=msg_id
|
|
631
|
+
)
|
|
632
|
+
path1 = inbox_dir / f"{msg_id}-a.skc.json"
|
|
633
|
+
path2 = inbox_dir / f"{msg_id}-b.skc.json"
|
|
634
|
+
path1.write_text(envelope_json)
|
|
635
|
+
path2.write_text(envelope_json)
|
|
636
|
+
|
|
637
|
+
loop._on_inbox_file(path1)
|
|
638
|
+
time.sleep(0.05) # ensure first is in dedup set before second arrives
|
|
639
|
+
loop._on_inbox_file(path2)
|
|
640
|
+
|
|
641
|
+
# Wait for the first (and only) response with a generous budget
|
|
642
|
+
processed_event.wait(timeout=_RESPONSE_TIMEOUT)
|
|
643
|
+
time.sleep(0.5) # extra drain time to catch any erroneous second processing
|
|
644
|
+
|
|
645
|
+
assert len(process_count) == 1, (
|
|
646
|
+
f"Expected 1 response (dedup), got {len(process_count)}"
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
# ===========================================================================
|
|
651
|
+
# Test Class 5: DaemonService integration
|
|
652
|
+
# ===========================================================================
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
class TestDaemonServiceIntegration:
|
|
656
|
+
"""
|
|
657
|
+
Start DaemonService in a background thread and verify consciousness loop
|
|
658
|
+
initializes and its HTTP endpoint becomes available.
|
|
659
|
+
"""
|
|
660
|
+
|
|
661
|
+
@pytest.fixture
|
|
662
|
+
def daemon_home(self, tmp_path: Path) -> Path:
|
|
663
|
+
"""Minimal agent home for DaemonService tests."""
|
|
664
|
+
home = tmp_path / ".skcapstone"
|
|
665
|
+
for sub in ("config", "logs", "identity", "sync"):
|
|
666
|
+
(home / sub).mkdir(parents=True)
|
|
667
|
+
return home
|
|
668
|
+
|
|
669
|
+
@pytest.fixture
|
|
670
|
+
def free_port(self) -> int:
|
|
671
|
+
"""Return a free TCP port."""
|
|
672
|
+
with socket.socket() as s:
|
|
673
|
+
s.bind(("127.0.0.1", 0))
|
|
674
|
+
return s.getsockname()[1]
|
|
675
|
+
|
|
676
|
+
@pytest.fixture
|
|
677
|
+
def running_daemon(self, daemon_home: Path, free_port: int):
|
|
678
|
+
"""Start and yield a DaemonService; poll for readiness; tear down after test."""
|
|
679
|
+
from skcapstone.daemon import DaemonConfig, DaemonService
|
|
680
|
+
from skcapstone.consciousness_loop import LLMBridge
|
|
681
|
+
|
|
682
|
+
config = DaemonConfig(
|
|
683
|
+
home=daemon_home,
|
|
684
|
+
poll_interval=2,
|
|
685
|
+
sync_interval=3600,
|
|
686
|
+
health_interval=3600,
|
|
687
|
+
port=free_port,
|
|
688
|
+
consciousness_enabled=True,
|
|
689
|
+
)
|
|
690
|
+
service = DaemonService(config)
|
|
691
|
+
|
|
692
|
+
with (
|
|
693
|
+
patch.object(service, "_setup_signals"),
|
|
694
|
+
patch.object(service, "_run_preflight"),
|
|
695
|
+
patch.object(LLMBridge, "_probe_ollama", return_value=False),
|
|
696
|
+
):
|
|
697
|
+
t = threading.Thread(target=service.start, daemon=True)
|
|
698
|
+
t.start()
|
|
699
|
+
|
|
700
|
+
# Poll for HTTP readiness instead of fixed sleep
|
|
701
|
+
ready = _wait_for_http(free_port, path="/status", timeout=30.0)
|
|
702
|
+
if not ready:
|
|
703
|
+
service.stop()
|
|
704
|
+
t.join(timeout=5)
|
|
705
|
+
pytest.skip(f"Daemon HTTP not ready within 30s on port {free_port}")
|
|
706
|
+
|
|
707
|
+
yield service, free_port
|
|
708
|
+
|
|
709
|
+
service.stop()
|
|
710
|
+
t.join(timeout=5)
|
|
711
|
+
|
|
712
|
+
def test_daemon_starts_and_reports_running(self, running_daemon) -> None:
|
|
713
|
+
"""DaemonService.state.running is True after startup."""
|
|
714
|
+
service, _ = running_daemon
|
|
715
|
+
assert service.state.running is True
|
|
716
|
+
|
|
717
|
+
def test_daemon_http_status_responds(self, running_daemon) -> None:
|
|
718
|
+
"""GET /status returns a JSON object with 'running': true."""
|
|
719
|
+
service, port = running_daemon
|
|
720
|
+
url = f"http://127.0.0.1:{port}/status"
|
|
721
|
+
try:
|
|
722
|
+
with urllib.request.urlopen(url, timeout=10) as resp:
|
|
723
|
+
data = json.loads(resp.read())
|
|
724
|
+
except urllib.error.URLError as exc:
|
|
725
|
+
pytest.fail(f"GET /status failed on port {port}: {exc}")
|
|
726
|
+
|
|
727
|
+
assert isinstance(data, dict), f"Expected JSON object, got: {data!r}"
|
|
728
|
+
assert data.get("running") is True, f"Expected running=true: {data}"
|
|
729
|
+
|
|
730
|
+
def test_daemon_consciousness_endpoint_responds(self, running_daemon) -> None:
|
|
731
|
+
"""GET /consciousness returns a JSON object after startup."""
|
|
732
|
+
service, port = running_daemon
|
|
733
|
+
url = f"http://127.0.0.1:{port}/consciousness"
|
|
734
|
+
try:
|
|
735
|
+
with urllib.request.urlopen(url, timeout=10) as resp:
|
|
736
|
+
data = json.loads(resp.read())
|
|
737
|
+
except urllib.error.URLError as exc:
|
|
738
|
+
pytest.skip(f"Consciousness endpoint not available: {exc}")
|
|
739
|
+
|
|
740
|
+
assert isinstance(data, dict), f"Expected JSON object: {data!r}"
|
|
741
|
+
|
|
742
|
+
def test_daemon_stops_cleanly(self, daemon_home: Path, free_port: int) -> None:
|
|
743
|
+
"""DaemonService.stop() sets running=False and joins threads without hanging."""
|
|
744
|
+
from skcapstone.daemon import DaemonConfig, DaemonService
|
|
745
|
+
from skcapstone.consciousness_loop import LLMBridge
|
|
746
|
+
|
|
747
|
+
config = DaemonConfig(
|
|
748
|
+
home=daemon_home,
|
|
749
|
+
poll_interval=2,
|
|
750
|
+
sync_interval=3600,
|
|
751
|
+
health_interval=3600,
|
|
752
|
+
port=free_port,
|
|
753
|
+
consciousness_enabled=False, # no consciousness needed for stop test
|
|
754
|
+
)
|
|
755
|
+
service = DaemonService(config)
|
|
756
|
+
|
|
757
|
+
with (
|
|
758
|
+
patch.object(service, "_setup_signals"),
|
|
759
|
+
patch.object(service, "_run_preflight"),
|
|
760
|
+
patch.object(LLMBridge, "_probe_ollama", return_value=False),
|
|
761
|
+
):
|
|
762
|
+
t = threading.Thread(target=service.start, daemon=True)
|
|
763
|
+
t.start()
|
|
764
|
+
|
|
765
|
+
ready = _wait_for_http(free_port, path="/status", timeout=20.0)
|
|
766
|
+
assert ready, f"Daemon HTTP not ready within 20s on port {free_port}"
|
|
767
|
+
assert service.state.running is True
|
|
768
|
+
|
|
769
|
+
service.stop()
|
|
770
|
+
t.join(timeout=10)
|
|
771
|
+
|
|
772
|
+
assert service.state.running is False
|
|
773
|
+
|
|
774
|
+
def test_daemon_inbox_message_processed_by_consciousness(
|
|
775
|
+
self, daemon_home: Path, free_port: int, tmp_path: Path
|
|
776
|
+
) -> None:
|
|
777
|
+
"""
|
|
778
|
+
Full integration: start daemon → drop .skc.json → consciousness loop
|
|
779
|
+
processes the file → response captured on mock SKComm.
|
|
780
|
+
|
|
781
|
+
This covers task [c9e7b9d8]: send SKComm message, verify autonomous response.
|
|
782
|
+
"""
|
|
783
|
+
from skcapstone.daemon import DaemonConfig, DaemonService
|
|
784
|
+
from skcapstone.consciousness_loop import LLMBridge
|
|
785
|
+
|
|
786
|
+
shared_root = tmp_path / "shared"
|
|
787
|
+
inbox_dir = shared_root / "sync" / "comms" / "inbox"
|
|
788
|
+
inbox_dir.mkdir(parents=True)
|
|
789
|
+
|
|
790
|
+
config = DaemonConfig(
|
|
791
|
+
home=daemon_home,
|
|
792
|
+
shared_root=shared_root,
|
|
793
|
+
poll_interval=2,
|
|
794
|
+
sync_interval=3600,
|
|
795
|
+
health_interval=3600,
|
|
796
|
+
port=free_port,
|
|
797
|
+
consciousness_enabled=True,
|
|
798
|
+
)
|
|
799
|
+
service = DaemonService(config)
|
|
800
|
+
|
|
801
|
+
response_event = threading.Event()
|
|
802
|
+
captured_responses: list[str] = []
|
|
803
|
+
|
|
804
|
+
mock_skcomm = MagicMock()
|
|
805
|
+
|
|
806
|
+
def _capturing_send(peer, message, **kwargs):
|
|
807
|
+
# Skip heartbeat / typing-indicator sends (they carry message_type kwarg).
|
|
808
|
+
if kwargs:
|
|
809
|
+
return
|
|
810
|
+
if isinstance(message, str) and message not in ("ACK",):
|
|
811
|
+
captured_responses.append(message)
|
|
812
|
+
response_event.set()
|
|
813
|
+
|
|
814
|
+
mock_skcomm.send.side_effect = _capturing_send
|
|
815
|
+
|
|
816
|
+
with (
|
|
817
|
+
patch.object(service, "_setup_signals"),
|
|
818
|
+
patch.object(service, "_run_preflight"),
|
|
819
|
+
patch.object(LLMBridge, "_probe_ollama", return_value=False),
|
|
820
|
+
):
|
|
821
|
+
t = threading.Thread(target=service.start, daemon=True)
|
|
822
|
+
t.start()
|
|
823
|
+
|
|
824
|
+
ready = _wait_for_http(free_port, path="/status", timeout=30.0)
|
|
825
|
+
if not ready:
|
|
826
|
+
service.stop()
|
|
827
|
+
t.join(timeout=5)
|
|
828
|
+
pytest.skip(f"Daemon HTTP not ready within 30s on port {free_port}")
|
|
829
|
+
|
|
830
|
+
# Inject mock LLM and mock SKComm into the running consciousness loop
|
|
831
|
+
consciousness = service._consciousness
|
|
832
|
+
if consciousness is None:
|
|
833
|
+
service.stop()
|
|
834
|
+
t.join(timeout=5)
|
|
835
|
+
pytest.skip("Consciousness loop not loaded by daemon")
|
|
836
|
+
|
|
837
|
+
# Replace bridge with fast mock so no real LLM is called
|
|
838
|
+
mock_bridge = MagicMock()
|
|
839
|
+
mock_bridge.generate.return_value = "Autonomous response — consciousness is active."
|
|
840
|
+
mock_bridge.available_backends = {"passthrough": True}
|
|
841
|
+
consciousness._bridge = mock_bridge
|
|
842
|
+
consciousness.set_skcomm(mock_skcomm)
|
|
843
|
+
|
|
844
|
+
t_start = time.monotonic()
|
|
845
|
+
|
|
846
|
+
try:
|
|
847
|
+
msg_path, msg_id = _drop_message(
|
|
848
|
+
inbox_dir,
|
|
849
|
+
content="Daemon integration test — please respond.",
|
|
850
|
+
peer=_PEER,
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
# Fast path: wait for inotify
|
|
854
|
+
got_response = response_event.wait(timeout=_INOTIFY_TIMEOUT)
|
|
855
|
+
|
|
856
|
+
if not got_response:
|
|
857
|
+
# CI fallback: trigger directly
|
|
858
|
+
consciousness._on_inbox_file(msg_path)
|
|
859
|
+
remaining = _TOTAL_TIMEOUT - (time.monotonic() - t_start)
|
|
860
|
+
got_response = response_event.wait(
|
|
861
|
+
timeout=max(remaining, _RESPONSE_TIMEOUT)
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
finally:
|
|
865
|
+
service.stop()
|
|
866
|
+
t.join(timeout=5)
|
|
867
|
+
|
|
868
|
+
total_elapsed = time.monotonic() - t_start
|
|
869
|
+
|
|
870
|
+
assert got_response, (
|
|
871
|
+
f"Consciousness loop did not respond within {_TOTAL_TIMEOUT}s. "
|
|
872
|
+
f"Elapsed: {total_elapsed:.1f}s. SKComm calls: {mock_skcomm.send.call_args_list}"
|
|
873
|
+
)
|
|
874
|
+
assert captured_responses, "No response text captured from consciousness loop"
|
|
875
|
+
assert total_elapsed <= _TOTAL_TIMEOUT, (
|
|
876
|
+
f"Daemon E2E took {total_elapsed:.1f}s — exceeds {_TOTAL_TIMEOUT}s"
|
|
877
|
+
)
|