362 lines
34 KiB
Markdown
362 lines
34 KiB
Markdown
# DM: доставка сообщений и файлов
|
||
|
||
Личные беседы принадлежат `src/dm/`. Общие CHAT-группы используются для знакомства,
|
||
проверки ролей и маршрутизации; группа не становится идентичностью беседы.
|
||
|
||
## Реализовано
|
||
|
||
- История и постоянная исходящая очередь сообщений в общей SQLite ядра.
|
||
- Доставка через живой маршрут router, включая BGP-транзит.
|
||
- При отсутствии маршрута — подписанный PUT доступному суперузлу общей группы сторон.
|
||
- Приём после проверки подписи и расшифровки, подтверждение только после commit.
|
||
- Доставка с суперузла после возвращения получателя, независимо от отправителя.
|
||
- Точное удаление промежуточного сообщения и сохранение квитанции для поздних повторов.
|
||
- Подготовка, шифрование, проверка и durable публикация медиа в рабочих потоках.
|
||
- Прямая передача файлов либо custody на отдельном M, доставка после restart и подтверждённое удаление.
|
||
- TTL и квота M; вложения и статусы доставки в Qt и Android.
|
||
- Голосовые Opus и видео MP4 с зашифрованными метаданными; общий ввод и отображение с каналами.
|
||
|
||
## Идентичность и шифрование
|
||
|
||
```text
|
||
conv_hash = SHA256("utun_dm_v" || min(node_a,node_b) || max(node_a,node_b))
|
||
conv_id = native_u64(conv_hash[0:8]) & 0x7FFFFFFFFFFFFFFF
|
||
content_key = SHA256(X25519(my_private, target_public) || "utun_dm_content")
|
||
msg_nonce = SHA256(conv_id || author || seq)[..13]
|
||
media_nonce = SHA256("utun_dm_media" || author || media_id || part)[..13]
|
||
```
|
||
|
||
Префикс conv_hash — 9 ASCII-байт без NUL. ID узлов подаются в хеш в порядке
|
||
байтов хоста; native_u64 читает первые 8 байт хеша в том же порядке.
|
||
Нулевой conv_id заменяется на 1.
|
||
|
||
Сообщения и медиа используют один content_key target, без ключей отдельных файлов.
|
||
AES-256-CCM: nonce 13 байт, tag 16 байт. Максимальная порция plaintext — 65535 байт;
|
||
порция медиа — 32768 байт. UUID медиа новый для каждой подготовки. Повтор доставки
|
||
использует неизменяемый ciphertext. Повторная подготовка после ошибки требует нового UUID.
|
||
|
||
Каноническое тело сообщения:
|
||
|
||
```text
|
||
[conv:8][seq:8][ts:8][author:8][ct_len:1][ct][enc_len:2][ciphertext+tag][author_sig:64]
|
||
```
|
||
|
||
Автор подписывает все поля до sig. conv связывает автора с получателем.
|
||
Для `ct=file` перед подписью добавляется descriptor `[UUID:16][plaintext_size:8][SHA256(ciphertext):32]`.
|
||
В зашифрованном теле находится `attachment_info`; пути хранения строятся только из UUID.
|
||
Размер data в API отправки ограничен 1024 байтами; для file сюда входит открытый
|
||
descriptor (56 байт), поэтому на зашифрованные метаданные остаётся до 968 байт.
|
||
Парсер входящего wire допускает до 1024 байт ciphertext plaintext отдельно от descriptor.
|
||
seq — положительное int64, ts — миллисекунды NTP-времени instance.
|
||
|
||
```text
|
||
[kind:1][name_len:2][duration_ms:4][width:2][height:2][UTF8_name:name_len][waveform:100, только voice]
|
||
```
|
||
|
||
Поля метаданных многобайтной длины имеют big-endian формат. kind: 1=file,
|
||
2=voice, 3=video. Имя — basename до 255 байт, без разделителей, управляющих символов
|
||
и некорректного UTF-8. FILE имеет нулевые duration/dimensions; VOICE — положительную
|
||
длительность и 100 символов уровней waveform; VIDEO — длительность и размеры.
|
||
Длительность ограничена 24 часами, размеры — 16384. Метаданные проверяются до
|
||
локального commit и до подтверждения получателем; S/M видят только ciphertext.
|
||
|
||
Имя собеседника ограничено `DM_PEER_NAME_MAX` (63 байта без NUL): создание,
|
||
загрузка и снимок бесед сохраняют только целые UTF-8 code points. Для 32 букв «Я»
|
||
сохраняется 31 буква (62 байта). При повреждённой кодировке, включая NUL внутри
|
||
SQL TEXT, остаётся корректный префикс; WARN содержит conv_id, смещение и длину.
|
||
Снимок нормализует также имя из nodes при пустом peer_name. Чтение не меняет БД;
|
||
при следующей штатной записи беседы нормализованное имя сохраняется вместе с ней.
|
||
Подробности исправления: [dm_audit_findings.txt](dm_audit_findings.txt), раздел9.
|
||
|
||
Медиа ciphertext — последовательность порций с отдельным tag каждой порции.
|
||
Последняя порция короче; пустой файл содержит один tag. Размер plaintext и SHA256
|
||
ciphertext должны входить в подписанный manifest. Потоковая расшифровка проверяет все
|
||
tags, точный размер и хеш; владелец публикует временный файл только после успеха
|
||
и собственного durable сохранения.
|
||
|
||
## Владение и данные
|
||
|
||
- `dm_core`: беседы, история, outbox и подтверждение получателя.
|
||
- `dm_history`: поколения, hidden, точные отпечатки удалённых сообщений и манифест локальных путей.
|
||
- `dm_mailbox`: промежуточные сообщения суперузла и квитанции.
|
||
- `dm_crypto`: вывод conv_id, content_key и nonce сообщений без БД/сети.
|
||
- `media_crypto`: общие CCM и потоковые файловые примитивы для DM и групповых вложений.
|
||
- `dm_media`: файлы endpoint, загрузки и отдельная custody M.
|
||
- `dm_mailbox_media`: durable задания S, выбор M и повтор удаления до DELETE_ACK.
|
||
- `file_transfer`: подписанная передача байтов, backpressure и отдельный CM handle каждой операции.
|
||
- `attachment`: формат, валидация и тип метаданных, без сети и БД.
|
||
- `voice_file`: общий Opus-кодек отправки; `attachment_send` готовит файл в worker
|
||
и передаёт результат отдельно в `chat_core` либо `dm_media`.
|
||
|
||
```text
|
||
dm_conversations(conv_id, peer_node_id, peer_x25519, peer_ed25519, peer_name,
|
||
group_id, last_out_seq, last_in_seq, created_at, last_ts)
|
||
dm_messages(conv_id, dir, seq, ts, author, ct, data, sig)
|
||
dm_outbox(conv_id, seq, body, retry_at)
|
||
dm_pending_messages(recipient, sender, seq, msg, group_id, retry_at)
|
||
dm_mail_receipts(recipient, sender, seq, receipt, group_id)
|
||
dm_history(conv_id, generation, hidden)
|
||
dm_deleted_messages(conv_id, dir, seq, body_hash, media_id, media_receipt)
|
||
dm_local_files(path, conv_id, generation, busy, retry_at)
|
||
```
|
||
|
||
Запись исходящего сообщения, увеличение seq и запись неизменяемого body в outbox —
|
||
одна транзакция. При ошибке откатывается всё, сообщение не выходит в сеть.
|
||
Приём, создание/обновление беседы и история — также одна транзакция.
|
||
|
||
Dedup — точный ключ (conv,dir,seq) с проверкой сохранённой подписи. last_in_seq —
|
||
статистика максимального номера, не курсор подтверждения: seq=99 после seq=100
|
||
сохраняется. Тело с другой подписью для уже сохранённого seq отклоняется.
|
||
|
||
Локальное удаление (`dm_history_delete`) сохраняет запись беседы, ключи и счётчики,
|
||
скрывает её (`dm_history.hidden`) и увеличивает постоянное `generation`.
|
||
В одной транзакции удаляются история, outbox и локальные media-записи, а SHA256
|
||
точных удалённых тел сохраняется в `dm_deleted_messages` по `(conv_id,dir,seq)`.
|
||
Файловая квитанция сохраняется только если она уже была получена; удаление само её не создаёт.
|
||
После commit отменяются незавершённые операции; физическая уборка файлов выполняется отдельно.
|
||
Это локальная операция: история адресата и промежуточные копии ею не отзываются.
|
||
|
||
Точный входящий удалённый повтор получает квитанцию сообщения без повторного создания
|
||
истории/файла и без открытия скрытой беседы. Иное тело с тем же ключом удалённой записи
|
||
отклоняется. Впервые доставленное сообщение, включая меньший seq, открывает беседу;
|
||
явный `dm_start` также снимает hidden без смены поколения. Отложенные отправки
|
||
проверяют generation, чтобы старый запрос не попал в повторно открытую беседу.
|
||
|
||
Загрузка беседы различает `FOUND`, `NOT_FOUND` и `ERROR`: отсутствие означает
|
||
только успешно завершённый SELECT без строки. Ошибка SQL, повреждённые ключи
|
||
или отказ X25519 прекращают открытие/приём/отправку/приглашение без пересоздания
|
||
беседы и сброса счётчиков. Полный снимок выдаётся только после успешной загрузки.
|
||
Исправление и отказные проверки описаны в [dm_audit_findings.txt](dm_audit_findings.txt).
|
||
|
||
Таймеры работают раз в 1с: outbox A и текстовая очередь S обрабатывают до 32 задач;
|
||
локальные файлы endpoint, custody M и media jobs S — до 8 задач каждый.
|
||
Обычный retry_at — через 5с; исходящий файл с квитанцией проверяется через 3600с,
|
||
неудачная уборка локального пути повторяется через 30с. Это время допуска следующей
|
||
попытки, не гарантия доставки за указанный срок. Перед отправкой проверяется backpressure router.
|
||
Потеря пути сохраняет durable задачу; stop отменяет таймеры, но сохраняет БД.
|
||
При старте медиа сбрасывает retry_at/downloading; текстовые очереди используют сохранённый retry_at.
|
||
|
||
Все DM API вызываются в uasync-потоке. Порядок запуска в `chat_service_start`:
|
||
mailbox (включая media coordinator), core/history, media/file_transfer; к этому времени
|
||
SQLite, router и media_async уже живы. Порядок остановки: media_quiesce прекращает
|
||
новые задачи и закрывает transfer/CM handles; media_async_destroy завершает workers
|
||
и callbacks с живыми owners; затем уничтожаются media, core и mailbox, после них группы/чат.
|
||
SQLite принадлежит ядру и остановкой DM не закрывается. `dm_media_destroy` при живой
|
||
работе логирует ошибку и оставляет owner; заменять drain таким вызовом нельзя.
|
||
|
||
## Подтверждения и протокол
|
||
|
||
Сервис `ETCP_RT_ID_DM=0x34`: MSG=1, RECEIPT=2.
|
||
|
||
```text
|
||
["DMACK001":8][conv:8][seq:8][author:8][recipient:8][SHA256(message):32][recipient_sig:64]
|
||
```
|
||
|
||
Квитанцию создаёт только получатель после commit. Подпись охватывает первые 72 байта.
|
||
Отправитель удаляет outbox только после проверки подписи и точного хеша тела.
|
||
История беседы сохраняется. Подтверждение транспорта router не заменяет квитанцию.
|
||
|
||
Сервис `ETCP_RT_ID_DM_MAILBOX=0x35`:
|
||
|
||
```text
|
||
PUT=1 [recipient:8][message]
|
||
DELIVER=2 [message]
|
||
RECEIPT=3 [receipt:136]
|
||
STORED=4 [conv:8][seq:8]
|
||
```
|
||
|
||
Все пакеты идут с подписью router; unsigned control отклоняется.
|
||
PUT принимается только от автора, при роли supernode=yes у локального узла
|
||
в общей группе автора и получателя. Роль берётся из подписанного adm_tags;
|
||
node_type=4 — производная сетевой классификации, для авторизации не используется.
|
||
storage=yes само по себе не даёт права хранить сообщения.
|
||
|
||
STORED подтверждает durable запись суперузлом, outbox до квитанции получателя остаётся
|
||
у автора. Суперузел повторяет DELIVER при появлении живого маршрута. RECEIPT проверяется
|
||
по точному сохранённому телу; запись квитанции и удаление копии — одна транзакция.
|
||
Квитанция пересылается автору. Поздний PUT возвращает её и не возрождает задачу.
|
||
|
||
dm_mail_receipts содержит только служебные квитанции; доставленное содержимое
|
||
с промежуточного узла удаляется. Ранее использовавшийся dm_mail/PULL/HELLO и
|
||
диапазонные ACK не поддерживаются: это devel, совместимость не сохраняется.
|
||
TTL текстовых сообщений не введён; недельный TTL из pm.txt относится к медиа.
|
||
|
||
## Доставка медиа по pm.txt
|
||
|
||
1. Общая передача байтов: явные peer/group/file/size, backpressure и CM handle
|
||
на всю активную операцию. Владение handle сохраняется после DOWN/TIMEOUT.
|
||
Используется `ETCP_RT_ID_FILE_TRANSFER=0x37`. Один callback читает/отправляет
|
||
не более 8 КиБ, продолжение через `call_soon`; inflight ограничен 16 пакетами.
|
||
2. Отдельный owner dm_media: подготовка ciphertext, manifest, отправка/скачивание,
|
||
временные файлы, TTL и лимит хранения. Каталоги dm_media/ и dm_pending/ вне
|
||
группового обхода очистки orphan-файлов; групповые объявления HAVE_BLOCK не используются.
|
||
3. Суперузел сначала durable регистрирует сообщение и media job, затем выбирает M.
|
||
M скачивает ciphertext у A и сообщает S подтверждённое хранение, место и expiry.
|
||
4. Текстовая квитанция и квитанция медиа независимы. Получатель подтверждает медиа
|
||
после проверки, публикации файла и commit. S хранит задачу удаления у M до DELETE_ACK
|
||
либо завершения подтверждённого TTL назначения; поздний STORED не возрождает доставку.
|
||
5. TTL медиа `dm_media_ttl_sec` по умолчанию 604800 с; квота `dm_media_storage_mb`
|
||
по умолчанию 1024 МиБ. Настройки на M. Новая custody отклоняется при
|
||
недостатке квоты; уже подтверждённые копии не вытесняются молча раньше срока.
|
||
6. Stop снимает новые задачи и закрывает transfer/CM handles, затем ждёт
|
||
media_async с живыми owner-контекстами. Группы уничтожаются после owners.
|
||
|
||
При проверке квоты M ошибки SQL чтения занятого места логируются и прекращают
|
||
обработку STORE без `REJECT` и регистрации custody. Назначение M сохраняется на S;
|
||
следующий STORE приходит через существующий таймер повторов. Квота сравнивается
|
||
только после получения строки SUM и успешного finalize. Реальный недостаток места
|
||
вызывает `REJECT` и выбор другого M. Исправление и регрессии:
|
||
[dm_audit_findings.txt](dm_audit_findings.txt), раздел6.
|
||
|
||
Освобождение назначения на S по EXPIRED/REJECT и сброс текущего holder выполняются
|
||
в одной транзакции. Ошибка prepare/bind/step/finalize/commit откатывает оба изменения.
|
||
Подтверждённое назначение завершается также таймером по сохранённому expires:
|
||
потерянный EXPIRED и offline B не оставляют stored/holder навсегда. После restart
|
||
таймер берёт те же durable факты. Если STORE ещё не подтверждён, S продолжает повторять
|
||
его; M возвращает STORED/EXPIRED из custody/tombstone. Квитанция B сохраняет приоритет
|
||
над TTL; старый control другого M не сбрасывает новое текущее назначение.
|
||
Текстовая история, подписи и сроки хранения M этим переходом не меняются.
|
||
Подробности и регрессии: [dm_audit_findings.txt](dm_audit_findings.txt), раздел11.
|
||
|
||
Control `ETCP_RT_ID_DM_MEDIA=0x38`: перед body идёт один байт команды.
|
||
Все многобайтные поля DM/control имеют native-endian порядок; исключение —
|
||
зашифрованный attachment_info с big-endian полями, описанный выше.
|
||
|
||
| Команда | Body |
|
||
| --- | --- |
|
||
| OFFER=1 | recipient:8, holder:8, expires:8, file-message |
|
||
| STORE=2 | recipient:8, file-message |
|
||
| STORED=3 | UUID:16, expires:8 |
|
||
| RECEIPT=4 / DELETE=5 | файловая квитанция:152 |
|
||
| DELETE_ACK=6 / EXPIRED=7 | UUID:16 |
|
||
| REJECT=8 | UUID:16, reason:1 (1 — недостаток квоты) |
|
||
|
||
Подпись router обязательна. Media receipt содержит `DMFILE01`, conv, seq, author,
|
||
recipient, UUID и SHA256 полного подписанного сообщения; B подписывает первые 88 байт.
|
||
Отдельные таблицы `dm_files`, `dm_custody`, `dm_custody_done`, `dm_media_jobs`,
|
||
`dm_media_holders` сохраняют состояния и tombstones. Все выданные grants M
|
||
учитываются при удалении; повторный STORE не продлевает TTL.
|
||
|
||
A предлагает собственный файл с holder=A и expires=0. S выбирает M с storage=yes
|
||
в общей группе A/B, исключая A/B и все прежние назначения этого UUID. Назначение
|
||
фиксируется до STORE; S/M могут быть одним узлом, local control обрабатывается синхронно.
|
||
Успешная передача control в router или локальный обработчик не означает durable успех.
|
||
|
||
EXPIRED относится к промежуточной копии. B помечает ещё не готовый файл expired,
|
||
но продолжает попытки получить оригинал у A. После проверки исходного ciphertext,
|
||
публикации и сохранения квитанции файл снова ready; поздний EXPIRED его не меняет.
|
||
Готовый файл A/B остаётся в локальной истории до явного удаления беседы.
|
||
При удалении busy-счётчик каждого пути удерживает его до callback worker/transfer;
|
||
уборщик работает по старым поколениям, использует busy=-1 для своего захвата
|
||
и синхронизирует каталог после удаления. Пользовательский исходник не входит в уборку,
|
||
если он не был явно передан как временный файл приложения.
|
||
|
||
Qt и Android получают историю через события ядра: 41 —
|
||
`[conv_len:1][conv:decimal][generation:8][JSON сообщений]`, 45 — JSON бесед.
|
||
Запрос истории с ненулевым generation читается только для текущей видимой беседы;
|
||
0 захватывает текущее поколение в uasync. UI отбрасывает снимок другого поколения.
|
||
Снимки ограничены 200 записями. Отправка вложения вызывает
|
||
`dm_send_file` в uasync; длительная обработка выполняется в `media_async`.
|
||
Android копирует выбранный URI и сохраняет полученный файл через `Dispatchers.IO`;
|
||
Qt сохраняет файл из контекстного меню в отдельном потоке. Выбор «как файл» сохраняет
|
||
оригинал; «как видео» в Qt использует общий FFmpeg worker и метаданные конечного MP4.
|
||
Android записывает MP4 через CameraX; импорт MP4 проходит через платформенный
|
||
MediaMetadataRetriever в IO. Другие контейнеры на Android отправляются как файлы.
|
||
|
||
Голосовая запись отдаёт worker собственный PCM либо компрессор без копирования
|
||
в GUI/uasync; worker выполняет flush, Opus-кодирование и вычисление waveform.
|
||
Кодирование Qt и Android использует один `voice_file`. Адресат фиксируется до
|
||
записи, выбора URI и подготовки; завершение не читает текущую беседу GUI.
|
||
Подготовленный PM-файл и Android cache удаляются после копирования, ошибки или отмены;
|
||
пользовательский исходник Qt остаётся во владении пользователя.
|
||
|
||
Событие 46 `[target_len:1][target][job_UUID:16][state:1]` сообщает начало (0),
|
||
готовность (1) и ошибку (2). Target канала — числовой ID, PM — `dm:<conv_id>`.
|
||
Подготовка, доставка и воспроизведение имеют независимые состояния. UI отображает
|
||
только задания текущей беседы. Состояния файла и delivery receipt приходят в JSON
|
||
41; `content_type`, duration, dimensions и waveform преобразуются в общие роли
|
||
сообщения. Канальные скачивания вызываются контроллером только для каналов;
|
||
PM-файлы доставляются автоматически через `dm_media`.
|
||
|
||
Android использует общие `ConversationComposer`/`ConversationMessages` для обеих
|
||
бесед; Qt — `InputBar` и `MessageDelegate`. Превью Qt создаются в общем пуле
|
||
с двумя workers; декодирование длинного Opus-файла выполняется вне GUI. Оба плеера
|
||
проверяют заголовок, пакеты и end marker; Qt ограничивает PCM буфер 256 МиБ.
|
||
|
||
## Проверки
|
||
|
||
Аудит контрактов DM завершён 27.10.2026: все семь заголовков `src/dm/`, этот документ,
|
||
`pm.txt` и `pm_invites.md` сверены с реализациями owners и относящимися к ним callers.
|
||
Это проверка соответствия документации; свежая интерактивная проверка клиентов и
|
||
новые платформенные сборки не выполнялись. Подробности и границы проверки —
|
||
[dm_audit_findings.txt](dm_audit_findings.txt), раздел 12.
|
||
Первоначальная проверка отказа SQLite выявила сброс счётчиков; дефект исправлен.
|
||
`test_dm_load` проверяет 79 отказных сценариев: чтение, bind/step/finalize, короткие
|
||
ключи и X25519; открытие/приём/отправку/JSON/invite до и после удаления; отдельные
|
||
отказы подготовки файла, отсутствие беседы, restart и продолжение seq. При отказе
|
||
сравниваются все поля DM-таблиц и отсутствие SQL-записей/событий успеха.
|
||
|
||
`test_dm_names`: 68 случаев с точным сравнением имени в SQL и JSON при создании,
|
||
загрузке, штатной записи, fallback из nodes и перезапуске. ASCII и 2/3/4-байтовые
|
||
символы проверяются до/на/за границей 63 байт; отдельно — overlong, surrogate,
|
||
значения выше U+10FFFF, неверные/оборванные последовательности, NUL и JSON-экранирование.
|
||
Снимки не меняют SQL; 273 полных JSON сохраняются для независимого строгого парсера.
|
||
|
||
`test_dm`: симметрия ключа target, CCM round-trip, пустое тело, пределы длины,
|
||
неверный ключ/tag, разделение nonce; потоковые файлы на границах 32 КиБ,
|
||
неверные author/UUID/hash, обрезание и лишние байты.
|
||
|
||
`test_dm_e2e`: реальные A–C–B соединения и BGP; нет физического A–B линка.
|
||
Проверяет out-of-order, dedup/conflict, подписи, SQL rollback при приёме и enqueue,
|
||
ACK чужого тела, отказ mailbox без роли, точное удаление и поздний PUT.
|
||
Первый сценарий передаёт 40 offline-сообщений (больше порции pump) и перезапускает источник с pending outbox; второй перезапускает
|
||
суперузел и доставляет первое сообщение при выключенном источнике.
|
||
|
||
`test_dm_media`: реальные A/B/S/M с раздельными ролями supernode и storage;
|
||
прямая передача, custody, restart S/M при выключенном A, точный файл на B,
|
||
удаление копии и тела S, поздние STORE/STORED, TTL, отказ по квоте, неверная квитанция,
|
||
SQL rollback регистрации, состояния JSON и отмена операции с CM handle.
|
||
Голосовое передаётся напрямую; метаданные видео сохраняются после offline custody
|
||
и restart S/M при выключенном источнике.
|
||
В Linux дополнительно проверяются prepare/step/finalize и неожиданный SQLITE_DONE
|
||
при подсчёте места, отсутствие REJECT/STORED и SQL-записей при ошибке; три сбоя
|
||
в первых трёх STORE, успешный четвёртый STORE на том же M и последующая доставка
|
||
после restart. Проверка настоящей квоты требует один REJECT до регистрации custody.
|
||
Дополнительно — 18 отказов освобождения EXPIRED/REJECT: prepare/bind/step/finalize
|
||
обоих UPDATE и commit, точный rollback назначения/job, успешный повтор, дубликат
|
||
и поздний STORED. Сетевой сценарий теряет EXPIRED, блокирует SQL-сброс, перезапускает S
|
||
и проверяет восстановление из сохранённого expires, состояние M и отсутствие ложной квитанции.
|
||
На M проверяются 11 SQL-отказов retirement: rollback сохраняет custody и не выдаёт ACK;
|
||
после restart M/S потерянный DELETE_ACK восстанавливается по tombstone и повтору DELETE.
|
||
После EXPIRED и возврата A проверяются точный оригинал на B, настоящая файловая
|
||
квитанция, очистка S и невлияние позднего EXPIRED на готовый файл.
|
||
|
||
`test_dm_worker`: повторное шифрование 8 МиБ не менее 0,5 с и проверка расшифровки;
|
||
work выполняется вне uasync, done в его потоке, heartbeat продолжает работать.
|
||
Destroy отменяет уже posted completion, callback вызывается ровно один раз.
|
||
|
||
Linux `test_dm_media_cleanup`: 13 отказов SQL при чтении назначений, записи ACK
|
||
и очистке body; сохранность квитанции, повтор, restart S, потерянный/повторный/чужой ACK,
|
||
несколько M и поздний STORED. Сеть в этом component-тесте перехвачена; подпись B и
|
||
реальное удаление M проверяет `test_dm_media`. Ошибка finalize после успешного
|
||
autocommit не означает rollback уже выполненной одношаговой записи.
|
||
|
||
`test_attachment`: строгий формат, повреждения и граничные длины, реальное
|
||
кодирование/декодирование 50 Opus-кадров, длительность и waveform.
|
||
`test_attachment_send`: выбранная беседа после смены текущего target, освобождение
|
||
PCM в worker, остановка до completion без регистрации и удаление отклонённого cache.
|
||
Qt `test_voice_file`: реальные PCM/Opus, повреждённые файлы и отмена async decoder.
|
||
|
||
Заключительный прогон 27.10.2026: штатные `test_dm`, `test_dm_history`, `test_dm_names`,
|
||
`test_dm_load`, `test_dm_e2e`, `test_dm_invite`, `test_dm_media`, `test_dm_worker`
|
||
и Linux `test_dm_media_cleanup` — 9/9 PASS.
|
||
Отдельный строгий JSON-парсер прочитал все 273 снимка имён. Заголовки проверены в C99/C++20;
|
||
в заключительном этапе менялись только документация, комментарии и форматирование деклараций.
|
||
Результаты старых сборок ниже относятся к указанной дате, а не к этому прогону.
|
||
|
||
Проверка 01.10.2026: чистые сборки Qt и Android APK; полный `check.sh` —
|
||
117 PASS, 0 FAIL, 1 SKIP (auto_socket_dynamic требует root), proxy/burst/load PASS.
|
||
Qt CTest: 7 PASS, 0 FAIL; Android APK собран после `clean`.
|
||
В тесте worker максимальный интервал heartbeat составил 10,3 мс.
|
||
Интерактивная проверка GUI на телефоне и десктопе не выполнялась.
|