v0.2.15
RustBox CI / rustbox (push) Canceled after 0s

This commit is contained in:
Mateusz Gruszczyński committed 2026-11-02 23:13:21 +01:00
1 parent fb9eab87eb
commit d91212d24c
40 files changed
+585 -380

No files matched your search

+1 -1
View File
@@ -453,7 +453,7 @@ def sqlite_tenant_schema() -> tuple[str, int]:
raise ValueError(f"invalid RustBox tenant schema markers in {schema_path}")
tenant_sql = schema.split(begin, 1)[1].split(end, 1)[0]
versions = [46]
versions = [50]
for path in root.glob("V[0-9][0-9][0-9]__*.sql"):
try:
versions.append(int(path.name[1:4]))
+60 -96
View File
@@ -84,10 +84,10 @@ reader_js = read("src/web/js/40-messages-reader.js")
compose_js = read("src/web/js/50-compose.js")
compose_css = read("src/web/css/40-compose.css")
mail_css = read("src/web/css/30-mail.css")
require("perPage: 300" in bootstrap_js and "messages_per_page: 300" in bootstrap_js, "browser mailbox default is 300 messages")
require("perPage: 300" in bootstrap_js and "messages_per_page: 300" in bootstrap_js and 'message_loading_mode: "pagination"' in bootstrap_js, "browser mailbox defaults to 300-message pagination")
require(re.search(r"default_per_page\(\)\s*->\s*u32\s*\{\s*300\s*\}", read("src/routes/messages/types.rs")) is not None, "folder message API default page size is 300")
require('1..=300' in read("src/routes/messages/mod.rs"), "folder message API accepts up to 300 messages per page")
require('20..=300' in read("src/db/mail_preferences.rs"), "mailbox preference accepts 300 messages per page")
require('20..=300' in read("src/db/mail_preferences.rs") and '"pagination" | "progressive" | "all"' in read("src/db/mail_preferences.rs"), "mailbox preferences accept 300-message pages and the three loading modes")
require('const appShortcut = keyboardIdle && !event.ctrlKey && !event.metaKey && !event.altKey;' in bindings_js, "single-key shortcuts preserve Ctrl/Cmd/Alt browser shortcuts")
require('realtimeNotificationsReady' in realtime_js and 'suppressRealtimeNotificationsForFolder' in realtime_js, "realtime new-mail notification gating is present")
require('realtimeFolderIsDrafts' in realtime_js and 'if (realtimeFolderIsDrafts(data?.folder)) return true;' in realtime_js, "realtime new-mail notifications are suppressed for Drafts folders")
@@ -105,12 +105,13 @@ require('clipboardData' in bindings_js and 'uploadInlineImage(images, range)' in
require('.compose-attachment-kind' in compose_css and 'Image attachment' in compose_js and 'Inline image' in compose_js, "composer distinguishes embedded images from regular attachments")
require("function mergeComposeAttachments" in compose_js and "await refreshComposeAttachmentsFromServer(id);" in compose_js and "attachments: mergeComposeAttachments(local.attachments, server.attachments)" in compose_js, "composer preserves multiple attachments and draft recovery merges authoritative server attachment metadata")
require("const attachments = mergeComposeAttachments(state.composeAttachments);" in compose_js and "Inline link" in compose_js and "detachComposeAttachmentFromBody" in compose_js, "composer attachment strip keeps inline and regular attachments visible together")
require('grid-template-columns: repeat(2, minmax(0, 1fr));' in mail_css and '.load-more {' in mail_css and 'width: 100%;' in mail_css.split('.load-more {', 1)[1].split('}', 1)[0], "message load actions share the full available list width")
require('.message-pagination {' in mail_css and 'grid-template-columns: 30px auto 30px;' in mail_css and '.pagination-arrow {' in mail_css and '.load-more {' in mail_css and 'width: 100%;' in mail_css.split('.load-more {', 1)[1].split('}', 1)[0], "message pagination uses compact SVG arrows while progressive-load controls span the message list width")
require('id="compose-fields-toggle"' in read("src/web/index.html") and 'function setComposeFieldsCollapsed' in compose_js and 'function toggleComposeFields' in compose_js and '.compose-dialog.compose-fields-collapsed .compose-addresses' in compose_css and 'setComposeFieldsCollapsed(false)' in bindings_js, "composer addressing fields can collapse to maximize editor space and reopen on invalid recipient input")
require('id="compose-save-draft"' in read("src/web/index.html") and '$("#compose-save-draft").addEventListener("click", saveComposeDraftToImap);' in bindings_js and 'saveCurrentDraft(false, true, true)' in compose_js, "composer exposes an explicit manual IMAP draft save before cancel")
require('rememberSignatureRange' in bindings_js and 'selectedSignatureRange' in bindings_js and 'if (!range || range.collapsed) return null;' in bindings_js and 'state.identitySignatureRange = null;' in bindings_js and '.signature-editor-toolbar button' in bindings_js and 'event.preventDefault();' in bindings_js and 'restoreEditorRange(range, signatureEditor);' in bindings_js, "signature formatting preserves real selections and refuses collapsed-caret commands that would enable sticky typing styles")
index_html = read("src/web/index.html")
require('<div class="folder-quota-label"><span>Mailbox storage</span>' in index_html and '<span id="mail-server-label">RustBox</span><span>__RUSTBOX_VERSION__</span>' in index_html, "mailbox storage and footer labels stay protocol-neutral")
require('<div class="identity-signature-field"><span id="identity-signature-label">HTML signature / footer</span>' in index_html and 'aria-labelledby="identity-signature-label"' in index_html and '<label class="identity-signature-field">' not in index_html, "HTML signature editor is not wrapped in a label that can implicitly activate the Bold toolbar button")
require('function ensureComposeEntryBeforeSignature' in read("src/web/js/72-openpgp-identities.js") and 'data-rustbox-signature-entry' in read("src/web/js/72-openpgp-identities.js") and 'compose-body-entry' in compose_css, "new visible signatures keep an editable compose body entry above the signature")
require('.compose-signature.compose-signature-new' in compose_css and 'margin-top: 2.4em;' in compose_css and 'border-top: 1px solid color-mix(in srgb, var(--line) 58%, transparent);' in compose_css and 'removeAttribute("data-rustbox-signature-entry")' in compose_js, "new-message signature spacing is scoped, the separator is subdued, and editor-only scaffolding is stripped from outgoing HTML")
@@ -121,6 +122,8 @@ require('bindPaneResizer("#message-resizer", "messagePaneWidth", 300, 760)' in b
require('bindPaneResizer("#folder-resizer", "folderPaneWidth", 180, 420)' in bindings_js and 'value.clamp(180, 420).to_db_value()' in display_prefs_rs and 'id="pref-folder-width" type="range"\n min="180" max="420"' in index_html, "folder pane resize limits are aligned at 180..420 px and normalized server-side")
require('suppressPaneWidthValidationError' in auth_js and 'temporarilyDisablePanePreference' in auth_js and 'data.temporarilyDisabled' not in auth_js and 'handle.dataset.temporarilyDisabled = "true"' in auth_js and 'if (!suppressPaneWidthValidationError(error)) toast(error.message, "error")' in auth_js, "pane-width validation incompatibilities disable the in-view resizer without a user-facing error")
require('id="identity-signature-mode"' in read("src/web/index.html") and 'value="silent"' in read("src/web/index.html") and 'value="visible"' in read("src/web/index.html") and 'signature_insert_mode' in read("src/web/js/72-openpgp-identities.js"), "identity UI exposes silent and visible signature insertion modes")
require('id="pref-adaptive-email-colors"' in read("src/web/index.html") and 'adaptive_email_colors' in display_prefs_rs and 'adaptiveEmailColors' in auth_js and 'adaptMessagePreviewColors' in reader_js, "Appearance exposes persisted adaptive HTML-mail colors and the reader applies them")
require('id="mail-pref-loading-mode"' in index_html and 'id="message-pagination"' in index_html and 'function changeMailboxPage(delta)' in reader_js and 'function loadNextProgressivePage()' in reader_js and 'function loadAllMessages()' in reader_js and 'handleMessageListScroll' in bindings_js, "Mailbox settings expose pagination, progressive loading and load-all behavior")
require('identity.signature_insert_mode != "visible"' in read("src/routes/send.rs") and 'data-rustbox-signature' in read("src/web/js/72-openpgp-identities.js"), "silent signatures append server-side while visible signatures remain composer-owned and de-duplicatable")
require('id="compose-delete-draft"' in read("src/web/index.html") and '$("#compose-delete-draft").addEventListener("click", deleteComposeDraft);' in bindings_js and 'function deleteComposeDraft()' in compose_js and 'imap_uid_validity' in compose_js, "composer exposes permanent draft deletion with current IMAP coordinates")
require('if req.sync_imap {' in read("src/routes/drafts.rs") and 'Failed to save draft to IMAP' in read("src/routes/drafts.rs") and 'AppError::ServiceUnavailable' in read("src/routes/drafts.rs"), "manual IMAP draft save fails visibly when the IMAP snapshot cannot be appended")
@@ -138,8 +141,9 @@ require('encoding_rs::Encoding::for_label' in read("src/imap/parse.rs") and 'dec
require('let is_pdf = response_content_type_is(&response, "application/pdf");' in read("src/routes/mod.rs") and 'HeaderValue::from_static("SAMEORIGIN")' in read("src/routes/mod.rs") and "HeaderValue::from_static(\"frame-ancestors 'self'\")" in read("src/routes/mod.rs"), "security headers allow same-origin framing for inline PDF previews only")
index_html = read("src/web/index.html")
require('class="compose-limit-summary"' in index_html and 'Per-file upload limit set with' not in index_html and 'Hard limit for the complete RFC822/MIME message' not in index_html, "Composing shows compact raw size limits without implementation details")
require("V050" in read("RELEASE-MANIFEST.txt") and "V051" in read("RELEASE-MANIFEST.txt"), "release manifest documents V050 and reserves V051")
require("V050" in read("migrations/README.md") and "V051" in read("migrations/README.md"), "migration guide documents V050 and reserves V051")
require("V049" in read("RELEASE-MANIFEST.txt") and "V050" in read("RELEASE-MANIFEST.txt") and "V051" in read("RELEASE-MANIFEST.txt") and "V052" not in read("RELEASE-MANIFEST.txt"), "release manifest documents V049 baseline, V050 current migration and next V051")
require("V049" in read("migrations/README.md") and "V050" in read("migrations/README.md") and "V051" in read("migrations/README.md") and "V052" not in read("migrations/README.md"), "migration guide documents V049 baseline, V050 current migration and next V051")
require("versions = [50]" in read("scripts/migrate_roundcube.py"), "Roundcube provisioning fallback follows the V050 consolidated baseline")
require((ROOT / "docs/STORAGE_SEARCH_OPERATIONS.md").is_file(), "storage/search operations guide exists")
require('imap_host = ""' not in read("config.dev.toml"), "development config does not override IMAP host with an invalid empty value")
require(not re.search(r"(?m)^IMAP_HOST=\s*$", read(".env.example")), "environment example does not set an invalid empty IMAP host")
@@ -548,114 +552,76 @@ try:
require(sqlite_generation == ("2",), "SQLite header-sync UPSERT remains unambiguous and increments generation")
account_default = conn.execute("PRAGMA table_info(rustbox_accounts)").fetchall()
version_default = next((row[4] for row in account_default if row[1] == "schema_version"), None)
require(str(version_default).strip("'\"") == "46", "fresh global compatibility baseline remains V046 before tenant registration")
require(str(version_default).strip("'\"") == "50", "fresh account schema version defaults to current V050")
display_columns = {row[1]: row for row in conn.execute("PRAGMA table_info(u_test__display_preferences)")}
require("adaptive_email_colors" in display_columns and str(display_columns["adaptive_email_colors"][4]) == "0", "V050 defaults adaptive HTML-mail colors off")
mail_columns = {row[1]: row for row in conn.execute("PRAGMA table_info(u_test__mail_preferences)")}
require("message_loading_mode" in mail_columns and str(mail_columns["message_loading_mode"][4]).strip("\'\"") == "pagination", "V050 defaults mailbox loading mode to pagination")
conn.close()
except Exception as exc:
errors.append(f"fresh SQLite schema execution failed: {exc}")
# V047 upgrade execution from a minimal V046 attachment table, including data preservation.
try:
conn = sqlite3.connect(":memory:")
conn.executescript(
"CREATE TABLE u_test__draft_attachments ("
"id TEXT PRIMARY KEY, draft_id TEXT NOT NULL, filename TEXT NOT NULL, "
"content_type TEXT NOT NULL, size INTEGER NOT NULL, file_path TEXT NOT NULL, "
"created_at TEXT NOT NULL);"
"INSERT INTO u_test__draft_attachments "
"(id, draft_id, filename, content_type, size, file_path, created_at) VALUES "
"('att-1', 'draft-1', 'report.pdf', 'application/pdf', 1234, '/legacy/report.pdf', '2026-01-01T00:00:00Z');"
)
migration = read("migrations/sqlite/V047__blob_keys_and_search_outbox.sql").replace("{{p}}", "u_test__")
conn.executescript(migration)
columns = {row[1] for row in conn.execute("PRAGMA table_info(u_test__draft_attachments)")}
require("checksum_sha256" in columns, "V047 adds checksum_sha256 to SQLite attachments")
row = conn.execute(
"SELECT id, draft_id, filename, content_type, size, file_path, created_at, checksum_sha256 "
"FROM u_test__draft_attachments WHERE id='att-1'"
).fetchone()
require(
row == ('att-1', 'draft-1', 'report.pdf', 'application/pdf', 1234, '/legacy/report.pdf', '2026-01-01T00:00:00Z', None),
"V047 preserves existing attachment rows and initializes checksum as NULL",
)
require(conn.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='u_test__search_outbox'").fetchone() is not None, "V047 creates SQLite search_outbox")
outbox_columns = {row[1] for row in conn.execute("PRAGMA table_info(u_test__search_outbox)")}
require("claimed_until" in outbox_columns, "V047 creates an independent SQLite outbox lease column")
conn.close()
except Exception as exc:
errors.append(f"SQLite V047 migration execution failed: {exc}")
# V048 upgrade execution from a V047-style folders table, preserving existing counters.
try:
conn = sqlite3.connect(":memory:")
conn.executescript(
"CREATE TABLE u_test__folders ("
"name TEXT PRIMARY KEY, delimiter TEXT, parent TEXT, flags TEXT NOT NULL DEFAULT '', "
"is_subscribed INTEGER NOT NULL DEFAULT 1, total_count INTEGER NOT NULL DEFAULT 0, "
"unread_count INTEGER NOT NULL DEFAULT 0, uid_validity INTEGER NOT NULL DEFAULT 0, "
"highest_modseq INTEGER NOT NULL DEFAULT 0, updated_at TEXT NOT NULL, messages_updated_at TEXT);"
"INSERT INTO u_test__folders "
"(name, total_count, unread_count, uid_validity, highest_modseq, updated_at) VALUES "
"('INBOX', 3444, 12, 77, 9001, '2026-11-01T00:00:00Z');"
)
migration = read("migrations/sqlite/V048__folder_counter_generation.sql").replace("{{p}}", "u_test__")
conn.executescript(migration)
columns = {row[1] for row in conn.execute("PRAGMA table_info(u_test__folders)")}
require("counter_generation" in columns, "V048 adds counter_generation to SQLite folders")
row = conn.execute(
"SELECT name, total_count, unread_count, uid_validity, highest_modseq, counter_generation "
"FROM u_test__folders WHERE name='INBOX'"
).fetchone()
require(
row == ('INBOX', 3444, 12, 77, 9001, 0),
"V048 preserves existing folder counters and initializes generation at zero",
)
conn.close()
except Exception as exc:
errors.append(f"SQLite V048 migration execution failed: {exc}")
# V049 creates the durable blob deletion outbox.
try:
conn = sqlite3.connect(":memory:")
migration = read("migrations/sqlite/V049__blob_delete_outbox.sql").replace("{{p}}", "u_test__")
conn.executescript(migration)
columns = {row[1] for row in conn.execute("PRAGMA table_info(u_test__blob_delete_outbox)")}
require({"id", "storage_key", "attempts", "available_at", "claimed_until", "last_error", "created_at", "updated_at"}.issubset(columns), "V049 creates SQLite blob_delete_outbox with durable retry state")
indexes = {row[1] for row in conn.execute("PRAGMA index_list(u_test__blob_delete_outbox)")}
require(any("blob_delete_outbox_ready" in name for name in indexes), "V049 creates SQLite blob cleanup readiness index")
conn.close()
except Exception as exc:
errors.append(f"SQLite V049 migration execution failed: {exc}")
# V050 adds compact-wide-row and signature insertion preferences.
# V047-V049 are squashed into the V049 compatibility baseline. V050 is the single runtime migration.
# V050 upgrade execution and migration/query parity across SQL engines.
# V050 upgrades a V049-compatible account schema with all unreleased 0.2.15 preferences.
try:
conn = sqlite3.connect(":memory:")
conn.executescript(
"CREATE TABLE u_test__display_preferences (id INTEGER PRIMARY KEY);"
"CREATE TABLE u_test__mail_preferences (id INTEGER PRIMARY KEY);"
"CREATE TABLE u_test__identities (id INTEGER PRIMARY KEY);"
)
migration = read("migrations/sqlite/V050__compact_rows_and_signature_mode.sql").replace("{{p}}", "u_test__")
conn.executescript(migration)
display_columns = {row[1]: row for row in conn.execute("PRAGMA table_info(u_test__display_preferences)")}
mail_columns = {row[1]: row for row in conn.execute("PRAGMA table_info(u_test__mail_preferences)")}
identity_columns = {row[1]: row for row in conn.execute("PRAGMA table_info(u_test__identities)")}
require("adaptive_email_colors" in display_columns and str(display_columns["adaptive_email_colors"][4]) == "0", "V050 defaults adaptive HTML-mail colors off in SQLite")
require("message_loading_mode" in mail_columns and str(mail_columns["message_loading_mode"][4]).strip("\'\"") == "pagination", "V050 defaults mailbox loading mode to pagination in SQLite")
require("compact_wide_rows" in mail_columns and str(mail_columns["compact_wide_rows"][4]) == "1", "V050 defaults compact wide rows on in SQLite")
require("signature_insert_mode" in identity_columns and identity_columns["signature_insert_mode"][4] == "'silent'", "V050 defaults identity signature insertion to silent in SQLite")
conn.execute("INSERT INTO u_test__display_preferences(id) VALUES (1)")
conn.execute("INSERT INTO u_test__mail_preferences(id) VALUES (1)")
conn.execute("INSERT INTO u_test__identities(id) VALUES (1)")
require(conn.execute("SELECT compact_wide_rows FROM u_test__mail_preferences WHERE id=1").fetchone()[0] == 1, "V050 applies compact-row default to existing/new SQLite rows")
require(conn.execute("SELECT signature_insert_mode FROM u_test__identities WHERE id=1").fetchone()[0] == "silent", "V050 applies silent-signature default to existing/new SQLite rows")
require(conn.execute("SELECT adaptive_email_colors FROM u_test__display_preferences WHERE id=1").fetchone()[0] == 0, "V050 applies adaptive-color default to SQLite rows")
require(conn.execute("SELECT message_loading_mode, compact_wide_rows FROM u_test__mail_preferences WHERE id=1").fetchone() == ("pagination", 1), "V050 applies mailbox pagination and compact-row defaults to SQLite rows")
require(conn.execute("SELECT signature_insert_mode FROM u_test__identities WHERE id=1").fetchone()[0] == "silent", "V050 applies silent-signature default to SQLite rows")
conn.close()
except Exception as exc:
errors.append(f"SQLite V050 migration execution failed: {exc}")
# Migration/query parity across SQL engines.
for dialect in ("sqlite", "mysql", "pgsql"):
require((ROOT / f"migrations/{dialect}/V047__blob_keys_and_search_outbox.sql").is_file(), f"V047 migration exists for {dialect}")
require((ROOT / f"migrations/{dialect}/V048__folder_counter_generation.sql").is_file(), f"V048 migration exists for {dialect}")
require((ROOT / f"migrations/{dialect}/V049__blob_delete_outbox.sql").is_file(), f"V049 migration exists for {dialect}")
require((ROOT / f"migrations/{dialect}/V050__compact_rows_and_signature_mode.sql").is_file(), f"V050 migration exists for {dialect}")
for retired in (
"V047__blob_keys_and_search_outbox.sql",
"V048__folder_counter_generation.sql",
"V049__blob_delete_outbox.sql",
"V051__adaptive_email_colors.sql",
):
require(not (ROOT / f"migrations/{dialect}/{retired}").exists(), f"consolidated/unreleased migration {retired} is absent for {dialect}")
require((ROOT / f"migrations/{dialect}/V050__compact_rows_and_signature_mode.sql").is_file(), f"V050 runtime migration exists for {dialect}")
migration_text = read(f"migrations/{dialect}/V050__compact_rows_and_signature_mode.sql")
require(
"adaptive_email_colors" in migration_text
and "message_loading_mode" in migration_text
and "compact_wide_rows" in migration_text
and "signature_insert_mode" in migration_text,
f"V050 runtime migration contains all unreleased preference columns for {dialect}",
)
schema_text = read(f"migrations/{dialect}/schema.sql")
require("Current runtime schema: V050" in schema_text and "counter_generation" in schema_text and "blob_delete_outbox" in schema_text and "compact_wide_rows" in schema_text and "signature_insert_mode" in schema_text, f"fresh {dialect} schema includes V050 compact-row/signature preferences")
require(
"Squashed compatibility baseline: V049. Current schema: V050." in schema_text
and "counter_generation" in schema_text
and "blob_delete_outbox" in schema_text
and "compact_wide_rows" in schema_text
and "signature_insert_mode" in schema_text
and "adaptive_email_colors" in schema_text
and "message_loading_mode" in schema_text
and "pagination" in schema_text,
f"fresh {dialect} schema contains current V050 schema on the V049 compatibility baseline",
)
queries = read(f"src/queries/{dialect}/queries.rs")
require("adaptive_email_colors" in queries, f"display-preference query includes adaptive_email_colors for {dialect}")
require("message_loading_mode" in queries, f"mail-preference query includes message_loading_mode for {dialect}")
folder_block = re.search(r"pub mod db_folders\s*\{(.*?)\n\}", queries, re.S)
require(folder_block is not None and "Q_032" in folder_block.group(1) and "counter_generation = counter_generation + 1" in folder_block.group(1), f"folder counter CAS increments generation for {dialect}")
block_match = re.search(r"pub mod db_search_outbox\s*\{(.*?)\n\}", queries, re.S)
@@ -671,16 +637,14 @@ for dialect in ("sqlite", "mysql", "pgsql"):
pool = read("src/db/pool.rs")
for dialect in ("SQLITE", "MYSQL", "PGSQL"):
require(f"{dialect}_MIGRATIONS" in pool and "V047__blob_keys_and_search_outbox.sql" in pool, f"runtime migration catalog includes V047 ({dialect.lower()})")
require(f"{dialect}_MIGRATIONS" in pool and "V048__folder_counter_generation.sql" in pool, f"runtime migration catalog includes V048 ({dialect.lower()})")
require(f"{dialect}_MIGRATIONS" in pool and "V049__blob_delete_outbox.sql" in pool, f"runtime migration catalog includes V049 ({dialect.lower()})")
require(f"{dialect}_MIGRATIONS" in pool and "V050__compact_rows_and_signature_mode.sql" in pool, f"runtime migration catalog includes V050 ({dialect.lower()})")
require("const BASE_SCHEMA_VERSION: u32 = 46" in pool, "legacy squashed baseline stays V046")
require(f"const {dialect}_MIGRATIONS: &[(u32, &str)]" in pool and "V050__compact_rows_and_signature_mode.sql" in pool, f"runtime migration catalog includes V050 ({dialect.lower()})")
require("pub const BASE_SCHEMA_VERSION: u32 = 49" in pool, "consolidated squashed baseline is V049")
require("V047__blob_keys_and_search_outbox.sql" not in pool and "V048__folder_counter_generation.sql" not in pool and "V049__blob_delete_outbox.sql" not in pool and "V051__adaptive_email_colors.sql" not in pool, "retired standalone migrations are absent from the runtime catalog")
require("latest_schema_version" in pool, "runtime schema version is computed from migrations")
require(
"latest_supported_sqlite = SQLITE_MIGRATIONS" in pool
and "if version > latest_supported_sqlite" in pool,
"legacy SQLite import accepts runtime migrations up to this build instead of stopping at V046",
"legacy SQLite import accepts the V049 baseline, applies V050 and rejects schemas newer than this build",
)
# Tenant/import table parity and transaction-bound mutation helpers.
@@ -777,11 +741,11 @@ require('json!({"term": {"user_hash": user_hash}})' in search_rs, "OpenSearch qu
require("process_search_outbox" in search_rs and "claim(&conn" in search_rs, "OpenSearch worker uses durable claimed outbox events")
require("fn event_lease_secs(&self) -> u64" in search_rs and "let lease_secs = backend.event_lease_secs();" in search_rs and "claim(&conn, &event, 60)" not in search_rs, "OpenSearch outbox lease is derived from HTTP timeout/failover budget instead of a fixed 60 seconds")
require("opensearch_event_lease_covers_timeout_and_failover_budget" in search_rs, "OpenSearch lease budget has a source-level unit regression test")
require("claimed_until" in read("migrations/sqlite/V047__blob_keys_and_search_outbox.sql") and "reenqueue_preserves_active_claim_lease" in read("src/db/search_outbox.rs"), "outbox re-enqueue cannot steal an active worker lease")
require("claimed_until" in read("migrations/sqlite/schema.sql") and "reenqueue_preserves_active_claim_lease" in read("src/db/search_outbox.rs"), "outbox re-enqueue cannot steal an active worker lease")
blob_delete_rs = read("src/db/blob_delete_outbox.rs")
attachments_rs = read("src/routes/attachments.rs")
send_rs = read("src/routes/send.rs")
require("mark_failure" in blob_delete_rs and "min(3600)" in blob_delete_rs and "max_attempts" not in blob_delete_rs and "dead_letter" not in read("migrations/sqlite/V049__blob_delete_outbox.sql"), "blob deletion outbox retries indefinitely with capped backoff and no permanent dead-letter")
require("mark_failure" in blob_delete_rs and "min(3600)" in blob_delete_rs and "max_attempts" not in blob_delete_rs and "dead_letter" not in read("migrations/sqlite/schema.sql").split("CREATE TABLE {{p}}blob_delete_outbox", 1)[1].split("CREATE TABLE {{p}}search_outbox", 1)[0], "blob deletion outbox retries indefinitely with capped backoff and no permanent dead-letter")
require("unchecked_transaction()" in attachments_rs and "blob_delete_outbox::enqueue(&transaction" in attachments_rs and "delete_draft_attachment(&transaction" in attachments_rs, "single attachment deletion atomically queues blob cleanup with metadata deletion")
require("blob_delete_outbox::enqueue(&transaction" in drafts_rs and "delete_draft(&transaction" in drafts_rs, "whole-draft deletion atomically queues every attachment blob before metadata cascade")
require("blob_delete_outbox::enqueue(&transaction" in send_rs and "delete_draft(&transaction" in send_rs, "post-send draft cleanup atomically queues attachment blobs")