@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,354 @@
|
|
|
1
|
+
"""Tests for Sovereign Heartbeat v2."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from skcapstone.heartbeat import (
|
|
12
|
+
AgentCapability,
|
|
13
|
+
Heartbeat,
|
|
14
|
+
HeartbeatBeacon,
|
|
15
|
+
MeshHealth,
|
|
16
|
+
NodeCapacity,
|
|
17
|
+
PeerInfo,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def home(tmp_path: Path) -> Path:
|
|
23
|
+
"""Create a minimal agent home."""
|
|
24
|
+
(tmp_path / "identity").mkdir()
|
|
25
|
+
(tmp_path / "identity" / "identity.json").write_text(json.dumps({
|
|
26
|
+
"name": "opus",
|
|
27
|
+
"fingerprint": "ABCD1234567890AB",
|
|
28
|
+
}), encoding="utf-8")
|
|
29
|
+
return tmp_path
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.fixture
|
|
33
|
+
def beacon(home: Path) -> HeartbeatBeacon:
|
|
34
|
+
"""Create an initialized HeartbeatBeacon."""
|
|
35
|
+
b = HeartbeatBeacon(home, agent_name="opus")
|
|
36
|
+
b.initialize()
|
|
37
|
+
return b
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _write_peer_heartbeat(
|
|
41
|
+
home: Path,
|
|
42
|
+
agent_name: str,
|
|
43
|
+
status: str = "alive",
|
|
44
|
+
ttl_seconds: int = 300,
|
|
45
|
+
age_seconds: float = 0,
|
|
46
|
+
capabilities: list[dict] | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Helper to create a peer heartbeat file."""
|
|
49
|
+
ts = datetime.now(timezone.utc) - timedelta(seconds=age_seconds)
|
|
50
|
+
hb = {
|
|
51
|
+
"agent_name": agent_name,
|
|
52
|
+
"status": status,
|
|
53
|
+
"hostname": f"{agent_name}-host",
|
|
54
|
+
"platform": "Linux x86_64",
|
|
55
|
+
"timestamp": ts.isoformat(),
|
|
56
|
+
"ttl_seconds": ttl_seconds,
|
|
57
|
+
"uptime_hours": 1.0,
|
|
58
|
+
"capabilities": capabilities or [],
|
|
59
|
+
"claimed_tasks": [],
|
|
60
|
+
"capacity": {},
|
|
61
|
+
}
|
|
62
|
+
hb_dir = home / "heartbeats"
|
|
63
|
+
hb_dir.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
(hb_dir / f"{agent_name}.json").write_text(
|
|
65
|
+
json.dumps(hb, indent=2), encoding="utf-8",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Initialization
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TestInitialization:
|
|
75
|
+
"""Tests for heartbeat setup."""
|
|
76
|
+
|
|
77
|
+
def test_initialize_creates_dir(self, home: Path) -> None:
|
|
78
|
+
"""Initialize creates heartbeats directory."""
|
|
79
|
+
b = HeartbeatBeacon(home)
|
|
80
|
+
b.initialize()
|
|
81
|
+
assert (home / "heartbeats").is_dir()
|
|
82
|
+
|
|
83
|
+
def test_initialize_idempotent(self, beacon: HeartbeatBeacon, home: Path) -> None:
|
|
84
|
+
"""Multiple initializations don't break anything."""
|
|
85
|
+
beacon.initialize()
|
|
86
|
+
beacon.initialize()
|
|
87
|
+
assert (home / "heartbeats").is_dir()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Pulse
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TestPulse:
|
|
96
|
+
"""Tests for heartbeat publishing."""
|
|
97
|
+
|
|
98
|
+
def test_pulse_creates_file(self, beacon: HeartbeatBeacon, home: Path) -> None:
|
|
99
|
+
"""Pulse creates the agent's heartbeat file."""
|
|
100
|
+
beacon.pulse()
|
|
101
|
+
assert (home / "heartbeats" / "opus.json").exists()
|
|
102
|
+
|
|
103
|
+
def test_pulse_returns_heartbeat(self, beacon: HeartbeatBeacon) -> None:
|
|
104
|
+
"""Pulse returns a Heartbeat object."""
|
|
105
|
+
hb = beacon.pulse()
|
|
106
|
+
assert hb.agent_name == "opus"
|
|
107
|
+
assert hb.status == "alive"
|
|
108
|
+
assert hb.is_alive is True
|
|
109
|
+
|
|
110
|
+
def test_pulse_with_status(self, beacon: HeartbeatBeacon) -> None:
|
|
111
|
+
"""Pulse accepts custom status."""
|
|
112
|
+
hb = beacon.pulse(status="busy")
|
|
113
|
+
assert hb.status == "busy"
|
|
114
|
+
|
|
115
|
+
def test_pulse_with_tasks(self, beacon: HeartbeatBeacon) -> None:
|
|
116
|
+
"""Pulse tracks claimed tasks."""
|
|
117
|
+
hb = beacon.pulse(claimed_tasks=["task1", "task2"])
|
|
118
|
+
assert hb.claimed_tasks == ["task1", "task2"]
|
|
119
|
+
|
|
120
|
+
def test_pulse_with_model(self, beacon: HeartbeatBeacon) -> None:
|
|
121
|
+
"""Pulse tracks loaded model."""
|
|
122
|
+
hb = beacon.pulse(loaded_model="claude-opus-4-6")
|
|
123
|
+
assert hb.loaded_model == "claude-opus-4-6"
|
|
124
|
+
|
|
125
|
+
def test_pulse_detects_capacity(self, beacon: HeartbeatBeacon) -> None:
|
|
126
|
+
"""Pulse detects node capacity."""
|
|
127
|
+
hb = beacon.pulse()
|
|
128
|
+
assert hb.capacity.cpu_count > 0
|
|
129
|
+
|
|
130
|
+
def test_pulse_detects_fingerprint(self, beacon: HeartbeatBeacon) -> None:
|
|
131
|
+
"""Pulse reads identity fingerprint."""
|
|
132
|
+
hb = beacon.pulse()
|
|
133
|
+
assert hb.fingerprint == "ABCD1234567890AB"
|
|
134
|
+
|
|
135
|
+
def test_pulse_atomic_write(self, beacon: HeartbeatBeacon, home: Path) -> None:
|
|
136
|
+
"""Pulse uses atomic write (no .tmp left)."""
|
|
137
|
+
beacon.pulse()
|
|
138
|
+
tmp = home / "heartbeats" / "opus.json.tmp"
|
|
139
|
+
assert not tmp.exists()
|
|
140
|
+
|
|
141
|
+
def test_pulse_with_capabilities(self, beacon: HeartbeatBeacon) -> None:
|
|
142
|
+
"""Pulse accepts custom capabilities."""
|
|
143
|
+
caps = [AgentCapability(name="code-review", version="2.0")]
|
|
144
|
+
hb = beacon.pulse(capabilities=caps)
|
|
145
|
+
assert len(hb.capabilities) == 1
|
|
146
|
+
assert hb.capabilities[0].name == "code-review"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
# Read heartbeat
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class TestReadHeartbeat:
|
|
155
|
+
"""Tests for reading heartbeats."""
|
|
156
|
+
|
|
157
|
+
def test_read_own_heartbeat(self, beacon: HeartbeatBeacon) -> None:
|
|
158
|
+
"""Read own heartbeat after pulse."""
|
|
159
|
+
beacon.pulse()
|
|
160
|
+
hb = beacon.read_heartbeat("opus")
|
|
161
|
+
assert hb is not None
|
|
162
|
+
assert hb.agent_name == "opus"
|
|
163
|
+
|
|
164
|
+
def test_read_peer_heartbeat(self, beacon: HeartbeatBeacon, home: Path) -> None:
|
|
165
|
+
"""Read a peer's heartbeat."""
|
|
166
|
+
_write_peer_heartbeat(home, "lumina")
|
|
167
|
+
hb = beacon.read_heartbeat("lumina")
|
|
168
|
+
assert hb is not None
|
|
169
|
+
assert hb.agent_name == "lumina"
|
|
170
|
+
|
|
171
|
+
def test_read_nonexistent(self, beacon: HeartbeatBeacon) -> None:
|
|
172
|
+
"""Reading nonexistent heartbeat returns None."""
|
|
173
|
+
assert beacon.read_heartbeat("ghost") is None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
# Discover peers
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class TestDiscoverPeers:
|
|
182
|
+
"""Tests for peer discovery."""
|
|
183
|
+
|
|
184
|
+
def test_discover_empty(self, beacon: HeartbeatBeacon) -> None:
|
|
185
|
+
"""Empty mesh returns no peers."""
|
|
186
|
+
peers = beacon.discover_peers()
|
|
187
|
+
assert peers == []
|
|
188
|
+
|
|
189
|
+
def test_discover_excludes_self(self, beacon: HeartbeatBeacon, home: Path) -> None:
|
|
190
|
+
"""Discovery excludes own heartbeat by default."""
|
|
191
|
+
beacon.pulse()
|
|
192
|
+
_write_peer_heartbeat(home, "lumina")
|
|
193
|
+
peers = beacon.discover_peers()
|
|
194
|
+
names = [p.agent_name for p in peers]
|
|
195
|
+
assert "lumina" in names
|
|
196
|
+
assert "opus" not in names
|
|
197
|
+
|
|
198
|
+
def test_discover_includes_self(self, beacon: HeartbeatBeacon, home: Path) -> None:
|
|
199
|
+
"""Discovery can include own heartbeat."""
|
|
200
|
+
beacon.pulse()
|
|
201
|
+
peers = beacon.discover_peers(include_self=True)
|
|
202
|
+
names = [p.agent_name for p in peers]
|
|
203
|
+
assert "opus" in names
|
|
204
|
+
|
|
205
|
+
def test_discover_marks_stale_offline(self, beacon: HeartbeatBeacon, home: Path) -> None:
|
|
206
|
+
"""Stale heartbeats are marked as offline."""
|
|
207
|
+
_write_peer_heartbeat(home, "stale-agent", ttl_seconds=60, age_seconds=120)
|
|
208
|
+
peers = beacon.discover_peers()
|
|
209
|
+
stale = next(p for p in peers if p.agent_name == "stale-agent")
|
|
210
|
+
assert stale.alive is False
|
|
211
|
+
assert stale.status == "offline"
|
|
212
|
+
|
|
213
|
+
def test_discover_live_peers(self, beacon: HeartbeatBeacon, home: Path) -> None:
|
|
214
|
+
"""Live heartbeats are correctly identified."""
|
|
215
|
+
_write_peer_heartbeat(home, "live-agent", ttl_seconds=300, age_seconds=10)
|
|
216
|
+
peers = beacon.discover_peers()
|
|
217
|
+
live = next(p for p in peers if p.agent_name == "live-agent")
|
|
218
|
+
assert live.alive is True
|
|
219
|
+
assert live.status == "alive"
|
|
220
|
+
|
|
221
|
+
def test_discover_with_capabilities(self, beacon: HeartbeatBeacon, home: Path) -> None:
|
|
222
|
+
"""Peer capabilities are included in discovery."""
|
|
223
|
+
_write_peer_heartbeat(
|
|
224
|
+
home, "capable-agent",
|
|
225
|
+
capabilities=[{"name": "code-review", "enabled": True}],
|
|
226
|
+
)
|
|
227
|
+
peers = beacon.discover_peers()
|
|
228
|
+
cap = next(p for p in peers if p.agent_name == "capable-agent")
|
|
229
|
+
assert "code-review" in cap.capabilities
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
# Mesh health
|
|
234
|
+
# ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class TestMeshHealth:
|
|
238
|
+
"""Tests for mesh health reporting."""
|
|
239
|
+
|
|
240
|
+
def test_mesh_health_empty(self, beacon: HeartbeatBeacon) -> None:
|
|
241
|
+
"""Empty mesh health."""
|
|
242
|
+
health = beacon.mesh_health()
|
|
243
|
+
assert health.total_peers == 0
|
|
244
|
+
assert health.alive_peers == 0
|
|
245
|
+
|
|
246
|
+
def test_mesh_health_mixed(self, beacon: HeartbeatBeacon, home: Path) -> None:
|
|
247
|
+
"""Mixed mesh with alive and stale peers."""
|
|
248
|
+
beacon.pulse()
|
|
249
|
+
_write_peer_heartbeat(home, "lumina", age_seconds=10)
|
|
250
|
+
_write_peer_heartbeat(home, "stale", ttl_seconds=60, age_seconds=120)
|
|
251
|
+
|
|
252
|
+
health = beacon.mesh_health()
|
|
253
|
+
assert health.total_peers == 3 # opus + lumina + stale
|
|
254
|
+
assert health.alive_peers == 2
|
|
255
|
+
assert health.offline_peers == 1
|
|
256
|
+
|
|
257
|
+
def test_mesh_health_capabilities(self, beacon: HeartbeatBeacon, home: Path) -> None:
|
|
258
|
+
"""Mesh health aggregates capabilities."""
|
|
259
|
+
_write_peer_heartbeat(
|
|
260
|
+
home, "agent-a",
|
|
261
|
+
capabilities=[{"name": "code-review", "enabled": True}],
|
|
262
|
+
)
|
|
263
|
+
_write_peer_heartbeat(
|
|
264
|
+
home, "agent-b",
|
|
265
|
+
capabilities=[{"name": "deployment", "enabled": True}],
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
health = beacon.mesh_health()
|
|
269
|
+
assert "code-review" in health.total_capabilities
|
|
270
|
+
assert "deployment" in health.total_capabilities
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# ---------------------------------------------------------------------------
|
|
274
|
+
# Find capable
|
|
275
|
+
# ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class TestFindCapable:
|
|
279
|
+
"""Tests for capability-based peer search."""
|
|
280
|
+
|
|
281
|
+
def test_find_capable_peers(self, beacon: HeartbeatBeacon, home: Path) -> None:
|
|
282
|
+
"""Find peers with a specific capability."""
|
|
283
|
+
_write_peer_heartbeat(
|
|
284
|
+
home, "reviewer",
|
|
285
|
+
capabilities=[{"name": "code-review", "enabled": True}],
|
|
286
|
+
)
|
|
287
|
+
_write_peer_heartbeat(
|
|
288
|
+
home, "deployer",
|
|
289
|
+
capabilities=[{"name": "deployment", "enabled": True}],
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
reviewers = beacon.find_capable("code-review")
|
|
293
|
+
assert len(reviewers) == 1
|
|
294
|
+
assert reviewers[0].agent_name == "reviewer"
|
|
295
|
+
|
|
296
|
+
def test_find_capable_none(self, beacon: HeartbeatBeacon) -> None:
|
|
297
|
+
"""No peers with capability returns empty."""
|
|
298
|
+
assert beacon.find_capable("nonexistent") == []
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
# Mark offline
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class TestMarkOffline:
|
|
307
|
+
"""Tests for offline marking."""
|
|
308
|
+
|
|
309
|
+
def test_mark_offline(self, beacon: HeartbeatBeacon) -> None:
|
|
310
|
+
"""Mark offline publishes offline status."""
|
|
311
|
+
beacon.mark_offline()
|
|
312
|
+
hb = beacon.read_heartbeat("opus")
|
|
313
|
+
assert hb is not None
|
|
314
|
+
assert hb.status == "offline"
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# ---------------------------------------------------------------------------
|
|
318
|
+
# Model tests
|
|
319
|
+
# ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class TestModels:
|
|
323
|
+
"""Tests for heartbeat models."""
|
|
324
|
+
|
|
325
|
+
def test_heartbeat_defaults(self) -> None:
|
|
326
|
+
"""Heartbeat has sensible defaults."""
|
|
327
|
+
hb = Heartbeat(agent_name="test")
|
|
328
|
+
assert hb.status == "alive"
|
|
329
|
+
assert hb.is_alive is True
|
|
330
|
+
assert hb.ttl_seconds == 300
|
|
331
|
+
|
|
332
|
+
def test_heartbeat_expired(self) -> None:
|
|
333
|
+
"""Expired heartbeat detected."""
|
|
334
|
+
hb = Heartbeat(
|
|
335
|
+
agent_name="old",
|
|
336
|
+
timestamp=datetime.now(timezone.utc) - timedelta(hours=1),
|
|
337
|
+
ttl_seconds=60,
|
|
338
|
+
)
|
|
339
|
+
assert hb.is_alive is False
|
|
340
|
+
|
|
341
|
+
def test_node_capacity_defaults(self) -> None:
|
|
342
|
+
"""NodeCapacity has sensible defaults."""
|
|
343
|
+
cap = NodeCapacity()
|
|
344
|
+
assert cap.cpu_count == 0
|
|
345
|
+
assert cap.gpu_available is False
|
|
346
|
+
|
|
347
|
+
def test_peer_info_defaults(self) -> None:
|
|
348
|
+
"""PeerInfo has sensible defaults."""
|
|
349
|
+
p = PeerInfo(
|
|
350
|
+
agent_name="test", status="alive",
|
|
351
|
+
alive=True, age_seconds=10,
|
|
352
|
+
)
|
|
353
|
+
assert p.capabilities == []
|
|
354
|
+
assert p.claimed_tasks == 0
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Tests for skcapstone.housekeeping — storage pruning."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from skcapstone.housekeeping import (
|
|
9
|
+
prune_acks,
|
|
10
|
+
prune_comms_outbox,
|
|
11
|
+
prune_seeds,
|
|
12
|
+
run_housekeeping,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def skcomm_home(tmp_path):
|
|
18
|
+
"""Create a mock ~/.skcomm directory with test ACK files."""
|
|
19
|
+
acks_dir = tmp_path / "acks"
|
|
20
|
+
acks_dir.mkdir()
|
|
21
|
+
return tmp_path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def skcapstone_home(tmp_path):
|
|
26
|
+
"""Create a mock ~/.skcapstone directory with sync structure."""
|
|
27
|
+
sync_dir = tmp_path / "sync"
|
|
28
|
+
comms_out = sync_dir / "comms" / "outbox"
|
|
29
|
+
seed_out = sync_dir / "sync" / "outbox"
|
|
30
|
+
comms_out.mkdir(parents=True)
|
|
31
|
+
seed_out.mkdir(parents=True)
|
|
32
|
+
return tmp_path
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestPruneAcks:
|
|
36
|
+
"""Tests for prune_acks."""
|
|
37
|
+
|
|
38
|
+
def test_no_acks_dir(self, tmp_path):
|
|
39
|
+
"""Returns 0 when acks directory doesn't exist."""
|
|
40
|
+
assert prune_acks(tmp_path) == 0
|
|
41
|
+
|
|
42
|
+
def test_empty_acks_dir(self, skcomm_home):
|
|
43
|
+
"""Returns 0 when acks directory is empty."""
|
|
44
|
+
assert prune_acks(skcomm_home) == 0
|
|
45
|
+
|
|
46
|
+
def test_deletes_old_acks(self, skcomm_home):
|
|
47
|
+
"""Deletes ACK files older than max_age_hours."""
|
|
48
|
+
acks_dir = skcomm_home / "acks"
|
|
49
|
+
# Create 5 old files (mtime set to 48h ago)
|
|
50
|
+
old_time = time.time() - (48 * 3600)
|
|
51
|
+
for i in range(5):
|
|
52
|
+
f = acks_dir / f"ack-{i}.json"
|
|
53
|
+
f.write_text("{}")
|
|
54
|
+
import os
|
|
55
|
+
os.utime(f, (old_time, old_time))
|
|
56
|
+
|
|
57
|
+
# Create 3 fresh files
|
|
58
|
+
for i in range(3):
|
|
59
|
+
f = acks_dir / f"fresh-{i}.json"
|
|
60
|
+
f.write_text("{}")
|
|
61
|
+
|
|
62
|
+
deleted = prune_acks(skcomm_home, max_age_hours=24)
|
|
63
|
+
assert deleted == 5
|
|
64
|
+
remaining = list(acks_dir.iterdir())
|
|
65
|
+
assert len(remaining) == 3
|
|
66
|
+
|
|
67
|
+
def test_respects_max_age(self, skcomm_home):
|
|
68
|
+
"""Only deletes files older than the specified max_age."""
|
|
69
|
+
acks_dir = skcomm_home / "acks"
|
|
70
|
+
# File 1h old
|
|
71
|
+
f = acks_dir / "recent.json"
|
|
72
|
+
f.write_text("{}")
|
|
73
|
+
import os
|
|
74
|
+
os.utime(f, (time.time() - 3600, time.time() - 3600))
|
|
75
|
+
|
|
76
|
+
assert prune_acks(skcomm_home, max_age_hours=2) == 0
|
|
77
|
+
assert prune_acks(skcomm_home, max_age_hours=0) == 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TestPruneCommsOutbox:
|
|
81
|
+
"""Tests for prune_comms_outbox."""
|
|
82
|
+
|
|
83
|
+
def test_no_outbox_dir(self, tmp_path):
|
|
84
|
+
"""Returns 0 when outbox directory doesn't exist."""
|
|
85
|
+
assert prune_comms_outbox(tmp_path) == 0
|
|
86
|
+
|
|
87
|
+
def test_empty_outbox(self, skcapstone_home):
|
|
88
|
+
"""Returns 0 when outbox is empty."""
|
|
89
|
+
assert prune_comms_outbox(skcapstone_home / "sync") == 0
|
|
90
|
+
|
|
91
|
+
def test_deletes_old_envelopes(self, skcapstone_home):
|
|
92
|
+
"""Deletes envelope files older than max_age_hours."""
|
|
93
|
+
agent_dir = skcapstone_home / "sync" / "comms" / "outbox" / "lumina"
|
|
94
|
+
agent_dir.mkdir(parents=True)
|
|
95
|
+
|
|
96
|
+
old_time = time.time() - (72 * 3600)
|
|
97
|
+
for i in range(4):
|
|
98
|
+
f = agent_dir / f"env-{i}.json"
|
|
99
|
+
f.write_text("{}")
|
|
100
|
+
import os
|
|
101
|
+
os.utime(f, (old_time, old_time))
|
|
102
|
+
|
|
103
|
+
f = agent_dir / "fresh.json"
|
|
104
|
+
f.write_text("{}")
|
|
105
|
+
|
|
106
|
+
deleted = prune_comms_outbox(skcapstone_home / "sync", max_age_hours=48)
|
|
107
|
+
assert deleted == 4
|
|
108
|
+
assert (agent_dir / "fresh.json").exists()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class TestPruneSeeds:
|
|
112
|
+
"""Tests for prune_seeds."""
|
|
113
|
+
|
|
114
|
+
def test_no_outbox_dir(self, tmp_path):
|
|
115
|
+
"""Returns 0 when seed outbox doesn't exist."""
|
|
116
|
+
assert prune_seeds(tmp_path / "nonexistent") == 0
|
|
117
|
+
|
|
118
|
+
def test_keeps_recent_seeds(self, skcapstone_home):
|
|
119
|
+
"""Keeps only keep_per_agent most recent seeds."""
|
|
120
|
+
seed_dir = skcapstone_home / "sync" / "sync" / "outbox"
|
|
121
|
+
|
|
122
|
+
# Create 15 seeds for agent "opus"
|
|
123
|
+
for i in range(15):
|
|
124
|
+
f = seed_dir / f"opus-170900000{i:01d}.json.gpg"
|
|
125
|
+
f.write_text("{}")
|
|
126
|
+
import os
|
|
127
|
+
os.utime(f, (time.time() - (15 - i) * 300, time.time() - (15 - i) * 300))
|
|
128
|
+
|
|
129
|
+
deleted = prune_seeds(seed_dir, keep_per_agent=10)
|
|
130
|
+
assert deleted == 5
|
|
131
|
+
remaining = list(seed_dir.iterdir())
|
|
132
|
+
assert len(remaining) == 10
|
|
133
|
+
|
|
134
|
+
def test_handles_multiple_agents(self, skcapstone_home):
|
|
135
|
+
"""Keeps seeds per-agent, not globally."""
|
|
136
|
+
seed_dir = skcapstone_home / "sync" / "sync" / "outbox"
|
|
137
|
+
|
|
138
|
+
for agent in ("opus", "lumina"):
|
|
139
|
+
for i in range(5):
|
|
140
|
+
f = seed_dir / f"{agent}-170900000{i}.json"
|
|
141
|
+
f.write_text("{}")
|
|
142
|
+
|
|
143
|
+
deleted = prune_seeds(seed_dir, keep_per_agent=3)
|
|
144
|
+
assert deleted == 4 # 2 excess per agent
|
|
145
|
+
|
|
146
|
+
def test_empty_outbox(self, skcapstone_home):
|
|
147
|
+
"""Returns 0 when seed outbox is empty."""
|
|
148
|
+
seed_dir = skcapstone_home / "sync" / "sync" / "outbox"
|
|
149
|
+
assert prune_seeds(seed_dir) == 0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class TestRunHousekeeping:
|
|
153
|
+
"""Tests for run_housekeeping."""
|
|
154
|
+
|
|
155
|
+
def test_dry_run(self, tmp_path):
|
|
156
|
+
"""Dry run reports counts without deleting."""
|
|
157
|
+
# Set up dirs
|
|
158
|
+
acks_dir = tmp_path / "skcomm" / "acks"
|
|
159
|
+
acks_dir.mkdir(parents=True)
|
|
160
|
+
for i in range(3):
|
|
161
|
+
f = acks_dir / f"old-{i}.json"
|
|
162
|
+
f.write_text("{}")
|
|
163
|
+
import os
|
|
164
|
+
os.utime(f, (time.time() - 48 * 3600, time.time() - 48 * 3600))
|
|
165
|
+
|
|
166
|
+
results = run_housekeeping(
|
|
167
|
+
skcapstone_home=tmp_path / "skcapstone",
|
|
168
|
+
skcomm_home=tmp_path / "skcomm",
|
|
169
|
+
dry_run=True,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
assert results.get("dry_run") is True
|
|
173
|
+
assert results["acks"]["would_delete"] == 3
|
|
174
|
+
# Files should still exist
|
|
175
|
+
assert len(list(acks_dir.iterdir())) == 3
|
|
176
|
+
|
|
177
|
+
def test_full_run(self, tmp_path):
|
|
178
|
+
"""Full run deletes files and reports summary."""
|
|
179
|
+
acks_dir = tmp_path / "skcomm" / "acks"
|
|
180
|
+
acks_dir.mkdir(parents=True)
|
|
181
|
+
for i in range(2):
|
|
182
|
+
f = acks_dir / f"old-{i}.json"
|
|
183
|
+
f.write_text("{}")
|
|
184
|
+
import os
|
|
185
|
+
os.utime(f, (time.time() - 48 * 3600, time.time() - 48 * 3600))
|
|
186
|
+
|
|
187
|
+
results = run_housekeeping(
|
|
188
|
+
skcapstone_home=tmp_path / "skcapstone",
|
|
189
|
+
skcomm_home=tmp_path / "skcomm",
|
|
190
|
+
dry_run=False,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
assert "summary" in results
|
|
194
|
+
assert results["acks"]["deleted"] == 2
|
|
195
|
+
assert len(list(acks_dir.iterdir())) == 0
|