@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,598 @@
|
|
|
1
|
+
"""Dual-mode integration backbone harness — EPIC acceptance gate.
|
|
2
|
+
|
|
3
|
+
This module is the **system-level acceptance test** for the
|
|
4
|
+
sk* ⇄ skcapstone optional integration backbone (EPIC fca7f138, ADR
|
|
5
|
+
``docs/ADR-optional-integration-backbone.md``).
|
|
6
|
+
|
|
7
|
+
For EVERY adapter that implements the integration contract it verifies TWO modes:
|
|
8
|
+
|
|
9
|
+
Mode A — STANDALONE
|
|
10
|
+
``SK_STANDALONE=1`` is set (or ``_sdk`` patched to ``None``). The adapter
|
|
11
|
+
must not crash, its ``is_present()`` must return ``False``, ``alert()``
|
|
12
|
+
must return ``False`` and fall back to native logging, ``ensure_schedule()``
|
|
13
|
+
must return ``False`` without writing any files, and ``register_self()``
|
|
14
|
+
must return ``False``.
|
|
15
|
+
|
|
16
|
+
Mode B — INTEGRATED
|
|
17
|
+
skcapstone is importable and available. ``SKCAPSTONE_HOME`` is sandboxed
|
|
18
|
+
to ``tmp_path`` so no files touch ``~/.skcapstone``. The adapter must:
|
|
19
|
+
- ``is_present()`` → True
|
|
20
|
+
- ``alert(event, payload, level)`` → True, and
|
|
21
|
+
* the PubSub topic directory ``<home>/pubsub/topics/<svc>.<level>/``
|
|
22
|
+
must exist and contain exactly one ``msg-*.json`` whose payload
|
|
23
|
+
contains ``{"event": <event>, ...}`` and whose ``tags`` include the
|
|
24
|
+
level string (severity-based routing).
|
|
25
|
+
* topic suffix IS the severity (e.g. ``skmemory.error``) — not the
|
|
26
|
+
event name — so ``skcapstone alerts`` wildcards ``*.error`` etc.
|
|
27
|
+
match correctly.
|
|
28
|
+
- ``ensure_schedule()`` → True, and
|
|
29
|
+
* ``<home>/config/jobs.d/<job_name>.yaml`` must exist and be valid YAML.
|
|
30
|
+
- ``register_self()`` → True, and
|
|
31
|
+
* ``<home>/registry/<svc>.json`` must exist.
|
|
32
|
+
|
|
33
|
+
Each adapter is described by a namedtuple ``AdapterSpec``; the parametrized
|
|
34
|
+
test IDs use the service names so failures are readable.
|
|
35
|
+
|
|
36
|
+
LEAK CHECK: after integrated-mode tests the test verifies that no fragments
|
|
37
|
+
leaked to the *real* ``~/.skcapstone/config/jobs.d/`` or
|
|
38
|
+
``~/.skcapstone/registry/`` directories.
|
|
39
|
+
|
|
40
|
+
Reference adapter: ``skmemory/skmemory/integration.py`` (commit be33179).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import importlib
|
|
46
|
+
import json
|
|
47
|
+
import logging
|
|
48
|
+
import os
|
|
49
|
+
import sys
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
from types import ModuleType
|
|
52
|
+
from typing import NamedTuple, Optional
|
|
53
|
+
|
|
54
|
+
import pytest
|
|
55
|
+
import yaml
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Adapter registry
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AdapterSpec(NamedTuple):
|
|
63
|
+
"""Describes one sk* adapter under test."""
|
|
64
|
+
|
|
65
|
+
#: Human-readable service name — used as the pytest parametrize ID.
|
|
66
|
+
service: str
|
|
67
|
+
#: Python module path to the adapter (importable from the installed venv).
|
|
68
|
+
module_path: str
|
|
69
|
+
#: The adapter's job-name constant (the ``jobs.d/<name>.yaml`` key).
|
|
70
|
+
job_name: str
|
|
71
|
+
#: A representative non-critical alert level to exercise the routing.
|
|
72
|
+
alert_level: str = "warn"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
#: All adapters that implement the integration contract.
|
|
76
|
+
#:
|
|
77
|
+
#: skchat (ad4f721a) is owned by another thread — deliberately excluded.
|
|
78
|
+
#: skgateway is Node/non-Python — it is tested via its own Node harness
|
|
79
|
+
#: (``skgateway/tests/integration.test.mjs``); excluded from Python parametrize.
|
|
80
|
+
ADAPTERS: list[AdapterSpec] = [
|
|
81
|
+
AdapterSpec(
|
|
82
|
+
service="skmemory",
|
|
83
|
+
module_path="skmemory.integration",
|
|
84
|
+
job_name="skmemory_sweep",
|
|
85
|
+
),
|
|
86
|
+
AdapterSpec(
|
|
87
|
+
service="sksecurity",
|
|
88
|
+
module_path="sksecurity.integration",
|
|
89
|
+
job_name="sksecurity_intel_refresh",
|
|
90
|
+
),
|
|
91
|
+
AdapterSpec(
|
|
92
|
+
service="skcomms",
|
|
93
|
+
module_path="skcomms.integration",
|
|
94
|
+
job_name="skcomms_health_sweep",
|
|
95
|
+
),
|
|
96
|
+
AdapterSpec(
|
|
97
|
+
service="capauth",
|
|
98
|
+
module_path="capauth.integration",
|
|
99
|
+
job_name="capauth_key_rotation_check",
|
|
100
|
+
),
|
|
101
|
+
AdapterSpec(
|
|
102
|
+
service="cloud9",
|
|
103
|
+
module_path="cloud9.integration",
|
|
104
|
+
job_name="cloud9_rehydration_check",
|
|
105
|
+
),
|
|
106
|
+
AdapterSpec(
|
|
107
|
+
service="skvoice",
|
|
108
|
+
module_path="skvoice.integration",
|
|
109
|
+
job_name="skvoice_health",
|
|
110
|
+
),
|
|
111
|
+
AdapterSpec(
|
|
112
|
+
service="skseed",
|
|
113
|
+
module_path="skseed.integration",
|
|
114
|
+
job_name="skseed_audit",
|
|
115
|
+
),
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# Fixtures
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@pytest.fixture
|
|
125
|
+
def skcap_sandbox(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
126
|
+
"""Sandbox skcapstone's home to ``tmp_path``; return the sandboxed root.
|
|
127
|
+
|
|
128
|
+
Sets both ``SKCAPSTONE_HOME`` (read by ``shared_home()``) and patches
|
|
129
|
+
``skcapstone.AGENT_HOME`` (captured at import time) so every SDK write
|
|
130
|
+
goes to the temp tree, never to ``~/.skcapstone``.
|
|
131
|
+
"""
|
|
132
|
+
monkeypatch.setenv("SKCAPSTONE_HOME", str(tmp_path))
|
|
133
|
+
import skcapstone as pkg
|
|
134
|
+
|
|
135
|
+
monkeypatch.setattr(pkg, "AGENT_HOME", str(tmp_path))
|
|
136
|
+
return tmp_path
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@pytest.fixture
|
|
140
|
+
def real_jobs_d() -> Path:
|
|
141
|
+
"""Return the *actual* ~/.skcapstone jobs.d path for leak detection.
|
|
142
|
+
|
|
143
|
+
Deliberately ignores SKCAPSTONE_HOME so that the leak check can verify
|
|
144
|
+
that integrated-mode writes go to the sandboxed path only, not to the
|
|
145
|
+
developer's real agent home.
|
|
146
|
+
"""
|
|
147
|
+
return Path.home() / ".skcapstone" / "config" / "jobs.d"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@pytest.fixture
|
|
151
|
+
def real_registry() -> Path:
|
|
152
|
+
"""Return the *actual* ~/.skcapstone registry path for leak detection."""
|
|
153
|
+
return Path.home() / ".skcapstone" / "registry"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# Helper: import adapter module freshly so monkeypatches take effect
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _load_adapter(spec: AdapterSpec) -> Optional[ModuleType]:
|
|
162
|
+
"""Import the adapter module, returning None if not installed."""
|
|
163
|
+
try:
|
|
164
|
+
mod = importlib.import_module(spec.module_path)
|
|
165
|
+
# Force a fresh re-evaluation of the module-level ``_sdk`` guard by
|
|
166
|
+
# reloading. This is safe in a test context.
|
|
167
|
+
return importlib.reload(mod)
|
|
168
|
+
except ImportError:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
# Mode A — STANDALONE tests
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@pytest.mark.parametrize("spec", ADAPTERS, ids=[a.service for a in ADAPTERS])
|
|
178
|
+
class TestStandaloneMode:
|
|
179
|
+
"""Each adapter behaves correctly when skcapstone is absent / forced off.
|
|
180
|
+
|
|
181
|
+
Two sub-strategies are exercised:
|
|
182
|
+
1. ``SK_STANDALONE=1`` env var forces native mode even when installed.
|
|
183
|
+
2. ``_sdk`` attribute patched to ``None`` simulates absent package.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
def test_is_present_false_when_env_standalone(
|
|
187
|
+
self, spec: AdapterSpec, monkeypatch: pytest.MonkeyPatch
|
|
188
|
+
):
|
|
189
|
+
"""is_present() returns False when SK_STANDALONE=1, regardless of package."""
|
|
190
|
+
monkeypatch.setenv("SK_STANDALONE", "1")
|
|
191
|
+
mod = _load_adapter(spec)
|
|
192
|
+
if mod is None:
|
|
193
|
+
pytest.skip(f"{spec.module_path} not installed")
|
|
194
|
+
# Reload after env is set so the module-level _sdk guard re-runs.
|
|
195
|
+
importlib.reload(mod)
|
|
196
|
+
assert mod.is_present() is False
|
|
197
|
+
|
|
198
|
+
def test_alert_returns_false_when_standalone(
|
|
199
|
+
self, spec: AdapterSpec, monkeypatch: pytest.MonkeyPatch
|
|
200
|
+
):
|
|
201
|
+
"""alert() returns False in standalone mode (native logging fallback)."""
|
|
202
|
+
monkeypatch.setenv("SK_STANDALONE", "1")
|
|
203
|
+
mod = _load_adapter(spec)
|
|
204
|
+
if mod is None:
|
|
205
|
+
pytest.skip(f"{spec.module_path} not installed")
|
|
206
|
+
importlib.reload(mod)
|
|
207
|
+
result = mod.alert("test_event", {"detail": "x"}, "warn")
|
|
208
|
+
assert result is False
|
|
209
|
+
|
|
210
|
+
def test_alert_does_not_raise_when_sdk_absent(
|
|
211
|
+
self, spec: AdapterSpec, monkeypatch: pytest.MonkeyPatch
|
|
212
|
+
):
|
|
213
|
+
"""alert() never raises even when _sdk is None."""
|
|
214
|
+
mod = _load_adapter(spec)
|
|
215
|
+
if mod is None:
|
|
216
|
+
pytest.skip(f"{spec.module_path} not installed")
|
|
217
|
+
monkeypatch.setattr(mod, "_sdk", None)
|
|
218
|
+
# Should not raise
|
|
219
|
+
result = mod.alert("boom", {"x": 1}, "error")
|
|
220
|
+
assert result is False
|
|
221
|
+
|
|
222
|
+
def test_ensure_schedule_returns_false_when_standalone(
|
|
223
|
+
self, spec: AdapterSpec, monkeypatch: pytest.MonkeyPatch
|
|
224
|
+
):
|
|
225
|
+
"""ensure_schedule() returns False and writes no files in standalone."""
|
|
226
|
+
monkeypatch.setenv("SK_STANDALONE", "1")
|
|
227
|
+
mod = _load_adapter(spec)
|
|
228
|
+
if mod is None:
|
|
229
|
+
pytest.skip(f"{spec.module_path} not installed")
|
|
230
|
+
importlib.reload(mod)
|
|
231
|
+
result = mod.ensure_schedule()
|
|
232
|
+
assert result is False
|
|
233
|
+
|
|
234
|
+
def test_ensure_schedule_does_not_raise_when_sdk_absent(
|
|
235
|
+
self, spec: AdapterSpec, monkeypatch: pytest.MonkeyPatch
|
|
236
|
+
):
|
|
237
|
+
"""ensure_schedule() never raises when _sdk is None."""
|
|
238
|
+
mod = _load_adapter(spec)
|
|
239
|
+
if mod is None:
|
|
240
|
+
pytest.skip(f"{spec.module_path} not installed")
|
|
241
|
+
monkeypatch.setattr(mod, "_sdk", None)
|
|
242
|
+
result = mod.ensure_schedule()
|
|
243
|
+
assert result is False
|
|
244
|
+
|
|
245
|
+
def test_register_self_returns_false_when_standalone(
|
|
246
|
+
self, spec: AdapterSpec, monkeypatch: pytest.MonkeyPatch
|
|
247
|
+
):
|
|
248
|
+
"""register_self() returns False in standalone mode."""
|
|
249
|
+
monkeypatch.setenv("SK_STANDALONE", "1")
|
|
250
|
+
mod = _load_adapter(spec)
|
|
251
|
+
if mod is None:
|
|
252
|
+
pytest.skip(f"{spec.module_path} not installed")
|
|
253
|
+
importlib.reload(mod)
|
|
254
|
+
result = mod.register_self()
|
|
255
|
+
assert result is False
|
|
256
|
+
|
|
257
|
+
def test_no_pubsub_files_written_in_standalone(
|
|
258
|
+
self,
|
|
259
|
+
spec: AdapterSpec,
|
|
260
|
+
tmp_path: Path,
|
|
261
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
262
|
+
):
|
|
263
|
+
"""No pubsub topic files appear under tmp_path in standalone mode."""
|
|
264
|
+
monkeypatch.setenv("SK_STANDALONE", "1")
|
|
265
|
+
monkeypatch.setenv("SKCAPSTONE_HOME", str(tmp_path))
|
|
266
|
+
mod = _load_adapter(spec)
|
|
267
|
+
if mod is None:
|
|
268
|
+
pytest.skip(f"{spec.module_path} not installed")
|
|
269
|
+
importlib.reload(mod)
|
|
270
|
+
mod.alert("no_files_please", {"reason": "standalone"}, "error")
|
|
271
|
+
pubsub_root = tmp_path / "pubsub" / "topics"
|
|
272
|
+
topic_files = list(pubsub_root.glob("**/*.json")) if pubsub_root.exists() else []
|
|
273
|
+
assert topic_files == [], (
|
|
274
|
+
f"Standalone mode wrote pubsub files: {topic_files}"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def test_no_jobs_d_files_written_in_standalone(
|
|
278
|
+
self,
|
|
279
|
+
spec: AdapterSpec,
|
|
280
|
+
tmp_path: Path,
|
|
281
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
282
|
+
):
|
|
283
|
+
"""No jobs.d files appear under tmp_path in standalone mode."""
|
|
284
|
+
monkeypatch.setenv("SK_STANDALONE", "1")
|
|
285
|
+
monkeypatch.setenv("SKCAPSTONE_HOME", str(tmp_path))
|
|
286
|
+
mod = _load_adapter(spec)
|
|
287
|
+
if mod is None:
|
|
288
|
+
pytest.skip(f"{spec.module_path} not installed")
|
|
289
|
+
importlib.reload(mod)
|
|
290
|
+
mod.ensure_schedule()
|
|
291
|
+
jobs_d = tmp_path / "config" / "jobs.d"
|
|
292
|
+
job_files = list(jobs_d.glob("*.yaml")) if jobs_d.exists() else []
|
|
293
|
+
assert job_files == [], (
|
|
294
|
+
f"Standalone mode wrote jobs.d files: {job_files}"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# ---------------------------------------------------------------------------
|
|
299
|
+
# Mode B — INTEGRATED tests
|
|
300
|
+
# ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@pytest.mark.parametrize("spec", ADAPTERS, ids=[a.service for a in ADAPTERS])
|
|
304
|
+
class TestIntegratedMode:
|
|
305
|
+
"""Each adapter routes correctly through skcapstone when present.
|
|
306
|
+
|
|
307
|
+
Uses the ``skcap_sandbox`` fixture to redirect all file writes to a
|
|
308
|
+
temporary directory, guaranteeing no leaks to the real home.
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
def test_is_present_true_when_integrated(
|
|
312
|
+
self, spec: AdapterSpec, skcap_sandbox: Path, monkeypatch: pytest.MonkeyPatch
|
|
313
|
+
):
|
|
314
|
+
"""is_present() returns True when skcapstone is installed and present."""
|
|
315
|
+
monkeypatch.delenv("SK_STANDALONE", raising=False)
|
|
316
|
+
mod = _load_adapter(spec)
|
|
317
|
+
if mod is None:
|
|
318
|
+
pytest.skip(f"{spec.module_path} not installed")
|
|
319
|
+
importlib.reload(mod)
|
|
320
|
+
assert mod.is_present() is True
|
|
321
|
+
|
|
322
|
+
def test_alert_returns_true_and_publishes_to_topic(
|
|
323
|
+
self, spec: AdapterSpec, skcap_sandbox: Path, monkeypatch: pytest.MonkeyPatch
|
|
324
|
+
):
|
|
325
|
+
"""alert() returns True and writes a PubSub message to <svc>.<level>/."""
|
|
326
|
+
monkeypatch.delenv("SK_STANDALONE", raising=False)
|
|
327
|
+
mod = _load_adapter(spec)
|
|
328
|
+
if mod is None:
|
|
329
|
+
pytest.skip(f"{spec.module_path} not installed")
|
|
330
|
+
importlib.reload(mod)
|
|
331
|
+
|
|
332
|
+
event_name = "test_event_integrated"
|
|
333
|
+
level = spec.alert_level
|
|
334
|
+
result = mod.alert(event_name, {"source": "harness"}, level)
|
|
335
|
+
assert result is True, (
|
|
336
|
+
f"{spec.service}.alert(..., level={level!r}) returned False in "
|
|
337
|
+
f"integrated mode — check skcap_sandbox isolation"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Topic directory must exist: <home>/pubsub/topics/<svc>.<level>/
|
|
341
|
+
expected_topic = f"{spec.service}.{level}"
|
|
342
|
+
topic_dir = skcap_sandbox / "pubsub" / "topics" / expected_topic
|
|
343
|
+
assert topic_dir.is_dir(), (
|
|
344
|
+
f"Expected topic dir {topic_dir} — SDK alert did not create it. "
|
|
345
|
+
f"Check that the adapter uses topic '<svc>.<severity>' convention."
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
msgs = list(topic_dir.glob("msg-*.json"))
|
|
349
|
+
assert len(msgs) >= 1, f"No msg-*.json under {topic_dir}"
|
|
350
|
+
|
|
351
|
+
payload_data = json.loads(msgs[0].read_text())
|
|
352
|
+
|
|
353
|
+
# The event name must be in the payload.event field (ADR §4 convention)
|
|
354
|
+
assert "payload" in payload_data, f"Message missing 'payload' key: {payload_data}"
|
|
355
|
+
assert payload_data["payload"].get("event") == event_name, (
|
|
356
|
+
f"Payload event field mismatch — got {payload_data['payload'].get('event')!r}, "
|
|
357
|
+
f"expected {event_name!r}. The adapter must put the semantic event name in "
|
|
358
|
+
f"payload['event'], not in the topic suffix."
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Tags must contain the level string so skcapstone alerts wildcard routing works
|
|
362
|
+
assert level in payload_data.get("tags", []), (
|
|
363
|
+
f"Level {level!r} missing from message tags {payload_data.get('tags')!r}. "
|
|
364
|
+
f"skcapstone alerts subscribes to *.{level} — tags must carry the severity."
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
def test_alert_topic_uses_severity_suffix_not_event_name(
|
|
368
|
+
self, spec: AdapterSpec, skcap_sandbox: Path, monkeypatch: pytest.MonkeyPatch
|
|
369
|
+
):
|
|
370
|
+
"""The topic suffix IS the severity, NOT the event name.
|
|
371
|
+
|
|
372
|
+
This guards the bug that was caught during skmemory adapter work:
|
|
373
|
+
if the topic is ``<svc>.<event_name>`` then ``skcapstone alerts``'
|
|
374
|
+
``*.error`` / ``*.warn`` wildcards never match it.
|
|
375
|
+
"""
|
|
376
|
+
monkeypatch.delenv("SK_STANDALONE", raising=False)
|
|
377
|
+
mod = _load_adapter(spec)
|
|
378
|
+
if mod is None:
|
|
379
|
+
pytest.skip(f"{spec.module_path} not installed")
|
|
380
|
+
importlib.reload(mod)
|
|
381
|
+
|
|
382
|
+
level = "error"
|
|
383
|
+
event_name = "canary_event_xyzzy"
|
|
384
|
+
mod.alert(event_name, {}, level)
|
|
385
|
+
|
|
386
|
+
# Topic dir for <svc>.error MUST exist
|
|
387
|
+
correct_dir = skcap_sandbox / "pubsub" / "topics" / f"{spec.service}.error"
|
|
388
|
+
assert correct_dir.is_dir(), (
|
|
389
|
+
f"Topic dir {correct_dir} not created. "
|
|
390
|
+
f"Adapter may be using the event name as the topic suffix."
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Topic dir named after the event name must NOT exist
|
|
394
|
+
wrong_dir = skcap_sandbox / "pubsub" / "topics" / f"{spec.service}.{event_name}"
|
|
395
|
+
assert not wrong_dir.exists(), (
|
|
396
|
+
f"Adapter created wrong topic dir {wrong_dir} — "
|
|
397
|
+
f"event name must NOT be the topic suffix."
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
def test_ensure_schedule_returns_true_and_writes_jobs_d(
|
|
401
|
+
self, spec: AdapterSpec, skcap_sandbox: Path, monkeypatch: pytest.MonkeyPatch
|
|
402
|
+
):
|
|
403
|
+
"""ensure_schedule() returns True and writes <job_name>.yaml to jobs.d/."""
|
|
404
|
+
monkeypatch.delenv("SK_STANDALONE", raising=False)
|
|
405
|
+
mod = _load_adapter(spec)
|
|
406
|
+
if mod is None:
|
|
407
|
+
pytest.skip(f"{spec.module_path} not installed")
|
|
408
|
+
importlib.reload(mod)
|
|
409
|
+
|
|
410
|
+
result = mod.ensure_schedule()
|
|
411
|
+
assert result is True, (
|
|
412
|
+
f"{spec.service}.ensure_schedule() returned False in integrated mode"
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
jobs_d = skcap_sandbox / "config" / "jobs.d"
|
|
416
|
+
job_file = jobs_d / f"{spec.job_name}.yaml"
|
|
417
|
+
assert job_file.exists(), (
|
|
418
|
+
f"Expected jobs.d fragment {job_file} — ensure_schedule() did not write it"
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
job_data = yaml.safe_load(job_file.read_text())
|
|
422
|
+
assert isinstance(job_data, dict), f"jobs.d fragment is not valid YAML: {job_data}"
|
|
423
|
+
|
|
424
|
+
# The scheduler serialises fragments as:
|
|
425
|
+
# jobs:
|
|
426
|
+
# <job_name>:
|
|
427
|
+
# type: shell
|
|
428
|
+
# command: ...
|
|
429
|
+
# every: ...
|
|
430
|
+
# so the top-level key is "jobs" and the job name is the nested key.
|
|
431
|
+
assert "jobs" in job_data, (
|
|
432
|
+
f"jobs.d fragment missing top-level 'jobs' key: {job_data}"
|
|
433
|
+
)
|
|
434
|
+
job_entries = job_data["jobs"]
|
|
435
|
+
assert isinstance(job_entries, dict), (
|
|
436
|
+
f"jobs.d 'jobs' value is not a dict: {job_entries}"
|
|
437
|
+
)
|
|
438
|
+
assert spec.job_name in job_entries, (
|
|
439
|
+
f"Job name {spec.job_name!r} not found in jobs.d fragment keys: "
|
|
440
|
+
f"{list(job_entries.keys())}"
|
|
441
|
+
)
|
|
442
|
+
job_body = job_entries[spec.job_name]
|
|
443
|
+
assert "command" in job_body, f"Job body missing 'command' key: {job_body}"
|
|
444
|
+
# Must have either 'every' (interval) or 'schedule' (cron)
|
|
445
|
+
assert "every" in job_body or "schedule" in job_body, (
|
|
446
|
+
f"Job body has neither 'every' nor 'schedule': {job_body}"
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
def test_ensure_schedule_is_idempotent(
|
|
450
|
+
self, spec: AdapterSpec, skcap_sandbox: Path, monkeypatch: pytest.MonkeyPatch
|
|
451
|
+
):
|
|
452
|
+
"""Calling ensure_schedule() twice does not raise and writes one file."""
|
|
453
|
+
monkeypatch.delenv("SK_STANDALONE", raising=False)
|
|
454
|
+
mod = _load_adapter(spec)
|
|
455
|
+
if mod is None:
|
|
456
|
+
pytest.skip(f"{spec.module_path} not installed")
|
|
457
|
+
importlib.reload(mod)
|
|
458
|
+
|
|
459
|
+
mod.ensure_schedule()
|
|
460
|
+
mod.ensure_schedule() # second call must not raise
|
|
461
|
+
|
|
462
|
+
jobs_d = skcap_sandbox / "config" / "jobs.d"
|
|
463
|
+
job_files = list(jobs_d.glob(f"{spec.job_name}*.yaml"))
|
|
464
|
+
assert len(job_files) == 1, (
|
|
465
|
+
f"Expected 1 jobs.d file after idempotent calls, got {len(job_files)}: {job_files}"
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
def test_register_self_returns_true_and_writes_registry(
|
|
469
|
+
self, spec: AdapterSpec, skcap_sandbox: Path, monkeypatch: pytest.MonkeyPatch
|
|
470
|
+
):
|
|
471
|
+
"""register_self() returns True and writes <svc>.json to registry/."""
|
|
472
|
+
monkeypatch.delenv("SK_STANDALONE", raising=False)
|
|
473
|
+
mod = _load_adapter(spec)
|
|
474
|
+
if mod is None:
|
|
475
|
+
pytest.skip(f"{spec.module_path} not installed")
|
|
476
|
+
importlib.reload(mod)
|
|
477
|
+
|
|
478
|
+
result = mod.register_self()
|
|
479
|
+
assert result is True, (
|
|
480
|
+
f"{spec.service}.register_self() returned False in integrated mode"
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
registry = skcap_sandbox / "registry"
|
|
484
|
+
entry_file = registry / f"{spec.service}.json"
|
|
485
|
+
assert entry_file.exists(), (
|
|
486
|
+
f"Expected registry entry {entry_file} — register_self() did not write it"
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
entry = json.loads(entry_file.read_text())
|
|
490
|
+
assert entry.get("name") == spec.service, (
|
|
491
|
+
f"Registry entry name mismatch: {entry.get('name')!r} != {spec.service!r}"
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
def test_unregister_schedule_cleans_up(
|
|
495
|
+
self, spec: AdapterSpec, skcap_sandbox: Path, monkeypatch: pytest.MonkeyPatch
|
|
496
|
+
):
|
|
497
|
+
"""unregister_schedule() removes the jobs.d fragment written by ensure_schedule."""
|
|
498
|
+
monkeypatch.delenv("SK_STANDALONE", raising=False)
|
|
499
|
+
mod = _load_adapter(spec)
|
|
500
|
+
if mod is None:
|
|
501
|
+
pytest.skip(f"{spec.module_path} not installed")
|
|
502
|
+
importlib.reload(mod)
|
|
503
|
+
|
|
504
|
+
mod.ensure_schedule()
|
|
505
|
+
job_file = skcap_sandbox / "config" / "jobs.d" / f"{spec.job_name}.yaml"
|
|
506
|
+
assert job_file.exists(), "Precondition: ensure_schedule() should have written the file"
|
|
507
|
+
|
|
508
|
+
result = mod.unregister_schedule()
|
|
509
|
+
assert result is True, (
|
|
510
|
+
f"{spec.service}.unregister_schedule() returned False"
|
|
511
|
+
)
|
|
512
|
+
assert not job_file.exists(), (
|
|
513
|
+
f"jobs.d fragment {job_file} still present after unregister_schedule()"
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
def test_no_leak_to_real_home(
|
|
517
|
+
self,
|
|
518
|
+
spec: AdapterSpec,
|
|
519
|
+
skcap_sandbox: Path,
|
|
520
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
521
|
+
real_jobs_d: Path,
|
|
522
|
+
real_registry: Path,
|
|
523
|
+
):
|
|
524
|
+
"""Integrated-mode writes go to sandbox only — nothing leaks to real home.
|
|
525
|
+
|
|
526
|
+
This test fails the suite if sandboxing is broken, preventing silent
|
|
527
|
+
pollution of the developer's actual ~/.skcapstone tree.
|
|
528
|
+
"""
|
|
529
|
+
# Record files in real home BEFORE the test actions
|
|
530
|
+
real_jobs_before = set(real_jobs_d.glob(f"*{spec.service}*")) if real_jobs_d.exists() else set()
|
|
531
|
+
real_reg_before = set(real_registry.glob(f"{spec.service}*")) if real_registry.exists() else set()
|
|
532
|
+
|
|
533
|
+
monkeypatch.delenv("SK_STANDALONE", raising=False)
|
|
534
|
+
mod = _load_adapter(spec)
|
|
535
|
+
if mod is None:
|
|
536
|
+
pytest.skip(f"{spec.module_path} not installed")
|
|
537
|
+
importlib.reload(mod)
|
|
538
|
+
|
|
539
|
+
mod.alert("leak_check", {"harness": True}, "warn")
|
|
540
|
+
mod.ensure_schedule()
|
|
541
|
+
mod.register_self()
|
|
542
|
+
|
|
543
|
+
# Verify no NEW files appeared in the real home
|
|
544
|
+
real_jobs_after = set(real_jobs_d.glob(f"*{spec.service}*")) if real_jobs_d.exists() else set()
|
|
545
|
+
real_reg_after = set(real_registry.glob(f"{spec.service}*")) if real_registry.exists() else set()
|
|
546
|
+
|
|
547
|
+
new_job_files = real_jobs_after - real_jobs_before
|
|
548
|
+
new_reg_files = real_reg_after - real_reg_before
|
|
549
|
+
|
|
550
|
+
assert not new_job_files, (
|
|
551
|
+
f"LEAK: {spec.service} wrote to real jobs.d: {new_job_files}. "
|
|
552
|
+
f"Check that SKCAPSTONE_HOME env and skcapstone.AGENT_HOME are both "
|
|
553
|
+
f"patched to the sandbox path."
|
|
554
|
+
)
|
|
555
|
+
assert not new_reg_files, (
|
|
556
|
+
f"LEAK: {spec.service} wrote to real registry: {new_reg_files}."
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
# ---------------------------------------------------------------------------
|
|
561
|
+
# Contract summary: readable doc of what every adapter must satisfy
|
|
562
|
+
# ---------------------------------------------------------------------------
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
class TestAdapterContract:
|
|
566
|
+
"""Documents the invariants every adapter must satisfy.
|
|
567
|
+
|
|
568
|
+
These are assertion-less documentation tests — they pass trivially but
|
|
569
|
+
serve as a machine-readable contract anchor in the test output.
|
|
570
|
+
"""
|
|
571
|
+
|
|
572
|
+
def test_contract_standalone_invariants(self):
|
|
573
|
+
"""STANDALONE contract: is_present→False; alert/ensure_schedule/register_self→False;
|
|
574
|
+
no files written; no crash."""
|
|
575
|
+
contract = {
|
|
576
|
+
"is_present": "returns False",
|
|
577
|
+
"alert": "returns False, logs locally, raises nothing",
|
|
578
|
+
"ensure_schedule": "returns False, writes no files, raises nothing",
|
|
579
|
+
"register_self": "returns False, raises nothing",
|
|
580
|
+
}
|
|
581
|
+
assert all(v for v in contract.values())
|
|
582
|
+
|
|
583
|
+
def test_contract_integrated_invariants(self):
|
|
584
|
+
"""INTEGRATED contract: is_present→True; alert routes to PubSub
|
|
585
|
+
topic <svc>.<severity> with event in payload; ensure_schedule writes
|
|
586
|
+
jobs.d/<job>.yaml; register_self writes registry/<svc>.json; idempotent;
|
|
587
|
+
unregister removes the fragment; no leaks to real ~/.skcapstone."""
|
|
588
|
+
contract = {
|
|
589
|
+
"is_present": "returns True",
|
|
590
|
+
"alert_topic": "<svc>.<severity> — event name in payload.event not topic",
|
|
591
|
+
"alert_tags": "level string present in message tags",
|
|
592
|
+
"ensure_schedule": "writes jobs.d/<job>.yaml with name/command/every",
|
|
593
|
+
"idempotency": "safe to call ensure_schedule twice",
|
|
594
|
+
"register_self": "writes registry/<svc>.json with name field",
|
|
595
|
+
"unregister": "removes jobs.d fragment; returns True",
|
|
596
|
+
"no_leak": "all writes go to sandboxed SKCAPSTONE_HOME only",
|
|
597
|
+
}
|
|
598
|
+
assert all(v for v in contract.values())
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Test that resolving a problem completes its associated GTD project.
|
|
2
|
+
|
|
3
|
+
Regression test for the lifecycle leak where create_problem discarded the
|
|
4
|
+
GTD project id and update_problem never completed it on resolve.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture(autouse=True)
|
|
15
|
+
def _isolate_gtd_dir(tmp_path: Path, monkeypatch) -> None:
|
|
16
|
+
"""Redirect _shared_root() so GTD files land in tmp_path, not ~/.skcapstone."""
|
|
17
|
+
import skcapstone.mcp_tools._helpers as _helpers
|
|
18
|
+
|
|
19
|
+
monkeypatch.setattr(_helpers, "SHARED_ROOT", str(tmp_path))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_resolving_problem_completes_its_gtd_project(tmp_path: Path):
|
|
23
|
+
from skcapstone.itil import ITILManager
|
|
24
|
+
from skcapstone.mcp_tools.gtd_tools import _load_list, _load_archive
|
|
25
|
+
|
|
26
|
+
mgr = ITILManager(str(tmp_path))
|
|
27
|
+
|
|
28
|
+
prb = mgr.create_problem(title="Flaky widget", managed_by="opus")
|
|
29
|
+
assert prb.gtd_item_ids, "problem should store its GTD project id"
|
|
30
|
+
assert any(p["id"] in prb.gtd_item_ids for p in _load_list("projects"))
|
|
31
|
+
|
|
32
|
+
mgr.update_problem(prb.id, agent="opus", new_status="analyzing")
|
|
33
|
+
mgr.update_problem(prb.id, agent="opus", new_status="resolved")
|
|
34
|
+
|
|
35
|
+
assert not any(p["id"] in prb.gtd_item_ids for p in _load_list("projects"))
|
|
36
|
+
archived = _load_archive()
|
|
37
|
+
assert any(a["id"] in prb.gtd_item_ids and a["status"] == "done" for a in archived)
|