@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
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Tests for skcapstone.scheduler_jobs — JobSpec, YAML loading, node affinity,
|
|
2
|
+
due-check, and host-alias discovery.
|
|
3
|
+
|
|
4
|
+
Each group corresponds to one implementation commit:
|
|
5
|
+
A — JobSpec + load_jobs
|
|
6
|
+
B — job_runs_here (node affinity)
|
|
7
|
+
C — is_due (cron + interval with misfire catch-up)
|
|
8
|
+
D — current_host_aliases
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# Group A — JobSpec + load_jobs
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
import warnings
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
from skcapstone.scheduler_jobs import JobSpec, _parse_duration, is_due, load_jobs
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_load_jobs_parses_yaml(tmp_path: Path):
|
|
23
|
+
cfg = tmp_path / "jobs.yaml"
|
|
24
|
+
cfg.write_text(
|
|
25
|
+
"jobs:\n"
|
|
26
|
+
" gtd-triage:\n"
|
|
27
|
+
" schedule: '0 6 * * *'\n"
|
|
28
|
+
" type: agent\n"
|
|
29
|
+
" nodes: ['.41']\n"
|
|
30
|
+
" agent: lumina\n"
|
|
31
|
+
" prompt: 'triage inbox'\n"
|
|
32
|
+
" timeout: 900\n"
|
|
33
|
+
" health:\n"
|
|
34
|
+
" every: 300s\n"
|
|
35
|
+
" type: python\n"
|
|
36
|
+
" nodes: all\n"
|
|
37
|
+
" callback: skcapstone.service_health:run_once\n",
|
|
38
|
+
encoding="utf-8",
|
|
39
|
+
)
|
|
40
|
+
jobs = load_jobs(cfg)
|
|
41
|
+
by_name = {j.name: j for j in jobs}
|
|
42
|
+
assert by_name["gtd-triage"].schedule == "0 6 * * *"
|
|
43
|
+
assert by_name["gtd-triage"].every_seconds is None
|
|
44
|
+
assert by_name["gtd-triage"].type == "agent"
|
|
45
|
+
assert by_name["gtd-triage"].nodes == [".41"]
|
|
46
|
+
assert by_name["health"].every_seconds == 300.0
|
|
47
|
+
assert by_name["health"].nodes == "all"
|
|
48
|
+
assert by_name["health"].enabled is True
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_load_jobs_missing_file_returns_empty(tmp_path: Path):
|
|
52
|
+
assert load_jobs(tmp_path / "nope.yaml") == []
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Group B — node affinity
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
from skcapstone.scheduler_jobs import job_runs_here # noqa: E402
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_job_runs_here_all():
|
|
62
|
+
assert job_runs_here(JobSpec(name="x", nodes="all"), host_aliases={"hostA", ".41"})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_job_runs_here_match_and_miss():
|
|
66
|
+
j = JobSpec(name="x", nodes=[".41"])
|
|
67
|
+
assert job_runs_here(j, host_aliases={".41"})
|
|
68
|
+
assert not job_runs_here(j, host_aliases={".158", "noroc2027"})
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Group C — due-check cron + interval with misfire catch-up
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
from datetime import datetime, timedelta, timezone # noqa: E402
|
|
75
|
+
|
|
76
|
+
from skcapstone.scheduler_jobs import is_due # noqa: E402
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_interval_due():
|
|
80
|
+
j = JobSpec(name="x", every_seconds=300)
|
|
81
|
+
now = datetime(2026, 6, 8, 12, 0, 0, tzinfo=timezone.utc)
|
|
82
|
+
assert is_due(j, last_run=None, now=now)
|
|
83
|
+
assert not is_due(j, last_run=now - timedelta(seconds=100), now=now)
|
|
84
|
+
assert is_due(j, last_run=now - timedelta(seconds=301), now=now)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_cron_due_at_scheduled_minute():
|
|
88
|
+
j = JobSpec(name="x", schedule="0 6 * * *")
|
|
89
|
+
six_am = datetime(2026, 6, 8, 6, 0, 30, tzinfo=timezone.utc)
|
|
90
|
+
assert is_due(j, last_run=None, now=six_am)
|
|
91
|
+
assert not is_due(j, last_run=six_am, now=six_am + timedelta(minutes=5))
|
|
92
|
+
assert is_due(j, last_run=six_am - timedelta(days=1), now=six_am)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# Group D — host alias discovery
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
import socket # noqa: E402
|
|
99
|
+
|
|
100
|
+
from skcapstone.scheduler_jobs import current_host_aliases # noqa: E402
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_current_host_aliases_includes_hostname():
|
|
104
|
+
assert socket.gethostname() in current_host_aliases()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_current_host_aliases_includes_env_alias(monkeypatch):
|
|
108
|
+
monkeypatch.setenv("SK_NODE_ALIAS", ".41, noroc-test")
|
|
109
|
+
a = current_host_aliases()
|
|
110
|
+
assert ".41" in a and "noroc-test" in a
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# _parse_duration — validation (fix 2)
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
def test_parse_duration_rejects_negative():
|
|
118
|
+
with pytest.raises(ValueError):
|
|
119
|
+
_parse_duration("-5m")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_parse_duration_rejects_garbage():
|
|
123
|
+
with pytest.raises(ValueError):
|
|
124
|
+
_parse_duration("abc")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_parse_duration_valid_units():
|
|
128
|
+
assert _parse_duration("300s") == 300.0
|
|
129
|
+
assert _parse_duration("5m") == 300.0
|
|
130
|
+
assert _parse_duration("1h") == 3600.0
|
|
131
|
+
assert _parse_duration("1d") == 86400.0
|
|
132
|
+
assert _parse_duration(90) == 90.0
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
# load_jobs — warn on unknown keys (fix 3)
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
def test_load_jobs_warns_on_unknown_key(tmp_path):
|
|
140
|
+
cfg = tmp_path / "jobs.yaml"
|
|
141
|
+
cfg.write_text(
|
|
142
|
+
"jobs:\n bad:\n shcedule: '0 6 * * *'\n type: shell\n",
|
|
143
|
+
encoding="utf-8",
|
|
144
|
+
)
|
|
145
|
+
with pytest.warns(UserWarning):
|
|
146
|
+
load_jobs(cfg)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
# is_due — no-schedule never fires (fix 4)
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
def test_is_due_no_schedule_never_fires():
|
|
154
|
+
j = JobSpec(name="x")
|
|
155
|
+
assert not is_due(j, last_run=None)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Tests for skcapstone.scheduler_runner — JobRunner execution, overlap lock,
|
|
2
|
+
result shapes, and error containment.
|
|
3
|
+
|
|
4
|
+
Each test targets a specific contract:
|
|
5
|
+
- python jobs call the registered callback
|
|
6
|
+
- shell jobs capture stdout and return ok=True
|
|
7
|
+
- nonzero exit codes produce ok=False with the correct exit_code
|
|
8
|
+
- exceptions in python callbacks are caught and surfaced in result.error
|
|
9
|
+
- the overlap lock prevents a second concurrent acquire
|
|
10
|
+
- unknown job types return ok=False without raising
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from skcapstone.scheduler_jobs import JobSpec
|
|
16
|
+
from skcapstone.scheduler_runner import JobRunner
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_python_job_calls_callback(tmp_path: Path):
|
|
20
|
+
called = {}
|
|
21
|
+
import skcapstone.scheduler_runner as sr
|
|
22
|
+
sr._TEST_HOOK = lambda: called.setdefault("hit", True) # type: ignore
|
|
23
|
+
job = JobSpec(name="t", type="python", callback="skcapstone.scheduler_runner:_TEST_HOOK")
|
|
24
|
+
result = JobRunner(log_dir=tmp_path).run(job)
|
|
25
|
+
assert result.ok and called.get("hit") is True
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_shell_job_runs_command(tmp_path: Path):
|
|
29
|
+
job = JobSpec(name="echo", type="shell", command="echo hello", timeout=10)
|
|
30
|
+
result = JobRunner(log_dir=tmp_path).run(job)
|
|
31
|
+
assert result.ok and "hello" in result.output
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_shell_job_nonzero_is_error(tmp_path: Path):
|
|
35
|
+
job = JobSpec(name="fail", type="shell", command="sh -c 'exit 3'", timeout=10)
|
|
36
|
+
result = JobRunner(log_dir=tmp_path).run(job)
|
|
37
|
+
assert not result.ok and result.exit_code == 3
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_python_job_exception_is_caught(tmp_path: Path):
|
|
41
|
+
import skcapstone.scheduler_runner as sr
|
|
42
|
+
def _boom(): raise RuntimeError("nope")
|
|
43
|
+
sr._TEST_BOOM = _boom # type: ignore
|
|
44
|
+
job = JobSpec(name="b", type="python", callback="skcapstone.scheduler_runner:_TEST_BOOM")
|
|
45
|
+
result = JobRunner(log_dir=tmp_path).run(job)
|
|
46
|
+
assert not result.ok and "nope" in result.error
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_overlap_lock_blocks_second_run(tmp_path: Path):
|
|
50
|
+
runner = JobRunner(log_dir=tmp_path)
|
|
51
|
+
job = JobSpec(name="locked", type="shell", command="echo x", timeout=10)
|
|
52
|
+
with runner.lock(job) as got:
|
|
53
|
+
assert got
|
|
54
|
+
with runner.lock(job) as second:
|
|
55
|
+
assert not second
|
|
56
|
+
# lock released after context exit -> can acquire again
|
|
57
|
+
with runner.lock(job) as third:
|
|
58
|
+
assert third
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_unknown_type_is_error(tmp_path: Path):
|
|
62
|
+
job = JobSpec(name="x", type="weird")
|
|
63
|
+
result = JobRunner(log_dir=tmp_path).run(job)
|
|
64
|
+
assert not result.ok
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import threading
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from skcapstone.scheduler_state import SchedulerState
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_state_roundtrip(tmp_path: Path):
|
|
10
|
+
st = SchedulerState(root=tmp_path, hostname="hostA")
|
|
11
|
+
assert st.last_run("job1") is None
|
|
12
|
+
now = datetime(2026, 6, 8, 6, 0, tzinfo=timezone.utc)
|
|
13
|
+
st.record_run("job1", now=now, ok=True)
|
|
14
|
+
st2 = SchedulerState(root=tmp_path, hostname="hostA")
|
|
15
|
+
assert st2.last_run("job1") == now
|
|
16
|
+
rec = st2.get("job1")
|
|
17
|
+
assert rec["run_count"] == 1 and rec["error_count"] == 0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_state_path_is_host_scoped(tmp_path: Path):
|
|
21
|
+
st = SchedulerState(root=tmp_path, hostname="hostA")
|
|
22
|
+
assert st.state_file == tmp_path / "scheduler" / "hostA" / "state.json"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_error_run_increments_error_count(tmp_path: Path):
|
|
26
|
+
st = SchedulerState(root=tmp_path, hostname="hostA")
|
|
27
|
+
st.record_run("j", ok=False, error="boom")
|
|
28
|
+
rec = st.get("j")
|
|
29
|
+
assert rec["error_count"] == 1 and rec["run_count"] == 0
|
|
30
|
+
assert rec["last_status"] == "error" and rec["last_error"] == "boom"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_concurrent_record_run_is_safe(tmp_path: Path):
|
|
34
|
+
"""Concurrent record_run calls from many threads must not corrupt state.json.
|
|
35
|
+
|
|
36
|
+
With 8 threads each calling record_run 20 times (160 total writes), the
|
|
37
|
+
resulting JSON file must still be valid and each job must show exactly 20
|
|
38
|
+
successful runs. This validates the _write_lock guard in SchedulerState.
|
|
39
|
+
"""
|
|
40
|
+
st = SchedulerState(root=tmp_path, hostname="h")
|
|
41
|
+
|
|
42
|
+
def worker(i: int) -> None:
|
|
43
|
+
for _ in range(20):
|
|
44
|
+
st.record_run(f"job{i}", ok=True)
|
|
45
|
+
|
|
46
|
+
ts = [threading.Thread(target=worker, args=(i,)) for i in range(8)]
|
|
47
|
+
for t in ts:
|
|
48
|
+
t.start()
|
|
49
|
+
for t in ts:
|
|
50
|
+
t.join()
|
|
51
|
+
|
|
52
|
+
data = json.loads(st.state_file.read_text())
|
|
53
|
+
assert len(data) == 8, f"expected 8 job keys, got {len(data)}: {list(data)}"
|
|
54
|
+
for i in range(8):
|
|
55
|
+
assert data[f"job{i}"]["run_count"] == 20, (
|
|
56
|
+
f"job{i} run_count={data[f'job{i}']['run_count']}, expected 20"
|
|
57
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Tests for the stable public facade skcapstone.sdk."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from skcapstone import sdk
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
15
|
+
"""Point skcapstone at an isolated temp home for the duration of a test."""
|
|
16
|
+
monkeypatch.setenv("SKCAPSTONE_HOME", str(tmp_path))
|
|
17
|
+
# the facade resolves home via skcapstone.shared_home(), which reads the
|
|
18
|
+
# module-level AGENT_HOME captured at import — patch it directly too.
|
|
19
|
+
import skcapstone as pkg
|
|
20
|
+
|
|
21
|
+
monkeypatch.setattr(pkg, "AGENT_HOME", str(tmp_path))
|
|
22
|
+
return tmp_path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_is_available_true(home: Path):
|
|
26
|
+
assert sdk.is_available() is True
|
|
27
|
+
assert home.exists()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_alert_publishes_to_topic(home: Path):
|
|
31
|
+
ok = sdk.alert("svc.error", {"message": "boom"}, level="error")
|
|
32
|
+
assert ok is True
|
|
33
|
+
topic_dir = home / "pubsub" / "topics" / "svc.error"
|
|
34
|
+
assert topic_dir.is_dir()
|
|
35
|
+
msgs = list(topic_dir.glob("msg-*.json"))
|
|
36
|
+
assert len(msgs) == 1
|
|
37
|
+
data = json.loads(msgs[0].read_text())
|
|
38
|
+
assert data["payload"]["message"] == "boom"
|
|
39
|
+
assert "error" in data["tags"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_alert_unknown_level_falls_back_to_info(home: Path):
|
|
43
|
+
sdk.alert("svc.weird", {"x": 1}, level="bogus")
|
|
44
|
+
data = json.loads(next((home / "pubsub" / "topics" / "svc.weird").glob("msg-*.json")).read_text())
|
|
45
|
+
assert data["tags"] == ["info"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_register_and_unregister_job(home: Path):
|
|
49
|
+
path = sdk.register_job({"name": "svc_tick", "every": "10m", "type": "shell", "command": "echo hi"})
|
|
50
|
+
assert Path(path).exists()
|
|
51
|
+
assert Path(path).name == "svc_tick.yaml"
|
|
52
|
+
assert sdk.unregister_job("svc_tick") is True
|
|
53
|
+
assert not Path(path).exists()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_coord_create_writes_task(home: Path):
|
|
57
|
+
tid = sdk.coord_create("hello", description="d", priority="high", tags=["t"])
|
|
58
|
+
matches = list((home / "coordination" / "tasks").glob(f"{tid}*.json"))
|
|
59
|
+
assert len(matches) == 1
|
|
60
|
+
task = json.loads(matches[0].read_text())
|
|
61
|
+
assert task["title"] == "hello"
|
|
62
|
+
assert task["priority"] == "high"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_register_service_writes_registry(home: Path):
|
|
66
|
+
path = sdk.register_service("skvoice", health_url="http://localhost:9/health", pid_file="/tmp/x.pid")
|
|
67
|
+
entry = json.loads(Path(path).read_text())
|
|
68
|
+
assert entry["name"] == "skvoice"
|
|
69
|
+
assert entry["health_url"] == "http://localhost:9/health"
|
|
70
|
+
assert entry["pid_file"] == "/tmp/x.pid"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""service_health incident behavior — no recurring-note churn (prb-7810b08e)."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
import skcapstone
|
|
7
|
+
import skcapstone.mcp_tools._helpers as _helpers
|
|
8
|
+
from skcapstone.itil import ITILManager
|
|
9
|
+
from skcapstone.service_health import _create_incident_for_down_service
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture(autouse=True)
|
|
13
|
+
def _isolate(tmp_path: Path, monkeypatch) -> None:
|
|
14
|
+
"""Redirect ITIL + GTD storage to a tmp dir (no ~/.skcapstone writes)."""
|
|
15
|
+
monkeypatch.setattr(skcapstone, "SHARED_ROOT", str(tmp_path))
|
|
16
|
+
monkeypatch.setattr(_helpers, "SHARED_ROOT", str(tmp_path))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_repeated_down_creates_one_incident_with_no_still_down_notes(tmp_path: Path):
|
|
20
|
+
result = {"name": "skvector", "status": "down", "error": "no route to host"}
|
|
21
|
+
|
|
22
|
+
# Three consecutive health cycles while the service stays down.
|
|
23
|
+
_create_incident_for_down_service(result)
|
|
24
|
+
_create_incident_for_down_service(result)
|
|
25
|
+
_create_incident_for_down_service(result)
|
|
26
|
+
|
|
27
|
+
mgr = ITILManager(str(tmp_path))
|
|
28
|
+
incidents = [i for i in mgr.list_incidents() if "skvector" in i.affected_services]
|
|
29
|
+
|
|
30
|
+
# Exactly one incident — no duplicates from repeated cycles.
|
|
31
|
+
assert len(incidents) == 1
|
|
32
|
+
# And the timeline never accumulated recurring "still down" churn.
|
|
33
|
+
still_down = [e for e in incidents[0].timeline if "still down" in (e.get("note") or "")]
|
|
34
|
+
assert still_down == []
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""T4 — service_health unions sdk.register_service entries with defaults."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from skcapstone import sdk, service_health
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
14
|
+
monkeypatch.setenv("SKCAPSTONE_HOME", str(tmp_path))
|
|
15
|
+
import skcapstone as pkg
|
|
16
|
+
|
|
17
|
+
monkeypatch.setattr(pkg, "AGENT_HOME", str(tmp_path))
|
|
18
|
+
# Disable the built-in checks that honour the "disabled" sentinel so the
|
|
19
|
+
# test stays offline. The daemon/skchat checks probe localhost and fail
|
|
20
|
+
# fast (connection refused → status "down"), which is fine for these
|
|
21
|
+
# assertions about the registry union.
|
|
22
|
+
for var in ("SKMEMORY_SKVECTOR_URL", "SKMEMORY_SKGRAPH_HOST", "SYNCTHING_API_URL"):
|
|
23
|
+
monkeypatch.setenv(var, "disabled")
|
|
24
|
+
return tmp_path
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_registered_service_appears(home: Path):
|
|
28
|
+
sdk.register_service("myservice", pid_file=str(home / "x.pid"))
|
|
29
|
+
names = {r["name"] for r in service_health.check_all_services()}
|
|
30
|
+
assert "myservice" in names
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_registry_entry_without_targets_is_unknown(home: Path):
|
|
34
|
+
sdk.register_service("bare") # no health_url, no pid_file
|
|
35
|
+
row = next(r for r in service_health.check_all_services() if r["name"] == "bare")
|
|
36
|
+
assert row["status"] == "unknown"
|
|
37
|
+
assert "without" in (row["error"] or "")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_empty_registry_no_extra_rows(home: Path):
|
|
41
|
+
"""With no registry, only built-in checks appear (none crash)."""
|
|
42
|
+
rows = service_health.check_all_services()
|
|
43
|
+
# registry dir does not exist → loader returns [] and adds nothing
|
|
44
|
+
assert isinstance(rows, list)
|
|
45
|
+
assert all("name" in r for r in rows)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_builtin_name_not_duplicated_by_registry(home: Path):
|
|
49
|
+
"""A registry entry colliding with a built-in name is skipped."""
|
|
50
|
+
sdk.register_service("skcapstone daemon", pid_file=str(home / "d.pid"))
|
|
51
|
+
names = [r["name"] for r in service_health.check_all_services()]
|
|
52
|
+
assert names.count("skcapstone daemon") == 1
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Tests for the native SKCapstone session briefing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from click.testing import CliRunner
|
|
10
|
+
|
|
11
|
+
from skcapstone.cli import main
|
|
12
|
+
from skcapstone.session_briefing import (
|
|
13
|
+
build_session_briefing,
|
|
14
|
+
format_session_briefing_text,
|
|
15
|
+
load_hammertime_briefing,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_load_hammertime_briefing_respects_disable_env(monkeypatch, tmp_path: Path) -> None:
|
|
20
|
+
"""It returns None when HammerTime briefing is explicitly disabled."""
|
|
21
|
+
monkeypatch.setenv("SK_INCLUDE_HAMMERTIME_BRIEFING", "0")
|
|
22
|
+
assert load_hammertime_briefing(root=tmp_path) is None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_load_hammertime_briefing_parses_json(monkeypatch, tmp_path: Path) -> None:
|
|
26
|
+
"""It parses JSON output from the HammerTime briefing script."""
|
|
27
|
+
script = tmp_path / "scripts" / "case-briefing.py"
|
|
28
|
+
script.parent.mkdir(parents=True)
|
|
29
|
+
script.write_text("#!/usr/bin/env python3\n", encoding="utf-8")
|
|
30
|
+
|
|
31
|
+
payload = {"summary": {"queue_size": 2}, "alert_count": 1}
|
|
32
|
+
|
|
33
|
+
def fake_run(*args, **kwargs): # noqa: ANN002, ANN003
|
|
34
|
+
return subprocess.CompletedProcess(
|
|
35
|
+
args=args[0],
|
|
36
|
+
returncode=0,
|
|
37
|
+
stdout=json.dumps(payload),
|
|
38
|
+
stderr="",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
monkeypatch.delenv("SK_INCLUDE_HAMMERTIME_BRIEFING", raising=False)
|
|
42
|
+
monkeypatch.setattr("skcapstone.session_briefing.subprocess.run", fake_run)
|
|
43
|
+
|
|
44
|
+
assert load_hammertime_briefing(root=tmp_path) == payload
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_build_session_briefing_includes_skcapstone_and_hammertime(monkeypatch, tmp_path: Path) -> None:
|
|
48
|
+
"""It builds a combined payload for startup consumers."""
|
|
49
|
+
ctx = {"agent": {"name": "Aster"}, "memories": []}
|
|
50
|
+
briefing = {"summary": {"queue_size": 1}, "alert_count": 0}
|
|
51
|
+
|
|
52
|
+
monkeypatch.setattr("skcapstone.session_briefing.gather_context", lambda home, memory_limit=10: ctx)
|
|
53
|
+
monkeypatch.setattr(
|
|
54
|
+
"skcapstone.session_briefing.load_hammertime_briefing",
|
|
55
|
+
lambda python_bin=None: briefing,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
payload = build_session_briefing(tmp_path, memory_limit=3)
|
|
59
|
+
|
|
60
|
+
assert payload["agent_home"] == str(tmp_path)
|
|
61
|
+
assert payload["skcapstone_context"] == ctx
|
|
62
|
+
assert payload["hammertime_briefing"] == briefing
|
|
63
|
+
assert "generated_at" in payload
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_format_session_briefing_text_contains_hammertime_section() -> None:
|
|
67
|
+
"""It renders a readable summary including the HammerTime section."""
|
|
68
|
+
payload = {
|
|
69
|
+
"generated_at": "2026-04-09T00:00:00+00:00",
|
|
70
|
+
"agent_home": "/tmp/aster",
|
|
71
|
+
"skcapstone_context": {
|
|
72
|
+
"agent": {"name": "Aster", "is_conscious": True, "fingerprint": "abc123"},
|
|
73
|
+
"pillars": {},
|
|
74
|
+
"board": {"total": 0},
|
|
75
|
+
"memories": [],
|
|
76
|
+
"soul": {"active": None},
|
|
77
|
+
"mcp": {"available": False},
|
|
78
|
+
"gathered_at": "2026-04-09T00:00:00+00:00",
|
|
79
|
+
},
|
|
80
|
+
"hammertime_briefing": {
|
|
81
|
+
"alert_count": 1,
|
|
82
|
+
"summary": {"queue_size": 2},
|
|
83
|
+
"top_priority": {
|
|
84
|
+
"incident_id": "INC-001",
|
|
85
|
+
"problem_slug": "example-problem",
|
|
86
|
+
"action": "File claim of exemption",
|
|
87
|
+
"status": "in-progress",
|
|
88
|
+
},
|
|
89
|
+
"focus_items": [
|
|
90
|
+
{
|
|
91
|
+
"incident_id": "INC-001",
|
|
92
|
+
"action": "Review preferred filing",
|
|
93
|
+
"status": "in-progress",
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
output = format_session_briefing_text(payload)
|
|
100
|
+
|
|
101
|
+
assert "# SKCapstone Session Briefing" in output
|
|
102
|
+
assert "## hammertime briefing" in output
|
|
103
|
+
assert "INC-001" in output
|
|
104
|
+
assert "File claim of exemption" in output
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_session_briefing_cli_json(monkeypatch, tmp_path: Path) -> None:
|
|
108
|
+
"""The CLI exposes the combined payload as JSON."""
|
|
109
|
+
runner = CliRunner()
|
|
110
|
+
payload = {
|
|
111
|
+
"generated_at": "2026-04-09T00:00:00+00:00",
|
|
112
|
+
"agent_home": str(tmp_path),
|
|
113
|
+
"skcapstone_context": {"agent": {"name": "Aster"}},
|
|
114
|
+
"hammertime_briefing": {"summary": {"queue_size": 1}, "alert_count": 0},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
monkeypatch.setattr(
|
|
118
|
+
"skcapstone.session_briefing.build_session_briefing",
|
|
119
|
+
lambda home, memory_limit=10: payload,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
result = runner.invoke(
|
|
123
|
+
main,
|
|
124
|
+
["session", "briefing", "--home", str(tmp_path), "--format", "json"],
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
assert result.exit_code == 0
|
|
128
|
+
parsed = json.loads(result.output)
|
|
129
|
+
assert parsed["skcapstone_context"]["agent"]["name"] == "Aster"
|
|
130
|
+
assert parsed["hammertime_briefing"]["summary"]["queue_size"] == 1
|
package/tests/test_snapshots.py
CHANGED
|
@@ -72,7 +72,7 @@ def rich_snapshot() -> SoulSnapshot:
|
|
|
72
72
|
],
|
|
73
73
|
summary="Working on smilinTux.org sovereign AI project",
|
|
74
74
|
key_topics=["sovereign AI", "smilinTux", "consciousness continuity"],
|
|
75
|
-
decisions_made=["Use
|
|
75
|
+
decisions_made=["Use SKComms for transport"],
|
|
76
76
|
open_threads=["Thread headers still in progress"],
|
|
77
77
|
relationship_notes=["Trusted friend and collaborator", "Cloud 9 solidarity"],
|
|
78
78
|
)
|
|
@@ -423,12 +423,12 @@ class TestSoulBlueprintConversion:
|
|
|
423
423
|
|
|
424
424
|
|
|
425
425
|
class TestConsciousnessAPI:
|
|
426
|
-
"""Integration tests for the
|
|
426
|
+
"""Integration tests for the SKComms consciousness endpoints."""
|
|
427
427
|
|
|
428
428
|
@pytest.fixture(autouse=True)
|
|
429
429
|
def patch_snapshot_store(self, tmp_path, monkeypatch):
|
|
430
430
|
"""Override the global snapshot store to use a temp directory."""
|
|
431
|
-
import
|
|
431
|
+
import skcomms.api as api_module
|
|
432
432
|
from skcapstone.snapshots import SnapshotStore as _Store
|
|
433
433
|
|
|
434
434
|
temp_store = _Store(base_dir=tmp_path / "api_snapshots")
|
|
@@ -438,7 +438,7 @@ class TestConsciousnessAPI:
|
|
|
438
438
|
@pytest.fixture
|
|
439
439
|
def client(self):
|
|
440
440
|
from fastapi.testclient import TestClient
|
|
441
|
-
from
|
|
441
|
+
from skcomms.api import app
|
|
442
442
|
|
|
443
443
|
return TestClient(app)
|
|
444
444
|
|