Add Core Hub production stabilization scheduled checks
Introduce the core-hub context resolver stabilization_check query, State Hub progress summaries, activity-definition projections, and unit tests for the CORE-WP-0007 post-cutover window.
This commit is contained in:
parent
c253139ee3
commit
151512a8fa
5 changed files with 383 additions and 1 deletions
|
|
@ -284,6 +284,79 @@ data:
|
||||||
---
|
---
|
||||||
|
|
||||||
# ActivityDefinition: Phase 5 Stabilization Closeout Check
|
# ActivityDefinition: Phase 5 Stabilization Closeout Check
|
||||||
|
core-hub-stabilization-daily.md: |
|
||||||
|
---
|
||||||
|
id: "b4c8e2f1-6a3d-4e5b-9f0c-2d7e8a1b3c4d"
|
||||||
|
name: "Core Hub Stabilization Daily Check"
|
||||||
|
type: activity-definition
|
||||||
|
version: "1.0"
|
||||||
|
enabled: true
|
||||||
|
owner: core-hub
|
||||||
|
governance: core-hub
|
||||||
|
status: active
|
||||||
|
created: "2026-07-07"
|
||||||
|
trigger:
|
||||||
|
type: cron
|
||||||
|
cron_expression: "0 9 * * *"
|
||||||
|
timezone: Europe/Berlin
|
||||||
|
misfire_policy: skip
|
||||||
|
context_sources:
|
||||||
|
- type: core-hub
|
||||||
|
query: stabilization_check
|
||||||
|
required: true
|
||||||
|
params:
|
||||||
|
source: activity-core
|
||||||
|
closeout: false
|
||||||
|
base_url: "https://hub.coulomb.social"
|
||||||
|
window_start: "2026-07-03T00:00:00+00:00"
|
||||||
|
window_end: "2026-07-10T17:35:00+00:00"
|
||||||
|
min_widget_types: 26
|
||||||
|
evidence_sinks:
|
||||||
|
- type: state-hub-progress
|
||||||
|
event_type: core_hub_stabilization_check
|
||||||
|
author: activity-core
|
||||||
|
workstream_id: a8d66822-e435-4b1e-ad81-37a298d1795e
|
||||||
|
task_id: 16eb7ce3-b574-4667-a189-c14ff5d0502b
|
||||||
|
bind_to: context.core_hub_stabilization_check
|
||||||
|
---
|
||||||
|
|
||||||
|
# ActivityDefinition: Core Hub Stabilization Daily Check
|
||||||
|
core-hub-stabilization-closeout.md: |
|
||||||
|
---
|
||||||
|
id: "c5d9f3a2-7b4e-5f6c-0a1d-3e8f9b2c4d5e"
|
||||||
|
name: "Core Hub Stabilization Closeout Check"
|
||||||
|
type: activity-definition
|
||||||
|
version: "1.0"
|
||||||
|
enabled: true
|
||||||
|
owner: core-hub
|
||||||
|
governance: core-hub
|
||||||
|
status: active
|
||||||
|
created: "2026-07-07"
|
||||||
|
trigger:
|
||||||
|
type: scheduled
|
||||||
|
at: "2026-07-10T17:35:00+00:00"
|
||||||
|
timezone: UTC
|
||||||
|
context_sources:
|
||||||
|
- type: core-hub
|
||||||
|
query: stabilization_check
|
||||||
|
required: true
|
||||||
|
params:
|
||||||
|
source: activity-core
|
||||||
|
closeout: true
|
||||||
|
base_url: "https://hub.coulomb.social"
|
||||||
|
window_start: "2026-07-03T00:00:00+00:00"
|
||||||
|
window_end: "2026-07-10T17:35:00+00:00"
|
||||||
|
min_widget_types: 26
|
||||||
|
evidence_sinks:
|
||||||
|
- type: state-hub-progress
|
||||||
|
event_type: core_hub_stabilization_closeout
|
||||||
|
author: activity-core
|
||||||
|
workstream_id: a8d66822-e435-4b1e-ad81-37a298d1795e
|
||||||
|
task_id: 16eb7ce3-b574-4667-a189-c14ff5d0502b
|
||||||
|
bind_to: context.core_hub_stabilization_check
|
||||||
|
---
|
||||||
|
|
||||||
|
# ActivityDefinition: Core Hub Stabilization Closeout Check
|
||||||
ops-service-inventory-probes.md: |
|
ops-service-inventory-probes.md: |
|
||||||
---
|
---
|
||||||
id: "40d15a87-7ff6-4d8e-992c-37df15f95110"
|
id: "40d15a87-7ff6-4d8e-992c-37df15f95110"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from activity_core.context_resolvers import ( # noqa: F401
|
from activity_core.context_resolvers import ( # noqa: F401
|
||||||
|
core_hub,
|
||||||
event_payload,
|
event_payload,
|
||||||
kaizen,
|
kaizen,
|
||||||
ops_inventory,
|
ops_inventory,
|
||||||
|
|
|
||||||
193
src/activity_core/context_resolvers/core_hub.py
Normal file
193
src/activity_core/context_resolvers/core_hub.py
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
"""Core Hub production stabilization context adapter.
|
||||||
|
|
||||||
|
Registered as source type ``core-hub``.
|
||||||
|
|
||||||
|
Supported queries:
|
||||||
|
- stabilization_check: public production surface gates for the post-cutover
|
||||||
|
window before CORE-WP-0007-T02 operator sign-off.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from activity_core.context_resolvers.base import CONTEXT_RESOLVER_REGISTRY, ContextResolver
|
||||||
|
from activity_core.context_resolvers.state_hub import _parse_iso_datetime, _utc_now
|
||||||
|
|
||||||
|
_DEFAULT_BASE_URL = "https://hub.coulomb.social"
|
||||||
|
_DEFAULT_TIMEOUT_SECONDS = 15.0
|
||||||
|
_PUBLIC_CHECKS: tuple[tuple[str, int], ...] = (
|
||||||
|
("/healthz", 200),
|
||||||
|
("/readyz", 200),
|
||||||
|
("/api/v2/widget-types", 200),
|
||||||
|
("/api/v2/hubs", 401),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CoreHubContextResolver(ContextResolver):
|
||||||
|
"""Resolve lightweight Core Hub stabilization checks over public HTTP."""
|
||||||
|
|
||||||
|
def resolve(self, query: str, event: Any, params: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
if query == "stabilization_check":
|
||||||
|
return _core_hub_stabilization_check(params)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _bounded_int(
|
||||||
|
value: Any,
|
||||||
|
*,
|
||||||
|
default: int,
|
||||||
|
minimum: int,
|
||||||
|
maximum: int,
|
||||||
|
) -> int:
|
||||||
|
try:
|
||||||
|
parsed = int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return default
|
||||||
|
return max(minimum, min(maximum, parsed))
|
||||||
|
|
||||||
|
|
||||||
|
def _probe_status(
|
||||||
|
client: httpx.Client,
|
||||||
|
path: str,
|
||||||
|
expected_status: int,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
response = client.get(path)
|
||||||
|
except Exception as exc: # pragma: no cover - network dependent
|
||||||
|
return {
|
||||||
|
"pass": False,
|
||||||
|
"path": path,
|
||||||
|
"expected_status": expected_status,
|
||||||
|
"error": str(exc),
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"pass": response.status_code == expected_status,
|
||||||
|
"path": path,
|
||||||
|
"expected_status": expected_status,
|
||||||
|
"status_code": response.status_code,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _widget_type_count(client: httpx.Client, minimum: int) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
response = client.get("/api/v2/widget-types")
|
||||||
|
except Exception as exc: # pragma: no cover - network dependent
|
||||||
|
return {
|
||||||
|
"pass": False,
|
||||||
|
"minimum": minimum,
|
||||||
|
"count": None,
|
||||||
|
"error": str(exc),
|
||||||
|
}
|
||||||
|
if response.status_code != 200:
|
||||||
|
return {
|
||||||
|
"pass": False,
|
||||||
|
"minimum": minimum,
|
||||||
|
"count": None,
|
||||||
|
"status_code": response.status_code,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
payload = response.json()
|
||||||
|
except ValueError as exc:
|
||||||
|
return {
|
||||||
|
"pass": False,
|
||||||
|
"minimum": minimum,
|
||||||
|
"count": None,
|
||||||
|
"error": f"invalid JSON: {exc}",
|
||||||
|
}
|
||||||
|
if isinstance(payload, list):
|
||||||
|
count = len(payload)
|
||||||
|
elif isinstance(payload, dict) and isinstance(payload.get("data"), list):
|
||||||
|
count = len(payload["data"])
|
||||||
|
else:
|
||||||
|
count = 0
|
||||||
|
return {
|
||||||
|
"pass": count >= minimum,
|
||||||
|
"minimum": minimum,
|
||||||
|
"count": count,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _core_hub_stabilization_check(params: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
closeout = bool(params.get("closeout"))
|
||||||
|
now = _utc_now()
|
||||||
|
window_start = _parse_iso_datetime(
|
||||||
|
params.get("window_start", "2026-07-03T00:00:00+00:00")
|
||||||
|
)
|
||||||
|
window_end = _parse_iso_datetime(
|
||||||
|
params.get("window_end", "2026-07-10T17:35:00+00:00")
|
||||||
|
)
|
||||||
|
within_window = bool(
|
||||||
|
window_start and window_end and window_start <= now <= window_end
|
||||||
|
)
|
||||||
|
|
||||||
|
if not within_window and not closeout:
|
||||||
|
return {
|
||||||
|
"kind": "core_hub_stabilization",
|
||||||
|
"skipped": True,
|
||||||
|
"reason": "outside_stabilization_window",
|
||||||
|
"closeout": closeout,
|
||||||
|
"overall_pass": True,
|
||||||
|
"operator_signoff_needed": False,
|
||||||
|
"base_url": str(params.get("base_url") or os.environ.get("CORE_HUB_BASE_URL") or _DEFAULT_BASE_URL),
|
||||||
|
"window": {
|
||||||
|
"start": window_start.isoformat() if window_start else None,
|
||||||
|
"end": window_end.isoformat() if window_end else None,
|
||||||
|
"within": within_window,
|
||||||
|
},
|
||||||
|
"checks": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
base_url = str(
|
||||||
|
params.get("base_url")
|
||||||
|
or os.environ.get("CORE_HUB_BASE_URL")
|
||||||
|
or _DEFAULT_BASE_URL
|
||||||
|
).rstrip("/")
|
||||||
|
timeout_seconds = float(
|
||||||
|
params.get("timeout_seconds", _DEFAULT_TIMEOUT_SECONDS)
|
||||||
|
)
|
||||||
|
min_widget_types = _bounded_int(
|
||||||
|
params.get("min_widget_types", 26),
|
||||||
|
default=26,
|
||||||
|
minimum=1,
|
||||||
|
maximum=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
checks: dict[str, Any] = {}
|
||||||
|
with httpx.Client(
|
||||||
|
base_url=base_url,
|
||||||
|
timeout=timeout_seconds,
|
||||||
|
headers={"Accept": "application/json", "User-Agent": "activity-core/core-hub-stabilization/0.1"},
|
||||||
|
follow_redirects=True,
|
||||||
|
) as client:
|
||||||
|
for path, expected_status in _PUBLIC_CHECKS:
|
||||||
|
key = path.strip("/").replace("/", "_").replace("-", "_") or "root"
|
||||||
|
checks[key] = _probe_status(client, path, expected_status)
|
||||||
|
checks["widget_types_count"] = _widget_type_count(client, min_widget_types)
|
||||||
|
|
||||||
|
overall_pass = all(section.get("pass") for section in checks.values())
|
||||||
|
result = {
|
||||||
|
"kind": "core_hub_stabilization",
|
||||||
|
"skipped": False,
|
||||||
|
"closeout": closeout,
|
||||||
|
"overall_pass": overall_pass,
|
||||||
|
"operator_signoff_needed": closeout and overall_pass,
|
||||||
|
"base_url": base_url,
|
||||||
|
"window": {
|
||||||
|
"start": window_start.isoformat() if window_start else None,
|
||||||
|
"end": window_end.isoformat() if window_end else None,
|
||||||
|
"within": within_window,
|
||||||
|
},
|
||||||
|
"checks": checks,
|
||||||
|
"source": str(params.get("source") or "activity-core"),
|
||||||
|
}
|
||||||
|
if closeout and not overall_pass:
|
||||||
|
raise RuntimeError(f"core hub stabilization closeout failed: {result}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
CONTEXT_RESOLVER_REGISTRY["core-hub"] = CoreHubContextResolver
|
||||||
|
|
@ -123,7 +123,11 @@ def _post_state_hub_progress(
|
||||||
"context_key": context_key,
|
"context_key": context_key,
|
||||||
}
|
}
|
||||||
|
|
||||||
if probe_result.get("checks") is not None:
|
if probe_result.get("kind") == "core_hub_stabilization":
|
||||||
|
compact = probe_result
|
||||||
|
summary = _core_hub_stabilization_summary_text(probe_result)
|
||||||
|
source_type = "core-hub"
|
||||||
|
elif probe_result.get("checks") is not None:
|
||||||
compact = probe_result
|
compact = probe_result
|
||||||
summary = _phase5_summary_text(probe_result)
|
summary = _phase5_summary_text(probe_result)
|
||||||
source_type = "state-hub"
|
source_type = "state-hub"
|
||||||
|
|
@ -503,6 +507,19 @@ def _compact_access_path(access_path: dict[str, Any]) -> dict[str, Any]:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _core_hub_stabilization_summary_text(result: dict[str, Any]) -> str:
|
||||||
|
checks = result.get("checks") or {}
|
||||||
|
widget_types = checks.get("widget_types_count") or {}
|
||||||
|
status = "pass" if result.get("overall_pass") else "fail"
|
||||||
|
mode = "closeout" if result.get("closeout") else "daily"
|
||||||
|
return (
|
||||||
|
f"Core Hub stabilization {mode}: {status}; "
|
||||||
|
f"base={result.get('base_url', '?')}; "
|
||||||
|
f"widget_types={widget_types.get('count', '?')}/"
|
||||||
|
f"{widget_types.get('minimum', '?')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _phase5_summary_text(result: dict[str, Any]) -> str:
|
def _phase5_summary_text(result: dict[str, Any]) -> str:
|
||||||
checks = result.get("checks") or {}
|
checks = result.get("checks") or {}
|
||||||
totals = checks.get("totals") or {}
|
totals = checks.get("totals") or {}
|
||||||
|
|
|
||||||
98
tests/test_core_hub_context_resolver.py
Normal file
98
tests/test_core_hub_context_resolver.py
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from activity_core.context_resolvers.core_hub import CoreHubContextResolver
|
||||||
|
|
||||||
|
|
||||||
|
class DummyResponse:
|
||||||
|
def __init__(self, payload: Any, status_code: int = 200) -> None:
|
||||||
|
self._payload = payload
|
||||||
|
self.status_code = status_code
|
||||||
|
self.text = str(payload)
|
||||||
|
|
||||||
|
def json(self) -> Any:
|
||||||
|
return self._payload
|
||||||
|
|
||||||
|
|
||||||
|
class DummyClient:
|
||||||
|
def __init__(self, responses: dict[str, tuple[Any, int]]) -> None:
|
||||||
|
self._responses = responses
|
||||||
|
|
||||||
|
def __enter__(self) -> DummyClient:
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args: Any) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get(self, path: str) -> DummyResponse:
|
||||||
|
payload, status = self._responses.get(
|
||||||
|
path,
|
||||||
|
({}, 404),
|
||||||
|
)
|
||||||
|
return DummyResponse(payload, status)
|
||||||
|
|
||||||
|
|
||||||
|
def test_core_hub_stabilization_check_passes(monkeypatch) -> None:
|
||||||
|
widget_types = [{"slug": f"type-{index}"} for index in range(26)]
|
||||||
|
responses = {
|
||||||
|
"/healthz": ({}, 200),
|
||||||
|
"/readyz": ({}, 200),
|
||||||
|
"/api/v2/widget-types": (widget_types, 200),
|
||||||
|
"/api/v2/hubs": ({}, 401),
|
||||||
|
}
|
||||||
|
fixed_now = datetime(2026, 7, 7, 10, 0, tzinfo=timezone.utc)
|
||||||
|
monkeypatch.setattr(httpx, "Client", lambda **kwargs: DummyClient(responses))
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"activity_core.context_resolvers.core_hub._utc_now",
|
||||||
|
lambda: fixed_now,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = CoreHubContextResolver().resolve("stabilization_check", None, {})
|
||||||
|
|
||||||
|
assert result["overall_pass"] is True
|
||||||
|
assert result["skipped"] is False
|
||||||
|
assert result["checks"]["widget_types_count"]["pass"] is True
|
||||||
|
assert result["checks"]["api_v2_hubs"]["pass"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_core_hub_stabilization_check_skips_outside_window(monkeypatch) -> None:
|
||||||
|
fixed_now = datetime(2026, 7, 12, 10, 0, tzinfo=timezone.utc)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"activity_core.context_resolvers.core_hub._utc_now",
|
||||||
|
lambda: fixed_now,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = CoreHubContextResolver().resolve("stabilization_check", None, {})
|
||||||
|
|
||||||
|
assert result["skipped"] is True
|
||||||
|
assert result["reason"] == "outside_stabilization_window"
|
||||||
|
|
||||||
|
|
||||||
|
def test_core_hub_stabilization_closeout_raises_on_failure(monkeypatch) -> None:
|
||||||
|
responses = {
|
||||||
|
"/healthz": ({}, 500),
|
||||||
|
"/readyz": ({}, 200),
|
||||||
|
"/api/v2/widget-types": ([], 200),
|
||||||
|
"/api/v2/hubs": ({}, 401),
|
||||||
|
}
|
||||||
|
fixed_now = datetime(2026, 7, 7, 10, 0, tzinfo=timezone.utc)
|
||||||
|
monkeypatch.setattr(httpx, "Client", lambda **kwargs: DummyClient(responses))
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"activity_core.context_resolvers.core_hub._utc_now",
|
||||||
|
lambda: fixed_now,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
CoreHubContextResolver().resolve(
|
||||||
|
"stabilization_check",
|
||||||
|
None,
|
||||||
|
{"closeout": True},
|
||||||
|
)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
assert "closeout failed" in str(exc)
|
||||||
|
else:
|
||||||
|
raise AssertionError("expected closeout failure")
|
||||||
Loading…
Add table
Add a link
Reference in a new issue