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:
@@ -167,6 +167,92 @@ def test_manifest_and_artifact_show_launch_profile(base_config):
|
||||
path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def test_manifest_records_initial_boundary_chain(base_config):
|
||||
"""Новый manifest явно хранит начало и правую границу истории."""
|
||||
from clickstream_generator.startup_history_artifact import (
|
||||
StartupHistoryArtifactBuilder,
|
||||
build_manifest,
|
||||
manifest_boundaries,
|
||||
)
|
||||
|
||||
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,
|
||||
),
|
||||
counters=builder.counters,
|
||||
state=state,
|
||||
)
|
||||
|
||||
assert manifest["boundaries"] == [
|
||||
"2026-01-01T00:00:00+00:00",
|
||||
"2026-01-01T01:00:00+00:00",
|
||||
]
|
||||
assert manifest_boundaries(manifest) == manifest["boundaries"]
|
||||
|
||||
|
||||
def test_legacy_manifest_without_boundaries_gets_endpoint_chain():
|
||||
"""Старый manifest без boundaries читается как пара [T0, T_end]."""
|
||||
from clickstream_generator.startup_history_artifact import manifest_boundaries
|
||||
|
||||
manifest = {
|
||||
"model_t0": "2026-01-01T00:00:00+00:00",
|
||||
"model_t_end": "2026-01-03T00:00:00+00:00",
|
||||
}
|
||||
|
||||
assert manifest_boundaries(manifest) == [
|
||||
"2026-01-01T00:00:00+00:00",
|
||||
"2026-01-03T00:00:00+00:00",
|
||||
]
|
||||
|
||||
|
||||
def test_manifest_rejects_boundary_chain_with_wrong_endpoint():
|
||||
"""Цепочка не может расходиться с текущим model_t_end."""
|
||||
from clickstream_generator.startup_history_artifact import manifest_boundaries
|
||||
|
||||
manifest = {
|
||||
"model_t0": "2026-01-01T00:00:00+00:00",
|
||||
"model_t_end": "2026-01-03T00:00:00+00:00",
|
||||
"boundaries": [
|
||||
"2026-01-01T00:00:00+00:00",
|
||||
"2026-01-02T00:00:00+00:00",
|
||||
],
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="model_t_end"):
|
||||
manifest_boundaries(manifest)
|
||||
|
||||
|
||||
def test_build_manifest_rejects_explicit_empty_boundaries(base_config):
|
||||
"""Явно пустая цепочка не подменяется границами по умолчанию."""
|
||||
from clickstream_generator.startup_history_artifact import (
|
||||
StartupHistoryArtifactBuilder,
|
||||
build_manifest,
|
||||
)
|
||||
|
||||
state = _state()
|
||||
builder = StartupHistoryArtifactBuilder()
|
||||
builder.add_batch(_batch())
|
||||
config = replace(
|
||||
base_config,
|
||||
model_t0=state.model_t0,
|
||||
model_t_end=state.model_timestamp,
|
||||
model_time_speed=1,
|
||||
model_timezone="UTC",
|
||||
seed=42,
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="T0 и T_end"):
|
||||
build_manifest(config, builder.counters, state, boundaries=[])
|
||||
|
||||
|
||||
def test_artifact_validation_rejects_mismatched_manifest(base_config):
|
||||
"""Валидация отвергает артефакт, где manifest не совпадает с событиями."""
|
||||
from clickstream_generator.startup_history_artifact import (
|
||||
|
||||
Reference in New Issue
Block a user