feat(generator): глагол next-day — следующий модельный день от слепка
- Зачем:
- режим кормления стенда порциями: менти триггерит «следующий день»,
видит полный цикл DWH за один шаг (задача 13, вариант 2 — генерация
от слепка T_end).
- Что:
- новый ограниченный режим next-day: восстановление мира из state,
генерация ровно [T_end, T_end+24h), публикация данные -> state ->
манифест (манифест — точка фиксации, автоотката нет).
- операция next-day в DAG generator_control: своя предпроверка границы
вместо clean-guard, идемпотентность через параметр expected_t_end.
- цепочка границ — накопительное поле boundaries в манифесте, старый
формат читается как [T0, T_end]; импорт не изменён.
- новая проверка цепочки (make generated-history-chain-check): непарные
счётчики и однородность по каждой границе, явный статус нулевого
стыка, хвост за границей по всем четырём топикам, литералы в UTC
с микросекундами.
- документация OPERATIONS.md: глагол, предпроверка, восстановление
после сбоя, ограничение retention; в задаче 13 — решения двух слепых
ревью постановки и кода с аргументами отклонений.
- Проверка:
- make test: 204 теста генератора + 31 контракт корня, зелёные.
- make generated-history-chain-check: зелёный, 2 внутренние границы,
непарные счётчики нулевые; учебный цикл: DM 322 -> 10026 -> 19196
за два next-day подряд.
- make generated-history-runtime-check (регрессия задачи 20): зелёный.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,94 @@ def test_import_env_requires_artifact_path_and_uses_backfill_contract():
|
||||
assert env["GEN_HISTORY_DURATION"] == "2d"
|
||||
|
||||
|
||||
def test_next_day_env_uses_world_settings_and_current_manifest_boundary():
|
||||
"""Next-day восстанавливает настройки мира из manifest, а не из формы DAG."""
|
||||
from clickstream_generator.airflow_control import build_next_day_env
|
||||
|
||||
manifest = {
|
||||
"gen_seed": 42,
|
||||
"model_t0": "2026-01-01T00:00:00+00:00",
|
||||
"model_t_end": "2026-01-03T00:00:00+00:00",
|
||||
"model_timezone": "UTC",
|
||||
"launch_profile": "daily-wave",
|
||||
"generation_settings": {
|
||||
"tick_seconds": 1,
|
||||
"lambda_base_per_min": 60,
|
||||
"jitter_pct": 0,
|
||||
"min_events_per_tick": 1,
|
||||
"max_events_per_tick": 1000,
|
||||
"max_session_events": 30,
|
||||
"max_active_sessions": 200,
|
||||
"population_max": 300,
|
||||
"p_new_user": 0.15,
|
||||
"min_return_minutes": 30,
|
||||
"model_time_speed": 60,
|
||||
},
|
||||
}
|
||||
|
||||
env = build_next_day_env(manifest)
|
||||
|
||||
assert env["GEN_RUN_MODE"] == "next-day"
|
||||
assert env["GEN_STATE_RESET"] == "false"
|
||||
assert env["GEN_MODEL_T_END"] == "2026-01-03T00:00:00+00:00"
|
||||
assert env["GEN_MODEL_TIME_SPEED"] == "60"
|
||||
assert env["GEN_MAX_SESSION_EVENTS"] == "30"
|
||||
assert env["GEN_DATA_DIR"] == "/opt/airflow/data"
|
||||
|
||||
|
||||
def test_next_day_env_map_covers_every_generation_setting(base_config):
|
||||
"""Новая настройка генерации не может потеряться между manifest и env."""
|
||||
from clickstream_generator.airflow_control import NEXT_DAY_SETTING_ENV_KEYS
|
||||
from clickstream_generator.startup_history_artifact import (
|
||||
generation_settings_from_config,
|
||||
)
|
||||
|
||||
assert set(NEXT_DAY_SETTING_ENV_KEYS) == set(
|
||||
generation_settings_from_config(base_config)
|
||||
)
|
||||
|
||||
|
||||
def test_next_day_precheck_rejects_stale_expected_boundary_with_both_values():
|
||||
"""Повторный запуск с прежней границей падает до записи данных."""
|
||||
from clickstream_generator.airflow_control import assert_expected_t_end
|
||||
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
assert_expected_t_end(
|
||||
"2026-01-03T00:00:00+00:00",
|
||||
"2026-01-04T00:00:00+00:00",
|
||||
)
|
||||
|
||||
message = str(exc_info.value)
|
||||
assert "2026-01-03T00:00:00+00:00" in message
|
||||
assert "2026-01-04T00:00:00+00:00" in message
|
||||
|
||||
|
||||
def test_next_day_precheck_requires_manifest_and_matching_state():
|
||||
"""Next-day громко отвергает пустой стенд и state не на T_end."""
|
||||
from clickstream_generator.airflow_control import assert_next_day_snapshot
|
||||
from test_startup_history_artifact import _state
|
||||
|
||||
state = _state()
|
||||
with pytest.raises(RuntimeError, match="Manifest"):
|
||||
assert_next_day_snapshot(None, state)
|
||||
|
||||
manifest = {
|
||||
"run_mode": "backfill",
|
||||
"model_t0": state.model_t0.isoformat(),
|
||||
"model_t_end": "2026-01-01T02:00:00+00:00",
|
||||
"gen_seed": state.gen_seed,
|
||||
"model_timezone": state.model_timezone,
|
||||
"state_version": state.version,
|
||||
"state": {
|
||||
"last_batch_id": state.last_batch_id,
|
||||
"model_timestamp": "2026-01-01T02:00:00+00:00",
|
||||
},
|
||||
"generation_settings": {"model_time_speed": state.model_time_speed},
|
||||
}
|
||||
with pytest.raises(RuntimeError, match="state.*T_end"):
|
||||
assert_next_day_snapshot(manifest, state)
|
||||
|
||||
|
||||
def test_target_dag_trigger_error_covers_missing_paused_and_ready_states():
|
||||
"""Проверка зависимого DAG различает три реальные ветки."""
|
||||
from clickstream_generator.airflow_control import target_dag_trigger_error
|
||||
|
||||
Reference in New Issue
Block a user