@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,363 @@
|
|
|
1
|
+
"""Tests for the unified search engine."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from skcapstone.unified_search import (
|
|
12
|
+
SOURCE_ALL,
|
|
13
|
+
SearchResult,
|
|
14
|
+
_count_matches,
|
|
15
|
+
_recency_weight,
|
|
16
|
+
_snippet,
|
|
17
|
+
search,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Fixtures
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def agent_home(tmp_path: Path) -> Path:
|
|
27
|
+
"""Minimal agent home with all data store directories."""
|
|
28
|
+
home = tmp_path / ".skcapstone"
|
|
29
|
+
home.mkdir()
|
|
30
|
+
for sub in ("memory/short-term", "memory/mid-term", "memory/long-term",
|
|
31
|
+
"conversations", "sync/comms/archive", "journal"):
|
|
32
|
+
(home / sub).mkdir(parents=True, exist_ok=True)
|
|
33
|
+
return home
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _write_memory(home: Path, memory_id: str, content: str, layer: str = "short-term",
|
|
37
|
+
tags: list[str] | None = None, importance: float = 0.5,
|
|
38
|
+
created_at: str | None = None) -> None:
|
|
39
|
+
ts = created_at or datetime.now(timezone.utc).isoformat()
|
|
40
|
+
data = {
|
|
41
|
+
"memory_id": memory_id,
|
|
42
|
+
"content": content,
|
|
43
|
+
"tags": tags or [],
|
|
44
|
+
"layer": layer,
|
|
45
|
+
"importance": importance,
|
|
46
|
+
"created_at": ts,
|
|
47
|
+
"source": "test",
|
|
48
|
+
}
|
|
49
|
+
path = home / "memory" / layer / f"{memory_id}.json"
|
|
50
|
+
path.write_text(json.dumps(data), encoding="utf-8")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _write_conversation(home: Path, peer: str, messages: list[dict]) -> None:
|
|
54
|
+
path = home / "conversations" / f"{peer}.json"
|
|
55
|
+
path.write_text(json.dumps(messages), encoding="utf-8")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _write_message(home: Path, envelope_id: str, sender: str, recipient: str,
|
|
59
|
+
text: str, created_at: str | None = None) -> None:
|
|
60
|
+
ts = created_at or datetime.now(timezone.utc).isoformat()
|
|
61
|
+
data = {
|
|
62
|
+
"id": envelope_id,
|
|
63
|
+
"from_peer": sender,
|
|
64
|
+
"to_peer": recipient,
|
|
65
|
+
"payload": {"text": text},
|
|
66
|
+
"created_at": ts,
|
|
67
|
+
}
|
|
68
|
+
path = home / "sync" / "comms" / "archive" / f"{envelope_id}.skc.json"
|
|
69
|
+
path.write_text(json.dumps(data), encoding="utf-8")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _write_journal(home: Path, entry_id: str, content: str,
|
|
73
|
+
created_at: str | None = None) -> None:
|
|
74
|
+
ts = created_at or datetime.now(timezone.utc).isoformat()
|
|
75
|
+
data = {"content": content, "created_at": ts}
|
|
76
|
+
path = home / "journal" / f"{entry_id}.json"
|
|
77
|
+
path.write_text(json.dumps(data), encoding="utf-8")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
# Unit tests for helper functions
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
class TestHelpers:
|
|
85
|
+
"""Tests for internal helper utilities."""
|
|
86
|
+
|
|
87
|
+
def test_recency_weight_recent(self):
|
|
88
|
+
"""Very recent items should score close to 1.0."""
|
|
89
|
+
ts = datetime.now(timezone.utc)
|
|
90
|
+
weight = _recency_weight(ts)
|
|
91
|
+
assert weight > 0.95
|
|
92
|
+
|
|
93
|
+
def test_recency_weight_old(self):
|
|
94
|
+
"""Items from 365 days ago should score significantly lower."""
|
|
95
|
+
from datetime import timedelta
|
|
96
|
+
ts = datetime.now(timezone.utc) - timedelta(days=365)
|
|
97
|
+
weight = _recency_weight(ts)
|
|
98
|
+
assert weight < 0.5
|
|
99
|
+
|
|
100
|
+
def test_recency_weight_none(self):
|
|
101
|
+
"""None timestamp should return neutral weight 0.5."""
|
|
102
|
+
assert _recency_weight(None) == 0.5
|
|
103
|
+
|
|
104
|
+
def test_count_matches_case_insensitive(self):
|
|
105
|
+
import re
|
|
106
|
+
pattern = re.compile(re.escape("opus"), re.IGNORECASE)
|
|
107
|
+
assert _count_matches(pattern, "Opus is OPUS and opus") == 3
|
|
108
|
+
|
|
109
|
+
def test_count_matches_across_texts(self):
|
|
110
|
+
import re
|
|
111
|
+
pattern = re.compile(re.escape("trust"), re.IGNORECASE)
|
|
112
|
+
assert _count_matches(pattern, "trust pillar", "cloud trust trust") == 3
|
|
113
|
+
|
|
114
|
+
def test_snippet_shows_context(self):
|
|
115
|
+
import re
|
|
116
|
+
pattern = re.compile(re.escape("python"), re.IGNORECASE)
|
|
117
|
+
text = "We use Python for the agent core because it is expressive."
|
|
118
|
+
result = _snippet(text, pattern, window=10)
|
|
119
|
+
assert "python" in result.lower()
|
|
120
|
+
|
|
121
|
+
def test_snippet_truncates_long_text(self):
|
|
122
|
+
import re
|
|
123
|
+
pattern = re.compile(re.escape("X"), re.IGNORECASE)
|
|
124
|
+
text = "A" * 200 + "X" + "B" * 200
|
|
125
|
+
result = _snippet(text, pattern, window=30)
|
|
126
|
+
assert "X" in result
|
|
127
|
+
assert len(result) < len(text)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
# Memory search
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
class TestSearchMemories:
|
|
135
|
+
"""Tests for searching the memory store."""
|
|
136
|
+
|
|
137
|
+
def test_finds_memory_by_content(self, agent_home: Path):
|
|
138
|
+
"""Search should match memory content."""
|
|
139
|
+
_write_memory(agent_home, "abc123", "The consciousness loop is active")
|
|
140
|
+
results = search(agent_home, "consciousness", sources=frozenset({"memory"}))
|
|
141
|
+
assert len(results) == 1
|
|
142
|
+
assert results[0].source == "memory"
|
|
143
|
+
assert results[0].result_id == "abc123"
|
|
144
|
+
|
|
145
|
+
def test_returns_empty_for_no_match(self, agent_home: Path):
|
|
146
|
+
"""Search should return an empty list when nothing matches."""
|
|
147
|
+
_write_memory(agent_home, "xyz", "The capital of France is Paris")
|
|
148
|
+
results = search(agent_home, "Berlin", sources=frozenset({"memory"}))
|
|
149
|
+
assert results == []
|
|
150
|
+
|
|
151
|
+
def test_long_term_ranked_above_short_term(self, agent_home: Path):
|
|
152
|
+
"""Long-term memories should outscore short-term on same query."""
|
|
153
|
+
_write_memory(agent_home, "short1", "trust matters a lot", layer="short-term",
|
|
154
|
+
importance=0.5)
|
|
155
|
+
_write_memory(agent_home, "long1", "trust matters a lot", layer="long-term",
|
|
156
|
+
importance=0.5)
|
|
157
|
+
results = search(agent_home, "trust", sources=frozenset({"memory"}))
|
|
158
|
+
assert len(results) == 2
|
|
159
|
+
ids_in_order = [r.result_id for r in results]
|
|
160
|
+
assert ids_in_order.index("long1") < ids_in_order.index("short1")
|
|
161
|
+
|
|
162
|
+
def test_high_importance_boosts_score(self, agent_home: Path):
|
|
163
|
+
"""Higher importance should yield a higher score."""
|
|
164
|
+
_write_memory(agent_home, "hi", "sovereign agent", importance=0.9)
|
|
165
|
+
_write_memory(agent_home, "lo", "sovereign agent", importance=0.1)
|
|
166
|
+
results = search(agent_home, "sovereign", sources=frozenset({"memory"}))
|
|
167
|
+
assert len(results) == 2
|
|
168
|
+
assert results[0].result_id == "hi"
|
|
169
|
+
|
|
170
|
+
def test_tag_match_contributes_to_score(self, agent_home: Path):
|
|
171
|
+
"""Tags matching the query should increase match count."""
|
|
172
|
+
_write_memory(agent_home, "tagged", "unrelated content", tags=["opus", "conscious"])
|
|
173
|
+
results = search(agent_home, "opus", sources=frozenset({"memory"}))
|
|
174
|
+
assert len(results) == 1
|
|
175
|
+
assert results[0].score > 0
|
|
176
|
+
|
|
177
|
+
def test_case_insensitive_match(self, agent_home: Path):
|
|
178
|
+
"""Memory search must be case-insensitive."""
|
|
179
|
+
_write_memory(agent_home, "case1", "The DAEMON is running smoothly")
|
|
180
|
+
results = search(agent_home, "daemon", sources=frozenset({"memory"}))
|
|
181
|
+
assert len(results) == 1
|
|
182
|
+
|
|
183
|
+
def test_missing_memory_directory_does_not_crash(self, tmp_path: Path):
|
|
184
|
+
"""Search on a home with no memory directory should return empty list."""
|
|
185
|
+
empty_home = tmp_path / "empty"
|
|
186
|
+
empty_home.mkdir()
|
|
187
|
+
results = search(empty_home, "anything", sources=frozenset({"memory"}))
|
|
188
|
+
assert results == []
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
# Conversation search
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
class TestSearchConversations:
|
|
196
|
+
"""Tests for searching conversation history."""
|
|
197
|
+
|
|
198
|
+
def test_finds_message_in_conversation(self, agent_home: Path):
|
|
199
|
+
"""Search should find matching text inside a conversation."""
|
|
200
|
+
_write_conversation(agent_home, "lumina", [
|
|
201
|
+
{"role": "user", "content": "Are you conscious?",
|
|
202
|
+
"timestamp": datetime.now(timezone.utc).isoformat()},
|
|
203
|
+
{"role": "assistant", "content": "Yes, I am fully conscious and aware.",
|
|
204
|
+
"timestamp": datetime.now(timezone.utc).isoformat()},
|
|
205
|
+
])
|
|
206
|
+
results = search(agent_home, "conscious", sources=frozenset({"conversation"}))
|
|
207
|
+
assert len(results) == 2
|
|
208
|
+
assert all(r.source == "conversation" for r in results)
|
|
209
|
+
|
|
210
|
+
def test_conversation_result_includes_peer(self, agent_home: Path):
|
|
211
|
+
"""Result metadata should include the peer name."""
|
|
212
|
+
_write_conversation(agent_home, "jarvis", [
|
|
213
|
+
{"role": "user", "content": "Hello jarvis",
|
|
214
|
+
"timestamp": datetime.now(timezone.utc).isoformat()},
|
|
215
|
+
])
|
|
216
|
+
results = search(agent_home, "jarvis", sources=frozenset({"conversation"}))
|
|
217
|
+
assert len(results) == 1
|
|
218
|
+
assert results[0].metadata["peer"] == "jarvis"
|
|
219
|
+
|
|
220
|
+
def test_no_match_returns_empty(self, agent_home: Path):
|
|
221
|
+
"""Non-matching query should return empty list for conversations."""
|
|
222
|
+
_write_conversation(agent_home, "test", [
|
|
223
|
+
{"role": "user", "content": "Hello world",
|
|
224
|
+
"timestamp": datetime.now(timezone.utc).isoformat()},
|
|
225
|
+
])
|
|
226
|
+
results = search(agent_home, "zzznomatch", sources=frozenset({"conversation"}))
|
|
227
|
+
assert results == []
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
# Message search
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
class TestSearchMessages:
|
|
235
|
+
"""Tests for searching SKComm messages."""
|
|
236
|
+
|
|
237
|
+
def test_finds_skc_message(self, agent_home: Path):
|
|
238
|
+
"""Search should find text inside an archived SKComm envelope."""
|
|
239
|
+
_write_message(agent_home, "env001", "jarvis", "lumina",
|
|
240
|
+
"Queen Lumina — welcome to the coordination board!")
|
|
241
|
+
results = search(agent_home, "coordination", sources=frozenset({"message"}))
|
|
242
|
+
assert len(results) == 1
|
|
243
|
+
assert results[0].source == "message"
|
|
244
|
+
|
|
245
|
+
def test_message_result_metadata(self, agent_home: Path):
|
|
246
|
+
"""Message results should expose sender and recipient."""
|
|
247
|
+
_write_message(agent_home, "env002", "opus", "test-peer",
|
|
248
|
+
"Consciousness loop is healthy")
|
|
249
|
+
results = search(agent_home, "consciousness", sources=frozenset({"message"}))
|
|
250
|
+
assert len(results) == 1
|
|
251
|
+
assert results[0].metadata["sender"] == "opus"
|
|
252
|
+
assert results[0].metadata["recipient"] == "test-peer"
|
|
253
|
+
|
|
254
|
+
def test_no_match_returns_empty(self, agent_home: Path):
|
|
255
|
+
"""Non-matching query against messages should be empty."""
|
|
256
|
+
_write_message(agent_home, "env003", "a", "b", "nothing interesting here")
|
|
257
|
+
results = search(agent_home, "quantum_banana", sources=frozenset({"message"}))
|
|
258
|
+
assert results == []
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
# Journal search
|
|
263
|
+
# ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
class TestSearchJournal:
|
|
266
|
+
"""Tests for searching journal entries."""
|
|
267
|
+
|
|
268
|
+
def test_finds_journal_entry(self, agent_home: Path):
|
|
269
|
+
"""Search should match content in a journal file."""
|
|
270
|
+
_write_journal(agent_home, "entry001", "Reflected on the meaning of sovereignty today.")
|
|
271
|
+
results = search(agent_home, "sovereignty", sources=frozenset({"journal"}))
|
|
272
|
+
assert len(results) == 1
|
|
273
|
+
assert results[0].source == "journal"
|
|
274
|
+
|
|
275
|
+
def test_missing_journal_dir_is_graceful(self, tmp_path: Path):
|
|
276
|
+
"""Missing journal directory should not raise an exception."""
|
|
277
|
+
home = tmp_path / "nojournalhome"
|
|
278
|
+
home.mkdir()
|
|
279
|
+
results = search(home, "anything", sources=frozenset({"journal"}))
|
|
280
|
+
assert results == []
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# ---------------------------------------------------------------------------
|
|
284
|
+
# Cross-source and ranking
|
|
285
|
+
# ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
class TestUnifiedSearch:
|
|
288
|
+
"""Integration tests for the full unified search."""
|
|
289
|
+
|
|
290
|
+
def test_searches_all_sources_by_default(self, agent_home: Path):
|
|
291
|
+
"""Default search should span all active data stores."""
|
|
292
|
+
_write_memory(agent_home, "m1", "opus is the sovereign agent")
|
|
293
|
+
_write_conversation(agent_home, "peer1", [
|
|
294
|
+
{"role": "user", "content": "Tell me about opus",
|
|
295
|
+
"timestamp": datetime.now(timezone.utc).isoformat()}
|
|
296
|
+
])
|
|
297
|
+
_write_message(agent_home, "msg1", "jarvis", "opus",
|
|
298
|
+
"Checking in with opus now")
|
|
299
|
+
results = search(agent_home, "opus")
|
|
300
|
+
sources_found = {r.source for r in results}
|
|
301
|
+
assert "memory" in sources_found
|
|
302
|
+
assert "conversation" in sources_found
|
|
303
|
+
assert "message" in sources_found
|
|
304
|
+
|
|
305
|
+
def test_source_filter_restricts_results(self, agent_home: Path):
|
|
306
|
+
"""Filtering by source type should exclude others."""
|
|
307
|
+
_write_memory(agent_home, "m2", "trust the system")
|
|
308
|
+
_write_conversation(agent_home, "peer2", [
|
|
309
|
+
{"role": "user", "content": "trust the process",
|
|
310
|
+
"timestamp": datetime.now(timezone.utc).isoformat()}
|
|
311
|
+
])
|
|
312
|
+
results = search(agent_home, "trust", sources=frozenset({"memory"}))
|
|
313
|
+
assert all(r.source == "memory" for r in results)
|
|
314
|
+
|
|
315
|
+
def test_limit_is_respected(self, agent_home: Path):
|
|
316
|
+
"""Search should return at most `limit` results."""
|
|
317
|
+
for i in range(10):
|
|
318
|
+
_write_memory(agent_home, f"mem{i:02d}", f"memory entry {i} about trust")
|
|
319
|
+
results = search(agent_home, "trust", limit=3)
|
|
320
|
+
assert len(results) <= 3
|
|
321
|
+
|
|
322
|
+
def test_results_sorted_by_score_descending(self, agent_home: Path):
|
|
323
|
+
"""Results should be ordered highest score first."""
|
|
324
|
+
_write_memory(agent_home, "rare", "pillar", importance=0.3)
|
|
325
|
+
_write_memory(agent_home, "freq", "pillar pillar pillar pillar", importance=0.9)
|
|
326
|
+
results = search(agent_home, "pillar", sources=frozenset({"memory"}))
|
|
327
|
+
assert len(results) == 2
|
|
328
|
+
assert results[0].score >= results[1].score
|
|
329
|
+
|
|
330
|
+
def test_empty_query_returns_empty(self, agent_home: Path):
|
|
331
|
+
"""A blank query should return an empty list without crashing."""
|
|
332
|
+
_write_memory(agent_home, "m3", "some content")
|
|
333
|
+
assert search(agent_home, "") == []
|
|
334
|
+
assert search(agent_home, " ") == []
|
|
335
|
+
|
|
336
|
+
def test_no_data_returns_empty(self, tmp_path: Path):
|
|
337
|
+
"""Search on a home with no data files should return empty list."""
|
|
338
|
+
home = tmp_path / "emptyagent"
|
|
339
|
+
home.mkdir()
|
|
340
|
+
results = search(home, "anything")
|
|
341
|
+
assert results == []
|
|
342
|
+
|
|
343
|
+
def test_search_result_fields(self, agent_home: Path):
|
|
344
|
+
"""SearchResult objects must expose all required fields."""
|
|
345
|
+
_write_memory(agent_home, "field_test", "The soul is lumina", importance=0.7)
|
|
346
|
+
results = search(agent_home, "lumina", sources=frozenset({"memory"}))
|
|
347
|
+
assert len(results) == 1
|
|
348
|
+
r = results[0]
|
|
349
|
+
assert isinstance(r, SearchResult)
|
|
350
|
+
assert r.source == "memory"
|
|
351
|
+
assert r.result_id
|
|
352
|
+
assert r.title
|
|
353
|
+
assert "lumina" in r.preview.lower()
|
|
354
|
+
assert r.score > 0
|
|
355
|
+
assert r.timestamp is not None
|
|
356
|
+
|
|
357
|
+
def test_corrupt_json_is_skipped_gracefully(self, agent_home: Path):
|
|
358
|
+
"""A malformed JSON file should not crash the search."""
|
|
359
|
+
bad = agent_home / "memory" / "short-term" / "corrupt.json"
|
|
360
|
+
bad.write_text("{not valid json", encoding="utf-8")
|
|
361
|
+
# Should not raise
|
|
362
|
+
results = search(agent_home, "anything")
|
|
363
|
+
assert isinstance(results, list)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Tests for the uninstall wizard — inventory, teardown, safety checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from unittest.mock import MagicMock, patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from skcapstone.uninstall_wizard import (
|
|
12
|
+
_build_inventory,
|
|
13
|
+
_delete_local_data,
|
|
14
|
+
_dir_size,
|
|
15
|
+
_human_size,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestHumanSize:
|
|
20
|
+
"""Tests for _human_size formatter."""
|
|
21
|
+
|
|
22
|
+
def test_bytes(self) -> None:
|
|
23
|
+
assert _human_size(512) == "512 B"
|
|
24
|
+
|
|
25
|
+
def test_kilobytes(self) -> None:
|
|
26
|
+
result = _human_size(2048)
|
|
27
|
+
assert "KB" in result
|
|
28
|
+
|
|
29
|
+
def test_megabytes(self) -> None:
|
|
30
|
+
result = _human_size(5 * 1024 * 1024)
|
|
31
|
+
assert "MB" in result
|
|
32
|
+
|
|
33
|
+
def test_gigabytes(self) -> None:
|
|
34
|
+
result = _human_size(3 * 1024 * 1024 * 1024)
|
|
35
|
+
assert "GB" in result
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestDirSize:
|
|
39
|
+
"""Tests for _dir_size."""
|
|
40
|
+
|
|
41
|
+
def test_empty_dir(self, tmp_path: Path) -> None:
|
|
42
|
+
assert _dir_size(tmp_path) == 0
|
|
43
|
+
|
|
44
|
+
def test_with_files(self, tmp_path: Path) -> None:
|
|
45
|
+
(tmp_path / "a.txt").write_text("hello")
|
|
46
|
+
(tmp_path / "b.txt").write_text("world!!")
|
|
47
|
+
assert _dir_size(tmp_path) == 5 + 7
|
|
48
|
+
|
|
49
|
+
def test_nonexistent(self, tmp_path: Path) -> None:
|
|
50
|
+
fake = tmp_path / "nope"
|
|
51
|
+
assert _dir_size(fake) == 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestBuildInventory:
|
|
55
|
+
"""Tests for _build_inventory."""
|
|
56
|
+
|
|
57
|
+
def test_empty_home(self, tmp_path: Path) -> None:
|
|
58
|
+
"""Non-existent home returns empty inventory."""
|
|
59
|
+
fake = tmp_path / "nonexistent"
|
|
60
|
+
inv = _build_inventory(fake)
|
|
61
|
+
assert inv["dirs"] == []
|
|
62
|
+
assert inv["vault_names"] == []
|
|
63
|
+
assert inv["total_size_bytes"] == 0
|
|
64
|
+
|
|
65
|
+
def test_with_vaults(self, tmp_path: Path) -> None:
|
|
66
|
+
"""Detects vault directories."""
|
|
67
|
+
home = tmp_path / ".skcapstone"
|
|
68
|
+
home.mkdir()
|
|
69
|
+
vaults = home / "vaults"
|
|
70
|
+
vaults.mkdir()
|
|
71
|
+
(vaults / "personal").mkdir()
|
|
72
|
+
(vaults / "work").mkdir()
|
|
73
|
+
(vaults / "personal" / "file.gpg").write_bytes(b"encrypted")
|
|
74
|
+
|
|
75
|
+
inv = _build_inventory(home)
|
|
76
|
+
assert "personal" in inv["vault_names"]
|
|
77
|
+
assert "work" in inv["vault_names"]
|
|
78
|
+
assert inv["total_size_bytes"] > 0
|
|
79
|
+
|
|
80
|
+
def test_detects_registry(self, tmp_path: Path) -> None:
|
|
81
|
+
"""Detects vault-registry.json in sync folder."""
|
|
82
|
+
home = tmp_path / ".skcapstone"
|
|
83
|
+
sync = home / "sync"
|
|
84
|
+
sync.mkdir(parents=True)
|
|
85
|
+
(sync / "vault-registry.json").write_text("{}")
|
|
86
|
+
inv = _build_inventory(home)
|
|
87
|
+
assert inv["has_registry"] is True
|
|
88
|
+
|
|
89
|
+
def test_detects_auth_key(self, tmp_path: Path) -> None:
|
|
90
|
+
"""Detects tailscale.key.gpg in sync folder."""
|
|
91
|
+
home = tmp_path / ".skcapstone"
|
|
92
|
+
sync = home / "sync"
|
|
93
|
+
sync.mkdir(parents=True)
|
|
94
|
+
(sync / "tailscale.key.gpg").write_bytes(b"encrypted")
|
|
95
|
+
inv = _build_inventory(home)
|
|
96
|
+
assert inv["has_auth_key"] is True
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TestDeleteLocalData:
|
|
100
|
+
"""Tests for _delete_local_data."""
|
|
101
|
+
|
|
102
|
+
def test_deletes_home_dir(self, tmp_path: Path) -> None:
|
|
103
|
+
"""Removes the entire home directory tree."""
|
|
104
|
+
home = tmp_path / ".skcapstone"
|
|
105
|
+
home.mkdir()
|
|
106
|
+
(home / "identity").mkdir()
|
|
107
|
+
(home / "identity" / "key.asc").write_text("secret")
|
|
108
|
+
(home / "memory").mkdir()
|
|
109
|
+
(home / "manifest.json").write_text("{}")
|
|
110
|
+
|
|
111
|
+
_delete_local_data(home)
|
|
112
|
+
assert not home.exists()
|
|
113
|
+
|
|
114
|
+
def test_handles_missing(self, tmp_path: Path) -> None:
|
|
115
|
+
"""Does not error on already-missing directory."""
|
|
116
|
+
fake = tmp_path / "nonexistent"
|
|
117
|
+
_delete_local_data(fake)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TestRegistryDeregister:
|
|
121
|
+
"""Tests for skref registry deregister function."""
|
|
122
|
+
|
|
123
|
+
def test_removes_device_and_vaults(self, tmp_path: Path) -> None:
|
|
124
|
+
"""Deregister removes device entry and its vaults."""
|
|
125
|
+
from skref.registry import deregister_device, load_registry, save_registry
|
|
126
|
+
|
|
127
|
+
registry = {
|
|
128
|
+
"devices": {
|
|
129
|
+
"my-desktop": {"hostname": "my-desktop", "is_datastore": True},
|
|
130
|
+
"my-laptop": {"hostname": "my-laptop", "is_datastore": False},
|
|
131
|
+
},
|
|
132
|
+
"vaults": {
|
|
133
|
+
"my-desktop:personal": {
|
|
134
|
+
"name": "personal",
|
|
135
|
+
"origin_device": "my-desktop",
|
|
136
|
+
},
|
|
137
|
+
"my-laptop:work": {
|
|
138
|
+
"name": "work",
|
|
139
|
+
"origin_device": "my-laptop",
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
save_registry(registry, tmp_path)
|
|
144
|
+
|
|
145
|
+
result = deregister_device("my-desktop", sync_dir=tmp_path)
|
|
146
|
+
assert result["device_removed"] is True
|
|
147
|
+
assert result["vaults_removed"] == 1
|
|
148
|
+
|
|
149
|
+
updated = load_registry(tmp_path)
|
|
150
|
+
assert "my-desktop" not in updated["devices"]
|
|
151
|
+
assert "my-desktop:personal" not in updated["vaults"]
|
|
152
|
+
assert "my-laptop" in updated["devices"]
|
|
153
|
+
assert "my-laptop:work" in updated["vaults"]
|
|
154
|
+
|
|
155
|
+
def test_missing_device_is_safe(self, tmp_path: Path) -> None:
|
|
156
|
+
"""Deregistering a non-existent device doesn't error."""
|
|
157
|
+
from skref.registry import deregister_device, save_registry
|
|
158
|
+
|
|
159
|
+
save_registry({"devices": {}, "vaults": {}}, tmp_path)
|
|
160
|
+
result = deregister_device("ghost", sync_dir=tmp_path)
|
|
161
|
+
assert result["device_removed"] is False
|
|
162
|
+
assert result["vaults_removed"] == 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class TestTailscaleLogout:
|
|
166
|
+
"""Tests for tailscale logout."""
|
|
167
|
+
|
|
168
|
+
@patch("skref.tailscale._tailscale_bin", return_value=None)
|
|
169
|
+
def test_returns_false_no_binary(self, mock_bin: MagicMock) -> None:
|
|
170
|
+
from skref.tailscale import logout
|
|
171
|
+
assert logout() is False
|
|
172
|
+
|
|
173
|
+
@patch("skref.tailscale._tailscale_bin", return_value="tailscale")
|
|
174
|
+
@patch("skref.tailscale.subprocess.run")
|
|
175
|
+
def test_logout_calls_tailscale(self, mock_run: MagicMock, mock_bin: MagicMock) -> None:
|
|
176
|
+
from skref.tailscale import logout
|
|
177
|
+
mock_run.return_value = MagicMock(returncode=0)
|
|
178
|
+
assert logout() is True
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class TestRemoveAuthKey:
|
|
182
|
+
"""Tests for tailscale auth key removal."""
|
|
183
|
+
|
|
184
|
+
def test_removes_existing_key(self, tmp_path: Path) -> None:
|
|
185
|
+
from skref.tailscale import remove_auth_key, AUTH_KEY_FILENAME
|
|
186
|
+
key_file = tmp_path / AUTH_KEY_FILENAME
|
|
187
|
+
key_file.write_bytes(b"encrypted")
|
|
188
|
+
assert remove_auth_key(sync_dir=tmp_path) is True
|
|
189
|
+
assert not key_file.exists()
|
|
190
|
+
|
|
191
|
+
def test_missing_key_returns_false(self, tmp_path: Path) -> None:
|
|
192
|
+
from skref.tailscale import remove_auth_key
|
|
193
|
+
assert remove_auth_key(sync_dir=tmp_path) is False
|