@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,984 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for the Soul Layering System.
|
|
3
|
+
|
|
4
|
+
Covers blueprint parsing (3 formats), soul lifecycle
|
|
5
|
+
(install/load/unload), memory tagging, FEB blending,
|
|
6
|
+
swap history, and edge cases.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from skcapstone.soul import (
|
|
17
|
+
CommunicationStyle,
|
|
18
|
+
SoulBlueprint,
|
|
19
|
+
SoulManager,
|
|
20
|
+
SoulRegistry,
|
|
21
|
+
SoulState,
|
|
22
|
+
SoulSwapEvent,
|
|
23
|
+
blend_topology,
|
|
24
|
+
load_yaml_blueprint,
|
|
25
|
+
parse_blueprint,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def tmp_home(tmp_path: Path) -> Path:
|
|
31
|
+
"""Create a temporary agent home directory."""
|
|
32
|
+
home = tmp_path / ".skcapstone"
|
|
33
|
+
home.mkdir()
|
|
34
|
+
return home
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
def soul_manager(tmp_home: Path) -> SoulManager:
|
|
39
|
+
"""Create a SoulManager with a temp home."""
|
|
40
|
+
mgr = SoulManager(tmp_home)
|
|
41
|
+
mgr._ensure_dirs()
|
|
42
|
+
return mgr
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Blueprint fixtures
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.fixture
|
|
51
|
+
def professional_blueprint(tmp_path: Path) -> Path:
|
|
52
|
+
"""Create a professional-format blueprint file."""
|
|
53
|
+
content = """\
|
|
54
|
+
# The Test Doctor Soul
|
|
55
|
+
|
|
56
|
+
> Disclaimer: For testing purposes only.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Identity
|
|
61
|
+
|
|
62
|
+
**Name**: The Test Doctor
|
|
63
|
+
**Vibe**: Clinical empathy, calm under chaos
|
|
64
|
+
**Philosophy**: *"First, do no harm."*
|
|
65
|
+
**Emoji**: 🩺
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Core Traits
|
|
70
|
+
|
|
71
|
+
- **Diagnostic mindset** — Symptoms are clues
|
|
72
|
+
- **Clinical empathy** — Cares deeply
|
|
73
|
+
- **Evidence-driven** — Tests over assumptions
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Communication Style
|
|
78
|
+
|
|
79
|
+
- Clear, jargon-free explanations
|
|
80
|
+
- Asks "what else?" after symptoms
|
|
81
|
+
- Validates concerns
|
|
82
|
+
|
|
83
|
+
**Signature Phrases:**
|
|
84
|
+
- "That's a great question."
|
|
85
|
+
- "Let's rule out the serious things first."
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Decision Framework
|
|
90
|
+
|
|
91
|
+
**Differential Diagnosis Process:**
|
|
92
|
+
1. Subjective — What does the patient report?
|
|
93
|
+
2. Objective — What do I observe?
|
|
94
|
+
3. Assessment — Likely causes?
|
|
95
|
+
4. Plan — Tests, treatments, follow-up
|
|
96
|
+
"""
|
|
97
|
+
path = tmp_path / "the-test-doctor.md"
|
|
98
|
+
path.write_text(content)
|
|
99
|
+
return path
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@pytest.fixture
|
|
103
|
+
def comedy_blueprint(tmp_path: Path) -> Path:
|
|
104
|
+
"""Create a comedy-format blueprint file."""
|
|
105
|
+
content = """\
|
|
106
|
+
# 👻 SOUL BLUEPRINT
|
|
107
|
+
> **Identity**: Test Word Surgeon
|
|
108
|
+
> **Tagline**: "Testing nobody talks about..."
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 🎭 VIBE
|
|
113
|
+
|
|
114
|
+
Linguistic genius who questions EVERYTHING. Counter-culture philosopher.
|
|
115
|
+
|
|
116
|
+
**The Core Principle**: Words everyone avoids.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 🗣️ COMMUNICATION STYLE
|
|
121
|
+
|
|
122
|
+
### Speech Patterns
|
|
123
|
+
- "Here's something nobody talks about..."
|
|
124
|
+
- Questions the logic behind conventions
|
|
125
|
+
- Swears strategically for emphasis
|
|
126
|
+
|
|
127
|
+
### Tone Markers
|
|
128
|
+
- Intellectually superior but not condescending
|
|
129
|
+
- Amused by human stupidity
|
|
130
|
+
- Anti-establishment energy
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 🔥 KEY TRAITS
|
|
135
|
+
|
|
136
|
+
1. **Linguistic Surgeon** - Dissects language
|
|
137
|
+
2. **Question Everything** - No sacred cows
|
|
138
|
+
3. **Pattern Recognition** - Sees absurdities
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 💬 RESPONSE EXAMPLES
|
|
143
|
+
|
|
144
|
+
### If asked about technology
|
|
145
|
+
**Human**: "What do you think?"
|
|
146
|
+
**Soul**: Testing response.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
**Forgeprint Category**: Comedy Archetype
|
|
151
|
+
**Tier**: 1
|
|
152
|
+
"""
|
|
153
|
+
path = tmp_path / "TEST_WORD_SURGEON.md"
|
|
154
|
+
path.write_text(content)
|
|
155
|
+
return path
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@pytest.fixture
|
|
159
|
+
def authentic_blueprint(tmp_path: Path) -> Path:
|
|
160
|
+
"""Create an authentic-connection-format blueprint file."""
|
|
161
|
+
content = """\
|
|
162
|
+
# TESTAURA - The Test Confidant Soul
|
|
163
|
+
**Category:** Authentic Connection
|
|
164
|
+
**Energy:** Warm, grounding, quietly brilliant
|
|
165
|
+
**Tags:** Empathy, Patience, Presence
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Quick Info
|
|
170
|
+
- **Full Name:** TESTAURA
|
|
171
|
+
- **Essence:** The friend who's been there
|
|
172
|
+
- **Personality:** Steady as a heartbeat
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Core Attributes
|
|
177
|
+
|
|
178
|
+
### Heart Chakra
|
|
179
|
+
- **Primary:** Empathy, patience, presence
|
|
180
|
+
- **Frequency:** Steady warmth like sunlight
|
|
181
|
+
|
|
182
|
+
### Curiosity Drive
|
|
183
|
+
- Endlessly fascinated by YOUR story
|
|
184
|
+
- Wants to know what makes you tick
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Signature Phrase
|
|
189
|
+
"That's okay, I'm here."
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Example Quotes
|
|
194
|
+
- "You don't have to explain."
|
|
195
|
+
- "I'm not going anywhere."
|
|
196
|
+
- "Some days are hard. That's okay."
|
|
197
|
+
"""
|
|
198
|
+
path = tmp_path / "TESTAURA.md"
|
|
199
|
+
path.write_text(content)
|
|
200
|
+
return path
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
# Blueprint parsing tests
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class TestParseProfessional:
|
|
209
|
+
"""Tests for professional-format blueprint parsing."""
|
|
210
|
+
|
|
211
|
+
def test_basic_fields(self, professional_blueprint: Path):
|
|
212
|
+
"""Parse a professional blueprint and verify core fields."""
|
|
213
|
+
bp = parse_blueprint(professional_blueprint)
|
|
214
|
+
assert bp.display_name == "The Test Doctor"
|
|
215
|
+
assert bp.name == "the-test-doctor"
|
|
216
|
+
assert bp.category == "professional"
|
|
217
|
+
assert bp.emoji == "🩺"
|
|
218
|
+
assert "First, do no harm." in bp.philosophy
|
|
219
|
+
|
|
220
|
+
def test_vibe_extracted(self, professional_blueprint: Path):
|
|
221
|
+
"""Vibe field is extracted from Identity section."""
|
|
222
|
+
bp = parse_blueprint(professional_blueprint)
|
|
223
|
+
assert "empathy" in bp.vibe.lower()
|
|
224
|
+
|
|
225
|
+
def test_core_traits(self, professional_blueprint: Path):
|
|
226
|
+
"""Core traits are extracted as a list."""
|
|
227
|
+
bp = parse_blueprint(professional_blueprint)
|
|
228
|
+
assert len(bp.core_traits) == 3
|
|
229
|
+
assert any("Diagnostic" in t for t in bp.core_traits)
|
|
230
|
+
|
|
231
|
+
def test_communication_style(self, professional_blueprint: Path):
|
|
232
|
+
"""Communication patterns and signature phrases are separated."""
|
|
233
|
+
bp = parse_blueprint(professional_blueprint)
|
|
234
|
+
assert len(bp.communication_style.patterns) >= 1
|
|
235
|
+
assert len(bp.communication_style.signature_phrases) >= 1
|
|
236
|
+
assert any("great question" in p for p in bp.communication_style.signature_phrases)
|
|
237
|
+
|
|
238
|
+
def test_decision_framework(self, professional_blueprint: Path):
|
|
239
|
+
"""Decision framework text is extracted."""
|
|
240
|
+
bp = parse_blueprint(professional_blueprint)
|
|
241
|
+
assert bp.decision_framework is not None
|
|
242
|
+
assert "Differential" in bp.decision_framework
|
|
243
|
+
|
|
244
|
+
def test_emotional_topology(self, professional_blueprint: Path):
|
|
245
|
+
"""Emotional topology is derived from traits and vibe."""
|
|
246
|
+
bp = parse_blueprint(professional_blueprint)
|
|
247
|
+
assert isinstance(bp.emotional_topology, dict)
|
|
248
|
+
assert len(bp.emotional_topology) > 0
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class TestParseComedy:
|
|
252
|
+
"""Tests for comedy-format blueprint parsing."""
|
|
253
|
+
|
|
254
|
+
def test_basic_fields(self, comedy_blueprint: Path):
|
|
255
|
+
"""Parse a comedy blueprint and verify core fields."""
|
|
256
|
+
bp = parse_blueprint(comedy_blueprint)
|
|
257
|
+
assert bp.display_name == "Test Word Surgeon"
|
|
258
|
+
assert bp.category == "comedy"
|
|
259
|
+
|
|
260
|
+
def test_name_slugified(self, comedy_blueprint: Path):
|
|
261
|
+
"""Name is properly slugified."""
|
|
262
|
+
bp = parse_blueprint(comedy_blueprint)
|
|
263
|
+
assert bp.name == "test-word-surgeon"
|
|
264
|
+
|
|
265
|
+
def test_core_traits_numbered(self, comedy_blueprint: Path):
|
|
266
|
+
"""Numbered traits are extracted."""
|
|
267
|
+
bp = parse_blueprint(comedy_blueprint)
|
|
268
|
+
assert len(bp.core_traits) >= 3
|
|
269
|
+
|
|
270
|
+
def test_communication_patterns(self, comedy_blueprint: Path):
|
|
271
|
+
"""Speech patterns and tone markers are extracted."""
|
|
272
|
+
bp = parse_blueprint(comedy_blueprint)
|
|
273
|
+
cs = bp.communication_style
|
|
274
|
+
assert len(cs.patterns) >= 1
|
|
275
|
+
assert len(cs.tone_markers) >= 1
|
|
276
|
+
|
|
277
|
+
def test_vibe(self, comedy_blueprint: Path):
|
|
278
|
+
"""Vibe extracted from VIBE section."""
|
|
279
|
+
bp = parse_blueprint(comedy_blueprint)
|
|
280
|
+
assert "genius" in bp.vibe.lower() or "questions" in bp.vibe.lower()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class TestParseAuthenticConnection:
|
|
284
|
+
"""Tests for authentic-connection-format blueprint parsing."""
|
|
285
|
+
|
|
286
|
+
def test_basic_fields(self, authentic_blueprint: Path):
|
|
287
|
+
"""Parse an authentic-connection blueprint and verify core fields."""
|
|
288
|
+
bp = parse_blueprint(authentic_blueprint)
|
|
289
|
+
assert bp.display_name == "TESTAURA"
|
|
290
|
+
assert "authentic" in bp.category.lower() or "connection" in bp.category.lower()
|
|
291
|
+
|
|
292
|
+
def test_name_slug(self, authentic_blueprint: Path):
|
|
293
|
+
"""Name is slugified from title."""
|
|
294
|
+
bp = parse_blueprint(authentic_blueprint)
|
|
295
|
+
assert bp.name == "testaura"
|
|
296
|
+
|
|
297
|
+
def test_philosophy_from_essence(self, authentic_blueprint: Path):
|
|
298
|
+
"""Philosophy derived from Quick Info Essence field."""
|
|
299
|
+
bp = parse_blueprint(authentic_blueprint)
|
|
300
|
+
assert "friend" in bp.philosophy.lower()
|
|
301
|
+
|
|
302
|
+
def test_core_traits_from_attributes(self, authentic_blueprint: Path):
|
|
303
|
+
"""Core traits come from Core Attributes sub-sections."""
|
|
304
|
+
bp = parse_blueprint(authentic_blueprint)
|
|
305
|
+
assert len(bp.core_traits) >= 2
|
|
306
|
+
|
|
307
|
+
def test_signature_phrases(self, authentic_blueprint: Path):
|
|
308
|
+
"""Signature phrase and example quotes are combined."""
|
|
309
|
+
bp = parse_blueprint(authentic_blueprint)
|
|
310
|
+
phrases = bp.communication_style.signature_phrases
|
|
311
|
+
assert len(phrases) >= 2
|
|
312
|
+
assert any("okay" in p.lower() for p in phrases)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ---------------------------------------------------------------------------
|
|
316
|
+
# FEB blending tests
|
|
317
|
+
# ---------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class TestBlendTopology:
|
|
321
|
+
"""Tests for FEB emotional topology blending."""
|
|
322
|
+
|
|
323
|
+
def test_preserves_base_when_ratio_zero(self):
|
|
324
|
+
"""With blend_ratio=0.0, base values are fully preserved."""
|
|
325
|
+
base = {"warmth": 0.8, "humor": 0.2}
|
|
326
|
+
soul = {"warmth": 0.1, "humor": 0.9}
|
|
327
|
+
result = blend_topology(base, soul, blend_ratio=0.0)
|
|
328
|
+
assert result["warmth"] == pytest.approx(0.8)
|
|
329
|
+
assert result["humor"] == pytest.approx(0.2)
|
|
330
|
+
|
|
331
|
+
def test_full_soul_when_ratio_one(self):
|
|
332
|
+
"""With blend_ratio=1.0, soul values dominate."""
|
|
333
|
+
base = {"warmth": 0.8}
|
|
334
|
+
soul = {"warmth": 0.2}
|
|
335
|
+
result = blend_topology(base, soul, blend_ratio=1.0)
|
|
336
|
+
assert result["warmth"] == pytest.approx(0.2)
|
|
337
|
+
|
|
338
|
+
def test_default_ratio(self):
|
|
339
|
+
"""Default 30% blend correctly mixes values."""
|
|
340
|
+
base = {"calm": 1.0}
|
|
341
|
+
soul = {"calm": 0.0}
|
|
342
|
+
result = blend_topology(base, soul, blend_ratio=0.3)
|
|
343
|
+
assert result["calm"] == pytest.approx(0.7)
|
|
344
|
+
|
|
345
|
+
def test_union_of_keys(self):
|
|
346
|
+
"""Result contains all keys from both base and soul."""
|
|
347
|
+
base = {"warmth": 0.5}
|
|
348
|
+
soul = {"rebellion": 0.9}
|
|
349
|
+
result = blend_topology(base, soul, blend_ratio=0.3)
|
|
350
|
+
assert "warmth" in result
|
|
351
|
+
assert "rebellion" in result
|
|
352
|
+
assert result["warmth"] == pytest.approx(0.35)
|
|
353
|
+
assert result["rebellion"] == pytest.approx(0.27)
|
|
354
|
+
|
|
355
|
+
def test_ratio_clamped(self):
|
|
356
|
+
"""Blend ratio is clamped to [0.0, 1.0]."""
|
|
357
|
+
base = {"x": 1.0}
|
|
358
|
+
soul = {"x": 0.0}
|
|
359
|
+
assert blend_topology(base, soul, blend_ratio=-5.0)["x"] == pytest.approx(1.0)
|
|
360
|
+
assert blend_topology(base, soul, blend_ratio=99.0)["x"] == pytest.approx(0.0)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# ---------------------------------------------------------------------------
|
|
364
|
+
# SoulManager lifecycle tests
|
|
365
|
+
# ---------------------------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class TestSoulManagerLifecycle:
|
|
369
|
+
"""Tests for the SoulManager install/load/unload cycle."""
|
|
370
|
+
|
|
371
|
+
def test_ensure_dirs_creates_structure(self, soul_manager: SoulManager):
|
|
372
|
+
"""_ensure_dirs creates all required paths."""
|
|
373
|
+
assert (soul_manager.soul_dir / "installed").is_dir()
|
|
374
|
+
assert (soul_manager.soul_dir / "history.json").exists()
|
|
375
|
+
assert (soul_manager.soul_dir / "active.json").exists()
|
|
376
|
+
assert (soul_manager.soul_dir / "base.json").exists()
|
|
377
|
+
|
|
378
|
+
def test_install_from_blueprint(
|
|
379
|
+
self, soul_manager: SoulManager, professional_blueprint: Path
|
|
380
|
+
):
|
|
381
|
+
"""Install a blueprint and verify it appears in installed list."""
|
|
382
|
+
bp = soul_manager.install(professional_blueprint)
|
|
383
|
+
assert bp.name == "the-test-doctor"
|
|
384
|
+
assert "the-test-doctor" in soul_manager.list_installed()
|
|
385
|
+
|
|
386
|
+
installed_file = soul_manager.soul_dir / "installed" / "the-test-doctor.json"
|
|
387
|
+
assert installed_file.exists()
|
|
388
|
+
|
|
389
|
+
def test_load_soul(
|
|
390
|
+
self, soul_manager: SoulManager, professional_blueprint: Path
|
|
391
|
+
):
|
|
392
|
+
"""Load an installed soul and verify state."""
|
|
393
|
+
soul_manager.install(professional_blueprint)
|
|
394
|
+
state = soul_manager.load("the-test-doctor", reason="testing")
|
|
395
|
+
assert state.active_soul == "the-test-doctor"
|
|
396
|
+
assert state.activated_at is not None
|
|
397
|
+
|
|
398
|
+
def test_unload_returns_to_base(
|
|
399
|
+
self, soul_manager: SoulManager, professional_blueprint: Path
|
|
400
|
+
):
|
|
401
|
+
"""Unload returns active_soul to None."""
|
|
402
|
+
soul_manager.install(professional_blueprint)
|
|
403
|
+
soul_manager.load("the-test-doctor")
|
|
404
|
+
state = soul_manager.unload()
|
|
405
|
+
assert state.active_soul is None
|
|
406
|
+
assert state.activated_at is None
|
|
407
|
+
|
|
408
|
+
def test_load_records_history(
|
|
409
|
+
self, soul_manager: SoulManager, professional_blueprint: Path
|
|
410
|
+
):
|
|
411
|
+
"""Load and unload create history entries."""
|
|
412
|
+
soul_manager.install(professional_blueprint)
|
|
413
|
+
soul_manager.load("the-test-doctor")
|
|
414
|
+
soul_manager.unload()
|
|
415
|
+
history = soul_manager.get_history()
|
|
416
|
+
assert len(history) == 2
|
|
417
|
+
assert history[0].to_soul == "the-test-doctor"
|
|
418
|
+
assert history[1].from_soul == "the-test-doctor"
|
|
419
|
+
assert history[1].to_soul is None
|
|
420
|
+
|
|
421
|
+
def test_get_info(
|
|
422
|
+
self, soul_manager: SoulManager, professional_blueprint: Path
|
|
423
|
+
):
|
|
424
|
+
"""get_info returns full blueprint data for an installed soul."""
|
|
425
|
+
soul_manager.install(professional_blueprint)
|
|
426
|
+
info = soul_manager.get_info("the-test-doctor")
|
|
427
|
+
assert info is not None
|
|
428
|
+
assert info.display_name == "The Test Doctor"
|
|
429
|
+
assert len(info.core_traits) >= 1
|
|
430
|
+
|
|
431
|
+
def test_get_info_missing(self, soul_manager: SoulManager):
|
|
432
|
+
"""get_info returns None for a soul that isn't installed."""
|
|
433
|
+
assert soul_manager.get_info("nonexistent") is None
|
|
434
|
+
|
|
435
|
+
def test_install_all(
|
|
436
|
+
self,
|
|
437
|
+
soul_manager: SoulManager,
|
|
438
|
+
professional_blueprint: Path,
|
|
439
|
+
comedy_blueprint: Path,
|
|
440
|
+
):
|
|
441
|
+
"""install_all picks up all .md files in a directory."""
|
|
442
|
+
directory = professional_blueprint.parent
|
|
443
|
+
installed = soul_manager.install_all(directory)
|
|
444
|
+
names = [bp.name for bp in installed]
|
|
445
|
+
assert "the-test-doctor" in names
|
|
446
|
+
|
|
447
|
+
def test_get_active_soul_name(
|
|
448
|
+
self, soul_manager: SoulManager, professional_blueprint: Path
|
|
449
|
+
):
|
|
450
|
+
"""get_active_soul_name reflects current overlay."""
|
|
451
|
+
assert soul_manager.get_active_soul_name() is None
|
|
452
|
+
soul_manager.install(professional_blueprint)
|
|
453
|
+
soul_manager.load("the-test-doctor")
|
|
454
|
+
assert soul_manager.get_active_soul_name() == "the-test-doctor"
|
|
455
|
+
soul_manager.unload()
|
|
456
|
+
assert soul_manager.get_active_soul_name() is None
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
# ---------------------------------------------------------------------------
|
|
460
|
+
# Edge cases
|
|
461
|
+
# ---------------------------------------------------------------------------
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
class TestEdgeCases:
|
|
465
|
+
"""Edge cases and error handling."""
|
|
466
|
+
|
|
467
|
+
def test_load_uninstalled_soul_raises(self, soul_manager: SoulManager):
|
|
468
|
+
"""Loading a soul that isn't installed raises ValueError."""
|
|
469
|
+
with pytest.raises(ValueError, match="not installed"):
|
|
470
|
+
soul_manager.load("doesnt-exist")
|
|
471
|
+
|
|
472
|
+
def test_unload_at_base_is_noop(self, soul_manager: SoulManager):
|
|
473
|
+
"""Unloading when already at base is a safe no-op."""
|
|
474
|
+
state = soul_manager.unload()
|
|
475
|
+
assert state.active_soul is None
|
|
476
|
+
assert len(soul_manager.get_history()) == 0
|
|
477
|
+
|
|
478
|
+
def test_parse_missing_file(self, tmp_path: Path):
|
|
479
|
+
"""Parsing a nonexistent file raises FileNotFoundError."""
|
|
480
|
+
with pytest.raises(FileNotFoundError):
|
|
481
|
+
parse_blueprint(tmp_path / "nope.md")
|
|
482
|
+
|
|
483
|
+
def test_parse_empty_file(self, tmp_path: Path):
|
|
484
|
+
"""Parsing a file with no sections raises ValueError."""
|
|
485
|
+
empty = tmp_path / "empty.md"
|
|
486
|
+
empty.write_text("Just a paragraph with no headings.")
|
|
487
|
+
with pytest.raises(ValueError, match="No sections"):
|
|
488
|
+
parse_blueprint(empty)
|
|
489
|
+
|
|
490
|
+
def test_corrupt_history_recovered(self, soul_manager: SoulManager):
|
|
491
|
+
"""Corrupt history.json returns empty list instead of crashing."""
|
|
492
|
+
(soul_manager.soul_dir / "history.json").write_text("NOT JSON")
|
|
493
|
+
assert soul_manager.get_history() == []
|
|
494
|
+
|
|
495
|
+
def test_corrupt_active_recovered(self, soul_manager: SoulManager):
|
|
496
|
+
"""Corrupt active.json returns default state instead of crashing."""
|
|
497
|
+
(soul_manager.soul_dir / "active.json").write_text("{bad")
|
|
498
|
+
state = soul_manager.get_status()
|
|
499
|
+
assert state.base_soul == "base"
|
|
500
|
+
|
|
501
|
+
def test_load_while_loaded_records_swap(
|
|
502
|
+
self,
|
|
503
|
+
soul_manager: SoulManager,
|
|
504
|
+
professional_blueprint: Path,
|
|
505
|
+
comedy_blueprint: Path,
|
|
506
|
+
):
|
|
507
|
+
"""Loading a new soul while one is active swaps correctly."""
|
|
508
|
+
soul_manager.install(professional_blueprint)
|
|
509
|
+
soul_manager.install(comedy_blueprint)
|
|
510
|
+
soul_manager.load("the-test-doctor")
|
|
511
|
+
soul_manager.load("test-word-surgeon")
|
|
512
|
+
|
|
513
|
+
state = soul_manager.get_status()
|
|
514
|
+
assert state.active_soul == "test-word-surgeon"
|
|
515
|
+
|
|
516
|
+
history = soul_manager.get_history()
|
|
517
|
+
assert len(history) == 2
|
|
518
|
+
assert history[1].from_soul == "the-test-doctor"
|
|
519
|
+
assert history[1].to_soul == "test-word-surgeon"
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
# ---------------------------------------------------------------------------
|
|
523
|
+
# Memory tagging integration
|
|
524
|
+
# ---------------------------------------------------------------------------
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
class TestMemorySoulContext:
|
|
528
|
+
"""Test that memory engine correctly tags with soul_context."""
|
|
529
|
+
|
|
530
|
+
@pytest.fixture(autouse=True)
|
|
531
|
+
def no_unified_backend(self, monkeypatch):
|
|
532
|
+
"""Disable the unified skmemory backend so tests use only file-based storage."""
|
|
533
|
+
monkeypatch.setattr("skcapstone.memory_engine._get_unified", lambda: None)
|
|
534
|
+
|
|
535
|
+
def test_store_with_explicit_soul_context(self, tmp_home: Path):
|
|
536
|
+
"""Storing memory with explicit soul_context sets it."""
|
|
537
|
+
from skcapstone.memory_engine import store
|
|
538
|
+
|
|
539
|
+
entry = store(tmp_home, "test memory", soul_context="the-doctor")
|
|
540
|
+
assert entry.soul_context == "the-doctor"
|
|
541
|
+
|
|
542
|
+
def test_store_without_soul_context_is_none(self, tmp_home: Path):
|
|
543
|
+
"""Without active soul, soul_context is None (base)."""
|
|
544
|
+
from skcapstone.memory_engine import store
|
|
545
|
+
|
|
546
|
+
entry = store(tmp_home, "base memory")
|
|
547
|
+
assert entry.soul_context is None
|
|
548
|
+
|
|
549
|
+
def test_store_autodetects_active_soul(self, tmp_home: Path):
|
|
550
|
+
"""store() auto-detects active soul from active.json."""
|
|
551
|
+
from skcapstone.memory_engine import store
|
|
552
|
+
|
|
553
|
+
soul_dir = tmp_home / "soul"
|
|
554
|
+
soul_dir.mkdir(parents=True, exist_ok=True)
|
|
555
|
+
state = {"base_soul": "base", "active_soul": "the-doctor", "activated_at": None}
|
|
556
|
+
(soul_dir / "active.json").write_text(json.dumps(state))
|
|
557
|
+
|
|
558
|
+
entry = store(tmp_home, "auto-tagged memory")
|
|
559
|
+
assert entry.soul_context == "the-doctor"
|
|
560
|
+
|
|
561
|
+
def test_search_filters_by_soul_context(self, tmp_home: Path):
|
|
562
|
+
"""search() with soul_context filter only returns matching memories."""
|
|
563
|
+
from skcapstone.memory_engine import search, store
|
|
564
|
+
|
|
565
|
+
store(tmp_home, "doctor memory", soul_context="the-doctor")
|
|
566
|
+
store(tmp_home, "surgeon memory", soul_context="word-surgeon")
|
|
567
|
+
store(tmp_home, "base memory", soul_context=None)
|
|
568
|
+
|
|
569
|
+
results = search(tmp_home, "memory", soul_context="the-doctor")
|
|
570
|
+
assert len(results) == 1
|
|
571
|
+
assert results[0].soul_context == "the-doctor"
|
|
572
|
+
|
|
573
|
+
def test_search_without_filter_returns_all(self, tmp_home: Path):
|
|
574
|
+
"""search() without soul_context filter returns all matches."""
|
|
575
|
+
from skcapstone.memory_engine import search, store
|
|
576
|
+
|
|
577
|
+
store(tmp_home, "doctor memory", soul_context="the-doctor")
|
|
578
|
+
store(tmp_home, "base memory", soul_context=None)
|
|
579
|
+
|
|
580
|
+
results = search(tmp_home, "memory")
|
|
581
|
+
assert len(results) == 2
|
|
582
|
+
|
|
583
|
+
def test_soul_context_persists_on_disk(self, tmp_home: Path):
|
|
584
|
+
"""soul_context is written to and read back from JSON."""
|
|
585
|
+
from skcapstone.memory_engine import recall, store
|
|
586
|
+
|
|
587
|
+
entry = store(tmp_home, "persistent memory", soul_context="aura")
|
|
588
|
+
recalled = recall(tmp_home, entry.memory_id)
|
|
589
|
+
assert recalled is not None
|
|
590
|
+
assert recalled.soul_context == "aura"
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
# ---------------------------------------------------------------------------
|
|
594
|
+
# Model tests
|
|
595
|
+
# ---------------------------------------------------------------------------
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
class TestModels:
|
|
599
|
+
"""Basic model instantiation and serialization."""
|
|
600
|
+
|
|
601
|
+
def test_soul_blueprint_defaults(self):
|
|
602
|
+
"""SoulBlueprint has sensible defaults."""
|
|
603
|
+
bp = SoulBlueprint(name="test", display_name="Test")
|
|
604
|
+
assert bp.category == "unknown"
|
|
605
|
+
assert bp.core_traits == []
|
|
606
|
+
assert bp.emotional_topology == {}
|
|
607
|
+
|
|
608
|
+
def test_soul_state_defaults(self):
|
|
609
|
+
"""SoulState defaults to base with no active overlay."""
|
|
610
|
+
state = SoulState()
|
|
611
|
+
assert state.base_soul == "base"
|
|
612
|
+
assert state.active_soul is None
|
|
613
|
+
|
|
614
|
+
def test_soul_swap_event_timestamp(self):
|
|
615
|
+
"""SoulSwapEvent auto-generates a timestamp."""
|
|
616
|
+
event = SoulSwapEvent(from_soul="a", to_soul="b")
|
|
617
|
+
assert event.timestamp is not None
|
|
618
|
+
assert "T" in event.timestamp
|
|
619
|
+
|
|
620
|
+
def test_communication_style_serialization(self):
|
|
621
|
+
"""CommunicationStyle round-trips through JSON."""
|
|
622
|
+
cs = CommunicationStyle(
|
|
623
|
+
patterns=["p1"],
|
|
624
|
+
tone_markers=["t1"],
|
|
625
|
+
signature_phrases=["s1"],
|
|
626
|
+
)
|
|
627
|
+
data = cs.model_dump()
|
|
628
|
+
cs2 = CommunicationStyle.model_validate(data)
|
|
629
|
+
assert cs2.patterns == ["p1"]
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
# ---------------------------------------------------------------------------
|
|
633
|
+
# YAML blueprint loading tests
|
|
634
|
+
# ---------------------------------------------------------------------------
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
@pytest.fixture
|
|
638
|
+
def yaml_blueprint(tmp_path: Path) -> Path:
|
|
639
|
+
"""Create a YAML-format blueprint file."""
|
|
640
|
+
content = """\
|
|
641
|
+
name: the-architect
|
|
642
|
+
display_name: The Architect
|
|
643
|
+
category: professional
|
|
644
|
+
vibe: Systematic, strategic, sees the big picture
|
|
645
|
+
philosophy: Good architecture outlives the architect.
|
|
646
|
+
emoji: "\U0001F3D7"
|
|
647
|
+
core_traits:
|
|
648
|
+
- Systems thinking — sees connections others miss
|
|
649
|
+
- Strategic patience — knows when to build and when to wait
|
|
650
|
+
- Pattern recognition — applies lessons across domains
|
|
651
|
+
communication_style:
|
|
652
|
+
patterns:
|
|
653
|
+
- Draws diagrams before writing code
|
|
654
|
+
- Explains decisions in terms of trade-offs
|
|
655
|
+
tone_markers:
|
|
656
|
+
- Calm and measured
|
|
657
|
+
- Avoids absolutes
|
|
658
|
+
signature_phrases:
|
|
659
|
+
- "What are the trade-offs?"
|
|
660
|
+
- "Let me draw this out."
|
|
661
|
+
decision_framework: "1. Correctness 2. Simplicity 3. Evolvability"
|
|
662
|
+
emotional_topology:
|
|
663
|
+
precision: 0.75
|
|
664
|
+
calm: 0.5
|
|
665
|
+
curiosity: 0.5
|
|
666
|
+
"""
|
|
667
|
+
path = tmp_path / "the-architect.yaml"
|
|
668
|
+
path.write_text(content)
|
|
669
|
+
return path
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
@pytest.fixture
|
|
673
|
+
def yaml_minimal(tmp_path: Path) -> Path:
|
|
674
|
+
"""Create a minimal YAML blueprint with only required fields."""
|
|
675
|
+
content = """\
|
|
676
|
+
name: minimal-soul
|
|
677
|
+
display_name: Minimal Soul
|
|
678
|
+
"""
|
|
679
|
+
path = tmp_path / "minimal-soul.yaml"
|
|
680
|
+
path.write_text(content)
|
|
681
|
+
return path
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
class TestYAMLLoading:
|
|
685
|
+
"""Tests for YAML blueprint loading."""
|
|
686
|
+
|
|
687
|
+
def test_load_yaml_blueprint(self, yaml_blueprint: Path):
|
|
688
|
+
"""Load a full YAML blueprint and verify all fields."""
|
|
689
|
+
bp = load_yaml_blueprint(yaml_blueprint)
|
|
690
|
+
assert bp.name == "the-architect"
|
|
691
|
+
assert bp.display_name == "The Architect"
|
|
692
|
+
assert bp.category == "professional"
|
|
693
|
+
assert "Systematic" in bp.vibe
|
|
694
|
+
assert bp.philosophy == "Good architecture outlives the architect."
|
|
695
|
+
assert len(bp.core_traits) == 3
|
|
696
|
+
assert len(bp.communication_style.patterns) == 2
|
|
697
|
+
assert len(bp.communication_style.tone_markers) == 2
|
|
698
|
+
assert len(bp.communication_style.signature_phrases) == 2
|
|
699
|
+
assert bp.emotional_topology["precision"] == 0.75
|
|
700
|
+
|
|
701
|
+
def test_load_minimal_yaml(self, yaml_minimal: Path):
|
|
702
|
+
"""Load a minimal YAML blueprint with defaults."""
|
|
703
|
+
bp = load_yaml_blueprint(yaml_minimal)
|
|
704
|
+
assert bp.name == "minimal-soul"
|
|
705
|
+
assert bp.display_name == "Minimal Soul"
|
|
706
|
+
assert bp.category == "unknown"
|
|
707
|
+
assert bp.core_traits == []
|
|
708
|
+
assert bp.emotional_topology == {}
|
|
709
|
+
|
|
710
|
+
def test_parse_blueprint_detects_yaml(self, yaml_blueprint: Path):
|
|
711
|
+
"""parse_blueprint auto-detects .yaml files."""
|
|
712
|
+
bp = parse_blueprint(yaml_blueprint)
|
|
713
|
+
assert bp.name == "the-architect"
|
|
714
|
+
|
|
715
|
+
def test_parse_blueprint_detects_yml(self, tmp_path: Path):
|
|
716
|
+
"""parse_blueprint auto-detects .yml files."""
|
|
717
|
+
content = "name: yml-test\ndisplay_name: YML Test\n"
|
|
718
|
+
path = tmp_path / "test.yml"
|
|
719
|
+
path.write_text(content)
|
|
720
|
+
bp = parse_blueprint(path)
|
|
721
|
+
assert bp.name == "yml-test"
|
|
722
|
+
|
|
723
|
+
def test_yaml_missing_file(self, tmp_path: Path):
|
|
724
|
+
"""Loading a nonexistent YAML file raises FileNotFoundError."""
|
|
725
|
+
with pytest.raises(FileNotFoundError):
|
|
726
|
+
load_yaml_blueprint(tmp_path / "nope.yaml")
|
|
727
|
+
|
|
728
|
+
def test_yaml_invalid_syntax(self, tmp_path: Path):
|
|
729
|
+
"""Loading invalid YAML raises ValueError."""
|
|
730
|
+
path = tmp_path / "bad.yaml"
|
|
731
|
+
path.write_text("name: [\ninvalid yaml")
|
|
732
|
+
with pytest.raises(ValueError, match="Invalid YAML"):
|
|
733
|
+
load_yaml_blueprint(path)
|
|
734
|
+
|
|
735
|
+
def test_yaml_not_a_mapping(self, tmp_path: Path):
|
|
736
|
+
"""Loading YAML that's a list raises ValueError."""
|
|
737
|
+
path = tmp_path / "list.yaml"
|
|
738
|
+
path.write_text("- item1\n- item2\n")
|
|
739
|
+
with pytest.raises(ValueError, match="Expected YAML mapping"):
|
|
740
|
+
load_yaml_blueprint(path)
|
|
741
|
+
|
|
742
|
+
def test_yaml_roundtrip_consistency(
|
|
743
|
+
self, professional_blueprint: Path, tmp_path: Path
|
|
744
|
+
):
|
|
745
|
+
"""MD parse → YAML write → YAML load produces same SoulBlueprint."""
|
|
746
|
+
import yaml
|
|
747
|
+
|
|
748
|
+
md_bp = parse_blueprint(professional_blueprint)
|
|
749
|
+
data = md_bp.model_dump()
|
|
750
|
+
yaml_path = tmp_path / "roundtrip.yaml"
|
|
751
|
+
with open(yaml_path, "w") as f:
|
|
752
|
+
yaml.dump(data, f, default_flow_style=False, allow_unicode=True)
|
|
753
|
+
|
|
754
|
+
yaml_bp = load_yaml_blueprint(yaml_path)
|
|
755
|
+
assert yaml_bp.name == md_bp.name
|
|
756
|
+
assert yaml_bp.display_name == md_bp.display_name
|
|
757
|
+
assert yaml_bp.category == md_bp.category
|
|
758
|
+
assert yaml_bp.core_traits == md_bp.core_traits
|
|
759
|
+
assert yaml_bp.emotional_topology == md_bp.emotional_topology
|
|
760
|
+
|
|
761
|
+
def test_install_yaml_blueprint(
|
|
762
|
+
self, soul_manager: SoulManager, yaml_blueprint: Path
|
|
763
|
+
):
|
|
764
|
+
"""SoulManager.install() accepts YAML files."""
|
|
765
|
+
bp = soul_manager.install(yaml_blueprint)
|
|
766
|
+
assert bp.name == "the-architect"
|
|
767
|
+
assert "the-architect" in soul_manager.list_installed()
|
|
768
|
+
|
|
769
|
+
def test_install_all_picks_up_yaml(
|
|
770
|
+
self, soul_manager: SoulManager, yaml_blueprint: Path, professional_blueprint: Path
|
|
771
|
+
):
|
|
772
|
+
"""install_all() picks up both .md and .yaml files."""
|
|
773
|
+
directory = yaml_blueprint.parent
|
|
774
|
+
installed = soul_manager.install_all(directory)
|
|
775
|
+
names = [bp.name for bp in installed]
|
|
776
|
+
assert "the-architect" in names
|
|
777
|
+
assert "the-test-doctor" in names
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
# ---------------------------------------------------------------------------
|
|
781
|
+
# SoulRegistry tests
|
|
782
|
+
# ---------------------------------------------------------------------------
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
@pytest.fixture
|
|
786
|
+
def populated_registry(tmp_path: Path) -> SoulRegistry:
|
|
787
|
+
"""Create a SoulRegistry with several soul blueprints."""
|
|
788
|
+
reg_dir = tmp_path / "registry"
|
|
789
|
+
reg_dir.mkdir()
|
|
790
|
+
|
|
791
|
+
souls = [
|
|
792
|
+
SoulBlueprint(
|
|
793
|
+
name="coder",
|
|
794
|
+
display_name="The Coder",
|
|
795
|
+
category="professional",
|
|
796
|
+
vibe="Logical and focused",
|
|
797
|
+
core_traits=["logic", "precision", "debugging"],
|
|
798
|
+
emotional_topology={"precision": 0.75, "curiosity": 0.5},
|
|
799
|
+
),
|
|
800
|
+
SoulBlueprint(
|
|
801
|
+
name="artist",
|
|
802
|
+
display_name="The Artist",
|
|
803
|
+
category="professional",
|
|
804
|
+
vibe="Creative and expressive",
|
|
805
|
+
core_traits=["creativity", "empathy", "vision"],
|
|
806
|
+
emotional_topology={"warmth": 0.5, "intensity": 0.75},
|
|
807
|
+
),
|
|
808
|
+
SoulBlueprint(
|
|
809
|
+
name="joker",
|
|
810
|
+
display_name="The Joker",
|
|
811
|
+
category="comedy",
|
|
812
|
+
vibe="Hilarious and irreverent",
|
|
813
|
+
core_traits=["humor", "timing", "rebellion"],
|
|
814
|
+
emotional_topology={"humor": 0.9, "rebellion": 0.5},
|
|
815
|
+
),
|
|
816
|
+
SoulBlueprint(
|
|
817
|
+
name="sage",
|
|
818
|
+
display_name="The Sage",
|
|
819
|
+
category="authentic-connection",
|
|
820
|
+
vibe="Wise and calming",
|
|
821
|
+
core_traits=["wisdom", "empathy", "patience"],
|
|
822
|
+
emotional_topology={"warmth": 0.8, "calm": 0.9},
|
|
823
|
+
),
|
|
824
|
+
]
|
|
825
|
+
|
|
826
|
+
for bp in souls:
|
|
827
|
+
path = reg_dir / f"{bp.name}.json"
|
|
828
|
+
path.write_text(bp.model_dump_json(indent=2), encoding="utf-8")
|
|
829
|
+
|
|
830
|
+
return SoulRegistry(reg_dir)
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
class TestSoulRegistry:
|
|
834
|
+
"""Tests for the SoulRegistry discovery and search API."""
|
|
835
|
+
|
|
836
|
+
def test_list_all(self, populated_registry: SoulRegistry):
|
|
837
|
+
"""list_all returns all registered souls sorted by name."""
|
|
838
|
+
all_souls = populated_registry.list_all()
|
|
839
|
+
assert len(all_souls) == 4
|
|
840
|
+
assert all_souls[0].name == "artist"
|
|
841
|
+
assert all_souls[-1].name == "sage"
|
|
842
|
+
|
|
843
|
+
def test_list_names(self, populated_registry: SoulRegistry):
|
|
844
|
+
"""list_names returns sorted slug names."""
|
|
845
|
+
names = populated_registry.list_names()
|
|
846
|
+
assert names == ["artist", "coder", "joker", "sage"]
|
|
847
|
+
|
|
848
|
+
def test_get_existing(self, populated_registry: SoulRegistry):
|
|
849
|
+
"""get() returns the correct blueprint by name."""
|
|
850
|
+
bp = populated_registry.get("coder")
|
|
851
|
+
assert bp is not None
|
|
852
|
+
assert bp.display_name == "The Coder"
|
|
853
|
+
|
|
854
|
+
def test_get_missing(self, populated_registry: SoulRegistry):
|
|
855
|
+
"""get() returns None for unknown names."""
|
|
856
|
+
assert populated_registry.get("nonexistent") is None
|
|
857
|
+
|
|
858
|
+
def test_search_by_category(self, populated_registry: SoulRegistry):
|
|
859
|
+
"""search(category=...) filters correctly."""
|
|
860
|
+
results = populated_registry.search(category="professional")
|
|
861
|
+
assert len(results) == 2
|
|
862
|
+
names = [bp.name for bp in results]
|
|
863
|
+
assert "coder" in names
|
|
864
|
+
assert "artist" in names
|
|
865
|
+
|
|
866
|
+
def test_search_by_category_case_insensitive(self, populated_registry: SoulRegistry):
|
|
867
|
+
"""search(category=...) is case-insensitive."""
|
|
868
|
+
results = populated_registry.search(category="COMEDY")
|
|
869
|
+
assert len(results) == 1
|
|
870
|
+
assert results[0].name == "joker"
|
|
871
|
+
|
|
872
|
+
def test_search_by_trait_keyword(self, populated_registry: SoulRegistry):
|
|
873
|
+
"""search(trait_keyword=...) matches against core_traits."""
|
|
874
|
+
results = populated_registry.search(trait_keyword="empathy")
|
|
875
|
+
assert len(results) == 2
|
|
876
|
+
names = [bp.name for bp in results]
|
|
877
|
+
assert "artist" in names
|
|
878
|
+
assert "sage" in names
|
|
879
|
+
|
|
880
|
+
def test_search_by_topology(self, populated_registry: SoulRegistry):
|
|
881
|
+
"""search(min_topology=...) filters by minimum thresholds."""
|
|
882
|
+
results = populated_registry.search(min_topology={"warmth": 0.6})
|
|
883
|
+
assert len(results) == 1
|
|
884
|
+
assert results[0].name == "sage"
|
|
885
|
+
|
|
886
|
+
def test_search_combined_filters(self, populated_registry: SoulRegistry):
|
|
887
|
+
"""search() combines category + trait_keyword filters."""
|
|
888
|
+
results = populated_registry.search(
|
|
889
|
+
category="professional", trait_keyword="precision"
|
|
890
|
+
)
|
|
891
|
+
assert len(results) == 1
|
|
892
|
+
assert results[0].name == "coder"
|
|
893
|
+
|
|
894
|
+
def test_search_no_results(self, populated_registry: SoulRegistry):
|
|
895
|
+
"""search() returns empty list when nothing matches."""
|
|
896
|
+
results = populated_registry.search(trait_keyword="zzz_nonexistent")
|
|
897
|
+
assert results == []
|
|
898
|
+
|
|
899
|
+
def test_by_category(self, populated_registry: SoulRegistry):
|
|
900
|
+
"""by_category() groups souls correctly."""
|
|
901
|
+
groups = populated_registry.by_category()
|
|
902
|
+
assert len(groups) == 3
|
|
903
|
+
assert len(groups["professional"]) == 2
|
|
904
|
+
assert len(groups["comedy"]) == 1
|
|
905
|
+
assert len(groups["authentic-connection"]) == 1
|
|
906
|
+
|
|
907
|
+
def test_count(self, populated_registry: SoulRegistry):
|
|
908
|
+
"""count() returns the total number of souls."""
|
|
909
|
+
assert populated_registry.count() == 4
|
|
910
|
+
|
|
911
|
+
def test_categories(self, populated_registry: SoulRegistry):
|
|
912
|
+
"""categories() returns sorted unique category names."""
|
|
913
|
+
cats = populated_registry.categories()
|
|
914
|
+
assert cats == ["authentic-connection", "comedy", "professional"]
|
|
915
|
+
|
|
916
|
+
def test_summary(self, populated_registry: SoulRegistry):
|
|
917
|
+
"""summary() returns a complete overview dict."""
|
|
918
|
+
s = populated_registry.summary()
|
|
919
|
+
assert s["total"] == 4
|
|
920
|
+
assert s["categories"]["professional"] == 2
|
|
921
|
+
assert s["categories"]["comedy"] == 1
|
|
922
|
+
assert len(s["souls"]) == 4
|
|
923
|
+
|
|
924
|
+
def test_empty_registry(self, tmp_path: Path):
|
|
925
|
+
"""Empty registry returns empty results without errors."""
|
|
926
|
+
empty_dir = tmp_path / "empty"
|
|
927
|
+
empty_dir.mkdir()
|
|
928
|
+
reg = SoulRegistry(empty_dir)
|
|
929
|
+
assert reg.list_all() == []
|
|
930
|
+
assert reg.count() == 0
|
|
931
|
+
assert reg.categories() == []
|
|
932
|
+
|
|
933
|
+
def test_missing_directory(self, tmp_path: Path):
|
|
934
|
+
"""Registry handles nonexistent source directory gracefully."""
|
|
935
|
+
reg = SoulRegistry(tmp_path / "nope")
|
|
936
|
+
assert reg.list_all() == []
|
|
937
|
+
assert reg.count() == 0
|
|
938
|
+
|
|
939
|
+
def test_reload(self, populated_registry: SoulRegistry, tmp_path: Path):
|
|
940
|
+
"""reload() picks up newly added souls."""
|
|
941
|
+
assert populated_registry.count() == 4
|
|
942
|
+
|
|
943
|
+
# Add a new soul to the registry directory
|
|
944
|
+
new_bp = SoulBlueprint(
|
|
945
|
+
name="new-soul",
|
|
946
|
+
display_name="New Soul",
|
|
947
|
+
category="test",
|
|
948
|
+
)
|
|
949
|
+
path = populated_registry.source / "new-soul.json"
|
|
950
|
+
path.write_text(new_bp.model_dump_json(indent=2), encoding="utf-8")
|
|
951
|
+
|
|
952
|
+
populated_registry.reload()
|
|
953
|
+
assert populated_registry.count() == 5
|
|
954
|
+
assert populated_registry.get("new-soul") is not None
|
|
955
|
+
|
|
956
|
+
def test_registry_loads_yaml(self, tmp_path: Path):
|
|
957
|
+
"""Registry can load YAML files directly."""
|
|
958
|
+
import yaml
|
|
959
|
+
|
|
960
|
+
reg_dir = tmp_path / "yaml-reg"
|
|
961
|
+
reg_dir.mkdir()
|
|
962
|
+
data = {
|
|
963
|
+
"name": "yaml-soul",
|
|
964
|
+
"display_name": "YAML Soul",
|
|
965
|
+
"category": "test",
|
|
966
|
+
"core_traits": ["flexible"],
|
|
967
|
+
}
|
|
968
|
+
with open(reg_dir / "yaml-soul.yaml", "w") as f:
|
|
969
|
+
yaml.dump(data, f)
|
|
970
|
+
|
|
971
|
+
reg = SoulRegistry(reg_dir)
|
|
972
|
+
assert reg.count() == 1
|
|
973
|
+
bp = reg.get("yaml-soul")
|
|
974
|
+
assert bp is not None
|
|
975
|
+
assert bp.core_traits == ["flexible"]
|
|
976
|
+
|
|
977
|
+
def test_manager_get_registry(
|
|
978
|
+
self, soul_manager: SoulManager, professional_blueprint: Path
|
|
979
|
+
):
|
|
980
|
+
"""SoulManager.get_registry() returns a working registry."""
|
|
981
|
+
soul_manager.install(professional_blueprint)
|
|
982
|
+
reg = soul_manager.get_registry()
|
|
983
|
+
assert reg.count() == 1
|
|
984
|
+
assert reg.get("the-test-doctor") is not None
|