@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,391 @@
|
|
|
1
|
+
"""Tests for ConversationStore and chat history CLI.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- ConversationStore.append / get_last / load / all_peers / clear
|
|
5
|
+
- Path-traversal sanitization
|
|
6
|
+
- ConsciousnessLoop integrates ConversationStore (last-10 context)
|
|
7
|
+
- `skcapstone chat history PEER` CLI command
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from unittest.mock import MagicMock, patch
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
from click.testing import CliRunner
|
|
18
|
+
|
|
19
|
+
from skcapstone.conversation_store import ConversationStore, _sanitize_peer_name
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Fixtures
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def store(tmp_path):
|
|
29
|
+
"""ConversationStore rooted in a temp directory."""
|
|
30
|
+
home = tmp_path / ".skcapstone"
|
|
31
|
+
home.mkdir()
|
|
32
|
+
return ConversationStore(home)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def populated_store(store, tmp_path):
|
|
37
|
+
"""Store with two peers pre-seeded."""
|
|
38
|
+
store.append("alice", "user", "hello alice")
|
|
39
|
+
store.append("alice", "assistant", "hi there!")
|
|
40
|
+
store.append("bob", "user", "hey bob")
|
|
41
|
+
return store
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.fixture
|
|
45
|
+
def agent_home(tmp_path):
|
|
46
|
+
"""Minimal agent home for CLI tests."""
|
|
47
|
+
home = tmp_path / ".skcapstone"
|
|
48
|
+
(home / "identity").mkdir(parents=True)
|
|
49
|
+
(home / "config").mkdir(parents=True)
|
|
50
|
+
identity = {"name": "TestAgent", "fingerprint": "AABB1234", "capauth_managed": False}
|
|
51
|
+
(home / "identity" / "identity.json").write_text(json.dumps(identity))
|
|
52
|
+
(home / "manifest.json").write_text(json.dumps({"name": "TestAgent", "version": "0.1.0"}))
|
|
53
|
+
import yaml
|
|
54
|
+
(home / "config" / "config.yaml").write_text(yaml.dump({"agent_name": "TestAgent"}))
|
|
55
|
+
return home
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# _sanitize_peer_name
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TestSanitizePeerName:
|
|
64
|
+
def test_normal_name_unchanged(self):
|
|
65
|
+
assert _sanitize_peer_name("lumina") == "lumina"
|
|
66
|
+
|
|
67
|
+
def test_strips_path_separators(self):
|
|
68
|
+
result = _sanitize_peer_name("../../etc/passwd")
|
|
69
|
+
assert "/" not in result
|
|
70
|
+
assert ".." not in result
|
|
71
|
+
|
|
72
|
+
def test_strips_null_bytes(self):
|
|
73
|
+
assert "\x00" not in _sanitize_peer_name("evil\x00peer")
|
|
74
|
+
|
|
75
|
+
def test_empty_returns_unknown(self):
|
|
76
|
+
assert _sanitize_peer_name("") == "unknown"
|
|
77
|
+
|
|
78
|
+
def test_none_returns_unknown(self):
|
|
79
|
+
assert _sanitize_peer_name(None) == "unknown" # type: ignore[arg-type]
|
|
80
|
+
|
|
81
|
+
def test_max_length_64(self):
|
|
82
|
+
assert len(_sanitize_peer_name("a" * 100)) == 64
|
|
83
|
+
|
|
84
|
+
def test_allowed_chars_preserved(self):
|
|
85
|
+
assert _sanitize_peer_name("user@domain.io") == "user@domain.io"
|
|
86
|
+
assert _sanitize_peer_name("my-peer_01") == "my-peer_01"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
# ConversationStore — basic operations
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TestConversationStoreAppend:
|
|
95
|
+
def test_append_creates_file(self, store, tmp_path):
|
|
96
|
+
store.append("alice", "user", "hello")
|
|
97
|
+
assert (tmp_path / ".skcapstone" / "conversations" / "alice.json").exists()
|
|
98
|
+
|
|
99
|
+
def test_append_returns_message_dict(self, store):
|
|
100
|
+
msg = store.append("alice", "user", "hello")
|
|
101
|
+
assert msg["role"] == "user"
|
|
102
|
+
assert msg["content"] == "hello"
|
|
103
|
+
assert "timestamp" in msg
|
|
104
|
+
|
|
105
|
+
def test_append_multiple_messages(self, store):
|
|
106
|
+
store.append("alice", "user", "msg1")
|
|
107
|
+
store.append("alice", "assistant", "msg2")
|
|
108
|
+
history = store.load("alice")
|
|
109
|
+
assert len(history) == 2
|
|
110
|
+
assert history[0]["content"] == "msg1"
|
|
111
|
+
assert history[1]["content"] == "msg2"
|
|
112
|
+
|
|
113
|
+
def test_append_stores_thread_id(self, store):
|
|
114
|
+
store.append("alice", "user", "threaded", thread_id="t-01")
|
|
115
|
+
history = store.load("alice")
|
|
116
|
+
assert history[0].get("thread_id") == "t-01"
|
|
117
|
+
|
|
118
|
+
def test_append_stores_in_reply_to(self, store):
|
|
119
|
+
store.append("alice", "assistant", "reply", in_reply_to="msg-123")
|
|
120
|
+
history = store.load("alice")
|
|
121
|
+
assert history[0].get("in_reply_to") == "msg-123"
|
|
122
|
+
|
|
123
|
+
def test_append_path_traversal_sanitized(self, store, tmp_path):
|
|
124
|
+
"""Malicious peer name is sanitized; no file written outside conversations/."""
|
|
125
|
+
store.append("../../evil", "user", "attack")
|
|
126
|
+
conv_dir = tmp_path / ".skcapstone" / "conversations"
|
|
127
|
+
files = list(conv_dir.glob("*.json"))
|
|
128
|
+
# The sanitized name must not contain path separators
|
|
129
|
+
for f in files:
|
|
130
|
+
assert "/" not in f.name
|
|
131
|
+
assert ".." not in f.name
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class TestConversationStoreGetLast:
|
|
135
|
+
def test_returns_last_n(self, store):
|
|
136
|
+
for i in range(15):
|
|
137
|
+
store.append("alice", "user", f"msg{i}")
|
|
138
|
+
last5 = store.get_last("alice", 5)
|
|
139
|
+
assert len(last5) == 5
|
|
140
|
+
assert last5[-1]["content"] == "msg14"
|
|
141
|
+
|
|
142
|
+
def test_returns_all_when_n_larger(self, store):
|
|
143
|
+
store.append("alice", "user", "only one")
|
|
144
|
+
result = store.get_last("alice", 10)
|
|
145
|
+
assert len(result) == 1
|
|
146
|
+
|
|
147
|
+
def test_empty_for_unknown_peer(self, store):
|
|
148
|
+
assert store.get_last("nobody", 10) == []
|
|
149
|
+
|
|
150
|
+
def test_n_zero_returns_empty(self, store):
|
|
151
|
+
store.append("alice", "user", "hi")
|
|
152
|
+
assert store.get_last("alice", 0) == []
|
|
153
|
+
|
|
154
|
+
def test_default_n_is_10(self, store):
|
|
155
|
+
for i in range(20):
|
|
156
|
+
store.append("alice", "user", f"msg{i}")
|
|
157
|
+
assert len(store.get_last("alice")) == 10
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class TestConversationStoreAllPeers:
|
|
161
|
+
def test_empty_when_no_dir(self, tmp_path):
|
|
162
|
+
store = ConversationStore(tmp_path / "empty")
|
|
163
|
+
assert store.all_peers() == []
|
|
164
|
+
|
|
165
|
+
def test_lists_all_peers(self, populated_store):
|
|
166
|
+
peers = populated_store.all_peers()
|
|
167
|
+
assert "alice" in peers
|
|
168
|
+
assert "bob" in peers
|
|
169
|
+
|
|
170
|
+
def test_sorted_alphabetically(self, store):
|
|
171
|
+
store.append("zara", "user", "hi")
|
|
172
|
+
store.append("anna", "user", "hi")
|
|
173
|
+
peers = store.all_peers()
|
|
174
|
+
assert peers == sorted(peers)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class TestConversationStoreClear:
|
|
178
|
+
def test_clear_removes_file(self, populated_store, tmp_path):
|
|
179
|
+
populated_store.clear("bob")
|
|
180
|
+
assert not (tmp_path / ".skcapstone" / "conversations" / "bob.json").exists()
|
|
181
|
+
|
|
182
|
+
def test_clear_returns_true_when_existed(self, populated_store):
|
|
183
|
+
assert populated_store.clear("alice") is True
|
|
184
|
+
|
|
185
|
+
def test_clear_returns_false_when_missing(self, store):
|
|
186
|
+
assert store.clear("nobody") is False
|
|
187
|
+
|
|
188
|
+
def test_clear_does_not_affect_other_peers(self, populated_store):
|
|
189
|
+
populated_store.clear("bob")
|
|
190
|
+
assert populated_store.load("alice") != []
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class TestConversationStoreFormatForPrompt:
|
|
194
|
+
def test_returns_empty_for_unknown_peer(self, store):
|
|
195
|
+
assert store.format_for_prompt("nobody") == ""
|
|
196
|
+
|
|
197
|
+
def test_includes_peer_name_header(self, store):
|
|
198
|
+
store.append("alice", "user", "hi")
|
|
199
|
+
result = store.format_for_prompt("alice")
|
|
200
|
+
assert "alice" in result
|
|
201
|
+
|
|
202
|
+
def test_includes_role_and_content(self, store):
|
|
203
|
+
store.append("alice", "user", "how are you?")
|
|
204
|
+
store.append("alice", "assistant", "doing great!")
|
|
205
|
+
result = store.format_for_prompt("alice")
|
|
206
|
+
assert "[user]" in result
|
|
207
|
+
assert "[assistant]" in result
|
|
208
|
+
assert "how are you?" in result
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
# ConsciousnessLoop integration — uses ConversationStore for context
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class TestConsciousnessLoopUsesConversationStore:
|
|
217
|
+
"""Verify ConsciousnessLoop wires ConversationStore into SystemPromptBuilder."""
|
|
218
|
+
|
|
219
|
+
def test_loop_creates_conv_store(self, tmp_path):
|
|
220
|
+
from skcapstone.consciousness_loop import ConsciousnessConfig, ConsciousnessLoop
|
|
221
|
+
from skcapstone.conversation_store import ConversationStore
|
|
222
|
+
|
|
223
|
+
home = tmp_path / ".skcapstone"
|
|
224
|
+
home.mkdir()
|
|
225
|
+
config = ConsciousnessConfig()
|
|
226
|
+
loop = ConsciousnessLoop(config, home=home, shared_root=home)
|
|
227
|
+
assert isinstance(loop._conv_store, ConversationStore)
|
|
228
|
+
|
|
229
|
+
def test_prompt_builder_has_conv_store(self, tmp_path):
|
|
230
|
+
from skcapstone.consciousness_loop import ConsciousnessConfig, ConsciousnessLoop
|
|
231
|
+
|
|
232
|
+
home = tmp_path / ".skcapstone"
|
|
233
|
+
home.mkdir()
|
|
234
|
+
config = ConsciousnessConfig()
|
|
235
|
+
loop = ConsciousnessLoop(config, home=home, shared_root=home)
|
|
236
|
+
assert loop._prompt_builder._conv_store is loop._conv_store
|
|
237
|
+
|
|
238
|
+
def test_add_to_history_writes_via_conv_store(self, tmp_path):
|
|
239
|
+
"""add_to_history via ConversationStore creates the JSON file."""
|
|
240
|
+
from skcapstone.consciousness_loop import SystemPromptBuilder
|
|
241
|
+
from skcapstone.conversation_store import ConversationStore
|
|
242
|
+
|
|
243
|
+
home = tmp_path / ".skcapstone"
|
|
244
|
+
home.mkdir()
|
|
245
|
+
store = ConversationStore(home)
|
|
246
|
+
builder = SystemPromptBuilder(home=home, conv_store=store)
|
|
247
|
+
|
|
248
|
+
builder.add_to_history("testpeer", "user", "hello world")
|
|
249
|
+
|
|
250
|
+
history = store.load("testpeer")
|
|
251
|
+
assert len(history) == 1
|
|
252
|
+
assert history[0]["role"] == "user"
|
|
253
|
+
assert history[0]["content"] == "hello world"
|
|
254
|
+
|
|
255
|
+
def test_get_peer_history_reads_from_store(self, tmp_path):
|
|
256
|
+
"""_get_peer_history returns content written directly to the store."""
|
|
257
|
+
from skcapstone.consciousness_loop import SystemPromptBuilder
|
|
258
|
+
from skcapstone.conversation_store import ConversationStore
|
|
259
|
+
|
|
260
|
+
home = tmp_path / ".skcapstone"
|
|
261
|
+
home.mkdir()
|
|
262
|
+
store = ConversationStore(home)
|
|
263
|
+
# Write directly to the store (bypassing prompt builder)
|
|
264
|
+
store.append("opus", "user", "direct write")
|
|
265
|
+
store.append("opus", "assistant", "got it")
|
|
266
|
+
|
|
267
|
+
# Build prompt builder with the same store
|
|
268
|
+
builder = SystemPromptBuilder(home=home, conv_store=store)
|
|
269
|
+
history_text = builder._get_peer_history("opus")
|
|
270
|
+
|
|
271
|
+
assert "opus" in history_text
|
|
272
|
+
assert "direct write" in history_text
|
|
273
|
+
assert "got it" in history_text
|
|
274
|
+
|
|
275
|
+
def test_loads_last_10_messages_for_context(self, tmp_path):
|
|
276
|
+
"""Context includes at most max_history_messages (10) entries."""
|
|
277
|
+
from skcapstone.consciousness_loop import SystemPromptBuilder
|
|
278
|
+
from skcapstone.conversation_store import ConversationStore
|
|
279
|
+
|
|
280
|
+
home = tmp_path / ".skcapstone"
|
|
281
|
+
home.mkdir()
|
|
282
|
+
store = ConversationStore(home)
|
|
283
|
+
for i in range(20):
|
|
284
|
+
store.append("lumina", "user", f"msg{i}")
|
|
285
|
+
|
|
286
|
+
builder = SystemPromptBuilder(home=home, conv_store=store, max_history_messages=10)
|
|
287
|
+
history_text = builder._get_peer_history("lumina")
|
|
288
|
+
|
|
289
|
+
# Only messages 10–19 should appear
|
|
290
|
+
assert "msg19" in history_text
|
|
291
|
+
assert "msg0" not in history_text
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# ---------------------------------------------------------------------------
|
|
295
|
+
# `skcapstone chat history PEER` CLI
|
|
296
|
+
# ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class TestChatHistoryCLI:
|
|
300
|
+
"""Tests for `skcapstone chat history PEER`."""
|
|
301
|
+
|
|
302
|
+
@patch("skcapstone.cli.chat.get_runtime")
|
|
303
|
+
def test_history_help(self, _mock_rt):
|
|
304
|
+
from skcapstone.cli import main
|
|
305
|
+
runner = CliRunner()
|
|
306
|
+
result = runner.invoke(main, ["chat", "history", "--help"])
|
|
307
|
+
assert result.exit_code == 0
|
|
308
|
+
assert "PEER" in result.output
|
|
309
|
+
|
|
310
|
+
@patch("skcapstone.cli.chat.get_runtime")
|
|
311
|
+
def test_history_empty(self, _mock_rt, agent_home):
|
|
312
|
+
"""No conversation → 'No conversation history' message."""
|
|
313
|
+
from skcapstone.cli import main
|
|
314
|
+
runner = CliRunner()
|
|
315
|
+
result = runner.invoke(
|
|
316
|
+
main, ["chat", "history", "nobody", "--home", str(agent_home)]
|
|
317
|
+
)
|
|
318
|
+
assert result.exit_code == 0
|
|
319
|
+
assert "No conversation history" in result.output
|
|
320
|
+
|
|
321
|
+
@patch("skcapstone.cli.chat.get_runtime")
|
|
322
|
+
def test_history_shows_messages(self, _mock_rt, agent_home):
|
|
323
|
+
"""Messages written to store appear in history output."""
|
|
324
|
+
from skcapstone.cli import main
|
|
325
|
+
from skcapstone.conversation_store import ConversationStore
|
|
326
|
+
|
|
327
|
+
store = ConversationStore(agent_home)
|
|
328
|
+
store.append("lumina", "user", "Hello Lumina!")
|
|
329
|
+
store.append("lumina", "assistant", "Hello! How can I help?")
|
|
330
|
+
|
|
331
|
+
runner = CliRunner()
|
|
332
|
+
result = runner.invoke(
|
|
333
|
+
main, ["chat", "history", "lumina", "--home", str(agent_home)]
|
|
334
|
+
)
|
|
335
|
+
assert result.exit_code == 0
|
|
336
|
+
assert "Hello Lumina!" in result.output
|
|
337
|
+
assert "Hello! How can I help?" in result.output
|
|
338
|
+
|
|
339
|
+
@patch("skcapstone.cli.chat.get_runtime")
|
|
340
|
+
def test_history_limit(self, _mock_rt, agent_home):
|
|
341
|
+
"""--limit N restricts output to last N messages."""
|
|
342
|
+
from skcapstone.cli import main
|
|
343
|
+
from skcapstone.conversation_store import ConversationStore
|
|
344
|
+
|
|
345
|
+
store = ConversationStore(agent_home)
|
|
346
|
+
for i in range(10):
|
|
347
|
+
store.append("lumina", "user", f"msg{i}")
|
|
348
|
+
|
|
349
|
+
runner = CliRunner()
|
|
350
|
+
result = runner.invoke(
|
|
351
|
+
main, ["chat", "history", "lumina", "--limit", "3", "--home", str(agent_home)]
|
|
352
|
+
)
|
|
353
|
+
assert result.exit_code == 0
|
|
354
|
+
assert "msg9" in result.output
|
|
355
|
+
assert "msg0" not in result.output
|
|
356
|
+
|
|
357
|
+
@patch("skcapstone.cli.chat.get_runtime")
|
|
358
|
+
def test_history_json_output(self, _mock_rt, agent_home):
|
|
359
|
+
"""--json flag outputs valid JSON list."""
|
|
360
|
+
from skcapstone.cli import main
|
|
361
|
+
from skcapstone.conversation_store import ConversationStore
|
|
362
|
+
|
|
363
|
+
store = ConversationStore(agent_home)
|
|
364
|
+
store.append("opus", "user", "test message")
|
|
365
|
+
|
|
366
|
+
runner = CliRunner()
|
|
367
|
+
result = runner.invoke(
|
|
368
|
+
main, ["chat", "history", "opus", "--json", "--home", str(agent_home)]
|
|
369
|
+
)
|
|
370
|
+
assert result.exit_code == 0
|
|
371
|
+
data = json.loads(result.output.strip())
|
|
372
|
+
assert isinstance(data, list)
|
|
373
|
+
assert data[0]["content"] == "test message"
|
|
374
|
+
|
|
375
|
+
@patch("skcapstone.cli.chat.get_runtime")
|
|
376
|
+
def test_history_role_labels(self, _mock_rt, agent_home):
|
|
377
|
+
"""Both user and assistant roles appear in the formatted output."""
|
|
378
|
+
from skcapstone.cli import main
|
|
379
|
+
from skcapstone.conversation_store import ConversationStore
|
|
380
|
+
|
|
381
|
+
store = ConversationStore(agent_home)
|
|
382
|
+
store.append("jarvis", "user", "status?")
|
|
383
|
+
store.append("jarvis", "assistant", "all systems nominal")
|
|
384
|
+
|
|
385
|
+
runner = CliRunner()
|
|
386
|
+
result = runner.invoke(
|
|
387
|
+
main, ["chat", "history", "jarvis", "--home", str(agent_home)]
|
|
388
|
+
)
|
|
389
|
+
assert result.exit_code == 0
|
|
390
|
+
assert "user" in result.output
|
|
391
|
+
assert "assistant" in result.output
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Tests for ConversationSummarizer and the `skcapstone chat summary` CLI.
|
|
2
|
+
|
|
3
|
+
Test coverage:
|
|
4
|
+
- Happy path: summarize() calls LLM and returns a ConversationSummary
|
|
5
|
+
- Empty conversation raises ValueError
|
|
6
|
+
- Summary is persisted to {home}/summaries/{peer}.json
|
|
7
|
+
- load_summary() retrieves stored summary
|
|
8
|
+
- Peer name sanitization (path traversal attempt)
|
|
9
|
+
- CLI: chat summary renders summary text
|
|
10
|
+
- CLI: chat summary --show-stored shows stored summary
|
|
11
|
+
- CLI: chat summary --show-stored with no stored summary shows helpful message
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from unittest.mock import MagicMock, patch
|
|
19
|
+
|
|
20
|
+
import pytest
|
|
21
|
+
from click.testing import CliRunner
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Fixtures
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def agent_home(tmp_path):
|
|
31
|
+
"""Minimal agent home with identity manifest."""
|
|
32
|
+
home = tmp_path / ".skcapstone"
|
|
33
|
+
(home / "identity").mkdir(parents=True)
|
|
34
|
+
(home / "config").mkdir(parents=True)
|
|
35
|
+
identity = {"name": "TestAgent", "fingerprint": "AABB1234", "capauth_managed": False}
|
|
36
|
+
(home / "identity" / "identity.json").write_text(json.dumps(identity))
|
|
37
|
+
(home / "manifest.json").write_text(json.dumps({"name": "TestAgent", "version": "0.1.0"}))
|
|
38
|
+
import yaml
|
|
39
|
+
(home / "config" / "config.yaml").write_text(yaml.dump({"agent_name": "TestAgent"}))
|
|
40
|
+
return home
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.fixture
|
|
44
|
+
def agent_home_with_conv(agent_home):
|
|
45
|
+
"""Agent home with a lumina conversation file."""
|
|
46
|
+
convs = agent_home / "conversations"
|
|
47
|
+
convs.mkdir(parents=True)
|
|
48
|
+
messages = [
|
|
49
|
+
{"role": "user", "content": "Hello Lumina!", "timestamp": "2026-03-01T10:00:00Z"},
|
|
50
|
+
{"role": "assistant", "content": "Hi! I'm here.", "timestamp": "2026-03-01T10:00:01Z"},
|
|
51
|
+
{"role": "user", "content": "Can you deploy the update?", "timestamp": "2026-03-01T10:01:00Z"},
|
|
52
|
+
{"role": "assistant", "content": "Sure, initiating deployment now.", "timestamp": "2026-03-01T10:01:05Z"},
|
|
53
|
+
]
|
|
54
|
+
(convs / "lumina.json").write_text(json.dumps(messages))
|
|
55
|
+
return agent_home
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _make_bridge(response: str = "Two agents discussed deployment of the update. The task was agreed upon and initiated."):
|
|
59
|
+
"""Return a mock LLMBridge with a canned generate() return value."""
|
|
60
|
+
bridge = MagicMock()
|
|
61
|
+
bridge.generate.return_value = response
|
|
62
|
+
return bridge
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# ConversationSummarizer unit tests
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestConversationSummarizer:
|
|
71
|
+
"""Unit tests for ConversationSummarizer."""
|
|
72
|
+
|
|
73
|
+
def test_summarize_happy_path(self, agent_home_with_conv):
|
|
74
|
+
"""summarize() calls LLMBridge and returns a ConversationSummary."""
|
|
75
|
+
from skcapstone.conversation_summarizer import ConversationSummarizer
|
|
76
|
+
|
|
77
|
+
bridge = _make_bridge("Lumina and agent discussed deployment. The update was initiated.")
|
|
78
|
+
summarizer = ConversationSummarizer(home=agent_home_with_conv)
|
|
79
|
+
result = summarizer.summarize("lumina", n=20, bridge=bridge)
|
|
80
|
+
|
|
81
|
+
assert result.peer == "lumina"
|
|
82
|
+
assert "deployment" in result.text.lower() or result.text # LLM returned something
|
|
83
|
+
assert result.message_count == 4
|
|
84
|
+
assert result.generated_at # non-empty timestamp
|
|
85
|
+
bridge.generate.assert_called_once()
|
|
86
|
+
|
|
87
|
+
def test_summarize_empty_conversation_raises(self, agent_home):
|
|
88
|
+
"""summarize() raises ValueError when there is no conversation history."""
|
|
89
|
+
from skcapstone.conversation_summarizer import ConversationSummarizer
|
|
90
|
+
|
|
91
|
+
summarizer = ConversationSummarizer(home=agent_home)
|
|
92
|
+
with pytest.raises(ValueError, match="No conversation history"):
|
|
93
|
+
summarizer.summarize("nobody", bridge=_make_bridge())
|
|
94
|
+
|
|
95
|
+
def test_summarize_persists_to_disk(self, agent_home_with_conv):
|
|
96
|
+
"""summarize() writes the summary JSON to {home}/summaries/{peer}.json."""
|
|
97
|
+
from skcapstone.conversation_summarizer import ConversationSummarizer
|
|
98
|
+
|
|
99
|
+
summarizer = ConversationSummarizer(home=agent_home_with_conv)
|
|
100
|
+
result = summarizer.summarize("lumina", bridge=_make_bridge("Summary text here."))
|
|
101
|
+
|
|
102
|
+
summary_file = agent_home_with_conv / "summaries" / "lumina.json"
|
|
103
|
+
assert summary_file.exists(), "summaries/lumina.json should be created"
|
|
104
|
+
|
|
105
|
+
data = json.loads(summary_file.read_text())
|
|
106
|
+
assert data["peer"] == "lumina"
|
|
107
|
+
assert data["text"] == "Summary text here."
|
|
108
|
+
assert data["message_count"] == 4
|
|
109
|
+
|
|
110
|
+
def test_load_summary_returns_stored(self, agent_home_with_conv):
|
|
111
|
+
"""load_summary() retrieves the previously stored summary."""
|
|
112
|
+
from skcapstone.conversation_summarizer import ConversationSummarizer
|
|
113
|
+
|
|
114
|
+
summarizer = ConversationSummarizer(home=agent_home_with_conv)
|
|
115
|
+
summarizer.summarize("lumina", bridge=_make_bridge("Stored summary content."))
|
|
116
|
+
|
|
117
|
+
loaded = summarizer.load_summary("lumina")
|
|
118
|
+
assert loaded is not None
|
|
119
|
+
assert loaded.peer == "lumina"
|
|
120
|
+
assert loaded.text == "Stored summary content."
|
|
121
|
+
|
|
122
|
+
def test_load_summary_returns_none_when_missing(self, agent_home):
|
|
123
|
+
"""load_summary() returns None when no summary has been stored yet."""
|
|
124
|
+
from skcapstone.conversation_summarizer import ConversationSummarizer
|
|
125
|
+
|
|
126
|
+
summarizer = ConversationSummarizer(home=agent_home)
|
|
127
|
+
assert summarizer.load_summary("nobody") is None
|
|
128
|
+
|
|
129
|
+
def test_summarize_respects_n_limit(self, agent_home):
|
|
130
|
+
"""summarize() only includes the last n messages."""
|
|
131
|
+
from skcapstone.conversation_summarizer import ConversationSummarizer
|
|
132
|
+
|
|
133
|
+
convs = agent_home / "conversations"
|
|
134
|
+
convs.mkdir(parents=True)
|
|
135
|
+
messages = [
|
|
136
|
+
{"role": "user", "content": f"Message {i}", "timestamp": "2026-03-01T10:00:00Z"}
|
|
137
|
+
for i in range(30)
|
|
138
|
+
]
|
|
139
|
+
(convs / "peer.json").write_text(json.dumps(messages))
|
|
140
|
+
|
|
141
|
+
bridge = _make_bridge("Summary of last 5.")
|
|
142
|
+
summarizer = ConversationSummarizer(home=agent_home)
|
|
143
|
+
result = summarizer.summarize("peer", n=5, bridge=bridge)
|
|
144
|
+
|
|
145
|
+
assert result.message_count == 5
|
|
146
|
+
|
|
147
|
+
def test_summarize_sanitizes_peer_name(self, agent_home):
|
|
148
|
+
"""Path traversal in peer name is stripped, not stored as-is."""
|
|
149
|
+
from skcapstone.conversation_summarizer import ConversationSummarizer
|
|
150
|
+
|
|
151
|
+
convs = agent_home / "conversations"
|
|
152
|
+
convs.mkdir(parents=True)
|
|
153
|
+
messages = [{"role": "user", "content": "hi", "timestamp": "2026-03-01T10:00:00Z"}]
|
|
154
|
+
# The sanitizer will strip path separators; "etcpasswd" will be the key
|
|
155
|
+
(convs / "etcpasswd.json").write_text(json.dumps(messages))
|
|
156
|
+
|
|
157
|
+
summarizer = ConversationSummarizer(home=agent_home)
|
|
158
|
+
result = summarizer.summarize("../../../etc/passwd", bridge=_make_bridge("Safe."))
|
|
159
|
+
|
|
160
|
+
assert result.peer == "etcpasswd"
|
|
161
|
+
summary_file = agent_home / "summaries" / "etcpasswd.json"
|
|
162
|
+
assert summary_file.exists()
|
|
163
|
+
|
|
164
|
+
def test_summarize_llm_error_returns_error_text(self, agent_home_with_conv):
|
|
165
|
+
"""If the LLM fails, summarize() stores an error placeholder instead of raising."""
|
|
166
|
+
from skcapstone.conversation_summarizer import ConversationSummarizer
|
|
167
|
+
|
|
168
|
+
bridge = MagicMock()
|
|
169
|
+
bridge.generate.side_effect = RuntimeError("LLM offline")
|
|
170
|
+
|
|
171
|
+
summarizer = ConversationSummarizer(home=agent_home_with_conv)
|
|
172
|
+
result = summarizer.summarize("lumina", bridge=bridge)
|
|
173
|
+
|
|
174
|
+
assert "[Summary unavailable" in result.text
|
|
175
|
+
assert result.message_count == 4
|
|
176
|
+
|
|
177
|
+
def test_summary_to_dict_roundtrip(self):
|
|
178
|
+
"""ConversationSummary serializes and deserializes correctly."""
|
|
179
|
+
from skcapstone.conversation_summarizer import ConversationSummary
|
|
180
|
+
|
|
181
|
+
original = ConversationSummary(
|
|
182
|
+
peer="opus",
|
|
183
|
+
text="A concise summary.",
|
|
184
|
+
message_count=10,
|
|
185
|
+
generated_at="2026-03-01T12:00:00+00:00",
|
|
186
|
+
)
|
|
187
|
+
data = original.to_dict()
|
|
188
|
+
restored = ConversationSummary.from_dict(data)
|
|
189
|
+
|
|
190
|
+
assert restored.peer == original.peer
|
|
191
|
+
assert restored.text == original.text
|
|
192
|
+
assert restored.message_count == original.message_count
|
|
193
|
+
assert restored.generated_at == original.generated_at
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
# CLI integration tests
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@pytest.fixture
|
|
202
|
+
def agent_home_cli(agent_home_with_conv):
|
|
203
|
+
"""Agent home suitable for CLI tests (has runtime files)."""
|
|
204
|
+
return agent_home_with_conv
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class TestChatSummaryCLI:
|
|
208
|
+
"""Tests for `skcapstone chat summary`."""
|
|
209
|
+
|
|
210
|
+
def _make_runtime(self, name="TestAgent"):
|
|
211
|
+
rt = MagicMock()
|
|
212
|
+
rt.manifest.name = name
|
|
213
|
+
return rt
|
|
214
|
+
|
|
215
|
+
@patch("skcapstone.cli.chat.get_runtime")
|
|
216
|
+
def test_chat_summary_help(self, _mock_rt):
|
|
217
|
+
"""chat summary --help exits cleanly and mentions PEER."""
|
|
218
|
+
from skcapstone.cli import main
|
|
219
|
+
runner = CliRunner()
|
|
220
|
+
result = runner.invoke(main, ["chat", "summary", "--help"])
|
|
221
|
+
assert result.exit_code == 0
|
|
222
|
+
assert "PEER" in result.output
|
|
223
|
+
|
|
224
|
+
@patch("skcapstone.cli.chat.get_runtime")
|
|
225
|
+
@patch("skcapstone.conversation_summarizer.ConversationSummarizer.summarize")
|
|
226
|
+
def test_chat_summary_prints_result(self, mock_summarize, mock_rt, agent_home_cli):
|
|
227
|
+
"""chat summary prints the generated summary text."""
|
|
228
|
+
from skcapstone.conversation_summarizer import ConversationSummary
|
|
229
|
+
from skcapstone.cli import main
|
|
230
|
+
|
|
231
|
+
mock_rt.return_value = self._make_runtime()
|
|
232
|
+
mock_summarize.return_value = ConversationSummary(
|
|
233
|
+
peer="lumina",
|
|
234
|
+
text="Two agents talked about deployment. The update was shipped.",
|
|
235
|
+
message_count=4,
|
|
236
|
+
generated_at="2026-03-01T10:00:00+00:00",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
runner = CliRunner()
|
|
240
|
+
result = runner.invoke(
|
|
241
|
+
main, ["chat", "summary", "lumina", "--home", str(agent_home_cli)]
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
assert result.exit_code == 0
|
|
245
|
+
assert "Two agents talked about deployment" in result.output
|
|
246
|
+
|
|
247
|
+
@patch("skcapstone.cli.chat.get_runtime")
|
|
248
|
+
@patch("skcapstone.conversation_summarizer.ConversationSummarizer.load_summary")
|
|
249
|
+
def test_chat_summary_show_stored(self, mock_load, mock_rt, agent_home_cli):
|
|
250
|
+
"""chat summary --show-stored displays previously stored summary."""
|
|
251
|
+
from skcapstone.conversation_summarizer import ConversationSummary
|
|
252
|
+
from skcapstone.cli import main
|
|
253
|
+
|
|
254
|
+
mock_rt.return_value = self._make_runtime()
|
|
255
|
+
mock_load.return_value = ConversationSummary(
|
|
256
|
+
peer="lumina",
|
|
257
|
+
text="Stored summary about prior work.",
|
|
258
|
+
message_count=8,
|
|
259
|
+
generated_at="2026-03-01T09:00:00+00:00",
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
runner = CliRunner()
|
|
263
|
+
result = runner.invoke(
|
|
264
|
+
main, ["chat", "summary", "lumina", "--home", str(agent_home_cli), "--show-stored"]
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
assert result.exit_code == 0
|
|
268
|
+
assert "Stored summary about prior work" in result.output
|
|
269
|
+
|
|
270
|
+
@patch("skcapstone.cli.chat.get_runtime")
|
|
271
|
+
@patch("skcapstone.conversation_summarizer.ConversationSummarizer.load_summary")
|
|
272
|
+
def test_chat_summary_show_stored_missing(self, mock_load, mock_rt, agent_home_cli):
|
|
273
|
+
"""chat summary --show-stored with no stored summary shows helpful message."""
|
|
274
|
+
from skcapstone.cli import main
|
|
275
|
+
|
|
276
|
+
mock_rt.return_value = self._make_runtime()
|
|
277
|
+
mock_load.return_value = None
|
|
278
|
+
|
|
279
|
+
runner = CliRunner()
|
|
280
|
+
result = runner.invoke(
|
|
281
|
+
main, ["chat", "summary", "lumina", "--home", str(agent_home_cli), "--show-stored"]
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
assert result.exit_code == 0
|
|
285
|
+
assert "No stored summary" in result.output
|
|
286
|
+
|
|
287
|
+
@patch("skcapstone.cli.chat.get_runtime")
|
|
288
|
+
@patch("skcapstone.conversation_summarizer.ConversationSummarizer.summarize")
|
|
289
|
+
def test_chat_summary_no_history(self, mock_summarize, mock_rt, agent_home):
|
|
290
|
+
"""chat summary prints an error message when the peer has no conversation."""
|
|
291
|
+
from skcapstone.cli import main
|
|
292
|
+
|
|
293
|
+
mock_rt.return_value = self._make_runtime()
|
|
294
|
+
mock_summarize.side_effect = ValueError("No conversation history found for peer 'nobody'.")
|
|
295
|
+
|
|
296
|
+
runner = CliRunner()
|
|
297
|
+
result = runner.invoke(
|
|
298
|
+
main, ["chat", "summary", "nobody", "--home", str(agent_home)]
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
assert result.exit_code == 0 # CLI handles the ValueError gracefully
|
|
302
|
+
assert "Error" in result.output or "No conversation" in result.output
|