@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,397 @@
|
|
|
1
|
+
"""Tests for the Team Engine — deployment orchestration for agent teams."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict
|
|
9
|
+
from unittest.mock import MagicMock
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from skcapstone.blueprints.schema import (
|
|
14
|
+
AgentRole,
|
|
15
|
+
AgentSpec,
|
|
16
|
+
BlueprintManifest,
|
|
17
|
+
ProviderType,
|
|
18
|
+
)
|
|
19
|
+
from skcapstone.team_engine import (
|
|
20
|
+
AgentStatus,
|
|
21
|
+
DeployedAgent,
|
|
22
|
+
ProviderBackend,
|
|
23
|
+
TeamDeployment,
|
|
24
|
+
TeamEngine,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Fixtures
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _make_blueprint(
|
|
34
|
+
agents: dict[str, dict] | None = None,
|
|
35
|
+
name: str = "test-team",
|
|
36
|
+
) -> BlueprintManifest:
|
|
37
|
+
"""Create a minimal BlueprintManifest for testing."""
|
|
38
|
+
if agents is None:
|
|
39
|
+
agents = {
|
|
40
|
+
"leader": {"role": "manager", "model": "reason"},
|
|
41
|
+
"worker": {"role": "worker", "model": "fast"},
|
|
42
|
+
}
|
|
43
|
+
specs = {}
|
|
44
|
+
for key, kwargs in agents.items():
|
|
45
|
+
specs[key] = AgentSpec(**kwargs)
|
|
46
|
+
return BlueprintManifest(
|
|
47
|
+
name=name,
|
|
48
|
+
slug=name,
|
|
49
|
+
version="1.0",
|
|
50
|
+
description="Test blueprint",
|
|
51
|
+
agents=specs,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class MockProvider(ProviderBackend):
|
|
56
|
+
"""Mock provider that tracks calls."""
|
|
57
|
+
|
|
58
|
+
provider_type = ProviderType.LOCAL
|
|
59
|
+
|
|
60
|
+
def __init__(self) -> None:
|
|
61
|
+
self.calls: list[tuple[str, str]] = []
|
|
62
|
+
self.fail_on: set[str] = set()
|
|
63
|
+
|
|
64
|
+
def provision(self, agent_name: str, spec: AgentSpec, team_name: str) -> Dict[str, Any]:
|
|
65
|
+
self.calls.append(("provision", agent_name))
|
|
66
|
+
if agent_name in self.fail_on:
|
|
67
|
+
raise RuntimeError(f"provision failed for {agent_name}")
|
|
68
|
+
return {"host": "localhost", "pid": 1234}
|
|
69
|
+
|
|
70
|
+
def configure(self, agent_name: str, spec: AgentSpec, provision_result: Dict[str, Any]) -> bool:
|
|
71
|
+
self.calls.append(("configure", agent_name))
|
|
72
|
+
if agent_name in self.fail_on:
|
|
73
|
+
raise RuntimeError(f"configure failed for {agent_name}")
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
def start(self, agent_name: str, provision_result: Dict[str, Any]) -> bool:
|
|
77
|
+
self.calls.append(("start", agent_name))
|
|
78
|
+
if agent_name in self.fail_on:
|
|
79
|
+
raise RuntimeError(f"start failed for {agent_name}")
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
def stop(self, agent_name: str, provision_result: Dict[str, Any]) -> bool:
|
|
83
|
+
self.calls.append(("stop", agent_name))
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
def destroy(self, agent_name: str, provision_result: Dict[str, Any]) -> bool:
|
|
87
|
+
self.calls.append(("destroy", agent_name))
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
def health_check(self, agent_name: str, provision_result: Dict[str, Any]) -> AgentStatus:
|
|
91
|
+
self.calls.append(("health_check", agent_name))
|
|
92
|
+
return AgentStatus.RUNNING
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@pytest.fixture
|
|
96
|
+
def home(tmp_path: Path) -> Path:
|
|
97
|
+
"""Create a minimal agent home directory."""
|
|
98
|
+
(tmp_path / "deployments").mkdir()
|
|
99
|
+
(tmp_path / "comms").mkdir()
|
|
100
|
+
return tmp_path
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@pytest.fixture
|
|
104
|
+
def provider() -> MockProvider:
|
|
105
|
+
"""Create a mock provider backend."""
|
|
106
|
+
return MockProvider()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@pytest.fixture
|
|
110
|
+
def engine(home: Path, provider: MockProvider) -> TeamEngine:
|
|
111
|
+
"""Create a TeamEngine with mock provider."""
|
|
112
|
+
return TeamEngine(home=home, provider=provider, comms_root=home / "comms")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@pytest.fixture
|
|
116
|
+
def blueprint() -> BlueprintManifest:
|
|
117
|
+
"""Create a basic two-agent blueprint."""
|
|
118
|
+
return _make_blueprint()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# Model tests
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TestModels:
|
|
127
|
+
"""Tests for team engine data models."""
|
|
128
|
+
|
|
129
|
+
def test_agent_status_values(self) -> None:
|
|
130
|
+
"""AgentStatus has expected values."""
|
|
131
|
+
assert AgentStatus.PENDING.value == "pending"
|
|
132
|
+
assert AgentStatus.RUNNING.value == "running"
|
|
133
|
+
assert AgentStatus.FAILED.value == "failed"
|
|
134
|
+
|
|
135
|
+
def test_deployed_agent_defaults(self) -> None:
|
|
136
|
+
"""DeployedAgent has sensible defaults."""
|
|
137
|
+
agent = DeployedAgent(
|
|
138
|
+
name="test",
|
|
139
|
+
instance_id="inst-1",
|
|
140
|
+
blueprint_slug="test-bp",
|
|
141
|
+
agent_spec_key="worker",
|
|
142
|
+
)
|
|
143
|
+
assert agent.status == AgentStatus.PENDING
|
|
144
|
+
assert agent.host is None
|
|
145
|
+
assert agent.error is None
|
|
146
|
+
|
|
147
|
+
def test_team_deployment_defaults(self) -> None:
|
|
148
|
+
"""TeamDeployment has sensible defaults."""
|
|
149
|
+
dep = TeamDeployment(
|
|
150
|
+
deployment_id="dep-1",
|
|
151
|
+
blueprint_slug="test-bp",
|
|
152
|
+
team_name="test-team",
|
|
153
|
+
provider=ProviderType.LOCAL,
|
|
154
|
+
)
|
|
155
|
+
assert dep.status == "deploying"
|
|
156
|
+
assert dep.agents == {}
|
|
157
|
+
|
|
158
|
+
def test_team_deployment_serialization(self) -> None:
|
|
159
|
+
"""TeamDeployment serializes to/from JSON."""
|
|
160
|
+
dep = TeamDeployment(
|
|
161
|
+
deployment_id="dep-1",
|
|
162
|
+
blueprint_slug="test-bp",
|
|
163
|
+
team_name="test-team",
|
|
164
|
+
provider=ProviderType.LOCAL,
|
|
165
|
+
agents={
|
|
166
|
+
"agent-a": DeployedAgent(
|
|
167
|
+
name="agent-a",
|
|
168
|
+
instance_id="i-1",
|
|
169
|
+
blueprint_slug="test-bp",
|
|
170
|
+
agent_spec_key="worker",
|
|
171
|
+
),
|
|
172
|
+
},
|
|
173
|
+
)
|
|
174
|
+
data = dep.model_dump_json()
|
|
175
|
+
restored = TeamDeployment.model_validate_json(data)
|
|
176
|
+
assert restored.deployment_id == "dep-1"
|
|
177
|
+
assert "agent-a" in restored.agents
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
# Initialization
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class TestInitialization:
|
|
186
|
+
"""Tests for TeamEngine setup."""
|
|
187
|
+
|
|
188
|
+
def test_engine_creates_deployments_dir(self, tmp_path: Path) -> None:
|
|
189
|
+
"""TeamEngine uses given home directory."""
|
|
190
|
+
engine = TeamEngine(home=tmp_path, provider=None)
|
|
191
|
+
assert engine._deployments_dir == tmp_path / "deployments"
|
|
192
|
+
|
|
193
|
+
def test_engine_without_provider(self, home: Path) -> None:
|
|
194
|
+
"""TeamEngine can be created without a provider (dry-run)."""
|
|
195
|
+
engine = TeamEngine(home=home, provider=None)
|
|
196
|
+
assert engine._provider is None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# Deploy order resolution
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class TestDeployOrder:
|
|
205
|
+
"""Tests for dependency-ordered deployment waves."""
|
|
206
|
+
|
|
207
|
+
def test_no_dependencies(self) -> None:
|
|
208
|
+
"""Agents without dependencies deploy in one wave."""
|
|
209
|
+
bp = _make_blueprint({
|
|
210
|
+
"a": {"role": "worker", "model": "fast"},
|
|
211
|
+
"b": {"role": "worker", "model": "fast"},
|
|
212
|
+
})
|
|
213
|
+
waves = TeamEngine.resolve_deploy_order(bp)
|
|
214
|
+
assert len(waves) == 1
|
|
215
|
+
assert set(waves[0]) == {"a", "b"}
|
|
216
|
+
|
|
217
|
+
def test_simple_dependency(self) -> None:
|
|
218
|
+
"""Agent with dependency deploys after its dependency."""
|
|
219
|
+
bp = _make_blueprint({
|
|
220
|
+
"leader": {"role": "manager", "model": "reason"},
|
|
221
|
+
"worker": {"role": "worker", "model": "fast", "depends_on": ["leader"]},
|
|
222
|
+
})
|
|
223
|
+
waves = TeamEngine.resolve_deploy_order(bp)
|
|
224
|
+
assert len(waves) == 2
|
|
225
|
+
assert "leader" in waves[0]
|
|
226
|
+
assert "worker" in waves[1]
|
|
227
|
+
|
|
228
|
+
def test_diamond_dependency(self) -> None:
|
|
229
|
+
"""Diamond dependency graph resolves correctly."""
|
|
230
|
+
bp = _make_blueprint({
|
|
231
|
+
"base": {"role": "manager", "model": "reason"},
|
|
232
|
+
"mid-a": {"role": "worker", "model": "fast", "depends_on": ["base"]},
|
|
233
|
+
"mid-b": {"role": "worker", "model": "fast", "depends_on": ["base"]},
|
|
234
|
+
"top": {"role": "worker", "model": "fast", "depends_on": ["mid-a", "mid-b"]},
|
|
235
|
+
})
|
|
236
|
+
waves = TeamEngine.resolve_deploy_order(bp)
|
|
237
|
+
assert len(waves) == 3
|
|
238
|
+
assert "base" in waves[0]
|
|
239
|
+
assert set(waves[1]) == {"mid-a", "mid-b"}
|
|
240
|
+
assert "top" in waves[2]
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
# Deployment
|
|
245
|
+
# ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class TestDeploy:
|
|
249
|
+
"""Tests for deployment orchestration."""
|
|
250
|
+
|
|
251
|
+
def test_deploy_creates_agents(
|
|
252
|
+
self, engine: TeamEngine, blueprint: BlueprintManifest,
|
|
253
|
+
) -> None:
|
|
254
|
+
"""Deploy creates agents for each spec in blueprint."""
|
|
255
|
+
dep = engine.deploy(blueprint)
|
|
256
|
+
assert len(dep.agents) == 2
|
|
257
|
+
assert "leader" in dep.agents or any(
|
|
258
|
+
"leader" in a.agent_spec_key for a in dep.agents.values()
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
def test_deploy_calls_provider(
|
|
262
|
+
self, engine: TeamEngine, provider: MockProvider, blueprint: BlueprintManifest,
|
|
263
|
+
) -> None:
|
|
264
|
+
"""Deploy calls provision/configure/start on provider."""
|
|
265
|
+
engine.deploy(blueprint)
|
|
266
|
+
actions = [action for action, _ in provider.calls]
|
|
267
|
+
assert "provision" in actions
|
|
268
|
+
assert "configure" in actions
|
|
269
|
+
assert "start" in actions
|
|
270
|
+
|
|
271
|
+
def test_deploy_saves_state(
|
|
272
|
+
self, engine: TeamEngine, blueprint: BlueprintManifest, home: Path,
|
|
273
|
+
) -> None:
|
|
274
|
+
"""Deploy persists state to disk."""
|
|
275
|
+
dep = engine.deploy(blueprint)
|
|
276
|
+
state_file = home / "deployments" / f"{dep.deployment_id}.json"
|
|
277
|
+
assert state_file.exists()
|
|
278
|
+
|
|
279
|
+
def test_deploy_returns_deployment(
|
|
280
|
+
self, engine: TeamEngine, blueprint: BlueprintManifest,
|
|
281
|
+
) -> None:
|
|
282
|
+
"""Deploy returns a TeamDeployment."""
|
|
283
|
+
dep = engine.deploy(blueprint)
|
|
284
|
+
assert isinstance(dep, TeamDeployment)
|
|
285
|
+
assert dep.blueprint_slug == "test-team"
|
|
286
|
+
|
|
287
|
+
def test_deploy_custom_name(
|
|
288
|
+
self, engine: TeamEngine, blueprint: BlueprintManifest,
|
|
289
|
+
) -> None:
|
|
290
|
+
"""Deploy accepts custom deployment name."""
|
|
291
|
+
dep = engine.deploy(blueprint, name="my-team")
|
|
292
|
+
assert dep.team_name == "my-team"
|
|
293
|
+
|
|
294
|
+
def test_deploy_without_provider(
|
|
295
|
+
self, home: Path, blueprint: BlueprintManifest,
|
|
296
|
+
) -> None:
|
|
297
|
+
"""Deploy works in dry-run mode without a provider."""
|
|
298
|
+
engine = TeamEngine(home=home, provider=None)
|
|
299
|
+
dep = engine.deploy(blueprint)
|
|
300
|
+
# All agents should be in pending or some initial state
|
|
301
|
+
assert len(dep.agents) >= 1
|
|
302
|
+
|
|
303
|
+
def test_deploy_handles_agent_failure(
|
|
304
|
+
self, engine: TeamEngine, provider: MockProvider,
|
|
305
|
+
) -> None:
|
|
306
|
+
"""Deploy continues even if one agent fails."""
|
|
307
|
+
provider.fail_on.add("worker")
|
|
308
|
+
bp = _make_blueprint({
|
|
309
|
+
"leader": {"role": "manager", "model": "reason"},
|
|
310
|
+
"worker": {"role": "worker", "model": "fast"},
|
|
311
|
+
})
|
|
312
|
+
dep = engine.deploy(bp)
|
|
313
|
+
# Deployment should still complete
|
|
314
|
+
assert len(dep.agents) >= 1
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# ---------------------------------------------------------------------------
|
|
318
|
+
# List / Get / Destroy
|
|
319
|
+
# ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class TestLifecycle:
|
|
323
|
+
"""Tests for listing, getting, and destroying deployments."""
|
|
324
|
+
|
|
325
|
+
def test_list_empty(self, engine: TeamEngine) -> None:
|
|
326
|
+
"""Empty engine returns no deployments."""
|
|
327
|
+
assert engine.list_deployments() == []
|
|
328
|
+
|
|
329
|
+
def test_list_after_deploy(
|
|
330
|
+
self, engine: TeamEngine, blueprint: BlueprintManifest,
|
|
331
|
+
) -> None:
|
|
332
|
+
"""Deployed teams appear in listing."""
|
|
333
|
+
engine.deploy(blueprint)
|
|
334
|
+
deps = engine.list_deployments()
|
|
335
|
+
assert len(deps) == 1
|
|
336
|
+
|
|
337
|
+
def test_get_deployment(
|
|
338
|
+
self, engine: TeamEngine, blueprint: BlueprintManifest,
|
|
339
|
+
) -> None:
|
|
340
|
+
"""Can retrieve a deployment by ID."""
|
|
341
|
+
dep = engine.deploy(blueprint)
|
|
342
|
+
retrieved = engine.get_deployment(dep.deployment_id)
|
|
343
|
+
assert retrieved is not None
|
|
344
|
+
assert retrieved.deployment_id == dep.deployment_id
|
|
345
|
+
|
|
346
|
+
def test_get_nonexistent(self, engine: TeamEngine) -> None:
|
|
347
|
+
"""Getting nonexistent deployment returns None."""
|
|
348
|
+
assert engine.get_deployment("ghost") is None
|
|
349
|
+
|
|
350
|
+
def test_destroy_deployment(
|
|
351
|
+
self, engine: TeamEngine, provider: MockProvider, blueprint: BlueprintManifest,
|
|
352
|
+
) -> None:
|
|
353
|
+
"""Destroy removes deployment state."""
|
|
354
|
+
dep = engine.deploy(blueprint)
|
|
355
|
+
result = engine.destroy_deployment(dep.deployment_id)
|
|
356
|
+
assert result is True
|
|
357
|
+
assert engine.get_deployment(dep.deployment_id) is None
|
|
358
|
+
|
|
359
|
+
def test_destroy_calls_provider(
|
|
360
|
+
self, engine: TeamEngine, provider: MockProvider, blueprint: BlueprintManifest,
|
|
361
|
+
) -> None:
|
|
362
|
+
"""Destroy calls provider.destroy on agents."""
|
|
363
|
+
dep = engine.deploy(blueprint)
|
|
364
|
+
provider.calls.clear()
|
|
365
|
+
engine.destroy_deployment(dep.deployment_id)
|
|
366
|
+
actions = [action for action, _ in provider.calls]
|
|
367
|
+
assert "destroy" in actions
|
|
368
|
+
|
|
369
|
+
def test_destroy_nonexistent(self, engine: TeamEngine) -> None:
|
|
370
|
+
"""Destroying nonexistent deployment returns False."""
|
|
371
|
+
assert engine.destroy_deployment("ghost") is False
|
|
372
|
+
|
|
373
|
+
def test_multiple_deployments(self, engine: TeamEngine) -> None:
|
|
374
|
+
"""Can have multiple active deployments."""
|
|
375
|
+
bp1 = _make_blueprint(name="team-alpha")
|
|
376
|
+
bp2 = _make_blueprint(name="team-bravo")
|
|
377
|
+
dep1 = engine.deploy(bp1)
|
|
378
|
+
dep2 = engine.deploy(bp2)
|
|
379
|
+
deps = engine.list_deployments()
|
|
380
|
+
assert len(deps) == 2
|
|
381
|
+
ids = {d.deployment_id for d in deps}
|
|
382
|
+
assert dep1.deployment_id in ids
|
|
383
|
+
assert dep2.deployment_id in ids
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# ---------------------------------------------------------------------------
|
|
387
|
+
# Provider backend
|
|
388
|
+
# ---------------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
class TestProviderBackend:
|
|
392
|
+
"""Tests for the abstract provider interface."""
|
|
393
|
+
|
|
394
|
+
def test_abstract_methods_raise(self) -> None:
|
|
395
|
+
"""ProviderBackend is an ABC — cannot be instantiated directly."""
|
|
396
|
+
with pytest.raises(TypeError):
|
|
397
|
+
ProviderBackend() # type: ignore[abstract]
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Tests for skcapstone.testrunner module.
|
|
2
|
+
|
|
3
|
+
Covers PackageResult, TestReport, _tail, _parse_pytest_summary,
|
|
4
|
+
and run_all_tests with mocked subprocess calls.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from unittest.mock import patch
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from skcapstone.testrunner import (
|
|
15
|
+
ECOSYSTEM_PACKAGES,
|
|
16
|
+
PackageResult,
|
|
17
|
+
TestReport,
|
|
18
|
+
_parse_pytest_summary,
|
|
19
|
+
_tail,
|
|
20
|
+
run_all_tests,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ── TestPackageResult ────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestPackageResult:
|
|
28
|
+
"""Tests for the PackageResult dataclass."""
|
|
29
|
+
|
|
30
|
+
def test_default_values(self):
|
|
31
|
+
"""Default numeric fields are zero, available is True."""
|
|
32
|
+
r = PackageResult(name="pkg")
|
|
33
|
+
assert r.passed == 0
|
|
34
|
+
assert r.failed == 0
|
|
35
|
+
assert r.errors == 0
|
|
36
|
+
assert r.skipped == 0
|
|
37
|
+
assert r.duration_s == 0.0
|
|
38
|
+
assert r.exit_code == -1
|
|
39
|
+
assert r.output == ""
|
|
40
|
+
assert r.available is True
|
|
41
|
+
|
|
42
|
+
def test_total_property(self):
|
|
43
|
+
"""total = passed + failed + errors."""
|
|
44
|
+
r = PackageResult(name="pkg", passed=3, failed=2, errors=1)
|
|
45
|
+
assert r.total == 6
|
|
46
|
+
|
|
47
|
+
def test_success_when_all_pass(self):
|
|
48
|
+
"""success is True when exit_code=0, failed=0, errors=0."""
|
|
49
|
+
r = PackageResult(name="pkg", passed=5, exit_code=0)
|
|
50
|
+
assert r.success is True
|
|
51
|
+
|
|
52
|
+
def test_success_false_when_failed_gt_zero(self):
|
|
53
|
+
"""success is False when failed > 0."""
|
|
54
|
+
r = PackageResult(name="pkg", passed=4, failed=1, exit_code=1)
|
|
55
|
+
assert r.success is False
|
|
56
|
+
|
|
57
|
+
def test_success_false_when_errors_gt_zero(self):
|
|
58
|
+
"""success is False when errors > 0 even if exit_code=0."""
|
|
59
|
+
r = PackageResult(name="pkg", passed=4, errors=1, exit_code=0)
|
|
60
|
+
assert r.success is False
|
|
61
|
+
|
|
62
|
+
def test_to_dict_serialization(self):
|
|
63
|
+
"""to_dict returns expected keys and computed values."""
|
|
64
|
+
r = PackageResult(
|
|
65
|
+
name="demo",
|
|
66
|
+
passed=10,
|
|
67
|
+
failed=2,
|
|
68
|
+
errors=1,
|
|
69
|
+
skipped=3,
|
|
70
|
+
duration_s=1.456,
|
|
71
|
+
exit_code=1,
|
|
72
|
+
available=True,
|
|
73
|
+
)
|
|
74
|
+
d = r.to_dict()
|
|
75
|
+
assert d["name"] == "demo"
|
|
76
|
+
assert d["passed"] == 10
|
|
77
|
+
assert d["failed"] == 2
|
|
78
|
+
assert d["errors"] == 1
|
|
79
|
+
assert d["skipped"] == 3
|
|
80
|
+
assert d["total"] == 13 # 10 + 2 + 1
|
|
81
|
+
assert d["duration_s"] == 1.46 # rounded to 2 decimals
|
|
82
|
+
assert d["success"] is False
|
|
83
|
+
assert d["available"] is True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ── TestTestReport ───────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TestTestReport:
|
|
90
|
+
"""Tests for the TestReport dataclass."""
|
|
91
|
+
|
|
92
|
+
def test_default_values(self):
|
|
93
|
+
"""Defaults to empty results and 0 duration."""
|
|
94
|
+
report = TestReport()
|
|
95
|
+
assert report.results == []
|
|
96
|
+
assert report.duration_s == 0.0
|
|
97
|
+
|
|
98
|
+
def test_total_passed_aggregation(self):
|
|
99
|
+
"""total_passed sums passed across all packages."""
|
|
100
|
+
report = TestReport(results=[
|
|
101
|
+
PackageResult(name="a", passed=3, exit_code=0),
|
|
102
|
+
PackageResult(name="b", passed=7, exit_code=0),
|
|
103
|
+
])
|
|
104
|
+
assert report.total_passed == 10
|
|
105
|
+
|
|
106
|
+
def test_total_failed_aggregation(self):
|
|
107
|
+
"""total_failed sums failed across all packages."""
|
|
108
|
+
report = TestReport(results=[
|
|
109
|
+
PackageResult(name="a", failed=1, exit_code=1),
|
|
110
|
+
PackageResult(name="b", failed=4, exit_code=1),
|
|
111
|
+
])
|
|
112
|
+
assert report.total_failed == 5
|
|
113
|
+
|
|
114
|
+
def test_all_passed_when_all_succeed(self):
|
|
115
|
+
"""all_passed is True when every available package succeeds."""
|
|
116
|
+
report = TestReport(results=[
|
|
117
|
+
PackageResult(name="a", passed=5, exit_code=0),
|
|
118
|
+
PackageResult(name="b", passed=3, exit_code=0),
|
|
119
|
+
])
|
|
120
|
+
assert report.all_passed is True
|
|
121
|
+
|
|
122
|
+
def test_all_passed_false_when_one_fails(self):
|
|
123
|
+
"""all_passed is False when at least one package fails."""
|
|
124
|
+
report = TestReport(results=[
|
|
125
|
+
PackageResult(name="a", passed=5, exit_code=0),
|
|
126
|
+
PackageResult(name="b", passed=2, failed=1, exit_code=1),
|
|
127
|
+
])
|
|
128
|
+
assert report.all_passed is False
|
|
129
|
+
|
|
130
|
+
def test_packages_tested_counts_only_available(self):
|
|
131
|
+
"""packages_tested counts only packages with available=True."""
|
|
132
|
+
report = TestReport(results=[
|
|
133
|
+
PackageResult(name="a", passed=5, exit_code=0, available=True),
|
|
134
|
+
PackageResult(name="b", available=False),
|
|
135
|
+
PackageResult(name="c", passed=2, exit_code=0, available=True),
|
|
136
|
+
])
|
|
137
|
+
assert report.packages_tested == 2
|
|
138
|
+
|
|
139
|
+
def test_to_dict_serialization(self):
|
|
140
|
+
"""to_dict returns all top-level keys and nested package dicts."""
|
|
141
|
+
report = TestReport(
|
|
142
|
+
results=[
|
|
143
|
+
PackageResult(name="x", passed=4, exit_code=0),
|
|
144
|
+
],
|
|
145
|
+
duration_s=2.789,
|
|
146
|
+
)
|
|
147
|
+
d = report.to_dict()
|
|
148
|
+
assert d["all_passed"] is True
|
|
149
|
+
assert d["total_passed"] == 4
|
|
150
|
+
assert d["total_failed"] == 0
|
|
151
|
+
assert d["total_errors"] == 0
|
|
152
|
+
assert d["packages_tested"] == 1
|
|
153
|
+
assert d["duration_s"] == 2.79
|
|
154
|
+
assert len(d["packages"]) == 1
|
|
155
|
+
assert d["packages"][0]["name"] == "x"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ── TestTail ─────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class TestTail:
|
|
162
|
+
"""Tests for the _tail helper."""
|
|
163
|
+
|
|
164
|
+
def test_basic_tail(self):
|
|
165
|
+
"""Returns the last N lines."""
|
|
166
|
+
text = "line1\nline2\nline3\nline4\nline5"
|
|
167
|
+
result = _tail(text, 3)
|
|
168
|
+
assert result == "line3\nline4\nline5"
|
|
169
|
+
|
|
170
|
+
def test_tail_more_than_available(self):
|
|
171
|
+
"""Returns all lines when N exceeds total."""
|
|
172
|
+
text = "a\nb"
|
|
173
|
+
result = _tail(text, 10)
|
|
174
|
+
assert result == "a\nb"
|
|
175
|
+
|
|
176
|
+
def test_empty_string(self):
|
|
177
|
+
"""Handles empty string without error."""
|
|
178
|
+
result = _tail("", 5)
|
|
179
|
+
assert result == ""
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ── TestParsePytestSummary ───────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class TestParsePytestSummary:
|
|
186
|
+
"""Tests for _parse_pytest_summary."""
|
|
187
|
+
|
|
188
|
+
def test_parses_passed_only(self):
|
|
189
|
+
"""Extracts passed count from a simple summary line."""
|
|
190
|
+
output = "===== 5 passed in 0.42s ====="
|
|
191
|
+
r = PackageResult(name="t")
|
|
192
|
+
_parse_pytest_summary(output, r)
|
|
193
|
+
assert r.passed == 5
|
|
194
|
+
assert r.failed == 0
|
|
195
|
+
|
|
196
|
+
def test_parses_passed_and_failed(self):
|
|
197
|
+
"""Extracts both passed and failed counts."""
|
|
198
|
+
output = "===== 3 passed, 1 failed in 1.2s ====="
|
|
199
|
+
r = PackageResult(name="t")
|
|
200
|
+
_parse_pytest_summary(output, r)
|
|
201
|
+
assert r.passed == 3
|
|
202
|
+
assert r.failed == 1
|
|
203
|
+
|
|
204
|
+
def test_parses_passed_and_error(self):
|
|
205
|
+
"""Extracts passed and error counts."""
|
|
206
|
+
output = "===== 2 passed, 1 error in 0.8s ====="
|
|
207
|
+
r = PackageResult(name="t")
|
|
208
|
+
_parse_pytest_summary(output, r)
|
|
209
|
+
assert r.passed == 2
|
|
210
|
+
assert r.errors == 1
|
|
211
|
+
|
|
212
|
+
def test_parses_passed_and_skipped(self):
|
|
213
|
+
"""Extracts passed and skipped counts."""
|
|
214
|
+
output = "===== 5 passed, 2 skipped in 0.5s ====="
|
|
215
|
+
r = PackageResult(name="t")
|
|
216
|
+
_parse_pytest_summary(output, r)
|
|
217
|
+
assert r.passed == 5
|
|
218
|
+
assert r.skipped == 2
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# ── TestRunAllTests ──────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class TestRunAllTests:
|
|
225
|
+
"""Tests for run_all_tests."""
|
|
226
|
+
|
|
227
|
+
def test_missing_test_dirs_marked_unavailable(self, tmp_path: Path):
|
|
228
|
+
"""Packages whose test dirs don't exist are marked unavailable."""
|
|
229
|
+
report = run_all_tests(tmp_path, packages=["skcapstone"])
|
|
230
|
+
pkg = report.results[0]
|
|
231
|
+
assert pkg.available is False
|
|
232
|
+
assert "not found" in pkg.output
|
|
233
|
+
|
|
234
|
+
def test_filter_by_package_names(self, tmp_path: Path):
|
|
235
|
+
"""Only requested packages appear in the report."""
|
|
236
|
+
report = run_all_tests(tmp_path, packages=["skcomm", "skchat"])
|
|
237
|
+
names = [r.name for r in report.results]
|
|
238
|
+
assert names == ["skcomm", "skchat"]
|