Skip to content

fix(photo-loader): HELD batch barrier so create_batch no longer auto-sends via cron#1190

Merged
axisrow merged 3 commits into
mainfrom
ao/tg_content_factory_5863f66be3-2/issue-1173-held-photo-batch
Jun 28, 2026
Merged

fix(photo-loader): HELD batch barrier so create_batch no longer auto-sends via cron#1190
axisrow merged 3 commits into
mainfrom
ao/tg_content_factory_5863f66be3-2/issue-1173-held-photo-batch

Conversation

@axisrow

@axisrow axisrow commented Jun 28, 2026

Copy link
Copy Markdown
Owner

What

create_batch no longer drops items into the cron-visible PENDING queue. A new PhotoBatchStatus.HELD state holds them back until an explicit publish_batch moves them into PENDING, so the photo_due cron job cannot send a freshly-created batch without a deliberate publish step.

Closes #1173

Owner decision (semantic)

Отложка ≠ крон. Постинг по крону НЕ нужен — для отложенной отправки есть нативная отложка Telegram (schedule_send → статус SCHEDULED). Поэтому автоподхват cron'ом свежесозданного батча — дефект: «создать батч» НЕ должно означать «создать и отправить».

HELD is a distinct pre-publish barrier, not a reuse of SCHEDULED (SCHEDULED = already queued server-side on Telegram via schedule_send). The cron job claim_next_due_item only claims PENDING, so HELD items are invisible to it.

