@smilintux/skcapstone 0.9.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 +278 -1
- package/docs/DREAMING.md +70 -0
- package/docs/GETTING_STARTED.md +10 -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.daemon.plist +52 -0
- package/launchd/com.skcapstone.memory-compress.plist +45 -0
- package/launchd/com.skcapstone.skcomms-heartbeat.plist +33 -0
- package/launchd/com.skcapstone.skcomms-queue-drain.plist +34 -0
- package/launchd/install-launchd.sh +156 -0
- 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 +95 -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 +196 -11
- package/scripts/model-fallback-monitor.sh +102 -0
- package/scripts/notion-api.py +259 -0
- package/scripts/nvidia-proxy.mjs +908 -0
- package/scripts/proxy-monitor.sh +89 -0
- 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 +856 -0
- package/scripts/telegram-catchup-all.sh +147 -0
- 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/blueprints/builtins/itil-operations.yaml +40 -0
- 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 +121 -42
- 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 +151 -88
- 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 +35 -25
- package/src/skcapstone/gui_installer.py +2 -2
- package/src/skcapstone/heartbeat.py +34 -30
- 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 +426 -0
- package/src/skcapstone/mcp_launcher.py +15 -1
- package/src/skcapstone/mcp_server.py +341 -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 +1000 -126
- 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 +72 -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
|
@@ -7,13 +7,31 @@ from unittest.mock import MagicMock, call, patch
|
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
|
|
10
|
-
from skcapstone.notifications import
|
|
10
|
+
from skcapstone.notifications import (
|
|
11
|
+
NotificationManager,
|
|
12
|
+
desktop_notifications_enabled,
|
|
13
|
+
notify,
|
|
14
|
+
get_manager,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture(autouse=True)
|
|
19
|
+
def _enable_desktop_notifications(monkeypatch):
|
|
20
|
+
"""Re-enable the desktop-notification guard for this module.
|
|
21
|
+
|
|
22
|
+
The session-wide conftest fixture disables notifications so test runs
|
|
23
|
+
don't flood the live desktop. Every test here mocks ``subprocess.run`` /
|
|
24
|
+
``osascript``, so nothing real is dispatched — they just need the guard
|
|
25
|
+
on to exercise the dispatch logic. Guard-specific tests override this.
|
|
26
|
+
"""
|
|
27
|
+
monkeypatch.setenv("SKCAPSTONE_DESKTOP_NOTIFY", "1")
|
|
11
28
|
|
|
12
29
|
|
|
13
30
|
# ---------------------------------------------------------------------------
|
|
14
31
|
# Helpers
|
|
15
32
|
# ---------------------------------------------------------------------------
|
|
16
33
|
|
|
34
|
+
|
|
17
35
|
def _make_mgr(debounce: float = 0.0) -> NotificationManager:
|
|
18
36
|
"""Return a fresh NotificationManager with zero debounce by default."""
|
|
19
37
|
return NotificationManager(debounce_seconds=debounce)
|
|
@@ -23,14 +41,17 @@ def _make_mgr(debounce: float = 0.0) -> NotificationManager:
|
|
|
23
41
|
# notify-send (Linux)
|
|
24
42
|
# ---------------------------------------------------------------------------
|
|
25
43
|
|
|
44
|
+
|
|
26
45
|
class TestNotifyLinux:
|
|
27
46
|
"""Tests for Linux notify-send path."""
|
|
28
47
|
|
|
29
48
|
def test_notify_send_called_with_correct_args(self):
|
|
30
49
|
"""notify-send is invoked with urgency, title, body."""
|
|
31
50
|
mgr = _make_mgr()
|
|
32
|
-
with
|
|
33
|
-
|
|
51
|
+
with (
|
|
52
|
+
patch("skcapstone.notifications.platform.system", return_value="Linux"),
|
|
53
|
+
patch("skcapstone.notifications.subprocess.run") as mock_run,
|
|
54
|
+
):
|
|
34
55
|
mock_run.return_value = MagicMock(returncode=0)
|
|
35
56
|
result = mgr.notify("Hello", "World", urgency="normal")
|
|
36
57
|
|
|
@@ -45,8 +66,10 @@ class TestNotifyLinux:
|
|
|
45
66
|
def test_notify_send_urgency_low(self):
|
|
46
67
|
"""Low urgency maps to notify-send --urgency low."""
|
|
47
68
|
mgr = _make_mgr()
|
|
48
|
-
with
|
|
49
|
-
|
|
69
|
+
with (
|
|
70
|
+
patch("skcapstone.notifications.platform.system", return_value="Linux"),
|
|
71
|
+
patch("skcapstone.notifications.subprocess.run") as mock_run,
|
|
72
|
+
):
|
|
50
73
|
mock_run.return_value = MagicMock(returncode=0)
|
|
51
74
|
mgr.notify("T", "B", urgency="low")
|
|
52
75
|
|
|
@@ -57,8 +80,10 @@ class TestNotifyLinux:
|
|
|
57
80
|
def test_notify_send_urgency_critical(self):
|
|
58
81
|
"""Critical urgency maps to notify-send --urgency critical."""
|
|
59
82
|
mgr = _make_mgr()
|
|
60
|
-
with
|
|
61
|
-
|
|
83
|
+
with (
|
|
84
|
+
patch("skcapstone.notifications.platform.system", return_value="Linux"),
|
|
85
|
+
patch("skcapstone.notifications.subprocess.run") as mock_run,
|
|
86
|
+
):
|
|
62
87
|
mock_run.return_value = MagicMock(returncode=0)
|
|
63
88
|
mgr.notify("T", "B", urgency="critical")
|
|
64
89
|
|
|
@@ -68,8 +93,10 @@ class TestNotifyLinux:
|
|
|
68
93
|
def test_notify_send_not_found_returns_false(self):
|
|
69
94
|
"""Returns False gracefully when notify-send binary is missing."""
|
|
70
95
|
mgr = _make_mgr()
|
|
71
|
-
with
|
|
72
|
-
|
|
96
|
+
with (
|
|
97
|
+
patch("skcapstone.notifications.platform.system", return_value="Linux"),
|
|
98
|
+
patch("skcapstone.notifications.subprocess.run", side_effect=FileNotFoundError),
|
|
99
|
+
):
|
|
73
100
|
result = mgr.notify("T", "B")
|
|
74
101
|
|
|
75
102
|
assert result is False
|
|
@@ -77,12 +104,15 @@ class TestNotifyLinux:
|
|
|
77
104
|
def test_notify_send_nonzero_exit_returns_false(self):
|
|
78
105
|
"""Returns False when notify-send exits non-zero."""
|
|
79
106
|
import subprocess
|
|
107
|
+
|
|
80
108
|
mgr = _make_mgr()
|
|
81
|
-
with
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
109
|
+
with (
|
|
110
|
+
patch("skcapstone.notifications.platform.system", return_value="Linux"),
|
|
111
|
+
patch(
|
|
112
|
+
"skcapstone.notifications.subprocess.run",
|
|
113
|
+
side_effect=subprocess.CalledProcessError(1, "notify-send", stderr=b"err"),
|
|
114
|
+
),
|
|
115
|
+
):
|
|
86
116
|
result = mgr.notify("T", "B")
|
|
87
117
|
|
|
88
118
|
assert result is False
|
|
@@ -90,12 +120,15 @@ class TestNotifyLinux:
|
|
|
90
120
|
def test_notify_send_timeout_returns_false(self):
|
|
91
121
|
"""Returns False when notify-send times out."""
|
|
92
122
|
import subprocess
|
|
123
|
+
|
|
93
124
|
mgr = _make_mgr()
|
|
94
|
-
with
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
125
|
+
with (
|
|
126
|
+
patch("skcapstone.notifications.platform.system", return_value="Linux"),
|
|
127
|
+
patch(
|
|
128
|
+
"skcapstone.notifications.subprocess.run",
|
|
129
|
+
side_effect=subprocess.TimeoutExpired("notify-send", 5),
|
|
130
|
+
),
|
|
131
|
+
):
|
|
99
132
|
result = mgr.notify("T", "B")
|
|
100
133
|
|
|
101
134
|
assert result is False
|
|
@@ -105,6 +138,7 @@ class TestNotifyLinux:
|
|
|
105
138
|
# osascript (macOS)
|
|
106
139
|
# ---------------------------------------------------------------------------
|
|
107
140
|
|
|
141
|
+
|
|
108
142
|
class TestNotifyMacOS:
|
|
109
143
|
"""Tests for macOS osascript path."""
|
|
110
144
|
|
|
@@ -159,6 +193,7 @@ class TestNotifyMacOS:
|
|
|
159
193
|
# Unsupported platform
|
|
160
194
|
# ---------------------------------------------------------------------------
|
|
161
195
|
|
|
196
|
+
|
|
162
197
|
class TestNotifyUnsupportedPlatform:
|
|
163
198
|
"""Windows and unknown platforms return False without error."""
|
|
164
199
|
|
|
@@ -185,14 +220,17 @@ class TestNotifyUnsupportedPlatform:
|
|
|
185
220
|
# Debounce logic
|
|
186
221
|
# ---------------------------------------------------------------------------
|
|
187
222
|
|
|
223
|
+
|
|
188
224
|
class TestDebounce:
|
|
189
225
|
"""Debounce prevents more than one notification per interval."""
|
|
190
226
|
|
|
191
227
|
def test_second_call_within_window_is_debounced(self):
|
|
192
228
|
"""A second notify() within the debounce window returns False."""
|
|
193
229
|
mgr = NotificationManager(debounce_seconds=5.0)
|
|
194
|
-
with
|
|
195
|
-
|
|
230
|
+
with (
|
|
231
|
+
patch("skcapstone.notifications.platform.system", return_value="Linux"),
|
|
232
|
+
patch("skcapstone.notifications.subprocess.run") as mock_run,
|
|
233
|
+
):
|
|
196
234
|
mock_run.return_value = MagicMock(returncode=0)
|
|
197
235
|
first = mgr.notify("T", "B")
|
|
198
236
|
second = mgr.notify("T", "B")
|
|
@@ -204,8 +242,10 @@ class TestDebounce:
|
|
|
204
242
|
def test_call_after_window_is_allowed(self):
|
|
205
243
|
"""A notify() after the debounce window passes through."""
|
|
206
244
|
mgr = NotificationManager(debounce_seconds=0.05)
|
|
207
|
-
with
|
|
208
|
-
|
|
245
|
+
with (
|
|
246
|
+
patch("skcapstone.notifications.platform.system", return_value="Linux"),
|
|
247
|
+
patch("skcapstone.notifications.subprocess.run") as mock_run,
|
|
248
|
+
):
|
|
209
249
|
mock_run.return_value = MagicMock(returncode=0)
|
|
210
250
|
mgr.notify("T", "B")
|
|
211
251
|
time.sleep(0.1)
|
|
@@ -217,13 +257,17 @@ class TestDebounce:
|
|
|
217
257
|
def test_debounce_does_not_update_timestamp_on_failed_dispatch(self):
|
|
218
258
|
"""Failed dispatch (notify-send missing) does not reset the debounce clock."""
|
|
219
259
|
mgr = NotificationManager(debounce_seconds=5.0)
|
|
220
|
-
with
|
|
221
|
-
|
|
260
|
+
with (
|
|
261
|
+
patch("skcapstone.notifications.platform.system", return_value="Linux"),
|
|
262
|
+
patch("skcapstone.notifications.subprocess.run", side_effect=FileNotFoundError),
|
|
263
|
+
):
|
|
222
264
|
mgr.notify("T", "B") # fails → _last_sent stays 0
|
|
223
265
|
|
|
224
266
|
# Now try again immediately — should not be debounced
|
|
225
|
-
with
|
|
226
|
-
|
|
267
|
+
with (
|
|
268
|
+
patch("skcapstone.notifications.platform.system", return_value="Linux"),
|
|
269
|
+
patch("skcapstone.notifications.subprocess.run") as mock_run2,
|
|
270
|
+
):
|
|
227
271
|
mock_run2.return_value = MagicMock(returncode=0)
|
|
228
272
|
result = mgr.notify("T", "B")
|
|
229
273
|
|
|
@@ -232,8 +276,10 @@ class TestDebounce:
|
|
|
232
276
|
def test_zero_debounce_allows_rapid_calls(self):
|
|
233
277
|
"""debounce_seconds=0 means every call is dispatched."""
|
|
234
278
|
mgr = NotificationManager(debounce_seconds=0)
|
|
235
|
-
with
|
|
236
|
-
|
|
279
|
+
with (
|
|
280
|
+
patch("skcapstone.notifications.platform.system", return_value="Linux"),
|
|
281
|
+
patch("skcapstone.notifications.subprocess.run") as mock_run,
|
|
282
|
+
):
|
|
237
283
|
mock_run.return_value = MagicMock(returncode=0)
|
|
238
284
|
for _ in range(3):
|
|
239
285
|
mgr.notify("T", "B")
|
|
@@ -245,17 +291,21 @@ class TestDebounce:
|
|
|
245
291
|
# Module-level helpers
|
|
246
292
|
# ---------------------------------------------------------------------------
|
|
247
293
|
|
|
294
|
+
|
|
248
295
|
class TestModuleLevelHelpers:
|
|
249
296
|
"""notify() convenience function and get_manager() singleton."""
|
|
250
297
|
|
|
251
298
|
def test_notify_convenience_function(self):
|
|
252
299
|
"""Module-level notify() delegates to the singleton manager."""
|
|
253
|
-
with
|
|
254
|
-
|
|
255
|
-
|
|
300
|
+
with (
|
|
301
|
+
patch("skcapstone.notifications.platform.system", return_value="Linux"),
|
|
302
|
+
patch("skcapstone.notifications.subprocess.run") as mock_run,
|
|
303
|
+
patch("skcapstone.notifications._manager", None),
|
|
304
|
+
):
|
|
256
305
|
mock_run.return_value = MagicMock(returncode=0)
|
|
257
306
|
# Reset singleton so debounce is fresh
|
|
258
307
|
import skcapstone.notifications as _notif_mod
|
|
308
|
+
|
|
259
309
|
_notif_mod._manager = None
|
|
260
310
|
result = notify("Hello", "World")
|
|
261
311
|
|
|
@@ -264,7 +314,47 @@ class TestModuleLevelHelpers:
|
|
|
264
314
|
def test_get_manager_returns_singleton(self):
|
|
265
315
|
"""get_manager() returns the same instance on repeated calls."""
|
|
266
316
|
import skcapstone.notifications as _notif_mod
|
|
317
|
+
|
|
267
318
|
_notif_mod._manager = None # reset
|
|
268
319
|
m1 = get_manager()
|
|
269
320
|
m2 = get_manager()
|
|
270
321
|
assert m1 is m2
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ---------------------------------------------------------------------------
|
|
325
|
+
# Desktop-notification guard (SKCAPSTONE_DESKTOP_NOTIFY)
|
|
326
|
+
# ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class TestDesktopNotificationGuard:
|
|
330
|
+
"""SKCAPSTONE_DESKTOP_NOTIFY suppresses every dispatch path."""
|
|
331
|
+
|
|
332
|
+
def test_enabled_by_default(self, monkeypatch):
|
|
333
|
+
"""Unset env var means notifications are enabled."""
|
|
334
|
+
monkeypatch.delenv("SKCAPSTONE_DESKTOP_NOTIFY", raising=False)
|
|
335
|
+
assert desktop_notifications_enabled() is True
|
|
336
|
+
|
|
337
|
+
@pytest.mark.parametrize("value", ["0", "false", "no", "off", "silent", "null", "none"])
|
|
338
|
+
def test_disabled_values(self, monkeypatch, value):
|
|
339
|
+
"""Recognised falsy values disable notifications (case-insensitive)."""
|
|
340
|
+
monkeypatch.setenv("SKCAPSTONE_DESKTOP_NOTIFY", value.upper())
|
|
341
|
+
assert desktop_notifications_enabled() is False
|
|
342
|
+
|
|
343
|
+
@pytest.mark.parametrize("value", ["1", "true", "yes", "on"])
|
|
344
|
+
def test_enabled_values(self, monkeypatch, value):
|
|
345
|
+
"""Other values keep notifications enabled."""
|
|
346
|
+
monkeypatch.setenv("SKCAPSTONE_DESKTOP_NOTIFY", value)
|
|
347
|
+
assert desktop_notifications_enabled() is True
|
|
348
|
+
|
|
349
|
+
def test_notify_short_circuits_when_disabled(self, monkeypatch):
|
|
350
|
+
"""notify() returns False and never shells out when disabled."""
|
|
351
|
+
monkeypatch.setenv("SKCAPSTONE_DESKTOP_NOTIFY", "0")
|
|
352
|
+
mgr = _make_mgr()
|
|
353
|
+
with (
|
|
354
|
+
patch("skcapstone.notifications.platform.system", return_value="Linux"),
|
|
355
|
+
patch("skcapstone.notifications.subprocess.run") as mock_run,
|
|
356
|
+
):
|
|
357
|
+
result = mgr.notify("Hello", "World")
|
|
358
|
+
|
|
359
|
+
assert result is False
|
|
360
|
+
mock_run.assert_not_called()
|
package/tests/test_onboard.py
CHANGED
|
@@ -7,7 +7,7 @@ Covers:
|
|
|
7
7
|
- _step_systemd_service(): Linux-only, click.confirm gating
|
|
8
8
|
- _step_doctor_check(): doctor diagnostics output
|
|
9
9
|
- _step_test_consciousness(): consciousness loop test with click.confirm gating
|
|
10
|
-
- TOTAL_STEPS constant updated to
|
|
10
|
+
- TOTAL_STEPS constant updated to 16
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
@@ -42,13 +42,13 @@ def tmp_home(tmp_path: Path) -> Path:
|
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
class TestTotalSteps:
|
|
45
|
-
"""Ensure TOTAL_STEPS
|
|
45
|
+
"""Ensure TOTAL_STEPS reflects the current 16-step wizard."""
|
|
46
46
|
|
|
47
|
-
def
|
|
48
|
-
"""Wizard now has
|
|
47
|
+
def test_total_steps_is_16(self) -> None:
|
|
48
|
+
"""Wizard now has 16 numbered steps."""
|
|
49
49
|
from skcapstone.onboard import TOTAL_STEPS
|
|
50
50
|
|
|
51
|
-
assert TOTAL_STEPS ==
|
|
51
|
+
assert TOTAL_STEPS == 16
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
# ---------------------------------------------------------------------------
|
|
@@ -152,71 +152,77 @@ class TestStepOllamaModels:
|
|
|
152
152
|
"""Tests for _step_ollama_models()."""
|
|
153
153
|
|
|
154
154
|
def test_skips_when_ollama_not_available(self) -> None:
|
|
155
|
-
"""Returns
|
|
155
|
+
"""Returns default structured result when prereqs['ollama'] is False."""
|
|
156
156
|
from skcapstone.onboard import _step_ollama_models
|
|
157
157
|
|
|
158
158
|
result = _step_ollama_models({"ollama": False})
|
|
159
159
|
|
|
160
|
-
assert result
|
|
160
|
+
assert result == {
|
|
161
|
+
"ok": False,
|
|
162
|
+
"model": "llama3.2",
|
|
163
|
+
"host": "http://localhost:11434",
|
|
164
|
+
}
|
|
161
165
|
|
|
162
166
|
def test_skips_when_user_declines(self) -> None:
|
|
163
|
-
"""Returns
|
|
167
|
+
"""Returns a non-ok result when user does not confirm the pull."""
|
|
164
168
|
from skcapstone.onboard import _step_ollama_models
|
|
165
169
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
with patch("shutil.which", side_effect=fake_which), \
|
|
170
|
-
patch("subprocess.run", return_value=MagicMock(returncode=0, stdout="")), \
|
|
170
|
+
with patch("subprocess.run", return_value=MagicMock(returncode=0, stdout="")), \
|
|
171
|
+
patch("click.prompt", side_effect=["http://localhost:11434", "llama3.2"]), \
|
|
171
172
|
patch("click.confirm", return_value=False):
|
|
172
173
|
result = _step_ollama_models({"ollama": True})
|
|
173
174
|
|
|
174
|
-
assert result
|
|
175
|
+
assert result == {
|
|
176
|
+
"ok": False,
|
|
177
|
+
"model": "llama3.2",
|
|
178
|
+
"host": "http://localhost:11434",
|
|
179
|
+
}
|
|
175
180
|
|
|
176
181
|
def test_returns_true_when_model_already_present(self) -> None:
|
|
177
|
-
"""Returns True without pulling if model already in 'ollama list'."""
|
|
182
|
+
"""Returns ok=True without pulling if model already in 'ollama list'."""
|
|
178
183
|
from skcapstone.onboard import _step_ollama_models
|
|
179
184
|
|
|
180
|
-
with patch("subprocess.run", return_value=MagicMock(returncode=0, stdout="
|
|
185
|
+
with patch("subprocess.run", return_value=MagicMock(returncode=0, stdout="NAME ID SIZE\nllama3.2 abc123 2.0 GB")), \
|
|
186
|
+
patch("click.prompt", side_effect=["http://localhost:11434", "llama3.2"]):
|
|
181
187
|
result = _step_ollama_models({"ollama": True})
|
|
182
188
|
|
|
183
|
-
assert result is True
|
|
189
|
+
assert result["ok"] is True
|
|
190
|
+
assert result["model"] == "llama3.2"
|
|
184
191
|
|
|
185
192
|
def test_returns_true_on_successful_pull(self) -> None:
|
|
186
|
-
"""Returns True after a successful ollama pull."""
|
|
193
|
+
"""Returns ok=True after a successful ollama pull."""
|
|
187
194
|
from skcapstone.onboard import _step_ollama_models
|
|
188
195
|
|
|
189
|
-
call_count = {"n": 0}
|
|
190
|
-
|
|
191
196
|
def fake_run(cmd, **kwargs):
|
|
192
|
-
call_count["n"] += 1
|
|
193
197
|
if "list" in cmd:
|
|
194
|
-
return MagicMock(returncode=0, stdout="")
|
|
195
|
-
return MagicMock(returncode=0)
|
|
198
|
+
return MagicMock(returncode=0, stdout="")
|
|
199
|
+
return MagicMock(returncode=0)
|
|
196
200
|
|
|
197
201
|
with patch("subprocess.run", side_effect=fake_run), \
|
|
202
|
+
patch("click.prompt", side_effect=["http://localhost:11434", "llama3.2"]), \
|
|
198
203
|
patch("click.confirm", return_value=True):
|
|
199
204
|
result = _step_ollama_models({"ollama": True})
|
|
200
205
|
|
|
201
|
-
assert result is True
|
|
206
|
+
assert result["ok"] is True
|
|
202
207
|
|
|
203
208
|
def test_returns_false_on_pull_failure(self) -> None:
|
|
204
|
-
"""Returns False when ollama pull exits non-zero."""
|
|
209
|
+
"""Returns ok=False when ollama pull exits non-zero."""
|
|
205
210
|
from skcapstone.onboard import _step_ollama_models
|
|
206
211
|
|
|
207
212
|
def fake_run(cmd, **kwargs):
|
|
208
213
|
if "list" in cmd:
|
|
209
214
|
return MagicMock(returncode=0, stdout="")
|
|
210
|
-
return MagicMock(returncode=1)
|
|
215
|
+
return MagicMock(returncode=1)
|
|
211
216
|
|
|
212
217
|
with patch("subprocess.run", side_effect=fake_run), \
|
|
218
|
+
patch("click.prompt", side_effect=["http://localhost:11434", "llama3.2"]), \
|
|
213
219
|
patch("click.confirm", return_value=True):
|
|
214
220
|
result = _step_ollama_models({"ollama": True})
|
|
215
221
|
|
|
216
|
-
assert result is False
|
|
222
|
+
assert result["ok"] is False
|
|
217
223
|
|
|
218
224
|
def test_returns_false_on_timeout(self) -> None:
|
|
219
|
-
"""Returns False when ollama pull times out."""
|
|
225
|
+
"""Returns ok=False when ollama pull times out."""
|
|
220
226
|
from skcapstone.onboard import _step_ollama_models
|
|
221
227
|
|
|
222
228
|
def fake_run(cmd, **kwargs):
|
|
@@ -225,10 +231,11 @@ class TestStepOllamaModels:
|
|
|
225
231
|
raise subprocess.TimeoutExpired(cmd, 600)
|
|
226
232
|
|
|
227
233
|
with patch("subprocess.run", side_effect=fake_run), \
|
|
234
|
+
patch("click.prompt", side_effect=["http://localhost:11434", "llama3.2"]), \
|
|
228
235
|
patch("click.confirm", return_value=True):
|
|
229
236
|
result = _step_ollama_models({"ollama": True})
|
|
230
237
|
|
|
231
|
-
assert result is False
|
|
238
|
+
assert result["ok"] is False
|
|
232
239
|
|
|
233
240
|
|
|
234
241
|
# ---------------------------------------------------------------------------
|
|
@@ -306,59 +313,55 @@ class TestStepConfigFiles:
|
|
|
306
313
|
|
|
307
314
|
|
|
308
315
|
class TestStepSystemdService:
|
|
309
|
-
"""Tests for
|
|
316
|
+
"""Tests for Linux systemd and platform dispatch."""
|
|
310
317
|
|
|
311
318
|
def test_returns_false_on_non_linux(self) -> None:
|
|
312
|
-
"""Returns False immediately on non-Linux platforms."""
|
|
313
|
-
from skcapstone.onboard import
|
|
319
|
+
"""Returns False immediately on unsupported non-Linux/macOS platforms."""
|
|
320
|
+
from skcapstone.onboard import _step_autostart_service
|
|
314
321
|
|
|
315
|
-
with patch("platform.system", return_value="
|
|
316
|
-
result =
|
|
322
|
+
with patch("platform.system", return_value="Windows"):
|
|
323
|
+
result = _step_autostart_service()
|
|
317
324
|
|
|
318
325
|
assert result is False
|
|
319
326
|
|
|
320
327
|
def test_returns_false_when_user_declines(self) -> None:
|
|
321
328
|
"""Returns False when user does not confirm the install."""
|
|
322
|
-
from skcapstone.onboard import
|
|
329
|
+
from skcapstone.onboard import _step_systemd_service_linux
|
|
323
330
|
|
|
324
|
-
with patch("
|
|
325
|
-
|
|
326
|
-
result = _step_systemd_service()
|
|
331
|
+
with patch("click.confirm", return_value=False):
|
|
332
|
+
result = _step_systemd_service_linux()
|
|
327
333
|
|
|
328
334
|
assert result is False
|
|
329
335
|
|
|
330
336
|
def test_returns_false_when_systemd_unavailable(self) -> None:
|
|
331
337
|
"""Returns False when systemd user session is not running."""
|
|
332
|
-
from skcapstone.onboard import
|
|
338
|
+
from skcapstone.onboard import _step_systemd_service_linux
|
|
333
339
|
|
|
334
|
-
with patch("
|
|
335
|
-
patch("click.confirm", return_value=True), \
|
|
340
|
+
with patch("click.confirm", return_value=True), \
|
|
336
341
|
patch("skcapstone.systemd.systemd_available", return_value=False):
|
|
337
|
-
result =
|
|
342
|
+
result = _step_systemd_service_linux()
|
|
338
343
|
|
|
339
344
|
assert result is False
|
|
340
345
|
|
|
341
346
|
def test_returns_true_on_successful_install(self) -> None:
|
|
342
347
|
"""Returns True when systemd install succeeds."""
|
|
343
|
-
from skcapstone.onboard import
|
|
348
|
+
from skcapstone.onboard import _step_systemd_service_linux
|
|
344
349
|
|
|
345
|
-
with patch("
|
|
346
|
-
patch("click.confirm", return_value=True), \
|
|
350
|
+
with patch("click.confirm", return_value=True), \
|
|
347
351
|
patch("skcapstone.systemd.systemd_available", return_value=True), \
|
|
348
352
|
patch("skcapstone.systemd.install_service", return_value={"installed": True, "enabled": True}):
|
|
349
|
-
result =
|
|
353
|
+
result = _step_systemd_service_linux()
|
|
350
354
|
|
|
351
355
|
assert result is True
|
|
352
356
|
|
|
353
357
|
def test_returns_false_on_install_failure(self) -> None:
|
|
354
358
|
"""Returns False when install_service reports not installed."""
|
|
355
|
-
from skcapstone.onboard import
|
|
359
|
+
from skcapstone.onboard import _step_systemd_service_linux
|
|
356
360
|
|
|
357
|
-
with patch("
|
|
358
|
-
patch("click.confirm", return_value=True), \
|
|
361
|
+
with patch("click.confirm", return_value=True), \
|
|
359
362
|
patch("skcapstone.systemd.systemd_available", return_value=True), \
|
|
360
363
|
patch("skcapstone.systemd.install_service", return_value={"installed": False}):
|
|
361
|
-
result =
|
|
364
|
+
result = _step_systemd_service_linux()
|
|
362
365
|
|
|
363
366
|
assert result is False
|
|
364
367
|
|
|
@@ -419,54 +422,39 @@ class TestStepTestConsciousness:
|
|
|
419
422
|
assert result is False
|
|
420
423
|
|
|
421
424
|
def test_returns_true_when_loop_responds(self, tmp_home: Path) -> None:
|
|
422
|
-
"""Returns True when
|
|
425
|
+
"""Returns True when the configured Ollama callback yields a response."""
|
|
423
426
|
from skcapstone.onboard import _step_test_consciousness
|
|
424
427
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
mock_builder = MagicMock()
|
|
428
|
-
mock_builder.build.return_value = "system prompt"
|
|
429
|
-
mock_config = MagicMock()
|
|
430
|
-
mock_config.max_context_tokens = 8000
|
|
428
|
+
mock_config = MagicMock(ollama_model="llama3.2", ollama_host="http://localhost:11434")
|
|
429
|
+
mock_callback = MagicMock(return_value="Hello, I am running fine.")
|
|
431
430
|
|
|
432
431
|
with patch("click.confirm", return_value=True), \
|
|
433
432
|
patch("skcapstone.consciousness_config.load_consciousness_config", return_value=mock_config), \
|
|
434
|
-
patch("
|
|
435
|
-
patch("skcapstone.consciousness_loop.SystemPromptBuilder", return_value=mock_builder), \
|
|
436
|
-
patch("skcapstone.consciousness_loop._classify_message",
|
|
437
|
-
return_value=MagicMock(tags=[], estimated_tokens=10)):
|
|
433
|
+
patch("skseed.llm.ollama_callback", return_value=mock_callback):
|
|
438
434
|
result = _step_test_consciousness(tmp_home)
|
|
439
435
|
|
|
440
436
|
assert result is True
|
|
441
437
|
|
|
442
438
|
def test_returns_false_when_loop_returns_empty(self, tmp_home: Path) -> None:
|
|
443
|
-
"""Returns False when
|
|
439
|
+
"""Returns False when the configured Ollama callback yields an empty string."""
|
|
444
440
|
from skcapstone.onboard import _step_test_consciousness
|
|
445
441
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
mock_builder = MagicMock()
|
|
449
|
-
mock_builder.build.return_value = "system prompt"
|
|
450
|
-
mock_config = MagicMock()
|
|
451
|
-
mock_config.max_context_tokens = 8000
|
|
442
|
+
mock_config = MagicMock(ollama_model="llama3.2", ollama_host="http://localhost:11434")
|
|
443
|
+
mock_callback = MagicMock(return_value="")
|
|
452
444
|
|
|
453
445
|
with patch("click.confirm", return_value=True), \
|
|
454
446
|
patch("skcapstone.consciousness_config.load_consciousness_config", return_value=mock_config), \
|
|
455
|
-
patch("
|
|
456
|
-
patch("skcapstone.consciousness_loop.SystemPromptBuilder", return_value=mock_builder), \
|
|
457
|
-
patch("skcapstone.consciousness_loop._classify_message",
|
|
458
|
-
return_value=MagicMock(tags=[], estimated_tokens=10)):
|
|
447
|
+
patch("skseed.llm.ollama_callback", return_value=mock_callback):
|
|
459
448
|
result = _step_test_consciousness(tmp_home)
|
|
460
449
|
|
|
461
450
|
assert result is False
|
|
462
451
|
|
|
463
452
|
def test_returns_false_on_exception(self, tmp_home: Path) -> None:
|
|
464
|
-
"""Returns False when
|
|
453
|
+
"""Returns False when the Ollama callback raises an exception."""
|
|
465
454
|
from skcapstone.onboard import _step_test_consciousness
|
|
466
455
|
|
|
467
456
|
with patch("click.confirm", return_value=True), \
|
|
468
|
-
patch("
|
|
469
|
-
side_effect=RuntimeError("no config")):
|
|
457
|
+
patch("skseed.llm.ollama_callback", side_effect=RuntimeError("backend down")):
|
|
470
458
|
result = _step_test_consciousness(tmp_home)
|
|
471
459
|
|
|
472
460
|
assert result is False
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Tests for human-operator manifest linking."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from skcapstone.operator_link import build_agent_manifest, discover_human_operator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_discover_human_operator_reads_capauth_profile(tmp_path: Path) -> None:
|
|
12
|
+
"""A CapAuth human profile is converted into operator metadata."""
|
|
13
|
+
capauth_home = tmp_path / ".capauth"
|
|
14
|
+
profile = capauth_home / "identity" / "profile.json"
|
|
15
|
+
profile.parent.mkdir(parents=True)
|
|
16
|
+
profile.write_text(
|
|
17
|
+
json.dumps(
|
|
18
|
+
{
|
|
19
|
+
"entity": {
|
|
20
|
+
"name": "Casey",
|
|
21
|
+
"entity_type": "human",
|
|
22
|
+
"email": "casey@example.com",
|
|
23
|
+
"handle": "casey@example.com",
|
|
24
|
+
},
|
|
25
|
+
"key_info": {
|
|
26
|
+
"fingerprint": "ABCDEF1234567890",
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
),
|
|
30
|
+
encoding="utf-8",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
operator = discover_human_operator(capauth_home)
|
|
34
|
+
|
|
35
|
+
assert operator == {
|
|
36
|
+
"name": "Casey",
|
|
37
|
+
"relationship": "human-operator",
|
|
38
|
+
"entity_type": "human",
|
|
39
|
+
"source": "capauth",
|
|
40
|
+
"email": "casey@example.com",
|
|
41
|
+
"handle": "casey@example.com",
|
|
42
|
+
"fingerprint": "ABCDEF1234567890",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_discover_human_operator_ignores_non_human_profile(tmp_path: Path) -> None:
|
|
47
|
+
"""AI CapAuth profiles are not treated as human operators."""
|
|
48
|
+
capauth_home = tmp_path / ".capauth"
|
|
49
|
+
profile = capauth_home / "identity" / "profile.json"
|
|
50
|
+
profile.parent.mkdir(parents=True)
|
|
51
|
+
profile.write_text(
|
|
52
|
+
json.dumps(
|
|
53
|
+
{
|
|
54
|
+
"entity": {
|
|
55
|
+
"name": "Jarvis",
|
|
56
|
+
"entity_type": "ai",
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
),
|
|
60
|
+
encoding="utf-8",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
assert discover_human_operator(capauth_home) is None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_build_agent_manifest_includes_operator_when_available() -> None:
|
|
67
|
+
"""Operator metadata is persisted directly in the manifest."""
|
|
68
|
+
manifest = build_agent_manifest(
|
|
69
|
+
"jarvis",
|
|
70
|
+
"0.6.0",
|
|
71
|
+
created_at="2026-01-01T00:00:00+00:00",
|
|
72
|
+
operator={"name": "Casey", "fingerprint": "FP123", "relationship": "human-operator"},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
assert manifest["name"] == "jarvis"
|
|
76
|
+
assert manifest["entity_type"] == "ai-agent"
|
|
77
|
+
assert manifest["operator"]["name"] == "Casey"
|
|
78
|
+
assert manifest["operator"]["fingerprint"] == "FP123"
|