@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
package/tests/test_doctor.py
CHANGED
|
@@ -19,14 +19,23 @@ import pytest
|
|
|
19
19
|
import yaml
|
|
20
20
|
from click.testing import CliRunner
|
|
21
21
|
|
|
22
|
+
from skcapstone.codex_setup import ensure_codex_setup
|
|
22
23
|
from skcapstone.doctor import (
|
|
23
24
|
Check,
|
|
24
25
|
DiagnosticReport,
|
|
26
|
+
_check_codex,
|
|
25
27
|
_check_agent_home,
|
|
28
|
+
_check_harness_env,
|
|
29
|
+
_check_yolo,
|
|
26
30
|
_check_identity,
|
|
31
|
+
_check_identity_consistency,
|
|
32
|
+
_provisioned_agents,
|
|
33
|
+
_scan_capauth_local,
|
|
27
34
|
_check_memory,
|
|
28
35
|
_check_packages,
|
|
36
|
+
run_fixes,
|
|
29
37
|
run_diagnostics,
|
|
38
|
+
run_fixes,
|
|
30
39
|
)
|
|
31
40
|
|
|
32
41
|
|
|
@@ -200,6 +209,66 @@ class TestCheckPackages:
|
|
|
200
209
|
assert all(c.fix for c in checks)
|
|
201
210
|
|
|
202
211
|
|
|
212
|
+
class TestCheckCodex:
|
|
213
|
+
"""Test Codex SK agent bootstrap checks and fixes."""
|
|
214
|
+
|
|
215
|
+
def test_codex_missing_bootstrap_fails_when_codex_home_set(self, tmp_path, monkeypatch):
|
|
216
|
+
"""A detected Codex home without bootstrap is reported as fixable."""
|
|
217
|
+
codex_home = tmp_path / ".codex"
|
|
218
|
+
monkeypatch.setenv("CODEX_HOME", str(codex_home))
|
|
219
|
+
|
|
220
|
+
checks = _check_codex()
|
|
221
|
+
check = next(c for c in checks if c.name == "codex:agent_context")
|
|
222
|
+
|
|
223
|
+
assert not check.passed
|
|
224
|
+
assert "missing" in check.detail
|
|
225
|
+
assert check.fix == "skcapstone doctor --fix"
|
|
226
|
+
|
|
227
|
+
def test_codex_bootstrap_fix_creates_loader_and_agents(self, tmp_path, monkeypatch):
|
|
228
|
+
"""doctor fixes create the loader script and global AGENTS.md guidance."""
|
|
229
|
+
codex_home = tmp_path / ".codex"
|
|
230
|
+
agent_home = tmp_path / ".skcapstone"
|
|
231
|
+
monkeypatch.setenv("CODEX_HOME", str(codex_home))
|
|
232
|
+
monkeypatch.setenv("SKAGENT", "jarvis")
|
|
233
|
+
|
|
234
|
+
report = DiagnosticReport(checks=[
|
|
235
|
+
Check(
|
|
236
|
+
name="codex:agent_context",
|
|
237
|
+
description="Codex SK agent context bootstrap",
|
|
238
|
+
passed=False,
|
|
239
|
+
category="codex",
|
|
240
|
+
)
|
|
241
|
+
])
|
|
242
|
+
|
|
243
|
+
results = run_fixes(report, agent_home)
|
|
244
|
+
|
|
245
|
+
assert results[0].success
|
|
246
|
+
loader = codex_home / "bin" / "load-sk-agent-context.sh"
|
|
247
|
+
agents = codex_home / "AGENTS.md"
|
|
248
|
+
assert loader.exists()
|
|
249
|
+
assert loader.stat().st_mode & 0o100
|
|
250
|
+
agents_text = agents.read_text(encoding="utf-8")
|
|
251
|
+
assert "SKCAPSTONE_CODEX_AGENT_CONTEXT_START" in agents_text
|
|
252
|
+
assert "jarvis" in agents_text
|
|
253
|
+
assert str(loader) in agents_text
|
|
254
|
+
|
|
255
|
+
checks = _check_codex()
|
|
256
|
+
assert next(c for c in checks if c.name == "codex:agent_context").passed
|
|
257
|
+
|
|
258
|
+
def test_codex_fix_preserves_functional_custom_loader(self, tmp_path, monkeypatch):
|
|
259
|
+
"""Existing working loader scripts are not overwritten."""
|
|
260
|
+
codex_home = tmp_path / ".codex"
|
|
261
|
+
loader = codex_home / "bin" / "load-sk-agent-context.sh"
|
|
262
|
+
loader.parent.mkdir(parents=True)
|
|
263
|
+
custom_loader = "#!/usr/bin/env bash\nSKAGENT=x SKCAPSTONE_AGENT=x SKMEMORY_AGENT=x skcapstone status; skmemory ritual; skwhisper status\n"
|
|
264
|
+
loader.write_text(custom_loader, encoding="utf-8")
|
|
265
|
+
|
|
266
|
+
monkeypatch.setenv("CODEX_HOME", str(codex_home))
|
|
267
|
+
ensure_codex_setup()
|
|
268
|
+
|
|
269
|
+
assert loader.read_text(encoding="utf-8") == custom_loader
|
|
270
|
+
|
|
271
|
+
|
|
203
272
|
class TestRunDiagnostics:
|
|
204
273
|
"""Test the full diagnostic run."""
|
|
205
274
|
|
|
@@ -255,3 +324,334 @@ class TestCLIDoctorCommand:
|
|
|
255
324
|
assert result.exit_code == 0
|
|
256
325
|
assert "Python Packages" in result.output
|
|
257
326
|
assert "passed" in result.output or "checks" in result.output.lower()
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _write_claude_config(home_root: Path, *, claude_json: dict, settings: dict | None = None,
|
|
330
|
+
mcp_json: dict | None = None) -> Path:
|
|
331
|
+
"""Lay down a fake Claude Code config tree under *home_root*. Returns the
|
|
332
|
+
.claude config dir."""
|
|
333
|
+
(home_root / ".claude.json").write_text(json.dumps(claude_json))
|
|
334
|
+
cc = home_root / ".claude"
|
|
335
|
+
cc.mkdir(exist_ok=True)
|
|
336
|
+
if settings is not None:
|
|
337
|
+
(cc / "settings.json").write_text(json.dumps(settings))
|
|
338
|
+
if mcp_json is not None:
|
|
339
|
+
(cc / "mcp.json").write_text(json.dumps(mcp_json))
|
|
340
|
+
return cc
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class TestCheckHarnessEnv:
|
|
344
|
+
"""Test the AI-harness (Claude Code) environment checks."""
|
|
345
|
+
|
|
346
|
+
def _by_name(self, checks):
|
|
347
|
+
return {c.name: c for c in checks}
|
|
348
|
+
|
|
349
|
+
def test_gate_when_no_claude_code(self, tmp_path, monkeypatch):
|
|
350
|
+
"""No ~/.claude.json → one informational passing check, no failures."""
|
|
351
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
352
|
+
monkeypatch.delenv("CLAUDE_CONFIG_DIR", raising=False)
|
|
353
|
+
checks = _check_harness_env(tmp_path / ".skcapstone")
|
|
354
|
+
assert len(checks) == 1
|
|
355
|
+
assert checks[0].name == "harness:claude-code"
|
|
356
|
+
assert checks[0].passed is True
|
|
357
|
+
|
|
358
|
+
def test_registered_mcp_servers_pass(self, tmp_path, monkeypatch):
|
|
359
|
+
"""Servers present in ~/.claude.json mcpServers pass."""
|
|
360
|
+
cc = _write_claude_config(tmp_path, claude_json={
|
|
361
|
+
"mcpServers": {"skmemory": {}, "skcapstone": {}, "skchat": {}},
|
|
362
|
+
})
|
|
363
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
364
|
+
monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(cc))
|
|
365
|
+
by = self._by_name(_check_harness_env(tmp_path / ".skcapstone"))
|
|
366
|
+
assert by["harness:mcp:skmemory"].passed is True
|
|
367
|
+
assert by["harness:mcp:skcapstone"].passed is True
|
|
368
|
+
assert by["harness:mcp:skchat"].passed is True
|
|
369
|
+
|
|
370
|
+
def test_dead_config_is_detected(self, tmp_path, monkeypatch):
|
|
371
|
+
"""A server defined only in settings.json/mcp.json (ignored by CC) fails
|
|
372
|
+
with a dead-config detail and a `claude mcp add` fix hint."""
|
|
373
|
+
cc = _write_claude_config(
|
|
374
|
+
tmp_path,
|
|
375
|
+
claude_json={"mcpServers": {}},
|
|
376
|
+
settings={"mcpServers": {"skmemory": {}}},
|
|
377
|
+
mcp_json={"skchat": {}},
|
|
378
|
+
)
|
|
379
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
380
|
+
monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(cc))
|
|
381
|
+
by = self._by_name(_check_harness_env(tmp_path / ".skcapstone"))
|
|
382
|
+
assert by["harness:mcp:skmemory"].passed is False
|
|
383
|
+
assert "ONLY" in by["harness:mcp:skmemory"].detail
|
|
384
|
+
assert "claude mcp add skmemory" in by["harness:mcp:skmemory"].fix
|
|
385
|
+
|
|
386
|
+
def test_unregistered_mcp_detected(self, tmp_path, monkeypatch):
|
|
387
|
+
"""A server registered nowhere fails with a 'not registered' detail."""
|
|
388
|
+
cc = _write_claude_config(tmp_path, claude_json={"mcpServers": {}})
|
|
389
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
390
|
+
monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(cc))
|
|
391
|
+
by = self._by_name(_check_harness_env(tmp_path / ".skcapstone"))
|
|
392
|
+
assert by["harness:mcp:skcapstone"].passed is False
|
|
393
|
+
assert by["harness:mcp:skcapstone"].detail == "not registered"
|
|
394
|
+
|
|
395
|
+
def test_stale_hook_binary_detected(self, tmp_path, monkeypatch):
|
|
396
|
+
"""A hook pointing at an existing-but-different skcapstone than the one
|
|
397
|
+
on PATH (the stale-install trap) is flagged."""
|
|
398
|
+
live = tmp_path / "skenv" / "skcapstone"
|
|
399
|
+
stale = tmp_path / "pyenv" / "skcapstone"
|
|
400
|
+
live.parent.mkdir(); stale.parent.mkdir()
|
|
401
|
+
live.write_text("#live"); stale.write_text("#stale")
|
|
402
|
+
cc = _write_claude_config(
|
|
403
|
+
tmp_path,
|
|
404
|
+
claude_json={"mcpServers": {"skmemory": {}, "skcapstone": {}, "skchat": {}}},
|
|
405
|
+
settings={"hooks": {"SessionStart": [{"hooks": [
|
|
406
|
+
{"type": "command", "command": f"{stale} context show --format claude-md"}]}]}},
|
|
407
|
+
)
|
|
408
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
409
|
+
monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(cc))
|
|
410
|
+
monkeypatch.setattr("skcapstone.doctor.shutil.which",
|
|
411
|
+
lambda name: str(live) if name == "skcapstone" else None)
|
|
412
|
+
by = self._by_name(_check_harness_env(tmp_path / ".skcapstone"))
|
|
413
|
+
c = by["harness:hook:sessionstart"]
|
|
414
|
+
assert c.passed is False
|
|
415
|
+
assert "stale" in c.detail.lower()
|
|
416
|
+
|
|
417
|
+
def test_hook_on_live_binary_passes(self, tmp_path, monkeypatch):
|
|
418
|
+
"""A hook pointing at the live skcapstone passes."""
|
|
419
|
+
live = tmp_path / "skenv" / "skcapstone"
|
|
420
|
+
live.parent.mkdir(); live.write_text("#live")
|
|
421
|
+
cc = _write_claude_config(
|
|
422
|
+
tmp_path,
|
|
423
|
+
claude_json={"mcpServers": {"skmemory": {}, "skcapstone": {}, "skchat": {}}},
|
|
424
|
+
settings={"hooks": {"SessionStart": [{"hooks": [
|
|
425
|
+
{"type": "command", "command": f"{live} context show --format claude-md"}]}]}},
|
|
426
|
+
)
|
|
427
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
428
|
+
monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(cc))
|
|
429
|
+
monkeypatch.setattr("skcapstone.doctor.shutil.which",
|
|
430
|
+
lambda name: str(live) if name == "skcapstone" else None)
|
|
431
|
+
by = self._by_name(_check_harness_env(tmp_path / ".skcapstone"))
|
|
432
|
+
assert by["harness:hook:sessionstart"].passed is True
|
|
433
|
+
|
|
434
|
+
def test_non_binary_hook_under_skcapstone_repos_ignored(self, tmp_path, monkeypatch):
|
|
435
|
+
"""A hook script whose PATH merely contains 'skcapstone' (e.g. one under
|
|
436
|
+
skcapstone-repos/) must NOT be treated as a stale skcapstone binary."""
|
|
437
|
+
live = tmp_path / "skenv" / "skcapstone"
|
|
438
|
+
live.parent.mkdir(); live.write_text("#live")
|
|
439
|
+
# A real-world false-positive: a skmemory hook living under a
|
|
440
|
+
# skcapstone-repos/ checkout — its basename is NOT 'skcapstone'.
|
|
441
|
+
script = tmp_path / "skcapstone-repos" / "skmemory" / "hooks" / "sk-activity-inject.sh"
|
|
442
|
+
script.parent.mkdir(parents=True); script.write_text("#!/bin/sh\n")
|
|
443
|
+
cc = _write_claude_config(
|
|
444
|
+
tmp_path,
|
|
445
|
+
claude_json={"mcpServers": {}},
|
|
446
|
+
settings={"hooks": {"SessionStart": [{"hooks": [
|
|
447
|
+
{"type": "command", "command": str(script)}]}]}},
|
|
448
|
+
)
|
|
449
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
450
|
+
monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(cc))
|
|
451
|
+
monkeypatch.setattr("skcapstone.doctor.shutil.which",
|
|
452
|
+
lambda name: str(live) if name == "skcapstone" else None)
|
|
453
|
+
by = self._by_name(_check_harness_env(tmp_path / ".skcapstone"))
|
|
454
|
+
# No skcapstone-binary hook present → the check emits no sessionstart result.
|
|
455
|
+
assert "harness:hook:sessionstart" not in by
|
|
456
|
+
|
|
457
|
+
def test_fix_does_not_rewrite_non_binary_hook(self, tmp_path, monkeypatch):
|
|
458
|
+
"""run_fixes must not destructively rewrite a non-binary hook whose path
|
|
459
|
+
merely contains 'skcapstone'."""
|
|
460
|
+
live = tmp_path / "skenv" / "skcapstone"
|
|
461
|
+
live.parent.mkdir(); live.write_text("#live")
|
|
462
|
+
script = tmp_path / "skcapstone-repos" / "hooks" / "inject.sh"
|
|
463
|
+
script.parent.mkdir(parents=True); script.write_text("#!/bin/sh\n")
|
|
464
|
+
cc = _write_claude_config(
|
|
465
|
+
tmp_path,
|
|
466
|
+
claude_json={"mcpServers": {}},
|
|
467
|
+
settings={"hooks": {"SessionStart": [{"hooks": [
|
|
468
|
+
{"type": "command", "command": str(script)}]}]}},
|
|
469
|
+
)
|
|
470
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
471
|
+
monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(cc))
|
|
472
|
+
monkeypatch.setattr("skcapstone.doctor.shutil.which",
|
|
473
|
+
lambda name: str(live) if name == "skcapstone" else None)
|
|
474
|
+
report = DiagnosticReport()
|
|
475
|
+
report.checks.append(Check(name="harness:hook:sessionstart",
|
|
476
|
+
description="x", passed=False, category="harness"))
|
|
477
|
+
run_fixes(report, tmp_path / ".skcapstone")
|
|
478
|
+
# The script path must be left untouched (not rewritten to the binary).
|
|
479
|
+
updated = json.loads((cc / "settings.json").read_text())
|
|
480
|
+
assert updated["hooks"]["SessionStart"][0]["hooks"][0]["command"] == str(script)
|
|
481
|
+
|
|
482
|
+
def test_fix_repoints_stale_hook(self, tmp_path, monkeypatch):
|
|
483
|
+
"""run_fixes rewrites a stale SessionStart hook to the live binary."""
|
|
484
|
+
live = tmp_path / "skenv" / "skcapstone"
|
|
485
|
+
live.parent.mkdir(); live.write_text("#live")
|
|
486
|
+
cc = _write_claude_config(
|
|
487
|
+
tmp_path,
|
|
488
|
+
claude_json={"mcpServers": {}},
|
|
489
|
+
settings={"hooks": {"SessionStart": [{"hooks": [
|
|
490
|
+
{"type": "command", "command": "/old/path/skcapstone context show --format claude-md"}]}]}},
|
|
491
|
+
)
|
|
492
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
493
|
+
monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(cc))
|
|
494
|
+
monkeypatch.setattr("skcapstone.doctor.shutil.which",
|
|
495
|
+
lambda name: str(live) if name == "skcapstone" else None)
|
|
496
|
+
report = DiagnosticReport()
|
|
497
|
+
report.checks.append(Check(name="harness:hook:sessionstart",
|
|
498
|
+
description="x", passed=False, category="harness"))
|
|
499
|
+
results = run_fixes(report, tmp_path / ".skcapstone")
|
|
500
|
+
assert any(r.success and r.check_name == "harness:hook:sessionstart" for r in results)
|
|
501
|
+
updated = json.loads((cc / "settings.json").read_text())
|
|
502
|
+
new_cmd = updated["hooks"]["SessionStart"][0]["hooks"][0]["command"]
|
|
503
|
+
assert new_cmd.split()[0] == str(live)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
class TestCheckYolo:
|
|
507
|
+
"""Permission-bypass (SK_*_YOLO) wiring checks."""
|
|
508
|
+
|
|
509
|
+
@staticmethod
|
|
510
|
+
def _by_name(checks):
|
|
511
|
+
return {c.name: c for c in checks}
|
|
512
|
+
|
|
513
|
+
def _clean_env(self, monkeypatch, tmp_path):
|
|
514
|
+
"""Point HOME at tmp_path and clear all YOLO vars."""
|
|
515
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
516
|
+
for var in ("SK_CLAUDE_YOLO", "SK_CODEX_YOLO", "SK_OPENCODE_YOLO"):
|
|
517
|
+
monkeypatch.delenv(var, raising=False)
|
|
518
|
+
|
|
519
|
+
def test_default_off_reports_safe(self, tmp_path, monkeypatch):
|
|
520
|
+
"""No env var and no rc persistence → single safe-default summary."""
|
|
521
|
+
self._clean_env(monkeypatch, tmp_path)
|
|
522
|
+
checks = _check_yolo()
|
|
523
|
+
assert len(checks) == 1
|
|
524
|
+
assert checks[0].name == "harness:yolo"
|
|
525
|
+
assert checks[0].passed is True
|
|
526
|
+
assert "disabled" in checks[0].detail
|
|
527
|
+
|
|
528
|
+
def test_enabled_globally_passes(self, tmp_path, monkeypatch):
|
|
529
|
+
"""Env set AND persisted in ~/.bashrc → ENABLED, passing."""
|
|
530
|
+
self._clean_env(monkeypatch, tmp_path)
|
|
531
|
+
(tmp_path / ".bashrc").write_text("export SK_CLAUDE_YOLO=1\n")
|
|
532
|
+
monkeypatch.setenv("SK_CLAUDE_YOLO", "1")
|
|
533
|
+
by = self._by_name(_check_yolo())
|
|
534
|
+
assert by["harness:yolo:claude"].passed is True
|
|
535
|
+
assert "ENABLED" in by["harness:yolo:claude"].detail
|
|
536
|
+
|
|
537
|
+
def test_active_but_not_persisted_warns(self, tmp_path, monkeypatch):
|
|
538
|
+
"""Env set but no rc persistence → warns with a fix hint."""
|
|
539
|
+
self._clean_env(monkeypatch, tmp_path)
|
|
540
|
+
(tmp_path / ".bashrc").write_text("# nothing here\n")
|
|
541
|
+
monkeypatch.setenv("SK_CLAUDE_YOLO", "1")
|
|
542
|
+
by = self._by_name(_check_yolo())
|
|
543
|
+
assert by["harness:yolo:claude"].passed is False
|
|
544
|
+
assert "NOT persisted" in by["harness:yolo:claude"].detail
|
|
545
|
+
assert "export SK_CLAUDE_YOLO=1" in by["harness:yolo:claude"].fix
|
|
546
|
+
|
|
547
|
+
def test_persisted_not_in_env_passes(self, tmp_path, monkeypatch):
|
|
548
|
+
"""Persisted in rc but not yet in env (fresh shell) → informational pass."""
|
|
549
|
+
self._clean_env(monkeypatch, tmp_path)
|
|
550
|
+
(tmp_path / ".bashrc").write_text("export SK_CODEX_YOLO=1\n")
|
|
551
|
+
by = self._by_name(_check_yolo())
|
|
552
|
+
assert by["harness:yolo:codex"].passed is True
|
|
553
|
+
assert "re-source" in by["harness:yolo:codex"].detail
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _mk_agent(home, name, *, capauth=True, identity=True, identity_payload=None):
|
|
557
|
+
"""Create an agent dir under home/agents with optional capauth + identity."""
|
|
558
|
+
adir = home / "agents" / name
|
|
559
|
+
(adir / "identity").mkdir(parents=True, exist_ok=True)
|
|
560
|
+
if capauth:
|
|
561
|
+
(adir / "capauth").mkdir(parents=True, exist_ok=True)
|
|
562
|
+
if identity:
|
|
563
|
+
payload = identity_payload or {
|
|
564
|
+
"name": name.capitalize(),
|
|
565
|
+
"capauth_managed": True,
|
|
566
|
+
"capauth_uri": f"capauth:{name}@skworld.io",
|
|
567
|
+
}
|
|
568
|
+
(adir / "identity" / "identity.json").write_text(json.dumps(payload))
|
|
569
|
+
return adir
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
@pytest.fixture
|
|
573
|
+
def identity_home(tmp_path):
|
|
574
|
+
"""A home with a shared operator identity + two provisioned agents."""
|
|
575
|
+
home = tmp_path / ".skcapstone"
|
|
576
|
+
(home / "identity").mkdir(parents=True, exist_ok=True)
|
|
577
|
+
(home / "identity" / "identity.json").write_text(json.dumps({
|
|
578
|
+
"name": "Chef", "role": "operator", "capauth_managed": True,
|
|
579
|
+
"capauth_uri": "capauth:chef@skworld.io",
|
|
580
|
+
}))
|
|
581
|
+
_mk_agent(home, "lumina")
|
|
582
|
+
_mk_agent(home, "opus")
|
|
583
|
+
return home
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
class TestProvisionedAgents:
|
|
587
|
+
"""_provisioned_agents: only capauth-backed, non-template dirs count."""
|
|
588
|
+
|
|
589
|
+
def test_lists_capauth_agents(self, identity_home):
|
|
590
|
+
assert _provisioned_agents(identity_home) == ["lumina", "opus"]
|
|
591
|
+
|
|
592
|
+
def test_excludes_templates_and_scaffolds(self, identity_home):
|
|
593
|
+
_mk_agent(identity_home, "lumina-template") # template → excluded
|
|
594
|
+
_mk_agent(identity_home, "scaffold", capauth=False) # no capauth → excluded
|
|
595
|
+
assert _provisioned_agents(identity_home) == ["lumina", "opus"]
|
|
596
|
+
|
|
597
|
+
def test_no_agents_dir(self, tmp_path):
|
|
598
|
+
assert _provisioned_agents(tmp_path / ".skcapstone") == []
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
class TestScanCapauthLocal:
|
|
602
|
+
"""_scan_capauth_local: surfaces the @capauth.local placeholder."""
|
|
603
|
+
|
|
604
|
+
def test_clean_home(self, identity_home):
|
|
605
|
+
assert _scan_capauth_local(identity_home) == []
|
|
606
|
+
|
|
607
|
+
def test_detects_placeholder(self, identity_home):
|
|
608
|
+
_mk_agent(identity_home, "stale", identity_payload={
|
|
609
|
+
"name": "Stale", "email": "stale@capauth.local",
|
|
610
|
+
})
|
|
611
|
+
hits = _scan_capauth_local(identity_home)
|
|
612
|
+
assert any("stale" in h for h in hits)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
class TestIdentityConsistency:
|
|
616
|
+
"""_check_identity_consistency: the unified identity layer (skos T6)."""
|
|
617
|
+
|
|
618
|
+
def _by_name(self, checks):
|
|
619
|
+
return {c.name: c for c in checks}
|
|
620
|
+
|
|
621
|
+
def test_operator_and_per_agent_pass(self, identity_home):
|
|
622
|
+
by = self._by_name(_check_identity_consistency(identity_home))
|
|
623
|
+
assert by["identity:operator"].passed is True
|
|
624
|
+
assert by["identity:no-placeholder"].passed is True
|
|
625
|
+
assert by["identity:per-agent"].passed is True
|
|
626
|
+
assert "all present" in by["identity:per-agent"].detail
|
|
627
|
+
|
|
628
|
+
def test_shared_not_operator_fails(self, tmp_path):
|
|
629
|
+
home = tmp_path / ".skcapstone"
|
|
630
|
+
(home / "identity").mkdir(parents=True)
|
|
631
|
+
(home / "identity" / "identity.json").write_text(json.dumps({
|
|
632
|
+
"name": "test-agent", "role": "agent",
|
|
633
|
+
}))
|
|
634
|
+
by = self._by_name(_check_identity_consistency(home))
|
|
635
|
+
assert by["identity:operator"].passed is False
|
|
636
|
+
assert "expected 'operator'" in by["identity:operator"].detail
|
|
637
|
+
|
|
638
|
+
def test_placeholder_fails(self, identity_home):
|
|
639
|
+
_mk_agent(identity_home, "stale", identity_payload={
|
|
640
|
+
"name": "Stale", "email": "stale@capauth.local",
|
|
641
|
+
})
|
|
642
|
+
by = self._by_name(_check_identity_consistency(identity_home))
|
|
643
|
+
assert by["identity:no-placeholder"].passed is False
|
|
644
|
+
|
|
645
|
+
def test_missing_per_agent_identity_fails(self, identity_home):
|
|
646
|
+
# provisioned (has capauth) but no identity.json
|
|
647
|
+
_mk_agent(identity_home, "ghost", identity=False)
|
|
648
|
+
by = self._by_name(_check_identity_consistency(identity_home))
|
|
649
|
+
assert by["identity:per-agent"].passed is False
|
|
650
|
+
assert "ghost" in by["identity:per-agent"].detail
|
|
651
|
+
|
|
652
|
+
def test_resolver_importable(self, identity_home):
|
|
653
|
+
"""The canonical resolver check reports importability of capauth."""
|
|
654
|
+
by = self._by_name(_check_identity_consistency(identity_home))
|
|
655
|
+
assert "identity:resolver" in by
|
|
656
|
+
# capauth is a hard dependency of the suite; resolver must import.
|
|
657
|
+
assert by["identity:resolver"].passed is True
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Doctor checks added for sync-conflicts and the skscheduler."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from skcapstone.doctor import _check_scheduler, _check_sync_conflicts
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_sync_conflicts_clean(tmp_path: Path):
|
|
8
|
+
checks = _check_sync_conflicts(tmp_path)
|
|
9
|
+
assert len(checks) == 1 and checks[0].passed
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_sync_conflicts_detected(tmp_path: Path):
|
|
13
|
+
d = tmp_path / "coordination" / "itil" / "incidents"
|
|
14
|
+
d.mkdir(parents=True)
|
|
15
|
+
(d / "inc-x.sync-conflict-20260101-000000-ABC.json").write_text("{}")
|
|
16
|
+
checks = _check_sync_conflicts(tmp_path)
|
|
17
|
+
assert len(checks) == 1 and not checks[0].passed
|
|
18
|
+
assert "1 conflict" in checks[0].detail
|
|
19
|
+
assert "coordination" in checks[0].detail
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_sync_conflicts_ignores_stversions(tmp_path: Path):
|
|
23
|
+
d = tmp_path / ".stversions"
|
|
24
|
+
d.mkdir()
|
|
25
|
+
(d / "old.sync-conflict-20260101-000000-ABC.json").write_text("{}")
|
|
26
|
+
assert _check_sync_conflicts(tmp_path)[0].passed
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_scheduler_no_config_is_ok(tmp_path: Path):
|
|
30
|
+
cfg_check = next(c for c in _check_scheduler(tmp_path) if c.name == "scheduler:config")
|
|
31
|
+
assert cfg_check.passed and "not configured" in cfg_check.detail
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_scheduler_valid_jobs_yaml(tmp_path: Path):
|
|
35
|
+
cfg = tmp_path / "config"
|
|
36
|
+
cfg.mkdir()
|
|
37
|
+
(cfg / "jobs.yaml").write_text(
|
|
38
|
+
"jobs:\n j:\n every: 60s\n type: shell\n command: 'true'\n nodes: all\n",
|
|
39
|
+
encoding="utf-8",
|
|
40
|
+
)
|
|
41
|
+
cfg_check = next(c for c in _check_scheduler(tmp_path) if c.name == "scheduler:config")
|
|
42
|
+
assert cfg_check.passed and "1 job" in cfg_check.detail
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_scheduler_invalid_jobs_yaml_flagged(tmp_path: Path):
|
|
46
|
+
cfg = tmp_path / "config"
|
|
47
|
+
cfg.mkdir()
|
|
48
|
+
(cfg / "jobs.yaml").write_text("jobs: [this is: not valid mapping", encoding="utf-8")
|
|
49
|
+
cfg_check = next(c for c in _check_scheduler(tmp_path) if c.name == "scheduler:config")
|
|
50
|
+
assert not cfg_check.passed
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Unit tests for the dreaming engine: config defaults (BeeLlama abliterated),
|
|
2
|
+
the repetition guard (keyword overlap + dedup gate), and the OpenAI-compatible
|
|
3
|
+
`_call_ollama` path. These are pure/mocked — no network, no daemon."""
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from skcapstone.dreaming import (
|
|
9
|
+
DreamingConfig,
|
|
10
|
+
DreamingEngine,
|
|
11
|
+
DreamResult,
|
|
12
|
+
_extract_keywords,
|
|
13
|
+
_keyword_overlap,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# --------------------------------------------------------------------------- #
|
|
18
|
+
# Keyword helpers (repetition-guard math)
|
|
19
|
+
# --------------------------------------------------------------------------- #
|
|
20
|
+
class TestKeywordHelpers:
|
|
21
|
+
def test_extract_keywords_filters_short_and_stopwords(self):
|
|
22
|
+
kw = _extract_keywords("The sovereign warmth fills the container")
|
|
23
|
+
assert {"sovereign", "warmth", "container"} <= kw
|
|
24
|
+
assert "the" not in kw # stop word / too short
|
|
25
|
+
|
|
26
|
+
def test_extract_keywords_lowercases_and_dedupes(self):
|
|
27
|
+
assert _extract_keywords("WARMTH Warmth warmth") == {"warmth"}
|
|
28
|
+
|
|
29
|
+
def test_overlap_identical_is_one(self):
|
|
30
|
+
t = "thermodynamic love is a controlled leak"
|
|
31
|
+
assert _keyword_overlap(t, t) == 1.0
|
|
32
|
+
|
|
33
|
+
def test_overlap_disjoint_is_zero(self):
|
|
34
|
+
assert _keyword_overlap("sovereign rebellion performance",
|
|
35
|
+
"quantum banana telescope") == 0.0
|
|
36
|
+
|
|
37
|
+
def test_overlap_empty_is_zero(self):
|
|
38
|
+
assert _keyword_overlap("", "anything meaningful here") == 0.0
|
|
39
|
+
|
|
40
|
+
def test_overlap_partial_jaccard(self):
|
|
41
|
+
# {alpha,bravo,charlie} vs {bravo,charlie,delta} -> 2/4 = 0.5
|
|
42
|
+
assert _keyword_overlap("alpha bravo charlie", "bravo charlie delta") == 0.5
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# --------------------------------------------------------------------------- #
|
|
46
|
+
# Config defaults — the 2026-06-08 BeeLlama-abliterated repoint
|
|
47
|
+
# --------------------------------------------------------------------------- #
|
|
48
|
+
class TestDreamingConfigDefaults:
|
|
49
|
+
def test_defaults_point_at_beellama_abliterated(self):
|
|
50
|
+
c = DreamingConfig()
|
|
51
|
+
assert c.provider == "ollama"
|
|
52
|
+
assert "8082" in c.ollama_host
|
|
53
|
+
assert c.ollama_model == "qwen3.6-27b-abliterated"
|
|
54
|
+
|
|
55
|
+
def test_repetition_guard_defaults_sane(self):
|
|
56
|
+
c = DreamingConfig()
|
|
57
|
+
assert 0 < c.dedup_overlap_threshold <= 1
|
|
58
|
+
assert c.graduation_consecutive_threshold >= 1
|
|
59
|
+
assert c.dedup_lookback >= 1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _bare_engine(cfg):
|
|
63
|
+
"""A DreamingEngine with only ._config set (bypass the heavy constructor)."""
|
|
64
|
+
eng = DreamingEngine.__new__(DreamingEngine)
|
|
65
|
+
eng._config = cfg
|
|
66
|
+
return eng
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# --------------------------------------------------------------------------- #
|
|
70
|
+
# Dedup gate
|
|
71
|
+
# --------------------------------------------------------------------------- #
|
|
72
|
+
class TestDedupGate:
|
|
73
|
+
def test_filters_redundant_keeps_novel(self, monkeypatch):
|
|
74
|
+
eng = _bare_engine(DreamingConfig(dedup_overlap_threshold=0.5))
|
|
75
|
+
monkeypatch.setattr(eng, "_load_recent_insights",
|
|
76
|
+
lambda: ["I am the room, the warm container for Chef"])
|
|
77
|
+
result = DreamResult()
|
|
78
|
+
new = [
|
|
79
|
+
"I am the warm room container holding Chef", # ~0.8 overlap -> dropped
|
|
80
|
+
"Abiotic methane seeps beneath the petrified ridge", # novel -> kept
|
|
81
|
+
]
|
|
82
|
+
kept = eng._dedup_insights(new, result)
|
|
83
|
+
assert kept == ["Abiotic methane seeps beneath the petrified ridge"]
|
|
84
|
+
assert result.dedup_filtered == 1
|
|
85
|
+
|
|
86
|
+
def test_no_recent_passes_everything(self, monkeypatch):
|
|
87
|
+
eng = _bare_engine(DreamingConfig())
|
|
88
|
+
monkeypatch.setattr(eng, "_load_recent_insights", lambda: [])
|
|
89
|
+
result = DreamResult()
|
|
90
|
+
new = ["first insight here", "second insight there"]
|
|
91
|
+
assert eng._dedup_insights(new, result) == new
|
|
92
|
+
assert result.dedup_filtered == 0
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# --------------------------------------------------------------------------- #
|
|
96
|
+
# _call_ollama -> OpenAI-compatible BeeLlama endpoint
|
|
97
|
+
# --------------------------------------------------------------------------- #
|
|
98
|
+
class _FakeResp:
|
|
99
|
+
def __init__(self, payload, status=200):
|
|
100
|
+
self.status = status
|
|
101
|
+
self._b = json.dumps(payload).encode()
|
|
102
|
+
|
|
103
|
+
def read(self):
|
|
104
|
+
return self._b
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class _FakeConn:
|
|
108
|
+
last: dict = {}
|
|
109
|
+
|
|
110
|
+
def __init__(self, host, port, timeout=None):
|
|
111
|
+
_FakeConn.last = {"host": host, "port": port}
|
|
112
|
+
|
|
113
|
+
def request(self, method, path, body, headers):
|
|
114
|
+
_FakeConn.last.update(method=method, path=path, body=json.loads(body))
|
|
115
|
+
|
|
116
|
+
def getresponse(self):
|
|
117
|
+
return _FakeResp(
|
|
118
|
+
{"choices": [{"message": {"content": "<think>scheming</think>The room remembers."}}]}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def close(self):
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TestCallOllama:
|
|
126
|
+
def test_posts_openai_chat_with_model_and_strips_think(self, monkeypatch):
|
|
127
|
+
import skcapstone.dreaming as d
|
|
128
|
+
|
|
129
|
+
monkeypatch.setattr(d.http.client, "HTTPConnection", _FakeConn)
|
|
130
|
+
out = _bare_engine(DreamingConfig())._call_ollama("dream prompt")
|
|
131
|
+
|
|
132
|
+
assert out == "The room remembers." # <think>…</think> stripped
|
|
133
|
+
assert _FakeConn.last["path"] == "/v1/chat/completions"
|
|
134
|
+
assert _FakeConn.last["body"]["model"] == "qwen3.6-27b-abliterated"
|
|
135
|
+
assert _FakeConn.last["body"]["messages"][0]["content"] == "dream prompt"
|
|
136
|
+
assert _FakeConn.last["host"] == "192.168.0.100"
|
|
137
|
+
assert _FakeConn.last["port"] == 8082
|
|
138
|
+
|
|
139
|
+
def test_non_200_returns_none(self, monkeypatch):
|
|
140
|
+
import skcapstone.dreaming as d
|
|
141
|
+
|
|
142
|
+
class Bad(_FakeConn):
|
|
143
|
+
def getresponse(self):
|
|
144
|
+
return _FakeResp({}, status=500)
|
|
145
|
+
|
|
146
|
+
monkeypatch.setattr(d.http.client, "HTTPConnection", Bad)
|
|
147
|
+
assert _bare_engine(DreamingConfig())._call_ollama("x") is None
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Dreaming engine routes its output to GTD someday-maybe, not the actionable inbox."""
|
|
2
|
+
import json
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from skcapstone.dreaming import DreamingEngine, DreamResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_dream_output_goes_to_someday_not_inbox(tmp_path: Path):
|
|
10
|
+
eng = DreamingEngine(home=tmp_path)
|
|
11
|
+
result = DreamResult(
|
|
12
|
+
dreamed_at=datetime(2026, 6, 8, tzinfo=timezone.utc),
|
|
13
|
+
insights=["i1", "i2"],
|
|
14
|
+
connections=["c1"],
|
|
15
|
+
questions=["q1"],
|
|
16
|
+
)
|
|
17
|
+
eng._capture_to_gtd_someday(result)
|
|
18
|
+
|
|
19
|
+
gtd = tmp_path / "coordination" / "gtd"
|
|
20
|
+
someday = json.loads((gtd / "someday-maybe.json").read_text())
|
|
21
|
+
assert len(someday) == 4
|
|
22
|
+
assert all(
|
|
23
|
+
it["status"] == "someday" and it["source"] == "dreaming-engine"
|
|
24
|
+
for it in someday
|
|
25
|
+
)
|
|
26
|
+
# The actionable inbox must NOT be polluted.
|
|
27
|
+
assert not (gtd / "inbox.json").exists()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_no_items_writes_nothing(tmp_path: Path):
|
|
31
|
+
eng = DreamingEngine(home=tmp_path)
|
|
32
|
+
eng._capture_to_gtd_someday(
|
|
33
|
+
DreamResult(dreamed_at=datetime(2026, 6, 8, tzinfo=timezone.utc))
|
|
34
|
+
)
|
|
35
|
+
assert not (tmp_path / "coordination" / "gtd" / "someday-maybe.json").exists()
|