Root cause (before)

  • PhotoTaskService.create_batch hardcoded status=PENDING on the batch and every item.
  • claim_next_due_item picks status=PENDING AND (schedule_at IS NULL OR schedule_at <= now).
  • Cron photo_due runs run_due every minute.
  • Result: create_batch with at=now (e.g. agent-tool after fix(agent): resolve 'me'/'self' target in create_photo_batch/create_auto_upload #1167) → item immediately due → cron sends within a minute, no separate confirm.

Fix

Core barrier:

  • New PhotoBatchStatus.HELD enum value (src/models.py).
  • create_batch now stores batch + items as HELD (src/services/photo_task_service.py).
  • claim_next_due_item only ever claimed PENDING — no change needed there; HELD is naturally invisible.
  • New PhotoLoaderRepository.publish_batch(batch_id): atomically UPDATE SET status=pending WHERE status=held + flips the batch status (src/database/repositories/photo_loader.py).
  • New PhotoTaskService.publish_batch(batch_id) thin wrapper (src/services/photo_task_service.py).

Consistency:

  • cancel_item now accepts HELD (held items are cancellable).
  • _sync_batch_status surfaces HELD when all items are held.
  • jobs_read_model maps HELD to PENDING for the UI.

Surfaces (CLI / Web / Agent parity)

Surface Create to HELD Publish
Web handle_photo_batch POST /dialogs/photos/batches/{id}/publish + dashboard button
CLI batch-create photo-loader publish BATCH_ID
Agent create_photo_batch publish_photo_batch (confirmation gate)

Parity docs updated: docs/reference/parity.md, docs/reference/agent-tools.md, docs/reference/cli.md, docs/feature-map.md.

Tests (RED to GREEN)

  • Service (the core regression): test_photo_task_create_batch_holds_until_publish — after create_batch, claim_next_due_item returns None and run_due processes 0; after publish_batch, the item becomes claimable and run_due sends it.
  • Repository: test_publish_batch_makes_held_item_claimable, test_cancel_held_item.
  • Lifecycle: test_sync_batch_all_held_stays_held.
  • Agent: TestPublishPhotoBatch (confirmation gate + success), plus existing create-against-real-service tests now assert status == HELD.
  • Web: test_photo_publish_batch_redirects_and_calls_service.
  • CLI: test_publish_action.
  • Permissions: tool_permissions_snapshot.json updated for publish_photo_batch (WRITE).

Verification

  • ruff check src/ tests/ conftest.py — clean
  • pytest targeted: 176 passed (32 repo+lifecycle, 65 agent+permissions, 21 CLI, 58 web/routes)

axisrow added 3 commits June 28, 2026 23:12
…sends via cron (#1173)

Introduces PhotoBatchStatus.HELD: create_batch now stores the batch and its
items as HELD, which claim_next_due_item (cron photo_due) ignores. A new
explicit publish_batch verb moves HELD items into the PENDING queue so only
run_photo_due / cron can send them afterwards.

Owner decision (#1173): posting via cron is a defect — 'create a batch' must
not mean 'create and send'. Deferred delivery already has native Telegram
scheduling (schedule_send -> SCHEDULED), so HELD is a distinct pre-publish
barrier, not a reuse of SCHEDULED.

Surfaces (CLI/Web/Agent parity):
- Web:  POST /dialogs/photos/batches/{id}/publish + dashboard button
- CLI:  photo-loader publish BATCH_ID
- Agent: publish_photo_batch tool with confirmation gate

Also: cancel_item now accepts HELD, _sync_batch_status surfaces HELD,
jobs_read_model maps HELD, tool_permissions_snapshot + parity docs updated.

Closes #1173
… edge-case test (#1190 review)

Dual-review fixups for PR #1190:

1. BLOCKER: add ('photo-loader','publish') to
   CLI_REAL_TG_MANUAL_OR_EXCLUDED_COMMANDS — the CLI leaf-coverage test
   (test_real_telegram_policy.py::test_cli_real_tg_parser_leaf_commands...)
   failed CI because the new local-only command was not manifested.

2. MED: wrap publish_batch in a single transaction() so the items-flip and
   the batch-status update are atomic. Without this the photo_due cron could
   claim a just-published item between the two writes, _sync_batch_status
   would flip the batch to RUNNING, and the second write would clobber it
   back to PENDING.

3. LOW: add test_missing_batch_id_returns_error for publish_photo_batch
   agent tool (batch_id is None path).
…gh (#1190)

Adding PhotoLoaderBundle.publish_batch (a one-line repo delegation) raised the
bundle method count from 135 to 136. Update the convention baseline accordingly.
@axisrow axisrow merged commit 87187aa into main Jun 28, 2026
4 checks passed
axisrow added a commit that referenced this pull request Jun 28, 2026
- Stub bundle.count_due_items in TestPhotoTaskServiceRunDue (4 tests) — run_due
  now calls it unconditionally; bare MagicMock bundles raised
  "MagicMock can't be used in 'await'" (signature-change-breaks-fakes, #1117).
- run_due item_id path: on_progress(1, max(due_total, 1)) avoids "[1/0]" when an
  item is claimed by id before its schedule_at; loop path caps total at
  processed so progress never exceeds the snapshot total.
- Merge resolution kept the #1190 HELD Publish button: photo_loader_batch_row.html
  now carries the publish-form column (HELD batches stay web-publishable) and the
  table header has the Прогресс + publish columns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TShpckA1DoLNezbD3txTL1
axisrow added a commit that referenced this pull request Jun 29, 2026
…-count) (#1194)

* feat(photo-loader): progress bar for batch scheduling (CLI + web item-count)

CLI: optional on_progress callback in PhotoTaskService.run_due and
PhotoAutoUploadService.run_due; run_due_impl prints plain "[done/total] ...
(~Xm left)" per item plus a final summary. No new dependencies (extends the
existing print(f"[N]...") pattern). Without a callback (None) behaviour is
unchanged — existing test fakes keep working.

Web (item-count parity): new repo count method (completed/total items per
batch), surfaced via jobs_read_model; new HTMX fragment
photo_loader_batch_row.html with hx-get/hx-trigger="every 5s" polling on
RUNNING batches (server-driven swap, per the HTMX policy — not fetch/SSE).

Tests: CLI progress callback, repo counts, jobs_read_model fields, route
fragment, photo task lifecycle. 99 passed isolated; ruff clean.

Closes #1152

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TShpckA1DoLNezbD3txTL1

* fix(photo-loader): review fixups for #1152 progress bar

- Stub bundle.count_due_items in TestPhotoTaskServiceRunDue (4 tests) — run_due
  now calls it unconditionally; bare MagicMock bundles raised
  "MagicMock can't be used in 'await'" (signature-change-breaks-fakes, #1117).
- run_due item_id path: on_progress(1, max(due_total, 1)) avoids "[1/0]" when an
  item is claimed by id before its schedule_at; loop path caps total at
  processed so progress never exceeds the snapshot total.
- Merge resolution kept the #1190 HELD Publish button: photo_loader_batch_row.html
  now carries the publish-form column (HELD batches stay web-publishable) and the
  table header has the Прогресс + publish columns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TShpckA1DoLNezbD3txTL1

* test(db): bump bundle method baseline 136→138 for #1152 count helpers

count_due_items / count_items_by_batch_status are consumed by
PhotoTaskService / JobsReadModel (which hold a PhotoLoaderBundle, not
db.repos), so the progress-count reads must live on the bundle surface.
Legitimate +2 growth — raise the ratchet cap accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TShpckA1DoLNezbD3txTL1

---------

Co-authored-by: axisrow <axisrow@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant