@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,310 @@
|
|
|
1
|
+
"""Tests for the universal agent context loader.
|
|
2
|
+
|
|
3
|
+
Covers context gathering, all four formatters, and CLI integration.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
from skcapstone.context_loader import (
|
|
15
|
+
FORMATTERS,
|
|
16
|
+
_gather_consciousness,
|
|
17
|
+
format_claude_md,
|
|
18
|
+
format_cursor_rules,
|
|
19
|
+
format_json,
|
|
20
|
+
format_text,
|
|
21
|
+
gather_context,
|
|
22
|
+
)
|
|
23
|
+
from skcapstone.memory_engine import store
|
|
24
|
+
from skcapstone.pillars.identity import generate_identity
|
|
25
|
+
from skcapstone.pillars.memory import initialize_memory
|
|
26
|
+
from skcapstone.pillars.security import initialize_security
|
|
27
|
+
from skcapstone.pillars.sync import initialize_sync
|
|
28
|
+
from skcapstone.pillars.trust import initialize_trust, record_trust_state
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _init_agent(home: Path, name: str = "context-test") -> None:
|
|
32
|
+
"""Set up a full agent for testing."""
|
|
33
|
+
generate_identity(home, name)
|
|
34
|
+
initialize_memory(home)
|
|
35
|
+
initialize_trust(home)
|
|
36
|
+
initialize_security(home)
|
|
37
|
+
initialize_sync(home)
|
|
38
|
+
|
|
39
|
+
manifest = {"name": name, "version": "0.1.0", "created_at": "2026-01-01T00:00:00Z", "connectors": []}
|
|
40
|
+
(home / "manifest.json").write_text(json.dumps(manifest, indent=2))
|
|
41
|
+
(home / "config").mkdir(exist_ok=True)
|
|
42
|
+
(home / "config" / "config.yaml").write_text(yaml.dump({"agent_name": name}))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TestGatherContext:
|
|
46
|
+
"""Tests for gather_context() data collection."""
|
|
47
|
+
|
|
48
|
+
def test_gathers_from_initialized_agent(self, tmp_agent_home: Path):
|
|
49
|
+
"""gather_context returns a full context dict for an initialized agent."""
|
|
50
|
+
_init_agent(tmp_agent_home)
|
|
51
|
+
store(tmp_agent_home, "Test memory for context", tags=["test"])
|
|
52
|
+
|
|
53
|
+
ctx = gather_context(tmp_agent_home)
|
|
54
|
+
|
|
55
|
+
assert "agent" in ctx
|
|
56
|
+
assert "pillars" in ctx
|
|
57
|
+
assert "board" in ctx
|
|
58
|
+
assert "memories" in ctx
|
|
59
|
+
assert "soul" in ctx
|
|
60
|
+
assert "mcp" in ctx
|
|
61
|
+
assert "gathered_at" in ctx
|
|
62
|
+
|
|
63
|
+
def test_agent_section(self, tmp_agent_home: Path):
|
|
64
|
+
"""Agent section contains name and consciousness state."""
|
|
65
|
+
_init_agent(tmp_agent_home, "sovereign-ctx")
|
|
66
|
+
ctx = gather_context(tmp_agent_home)
|
|
67
|
+
|
|
68
|
+
assert ctx["agent"]["name"] == "sovereign-ctx"
|
|
69
|
+
assert "is_conscious" in ctx["agent"]
|
|
70
|
+
assert "fingerprint" in ctx["agent"]
|
|
71
|
+
|
|
72
|
+
def test_pillars_section(self, tmp_agent_home: Path):
|
|
73
|
+
"""Pillars section lists all five pillars."""
|
|
74
|
+
_init_agent(tmp_agent_home)
|
|
75
|
+
ctx = gather_context(tmp_agent_home)
|
|
76
|
+
|
|
77
|
+
pillars = ctx["pillars"]
|
|
78
|
+
for name in ("identity", "memory", "trust", "security", "sync"):
|
|
79
|
+
assert name in pillars
|
|
80
|
+
|
|
81
|
+
def test_memories_included(self, tmp_agent_home: Path):
|
|
82
|
+
"""Recent memories appear in the context."""
|
|
83
|
+
_init_agent(tmp_agent_home)
|
|
84
|
+
store(tmp_agent_home, "First context memory", tags=["alpha"])
|
|
85
|
+
store(tmp_agent_home, "Second context memory", tags=["beta"])
|
|
86
|
+
|
|
87
|
+
ctx = gather_context(tmp_agent_home, memory_limit=5)
|
|
88
|
+
assert len(ctx["memories"]) >= 2
|
|
89
|
+
|
|
90
|
+
def test_memory_limit_respected(self, tmp_agent_home: Path):
|
|
91
|
+
"""Memory limit caps the number of memories returned."""
|
|
92
|
+
_init_agent(tmp_agent_home)
|
|
93
|
+
for i in range(10):
|
|
94
|
+
store(tmp_agent_home, f"Memory number {i}")
|
|
95
|
+
|
|
96
|
+
ctx = gather_context(tmp_agent_home, memory_limit=3)
|
|
97
|
+
assert len(ctx["memories"]) <= 3
|
|
98
|
+
|
|
99
|
+
def test_empty_agent_home(self, tmp_agent_home: Path):
|
|
100
|
+
"""gather_context handles an uninitialized agent gracefully."""
|
|
101
|
+
ctx = gather_context(tmp_agent_home)
|
|
102
|
+
assert ctx["agent"].get("name") is not None
|
|
103
|
+
assert ctx["memories"] == []
|
|
104
|
+
|
|
105
|
+
def test_board_section(self, tmp_agent_home: Path):
|
|
106
|
+
"""Board section includes task counts."""
|
|
107
|
+
_init_agent(tmp_agent_home)
|
|
108
|
+
from skcapstone.coordination import Board, Task
|
|
109
|
+
|
|
110
|
+
board = Board(tmp_agent_home)
|
|
111
|
+
board.ensure_dirs()
|
|
112
|
+
board.create_task(Task(title="Test task"))
|
|
113
|
+
|
|
114
|
+
ctx = gather_context(tmp_agent_home)
|
|
115
|
+
assert ctx["board"]["total"] >= 1
|
|
116
|
+
|
|
117
|
+
def test_soul_section_base(self, tmp_agent_home: Path):
|
|
118
|
+
"""Soul section reports base when no overlay is active."""
|
|
119
|
+
_init_agent(tmp_agent_home)
|
|
120
|
+
ctx = gather_context(tmp_agent_home)
|
|
121
|
+
assert ctx["soul"]["active"] is None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class TestFormatText:
|
|
125
|
+
"""Tests for plain text formatter."""
|
|
126
|
+
|
|
127
|
+
def test_contains_agent_name(self, tmp_agent_home: Path):
|
|
128
|
+
"""Text output includes the agent name."""
|
|
129
|
+
_init_agent(tmp_agent_home, "text-test")
|
|
130
|
+
ctx = gather_context(tmp_agent_home)
|
|
131
|
+
output = format_text(ctx)
|
|
132
|
+
|
|
133
|
+
assert "text-test" in output
|
|
134
|
+
assert "SKCapstone Agent Context" in output
|
|
135
|
+
|
|
136
|
+
def test_contains_pillars(self, tmp_agent_home: Path):
|
|
137
|
+
"""Text output lists pillar statuses."""
|
|
138
|
+
_init_agent(tmp_agent_home)
|
|
139
|
+
ctx = gather_context(tmp_agent_home)
|
|
140
|
+
output = format_text(ctx)
|
|
141
|
+
|
|
142
|
+
assert "Pillars" in output
|
|
143
|
+
assert "identity" in output
|
|
144
|
+
assert "memory" in output
|
|
145
|
+
|
|
146
|
+
def test_contains_memories(self, tmp_agent_home: Path):
|
|
147
|
+
"""Text output shows recent memories."""
|
|
148
|
+
_init_agent(tmp_agent_home)
|
|
149
|
+
store(tmp_agent_home, "Remember this for text test")
|
|
150
|
+
ctx = gather_context(tmp_agent_home)
|
|
151
|
+
output = format_text(ctx)
|
|
152
|
+
|
|
153
|
+
assert "Recent Memories" in output
|
|
154
|
+
assert "Remember this" in output
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class TestFormatJson:
|
|
158
|
+
"""Tests for JSON formatter."""
|
|
159
|
+
|
|
160
|
+
def test_valid_json(self, tmp_agent_home: Path):
|
|
161
|
+
"""JSON output is valid parseable JSON."""
|
|
162
|
+
_init_agent(tmp_agent_home)
|
|
163
|
+
ctx = gather_context(tmp_agent_home)
|
|
164
|
+
output = format_json(ctx)
|
|
165
|
+
|
|
166
|
+
parsed = json.loads(output)
|
|
167
|
+
assert "agent" in parsed
|
|
168
|
+
assert "pillars" in parsed
|
|
169
|
+
|
|
170
|
+
def test_roundtrip(self, tmp_agent_home: Path):
|
|
171
|
+
"""JSON output can be parsed back to the original structure."""
|
|
172
|
+
_init_agent(tmp_agent_home)
|
|
173
|
+
ctx = gather_context(tmp_agent_home)
|
|
174
|
+
output = format_json(ctx)
|
|
175
|
+
parsed = json.loads(output)
|
|
176
|
+
|
|
177
|
+
assert parsed["agent"]["name"] == ctx["agent"]["name"]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TestFormatClaudeMd:
|
|
181
|
+
"""Tests for Claude Code CLAUDE.md formatter."""
|
|
182
|
+
|
|
183
|
+
def test_markdown_structure(self, tmp_agent_home: Path):
|
|
184
|
+
"""CLAUDE.md has proper markdown headers."""
|
|
185
|
+
_init_agent(tmp_agent_home, "claude-test")
|
|
186
|
+
ctx = gather_context(tmp_agent_home)
|
|
187
|
+
output = format_claude_md(ctx)
|
|
188
|
+
|
|
189
|
+
assert "# SKCapstone Agent Context" in output
|
|
190
|
+
assert "## Agent Identity" in output
|
|
191
|
+
assert "## Pillar Status" in output
|
|
192
|
+
assert "## Coordination Board" in output
|
|
193
|
+
|
|
194
|
+
def test_contains_cli_reference(self, tmp_agent_home: Path):
|
|
195
|
+
"""CLAUDE.md includes CLI command reference."""
|
|
196
|
+
_init_agent(tmp_agent_home)
|
|
197
|
+
ctx = gather_context(tmp_agent_home)
|
|
198
|
+
output = format_claude_md(ctx)
|
|
199
|
+
|
|
200
|
+
assert "## CLI Reference" in output
|
|
201
|
+
assert "skcapstone status" in output
|
|
202
|
+
assert "skcapstone memory" in output
|
|
203
|
+
|
|
204
|
+
def test_contains_agent_name(self, tmp_agent_home: Path):
|
|
205
|
+
"""CLAUDE.md contains the agent name."""
|
|
206
|
+
_init_agent(tmp_agent_home, "claude-agent")
|
|
207
|
+
ctx = gather_context(tmp_agent_home)
|
|
208
|
+
output = format_claude_md(ctx)
|
|
209
|
+
|
|
210
|
+
assert "claude-agent" in output
|
|
211
|
+
|
|
212
|
+
def test_pillar_table(self, tmp_agent_home: Path):
|
|
213
|
+
"""CLAUDE.md contains a pillar status table."""
|
|
214
|
+
_init_agent(tmp_agent_home)
|
|
215
|
+
ctx = gather_context(tmp_agent_home)
|
|
216
|
+
output = format_claude_md(ctx)
|
|
217
|
+
|
|
218
|
+
assert "| Pillar | Status |" in output
|
|
219
|
+
assert "identity" in output
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class TestFormatCursorRules:
|
|
223
|
+
"""Tests for Cursor .mdc rule formatter."""
|
|
224
|
+
|
|
225
|
+
def test_mdc_frontmatter(self, tmp_agent_home: Path):
|
|
226
|
+
"""Cursor rules file has proper MDC frontmatter."""
|
|
227
|
+
_init_agent(tmp_agent_home)
|
|
228
|
+
ctx = gather_context(tmp_agent_home)
|
|
229
|
+
output = format_cursor_rules(ctx)
|
|
230
|
+
|
|
231
|
+
assert output.startswith("---")
|
|
232
|
+
assert "description:" in output
|
|
233
|
+
assert "alwaysApply: true" in output
|
|
234
|
+
|
|
235
|
+
def test_contains_agent_info(self, tmp_agent_home: Path):
|
|
236
|
+
"""Cursor rules contain agent identity info."""
|
|
237
|
+
_init_agent(tmp_agent_home, "cursor-test")
|
|
238
|
+
ctx = gather_context(tmp_agent_home)
|
|
239
|
+
output = format_cursor_rules(ctx)
|
|
240
|
+
|
|
241
|
+
assert "cursor-test" in output
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class TestFormattersRegistry:
|
|
245
|
+
"""Tests for the FORMATTERS dict."""
|
|
246
|
+
|
|
247
|
+
def test_all_formatters_registered(self):
|
|
248
|
+
"""All four formatters are in the registry."""
|
|
249
|
+
assert "text" in FORMATTERS
|
|
250
|
+
assert "json" in FORMATTERS
|
|
251
|
+
assert "claude-md" in FORMATTERS
|
|
252
|
+
assert "cursor-rules" in FORMATTERS
|
|
253
|
+
|
|
254
|
+
def test_all_formatters_callable(self):
|
|
255
|
+
"""All formatters are callable."""
|
|
256
|
+
for name, fn in FORMATTERS.items():
|
|
257
|
+
assert callable(fn), f"Formatter '{name}' is not callable"
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class TestGatherConsciousness:
|
|
261
|
+
"""Tests for _gather_consciousness() fallback logic."""
|
|
262
|
+
|
|
263
|
+
def test_returns_expected_keys(self, tmp_agent_home: Path):
|
|
264
|
+
"""_gather_consciousness returns all required keys."""
|
|
265
|
+
result = _gather_consciousness(tmp_agent_home)
|
|
266
|
+
|
|
267
|
+
for key in ("enabled", "backends_available", "messages_processed",
|
|
268
|
+
"active_conversations", "inotify_active"):
|
|
269
|
+
assert key in result, f"missing key: {key}"
|
|
270
|
+
|
|
271
|
+
def test_no_daemon_no_config_returns_disabled(self, tmp_agent_home: Path):
|
|
272
|
+
"""Without daemon or config, enabled is False and lists are empty."""
|
|
273
|
+
from unittest.mock import patch
|
|
274
|
+
import urllib.error
|
|
275
|
+
|
|
276
|
+
with patch(
|
|
277
|
+
"urllib.request.urlopen",
|
|
278
|
+
side_effect=urllib.error.URLError("connection refused"),
|
|
279
|
+
):
|
|
280
|
+
result = _gather_consciousness(tmp_agent_home)
|
|
281
|
+
|
|
282
|
+
assert result["enabled"] is False
|
|
283
|
+
assert result["backends_available"] == []
|
|
284
|
+
assert result["messages_processed"] == 0
|
|
285
|
+
assert result["active_conversations"] == 0
|
|
286
|
+
assert result["inotify_active"] is False
|
|
287
|
+
|
|
288
|
+
def test_config_file_triggers_enabled(self, tmp_agent_home: Path):
|
|
289
|
+
"""If consciousness.yaml exists, enabled is True (config fallback)."""
|
|
290
|
+
config_dir = tmp_agent_home / "config"
|
|
291
|
+
config_dir.mkdir(exist_ok=True)
|
|
292
|
+
(config_dir / "consciousness.yaml").write_text("enabled: true\n")
|
|
293
|
+
|
|
294
|
+
result = _gather_consciousness(tmp_agent_home)
|
|
295
|
+
|
|
296
|
+
assert result["enabled"] is True
|
|
297
|
+
|
|
298
|
+
def test_gather_context_includes_consciousness_key(self, tmp_agent_home: Path):
|
|
299
|
+
"""gather_context() output includes the 'consciousness' key."""
|
|
300
|
+
ctx = gather_context(tmp_agent_home)
|
|
301
|
+
assert "consciousness" in ctx
|
|
302
|
+
|
|
303
|
+
def test_format_text_includes_consciousness_section(self, tmp_agent_home: Path):
|
|
304
|
+
"""format_text() output includes a Consciousness section."""
|
|
305
|
+
_init_agent(tmp_agent_home)
|
|
306
|
+
ctx = gather_context(tmp_agent_home)
|
|
307
|
+
output = format_text(ctx)
|
|
308
|
+
|
|
309
|
+
assert "Consciousness" in output
|
|
310
|
+
assert "Status:" in output
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Tests for the conversation API endpoints in the daemon HTTP server.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
GET /api/v1/conversations — list all peers
|
|
5
|
+
GET /api/v1/conversations/{peer} — full history for a peer
|
|
6
|
+
POST /api/v1/conversations/{peer}/send — send message, write to outbox
|
|
7
|
+
DELETE /api/v1/conversations/{peer} — clear history
|
|
8
|
+
Path-traversal sanitization
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import socket
|
|
15
|
+
import time
|
|
16
|
+
import threading
|
|
17
|
+
import urllib.error
|
|
18
|
+
import urllib.request
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from unittest.mock import patch
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
23
|
+
|
|
24
|
+
from skcapstone.daemon import DaemonConfig, DaemonService, _sanitize_peer
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Helpers
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
def _find_free_port() -> int:
|
|
32
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
33
|
+
s.bind(("127.0.0.1", 0))
|
|
34
|
+
return s.getsockname()[1]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _api(port: int, path: str, *, method: str = "GET", body: bytes | None = None) -> tuple[int, dict]:
|
|
38
|
+
url = f"http://127.0.0.1:{port}{path}"
|
|
39
|
+
req = urllib.request.Request(url, data=body, method=method)
|
|
40
|
+
if body is not None:
|
|
41
|
+
req.add_header("Content-Type", "application/json")
|
|
42
|
+
req.add_header("Content-Length", str(len(body)))
|
|
43
|
+
try:
|
|
44
|
+
with urllib.request.urlopen(req, timeout=3) as resp:
|
|
45
|
+
return resp.status, json.loads(resp.read())
|
|
46
|
+
except urllib.error.HTTPError as exc:
|
|
47
|
+
return exc.code, json.loads(exc.read())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.fixture
|
|
51
|
+
def conv_home(tmp_path):
|
|
52
|
+
"""Agent home with conversations directory pre-populated."""
|
|
53
|
+
home = tmp_path / ".skcapstone"
|
|
54
|
+
(home / "logs").mkdir(parents=True)
|
|
55
|
+
conv_dir = home / "conversations"
|
|
56
|
+
conv_dir.mkdir()
|
|
57
|
+
|
|
58
|
+
# Two peers with conversation history
|
|
59
|
+
alice = [
|
|
60
|
+
{"role": "user", "content": "hello alice", "timestamp": "2026-01-01T10:00:00+00:00"},
|
|
61
|
+
{"role": "assistant", "content": "hi there", "timestamp": "2026-01-01T10:00:01+00:00"},
|
|
62
|
+
]
|
|
63
|
+
bob = [
|
|
64
|
+
{"role": "user", "content": "hey bob", "timestamp": "2026-01-02T12:00:00+00:00"},
|
|
65
|
+
]
|
|
66
|
+
(conv_dir / "alice.json").write_text(json.dumps(alice), encoding="utf-8")
|
|
67
|
+
(conv_dir / "bob.json").write_text(json.dumps(bob), encoding="utf-8")
|
|
68
|
+
return home
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.fixture
|
|
72
|
+
def live_server(conv_home):
|
|
73
|
+
"""Start the daemon API server, yield (svc, port), then stop."""
|
|
74
|
+
config = DaemonConfig(
|
|
75
|
+
home=conv_home,
|
|
76
|
+
shared_root=conv_home, # keep test data isolated from real ~/.skcapstone
|
|
77
|
+
port=_find_free_port(),
|
|
78
|
+
poll_interval=60,
|
|
79
|
+
)
|
|
80
|
+
svc = DaemonService(config)
|
|
81
|
+
svc.state.running = True
|
|
82
|
+
|
|
83
|
+
with patch.object(svc, "_load_components"):
|
|
84
|
+
svc._write_pid()
|
|
85
|
+
svc._start_api_server()
|
|
86
|
+
time.sleep(0.4)
|
|
87
|
+
yield svc, config.port
|
|
88
|
+
svc.stop()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# Unit tests: _sanitize_peer
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
class TestSanitizePeer:
|
|
96
|
+
def test_normal_name(self):
|
|
97
|
+
assert _sanitize_peer("alice") == "alice"
|
|
98
|
+
|
|
99
|
+
def test_strips_slashes(self):
|
|
100
|
+
assert "/" not in _sanitize_peer("../etc/passwd")
|
|
101
|
+
assert "\\" not in _sanitize_peer("..\\windows")
|
|
102
|
+
|
|
103
|
+
def test_path_traversal(self):
|
|
104
|
+
result = _sanitize_peer("../../etc/passwd")
|
|
105
|
+
assert ".." not in result
|
|
106
|
+
assert "/" not in result
|
|
107
|
+
|
|
108
|
+
def test_empty_string(self):
|
|
109
|
+
assert _sanitize_peer("") == ""
|
|
110
|
+
|
|
111
|
+
def test_none_returns_empty(self):
|
|
112
|
+
assert _sanitize_peer(None) == "" # type: ignore[arg-type]
|
|
113
|
+
|
|
114
|
+
def test_max_length(self):
|
|
115
|
+
long = "a" * 100
|
|
116
|
+
assert len(_sanitize_peer(long)) <= 64
|
|
117
|
+
|
|
118
|
+
def test_allowed_chars(self):
|
|
119
|
+
assert _sanitize_peer("user@domain.io") == "user@domain.io"
|
|
120
|
+
assert _sanitize_peer("my-peer_01") == "my-peer_01"
|
|
121
|
+
|
|
122
|
+
def test_null_bytes_stripped(self):
|
|
123
|
+
assert "\x00" not in _sanitize_peer("evil\x00peer")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
# Integration tests: GET /api/v1/conversations
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
class TestListConversations:
|
|
131
|
+
def test_returns_list(self, live_server):
|
|
132
|
+
svc, port = live_server
|
|
133
|
+
status, data = _api(port, "/api/v1/conversations")
|
|
134
|
+
assert status == 200
|
|
135
|
+
assert "conversations" in data
|
|
136
|
+
assert isinstance(data["conversations"], list)
|
|
137
|
+
|
|
138
|
+
def test_includes_both_peers(self, live_server):
|
|
139
|
+
svc, port = live_server
|
|
140
|
+
_, data = _api(port, "/api/v1/conversations")
|
|
141
|
+
peers = {c["peer"] for c in data["conversations"]}
|
|
142
|
+
assert "alice" in peers
|
|
143
|
+
assert "bob" in peers
|
|
144
|
+
|
|
145
|
+
def test_message_count(self, live_server):
|
|
146
|
+
svc, port = live_server
|
|
147
|
+
_, data = _api(port, "/api/v1/conversations")
|
|
148
|
+
alice = next(c for c in data["conversations"] if c["peer"] == "alice")
|
|
149
|
+
assert alice["message_count"] == 2
|
|
150
|
+
|
|
151
|
+
def test_last_message_time_present(self, live_server):
|
|
152
|
+
svc, port = live_server
|
|
153
|
+
_, data = _api(port, "/api/v1/conversations")
|
|
154
|
+
alice = next(c for c in data["conversations"] if c["peer"] == "alice")
|
|
155
|
+
assert "last_message_time" in alice
|
|
156
|
+
assert alice["last_message_time"] is not None
|
|
157
|
+
|
|
158
|
+
def test_last_message_preview_present(self, live_server):
|
|
159
|
+
svc, port = live_server
|
|
160
|
+
_, data = _api(port, "/api/v1/conversations")
|
|
161
|
+
alice = next(c for c in data["conversations"] if c["peer"] == "alice")
|
|
162
|
+
assert "last_message_preview" in alice
|
|
163
|
+
assert isinstance(alice["last_message_preview"], str)
|
|
164
|
+
|
|
165
|
+
def test_empty_conversations_dir(self, tmp_path):
|
|
166
|
+
home = tmp_path / ".skcapstone"
|
|
167
|
+
(home / "logs").mkdir(parents=True)
|
|
168
|
+
(home / "conversations").mkdir()
|
|
169
|
+
config = DaemonConfig(
|
|
170
|
+
home=home,
|
|
171
|
+
shared_root=home,
|
|
172
|
+
port=_find_free_port(),
|
|
173
|
+
poll_interval=60,
|
|
174
|
+
)
|
|
175
|
+
svc = DaemonService(config)
|
|
176
|
+
svc.state.running = True
|
|
177
|
+
with patch.object(svc, "_load_components"):
|
|
178
|
+
svc._write_pid()
|
|
179
|
+
svc._start_api_server()
|
|
180
|
+
time.sleep(0.4)
|
|
181
|
+
try:
|
|
182
|
+
_, data = _api(config.port, "/api/v1/conversations")
|
|
183
|
+
assert data["conversations"] == []
|
|
184
|
+
finally:
|
|
185
|
+
svc.stop()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
# Integration tests: GET /api/v1/conversations/{peer}
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
class TestGetConversation:
|
|
193
|
+
def test_existing_peer(self, live_server):
|
|
194
|
+
svc, port = live_server
|
|
195
|
+
status, data = _api(port, "/api/v1/conversations/alice")
|
|
196
|
+
assert status == 200
|
|
197
|
+
assert data["peer"] == "alice"
|
|
198
|
+
assert len(data["messages"]) == 2
|
|
199
|
+
|
|
200
|
+
def test_missing_peer_404(self, live_server):
|
|
201
|
+
svc, port = live_server
|
|
202
|
+
status, _ = _api(port, "/api/v1/conversations/nobody")
|
|
203
|
+
assert status == 404
|
|
204
|
+
|
|
205
|
+
def test_path_traversal_rejected(self, live_server):
|
|
206
|
+
svc, port = live_server
|
|
207
|
+
# After sanitization "../../etc/passwd" → "etcpasswd" which doesn't exist → 404 or 400
|
|
208
|
+
status, _ = _api(port, "/api/v1/conversations/../../etc/passwd")
|
|
209
|
+
assert status in (400, 404)
|
|
210
|
+
|
|
211
|
+
def test_get_on_send_returns_405(self, live_server):
|
|
212
|
+
svc, port = live_server
|
|
213
|
+
status, data = _api(port, "/api/v1/conversations/alice/send")
|
|
214
|
+
assert status == 405
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
# Integration tests: POST /api/v1/conversations/{peer}/send
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
class TestSendMessage:
|
|
222
|
+
def test_send_returns_sent(self, live_server):
|
|
223
|
+
svc, port = live_server
|
|
224
|
+
body = json.dumps({"content": "hello world"}).encode()
|
|
225
|
+
status, data = _api(port, "/api/v1/conversations/alice/send", method="POST", body=body)
|
|
226
|
+
assert status == 200
|
|
227
|
+
assert data["status"] == "sent"
|
|
228
|
+
assert "message_id" in data
|
|
229
|
+
|
|
230
|
+
def test_send_writes_outbox_file(self, live_server, conv_home):
|
|
231
|
+
svc, port = live_server
|
|
232
|
+
body = json.dumps({"content": "test outbox write"}).encode()
|
|
233
|
+
status, resp = _api(port, "/api/v1/conversations/alice/send", method="POST", body=body)
|
|
234
|
+
assert status == 200, resp
|
|
235
|
+
msg_id = resp["message_id"]
|
|
236
|
+
# shared_root == conv_home in tests
|
|
237
|
+
outbox = svc.config.shared_root / "sync" / "comms" / "outbox" / f"{msg_id}.skc.json"
|
|
238
|
+
assert outbox.exists(), f"Outbox file not created: {outbox}"
|
|
239
|
+
envelope = json.loads(outbox.read_text())
|
|
240
|
+
assert envelope["recipient"] == "alice"
|
|
241
|
+
assert envelope["payload"]["content"] == "test outbox write"
|
|
242
|
+
|
|
243
|
+
def test_send_missing_content_400(self, live_server):
|
|
244
|
+
svc, port = live_server
|
|
245
|
+
body = json.dumps({"content": ""}).encode()
|
|
246
|
+
status, data = _api(port, "/api/v1/conversations/alice/send", method="POST", body=body)
|
|
247
|
+
assert status == 400
|
|
248
|
+
assert "content" in data["error"].lower()
|
|
249
|
+
|
|
250
|
+
def test_send_invalid_json_400(self, live_server):
|
|
251
|
+
svc, port = live_server
|
|
252
|
+
status, data = _api(
|
|
253
|
+
port, "/api/v1/conversations/alice/send", method="POST", body=b"not-json"
|
|
254
|
+
)
|
|
255
|
+
assert status == 400
|
|
256
|
+
|
|
257
|
+
def test_send_path_traversal_rejected(self, live_server):
|
|
258
|
+
svc, port = live_server
|
|
259
|
+
body = json.dumps({"content": "hi"}).encode()
|
|
260
|
+
# URL path traversal: peer sanitizes to empty or benign string
|
|
261
|
+
status, _ = _api(
|
|
262
|
+
port, "/api/v1/conversations/../../evil/send", method="POST", body=body
|
|
263
|
+
)
|
|
264
|
+
# Either 400 (invalid peer) or 200 (sanitized to "evil" which is fine) — not a server error
|
|
265
|
+
assert status in (200, 400)
|
|
266
|
+
|
|
267
|
+
def test_send_unique_message_ids(self, live_server):
|
|
268
|
+
svc, port = live_server
|
|
269
|
+
body = json.dumps({"content": "msg"}).encode()
|
|
270
|
+
ids = set()
|
|
271
|
+
for _ in range(5):
|
|
272
|
+
_, resp = _api(port, "/api/v1/conversations/alice/send", method="POST", body=body)
|
|
273
|
+
ids.add(resp["message_id"])
|
|
274
|
+
assert len(ids) == 5, "message_id should be unique per send"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# ---------------------------------------------------------------------------
|
|
278
|
+
# Integration tests: DELETE /api/v1/conversations/{peer}
|
|
279
|
+
# ---------------------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
class TestDeleteConversation:
|
|
282
|
+
def test_delete_existing_peer(self, live_server, conv_home):
|
|
283
|
+
svc, port = live_server
|
|
284
|
+
assert (conv_home / "conversations" / "bob.json").exists()
|
|
285
|
+
status, data = _api(port, "/api/v1/conversations/bob", method="DELETE")
|
|
286
|
+
assert status == 200
|
|
287
|
+
assert data["status"] == "deleted"
|
|
288
|
+
assert not (conv_home / "conversations" / "bob.json").exists()
|
|
289
|
+
|
|
290
|
+
def test_delete_missing_peer_404(self, live_server):
|
|
291
|
+
svc, port = live_server
|
|
292
|
+
status, _ = _api(port, "/api/v1/conversations/nobody", method="DELETE")
|
|
293
|
+
assert status == 404
|
|
294
|
+
|
|
295
|
+
def test_delete_invalid_peer_400(self, live_server):
|
|
296
|
+
svc, port = live_server
|
|
297
|
+
status, _ = _api(port, "/api/v1/conversations/", method="DELETE")
|
|
298
|
+
assert status in (400, 404)
|
|
299
|
+
|
|
300
|
+
def test_delete_does_not_affect_other_peers(self, live_server, conv_home):
|
|
301
|
+
svc, port = live_server
|
|
302
|
+
_api(port, "/api/v1/conversations/bob", method="DELETE")
|
|
303
|
+
# alice should still exist
|
|
304
|
+
assert (conv_home / "conversations" / "alice.json").exists()
|
|
305
|
+
status, _ = _api(port, "/api/v1/conversations/alice")
|
|
306
|
+
assert status == 200
|