feat(generator): добавлены профили запуска

- Зачем:
  - запуск генератора должен быть понятным перед будущим DAG-пультом.
- Что:
  - добавлены глаголы запуска backfill, continue и reset.
  - добавлены профили ci и daily-wave с расчётом длительности истории.
  - обновлены runbook и документы запуска под профильный интерфейс.
- Проверка:
  - uv run --with-requirements generator/requirements.txt pytest generator/tests -q.
  - bash -n scripts/run_generator.sh scripts/export_startup_history_artifact.sh scripts/import_startup_history_artifact.sh scripts/run_generated_history_analytics.sh.
  - PROFILE=daily-wave COMPOSE_BIN=true bash scripts/run_generator.sh backfill.
This commit is contained in:
2026-07-04 18:51:10 +03:00
parent d2899e9ee3
commit 4f8f992363
19 changed files with 547 additions and 101 deletions
+18
View File
@@ -118,3 +118,21 @@ class TestConfigDefaults:
finally:
if orig_value is not None:
os.environ["GEN_TICK_SECONDS"] = orig_value
def test_launch_profile_defaults_to_ci(self, monkeypatch, data_dir):
"""По умолчанию выбран быстрый профиль запуска ci."""
monkeypatch.setenv("GEN_DATA_DIR", str(data_dir))
monkeypatch.delenv("GEN_LAUNCH_PROFILE", raising=False)
config = Config()
assert config.launch_profile == "ci"
def test_launch_profile_can_be_read_from_env(self, monkeypatch, data_dir):
"""Выбранный профиль запуска попадает в конфигурацию для логов."""
monkeypatch.setenv("GEN_DATA_DIR", str(data_dir))
monkeypatch.setenv("GEN_LAUNCH_PROFILE", "daily-wave")
config = Config()
assert config.launch_profile == "daily-wave"
+66
View File
@@ -0,0 +1,66 @@
"""
Тесты профилей и глаголов запуска генератора.
"""
from datetime import datetime, timezone
def test_duration_2d_sets_model_t_end_from_t0():
"""Длительность 2d считается от T0 без ручного GEN_MODEL_T_END."""
from clickstream_generator.launch import build_launch_env
env = build_launch_env("backfill", profile_name="ci", duration="2d")
assert env["GEN_MODEL_T0"] == "2026-01-01T00:00:00+00:00"
assert env["GEN_MODEL_T_END"] == "2026-01-03T00:00:00+00:00"
def test_daily_wave_profile_uses_two_days_by_default():
"""Профиль daily-wave даёт историю с суточной волной."""
from clickstream_generator.launch import build_launch_env
env = build_launch_env("backfill", profile_name="daily-wave")
assert env["GEN_LAUNCH_PROFILE"] == "daily-wave"
assert env["GEN_HISTORY_DURATION"] == "2d"
assert env["GEN_MODEL_T_END"] == "2026-01-03T00:00:00+00:00"
def test_backfill_verb_maps_to_existing_low_level_flags():
"""Глагол backfill оставляет прежнюю механику запуска под капотом."""
from clickstream_generator.launch import build_launch_env
env = build_launch_env("backfill", profile_name="ci")
assert env["GEN_RUN_MODE"] == "backfill"
assert env["GEN_STATE_RESET"] == "true"
def test_continue_and_reset_verbs_map_to_existing_low_level_flags():
"""Глаголы continue и reset задают только старый контракт state."""
from clickstream_generator.launch import build_launch_env
continue_env = build_launch_env("continue", profile_name="ci")
reset_env = build_launch_env("reset", profile_name="ci")
assert continue_env["GEN_RUN_MODE"] == "live"
assert continue_env["GEN_STATE_RESET"] == "false"
assert continue_env["GEN_MODEL_T_END"] == "2026-01-01T06:00:00+00:00"
assert reset_env["GEN_RUN_MODE"] == "live"
assert reset_env["GEN_STATE_RESET"] == "true"
assert "GEN_MODEL_T_END" not in reset_env
def test_ci_profile_keeps_previous_default_backfill_window():
"""CI-профиль сохраняет прежний 6-часовой проверочный запуск."""
from clickstream_generator.launch import build_launch_env
env = build_launch_env("backfill", profile_name="ci")
t0 = datetime.fromisoformat(env["GEN_MODEL_T0"])
t_end = datetime.fromisoformat(env["GEN_MODEL_T_END"])
assert t0 == datetime(2026, 1, 1, 0, 0, tzinfo=timezone.utc)
assert (t_end - t0).total_seconds() == 6 * 60 * 60
assert env["GEN_TICK_SECONDS"] == "60"
assert env["GEN_LAMBDA_BASE_PER_MIN"] == "60"
assert env["GEN_JITTER_PCT"] == "0"
@@ -130,6 +130,43 @@ def test_artifact_roundtrip_keeps_events_state_and_manifest(base_config):
path.unlink(missing_ok=True)
def test_manifest_and_artifact_show_launch_profile(base_config):
"""Manifest и файл артефакта показывают выбранный профиль запуска."""
from clickstream_generator.startup_history_artifact import (
StartupHistoryArtifactBuilder,
build_manifest,
load_startup_history_artifact,
write_startup_history_artifact,
)
state = _state()
builder = StartupHistoryArtifactBuilder()
builder.add_batch(_batch())
manifest = build_manifest(
config=replace(
base_config,
model_t0=state.model_t0,
model_t_end=state.model_timestamp,
model_time_speed=1,
model_timezone="UTC",
seed=42,
launch_profile="daily-wave",
),
counters=builder.counters,
state=state,
)
artifact = builder.to_artifact(manifest=manifest, state=state)
path = base_config.data_dir.parent / "tmp-startup-history-profile.json"
try:
write_startup_history_artifact(path, artifact)
loaded = load_startup_history_artifact(path)
assert manifest["launch_profile"] == "daily-wave"
assert loaded["manifest"]["launch_profile"] == "daily-wave"
finally:
path.unlink(missing_ok=True)
def test_artifact_validation_rejects_mismatched_manifest(base_config):
"""Валидация отвергает артефакт, где manifest не совпадает с событиями."""
from clickstream_generator.startup_history_artifact import (