- else (catch-all) branch now appends to slot_timings with 'unexpected' status
- Target timestamp display fixed: now uses datetime.fromtimestamp(TARGET_TS)
instead of hardcoded 'Oct 4 00:00:00'
- Added booking_run_2.log: Oct 20 midnight attempt (blocked by duplicate cita)
Bugs fixed:
- _track_rtt(ms) on success log returned None → switched to _current_load()
- _track_rtt(round(ms,1)) on 'Hora no permitida' path: returned None,
double-tracked RTT (already tracked in make_request), AND called round()
in the path (violates zero-overhead rule) → switched to _current_load()
- cur_load = _current_load() in skew retry loop: computed but never used
→ removed (dead code, adds overhead per retry iteration)
Duplicate detection:
- Added DUPLICATE_PATTERNS tuple centralized duplicate string matching
- Added 'Uciente' discovered: server returns 'Usted ya tiene cita previa en
la gestion especificada' when NIE already has a cita for this servicio/cola
- Added _is_duplicate() helper, used in all 3 detection sites (initial, retry,
reconnect)
- Updated 'else' catch-all with clearer guidance message
Tests:
- Updated test_loop_breaks_on_duplicate to work with _is_duplicate() refactor
- Added test_duplicate_detection_includes_new_string
- Updated E2E getHorasCitas test: Oct 20 fully booked → Oct 21 (9 slots)
Added CITA_TYPES catalog (44 servicio/cola combos from live API),
DOC_TYPES, argparse CLI (--servicio/-s, --cola/-c, form params,
--help-citas, --dry-run, --prompt), validate_config() with per-type
field requirements, and prompt_missing() for interactive mode.
Zero impact on defaults: no args = original behavior.
Verified via benchmark: 0.076µs per call (list append), 0.0044% of RTT.
The load computation (sum/variance/σ) is deferred to _compute_load()
which is ONLY called from log messages — never in make_request or
the retry loop.
- _track_rtt() now returns None, appends only (was: appended + computed load)
- _compute_load() extracted from _track_rtt for lazy load estimation
- _current_load() delegates to _compute_load()
- Removed round() from hot path — raw float append, rounding deferred
- Removed round() from make_request _track_rtt call
- Tests: 79 passed (added TestHotPathZeroOverhead: 4 tests)
Enhanced logging and timing infrastructure:
1. log() now supports severity levels: INFO, WARN, ERROR, SUCCESS, PHASE
- Colored output in TTY, plain in non-TTY (file persistence)
- All phase transitions use PHASE level for easy filtering
2. bench() now accepts category parameter for filtering:
Categories: TLS_CONNECT, TLS_RECONNECT, CHECK_SLOTS, BOOKING_ATTEMPT,
RETRY, RETRY_CONN, VERIFY, CHECK, RECOVERY
3. Server load estimation — tracks RTT via _track_rtt():
- _rtt_history: all response times in ms
- _current_load(): LOW/MODERATE/HIGH based on σ < 25/100ms thresholds
- _load_trend(): STABLE/INCREASING/DECREASING via half-comparison
- Load appears in every slot summary and benchmark
4. Per-slot timing tracking:
- slot_timings list: {hora, total_ms, attempts, avg_rtt_ms, status}
- t_booking_start + t_prefire for total time-to-success
- t_prefire_to_success + t_success_total on booking success
5. Enhanced benchmark summary:
- SERVER LOAD ESTIMATION section (RTT stats, variance, trend)
- PER-SLOT TIMING table (Hora, Status, Attempts, Total ms, Avg RTT)
- TIME TO SUCCESS section (pre-fire→cita, booking-start→cita)
- All BENCH entries now show category column
6. EXCEPTION branch now handles all response types explicitly:
- Cita insertada, duplicate, EXCEPTION, Hora no permitida,
Fecha posterior, other — no more catch-all else clause
Tests: 75 passed (17 new tests for logging/severity/load/timings)
Critical logic bug caught by careful review:
- When 'Cita insertada' is returned on a retry, the cita is ALREADY
inserted into the database — no further booking action needed, just
confirm and exit (the comment 'Book it!' was misleading).
- When a duplicate ('ya existente'/'DNI'/'existe') was detected during
skew retry, the code only broke the inner while loop. Since booked_hora
was None, 'if booked_hora: break' did NOT trigger, and the outer for
loop continued to the NEXT slot — potentially booking additional
slots when a cita for this person already exists.
- The EXCEPTION branch's force-reconnect retry had NO duplicate detection
at all — it fell through to a generic 'else: log()' for any unexpected
response, including duplicates.
Fixes:
- Added duplicate_detected flag (initialized False) checked in outer
loop: 'if booked_hora or duplicate_detected: break'
- Set duplicate_detected = True in ALL three duplicate detection paths:
(1) initial attempt, (2) skew retry loop, (3) EXCEPTION retry
- Added explicit 'Hora no permitida' and 'Fecha posterior' handling
in EXCEPTION retry branch (was generic else)
- Removed stale '5x43ms' comment → reflects true '120s 3-phase backoff'
- Clarified comments: 'Cita insertada' = already booked (success),
not a booking action to perform
Tests: 58 passed (added test_duplicate_detected_flag_stops_outer_loop)
CRITICAL BUG FIX: skew_retries_exhausted was a global skip gate — once
first slot exhausted retries, ALL subsequent slots skipped retries.
Later slots could miss the midnight transition during their own window.
New design:
- Two-phase pseudo-exponential backoff PER SLOT (not global):
Phase 1 (aggressive): 43ms x 1.5^n for first 6 retries
Phase 2 (gradual): 330ms x 1.15^n after 6 (less bot-spammy)
- 2.5s time budget per slot (covers NTP skew up to ~2s + lift)
- skew_retries_exhausted is now diagnostics-only (set but NOT used to skip)
- _skew_delay() helper computes two-phase delays
- Only 'Hora no permitida' certifies a slot as taken
- 'Fecha posterior' never a give-up reason — server date not rolled yet
Tests: 57 passed
Live: 8 retries on 15:15, all 9 slots still free post-test
The skew_retries_exhausted flag was a premature optimization that skipped
5x43ms retries on all subsequent slots once the first slot exhausted
its retries. This is a bug: the server date may roll during a later
slot's retry window even if earlier slots failed, so each slot must get
its own full retry chance.
Changes:
- Removed 'if skew_retries_exhausted:' skip gate before the retry loop
- Each slot now always runs 'for skew_attempt in range(5)' independently
- skew_retries_exhausted kept as diagnostics-only variable (not used
to skip), preserving test assertions
- Updated loop header comment + Phase 3c docstring
- Added test_each_slot_gets_own_retry_window to prevent regression
Tests: 57 passed (56 + 1 new)