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
|
||||
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: |
|
||||
---
|
||||
id: "40d15a87-7ff6-4d8e-992c-37df15f95110"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from activity_core.context_resolvers import ( # noqa: F401
|
||||
core_hub,
|
||||
event_payload,
|
||||
kaizen,
|
||||
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,
|
||||
}
|
||||
|
||||
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
|
||||
summary = _phase5_summary_text(probe_result)
|
||||
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:
|
||||
checks = result.get("checks") 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