@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,441 @@
|
|
|
1
|
+
"""Tests for the sovereign agent export/import bundle system."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from skcapstone.export import (
|
|
12
|
+
BUNDLE_VERSION,
|
|
13
|
+
export_bundle,
|
|
14
|
+
import_bundle,
|
|
15
|
+
)
|
|
16
|
+
from skcapstone.memory_engine import list_memories, store as memory_store
|
|
17
|
+
from skcapstone.models import MemoryLayer
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Fixtures
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def agent_home(tmp_path: Path) -> Path:
|
|
27
|
+
"""Minimal agent home with required directory structure."""
|
|
28
|
+
home = tmp_path / ".skcapstone"
|
|
29
|
+
home.mkdir()
|
|
30
|
+
return home
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def populated_home(agent_home: Path) -> Path:
|
|
35
|
+
"""Agent home pre-populated with identity, config, soul, memories, conversations."""
|
|
36
|
+
# Identity
|
|
37
|
+
identity_dir = agent_home / "identity"
|
|
38
|
+
identity_dir.mkdir()
|
|
39
|
+
(identity_dir / "identity.json").write_text(
|
|
40
|
+
json.dumps({"name": "test-agent", "fingerprint": "DEADBEEF", "email": "test@example.com"}),
|
|
41
|
+
encoding="utf-8",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Config
|
|
45
|
+
config_dir = agent_home / "config"
|
|
46
|
+
config_dir.mkdir()
|
|
47
|
+
(config_dir / "config.yaml").write_text(
|
|
48
|
+
yaml.safe_dump({"agent_name": "test-agent", "auto_rehydrate": False}),
|
|
49
|
+
encoding="utf-8",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Soul
|
|
53
|
+
soul_dir = agent_home / "soul"
|
|
54
|
+
soul_dir.mkdir()
|
|
55
|
+
(soul_dir / "base.json").write_text(
|
|
56
|
+
json.dumps({"name": "base", "display_name": "Base Soul"}), encoding="utf-8"
|
|
57
|
+
)
|
|
58
|
+
installed_dir = soul_dir / "installed"
|
|
59
|
+
installed_dir.mkdir()
|
|
60
|
+
(installed_dir / "lumina.json").write_text(
|
|
61
|
+
json.dumps({"name": "lumina", "display_name": "Lumina", "vibe": "warm"}),
|
|
62
|
+
encoding="utf-8",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Memories
|
|
66
|
+
memory_store(agent_home, "Sovereign memory one", tags=["test"], importance=0.6)
|
|
67
|
+
memory_store(agent_home, "Sovereign memory two", tags=["core"], importance=0.9)
|
|
68
|
+
|
|
69
|
+
# Conversations
|
|
70
|
+
conv_dir = agent_home / "conversations"
|
|
71
|
+
conv_dir.mkdir()
|
|
72
|
+
(conv_dir / "peer-alice.json").write_text(
|
|
73
|
+
json.dumps([
|
|
74
|
+
{"role": "user", "content": "Hello", "timestamp": "2026-03-01T10:00:00+00:00"},
|
|
75
|
+
{"role": "assistant", "content": "Hi there!", "timestamp": "2026-03-01T10:00:01+00:00"},
|
|
76
|
+
]),
|
|
77
|
+
encoding="utf-8",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return agent_home
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# Test: export_bundle structure
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestExportBundle:
|
|
89
|
+
"""Tests for export_bundle()."""
|
|
90
|
+
|
|
91
|
+
def test_export_returns_dict_with_required_keys(self, populated_home: Path):
|
|
92
|
+
"""Exported bundle must contain all required top-level keys."""
|
|
93
|
+
bundle = export_bundle(populated_home)
|
|
94
|
+
|
|
95
|
+
assert isinstance(bundle, dict)
|
|
96
|
+
for key in ("bundle_version", "exported_at", "agent_name", "skcapstone_version",
|
|
97
|
+
"identity", "config", "soul", "memories", "conversations"):
|
|
98
|
+
assert key in bundle, f"Missing key: {key}"
|
|
99
|
+
|
|
100
|
+
def test_bundle_version_is_correct(self, agent_home: Path):
|
|
101
|
+
"""bundle_version must equal BUNDLE_VERSION constant."""
|
|
102
|
+
bundle = export_bundle(agent_home)
|
|
103
|
+
assert bundle["bundle_version"] == BUNDLE_VERSION
|
|
104
|
+
|
|
105
|
+
def test_export_includes_identity(self, populated_home: Path):
|
|
106
|
+
"""Identity section must include the agent fingerprint."""
|
|
107
|
+
bundle = export_bundle(populated_home)
|
|
108
|
+
assert bundle["identity"]["fingerprint"] == "DEADBEEF"
|
|
109
|
+
assert bundle["identity"]["name"] == "test-agent"
|
|
110
|
+
|
|
111
|
+
def test_export_includes_config(self, populated_home: Path):
|
|
112
|
+
"""Config section must include the agent name."""
|
|
113
|
+
bundle = export_bundle(populated_home)
|
|
114
|
+
assert bundle["config"]["agent_name"] == "test-agent"
|
|
115
|
+
|
|
116
|
+
def test_export_includes_soul(self, populated_home: Path):
|
|
117
|
+
"""Soul section must include base soul and installed overlays."""
|
|
118
|
+
bundle = export_bundle(populated_home)
|
|
119
|
+
soul = bundle["soul"]
|
|
120
|
+
assert soul["base"]["name"] == "base"
|
|
121
|
+
assert "lumina" in soul["installed"]
|
|
122
|
+
assert soul["installed"]["lumina"]["vibe"] == "warm"
|
|
123
|
+
|
|
124
|
+
def test_export_includes_memories(self, populated_home: Path):
|
|
125
|
+
"""Memories list must contain all stored memories."""
|
|
126
|
+
bundle = export_bundle(populated_home)
|
|
127
|
+
assert len(bundle["memories"]) == 2
|
|
128
|
+
contents = {m["content"] for m in bundle["memories"]}
|
|
129
|
+
assert "Sovereign memory one" in contents
|
|
130
|
+
assert "Sovereign memory two" in contents
|
|
131
|
+
|
|
132
|
+
def test_export_includes_conversations(self, populated_home: Path):
|
|
133
|
+
"""Conversations dict must include peer histories."""
|
|
134
|
+
bundle = export_bundle(populated_home)
|
|
135
|
+
assert "peer-alice" in bundle["conversations"]
|
|
136
|
+
msgs = bundle["conversations"]["peer-alice"]
|
|
137
|
+
assert len(msgs) == 2
|
|
138
|
+
assert msgs[0]["role"] == "user"
|
|
139
|
+
assert msgs[0]["content"] == "Hello"
|
|
140
|
+
|
|
141
|
+
def test_export_is_json_serializable(self, populated_home: Path):
|
|
142
|
+
"""The bundle must be fully JSON serializable without errors."""
|
|
143
|
+
bundle = export_bundle(populated_home)
|
|
144
|
+
serialized = json.dumps(bundle)
|
|
145
|
+
assert len(serialized) > 0
|
|
146
|
+
# Round-trip: must be parseable back
|
|
147
|
+
parsed = json.loads(serialized)
|
|
148
|
+
assert parsed["bundle_version"] == BUNDLE_VERSION
|
|
149
|
+
|
|
150
|
+
def test_export_empty_home(self, agent_home: Path):
|
|
151
|
+
"""Export from an empty agent home must succeed with empty sections."""
|
|
152
|
+
bundle = export_bundle(agent_home)
|
|
153
|
+
assert bundle["identity"] == {}
|
|
154
|
+
assert bundle["config"] == {}
|
|
155
|
+
assert bundle["memories"] == []
|
|
156
|
+
assert bundle["conversations"] == {}
|
|
157
|
+
|
|
158
|
+
def test_export_agent_name_from_identity(self, populated_home: Path):
|
|
159
|
+
"""Agent name is read from identity.json."""
|
|
160
|
+
bundle = export_bundle(populated_home)
|
|
161
|
+
assert bundle["agent_name"] == "test-agent"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
# Test: import_bundle memories
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class TestImportBundleMemories:
|
|
170
|
+
"""Tests for memory import in import_bundle()."""
|
|
171
|
+
|
|
172
|
+
def test_import_restores_memories(self, agent_home: Path, populated_home: Path):
|
|
173
|
+
"""Importing a bundle should restore all memories into target home."""
|
|
174
|
+
bundle = export_bundle(populated_home)
|
|
175
|
+
|
|
176
|
+
target = agent_home.parent / "target"
|
|
177
|
+
target.mkdir()
|
|
178
|
+
|
|
179
|
+
result = import_bundle(target, bundle)
|
|
180
|
+
assert result["memories_imported"] == 2
|
|
181
|
+
|
|
182
|
+
memories = list_memories(target)
|
|
183
|
+
contents = {m.content for m in memories}
|
|
184
|
+
assert "Sovereign memory one" in contents
|
|
185
|
+
assert "Sovereign memory two" in contents
|
|
186
|
+
|
|
187
|
+
def test_import_is_idempotent_for_memories(self, agent_home: Path, populated_home: Path):
|
|
188
|
+
"""Re-importing the same bundle should not duplicate memories."""
|
|
189
|
+
bundle = export_bundle(populated_home)
|
|
190
|
+
|
|
191
|
+
target = agent_home.parent / "target2"
|
|
192
|
+
target.mkdir()
|
|
193
|
+
|
|
194
|
+
first = import_bundle(target, bundle)
|
|
195
|
+
second = import_bundle(target, bundle)
|
|
196
|
+
|
|
197
|
+
assert first["memories_imported"] == 2
|
|
198
|
+
assert second["memories_imported"] == 0 # already present
|
|
199
|
+
|
|
200
|
+
memories = list_memories(target)
|
|
201
|
+
assert len(memories) == 2
|
|
202
|
+
|
|
203
|
+
def test_import_preserves_existing_memories(self, populated_home: Path):
|
|
204
|
+
"""Import should not overwrite memories already in the target."""
|
|
205
|
+
# Pre-store a memory in the target
|
|
206
|
+
pre_entry = memory_store(populated_home, "Pre-existing memory")
|
|
207
|
+
|
|
208
|
+
# Export from a second home
|
|
209
|
+
source = populated_home.parent / "source"
|
|
210
|
+
source.mkdir()
|
|
211
|
+
memory_store(source, "New from source", tags=["new"])
|
|
212
|
+
bundle = export_bundle(source)
|
|
213
|
+
|
|
214
|
+
result = import_bundle(populated_home, bundle)
|
|
215
|
+
assert result["memories_imported"] == 1
|
|
216
|
+
|
|
217
|
+
memories = list_memories(populated_home)
|
|
218
|
+
contents = {m.content for m in memories}
|
|
219
|
+
assert "Pre-existing memory" in contents
|
|
220
|
+
assert "New from source" in contents
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
# Test: import_bundle conversations
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class TestImportBundleConversations:
|
|
229
|
+
"""Tests for conversation import in import_bundle()."""
|
|
230
|
+
|
|
231
|
+
def test_import_restores_conversations(self, agent_home: Path, populated_home: Path):
|
|
232
|
+
"""Importing a bundle should restore conversation histories."""
|
|
233
|
+
bundle = export_bundle(populated_home)
|
|
234
|
+
|
|
235
|
+
target = agent_home.parent / "conv-target"
|
|
236
|
+
target.mkdir()
|
|
237
|
+
|
|
238
|
+
result = import_bundle(target, bundle)
|
|
239
|
+
assert result["conversations_imported"] == 2
|
|
240
|
+
|
|
241
|
+
conv_file = target / "conversations" / "peer-alice.json"
|
|
242
|
+
assert conv_file.exists()
|
|
243
|
+
messages = json.loads(conv_file.read_text())
|
|
244
|
+
assert len(messages) == 2
|
|
245
|
+
|
|
246
|
+
def test_import_merges_conversations(self, tmp_path: Path, populated_home: Path):
|
|
247
|
+
"""Import should merge new messages without duplicating existing ones."""
|
|
248
|
+
# Create a fresh target with one existing message for peer-alice
|
|
249
|
+
target = tmp_path / "merge-target"
|
|
250
|
+
target.mkdir()
|
|
251
|
+
conv_dir = target / "conversations"
|
|
252
|
+
conv_dir.mkdir()
|
|
253
|
+
existing = [{"role": "user", "content": "Hello", "timestamp": "2026-03-01T10:00:00+00:00"}]
|
|
254
|
+
(conv_dir / "peer-alice.json").write_text(json.dumps(existing), encoding="utf-8")
|
|
255
|
+
|
|
256
|
+
bundle = export_bundle(populated_home)
|
|
257
|
+
result = import_bundle(target, bundle)
|
|
258
|
+
|
|
259
|
+
# Should add only the "assistant" message (second msg), not re-add "Hello"
|
|
260
|
+
assert result["conversations_imported"] == 1
|
|
261
|
+
|
|
262
|
+
messages = json.loads((conv_dir / "peer-alice.json").read_text())
|
|
263
|
+
assert len(messages) == 2
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
# Test: import_bundle identity / config / soul
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class TestImportBundleFiles:
|
|
272
|
+
"""Tests for identity, config, and soul file import."""
|
|
273
|
+
|
|
274
|
+
def test_import_writes_identity_when_absent(self, tmp_path: Path, populated_home: Path):
|
|
275
|
+
"""Identity should be written when the target file does not exist."""
|
|
276
|
+
target = tmp_path / "fresh-agent"
|
|
277
|
+
target.mkdir()
|
|
278
|
+
bundle = export_bundle(populated_home)
|
|
279
|
+
result = import_bundle(target, bundle)
|
|
280
|
+
assert result["identity_written"] is True
|
|
281
|
+
identity = json.loads((target / "identity" / "identity.json").read_text())
|
|
282
|
+
assert identity["fingerprint"] == "DEADBEEF"
|
|
283
|
+
|
|
284
|
+
def test_import_skips_identity_when_present(self, populated_home: Path):
|
|
285
|
+
"""Identity must not be overwritten by default when already present."""
|
|
286
|
+
bundle = export_bundle(populated_home)
|
|
287
|
+
# Modify bundle identity
|
|
288
|
+
bundle["identity"]["fingerprint"] = "MODIFIED"
|
|
289
|
+
result = import_bundle(populated_home, bundle)
|
|
290
|
+
assert result["identity_written"] is False
|
|
291
|
+
# Original fingerprint is preserved
|
|
292
|
+
identity = json.loads((populated_home / "identity" / "identity.json").read_text())
|
|
293
|
+
assert identity["fingerprint"] == "DEADBEEF"
|
|
294
|
+
|
|
295
|
+
def test_import_overwrites_identity_with_flag(self, populated_home: Path):
|
|
296
|
+
"""--overwrite-identity should force-write identity."""
|
|
297
|
+
bundle = export_bundle(populated_home)
|
|
298
|
+
bundle["identity"]["fingerprint"] = "NEWPRINT"
|
|
299
|
+
result = import_bundle(populated_home, bundle, overwrite_identity=True)
|
|
300
|
+
assert result["identity_written"] is True
|
|
301
|
+
identity = json.loads((populated_home / "identity" / "identity.json").read_text())
|
|
302
|
+
assert identity["fingerprint"] == "NEWPRINT"
|
|
303
|
+
|
|
304
|
+
def test_import_writes_soul_files(self, tmp_path: Path, populated_home: Path):
|
|
305
|
+
"""Soul base and installed overlays should be written to a new home."""
|
|
306
|
+
target = tmp_path / "soul-target"
|
|
307
|
+
target.mkdir()
|
|
308
|
+
bundle = export_bundle(populated_home)
|
|
309
|
+
result = import_bundle(target, bundle)
|
|
310
|
+
assert result["soul_files_written"] >= 1
|
|
311
|
+
assert (target / "soul" / "base.json").exists()
|
|
312
|
+
assert (target / "soul" / "installed" / "lumina.json").exists()
|
|
313
|
+
|
|
314
|
+
def test_import_writes_config(self, tmp_path: Path, populated_home: Path):
|
|
315
|
+
"""Config should be written to a new home."""
|
|
316
|
+
target = tmp_path / "config-target"
|
|
317
|
+
target.mkdir()
|
|
318
|
+
bundle = export_bundle(populated_home)
|
|
319
|
+
result = import_bundle(target, bundle)
|
|
320
|
+
assert result["config_written"] is True
|
|
321
|
+
config = yaml.safe_load((target / "config" / "config.yaml").read_text())
|
|
322
|
+
assert config["agent_name"] == "test-agent"
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# ---------------------------------------------------------------------------
|
|
326
|
+
# Test: import_bundle validation
|
|
327
|
+
# ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class TestImportBundleValidation:
|
|
331
|
+
"""Tests for bundle validation in import_bundle()."""
|
|
332
|
+
|
|
333
|
+
def test_import_rejects_non_dict(self, agent_home: Path):
|
|
334
|
+
"""A non-dict bundle should raise ValueError."""
|
|
335
|
+
with pytest.raises(ValueError, match="JSON object"):
|
|
336
|
+
import_bundle(agent_home, []) # type: ignore[arg-type]
|
|
337
|
+
|
|
338
|
+
def test_import_rejects_missing_version(self, agent_home: Path):
|
|
339
|
+
"""A bundle with no bundle_version should raise ValueError."""
|
|
340
|
+
with pytest.raises(ValueError, match="bundle_version"):
|
|
341
|
+
import_bundle(agent_home, {"memories": []})
|
|
342
|
+
|
|
343
|
+
def test_import_rejects_wrong_version(self, agent_home: Path):
|
|
344
|
+
"""A bundle with a wrong version should raise ValueError."""
|
|
345
|
+
with pytest.raises(ValueError, match="Unsupported bundle_version"):
|
|
346
|
+
import_bundle(agent_home, {"bundle_version": 99, "memories": []})
|
|
347
|
+
|
|
348
|
+
def test_import_empty_bundle_succeeds(self, agent_home: Path):
|
|
349
|
+
"""A minimal valid bundle with no data should import without error."""
|
|
350
|
+
minimal = {"bundle_version": BUNDLE_VERSION}
|
|
351
|
+
result = import_bundle(agent_home, minimal)
|
|
352
|
+
assert result["memories_imported"] == 0
|
|
353
|
+
assert result["conversations_imported"] == 0
|
|
354
|
+
assert result["errors"] == []
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# ---------------------------------------------------------------------------
|
|
358
|
+
# Test: CLI via Click test runner
|
|
359
|
+
# ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class TestExportCLI:
|
|
363
|
+
"""Tests for skcapstone export/import via Click test runner."""
|
|
364
|
+
|
|
365
|
+
def test_export_command_stdout(self, populated_home: Path):
|
|
366
|
+
"""skcapstone export should write valid JSON to stdout."""
|
|
367
|
+
from click.testing import CliRunner
|
|
368
|
+
from skcapstone.cli import main
|
|
369
|
+
|
|
370
|
+
runner = CliRunner()
|
|
371
|
+
result = runner.invoke(main, ["--agent", "", "export", "--home", str(populated_home)])
|
|
372
|
+
assert result.exit_code == 0, result.output
|
|
373
|
+
bundle = json.loads(result.output)
|
|
374
|
+
assert bundle["bundle_version"] == BUNDLE_VERSION
|
|
375
|
+
assert len(bundle["memories"]) == 2
|
|
376
|
+
|
|
377
|
+
def test_export_command_to_file(self, populated_home: Path, tmp_path: Path):
|
|
378
|
+
"""skcapstone export --output should write a JSON file."""
|
|
379
|
+
from click.testing import CliRunner
|
|
380
|
+
from skcapstone.cli import main
|
|
381
|
+
|
|
382
|
+
out_file = tmp_path / "bundle.json"
|
|
383
|
+
runner = CliRunner()
|
|
384
|
+
result = runner.invoke(
|
|
385
|
+
main,
|
|
386
|
+
["--agent", "", "export", "--home", str(populated_home), "--output", str(out_file)],
|
|
387
|
+
)
|
|
388
|
+
assert result.exit_code == 0, result.output
|
|
389
|
+
assert out_file.exists()
|
|
390
|
+
bundle = json.loads(out_file.read_text())
|
|
391
|
+
assert bundle["bundle_version"] == BUNDLE_VERSION
|
|
392
|
+
|
|
393
|
+
def test_import_command(self, populated_home: Path, tmp_path: Path):
|
|
394
|
+
"""skcapstone import should restore memories from a bundle file."""
|
|
395
|
+
from click.testing import CliRunner
|
|
396
|
+
from skcapstone.cli import main
|
|
397
|
+
|
|
398
|
+
# Export first
|
|
399
|
+
out_file = tmp_path / "bundle.json"
|
|
400
|
+
runner = CliRunner()
|
|
401
|
+
runner.invoke(
|
|
402
|
+
main,
|
|
403
|
+
["--agent", "", "export", "--home", str(populated_home), "--output", str(out_file)],
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# Import into a fresh home
|
|
407
|
+
new_home = tmp_path / "new_agent"
|
|
408
|
+
new_home.mkdir()
|
|
409
|
+
result = runner.invoke(
|
|
410
|
+
main,
|
|
411
|
+
["--agent", "", "import", str(out_file), "--home", str(new_home)],
|
|
412
|
+
)
|
|
413
|
+
assert result.exit_code == 0, result.output
|
|
414
|
+
assert "Import complete" in result.output
|
|
415
|
+
|
|
416
|
+
memories = list_memories(new_home)
|
|
417
|
+
assert len(memories) == 2
|
|
418
|
+
|
|
419
|
+
def test_export_nonexistent_home_fails(self, tmp_path: Path):
|
|
420
|
+
"""export from a non-existent home should exit with error."""
|
|
421
|
+
from click.testing import CliRunner
|
|
422
|
+
from skcapstone.cli import main
|
|
423
|
+
|
|
424
|
+
runner = CliRunner()
|
|
425
|
+
result = runner.invoke(
|
|
426
|
+
main,
|
|
427
|
+
["--agent", "", "export", "--home", str(tmp_path / "does-not-exist")],
|
|
428
|
+
)
|
|
429
|
+
assert result.exit_code != 0
|
|
430
|
+
|
|
431
|
+
def test_import_nonexistent_bundle_fails(self, agent_home: Path):
|
|
432
|
+
"""import from a missing file should exit with error."""
|
|
433
|
+
from click.testing import CliRunner
|
|
434
|
+
from skcapstone.cli import main
|
|
435
|
+
|
|
436
|
+
runner = CliRunner()
|
|
437
|
+
result = runner.invoke(
|
|
438
|
+
main,
|
|
439
|
+
["--agent", "", "import", "/tmp/nonexistent_bundle.json", "--home", str(agent_home)],
|
|
440
|
+
)
|
|
441
|
+
assert result.exit_code != 0
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Tests for the FallbackTracker — graceful degradation logging."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from skcapstone.fallback_tracker import FallbackEvent, FallbackTracker, get_tracker
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _event(
|
|
14
|
+
primary="gpt-4o",
|
|
15
|
+
primary_backend="openai",
|
|
16
|
+
fallback_model="llama3.2",
|
|
17
|
+
fallback_backend="ollama",
|
|
18
|
+
reason="primary failed",
|
|
19
|
+
success=True,
|
|
20
|
+
) -> FallbackEvent:
|
|
21
|
+
return FallbackEvent(
|
|
22
|
+
primary_model=primary,
|
|
23
|
+
primary_backend=primary_backend,
|
|
24
|
+
fallback_model=fallback_model,
|
|
25
|
+
fallback_backend=fallback_backend,
|
|
26
|
+
reason=reason,
|
|
27
|
+
success=success,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# FallbackEvent — model tests
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TestFallbackEvent:
|
|
37
|
+
def test_defaults_set_timestamp(self):
|
|
38
|
+
"""Timestamp is populated automatically."""
|
|
39
|
+
evt = _event()
|
|
40
|
+
assert evt.timestamp # non-empty string
|
|
41
|
+
assert "T" in evt.timestamp # ISO format
|
|
42
|
+
|
|
43
|
+
def test_fields_round_trip(self):
|
|
44
|
+
"""model_dump() and re-instantiation preserve all fields."""
|
|
45
|
+
evt = _event(reason="timeout", success=False)
|
|
46
|
+
dumped = evt.model_dump()
|
|
47
|
+
restored = FallbackEvent(**dumped)
|
|
48
|
+
assert restored.reason == "timeout"
|
|
49
|
+
assert restored.success is False
|
|
50
|
+
assert restored.primary_model == "gpt-4o"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# FallbackTracker — happy path
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestFallbackTrackerHappyPath:
|
|
59
|
+
def test_record_and_load(self, tmp_path):
|
|
60
|
+
"""Record an event, then load it back."""
|
|
61
|
+
tracker = FallbackTracker(path=tmp_path / "fallbacks.json")
|
|
62
|
+
evt = _event()
|
|
63
|
+
tracker.record(evt)
|
|
64
|
+
|
|
65
|
+
loaded = tracker.load_events()
|
|
66
|
+
assert len(loaded) == 1
|
|
67
|
+
assert loaded[0].primary_model == "gpt-4o"
|
|
68
|
+
assert loaded[0].fallback_backend == "ollama"
|
|
69
|
+
|
|
70
|
+
def test_multiple_events_newest_first(self, tmp_path):
|
|
71
|
+
"""load_events returns events newest-first."""
|
|
72
|
+
tracker = FallbackTracker(path=tmp_path / "fallbacks.json")
|
|
73
|
+
tracker.record(_event(reason="first"))
|
|
74
|
+
tracker.record(_event(reason="second"))
|
|
75
|
+
tracker.record(_event(reason="third"))
|
|
76
|
+
|
|
77
|
+
loaded = tracker.load_events()
|
|
78
|
+
assert loaded[0].reason == "third"
|
|
79
|
+
assert loaded[1].reason == "second"
|
|
80
|
+
assert loaded[2].reason == "first"
|
|
81
|
+
|
|
82
|
+
def test_limit_parameter(self, tmp_path):
|
|
83
|
+
"""limit= caps the returned events."""
|
|
84
|
+
tracker = FallbackTracker(path=tmp_path / "fallbacks.json")
|
|
85
|
+
for i in range(5):
|
|
86
|
+
tracker.record(_event(reason=f"event-{i}"))
|
|
87
|
+
|
|
88
|
+
assert len(tracker.load_events(limit=2)) == 2
|
|
89
|
+
assert len(tracker.load_events(limit=0)) == 5
|
|
90
|
+
|
|
91
|
+
def test_file_is_valid_json(self, tmp_path):
|
|
92
|
+
"""The written file is valid JSON list."""
|
|
93
|
+
path = tmp_path / "fallbacks.json"
|
|
94
|
+
tracker = FallbackTracker(path=path)
|
|
95
|
+
tracker.record(_event())
|
|
96
|
+
|
|
97
|
+
data = json.loads(path.read_text())
|
|
98
|
+
assert isinstance(data, list)
|
|
99
|
+
assert len(data) == 1
|
|
100
|
+
assert data[0]["primary_model"] == "gpt-4o"
|
|
101
|
+
|
|
102
|
+
def test_success_and_failure_events(self, tmp_path):
|
|
103
|
+
"""success=True and success=False events are both stored."""
|
|
104
|
+
tracker = FallbackTracker(path=tmp_path / "fallbacks.json")
|
|
105
|
+
tracker.record(_event(success=True, reason="worked"))
|
|
106
|
+
tracker.record(_event(success=False, reason="failed"))
|
|
107
|
+
|
|
108
|
+
events = tracker.load_events()
|
|
109
|
+
successes = [e for e in events if e.success]
|
|
110
|
+
failures = [e for e in events if not e.success]
|
|
111
|
+
assert len(successes) == 1
|
|
112
|
+
assert len(failures) == 1
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
# FallbackTracker — edge cases
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TestFallbackTrackerEdgeCases:
|
|
121
|
+
def test_missing_file_returns_empty(self, tmp_path):
|
|
122
|
+
"""load_events on a non-existent file returns []."""
|
|
123
|
+
tracker = FallbackTracker(path=tmp_path / "nonexistent.json")
|
|
124
|
+
assert tracker.load_events() == []
|
|
125
|
+
|
|
126
|
+
def test_corrupt_file_returns_empty(self, tmp_path):
|
|
127
|
+
"""A corrupt JSON file is treated as empty (no exception raised)."""
|
|
128
|
+
path = tmp_path / "fallbacks.json"
|
|
129
|
+
path.write_text("not valid json!!!", encoding="utf-8")
|
|
130
|
+
|
|
131
|
+
tracker = FallbackTracker(path=path)
|
|
132
|
+
assert tracker.load_events() == []
|
|
133
|
+
|
|
134
|
+
def test_max_events_pruning(self, tmp_path):
|
|
135
|
+
"""Old events are pruned when max_events is exceeded."""
|
|
136
|
+
tracker = FallbackTracker(path=tmp_path / "fallbacks.json", max_events=3)
|
|
137
|
+
for i in range(5):
|
|
138
|
+
tracker.record(_event(reason=f"e{i}"))
|
|
139
|
+
|
|
140
|
+
events = tracker.load_events()
|
|
141
|
+
assert len(events) == 3
|
|
142
|
+
# Newest three should be retained (newest-first order)
|
|
143
|
+
reasons = [e.reason for e in events]
|
|
144
|
+
assert "e4" in reasons
|
|
145
|
+
assert "e3" in reasons
|
|
146
|
+
assert "e2" in reasons
|
|
147
|
+
assert "e0" not in reasons
|
|
148
|
+
|
|
149
|
+
def test_clear_removes_all_events(self, tmp_path):
|
|
150
|
+
"""clear() deletes all events and returns count."""
|
|
151
|
+
tracker = FallbackTracker(path=tmp_path / "fallbacks.json")
|
|
152
|
+
for i in range(4):
|
|
153
|
+
tracker.record(_event(reason=f"e{i}"))
|
|
154
|
+
|
|
155
|
+
count = tracker.clear()
|
|
156
|
+
assert count == 4
|
|
157
|
+
assert tracker.load_events() == []
|
|
158
|
+
|
|
159
|
+
def test_clear_on_empty_returns_zero(self, tmp_path):
|
|
160
|
+
"""clear() on an empty store returns 0."""
|
|
161
|
+
tracker = FallbackTracker(path=tmp_path / "fallbacks.json")
|
|
162
|
+
assert tracker.clear() == 0
|
|
163
|
+
|
|
164
|
+
def test_parent_dir_created_automatically(self, tmp_path):
|
|
165
|
+
"""Missing parent directories are created on first write."""
|
|
166
|
+
nested = tmp_path / "a" / "b" / "c" / "fallbacks.json"
|
|
167
|
+
tracker = FallbackTracker(path=nested)
|
|
168
|
+
tracker.record(_event())
|
|
169
|
+
assert nested.exists()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
# FallbackTracker — thread safety
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class TestFallbackTrackerConcurrency:
|
|
178
|
+
def test_concurrent_writes(self, tmp_path):
|
|
179
|
+
"""Concurrent record() calls from multiple threads don't corrupt the file."""
|
|
180
|
+
import threading
|
|
181
|
+
|
|
182
|
+
tracker = FallbackTracker(path=tmp_path / "fallbacks.json")
|
|
183
|
+
errors: list[Exception] = []
|
|
184
|
+
|
|
185
|
+
def write_events():
|
|
186
|
+
try:
|
|
187
|
+
for i in range(10):
|
|
188
|
+
tracker.record(_event(reason=f"thread-{i}"))
|
|
189
|
+
except Exception as exc: # noqa: BLE001
|
|
190
|
+
errors.append(exc)
|
|
191
|
+
|
|
192
|
+
threads = [threading.Thread(target=write_events) for _ in range(4)]
|
|
193
|
+
for t in threads:
|
|
194
|
+
t.start()
|
|
195
|
+
for t in threads:
|
|
196
|
+
t.join()
|
|
197
|
+
|
|
198
|
+
assert not errors, f"Thread errors: {errors}"
|
|
199
|
+
events = tracker.load_events()
|
|
200
|
+
# max_events default is 1000, 4*10=40 events — all should be present
|
|
201
|
+
assert len(events) == 40
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
# get_tracker singleton
|
|
206
|
+
# ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class TestGetTrackerSingleton:
|
|
210
|
+
def test_same_instance_returned(self):
|
|
211
|
+
"""get_tracker() returns the same object on repeated calls."""
|
|
212
|
+
t1 = get_tracker()
|
|
213
|
+
t2 = get_tracker()
|
|
214
|
+
assert t1 is t2
|
|
215
|
+
|
|
216
|
+
def test_singleton_is_fallback_tracker(self):
|
|
217
|
+
"""Singleton is a FallbackTracker instance."""
|
|
218
|
+
tracker = get_tracker()
|
|
219
|
+
assert isinstance(tracker, FallbackTracker)
|