@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,292 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tests/test_e2e_automated.py — Automated multi-agent E2E test via subprocess.
|
|
3
|
+
|
|
4
|
+
Starts the real skcapstone daemon, injects a message into the inbox,
|
|
5
|
+
and verifies that a response appears within the timeout window.
|
|
6
|
+
|
|
7
|
+
Marks are applied so the test is automatically skipped in unit-test
|
|
8
|
+
environments where the CLI is not installed or system requirements
|
|
9
|
+
are not met.
|
|
10
|
+
|
|
11
|
+
Run manually:
|
|
12
|
+
pytest tests/test_e2e_automated.py -v -s --timeout=360
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import shutil
|
|
20
|
+
import signal
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
import tempfile
|
|
24
|
+
import time
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
import pytest
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Skip guards
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
pytestmark = [
|
|
34
|
+
pytest.mark.skipif(
|
|
35
|
+
not shutil.which("skcapstone"),
|
|
36
|
+
reason="skcapstone CLI not installed — skipping live E2E",
|
|
37
|
+
),
|
|
38
|
+
pytest.mark.e2e,
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Constants
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
DAEMON_PORT = int(os.environ.get("E2E_PORT", "17777")) # offset to avoid collision
|
|
46
|
+
STARTUP_WAIT = int(os.environ.get("E2E_STARTUP_WAIT", "10"))
|
|
47
|
+
POLL_TIMEOUT = int(os.environ.get("E2E_POLL_TIMEOUT", "300"))
|
|
48
|
+
PEER = os.environ.get("E2E_PEER", "test-peer")
|
|
49
|
+
AGENT_HOME = Path(
|
|
50
|
+
os.environ.get("SKCAPSTONE_ROOT", os.environ.get("SKCAPSTONE_HOME", "~/.skcapstone"))
|
|
51
|
+
).expanduser()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Helpers
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _write_test_message(inbox_dir: Path, peer: str) -> tuple[Path, str]:
|
|
60
|
+
"""Write a test .skc.json message to inbox_dir and return (path, msg_id)."""
|
|
61
|
+
ts = int(time.time())
|
|
62
|
+
msg_id = f"e2e-auto-{ts}"
|
|
63
|
+
msg = {
|
|
64
|
+
"sender": peer,
|
|
65
|
+
"recipient": "Opus",
|
|
66
|
+
"payload": {
|
|
67
|
+
"content": f"Ping test — automated pytest E2E at {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}",
|
|
68
|
+
"content_type": "text",
|
|
69
|
+
},
|
|
70
|
+
"message_id": msg_id,
|
|
71
|
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S+00:00", time.gmtime()),
|
|
72
|
+
}
|
|
73
|
+
path = inbox_dir / f"{msg_id}.skc.json"
|
|
74
|
+
path.write_text(json.dumps(msg))
|
|
75
|
+
return path, msg_id
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _poll_for_response(
|
|
79
|
+
outbox_dir: Path,
|
|
80
|
+
conv_file: Path,
|
|
81
|
+
inbox_msg_path: Path,
|
|
82
|
+
timeout_secs: int,
|
|
83
|
+
) -> bool:
|
|
84
|
+
"""
|
|
85
|
+
Return True if a response is detected within timeout_secs.
|
|
86
|
+
|
|
87
|
+
Detection strategy (either satisfies the check):
|
|
88
|
+
1. A new .skc.json appears in outbox_dir AFTER inbox_msg_path's mtime.
|
|
89
|
+
2. conv_file is created/updated AFTER inbox_msg_path's mtime.
|
|
90
|
+
"""
|
|
91
|
+
outbox_dir.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
ref_mtime = inbox_msg_path.stat().st_mtime
|
|
93
|
+
|
|
94
|
+
deadline = time.monotonic() + timeout_secs
|
|
95
|
+
poll_interval = 2.0
|
|
96
|
+
last_log = time.monotonic()
|
|
97
|
+
|
|
98
|
+
while time.monotonic() < deadline:
|
|
99
|
+
# Check outbox for new envelope
|
|
100
|
+
for skc in outbox_dir.glob("*.skc.json"):
|
|
101
|
+
if skc.stat().st_mtime > ref_mtime:
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
# Check conversations file (passthrough / no-SKComm fallback)
|
|
105
|
+
if conv_file.exists() and conv_file.stat().st_mtime > ref_mtime:
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
now = time.monotonic()
|
|
109
|
+
if now - last_log >= 30:
|
|
110
|
+
elapsed = timeout_secs - (deadline - now)
|
|
111
|
+
print(f"\n [e2e] still waiting… {elapsed:.0f}s elapsed / {timeout_secs}s timeout")
|
|
112
|
+
last_log = now
|
|
113
|
+
|
|
114
|
+
time.sleep(poll_interval)
|
|
115
|
+
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# Fixtures
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@pytest.fixture(scope="module")
|
|
125
|
+
def daemon_process():
|
|
126
|
+
"""
|
|
127
|
+
Start the skcapstone daemon in the background for the duration of the module.
|
|
128
|
+
|
|
129
|
+
Yields the subprocess.Popen handle; tears down on module exit.
|
|
130
|
+
"""
|
|
131
|
+
log_fd, log_path = tempfile.mkstemp(prefix="skcapstone-e2e-", suffix=".log")
|
|
132
|
+
|
|
133
|
+
env = os.environ.copy()
|
|
134
|
+
env.setdefault("SKCAPSTONE_ROOT", str(AGENT_HOME))
|
|
135
|
+
|
|
136
|
+
proc = subprocess.Popen(
|
|
137
|
+
[
|
|
138
|
+
"skcapstone",
|
|
139
|
+
"daemon",
|
|
140
|
+
"start",
|
|
141
|
+
"--foreground",
|
|
142
|
+
"--port",
|
|
143
|
+
str(DAEMON_PORT),
|
|
144
|
+
],
|
|
145
|
+
stdout=log_fd,
|
|
146
|
+
stderr=log_fd,
|
|
147
|
+
env=env,
|
|
148
|
+
preexec_fn=os.setsid, # separate process group for clean teardown
|
|
149
|
+
)
|
|
150
|
+
os.close(log_fd)
|
|
151
|
+
|
|
152
|
+
print(f"\n [e2e] Daemon started (PID {proc.pid}) — log: {log_path}")
|
|
153
|
+
print(f" [e2e] Waiting {STARTUP_WAIT}s for startup…")
|
|
154
|
+
time.sleep(STARTUP_WAIT)
|
|
155
|
+
|
|
156
|
+
if proc.poll() is not None:
|
|
157
|
+
with open(log_path) as fh:
|
|
158
|
+
tail = fh.read()[-2000:]
|
|
159
|
+
pytest.fail(
|
|
160
|
+
f"Daemon exited prematurely (rc={proc.returncode}).\n"
|
|
161
|
+
f"Log tail:\n{tail}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
yield proc, log_path
|
|
165
|
+
|
|
166
|
+
# Teardown — send SIGTERM to the whole process group
|
|
167
|
+
try:
|
|
168
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
|
169
|
+
except ProcessLookupError:
|
|
170
|
+
pass
|
|
171
|
+
try:
|
|
172
|
+
proc.wait(timeout=10)
|
|
173
|
+
except subprocess.TimeoutExpired:
|
|
174
|
+
try:
|
|
175
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
|
176
|
+
except ProcessLookupError:
|
|
177
|
+
pass
|
|
178
|
+
print(f"\n [e2e] Daemon stopped. Log: {log_path}")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
# Tests
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TestDaemonStartup:
|
|
187
|
+
"""Verify the daemon starts and exposes its HTTP API."""
|
|
188
|
+
|
|
189
|
+
def test_daemon_is_running(self, daemon_process):
|
|
190
|
+
"""The daemon subprocess must still be alive after startup wait."""
|
|
191
|
+
proc, _ = daemon_process
|
|
192
|
+
assert proc.poll() is None, "Daemon exited before tests ran"
|
|
193
|
+
|
|
194
|
+
def test_consciousness_endpoint_responds(self, daemon_process):
|
|
195
|
+
"""GET /consciousness must return valid JSON."""
|
|
196
|
+
import urllib.request
|
|
197
|
+
import urllib.error
|
|
198
|
+
|
|
199
|
+
url = f"http://127.0.0.1:{DAEMON_PORT}/consciousness"
|
|
200
|
+
try:
|
|
201
|
+
with urllib.request.urlopen(url, timeout=10) as resp:
|
|
202
|
+
body = resp.read().decode()
|
|
203
|
+
data = json.loads(body)
|
|
204
|
+
except urllib.error.URLError as exc:
|
|
205
|
+
pytest.fail(f"/consciousness unreachable on port {DAEMON_PORT}: {exc}")
|
|
206
|
+
|
|
207
|
+
assert isinstance(data, dict), f"Expected JSON object, got: {body[:200]}"
|
|
208
|
+
# The endpoint should include some status indicator
|
|
209
|
+
assert data, "Response JSON is empty"
|
|
210
|
+
|
|
211
|
+
def test_consciousness_status_active(self, daemon_process):
|
|
212
|
+
"""The /consciousness endpoint should report an active/running status."""
|
|
213
|
+
import urllib.request
|
|
214
|
+
|
|
215
|
+
url = f"http://127.0.0.1:{DAEMON_PORT}/consciousness"
|
|
216
|
+
with urllib.request.urlopen(url, timeout=10) as resp:
|
|
217
|
+
data = json.loads(resp.read())
|
|
218
|
+
|
|
219
|
+
status = str(data.get("status", "")).lower()
|
|
220
|
+
# Accept various status strings that indicate the loop is running
|
|
221
|
+
active_statuses = {"active", "ok", "running", "started", "conscious"}
|
|
222
|
+
assert status in active_statuses or data.get("conscious") is True, (
|
|
223
|
+
f"Expected active status, got: {data}"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class TestMessageRoundTrip:
|
|
228
|
+
"""End-to-end: inject message → daemon processes → response appears."""
|
|
229
|
+
|
|
230
|
+
@pytest.fixture(autouse=True)
|
|
231
|
+
def _setup_dirs(self):
|
|
232
|
+
"""Ensure inbox and outbox directories exist before each test."""
|
|
233
|
+
inbox = AGENT_HOME / "sync" / "comms" / "inbox" / PEER
|
|
234
|
+
outbox = AGENT_HOME / "sync" / "comms" / "outbox" / PEER
|
|
235
|
+
inbox.mkdir(parents=True, exist_ok=True)
|
|
236
|
+
outbox.mkdir(parents=True, exist_ok=True)
|
|
237
|
+
|
|
238
|
+
def test_inbox_message_is_processed(self, daemon_process):
|
|
239
|
+
"""
|
|
240
|
+
Writing a .skc.json to the inbox triggers the consciousness loop
|
|
241
|
+
and produces a response in the outbox OR updates conversations/.
|
|
242
|
+
"""
|
|
243
|
+
inbox_dir = AGENT_HOME / "sync" / "comms" / "inbox" / PEER
|
|
244
|
+
outbox_dir = AGENT_HOME / "sync" / "comms" / "outbox" / PEER
|
|
245
|
+
conv_file = AGENT_HOME / "conversations" / f"{PEER}.json"
|
|
246
|
+
|
|
247
|
+
msg_path, msg_id = _write_test_message(inbox_dir, PEER)
|
|
248
|
+
print(f"\n [e2e] Message written: {msg_path} (id={msg_id})")
|
|
249
|
+
|
|
250
|
+
found = _poll_for_response(
|
|
251
|
+
outbox_dir=outbox_dir,
|
|
252
|
+
conv_file=conv_file,
|
|
253
|
+
inbox_msg_path=msg_path,
|
|
254
|
+
timeout_secs=POLL_TIMEOUT,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
assert found, (
|
|
258
|
+
f"No response detected within {POLL_TIMEOUT}s.\n"
|
|
259
|
+
f" inbox_dir: {inbox_dir}\n"
|
|
260
|
+
f" outbox_dir: {outbox_dir}\n"
|
|
261
|
+
f" conv_file: {conv_file}"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def test_conversations_file_updated(self, daemon_process):
|
|
265
|
+
"""
|
|
266
|
+
After a message is processed, ~/.skcapstone/conversations/<peer>.json
|
|
267
|
+
must exist and contain valid JSON with the peer's conversation history.
|
|
268
|
+
"""
|
|
269
|
+
inbox_dir = AGENT_HOME / "sync" / "comms" / "inbox" / PEER
|
|
270
|
+
outbox_dir = AGENT_HOME / "sync" / "comms" / "outbox" / PEER
|
|
271
|
+
conv_file = AGENT_HOME / "conversations" / f"{PEER}.json"
|
|
272
|
+
|
|
273
|
+
msg_path, msg_id = _write_test_message(inbox_dir, PEER)
|
|
274
|
+
print(f"\n [e2e] Message written: {msg_path} (id={msg_id})")
|
|
275
|
+
|
|
276
|
+
# Wait for conversations file to appear
|
|
277
|
+
deadline = time.monotonic() + POLL_TIMEOUT
|
|
278
|
+
ref_mtime = msg_path.stat().st_mtime
|
|
279
|
+
while time.monotonic() < deadline:
|
|
280
|
+
if conv_file.exists() and conv_file.stat().st_mtime >= ref_mtime:
|
|
281
|
+
break
|
|
282
|
+
time.sleep(2)
|
|
283
|
+
|
|
284
|
+
assert conv_file.exists(), (
|
|
285
|
+
f"conversations/{PEER}.json not found after {POLL_TIMEOUT}s"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
content = conv_file.read_text()
|
|
289
|
+
data = json.loads(content) # raises if invalid JSON
|
|
290
|
+
assert isinstance(data, (dict, list)), (
|
|
291
|
+
f"Unexpected conversations format: {content[:200]}"
|
|
292
|
+
)
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"""Tests for the persistent error recovery queue."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from skcapstone.error_queue import (
|
|
13
|
+
ErrorEntry,
|
|
14
|
+
ErrorQueue,
|
|
15
|
+
ErrorStatus,
|
|
16
|
+
_backoff_ts,
|
|
17
|
+
_now_iso,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Fixtures
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def queue_path(tmp_path: Path) -> Path:
|
|
28
|
+
"""Return a temp path for the error queue JSON file."""
|
|
29
|
+
return tmp_path / "error_queue.json"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.fixture
|
|
33
|
+
def queue(queue_path: Path) -> ErrorQueue:
|
|
34
|
+
"""Return an ErrorQueue backed by a temp file with fast backoff."""
|
|
35
|
+
return ErrorQueue(path=queue_path, max_retries=3, base_backoff=0)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# ErrorEntry serialisation
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TestErrorEntry:
|
|
44
|
+
"""Tests for ErrorEntry (de)serialisation."""
|
|
45
|
+
|
|
46
|
+
def test_roundtrip(self) -> None:
|
|
47
|
+
"""to_dict / from_dict round-trip preserves all fields."""
|
|
48
|
+
entry = ErrorEntry(
|
|
49
|
+
operation_type="llm_call",
|
|
50
|
+
payload={"model": "llama3", "prompt": "hello"},
|
|
51
|
+
error_message="timeout",
|
|
52
|
+
)
|
|
53
|
+
entry.retry_count = 2
|
|
54
|
+
entry.next_retry_at = "2099-01-01T00:00:00+00:00"
|
|
55
|
+
entry.status = ErrorStatus.PENDING
|
|
56
|
+
|
|
57
|
+
restored = ErrorEntry.from_dict(entry.to_dict())
|
|
58
|
+
|
|
59
|
+
assert restored.entry_id == entry.entry_id
|
|
60
|
+
assert restored.operation_type == "llm_call"
|
|
61
|
+
assert restored.payload == {"model": "llama3", "prompt": "hello"}
|
|
62
|
+
assert restored.error_message == "timeout"
|
|
63
|
+
assert restored.retry_count == 2
|
|
64
|
+
assert restored.next_retry_at == entry.next_retry_at
|
|
65
|
+
assert restored.status == ErrorStatus.PENDING
|
|
66
|
+
|
|
67
|
+
def test_defaults(self) -> None:
|
|
68
|
+
"""entry_id and created_at are auto-generated when omitted."""
|
|
69
|
+
entry = ErrorEntry(
|
|
70
|
+
operation_type="sync", payload={}, error_message="network error"
|
|
71
|
+
)
|
|
72
|
+
assert len(entry.entry_id) == 32 # uuid4 hex
|
|
73
|
+
assert "T" in entry.created_at # ISO-8601
|
|
74
|
+
assert entry.retry_count == 0
|
|
75
|
+
assert entry.status == ErrorStatus.PENDING
|
|
76
|
+
|
|
77
|
+
def test_repr(self) -> None:
|
|
78
|
+
"""__repr__ includes type, retry count, and status."""
|
|
79
|
+
entry = ErrorEntry("message_send", {}, "refused")
|
|
80
|
+
r = repr(entry)
|
|
81
|
+
assert "message_send" in r
|
|
82
|
+
assert "retries=0" in r
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# ErrorQueue — basic operations
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestErrorQueueBasic:
|
|
91
|
+
"""Tests for enqueue, list, and persistence."""
|
|
92
|
+
|
|
93
|
+
def test_enqueue_creates_entry(self, queue: ErrorQueue) -> None:
|
|
94
|
+
"""enqueue() adds an entry and returns it."""
|
|
95
|
+
entry = queue.enqueue("llm_call", {"model": "grok"}, "500 error")
|
|
96
|
+
|
|
97
|
+
assert entry.operation_type == "llm_call"
|
|
98
|
+
assert entry.status == ErrorStatus.PENDING
|
|
99
|
+
assert entry.retry_count == 0
|
|
100
|
+
|
|
101
|
+
def test_enqueue_persists_to_disk(self, queue: ErrorQueue, queue_path: Path) -> None:
|
|
102
|
+
"""enqueue() writes JSON to the configured path."""
|
|
103
|
+
queue.enqueue("sync", {}, "network timeout")
|
|
104
|
+
|
|
105
|
+
assert queue_path.exists()
|
|
106
|
+
data = json.loads(queue_path.read_text())
|
|
107
|
+
assert len(data) == 1
|
|
108
|
+
assert data[0]["operation_type"] == "sync"
|
|
109
|
+
|
|
110
|
+
def test_list_returns_newest_first(self, queue: ErrorQueue) -> None:
|
|
111
|
+
"""list_entries() returns entries sorted newest-first."""
|
|
112
|
+
e1 = queue.enqueue("llm_call", {}, "err1")
|
|
113
|
+
e2 = queue.enqueue("message_send", {}, "err2")
|
|
114
|
+
|
|
115
|
+
entries = queue.list_entries()
|
|
116
|
+
ids = [e.entry_id for e in entries]
|
|
117
|
+
assert ids.index(e2.entry_id) < ids.index(e1.entry_id)
|
|
118
|
+
|
|
119
|
+
def test_list_excludes_resolved_by_default(self, queue: ErrorQueue) -> None:
|
|
120
|
+
"""list_entries() hides resolved entries unless include_resolved=True."""
|
|
121
|
+
entry = queue.enqueue("llm_call", {}, "err")
|
|
122
|
+
# Force-resolve by marking directly in JSON
|
|
123
|
+
entries = queue._load()
|
|
124
|
+
entries[0].status = ErrorStatus.RESOLVED
|
|
125
|
+
queue._save(entries)
|
|
126
|
+
|
|
127
|
+
assert queue.list_entries() == []
|
|
128
|
+
assert len(queue.list_entries(include_resolved=True)) == 1
|
|
129
|
+
|
|
130
|
+
def test_list_filter_by_status(self, queue: ErrorQueue) -> None:
|
|
131
|
+
"""list_entries(status=...) filters correctly."""
|
|
132
|
+
queue.enqueue("llm_call", {}, "err")
|
|
133
|
+
entries = queue._load()
|
|
134
|
+
entries[0].status = ErrorStatus.EXHAUSTED
|
|
135
|
+
queue._save(entries)
|
|
136
|
+
|
|
137
|
+
assert len(queue.list_entries(status="exhausted")) == 1
|
|
138
|
+
assert queue.list_entries(status="pending") == []
|
|
139
|
+
|
|
140
|
+
def test_queue_survives_reload(self, queue_path: Path) -> None:
|
|
141
|
+
"""Data persists across separate ErrorQueue instances."""
|
|
142
|
+
q1 = ErrorQueue(path=queue_path, base_backoff=0)
|
|
143
|
+
q1.enqueue("sync", {"x": 1}, "disk full")
|
|
144
|
+
|
|
145
|
+
q2 = ErrorQueue(path=queue_path, base_backoff=0)
|
|
146
|
+
entries = q2.list_entries()
|
|
147
|
+
assert len(entries) == 1
|
|
148
|
+
assert entries[0].payload == {"x": 1}
|
|
149
|
+
|
|
150
|
+
def test_empty_queue_when_file_missing(self, tmp_path: Path) -> None:
|
|
151
|
+
"""list_entries() returns [] when queue file does not exist."""
|
|
152
|
+
q = ErrorQueue(path=tmp_path / "nonexistent.json")
|
|
153
|
+
assert q.list_entries() == []
|
|
154
|
+
|
|
155
|
+
def test_corrupt_file_returns_empty(self, queue_path: Path) -> None:
|
|
156
|
+
"""A corrupt JSON file is treated as an empty queue."""
|
|
157
|
+
queue_path.write_text("NOT JSON", encoding="utf-8")
|
|
158
|
+
q = ErrorQueue(path=queue_path)
|
|
159
|
+
assert q.list_entries() == []
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
# ErrorQueue — retry logic
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class TestErrorQueueRetry:
|
|
168
|
+
"""Tests for retry and exponential backoff."""
|
|
169
|
+
|
|
170
|
+
def test_retry_success_marks_resolved(self, queue: ErrorQueue) -> None:
|
|
171
|
+
"""A successful retry marks the entry resolved."""
|
|
172
|
+
entry = queue.enqueue("llm_call", {}, "timeout")
|
|
173
|
+
|
|
174
|
+
result = queue.retry(entry.entry_id, handler=lambda e: True)
|
|
175
|
+
|
|
176
|
+
assert result is True
|
|
177
|
+
loaded = queue._load()
|
|
178
|
+
assert loaded[0].status == ErrorStatus.RESOLVED
|
|
179
|
+
|
|
180
|
+
def test_retry_failure_increments_count(self, queue: ErrorQueue) -> None:
|
|
181
|
+
"""A failed retry increments retry_count and stays PENDING."""
|
|
182
|
+
entry = queue.enqueue("message_send", {}, "refused")
|
|
183
|
+
|
|
184
|
+
queue.retry(entry.entry_id, handler=lambda e: False)
|
|
185
|
+
|
|
186
|
+
loaded = queue._load()
|
|
187
|
+
assert loaded[0].retry_count == 1
|
|
188
|
+
assert loaded[0].status == ErrorStatus.PENDING
|
|
189
|
+
|
|
190
|
+
def test_retry_exhausted_after_max_retries(self, queue: ErrorQueue) -> None:
|
|
191
|
+
"""After max_retries failed attempts the entry is EXHAUSTED."""
|
|
192
|
+
entry = queue.enqueue("sync", {}, "server down")
|
|
193
|
+
|
|
194
|
+
for _ in range(queue._max_retries):
|
|
195
|
+
queue.retry(entry.entry_id, handler=lambda e: False)
|
|
196
|
+
|
|
197
|
+
loaded = queue._load()
|
|
198
|
+
assert loaded[0].status == ErrorStatus.EXHAUSTED
|
|
199
|
+
assert loaded[0].next_retry_at is None
|
|
200
|
+
|
|
201
|
+
def test_exhausted_entry_skips_retry(self, queue: ErrorQueue) -> None:
|
|
202
|
+
"""Retrying an exhausted entry returns False immediately."""
|
|
203
|
+
entry = queue.enqueue("llm_call", {}, "404")
|
|
204
|
+
entries = queue._load()
|
|
205
|
+
entries[0].status = ErrorStatus.EXHAUSTED
|
|
206
|
+
queue._save(entries)
|
|
207
|
+
|
|
208
|
+
result = queue.retry(entry.entry_id, handler=lambda e: True)
|
|
209
|
+
assert result is False
|
|
210
|
+
|
|
211
|
+
def test_retry_unknown_id_returns_false(self, queue: ErrorQueue) -> None:
|
|
212
|
+
"""Retrying an unknown entry_id returns False."""
|
|
213
|
+
result = queue.retry("deadbeef" * 4, handler=lambda e: True)
|
|
214
|
+
assert result is False
|
|
215
|
+
|
|
216
|
+
def test_backoff_increases_with_attempts(self) -> None:
|
|
217
|
+
"""_backoff_ts produces later timestamps for higher attempt numbers."""
|
|
218
|
+
t0 = _backoff_ts(0, base=10)
|
|
219
|
+
t1 = _backoff_ts(1, base=10)
|
|
220
|
+
t2 = _backoff_ts(2, base=10)
|
|
221
|
+
assert t0 < t1 < t2
|
|
222
|
+
|
|
223
|
+
def test_retry_all_due_processes_only_due(self, queue_path: Path) -> None:
|
|
224
|
+
"""retry_all_due() skips entries whose next_retry_at is in the future."""
|
|
225
|
+
q = ErrorQueue(path=queue_path, base_backoff=0)
|
|
226
|
+
past = (datetime.now(timezone.utc) - timedelta(seconds=5)).isoformat()
|
|
227
|
+
future = (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat()
|
|
228
|
+
|
|
229
|
+
# Create both entries then manually set their next_retry_at
|
|
230
|
+
e_due = q.enqueue("llm_call", {}, "err")
|
|
231
|
+
e_future = q.enqueue("sync", {}, "err2")
|
|
232
|
+
|
|
233
|
+
entries = q._load()
|
|
234
|
+
for e in entries:
|
|
235
|
+
if e.entry_id == e_due.entry_id:
|
|
236
|
+
e.next_retry_at = past
|
|
237
|
+
else:
|
|
238
|
+
e.next_retry_at = future
|
|
239
|
+
q._save(entries)
|
|
240
|
+
|
|
241
|
+
called: list[str] = []
|
|
242
|
+
|
|
243
|
+
def handler(entry: ErrorEntry) -> bool:
|
|
244
|
+
called.append(entry.entry_id)
|
|
245
|
+
return True
|
|
246
|
+
|
|
247
|
+
q.retry_all_due(handler=handler)
|
|
248
|
+
assert e_due.entry_id in called
|
|
249
|
+
assert e_future.entry_id not in called
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
# ErrorQueue — remove / clear
|
|
254
|
+
# ---------------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class TestErrorQueueClear:
|
|
258
|
+
"""Tests for remove and clear_all."""
|
|
259
|
+
|
|
260
|
+
def test_remove_existing_entry(self, queue: ErrorQueue) -> None:
|
|
261
|
+
"""remove() deletes a specific entry and returns True."""
|
|
262
|
+
entry = queue.enqueue("sync", {}, "err")
|
|
263
|
+
result = queue.remove(entry.entry_id)
|
|
264
|
+
assert result is True
|
|
265
|
+
assert queue.list_entries() == []
|
|
266
|
+
|
|
267
|
+
def test_remove_nonexistent_returns_false(self, queue: ErrorQueue) -> None:
|
|
268
|
+
"""remove() returns False for an unknown entry_id."""
|
|
269
|
+
assert queue.remove("no-such-id") is False
|
|
270
|
+
|
|
271
|
+
def test_clear_all(self, queue: ErrorQueue) -> None:
|
|
272
|
+
"""clear_all() removes every entry and returns the count."""
|
|
273
|
+
queue.enqueue("llm_call", {}, "err1")
|
|
274
|
+
queue.enqueue("sync", {}, "err2")
|
|
275
|
+
|
|
276
|
+
removed = queue.clear_all()
|
|
277
|
+
assert removed == 2
|
|
278
|
+
assert queue.list_entries() == []
|
|
279
|
+
|
|
280
|
+
def test_clear_by_status(self, queue: ErrorQueue) -> None:
|
|
281
|
+
"""clear_all(status=...) only removes matching entries."""
|
|
282
|
+
e1 = queue.enqueue("llm_call", {}, "err1")
|
|
283
|
+
e2 = queue.enqueue("sync", {}, "err2")
|
|
284
|
+
|
|
285
|
+
entries = queue._load()
|
|
286
|
+
for e in entries:
|
|
287
|
+
if e.entry_id == e1.entry_id:
|
|
288
|
+
e.status = ErrorStatus.EXHAUSTED
|
|
289
|
+
queue._save(entries)
|
|
290
|
+
|
|
291
|
+
removed = queue.clear_all(status=ErrorStatus.EXHAUSTED)
|
|
292
|
+
assert removed == 1
|
|
293
|
+
|
|
294
|
+
remaining = queue.list_entries(include_resolved=True)
|
|
295
|
+
assert len(remaining) == 1
|
|
296
|
+
assert remaining[0].entry_id == e2.entry_id
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
# ErrorQueue — stats
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class TestErrorQueueStats:
|
|
305
|
+
"""Tests for the stats() summary method."""
|
|
306
|
+
|
|
307
|
+
def test_stats_counts_correctly(self, queue: ErrorQueue) -> None:
|
|
308
|
+
"""stats() returns accurate counts per status."""
|
|
309
|
+
queue.enqueue("llm_call", {}, "err1")
|
|
310
|
+
queue.enqueue("sync", {}, "err2")
|
|
311
|
+
|
|
312
|
+
entries = queue._load()
|
|
313
|
+
entries[0].status = ErrorStatus.EXHAUSTED
|
|
314
|
+
queue._save(entries)
|
|
315
|
+
|
|
316
|
+
s = queue.stats()
|
|
317
|
+
assert s["total"] == 2
|
|
318
|
+
assert s["exhausted"] == 1
|
|
319
|
+
assert s["pending"] == 1
|
|
320
|
+
|
|
321
|
+
def test_stats_empty_queue(self, queue: ErrorQueue) -> None:
|
|
322
|
+
"""stats() on an empty queue returns zeros."""
|
|
323
|
+
s = queue.stats()
|
|
324
|
+
assert s["total"] == 0
|
|
325
|
+
for status in ErrorStatus:
|
|
326
|
+
assert s.get(status.value, 0) == 0
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# ---------------------------------------------------------------------------
|
|
330
|
+
# CLI smoke-tests
|
|
331
|
+
# ---------------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class TestErrorQueueCLI:
|
|
335
|
+
"""Smoke-tests for the `skcapstone errors` CLI commands."""
|
|
336
|
+
|
|
337
|
+
def test_cli_list_empty(self, tmp_path: Path) -> None:
|
|
338
|
+
"""errors list on empty queue exits 0 and prints 'empty'."""
|
|
339
|
+
from click.testing import CliRunner
|
|
340
|
+
from skcapstone.cli import main
|
|
341
|
+
|
|
342
|
+
runner = CliRunner()
|
|
343
|
+
result = runner.invoke(
|
|
344
|
+
main,
|
|
345
|
+
["errors", "list", "--path", str(tmp_path / "eq.json")],
|
|
346
|
+
)
|
|
347
|
+
assert result.exit_code == 0
|
|
348
|
+
assert "empty" in result.output.lower() or "0 total" in result.output.lower() or "Queue" in result.output
|
|
349
|
+
|
|
350
|
+
def test_cli_list_shows_entry(self, tmp_path: Path) -> None:
|
|
351
|
+
"""errors list shows an enqueued entry."""
|
|
352
|
+
from click.testing import CliRunner
|
|
353
|
+
from skcapstone.cli import main
|
|
354
|
+
|
|
355
|
+
q_path = tmp_path / "eq.json"
|
|
356
|
+
q = ErrorQueue(path=q_path, base_backoff=0)
|
|
357
|
+
q.enqueue("llm_call", {"model": "grok"}, "test error msg")
|
|
358
|
+
|
|
359
|
+
runner = CliRunner()
|
|
360
|
+
result = runner.invoke(main, ["errors", "list", "--path", str(q_path)])
|
|
361
|
+
assert result.exit_code == 0
|
|
362
|
+
assert "llm_call" in result.output
|
|
363
|
+
|
|
364
|
+
def test_cli_stats(self, tmp_path: Path) -> None:
|
|
365
|
+
"""errors stats exits 0 and shows totals panel."""
|
|
366
|
+
from click.testing import CliRunner
|
|
367
|
+
from skcapstone.cli import main
|
|
368
|
+
|
|
369
|
+
q_path = tmp_path / "eq.json"
|
|
370
|
+
q = ErrorQueue(path=q_path, base_backoff=0)
|
|
371
|
+
q.enqueue("sync", {}, "err")
|
|
372
|
+
|
|
373
|
+
runner = CliRunner()
|
|
374
|
+
result = runner.invoke(main, ["errors", "stats", "--path", str(q_path)])
|
|
375
|
+
assert result.exit_code == 0
|
|
376
|
+
assert "Total" in result.output or "1" in result.output
|
|
377
|
+
|
|
378
|
+
def test_cli_clear_all_with_force(self, tmp_path: Path) -> None:
|
|
379
|
+
"""errors clear --all --force removes all entries."""
|
|
380
|
+
from click.testing import CliRunner
|
|
381
|
+
from skcapstone.cli import main
|
|
382
|
+
|
|
383
|
+
q_path = tmp_path / "eq.json"
|
|
384
|
+
q = ErrorQueue(path=q_path, base_backoff=0)
|
|
385
|
+
q.enqueue("llm_call", {}, "err1")
|
|
386
|
+
q.enqueue("sync", {}, "err2")
|
|
387
|
+
|
|
388
|
+
runner = CliRunner()
|
|
389
|
+
result = runner.invoke(
|
|
390
|
+
main, ["errors", "clear", "--all", "--force", "--path", str(q_path)]
|
|
391
|
+
)
|
|
392
|
+
assert result.exit_code == 0
|
|
393
|
+
assert q.list_entries() == []
|
|
394
|
+
|
|
395
|
+
def test_cli_retry_no_args_fails(self, tmp_path: Path) -> None:
|
|
396
|
+
"""errors retry without ENTRY_ID and without --all exits non-zero."""
|
|
397
|
+
from click.testing import CliRunner
|
|
398
|
+
from skcapstone.cli import main
|
|
399
|
+
|
|
400
|
+
runner = CliRunner()
|
|
401
|
+
result = runner.invoke(
|
|
402
|
+
main, ["errors", "retry", "--path", str(tmp_path / "eq.json")]
|
|
403
|
+
)
|
|
404
|
+
assert result.exit_code != 0 or "ENTRY_ID" in result.output or "all" in result.output
|