@smilintux/skcapstone 0.10.0 → 0.12.5
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 +10 -4
- package/.github/workflows/ci.yml +2 -2
- package/.github/workflows/publish.yml +9 -2
- package/.openclaw-workspace.json +2 -2
- package/CLAUDE.md +37 -0
- package/MISSION.md +17 -2
- package/README.md +282 -3
- package/docker/Dockerfile +7 -7
- package/docker/compose-templates/dev-team.yml +12 -12
- package/docker/compose-templates/mini-team.yml +9 -9
- package/docker/compose-templates/ops-team.yml +10 -10
- package/docker/compose-templates/research-team.yml +10 -10
- package/docker/entrypoint.sh +4 -4
- package/docs/ADR-optional-integration-backbone.md +181 -0
- package/docs/ARCHITECTURE.md +186 -43
- package/docs/BOND_WITH_GROK.md +6 -6
- package/docs/CUSTOM_AGENT.md +123 -30
- package/docs/DREAMING.md +70 -0
- package/docs/GETTING_STARTED.md +7 -7
- package/docs/QUICKSTART.md +10 -6
- package/docs/SKJOULE_ARCHITECTURE.md +3 -3
- package/docs/SOUL_SWAPPER.md +5 -5
- package/docs/hammertime-audit.md +402 -0
- package/docs/sk-integration-HANDOFF.md +117 -0
- package/docs/skscheduler.md +155 -0
- package/docs/superpowers/examples/jobs.yaml +31 -0
- package/docs/superpowers/plans/2026-06-08-skscheduler.md +1265 -0
- package/docs/superpowers/specs/2026-06-08-skscheduler-design.md +186 -0
- package/examples/custom-bond-template.json +1 -1
- package/examples/grok-feb.json +1 -1
- package/examples/queen-ava-feb.json +1 -1
- package/launchd/{com.skcapstone.skcomm-heartbeat.plist → com.skcapstone.skcomms-heartbeat.plist} +4 -4
- package/launchd/{com.skcapstone.skcomm-queue-drain.plist → com.skcapstone.skcomms-queue-drain.plist} +4 -4
- package/launchd/install-launchd.sh +6 -6
- package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/index.ts +3 -2
- package/package.json +1 -1
- package/pyproject.toml +16 -10
- package/scripts/archive-sessions.sh +7 -0
- package/scripts/check-updates.py +4 -4
- package/scripts/install-bundle.sh +8 -8
- package/scripts/install.ps1 +12 -11
- package/scripts/install.sh +159 -5
- package/scripts/model-fallback-monitor.sh +102 -0
- package/scripts/nvidia-proxy.mjs +78 -26
- package/scripts/refresh-anthropic-token.sh +172 -0
- package/scripts/release.sh +98 -0
- package/scripts/session-to-memory.py +219 -0
- package/scripts/skgateway.mjs +3 -3
- package/scripts/telegram-catchup-all.sh +12 -1
- package/scripts/verify_install.sh +2 -2
- package/scripts/wargov-ufo-capture/README.md +43 -0
- package/scripts/wargov-ufo-capture/cdp_capture_release2.py +273 -0
- package/scripts/wargov-ufo-capture/cdp_capture_splc_doj.py +246 -0
- package/scripts/wargov-ufo-capture/cdp_finish.py +271 -0
- package/scripts/wargov-ufo-capture/cdp_probe.py +188 -0
- package/scripts/wargov-ufo-capture/cdp_splc_pressrelease.py +101 -0
- package/scripts/wargov-ufo-capture/parse_csv.py +95 -0
- package/scripts/wargov-ufo-capture/pull_dvids.sh +107 -0
- package/scripts/watch-anthropic-token.sh +212 -0
- package/scripts/windows/install-tasks.ps1 +7 -7
- package/scripts/windows/skcapstone-task.xml +1 -1
- package/src/skcapstone/__init__.py +45 -3
- package/src/skcapstone/_cli_monolith.py +20 -15
- package/src/skcapstone/activity.py +5 -1
- package/src/skcapstone/agent_card.py +3 -2
- package/src/skcapstone/api.py +41 -40
- package/src/skcapstone/auction.py +14 -11
- package/src/skcapstone/backup.py +2 -1
- package/src/skcapstone/blueprint_registry.py +4 -3
- package/src/skcapstone/brain_first.py +238 -0
- package/src/skcapstone/changelog.py +1 -1
- package/src/skcapstone/chat.py +22 -17
- package/src/skcapstone/cli/__init__.py +9 -1
- package/src/skcapstone/cli/_common.py +1 -0
- package/src/skcapstone/cli/agents_spawner.py +5 -2
- package/src/skcapstone/cli/alerts.py +25 -4
- package/src/skcapstone/cli/bench.py +15 -15
- package/src/skcapstone/cli/chat.py +7 -4
- package/src/skcapstone/cli/consciousness.py +5 -2
- package/src/skcapstone/cli/context_cmd.py +18 -4
- package/src/skcapstone/cli/daemon.py +11 -7
- package/src/skcapstone/cli/gtd.py +26 -1
- package/src/skcapstone/cli/housekeeping.py +3 -3
- package/src/skcapstone/cli/identity_cmd.py +378 -0
- package/src/skcapstone/cli/joule_cmd.py +7 -3
- package/src/skcapstone/cli/memory.py +8 -6
- package/src/skcapstone/cli/peers_dir.py +1 -1
- package/src/skcapstone/cli/register_cmd.py +29 -3
- package/src/skcapstone/cli/scheduler_cmd.py +167 -0
- package/src/skcapstone/cli/session.py +25 -0
- package/src/skcapstone/cli/setup.py +96 -29
- package/src/skcapstone/cli/shell_cmd.py +53 -1
- package/src/skcapstone/cli/skills_cmd.py +2 -2
- package/src/skcapstone/cli/soul.py +8 -5
- package/src/skcapstone/cli/status.py +37 -11
- package/src/skcapstone/cli/telegram.py +21 -0
- package/src/skcapstone/cli/test_cmd.py +5 -5
- package/src/skcapstone/cli/test_connection.py +2 -2
- package/src/skcapstone/cli/upgrade_cmd.py +23 -14
- package/src/skcapstone/cli/version_cmd.py +1 -1
- package/src/skcapstone/cli/watch_cmd.py +9 -6
- package/src/skcapstone/cloud9_bridge.py +14 -14
- package/src/skcapstone/codex_setup.py +255 -0
- package/src/skcapstone/config_validator.py +7 -4
- package/src/skcapstone/consciousness_config.py +5 -1
- package/src/skcapstone/consciousness_loop.py +313 -273
- package/src/skcapstone/context_loader.py +121 -0
- package/src/skcapstone/coord_federation.py +2 -1
- package/src/skcapstone/coordination.py +23 -6
- package/src/skcapstone/crush_integration.py +2 -1
- package/src/skcapstone/daemon.py +132 -77
- package/src/skcapstone/dashboard.py +10 -10
- package/src/skcapstone/data/sk-agent-picker.sh +421 -0
- package/src/skcapstone/data/systemd/skcapstone-api.socket +9 -0
- package/src/skcapstone/data/systemd/skcapstone-memory-compress.service +18 -0
- package/src/skcapstone/data/systemd/skcapstone-memory-compress.timer +11 -0
- package/src/skcapstone/data/systemd/skcapstone.service +37 -0
- package/src/skcapstone/data/systemd/skcapstone@.service +50 -0
- package/src/skcapstone/data/systemd/skcomms-heartbeat.service +18 -0
- package/{systemd/skcomm-heartbeat.timer → src/skcapstone/data/systemd/skcomms-heartbeat.timer} +2 -2
- package/src/skcapstone/data/systemd/skcomms-queue-drain.service +17 -0
- package/{systemd/skcomm-queue-drain.timer → src/skcapstone/data/systemd/skcomms-queue-drain.timer} +2 -2
- package/src/skcapstone/defaults/claude/CLAUDE.md +67 -0
- package/src/skcapstone/defaults/claude/settings.json +74 -0
- package/src/skcapstone/defaults/lumina/config/claude-hooks.md +57 -0
- package/src/skcapstone/defaults/lumina/config/skgraph.yaml +55 -10
- package/src/skcapstone/defaults/lumina/config/skmemory.yaml +79 -13
- package/src/skcapstone/defaults/lumina/config/skvector.yaml +60 -9
- package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +2 -2
- package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +2 -2
- package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +9 -9
- package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +2 -2
- package/src/skcapstone/defaults/unhinged.json +13 -0
- package/src/skcapstone/discovery.py +43 -20
- package/src/skcapstone/doctor.py +941 -22
- package/src/skcapstone/dreaming.py +1183 -109
- package/src/skcapstone/emotion_tracker.py +2 -2
- package/src/skcapstone/export.py +4 -3
- package/src/skcapstone/fuse_mount.py +14 -12
- package/src/skcapstone/gui_installer.py +2 -2
- package/src/skcapstone/heartbeat.py +1 -1
- package/src/skcapstone/housekeeping.py +14 -14
- package/src/skcapstone/install_wizard.py +209 -7
- package/src/skcapstone/itil.py +13 -4
- package/src/skcapstone/kms_scheduler.py +10 -8
- package/src/skcapstone/launchd.py +19 -19
- package/src/skcapstone/mcp_launcher.py +15 -1
- package/src/skcapstone/mcp_server.py +83 -49
- package/src/skcapstone/mcp_tools/__init__.py +2 -0
- package/src/skcapstone/mcp_tools/_helpers.py +2 -2
- package/src/skcapstone/mcp_tools/ansible_tools.py +7 -4
- package/src/skcapstone/mcp_tools/brain_first_tools.py +90 -0
- package/src/skcapstone/mcp_tools/capauth_tools.py +7 -4
- package/src/skcapstone/mcp_tools/comm_tools.py +10 -10
- package/src/skcapstone/mcp_tools/coord_tools.py +8 -4
- package/src/skcapstone/mcp_tools/did_tools.py +11 -8
- package/src/skcapstone/mcp_tools/gtd_tools.py +4 -4
- package/src/skcapstone/mcp_tools/memory_tools.py +6 -2
- package/src/skcapstone/mcp_tools/notification_tools.py +22 -6
- package/src/skcapstone/mcp_tools/{skcomm_tools.py → skcomms_tools.py} +14 -14
- package/src/skcapstone/mcp_tools/soul_tools.py +8 -2
- package/src/skcapstone/mdns_discovery.py +2 -2
- package/src/skcapstone/memory_curator.py +1 -1
- package/src/skcapstone/memory_engine.py +10 -3
- package/src/skcapstone/metrics.py +30 -16
- package/src/skcapstone/migrate_memories.py +4 -3
- package/src/skcapstone/migrate_multi_agent.py +8 -7
- package/src/skcapstone/models.py +47 -5
- package/src/skcapstone/notifications.py +42 -18
- package/src/skcapstone/onboard.py +875 -121
- package/src/skcapstone/operator_link.py +170 -0
- package/src/skcapstone/peer_directory.py +4 -4
- package/src/skcapstone/peers.py +19 -19
- package/src/skcapstone/pillars/__init__.py +7 -5
- package/src/skcapstone/pillars/consciousness.py +191 -0
- package/src/skcapstone/pillars/identity.py +51 -7
- package/src/skcapstone/pillars/memory.py +9 -3
- package/src/skcapstone/pillars/sync.py +2 -2
- package/src/skcapstone/preflight.py +3 -3
- package/src/skcapstone/providers/docker.py +28 -28
- package/src/skcapstone/register.py +6 -6
- package/src/skcapstone/registry_client.py +5 -4
- package/src/skcapstone/runtime.py +14 -3
- package/src/skcapstone/scheduled_tasks.py +254 -19
- package/src/skcapstone/scheduler_jobs.py +456 -0
- package/src/skcapstone/scheduler_runner.py +239 -0
- package/src/skcapstone/scheduler_state.py +162 -0
- package/src/skcapstone/sdk.py +310 -0
- package/src/skcapstone/service_health.py +279 -39
- package/src/skcapstone/session_briefing.py +108 -0
- package/src/skcapstone/session_capture.py +1 -1
- package/src/skcapstone/shell.py +7 -1
- package/src/skcapstone/soul.py +3 -1
- package/src/skcapstone/soul_switch.py +3 -1
- package/src/skcapstone/summary.py +6 -6
- package/src/skcapstone/sync_engine.py +15 -15
- package/src/skcapstone/sync_watcher.py +2 -2
- package/src/skcapstone/systemd.py +55 -21
- package/src/skcapstone/team_comms.py +8 -8
- package/src/skcapstone/team_engine.py +1 -1
- package/src/skcapstone/testrunner.py +3 -3
- package/src/skcapstone/trust_graph.py +40 -5
- package/src/skcapstone/unified_search.py +15 -6
- package/src/skcapstone/uninstall_wizard.py +11 -3
- package/src/skcapstone/version_check.py +8 -4
- package/src/skcapstone/warmth_anchor.py +4 -2
- package/src/skcapstone/whoami.py +4 -4
- package/systemd/skcapstone.service +4 -6
- package/systemd/skcapstone@.service +7 -8
- package/systemd/skcomms-heartbeat.service +21 -0
- package/systemd/skcomms-heartbeat.timer +12 -0
- package/systemd/skcomms-queue-drain.service +17 -0
- package/systemd/skcomms-queue-drain.timer +12 -0
- package/tests/conftest.py +39 -0
- package/tests/integration/test_consciousness_e2e.py +39 -39
- package/tests/test_agent_card.py +1 -1
- package/tests/test_agent_home_scaffold.py +34 -0
- package/tests/test_alerts_consumer_topics.py +27 -0
- package/tests/test_backup.py +2 -1
- package/tests/test_chat.py +6 -6
- package/tests/test_claude_md.py +2 -2
- package/tests/test_cli_skills.py +10 -10
- package/tests/test_cli_test_cmd.py +4 -4
- package/tests/test_cli_test_connection.py +1 -1
- package/tests/test_cloud9_bridge.py +6 -6
- package/tests/test_consciousness_e2e.py +1 -1
- package/tests/test_consciousness_loop.py +10 -10
- package/tests/test_coordination.py +25 -0
- package/tests/test_cross_package.py +21 -21
- package/tests/test_daemon.py +4 -4
- package/tests/test_daemon_shutdown.py +1 -1
- package/tests/test_docker_provider.py +29 -29
- package/tests/test_doctor.py +400 -0
- package/tests/test_doctor_skscheduler.py +50 -0
- package/tests/test_dreaming_engine.py +147 -0
- package/tests/test_dreaming_gtd_capture.py +35 -0
- package/tests/test_e2e_automated.py +8 -5
- package/tests/test_fuse_mount.py +10 -10
- package/tests/test_gtd_brief.py +46 -0
- package/tests/test_gtd_malformed_tolerance.py +31 -0
- package/tests/test_housekeeping.py +15 -15
- package/tests/test_identity_migrate.py +251 -0
- package/tests/test_integration_backbone.py +598 -0
- package/tests/test_itil_gtd_lifecycle.py +37 -0
- package/tests/test_jobs_dropins.py +84 -0
- package/tests/test_mcp_server.py +82 -37
- package/tests/test_models.py +48 -4
- package/tests/test_multi_agent.py +31 -29
- package/tests/test_notifications.py +122 -32
- package/tests/test_onboard.py +63 -75
- package/tests/test_operator_link.py +78 -0
- package/tests/test_peers.py +14 -14
- package/tests/test_pillars.py +98 -0
- package/tests/test_preflight.py +3 -3
- package/tests/test_runtime.py +21 -0
- package/tests/test_scheduled_tasks.py +11 -6
- package/tests/test_scheduler_cli.py +47 -0
- package/tests/test_scheduler_features.py +133 -0
- package/tests/test_scheduler_integration.py +87 -0
- package/tests/test_scheduler_jobs.py +155 -0
- package/tests/test_scheduler_runner.py +64 -0
- package/tests/test_scheduler_state.py +57 -0
- package/tests/test_sdk.py +70 -0
- package/tests/test_service_health_incidents.py +34 -0
- package/tests/test_service_registry.py +52 -0
- package/tests/test_session_briefing.py +130 -0
- package/tests/test_snapshots.py +4 -4
- package/tests/test_sync_pipeline.py +26 -26
- package/tests/test_team_comms.py +2 -2
- package/tests/test_testrunner.py +2 -2
- package/tests/test_trust_graph.py +18 -0
- package/tests/test_unified_search.py +2 -2
- package/tests/test_version_check.py +10 -0
- package/tests/test_version_cmd.py +8 -8
- package/tests/test_whoami.py +1 -1
- package/systemd/skcomm-heartbeat.service +0 -18
- package/systemd/skcomm-queue-drain.service +0 -17
- /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/package.json +0 -0
- /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/openclaw.plugin.json +0 -0
package/tests/test_peers.py
CHANGED
|
@@ -6,7 +6,7 @@ Covers:
|
|
|
6
6
|
- list_peers (empty, with peers)
|
|
7
7
|
- get_peer / remove_peer
|
|
8
8
|
- PeerRecord model
|
|
9
|
-
-
|
|
9
|
+
- SKComms peer file creation
|
|
10
10
|
- CLI commands (add, list, remove, show)
|
|
11
11
|
"""
|
|
12
12
|
|
|
@@ -30,9 +30,9 @@ from skcapstone.peers import (
|
|
|
30
30
|
|
|
31
31
|
@pytest.fixture
|
|
32
32
|
def homes(tmp_path):
|
|
33
|
-
"""Create skcapstone and
|
|
33
|
+
"""Create skcapstone and skcomms home directories."""
|
|
34
34
|
sk = tmp_path / ".skcapstone"
|
|
35
|
-
sc = tmp_path / ".
|
|
35
|
+
sc = tmp_path / ".skcomms"
|
|
36
36
|
sk.mkdir()
|
|
37
37
|
sc.mkdir()
|
|
38
38
|
return sk, sc
|
|
@@ -87,7 +87,7 @@ class TestAddPeerFromCard:
|
|
|
87
87
|
def test_add_from_card(self, card_file, homes):
|
|
88
88
|
"""Card import creates peer records in both registries."""
|
|
89
89
|
sk, sc = homes
|
|
90
|
-
peer = add_peer_from_card(card_file, skcapstone_home=sk,
|
|
90
|
+
peer = add_peer_from_card(card_file, skcapstone_home=sk, skcomms_home=sc)
|
|
91
91
|
|
|
92
92
|
assert peer.name == "Lumina"
|
|
93
93
|
assert peer.fingerprint == "AABB1122CCDD3344EEFF5566"
|
|
@@ -104,7 +104,7 @@ class TestAddPeerFromCard:
|
|
|
104
104
|
"""Nonexistent card raises FileNotFoundError."""
|
|
105
105
|
sk, sc = homes
|
|
106
106
|
with pytest.raises(FileNotFoundError):
|
|
107
|
-
add_peer_from_card(Path("/nope.json"), skcapstone_home=sk,
|
|
107
|
+
add_peer_from_card(Path("/nope.json"), skcapstone_home=sk, skcomms_home=sc)
|
|
108
108
|
|
|
109
109
|
def test_invalid_json_raises(self, tmp_path, homes):
|
|
110
110
|
"""Invalid JSON raises ValueError."""
|
|
@@ -112,7 +112,7 @@ class TestAddPeerFromCard:
|
|
|
112
112
|
bad = tmp_path / "bad.json"
|
|
113
113
|
bad.write_text("{{{not json")
|
|
114
114
|
with pytest.raises(ValueError):
|
|
115
|
-
add_peer_from_card(bad, skcapstone_home=sk,
|
|
115
|
+
add_peer_from_card(bad, skcapstone_home=sk, skcomms_home=sc)
|
|
116
116
|
|
|
117
117
|
def test_missing_name_raises(self, tmp_path, homes):
|
|
118
118
|
"""Card without name raises ValueError."""
|
|
@@ -120,7 +120,7 @@ class TestAddPeerFromCard:
|
|
|
120
120
|
no_name = tmp_path / "noname.json"
|
|
121
121
|
no_name.write_text(json.dumps({"fingerprint": "123"}))
|
|
122
122
|
with pytest.raises(ValueError, match="name"):
|
|
123
|
-
add_peer_from_card(no_name, skcapstone_home=sk,
|
|
123
|
+
add_peer_from_card(no_name, skcapstone_home=sk, skcomms_home=sc)
|
|
124
124
|
|
|
125
125
|
|
|
126
126
|
class TestAddPeerManual:
|
|
@@ -131,7 +131,7 @@ class TestAddPeerManual:
|
|
|
131
131
|
sk, sc = homes
|
|
132
132
|
peer = add_peer_manual(
|
|
133
133
|
name="Opus", email="opus@smilintux.org",
|
|
134
|
-
skcapstone_home=sk,
|
|
134
|
+
skcapstone_home=sk, skcomms_home=sc,
|
|
135
135
|
)
|
|
136
136
|
assert peer.name == "Opus"
|
|
137
137
|
assert peer.email == "opus@smilintux.org"
|
|
@@ -145,7 +145,7 @@ class TestAddPeerManual:
|
|
|
145
145
|
|
|
146
146
|
peer = add_peer_manual(
|
|
147
147
|
name="Opus", public_key_path=key_file,
|
|
148
|
-
skcapstone_home=sk,
|
|
148
|
+
skcapstone_home=sk, skcomms_home=sc,
|
|
149
149
|
)
|
|
150
150
|
assert peer.public_key != ""
|
|
151
151
|
assert peer.trust_level == "verified"
|
|
@@ -163,7 +163,7 @@ class TestListPeers:
|
|
|
163
163
|
def test_list_with_peers(self, card_file, homes):
|
|
164
164
|
"""Added peers appear in listing."""
|
|
165
165
|
sk, sc = homes
|
|
166
|
-
add_peer_from_card(card_file, skcapstone_home=sk,
|
|
166
|
+
add_peer_from_card(card_file, skcapstone_home=sk, skcomms_home=sc)
|
|
167
167
|
|
|
168
168
|
peers = list_peers(skcapstone_home=sk)
|
|
169
169
|
assert len(peers) == 1
|
|
@@ -176,7 +176,7 @@ class TestGetPeer:
|
|
|
176
176
|
def test_get_existing(self, card_file, homes):
|
|
177
177
|
"""Known peer is returned."""
|
|
178
178
|
sk, sc = homes
|
|
179
|
-
add_peer_from_card(card_file, skcapstone_home=sk,
|
|
179
|
+
add_peer_from_card(card_file, skcapstone_home=sk, skcomms_home=sc)
|
|
180
180
|
|
|
181
181
|
peer = get_peer("Lumina", skcapstone_home=sk)
|
|
182
182
|
assert peer is not None
|
|
@@ -194,16 +194,16 @@ class TestRemovePeer:
|
|
|
194
194
|
def test_remove_existing(self, card_file, homes):
|
|
195
195
|
"""Removing an existing peer cleans up all files."""
|
|
196
196
|
sk, sc = homes
|
|
197
|
-
add_peer_from_card(card_file, skcapstone_home=sk,
|
|
197
|
+
add_peer_from_card(card_file, skcapstone_home=sk, skcomms_home=sc)
|
|
198
198
|
|
|
199
|
-
assert remove_peer("Lumina", skcapstone_home=sk,
|
|
199
|
+
assert remove_peer("Lumina", skcapstone_home=sk, skcomms_home=sc)
|
|
200
200
|
assert not (sk / "peers" / "lumina.json").exists()
|
|
201
201
|
assert not (sc / "peers" / "lumina.yml").exists()
|
|
202
202
|
|
|
203
203
|
def test_remove_unknown(self, homes):
|
|
204
204
|
"""Removing unknown peer returns False."""
|
|
205
205
|
sk, sc = homes
|
|
206
|
-
assert not remove_peer("Nobody", skcapstone_home=sk,
|
|
206
|
+
assert not remove_peer("Nobody", skcapstone_home=sk, skcomms_home=sc)
|
|
207
207
|
|
|
208
208
|
|
|
209
209
|
class TestCLI:
|
package/tests/test_pillars.py
CHANGED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
+
from skcapstone.pillars.consciousness import initialize_consciousness
|
|
8
9
|
from skcapstone.pillars.identity import generate_identity
|
|
9
10
|
from skcapstone.pillars.security import (
|
|
10
11
|
AuditEntry,
|
|
@@ -163,3 +164,100 @@ class TestSecurityPillar:
|
|
|
163
164
|
"""read_audit_log returns empty list when no log exists."""
|
|
164
165
|
entries = read_audit_log(tmp_path / "nonexistent")
|
|
165
166
|
assert entries == []
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class TestConsciousnessPillar:
|
|
170
|
+
"""Tests for consciousness pillar initialization (SKWhisper + SKTrip)."""
|
|
171
|
+
|
|
172
|
+
def test_returns_missing_when_no_skwhisper(self, tmp_agent_home: Path):
|
|
173
|
+
"""initialize_consciousness returns MISSING when no SKWhisper data exists."""
|
|
174
|
+
state = initialize_consciousness(tmp_agent_home)
|
|
175
|
+
assert state.status == PillarStatus.MISSING
|
|
176
|
+
|
|
177
|
+
def test_degraded_with_digested_sessions_no_daemon(self, tmp_agent_home: Path, monkeypatch):
|
|
178
|
+
"""DEGRADED when sessions have been digested but daemon is not running."""
|
|
179
|
+
import os
|
|
180
|
+
agent_name = os.environ.get("SKCAPSTONE_AGENT", "lumina")
|
|
181
|
+
whisper_dir = tmp_agent_home / "agents" / agent_name / "skwhisper"
|
|
182
|
+
whisper_dir.mkdir(parents=True, exist_ok=True)
|
|
183
|
+
|
|
184
|
+
# Write state.json with one digested session
|
|
185
|
+
state_json = whisper_dir / "state.json"
|
|
186
|
+
state_json.write_text(json.dumps({
|
|
187
|
+
"last_digest": "2026-03-25T12:00:00+00:00",
|
|
188
|
+
"sessions": {
|
|
189
|
+
"abc123": {"digested_at": "2026-03-25T12:00:00+00:00"}
|
|
190
|
+
}
|
|
191
|
+
}))
|
|
192
|
+
|
|
193
|
+
# Daemon not running (systemctl will fail in test env)
|
|
194
|
+
state = initialize_consciousness(tmp_agent_home)
|
|
195
|
+
assert state.sessions_digested == 1
|
|
196
|
+
assert state.sessions_pending == 0
|
|
197
|
+
assert state.status in (PillarStatus.DEGRADED, PillarStatus.ACTIVE)
|
|
198
|
+
|
|
199
|
+
def test_whisper_md_age_tracked(self, tmp_agent_home: Path):
|
|
200
|
+
"""whisper.md existence and age are captured correctly."""
|
|
201
|
+
import os
|
|
202
|
+
agent_name = os.environ.get("SKCAPSTONE_AGENT", "lumina")
|
|
203
|
+
whisper_dir = tmp_agent_home / "agents" / agent_name / "skwhisper"
|
|
204
|
+
whisper_dir.mkdir(parents=True, exist_ok=True)
|
|
205
|
+
|
|
206
|
+
whisper_md = whisper_dir / "whisper.md"
|
|
207
|
+
whisper_md.write_text("# Whisper context\n")
|
|
208
|
+
|
|
209
|
+
state = initialize_consciousness(tmp_agent_home)
|
|
210
|
+
assert state.whisper_md == whisper_md
|
|
211
|
+
assert state.whisper_md_age_hours >= 0.0
|
|
212
|
+
|
|
213
|
+
def test_patterns_json_topic_count(self, tmp_agent_home: Path):
|
|
214
|
+
"""topics_tracked reflects the number of topics in patterns.json."""
|
|
215
|
+
import os
|
|
216
|
+
agent_name = os.environ.get("SKCAPSTONE_AGENT", "lumina")
|
|
217
|
+
whisper_dir = tmp_agent_home / "agents" / agent_name / "skwhisper"
|
|
218
|
+
whisper_dir.mkdir(parents=True, exist_ok=True)
|
|
219
|
+
|
|
220
|
+
patterns = whisper_dir / "patterns.json"
|
|
221
|
+
patterns.write_text(json.dumps({
|
|
222
|
+
"topics": {"sovereignty": {}, "memory": {}, "consciousness": {}}
|
|
223
|
+
}))
|
|
224
|
+
|
|
225
|
+
state = initialize_consciousness(tmp_agent_home)
|
|
226
|
+
assert state.topics_tracked == 3
|
|
227
|
+
|
|
228
|
+
def test_active_with_daemon_fresh_whisper_and_patterns(
|
|
229
|
+
self, tmp_agent_home: Path, monkeypatch
|
|
230
|
+
):
|
|
231
|
+
"""Fresh SKWhisper context plus tracked patterns is active when daemon is active."""
|
|
232
|
+
agent_name = "jarvis"
|
|
233
|
+
monkeypatch.setenv("SKCAPSTONE_AGENT", agent_name)
|
|
234
|
+
whisper_dir = tmp_agent_home / "agents" / agent_name / "skwhisper"
|
|
235
|
+
whisper_dir.mkdir(parents=True, exist_ok=True)
|
|
236
|
+
(whisper_dir / "whisper.md").write_text("# Whisper context\n")
|
|
237
|
+
(whisper_dir / "patterns.json").write_text(json.dumps({
|
|
238
|
+
"topics": {"sovereignty": {}, "memory": {}}
|
|
239
|
+
}))
|
|
240
|
+
|
|
241
|
+
class Result:
|
|
242
|
+
stdout = "active\n"
|
|
243
|
+
|
|
244
|
+
monkeypatch.setattr(
|
|
245
|
+
"subprocess.run",
|
|
246
|
+
lambda *args, **kwargs: Result(),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
state = initialize_consciousness(tmp_agent_home)
|
|
250
|
+
|
|
251
|
+
assert state.status == PillarStatus.ACTIVE
|
|
252
|
+
|
|
253
|
+
def test_trip_sessions_counted(self, tmp_agent_home: Path):
|
|
254
|
+
"""trip_sessions counts .json files in the sktrip directory."""
|
|
255
|
+
import os
|
|
256
|
+
agent_name = os.environ.get("SKCAPSTONE_AGENT", "lumina")
|
|
257
|
+
trip_dir = tmp_agent_home / "agents" / agent_name / "sktrip"
|
|
258
|
+
trip_dir.mkdir(parents=True, exist_ok=True)
|
|
259
|
+
(trip_dir / "trip-001.json").write_text("{}")
|
|
260
|
+
(trip_dir / "trip-002.json").write_text("{}")
|
|
261
|
+
|
|
262
|
+
state = initialize_consciousness(tmp_agent_home)
|
|
263
|
+
assert state.trip_sessions == 2
|
package/tests/test_preflight.py
CHANGED
|
@@ -301,15 +301,15 @@ class TestPreflightCheckerPackages:
|
|
|
301
301
|
real_import = builtins.__import__
|
|
302
302
|
|
|
303
303
|
def _mock_import(name, *args, **kwargs):
|
|
304
|
-
if name == "
|
|
305
|
-
raise ImportError("no module named
|
|
304
|
+
if name == "skcomms":
|
|
305
|
+
raise ImportError("no module named skcomms")
|
|
306
306
|
return real_import(name, *args, **kwargs)
|
|
307
307
|
|
|
308
308
|
checker = PreflightChecker(home=tmp_path)
|
|
309
309
|
with patch("builtins.__import__", side_effect=_mock_import):
|
|
310
310
|
result = checker.check_packages()
|
|
311
311
|
assert result.status == "fail"
|
|
312
|
-
assert "
|
|
312
|
+
assert "skcomms" in result.message
|
|
313
313
|
|
|
314
314
|
def test_all_present_ok(self, tmp_path: Path) -> None:
|
|
315
315
|
checker = PreflightChecker(home=tmp_path)
|
package/tests/test_runtime.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
from skcapstone.runtime import AgentRuntime
|
|
@@ -28,6 +29,26 @@ class TestAgentRuntime:
|
|
|
28
29
|
assert manifest.name == "test-agent"
|
|
29
30
|
assert manifest.last_awakened is not None
|
|
30
31
|
|
|
32
|
+
def test_awaken_prefers_identity_name_over_shared_config(self, tmp_path: Path):
|
|
33
|
+
"""Agent-local identity should beat a shared-root fallback config name."""
|
|
34
|
+
shared_root = tmp_path / ".skcapstone"
|
|
35
|
+
agent_home = shared_root / "agents" / "aster"
|
|
36
|
+
(agent_home / "identity").mkdir(parents=True)
|
|
37
|
+
(agent_home / "config").mkdir(parents=True)
|
|
38
|
+
|
|
39
|
+
(shared_root / "config").mkdir(parents=True)
|
|
40
|
+
(shared_root / "config" / "config.yaml").write_text("agent_name: Jarvis\n")
|
|
41
|
+
(agent_home / "manifest.json").write_text(json.dumps({"name": "aster"}))
|
|
42
|
+
(agent_home / "identity" / "identity.json").write_text(json.dumps({
|
|
43
|
+
"name": "Aster",
|
|
44
|
+
"fingerprint": "A" * 40,
|
|
45
|
+
"capauth_managed": True,
|
|
46
|
+
}))
|
|
47
|
+
|
|
48
|
+
runtime = AgentRuntime(home=agent_home)
|
|
49
|
+
manifest = runtime.awaken()
|
|
50
|
+
assert manifest.name == "Aster"
|
|
51
|
+
|
|
31
52
|
def test_register_connector(self, initialized_agent_home: Path):
|
|
32
53
|
"""Registering a connector should persist it."""
|
|
33
54
|
runtime = AgentRuntime(home=initialized_agent_home)
|
|
@@ -225,12 +225,14 @@ class TestBuildScheduler:
|
|
|
225
225
|
stop = threading.Event()
|
|
226
226
|
scheduler = build_scheduler(tmp_path, stop)
|
|
227
227
|
names = {s["name"] for s in scheduler.status()}
|
|
228
|
-
|
|
228
|
+
# The core standard tasks must be registered (more — sync, service
|
|
229
|
+
# health, itil, dreaming — have since been added to build_scheduler).
|
|
230
|
+
assert {
|
|
229
231
|
"heartbeat_pulse",
|
|
230
232
|
"backend_reprobe",
|
|
231
233
|
"memory_promotion_sweep",
|
|
232
234
|
"profile_freshness_check",
|
|
233
|
-
}
|
|
235
|
+
}.issubset(names)
|
|
234
236
|
|
|
235
237
|
def test_heartbeat_interval_is_30s(self, tmp_path):
|
|
236
238
|
stop = threading.Event()
|
|
@@ -292,12 +294,15 @@ class TestMemoryPromotionTask:
|
|
|
292
294
|
with patch("skcapstone.memory_promoter.PromotionEngine", return_value=mock_engine):
|
|
293
295
|
callback() # should not raise
|
|
294
296
|
|
|
295
|
-
def
|
|
296
|
-
"""
|
|
297
|
+
def test_import_error_is_swallowed_not_propagated(self, tmp_path):
|
|
298
|
+
"""A failing promotion sweep must NOT crash the scheduler: the sweep runs
|
|
299
|
+
in a background thread and catches/logs its own errors."""
|
|
300
|
+
import time as _time
|
|
301
|
+
|
|
297
302
|
callback = make_memory_promotion_task(tmp_path)
|
|
298
303
|
with patch("skcapstone.memory_promoter.PromotionEngine", side_effect=RuntimeError("unavailable")):
|
|
299
|
-
|
|
300
|
-
|
|
304
|
+
callback() # spawns a daemon thread; must return without raising
|
|
305
|
+
_time.sleep(0.1) # let the background sweep run and swallow the error
|
|
301
306
|
|
|
302
307
|
|
|
303
308
|
class TestBackendReprobeTask:
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Tests for `skcapstone scheduler` CLI command group."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from click.testing import CliRunner
|
|
5
|
+
from skcapstone.cli.scheduler_cmd import register_scheduler_commands
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _app(tmp_path, monkeypatch):
|
|
9
|
+
monkeypatch.setenv("SKCAPSTONE_HOME", str(tmp_path))
|
|
10
|
+
(tmp_path / "config").mkdir(parents=True, exist_ok=True)
|
|
11
|
+
(tmp_path / "config" / "jobs.yaml").write_text(
|
|
12
|
+
"jobs:\n demo:\n every: 60s\n type: shell\n command: 'echo hi'\n nodes: all\n",
|
|
13
|
+
encoding="utf-8")
|
|
14
|
+
|
|
15
|
+
@click.group()
|
|
16
|
+
def main():
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
register_scheduler_commands(main)
|
|
20
|
+
return main
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_scheduler_list(tmp_path, monkeypatch):
|
|
24
|
+
main = _app(tmp_path, monkeypatch)
|
|
25
|
+
res = CliRunner().invoke(main, ["scheduler", "list"])
|
|
26
|
+
assert res.exit_code == 0 and "demo" in res.output
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_scheduler_run_now(tmp_path, monkeypatch):
|
|
30
|
+
main = _app(tmp_path, monkeypatch)
|
|
31
|
+
res = CliRunner().invoke(main, ["scheduler", "run", "demo"])
|
|
32
|
+
assert res.exit_code == 0 and "hi" in res.output
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_scheduler_run_unknown_job_errors(tmp_path, monkeypatch):
|
|
36
|
+
main = _app(tmp_path, monkeypatch)
|
|
37
|
+
res = CliRunner().invoke(main, ["scheduler", "run", "nope"])
|
|
38
|
+
assert res.exit_code != 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_scheduler_disable_then_list(tmp_path, monkeypatch):
|
|
42
|
+
main = _app(tmp_path, monkeypatch)
|
|
43
|
+
assert CliRunner().invoke(main, ["scheduler", "disable", "demo"]).exit_code == 0
|
|
44
|
+
# after disable, jobs.yaml should mark it disabled
|
|
45
|
+
import yaml
|
|
46
|
+
data = yaml.safe_load((tmp_path / "config" / "jobs.yaml").read_text())
|
|
47
|
+
assert data["jobs"]["demo"]["enabled"] is False
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Tests for skscheduler reliability features (2026-06-09):
|
|
2
|
+
retries + backoff, jitter parsing, and the notify (sk-alert) hook."""
|
|
3
|
+
|
|
4
|
+
import threading
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from skcapstone import scheduled_tasks as st
|
|
10
|
+
from skcapstone.scheduled_tasks import TaskScheduler
|
|
11
|
+
from skcapstone.scheduler_jobs import JobSpec, load_jobs
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# --- JobSpec / loader -------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
def test_load_jobs_parses_reliability_fields(tmp_path: Path):
|
|
17
|
+
cfg = tmp_path / "jobs.yaml"
|
|
18
|
+
cfg.write_text(
|
|
19
|
+
"jobs:\n"
|
|
20
|
+
" flaky:\n"
|
|
21
|
+
" every: 60s\n"
|
|
22
|
+
" type: shell\n"
|
|
23
|
+
" command: 'true'\n"
|
|
24
|
+
" retries: 3\n"
|
|
25
|
+
" retry_backoff: 2.5\n"
|
|
26
|
+
" jitter: 30\n"
|
|
27
|
+
" notify: on_failure\n"
|
|
28
|
+
" notify_level: crit\n"
|
|
29
|
+
)
|
|
30
|
+
(job,) = load_jobs(cfg)
|
|
31
|
+
assert job.retries == 3
|
|
32
|
+
assert job.retry_backoff == 2.5
|
|
33
|
+
assert job.jitter == 30.0
|
|
34
|
+
assert job.notify == "on_failure"
|
|
35
|
+
assert job.notify_level == "crit"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_jobspec_reliability_defaults():
|
|
39
|
+
j = JobSpec(name="x")
|
|
40
|
+
assert j.retries == 0 and j.retry_backoff == 0.0 and j.jitter == 0.0
|
|
41
|
+
assert j.notify == "off" and j.notify_level == "warn"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# --- notify hook ------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
class _Res:
|
|
47
|
+
def __init__(self, ok, output="", error=""):
|
|
48
|
+
self.ok, self.output, self.error = ok, output, error
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _patch_alert(monkeypatch):
|
|
52
|
+
calls = []
|
|
53
|
+
monkeypatch.setattr(st.shutil, "which", lambda _n: "/usr/bin/sk-alert")
|
|
54
|
+
monkeypatch.setattr(st.subprocess, "run", lambda args, **k: calls.append(args) or None)
|
|
55
|
+
return calls
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_notify_off_sends_nothing(monkeypatch):
|
|
59
|
+
calls = _patch_alert(monkeypatch)
|
|
60
|
+
TaskScheduler._maybe_notify(JobSpec(name="j", notify="off"), _Res(False, error="boom"), 1)
|
|
61
|
+
assert calls == []
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_notify_on_failure_fires_with_tail(monkeypatch):
|
|
65
|
+
calls = _patch_alert(monkeypatch)
|
|
66
|
+
TaskScheduler._maybe_notify(
|
|
67
|
+
JobSpec(name="j", notify="on_failure", notify_level="crit"),
|
|
68
|
+
_Res(False, output="line1\nline2", error="boom"), attempts=2)
|
|
69
|
+
assert len(calls) == 1
|
|
70
|
+
args = calls[0]
|
|
71
|
+
assert "-l" in args and "crit" in args
|
|
72
|
+
assert "❌ FAILED" in args[-1] and "after 2 attempts" in args[-1]
|
|
73
|
+
assert "line2" in args[-1]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_notify_on_success_skips_failure(monkeypatch):
|
|
77
|
+
calls = _patch_alert(monkeypatch)
|
|
78
|
+
TaskScheduler._maybe_notify(JobSpec(name="j", notify="on_success"), _Res(False, error="x"), 1)
|
|
79
|
+
assert calls == []
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_notify_always_fires_on_success_info_level(monkeypatch):
|
|
83
|
+
calls = _patch_alert(monkeypatch)
|
|
84
|
+
TaskScheduler._maybe_notify(JobSpec(name="j", notify="always"), _Res(True, output="done"), 1)
|
|
85
|
+
assert len(calls) == 1 and "info" in calls[0] and "✅ ok" in calls[0][-1]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# --- retries ----------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
def test_run_config_job_retries_until_success(monkeypatch):
|
|
91
|
+
seq = [_Res(False, error="e1"), _Res(False, error="e2"), _Res(True)]
|
|
92
|
+
n = {"runs": 0}
|
|
93
|
+
recorded = {}
|
|
94
|
+
|
|
95
|
+
class FakeRunner:
|
|
96
|
+
@contextmanager
|
|
97
|
+
def lock(self, job):
|
|
98
|
+
yield True
|
|
99
|
+
def run(self, job):
|
|
100
|
+
r = seq[n["runs"]]; n["runs"] += 1; return r
|
|
101
|
+
|
|
102
|
+
class FakeState:
|
|
103
|
+
def record_run(self, name, now, ok, error):
|
|
104
|
+
recorded.update(ok=ok, error=error, name=name)
|
|
105
|
+
|
|
106
|
+
mgr = TaskScheduler(home=Path("/tmp"), stop_event=threading.Event())
|
|
107
|
+
mgr._job_runner = FakeRunner()
|
|
108
|
+
mgr._state = FakeState()
|
|
109
|
+
job = JobSpec(name="flaky", retries=2, retry_backoff=0.0, jitter=0.0)
|
|
110
|
+
mgr._run_config_job(job, datetime.now(timezone.utc))
|
|
111
|
+
assert n["runs"] == 3 # failed twice, succeeded on 3rd
|
|
112
|
+
assert recorded["ok"] is True
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_run_config_job_stops_after_exhausting_retries(monkeypatch):
|
|
116
|
+
n = {"runs": 0}; recorded = {}
|
|
117
|
+
|
|
118
|
+
class FakeRunner:
|
|
119
|
+
@contextmanager
|
|
120
|
+
def lock(self, job):
|
|
121
|
+
yield True
|
|
122
|
+
def run(self, job):
|
|
123
|
+
n["runs"] += 1; return _Res(False, error="always fails")
|
|
124
|
+
|
|
125
|
+
class FakeState:
|
|
126
|
+
def record_run(self, name, now, ok, error):
|
|
127
|
+
recorded.update(ok=ok)
|
|
128
|
+
|
|
129
|
+
mgr = TaskScheduler(home=Path("/tmp"), stop_event=threading.Event())
|
|
130
|
+
mgr._job_runner = FakeRunner(); mgr._state = FakeState()
|
|
131
|
+
mgr._run_config_job(JobSpec(name="bad", retries=1), datetime.now(timezone.utc))
|
|
132
|
+
assert n["runs"] == 2 # 1 + 1 retry
|
|
133
|
+
assert recorded["ok"] is False
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Integration tests for config-driven jobs in TaskScheduler.
|
|
2
|
+
|
|
3
|
+
Tests that load_config_jobs and tick_config_jobs work correctly, and that
|
|
4
|
+
build_scheduler picks up a jobs.yaml file from the config directory.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import threading
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from skcapstone.scheduler_jobs import JobSpec
|
|
15
|
+
from skcapstone.scheduled_tasks import TaskScheduler
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_due_config_job_for_this_host_fires(tmp_path: Path):
|
|
19
|
+
"""Due jobs are dispatched to a worker thread; assert fired after the thread completes."""
|
|
20
|
+
sched = TaskScheduler(home=tmp_path, stop_event=threading.Event())
|
|
21
|
+
fired = []
|
|
22
|
+
done = threading.Event()
|
|
23
|
+
job = JobSpec(name="j", type="shell", command="true", every_seconds=1, nodes=["hostA"])
|
|
24
|
+
sched.load_config_jobs(jobs=[job], hostname="hostA", host_aliases={"hostA"}, state_root=tmp_path)
|
|
25
|
+
from skcapstone.scheduler_runner import JobResult
|
|
26
|
+
|
|
27
|
+
def _run(j):
|
|
28
|
+
fired.append(j.name)
|
|
29
|
+
done.set()
|
|
30
|
+
return JobResult(ok=True)
|
|
31
|
+
|
|
32
|
+
sched._job_runner.run = _run # type: ignore
|
|
33
|
+
sched.tick_config_jobs(now=datetime(2026, 6, 8, 12, 0, tzinfo=timezone.utc))
|
|
34
|
+
assert done.wait(2), "job should have run within 2s"
|
|
35
|
+
assert fired == ["j"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_job_not_for_this_host_skipped(tmp_path: Path):
|
|
39
|
+
sched = TaskScheduler(home=tmp_path, stop_event=threading.Event())
|
|
40
|
+
fired = []
|
|
41
|
+
job = JobSpec(name="j", type="shell", command="true", every_seconds=1, nodes=[".41"])
|
|
42
|
+
sched.load_config_jobs(jobs=[job], hostname="hostB", host_aliases={"hostB"}, state_root=tmp_path)
|
|
43
|
+
sched._job_runner.run = lambda j: fired.append(j.name) # type: ignore
|
|
44
|
+
sched.tick_config_jobs()
|
|
45
|
+
assert fired == []
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_build_scheduler_loads_jobs_yaml(tmp_path, monkeypatch):
|
|
49
|
+
cfg_dir = tmp_path / "config"; cfg_dir.mkdir()
|
|
50
|
+
(cfg_dir / "jobs.yaml").write_text(
|
|
51
|
+
"jobs:\n noop:\n every: 60s\n type: shell\n command: 'true'\n nodes: all\n",
|
|
52
|
+
encoding="utf-8")
|
|
53
|
+
from skcapstone.scheduled_tasks import build_scheduler
|
|
54
|
+
sched = build_scheduler(home=tmp_path, stop_event=threading.Event())
|
|
55
|
+
assert any(j.name == "noop" for j in sched._config_jobs)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_tick_does_not_block_on_slow_job(tmp_path):
|
|
59
|
+
"""Verify that tick_config_jobs returns immediately even when a job is slow.
|
|
60
|
+
|
|
61
|
+
The job worker runs in a daemon thread; a slow job must not hold up the
|
|
62
|
+
scheduler tick thread (which also drives heartbeats and built-in tasks).
|
|
63
|
+
"""
|
|
64
|
+
import time
|
|
65
|
+
from datetime import datetime, timezone
|
|
66
|
+
|
|
67
|
+
sched = TaskScheduler(home=tmp_path, stop_event=threading.Event())
|
|
68
|
+
job = JobSpec(name="slow", type="shell", command="true", every_seconds=1, nodes=["h"])
|
|
69
|
+
sched.load_config_jobs(jobs=[job], hostname="h", host_aliases={"h"}, state_root=tmp_path)
|
|
70
|
+
started = threading.Event()
|
|
71
|
+
release = threading.Event()
|
|
72
|
+
|
|
73
|
+
def slow_run(j):
|
|
74
|
+
started.set()
|
|
75
|
+
release.wait(5)
|
|
76
|
+
from skcapstone.scheduler_runner import JobResult
|
|
77
|
+
return JobResult(ok=True)
|
|
78
|
+
|
|
79
|
+
sched._job_runner.run = slow_run # type: ignore
|
|
80
|
+
|
|
81
|
+
t0 = time.monotonic()
|
|
82
|
+
sched.tick_config_jobs(now=datetime(2026, 6, 8, 12, 0, tzinfo=timezone.utc))
|
|
83
|
+
elapsed = time.monotonic() - t0
|
|
84
|
+
|
|
85
|
+
assert elapsed < 1.0, f"tick must not block on the job, but took {elapsed:.3f}s"
|
|
86
|
+
assert started.wait(2), "job worker should have started in background"
|
|
87
|
+
release.set()
|