thth 2.13.0__tar.gz → 2.13.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (456) hide show
  1. {thth-2.13.0 → thth-2.13.2}/PKG-INFO +1 -1
  2. thth-2.13.2/tests/test_v2131_send_dry_run.py +47 -0
  3. thth-2.13.2/tests/test_v2131_threads_publish_wait.py +84 -0
  4. thth-2.13.2/tests/test_v2132_send_inflight_hint.py +67 -0
  5. thth-2.13.2/tests/test_v2132_threads_create_timeout.py +84 -0
  6. thth-2.13.2/tests/test_v2132_threads_create_unknown.py +90 -0
  7. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_bluesky_webm_named.py +2 -1
  8. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_media.py +18 -0
  9. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_video.py +31 -2
  10. thth-2.13.2/thth/VERSION +1 -0
  11. {thth-2.13.0 → thth-2.13.2}/thth/adapters/threads_media.py +97 -11
  12. {thth-2.13.0 → thth-2.13.2}/thth/cli.py +32 -4
  13. {thth-2.13.0 → thth-2.13.2}/thth/core.py +23 -1
  14. {thth-2.13.0 → thth-2.13.2}/thth/inflight.py +10 -0
  15. {thth-2.13.0 → thth-2.13.2}/thth/media.py +2 -2
  16. {thth-2.13.0 → thth-2.13.2}/thth/media_delivery.py +66 -5
  17. {thth-2.13.0 → thth-2.13.2}/thth/mediaformats.py +19 -10
  18. thth-2.13.0/thth/VERSION +0 -1
  19. {thth-2.13.0 → thth-2.13.2}/.gitignore +0 -0
  20. {thth-2.13.0 → thth-2.13.2}/LICENSE +0 -0
  21. {thth-2.13.0 → thth-2.13.2}/README.md +0 -0
  22. {thth-2.13.0 → thth-2.13.2}/accounts.example/bluesky.json +0 -0
  23. {thth-2.13.0 → thth-2.13.2}/accounts.example/mastodon.json +0 -0
  24. {thth-2.13.0 → thth-2.13.2}/accounts.example/threads.json +0 -0
  25. {thth-2.13.0 → thth-2.13.2}/accounts.example/x.json +0 -0
  26. {thth-2.13.0 → thth-2.13.2}/bin/thth +0 -0
  27. {thth-2.13.0 → thth-2.13.2}/bin/thth-remote +0 -0
  28. {thth-2.13.0 → thth-2.13.2}/bin/thth-run +0 -0
  29. {thth-2.13.0 → thth-2.13.2}/mcp/server.py +0 -0
  30. {thth-2.13.0 → thth-2.13.2}/pyproject.toml +0 -0
  31. {thth-2.13.0 → thth-2.13.2}/skills/thth/SKILL.md +0 -0
  32. {thth-2.13.0 → thth-2.13.2}/skills/thth-admin/SKILL.md +0 -0
  33. {thth-2.13.0 → thth-2.13.2}/systemd/thth@.service +0 -0
  34. {thth-2.13.0 → thth-2.13.2}/tests/__init__.py +0 -0
  35. {thth-2.13.0 → thth-2.13.2}/tests/conftest.py +0 -0
  36. {thth-2.13.0 → thth-2.13.2}/tests/coordination_snapshot.py +0 -0
  37. {thth-2.13.0 → thth-2.13.2}/tests/fixtures/GraphemeBreakTest-17.0.0.txt +0 -0
  38. {thth-2.13.0 → thth-2.13.2}/tests/fixtures/build_injection_eval.py +0 -0
  39. {thth-2.13.0 → thth-2.13.2}/tests/fixtures/callback/worker_responses.json +0 -0
  40. {thth-2.13.0 → thth-2.13.2}/tests/fixtures/injection_eval.json +0 -0
  41. {thth-2.13.0 → thth-2.13.2}/tests/fixtures/umami-bile.md +0 -0
  42. {thth-2.13.0 → thth-2.13.2}/tests/helpers/__init__.py +0 -0
  43. {thth-2.13.0 → thth-2.13.2}/tests/helpers/fake_oauth_server.py +0 -0
  44. {thth-2.13.0 → thth-2.13.2}/tests/helpers/fake_push_adapter.py +0 -0
  45. {thth-2.13.0 → thth-2.13.2}/tests/helpers/fake_redirect_server.py +0 -0
  46. {thth-2.13.0 → thth-2.13.2}/tests/helpers/fake_search_adapter.py +0 -0
  47. {thth-2.13.0 → thth-2.13.2}/tests/helpers/fake_threads_reader.py +0 -0
  48. {thth-2.13.0 → thth-2.13.2}/tests/helpers/fake_threads_writes.py +0 -0
  49. {thth-2.13.0 → thth-2.13.2}/tests/helpers/hold_lock.py +0 -0
  50. {thth-2.13.0 → thth-2.13.2}/tests/helpers/hold_repo_lock.py +0 -0
  51. {thth-2.13.0 → thth-2.13.2}/tests/helpers/worker_probe.mjs +0 -0
  52. {thth-2.13.0 → thth-2.13.2}/tests/test_27_acceptance_fixes.py +0 -0
  53. {thth-2.13.0 → thth-2.13.2}/tests/test_account_name_check.py +0 -0
  54. {thth-2.13.0 → thth-2.13.2}/tests/test_account_report.py +0 -0
  55. {thth-2.13.0 → thth-2.13.2}/tests/test_accounts_dir.py +0 -0
  56. {thth-2.13.0 → thth-2.13.2}/tests/test_adapter_boundary.py +0 -0
  57. {thth-2.13.0 → thth-2.13.2}/tests/test_admin_http_mcp.py +0 -0
  58. {thth-2.13.0 → thth-2.13.2}/tests/test_admin_log.py +0 -0
  59. {thth-2.13.0 → thth-2.13.2}/tests/test_admin_notifications.py +0 -0
  60. {thth-2.13.0 → thth-2.13.2}/tests/test_admin_repairs.py +0 -0
  61. {thth-2.13.0 → thth-2.13.2}/tests/test_admin_report.py +0 -0
  62. {thth-2.13.0 → thth-2.13.2}/tests/test_after_cli.py +0 -0
  63. {thth-2.13.0 → thth-2.13.2}/tests/test_analytics_comparison.py +0 -0
  64. {thth-2.13.0 → thth-2.13.2}/tests/test_analytics_report.py +0 -0
  65. {thth-2.13.0 → thth-2.13.2}/tests/test_api_diagnostic.py +0 -0
  66. {thth-2.13.0 → thth-2.13.2}/tests/test_api_error_body.py +0 -0
  67. {thth-2.13.0 → thth-2.13.2}/tests/test_app_env_cli.py +0 -0
  68. {thth-2.13.0 → thth-2.13.2}/tests/test_approval.py +0 -0
  69. {thth-2.13.0 → thth-2.13.2}/tests/test_approve_cmd.py +0 -0
  70. {thth-2.13.0 → thth-2.13.2}/tests/test_ask.py +0 -0
  71. {thth-2.13.0 → thth-2.13.2}/tests/test_atlas_fifth_review.py +0 -0
  72. {thth-2.13.0 → thth-2.13.2}/tests/test_atlas_fourth_review.py +0 -0
  73. {thth-2.13.0 → thth-2.13.2}/tests/test_atlas_review.py +0 -0
  74. {thth-2.13.0 → thth-2.13.2}/tests/test_atlas_second_review.py +0 -0
  75. {thth-2.13.0 → thth-2.13.2}/tests/test_atlas_third_review.py +0 -0
  76. {thth-2.13.0 → thth-2.13.2}/tests/test_audit1_fixes.py +0 -0
  77. {thth-2.13.0 → thth-2.13.2}/tests/test_audit2_fixes.py +0 -0
  78. {thth-2.13.0 → thth-2.13.2}/tests/test_audit_b_counterexamples.py +0 -0
  79. {thth-2.13.0 → thth-2.13.2}/tests/test_audit_b_m1_semantics.py +0 -0
  80. {thth-2.13.0 → thth-2.13.2}/tests/test_audit_b_n1_depth.py +0 -0
  81. {thth-2.13.0 → thth-2.13.2}/tests/test_audit_b_r1_closure.py +0 -0
  82. {thth-2.13.0 → thth-2.13.2}/tests/test_audit_v201.py +0 -0
  83. {thth-2.13.0 → thth-2.13.2}/tests/test_audit_v23_details.py +0 -0
  84. {thth-2.13.0 → thth-2.13.2}/tests/test_auth.py +0 -0
  85. {thth-2.13.0 → thth-2.13.2}/tests/test_auth_identity_required.py +0 -0
  86. {thth-2.13.0 → thth-2.13.2}/tests/test_behind_remote.py +0 -0
  87. {thth-2.13.0 → thth-2.13.2}/tests/test_bluesky_adapter.py +0 -0
  88. {thth-2.13.0 → thth-2.13.2}/tests/test_bluesky_secret_scrub.py +0 -0
  89. {thth-2.13.0 → thth-2.13.2}/tests/test_board_needs_review.py +0 -0
  90. {thth-2.13.0 → thth-2.13.2}/tests/test_board_running.py +0 -0
  91. {thth-2.13.0 → thth-2.13.2}/tests/test_bundle.py +0 -0
  92. {thth-2.13.0 → thth-2.13.2}/tests/test_charcount.py +0 -0
  93. {thth-2.13.0 → thth-2.13.2}/tests/test_cli_entrypoint.py +0 -0
  94. {thth-2.13.0 → thth-2.13.2}/tests/test_cli_help.py +0 -0
  95. {thth-2.13.0 → thth-2.13.2}/tests/test_cli_vm_path.py +0 -0
  96. {thth-2.13.0 → thth-2.13.2}/tests/test_collect.py +0 -0
  97. {thth-2.13.0 → thth-2.13.2}/tests/test_collect_engagements.py +0 -0
  98. {thth-2.13.0 → thth-2.13.2}/tests/test_collect_runs.py +0 -0
  99. {thth-2.13.0 → thth-2.13.2}/tests/test_collect_sent.py +0 -0
  100. {thth-2.13.0 → thth-2.13.2}/tests/test_comparability.py +0 -0
  101. {thth-2.13.0 → thth-2.13.2}/tests/test_control_chars_refused.py +0 -0
  102. {thth-2.13.0 → thth-2.13.2}/tests/test_conversation_paging.py +0 -0
  103. {thth-2.13.0 → thth-2.13.2}/tests/test_count_for.py +0 -0
  104. {thth-2.13.0 → thth-2.13.2}/tests/test_docs_links.py +0 -0
  105. {thth-2.13.0 → thth-2.13.2}/tests/test_doctor.py +0 -0
  106. {thth-2.13.0 → thth-2.13.2}/tests/test_doctor_all_permissions.py +0 -0
  107. {thth-2.13.0 → thth-2.13.2}/tests/test_doctor_next_step.py +0 -0
  108. {thth-2.13.0 → thth-2.13.2}/tests/test_duplicate_front_matter_keys.py +0 -0
  109. {thth-2.13.0 → thth-2.13.2}/tests/test_editorial_audit.py +0 -0
  110. {thth-2.13.0 → thth-2.13.2}/tests/test_editorial_n3_n4.py +0 -0
  111. {thth-2.13.0 → thth-2.13.2}/tests/test_engagements.py +0 -0
  112. {thth-2.13.0 → thth-2.13.2}/tests/test_external_acceptance.py +0 -0
  113. {thth-2.13.0 → thth-2.13.2}/tests/test_fake_api.py +0 -0
  114. {thth-2.13.0 → thth-2.13.2}/tests/test_form_spec.py +0 -0
  115. {thth-2.13.0 → thth-2.13.2}/tests/test_form_spec_cli.py +0 -0
  116. {thth-2.13.0 → thth-2.13.2}/tests/test_forms.py +0 -0
  117. {thth-2.13.0 → thth-2.13.2}/tests/test_fresh_install.py +0 -0
  118. {thth-2.13.0 → thth-2.13.2}/tests/test_front_matter_injection.py +0 -0
  119. {thth-2.13.0 → thth-2.13.2}/tests/test_healthcheck_notifications.py +0 -0
  120. {thth-2.13.0 → thth-2.13.2}/tests/test_hypotheses.py +0 -0
  121. {thth-2.13.0 → thth-2.13.2}/tests/test_hypothesis_boundaries.py +0 -0
  122. {thth-2.13.0 → thth-2.13.2}/tests/test_hypothesis_units.py +0 -0
  123. {thth-2.13.0 → thth-2.13.2}/tests/test_impact_cli.py +0 -0
  124. {thth-2.13.0 → thth-2.13.2}/tests/test_inbox_granted_scopes.py +0 -0
  125. {thth-2.13.0 → thth-2.13.2}/tests/test_incident_notifications.py +0 -0
  126. {thth-2.13.0 → thth-2.13.2}/tests/test_independent_thread_review.py +0 -0
  127. {thth-2.13.0 → thth-2.13.2}/tests/test_independent_topic_review.py +0 -0
  128. {thth-2.13.0 → thth-2.13.2}/tests/test_independent_topic_review_r2.py +0 -0
  129. {thth-2.13.0 → thth-2.13.2}/tests/test_inflight.py +0 -0
  130. {thth-2.13.0 → thth-2.13.2}/tests/test_inflight_tag_body_hash.py +0 -0
  131. {thth-2.13.0 → thth-2.13.2}/tests/test_jst.py +0 -0
  132. {thth-2.13.0 → thth-2.13.2}/tests/test_lint.py +0 -0
  133. {thth-2.13.0 → thth-2.13.2}/tests/test_list_projections.py +0 -0
  134. {thth-2.13.0 → thth-2.13.2}/tests/test_llm_trial.py +0 -0
  135. {thth-2.13.0 → thth-2.13.2}/tests/test_llm_trial_engage.py +0 -0
  136. {thth-2.13.0 → thth-2.13.2}/tests/test_lock.py +0 -0
  137. {thth-2.13.0 → thth-2.13.2}/tests/test_m2_independent.py +0 -0
  138. {thth-2.13.0 → thth-2.13.2}/tests/test_maintain.py +0 -0
  139. {thth-2.13.0 → thth-2.13.2}/tests/test_mastodon_adapter.py +0 -0
  140. {thth-2.13.0 → thth-2.13.2}/tests/test_mastodon_visibility.py +0 -0
  141. {thth-2.13.0 → thth-2.13.2}/tests/test_max_per_run.py +0 -0
  142. {thth-2.13.0 → thth-2.13.2}/tests/test_mcp.py +0 -0
  143. {thth-2.13.0 → thth-2.13.2}/tests/test_mcp_robustness.py +0 -0
  144. {thth-2.13.0 → thth-2.13.2}/tests/test_measured.py +0 -0
  145. {thth-2.13.0 → thth-2.13.2}/tests/test_media_accounts.py +0 -0
  146. {thth-2.13.0 → thth-2.13.2}/tests/test_media_next_steps.py +0 -0
  147. {thth-2.13.0 → thth-2.13.2}/tests/test_media_roundtrip.py +0 -0
  148. {thth-2.13.0 → thth-2.13.2}/tests/test_mismatch_diagnostics.py +0 -0
  149. {thth-2.13.0 → thth-2.13.2}/tests/test_n5_reply_null_element.py +0 -0
  150. {thth-2.13.0 → thth-2.13.2}/tests/test_n6_replies_incomplete_ledger.py +0 -0
  151. {thth-2.13.0 → thth-2.13.2}/tests/test_n7_doctor_error_body.py +0 -0
  152. {thth-2.13.0 → thth-2.13.2}/tests/test_no_limits.py +0 -0
  153. {thth-2.13.0 → thth-2.13.2}/tests/test_observation_provenance.py +0 -0
  154. {thth-2.13.0 → thth-2.13.2}/tests/test_operations_handoff.py +0 -0
  155. {thth-2.13.0 → thth-2.13.2}/tests/test_packaging.py +0 -0
  156. {thth-2.13.0 → thth-2.13.2}/tests/test_preview.py +0 -0
  157. {thth-2.13.0 → thth-2.13.2}/tests/test_production_mode.py +0 -0
  158. {thth-2.13.0 → thth-2.13.2}/tests/test_pull_cli.py +0 -0
  159. {thth-2.13.0 → thth-2.13.2}/tests/test_queue.py +0 -0
  160. {thth-2.13.0 → thth-2.13.2}/tests/test_quiet_hours.py +0 -0
  161. {thth-2.13.0 → thth-2.13.2}/tests/test_r2_r4_r5.py +0 -0
  162. {thth-2.13.0 → thth-2.13.2}/tests/test_read_cli_bare_runtimeerror.py +0 -0
  163. {thth-2.13.0 → thth-2.13.2}/tests/test_reason_adoption.py +0 -0
  164. {thth-2.13.0 → thth-2.13.2}/tests/test_redact.py +0 -0
  165. {thth-2.13.0 → thth-2.13.2}/tests/test_redirect_token_leak.py +0 -0
  166. {thth-2.13.0 → thth-2.13.2}/tests/test_refresh.py +0 -0
  167. {thth-2.13.0 → thth-2.13.2}/tests/test_refresh_destination.py +0 -0
  168. {thth-2.13.0 → thth-2.13.2}/tests/test_rejudge_new.py +0 -0
  169. {thth-2.13.0 → thth-2.13.2}/tests/test_rejudge_r2_r3.py +0 -0
  170. {thth-2.13.0 → thth-2.13.2}/tests/test_replies.py +0 -0
  171. {thth-2.13.0 → thth-2.13.2}/tests/test_replies_refresh.py +0 -0
  172. {thth-2.13.0 → thth-2.13.2}/tests/test_repo_lock.py +0 -0
  173. {thth-2.13.0 → thth-2.13.2}/tests/test_repo_sync.py +0 -0
  174. {thth-2.13.0 → thth-2.13.2}/tests/test_report_checkout_import.py +0 -0
  175. {thth-2.13.0 → thth-2.13.2}/tests/test_report_http.py +0 -0
  176. {thth-2.13.0 → thth-2.13.2}/tests/test_report_isolation.py +0 -0
  177. {thth-2.13.0 → thth-2.13.2}/tests/test_report_service.py +0 -0
  178. {thth-2.13.0 → thth-2.13.2}/tests/test_reverse_audit.py +0 -0
  179. {thth-2.13.0 → thth-2.13.2}/tests/test_review_case_id_cluster_m2.py +0 -0
  180. {thth-2.13.0 → thth-2.13.2}/tests/test_review_case_id_cluster_r2.py +0 -0
  181. {thth-2.13.0 → thth-2.13.2}/tests/test_review_cli.py +0 -0
  182. {thth-2.13.0 → thth-2.13.2}/tests/test_review_evidence_account.py +0 -0
  183. {thth-2.13.0 → thth-2.13.2}/tests/test_review_form_spec_and_case_id.py +0 -0
  184. {thth-2.13.0 → thth-2.13.2}/tests/test_review_provenance.py +0 -0
  185. {thth-2.13.0 → thth-2.13.2}/tests/test_review_records.py +0 -0
  186. {thth-2.13.0 → thth-2.13.2}/tests/test_revoke.py +0 -0
  187. {thth-2.13.0 → thth-2.13.2}/tests/test_roundtrip.py +0 -0
  188. {thth-2.13.0 → thth-2.13.2}/tests/test_run_missing_env.py +0 -0
  189. {thth-2.13.0 → thth-2.13.2}/tests/test_run_record_failclosed.py +0 -0
  190. {thth-2.13.0 → thth-2.13.2}/tests/test_runs_collected.py +0 -0
  191. {thth-2.13.0 → thth-2.13.2}/tests/test_runs_read_rows.py +0 -0
  192. {thth-2.13.0 → thth-2.13.2}/tests/test_schedule_multiple.py +0 -0
  193. {thth-2.13.0 → thth-2.13.2}/tests/test_schedule_view.py +0 -0
  194. {thth-2.13.0 → thth-2.13.2}/tests/test_scopes_source_requested.py +0 -0
  195. {thth-2.13.0 → thth-2.13.2}/tests/test_search_engage_targets.py +0 -0
  196. {thth-2.13.0 → thth-2.13.2}/tests/test_search_media_routes.py +0 -0
  197. {thth-2.13.0 → thth-2.13.2}/tests/test_secret_value_redact.py +0 -0
  198. {thth-2.13.0 → thth-2.13.2}/tests/test_secrets_grep.py +0 -0
  199. {thth-2.13.0 → thth-2.13.2}/tests/test_security_small_fixes.py +0 -0
  200. {thth-2.13.0 → thth-2.13.2}/tests/test_select.py +0 -0
  201. {thth-2.13.0 → thth-2.13.2}/tests/test_selfupdate.py +0 -0
  202. {thth-2.13.0 → thth-2.13.2}/tests/test_send.py +0 -0
  203. {thth-2.13.0 → thth-2.13.2}/tests/test_send_cli.py +0 -0
  204. {thth-2.13.0 → thth-2.13.2}/tests/test_send_control_chars.py +0 -0
  205. {thth-2.13.0 → thth-2.13.2}/tests/test_send_media.py +0 -0
  206. {thth-2.13.0 → thth-2.13.2}/tests/test_send_visibility.py +0 -0
  207. {thth-2.13.0 → thth-2.13.2}/tests/test_sent_integrity.py +0 -0
  208. {thth-2.13.0 → thth-2.13.2}/tests/test_server_json.py +0 -0
  209. {thth-2.13.0 → thth-2.13.2}/tests/test_site.py +0 -0
  210. {thth-2.13.0 → thth-2.13.2}/tests/test_skill_doc.py +0 -0
  211. {thth-2.13.0 → thth-2.13.2}/tests/test_study_report.py +0 -0
  212. {thth-2.13.0 → thth-2.13.2}/tests/test_systemd.py +0 -0
  213. {thth-2.13.0 → thth-2.13.2}/tests/test_thread_publish.py +0 -0
  214. {thth-2.13.0 → thth-2.13.2}/tests/test_thread_read.py +0 -0
  215. {thth-2.13.0 → thth-2.13.2}/tests/test_threadrun.py +0 -0
  216. {thth-2.13.0 → thth-2.13.2}/tests/test_threads_read_permissions.py +0 -0
  217. {thth-2.13.0 → thth-2.13.2}/tests/test_threadshape.py +0 -0
  218. {thth-2.13.0 → thth-2.13.2}/tests/test_thth_root.py +0 -0
  219. {thth-2.13.0 → thth-2.13.2}/tests/test_token_exists.py +0 -0
  220. {thth-2.13.0 → thth-2.13.2}/tests/test_token_scopes_record.py +0 -0
  221. {thth-2.13.0 → thth-2.13.2}/tests/test_token_set.py +0 -0
  222. {thth-2.13.0 → thth-2.13.2}/tests/test_topic.py +0 -0
  223. {thth-2.13.0 → thth-2.13.2}/tests/test_topic_advice.py +0 -0
  224. {thth-2.13.0 → thth-2.13.2}/tests/test_topic_cli.py +0 -0
  225. {thth-2.13.0 → thth-2.13.2}/tests/test_topic_models.py +0 -0
  226. {thth-2.13.0 → thth-2.13.2}/tests/test_topic_review_fixes.py +0 -0
  227. {thth-2.13.0 → thth-2.13.2}/tests/test_topic_shelf_observers.py +0 -0
  228. {thth-2.13.0 → thth-2.13.2}/tests/test_topic_store.py +0 -0
  229. {thth-2.13.0 → thth-2.13.2}/tests/test_topics_record_concurrent.py +0 -0
  230. {thth-2.13.0 → thth-2.13.2}/tests/test_topics_tool.py +0 -0
  231. {thth-2.13.0 → thth-2.13.2}/tests/test_trial_frictions.py +0 -0
  232. {thth-2.13.0 → thth-2.13.2}/tests/test_v210_admin_transaction.py +0 -0
  233. {thth-2.13.0 → thth-2.13.2}/tests/test_v210_approve_account.py +0 -0
  234. {thth-2.13.0 → thth-2.13.2}/tests/test_v210_json_contract.py +0 -0
  235. {thth-2.13.0 → thth-2.13.2}/tests/test_v210_length.py +0 -0
  236. {thth-2.13.0 → thth-2.13.2}/tests/test_v210_lock_wait.py +0 -0
  237. {thth-2.13.0 → thth-2.13.2}/tests/test_v210_mastodon_scopes.py +0 -0
  238. {thth-2.13.0 → thth-2.13.2}/tests/test_v210_mentions.py +0 -0
  239. {thth-2.13.0 → thth-2.13.2}/tests/test_v210_post_urls.py +0 -0
  240. {thth-2.13.0 → thth-2.13.2}/tests/test_v210_repo_paths.py +0 -0
  241. {thth-2.13.0 → thth-2.13.2}/tests/test_v210_reposts.py +0 -0
  242. {thth-2.13.0 → thth-2.13.2}/tests/test_v210_study_add.py +0 -0
  243. {thth-2.13.0 → thth-2.13.2}/tests/test_v210_tool_version.py +0 -0
  244. {thth-2.13.0 → thth-2.13.2}/tests/test_v210_unanswered.py +0 -0
  245. {thth-2.13.0 → thth-2.13.2}/tests/test_v210_unanswered_scope.py +0 -0
  246. {thth-2.13.0 → thth-2.13.2}/tests/test_v210_where_arguments.py +0 -0
  247. {thth-2.13.0 → thth-2.13.2}/tests/test_v210_where_filters.py +0 -0
  248. {thth-2.13.0 → thth-2.13.2}/tests/test_v210_who_arguments.py +0 -0
  249. {thth-2.13.0 → thth-2.13.2}/tests/test_v211_auth_integration.py +0 -0
  250. {thth-2.13.0 → thth-2.13.2}/tests/test_v211_authflow.py +0 -0
  251. {thth-2.13.0 → thth-2.13.2}/tests/test_v211_bluesky_stdin.py +0 -0
  252. {thth-2.13.0 → thth-2.13.2}/tests/test_v211_doctor_binding.py +0 -0
  253. {thth-2.13.0 → thth-2.13.2}/tests/test_v211_mastodon_auth.py +0 -0
  254. {thth-2.13.0 → thth-2.13.2}/tests/test_v211_x_auth.py +0 -0
  255. {thth-2.13.0 → thth-2.13.2}/tests/test_v212_approval_relay.py +0 -0
  256. {thth-2.13.0 → thth-2.13.2}/tests/test_v212_ask_coordination.py +0 -0
  257. {thth-2.13.0 → thth-2.13.2}/tests/test_v212_budget.py +0 -0
  258. {thth-2.13.0 → thth-2.13.2}/tests/test_v212_budget_auth.py +0 -0
  259. {thth-2.13.0 → thth-2.13.2}/tests/test_v212_budget_mcp.py +0 -0
  260. {thth-2.13.0 → thth-2.13.2}/tests/test_v212_deletion.py +0 -0
  261. {thth-2.13.0 → thth-2.13.2}/tests/test_v212_job_completion_fault.py +0 -0
  262. {thth-2.13.0 → thth-2.13.2}/tests/test_v212_leave.py +0 -0
  263. {thth-2.13.0 → thth-2.13.2}/tests/test_v212_leave_gate.py +0 -0
  264. {thth-2.13.0 → thth-2.13.2}/tests/test_v212_leave_providers.py +0 -0
  265. {thth-2.13.0 → thth-2.13.2}/tests/test_v212_leave_token_safety.py +0 -0
  266. {thth-2.13.0 → thth-2.13.2}/tests/test_v212_mastodon_guide.py +0 -0
  267. {thth-2.13.0 → thth-2.13.2}/tests/test_v212_mcp_compat.py +0 -0
  268. {thth-2.13.0 → thth-2.13.2}/tests/test_v212_missing_token.py +0 -0
  269. {thth-2.13.0 → thth-2.13.2}/tests/test_v212_read_coordination.py +0 -0
  270. {thth-2.13.0 → thth-2.13.2}/tests/test_v212_server_writes.py +0 -0
  271. {thth-2.13.0 → thth-2.13.2}/tests/test_v212_token_parent.py +0 -0
  272. {thth-2.13.0 → thth-2.13.2}/tests/test_v212_write_storage.py +0 -0
  273. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_acceptance_fixes.py +0 -0
  274. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_adts_asf.py +0 -0
  275. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_approval_attachments.py +0 -0
  276. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_attachment_kinds.py +0 -0
  277. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_bluesky_external.py +0 -0
  278. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_bluesky_media.py +0 -0
  279. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_bluesky_metadata.py +0 -0
  280. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_bluesky_metadata_uri.py +0 -0
  281. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_bluesky_metadata_video.py +0 -0
  282. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_bluesky_record.py +0 -0
  283. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_bluesky_record_video.py +0 -0
  284. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_bluesky_video.py +0 -0
  285. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_bmff_typed_metadata.py +0 -0
  286. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_budget_lock.py +0 -0
  287. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_capabilities.py +0 -0
  288. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_carryover_timeout.py +0 -0
  289. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_graphemes.py +0 -0
  290. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_auth_media.py +0 -0
  291. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_bmff_audio.py +0 -0
  292. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_flac.py +0 -0
  293. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_gif.py +0 -0
  294. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_link.py +0 -0
  295. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_mp3.py +0 -0
  296. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_ogg_opus.py +0 -0
  297. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_ogg_vorbis.py +0 -0
  298. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_poll.py +0 -0
  299. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_primary_size_boundary.py +0 -0
  300. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_quote.py +0 -0
  301. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_thumbnail.py +0 -0
  302. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_wav.py +0 -0
  303. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_webm_audio.py +0 -0
  304. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_webm_video.py +0 -0
  305. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_media_cleanup.py +0 -0
  306. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_media_formats.py +0 -0
  307. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_media_foundation.py +0 -0
  308. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_media_prepared.py +0 -0
  309. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_media_relay.py +0 -0
  310. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_media_relay_refusals.py +0 -0
  311. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_media_upload.py +0 -0
  312. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_receipt_deadline.py +0 -0
  313. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_review_followups.py +0 -0
  314. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_stage810_audit.py +0 -0
  315. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_static_diagnostics.py +0 -0
  316. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_stop_observation.py +0 -0
  317. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_systemd_worker.py +0 -0
  318. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_ghost.py +0 -0
  319. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_gif.py +0 -0
  320. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_link.py +0 -0
  321. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_media.py +0 -0
  322. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_media_spoiler.py +0 -0
  323. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_poll.py +0 -0
  324. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_quote.py +0 -0
  325. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_quote_media.py +0 -0
  326. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_replies.py +0 -0
  327. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_text.py +0 -0
  328. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_text_spoiler.py +0 -0
  329. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_transport_review.py +0 -0
  330. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_video_typed_metadata.py +0 -0
  331. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_vorbis_comment_boundary.py +0 -0
  332. {thth-2.13.0 → thth-2.13.2}/tests/test_v213_webm_schema_boundary.py +0 -0
  333. {thth-2.13.0 → thth-2.13.2}/tests/test_v21b_approved_writes.py +0 -0
  334. {thth-2.13.0 → thth-2.13.2}/tests/test_v28_tags.py +0 -0
  335. {thth-2.13.0 → thth-2.13.2}/tests/test_v31_analysis.py +0 -0
  336. {thth-2.13.0 → thth-2.13.2}/tests/test_v31_collection.py +0 -0
  337. {thth-2.13.0 → thth-2.13.2}/tests/test_value_domains_shown.py +0 -0
  338. {thth-2.13.0 → thth-2.13.2}/tests/test_version.py +0 -0
  339. {thth-2.13.0 → thth-2.13.2}/tests/test_where_cli.py +0 -0
  340. {thth-2.13.0 → thth-2.13.2}/tests/test_who_cli.py +0 -0
  341. {thth-2.13.0 → thth-2.13.2}/tests/test_writeback_rebase.py +0 -0
  342. {thth-2.13.0 → thth-2.13.2}/tests/test_writeback_validate.py +0 -0
  343. {thth-2.13.0 → thth-2.13.2}/tests/test_x_metrics.py +0 -0
  344. {thth-2.13.0 → thth-2.13.2}/thth/__init__.py +0 -0
  345. {thth-2.13.0 → thth-2.13.2}/thth/__main__.py +0 -0
  346. {thth-2.13.0 → thth-2.13.2}/thth/_grapheme_data.py +0 -0
  347. {thth-2.13.0 → thth-2.13.2}/thth/account_cli.py +0 -0
  348. {thth-2.13.0 → thth-2.13.2}/thth/account_report.py +0 -0
  349. {thth-2.13.0 → thth-2.13.2}/thth/accounts.py +0 -0
  350. {thth-2.13.0 → thth-2.13.2}/thth/adapters/__init__.py +0 -0
  351. {thth-2.13.0 → thth-2.13.2}/thth/adapters/auth_mastodon.py +0 -0
  352. {thth-2.13.0 → thth-2.13.2}/thth/adapters/auth_threads.py +0 -0
  353. {thth-2.13.0 → thth-2.13.2}/thth/adapters/auth_x.py +0 -0
  354. {thth-2.13.0 → thth-2.13.2}/thth/adapters/base.py +0 -0
  355. {thth-2.13.0 → thth-2.13.2}/thth/adapters/bluesky.py +0 -0
  356. {thth-2.13.0 → thth-2.13.2}/thth/adapters/bluesky_media.py +0 -0
  357. {thth-2.13.0 → thth-2.13.2}/thth/adapters/bluesky_video.py +0 -0
  358. {thth-2.13.0 → thth-2.13.2}/thth/adapters/mastodon.py +0 -0
  359. {thth-2.13.0 → thth-2.13.2}/thth/adapters/mastodon_media.py +0 -0
  360. {thth-2.13.0 → thth-2.13.2}/thth/adapters/threads.py +0 -0
  361. {thth-2.13.0 → thth-2.13.2}/thth/admin_log.py +0 -0
  362. {thth-2.13.0 → thth-2.13.2}/thth/admin_notifications.py +0 -0
  363. {thth-2.13.0 → thth-2.13.2}/thth/admin_report.py +0 -0
  364. {thth-2.13.0 → thth-2.13.2}/thth/after_cli.py +0 -0
  365. {thth-2.13.0 → thth-2.13.2}/thth/analytics_comparison.py +0 -0
  366. {thth-2.13.0 → thth-2.13.2}/thth/analytics_report.py +0 -0
  367. {thth-2.13.0 → thth-2.13.2}/thth/analytics_shapes.py +0 -0
  368. {thth-2.13.0 → thth-2.13.2}/thth/analytics_threads.py +0 -0
  369. {thth-2.13.0 → thth-2.13.2}/thth/api_diagnostic.py +0 -0
  370. {thth-2.13.0 → thth-2.13.2}/thth/appconfig.py +0 -0
  371. {thth-2.13.0 → thth-2.13.2}/thth/appenv.py +0 -0
  372. {thth-2.13.0 → thth-2.13.2}/thth/approval.py +0 -0
  373. {thth-2.13.0 → thth-2.13.2}/thth/approval_jobs.py +0 -0
  374. {thth-2.13.0 → thth-2.13.2}/thth/approval_relay.py +0 -0
  375. {thth-2.13.0 → thth-2.13.2}/thth/ask.py +0 -0
  376. {thth-2.13.0 → thth-2.13.2}/thth/ask_cli.py +0 -0
  377. {thth-2.13.0 → thth-2.13.2}/thth/audioformats.py +0 -0
  378. {thth-2.13.0 → thth-2.13.2}/thth/authclients.py +0 -0
  379. {thth-2.13.0 → thth-2.13.2}/thth/authflow.py +0 -0
  380. {thth-2.13.0 → thth-2.13.2}/thth/authpassword.py +0 -0
  381. {thth-2.13.0 → thth-2.13.2}/thth/bluesky_metadata.py +0 -0
  382. {thth-2.13.0 → thth-2.13.2}/thth/budget_x.py +0 -0
  383. {thth-2.13.0 → thth-2.13.2}/thth/bundle.py +0 -0
  384. {thth-2.13.0 → thth-2.13.2}/thth/collect.py +0 -0
  385. {thth-2.13.0 → thth-2.13.2}/thth/collection_context.py +0 -0
  386. {thth-2.13.0 → thth-2.13.2}/thth/collection_status.py +0 -0
  387. {thth-2.13.0 → thth-2.13.2}/thth/deletion.py +0 -0
  388. {thth-2.13.0 → thth-2.13.2}/thth/doctor.py +0 -0
  389. {thth-2.13.0 → thth-2.13.2}/thth/ebmlaudio.py +0 -0
  390. {thth-2.13.0 → thth-2.13.2}/thth/ebmlvideo.py +0 -0
  391. {thth-2.13.0 → thth-2.13.2}/thth/engagements.py +0 -0
  392. {thth-2.13.0 → thth-2.13.2}/thth/flacformats.py +0 -0
  393. {thth-2.13.0 → thth-2.13.2}/thth/forms.py +0 -0
  394. {thth-2.13.0 → thth-2.13.2}/thth/graphemes.py +0 -0
  395. {thth-2.13.0 → thth-2.13.2}/thth/handoff_cursor.py +0 -0
  396. {thth-2.13.0 → thth-2.13.2}/thth/healthcheck.py +0 -0
  397. {thth-2.13.0 → thth-2.13.2}/thth/httpsafe.py +0 -0
  398. {thth-2.13.0 → thth-2.13.2}/thth/incident.py +0 -0
  399. {thth-2.13.0 → thth-2.13.2}/thth/jst.py +0 -0
  400. {thth-2.13.0 → thth-2.13.2}/thth/leave.py +0 -0
  401. {thth-2.13.0 → thth-2.13.2}/thth/leave_gate.py +0 -0
  402. {thth-2.13.0 → thth-2.13.2}/thth/lint.py +0 -0
  403. {thth-2.13.0 → thth-2.13.2}/thth/lock.py +0 -0
  404. {thth-2.13.0 → thth-2.13.2}/thth/maintain.py +0 -0
  405. {thth-2.13.0 → thth-2.13.2}/thth/managed_repo.py +0 -0
  406. {thth-2.13.0 → thth-2.13.2}/thth/measured.py +0 -0
  407. {thth-2.13.0 → thth-2.13.2}/thth/media_capabilities.py +0 -0
  408. {thth-2.13.0 → thth-2.13.2}/thth/media_cleanup.py +0 -0
  409. {thth-2.13.0 → thth-2.13.2}/thth/media_relay.py +0 -0
  410. {thth-2.13.0 → thth-2.13.2}/thth/media_uploads.py +0 -0
  411. {thth-2.13.0 → thth-2.13.2}/thth/mpeg_audio.py +0 -0
  412. {thth-2.13.0 → thth-2.13.2}/thth/oauth.py +0 -0
  413. {thth-2.13.0 → thth-2.13.2}/thth/oggformats.py +0 -0
  414. {thth-2.13.0 → thth-2.13.2}/thth/operations_handoff.py +0 -0
  415. {thth-2.13.0 → thth-2.13.2}/thth/postid.py +0 -0
  416. {thth-2.13.0 → thth-2.13.2}/thth/queuefile.py +0 -0
  417. {thth-2.13.0 → thth-2.13.2}/thth/read_coordination.py +0 -0
  418. {thth-2.13.0 → thth-2.13.2}/thth/read_window.py +0 -0
  419. {thth-2.13.0 → thth-2.13.2}/thth/redact.py +0 -0
  420. {thth-2.13.0 → thth-2.13.2}/thth/replies.py +0 -0
  421. {thth-2.13.0 → thth-2.13.2}/thth/report.py +0 -0
  422. {thth-2.13.0 → thth-2.13.2}/thth/report_details.py +0 -0
  423. {thth-2.13.0 → thth-2.13.2}/thth/report_http.py +0 -0
  424. {thth-2.13.0 → thth-2.13.2}/thth/report_isolation.py +0 -0
  425. {thth-2.13.0 → thth-2.13.2}/thth/report_service.py +0 -0
  426. {thth-2.13.0 → thth-2.13.2}/thth/retract_cli.py +0 -0
  427. {thth-2.13.0 → thth-2.13.2}/thth/runs.py +0 -0
  428. {thth-2.13.0 → thth-2.13.2}/thth/scopes.py +0 -0
  429. {thth-2.13.0 → thth-2.13.2}/thth/secrets_fs.py +0 -0
  430. {thth-2.13.0 → thth-2.13.2}/thth/select.py +0 -0
  431. {thth-2.13.0 → thth-2.13.2}/thth/selfupdate.py +0 -0
  432. {thth-2.13.0 → thth-2.13.2}/thth/sent.py +0 -0
  433. {thth-2.13.0 → thth-2.13.2}/thth/server_files.py +0 -0
  434. {thth-2.13.0 → thth-2.13.2}/thth/server_writes.py +0 -0
  435. {thth-2.13.0 → thth-2.13.2}/thth/stop_observation.py +0 -0
  436. {thth-2.13.0 → thth-2.13.2}/thth/study_cli.py +0 -0
  437. {thth-2.13.0 → thth-2.13.2}/thth/study_report.py +0 -0
  438. {thth-2.13.0 → thth-2.13.2}/thth/systemd_gen.py +0 -0
  439. {thth-2.13.0 → thth-2.13.2}/thth/tags.py +0 -0
  440. {thth-2.13.0 → thth-2.13.2}/thth/thread_read.py +0 -0
  441. {thth-2.13.0 → thth-2.13.2}/thth/threadrun.py +0 -0
  442. {thth-2.13.0 → thth-2.13.2}/thth/threads_read_cli.py +0 -0
  443. {thth-2.13.0 → thth-2.13.2}/thth/threadshape.py +0 -0
  444. {thth-2.13.0 → thth-2.13.2}/thth/threadthrow.py +0 -0
  445. {thth-2.13.0 → thth-2.13.2}/thth/tool_version.py +0 -0
  446. {thth-2.13.0 → thth-2.13.2}/thth/topic_advice.py +0 -0
  447. {thth-2.13.0 → thth-2.13.2}/thth/topic_cli.py +0 -0
  448. {thth-2.13.0 → thth-2.13.2}/thth/topic_models.py +0 -0
  449. {thth-2.13.0 → thth-2.13.2}/thth/topic_store.py +0 -0
  450. {thth-2.13.0 → thth-2.13.2}/thth/topics.py +0 -0
  451. {thth-2.13.0 → thth-2.13.2}/thth/unanswered.py +0 -0
  452. {thth-2.13.0 → thth-2.13.2}/thth/vorbisformats.py +0 -0
  453. {thth-2.13.0 → thth-2.13.2}/thth/where_cli.py +0 -0
  454. {thth-2.13.0 → thth-2.13.2}/thth/who_cli.py +0 -0
  455. {thth-2.13.0 → thth-2.13.2}/thth/writeback.py +0 -0
  456. {thth-2.13.0 → thth-2.13.2}/thth/x_metrics.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: thth
3
- Version: 2.13.0
3
+ Version: 2.13.2
4
4
  Summary: 承認済みの投稿だけを投げる門(Threads・Bluesky・Mastodon)。人が承認し、記録は利用者の git に残る。
5
5
  Project-URL: Homepage, https://thth.me
6
6
  Project-URL: Source, https://github.com/aokings/thth
@@ -0,0 +1,47 @@
1
+ """2.13.1 実機直し: dry-run は理由を言い、publish が断るものをここで断る。"""
2
+ import re
3
+ import pytest
4
+ from thth import cli, core, read_coordination
5
+ from tests.test_v213_media_foundation import jpeg
6
+ from tests.test_v213_media_formats import box, mp4
7
+ from tests.test_v213_threads_media import env, wire
8
+
9
+
10
+ @pytest.fixture
11
+ def send(env, monkeypatch):
12
+ monkeypatch.setattr(core, '_append_run', lambda *a, **k: None)
13
+ monkeypatch.setattr(read_coordination, 'invoke',
14
+ lambda args, name, fn=None: fn() if fn else args.func(args))
15
+ return lambda name, alt='説明': cli.main(['send', 'alpha', '--media', name, '--alt', alt])
16
+
17
+
18
+ def sized_jpeg(total):
19
+ """Pad the entropy-coded data: the public bytes are exactly `total` long."""
20
+ base = jpeg()
21
+ return base[:-3] + b'\x3f' * (1 + total - len(base)) + b'\xff\xd9'
22
+
23
+
24
+ def test_dry_run_states_the_media_reason_and_a_next_step(env, send, wire, capsys):
25
+ # fragment(moof)=本当に秒数の無い構造。ffmpeg の edit list とは違う。
26
+ (env[2] / 'v.mp4').write_bytes(mp4() + box(b'moof', b''))
27
+ assert send('v.mp4', '動画') == 2
28
+ shown = capsys.readouterr()
29
+ output = shown.out + shown.err
30
+ # 実機では `mode: rehearsal` の 1 行だけで 2 で終わっていた。
31
+ assert output.splitlines()[0] == 'mode: rehearsal'
32
+ assert 'duration_unverifiable' in output and '+faststart' in output
33
+ # 理由も次の一歩も静的: パスは 1 つも映らない。
34
+ assert str(env[2]) not in output and 'v.mp4' not in output
35
+ assert re.search(r'(^|[\s"\'(])/[A-Za-z]', output, re.M) is None
36
+ assert not wire['calls'] and not env[3]['upload']
37
+
38
+
39
+ @pytest.mark.parametrize('total,refused', [(8_000_000, False), (8_000_001, True)])
40
+ def test_threads_byte_cap_is_applied_in_the_dry_run(env, send, wire, capsys, total, refused):
41
+ (env[2] / 'big.jpg').write_bytes(sized_jpeg(total))
42
+ assert send('big.jpg') == (2 if refused else 0)
43
+ shown = capsys.readouterr()
44
+ output = shown.out + shown.err
45
+ assert ('media_limit_exceeded: bytes' in output) is refused
46
+ assert ('digest: ' in output) is (not refused)
47
+ assert not wire['calls'] and not env[3]['upload']
@@ -0,0 +1,84 @@
1
+ """2.13.1 実機直し: FINISHED の直後に出さない。4xx は Graph の番号だけ名指す。"""
2
+ import json
3
+ import pytest
4
+ from thth.adapters import threads, threads_media as tm
5
+ from tests.test_v213_threads_media import env, wire, invoke, posts
6
+
7
+
8
+ @pytest.fixture
9
+ def clock(monkeypatch):
10
+ """凍った時計。`time.sleep` だけが進める——実機の揺れを試験に入れない。"""
11
+ state = {'now': 0.0, 'slept': []}
12
+ monkeypatch.setattr(tm.time, 'monotonic', lambda: state['now'])
13
+ def sleep(seconds):
14
+ state['slept'].append(seconds)
15
+ state['now'] += seconds
16
+ monkeypatch.setattr(tm.time, 'sleep', sleep)
17
+ return state
18
+
19
+
20
+ def timeline(monkeypatch, clock):
21
+ seen = []
22
+ original = tm._json
23
+ def record(adapter, method, path, params, **kwargs):
24
+ seen.append((method, path, clock['now']))
25
+ return original(adapter, method, path, params, **kwargs)
26
+ monkeypatch.setattr(tm, '_json', record)
27
+ return seen
28
+
29
+
30
+ def test_publish_waits_the_same_wait_seconds_after_finished(env, wire, monkeypatch, clock):
31
+ env[1].wait_seconds = 30.0
32
+ seen = timeline(monkeypatch, clock)
33
+ result, journal, _ = invoke(env)
34
+ assert result.post_id == '100' and journal['media']['phase'] == 'published'
35
+ # FINISHED を読んだのは 0 秒。公開は 30 秒待ったあと 1 回だけ。
36
+ assert [row[2] for row in seen if row[0] == 'GET'] == [0.0]
37
+ assert [row[2] for row in seen if row[1].endswith('/threads_publish')] == [30.0]
38
+ assert clock['slept'] == [30.0]
39
+
40
+
41
+ def test_the_wait_never_outlives_the_grant(env, wire, clock):
42
+ # 画像の付与は 600 秒。1800 秒待てば URL が死ぬので、待たずに出す。
43
+ env[1].wait_seconds = 1800.0
44
+ result, _, _ = invoke(env)
45
+ assert result.post_id == '100' and clock['slept'] == []
46
+
47
+
48
+ def test_the_veto_after_the_wait_is_honoured(env, wire, clock):
49
+ env[1].wait_seconds = 30.0
50
+ asked = []
51
+ def veto():
52
+ asked.append(clock['now'])
53
+ return 'stopped_after_wait' if clock['now'] >= 30.0 else None
54
+ result, journal, _ = invoke(env, veto=veto)
55
+ assert max(asked) >= 30.0 and clock['slept'] == [30.0]
56
+ assert result.error == 'stopped_after_wait' and result.failure == 'media_held'
57
+ assert not posts(wire, '/threads_publish')
58
+ assert journal['media']['phase'] == 'held' and journal['media']['reason'] == 'stopped_after_wait'
59
+
60
+
61
+ def test_publish_4xx_names_the_graph_numbers_and_nothing_else(env, wire):
62
+ wire['publish'] = (400, {'error': {'message': 'Media ID is not available',
63
+ 'type': 'OAuthException', 'code': 100,
64
+ 'error_subcode': 2207032, 'fbtrace_id': 'AsEcReT'}})
65
+ result, journal, _ = invoke(env)
66
+ assert result.post_id is None and result.failure == 'media_held'
67
+ assert result.error == 'media_publishing_http_400_code_100_subcode_2207032'
68
+ assert journal['media']['phase'] == 'held' and journal['media']['reason'] == result.error
69
+ blob = json.dumps(journal, ensure_ascii=False) + str(result.error)
70
+ for secret in ('Media ID', 'OAuthException', 'AsEcReT'):
71
+ assert secret not in blob
72
+
73
+
74
+ @pytest.mark.parametrize('body', [{}, {'error': {'message': 'nope'}}, {'error': 'nope'}])
75
+ def test_a_4xx_without_numbers_keeps_the_plain_reason(env, wire, body):
76
+ wire['publish'] = (400, body)
77
+ result, _, _ = invoke(env)
78
+ assert result.error == 'media_publishing_http_400'
79
+
80
+
81
+ def test_the_env_override_reaches_the_attachment_path(monkeypatch):
82
+ monkeypatch.setenv('THTH_THREADS_WAIT_SECONDS', '7')
83
+ adapter = threads.ThreadsAdapter.from_account({'account': 'alpha', 'media': 'threads'}, {})
84
+ assert adapter.wait_seconds == 7.0
@@ -0,0 +1,67 @@
1
+ """2.13.2 実機直し: inflight が残っているなら、dry-run でも断って次の一歩を言う。
2
+
3
+ 作成の時間切れ(`media_creating_timeout`)は inflight を `unknown` のまま
4
+ 残す。そのあと `thth send` を打った人が最初に見るのがここ——2.13.1 までは
5
+ `--production` なしだと digest まで出して 0 で終わり、残っていることを一言も
6
+ 言わなかった。`--production` を付ければ断るが、理由 1 行だけで「どうすれば
7
+ いいか」は無かった。
8
+ """
9
+ import pytest
10
+ from thth import accounts, cli, core, inflight, media, media_delivery, read_coordination
11
+ from tests.test_v213_threads_media import env, wire
12
+
13
+
14
+ @pytest.fixture
15
+ def send(env, monkeypatch):
16
+ monkeypatch.setattr(core, '_append_run', lambda *a, **k: None)
17
+ monkeypatch.setattr(read_coordination, 'invoke',
18
+ lambda args, name, fn=None: fn() if fn else args.func(args))
19
+ return lambda *extra: cli.main(['send', 'alpha', '--media', 'a.png', '--alt', '説明', *extra])
20
+
21
+
22
+ def seed_unknown(env, reason='media_creating_timeout'):
23
+ """作成の時間切れが残す journal と同じ形(phase `unknown`・remote_ids 空)。"""
24
+ state = accounts.state_dir_for('alpha')
25
+ manifest = media.manifest_for({'media': [{'file': 'a.png', 'alt': '説明'}]}, env[0])
26
+ inflight.write(state, file='(send)', started='2030-01-01T00:00:00+09:00')
27
+ media_delivery._progress('alpha', manifest, 'https://graph.invalid', 'unknown',
28
+ remote_ids=[], reason=reason)
29
+ return state
30
+
31
+
32
+ @pytest.mark.parametrize('extra', [(), ('--production',)])
33
+ def test_send_refuses_and_says_what_to_do(env, send, wire, capsys, extra):
34
+ seed_unknown(env)
35
+ assert send(*extra) == 1
36
+ shown = capsys.readouterr()
37
+ output = shown.out + shown.err
38
+ assert 'inflight が残っています' in output
39
+ assert inflight.NEXT_STEP in output
40
+ # 断ったのだから digest も本文の下見も出ない。媒体には一度も触らない。
41
+ assert 'digest: ' not in output and not wire['calls'] and not env[3]['upload']
42
+ # 次の一歩は 1 行だけ。
43
+ assert len([line for line in output.splitlines() if line.startswith('次の一歩')]) == 1
44
+
45
+
46
+ def test_the_refusal_stays_static(env, send, capsys):
47
+ seed_unknown(env)
48
+ assert send() == 1
49
+ shown = capsys.readouterr()
50
+ output = shown.out + shown.err
51
+ # repo の実パスもアカウントの state パスも映さない(既存の静的の約束)。
52
+ assert str(env[2]) not in output and str(accounts.state_dir_for('alpha')) not in output
53
+
54
+
55
+ def test_the_inflight_is_left_exactly_as_it_was(env, send):
56
+ state = seed_unknown(env)
57
+ before = inflight.read(state)
58
+ assert send() == 1 and send('--production') == 1
59
+ assert inflight.read(state) == before
60
+
61
+
62
+ def test_without_an_inflight_the_dry_run_still_shows_the_digest(env, send, capsys):
63
+ assert send() == 0
64
+ shown = capsys.readouterr()
65
+ output = shown.out + shown.err
66
+ assert 'digest: ' in output and 'inflight が残っています' not in output
67
+ assert inflight.NEXT_STEP not in output
@@ -0,0 +1,84 @@
1
+ """2.13.2 実機直し: container の作成は Meta の取り込みを待つ(10 秒では足りない)。
2
+
3
+ 実機 2026-09-22(masaru-threads・`--media docs/media/photo.jpg`): `POST
4
+ /{user}/threads` に `image_url` を渡すと、Meta はその POST を開いたまま
5
+ thth.me/R2 から画像を取りに行く。2.13.0 の同じ添付は通ったのに 2.13.1 は
6
+ `media_creating_failed`(phase `unknown`・`remote_ids=[]`)で落ちた——違いは
7
+ Meta の取り込みにかかった時間で、こちらの 10 秒(`adapter.timeout`)が短い。
8
+ """
9
+ import time
10
+ import pytest
11
+ from thth.adapters import threads_media as tm
12
+ from tests.test_v213_threads_media import env, wire, invoke, posts
13
+
14
+
15
+ @pytest.fixture
16
+ def clock(monkeypatch):
17
+ """凍った時計。`time.sleep` と偽 Graph の遅れだけが進める。"""
18
+ state = {'now': 0.0}
19
+ monkeypatch.setattr(tm.time, 'monotonic', lambda: state['now'])
20
+ monkeypatch.setattr(tm.time, 'sleep', lambda seconds: state.update(now=state['now'] + seconds))
21
+ return state
22
+
23
+
24
+ def sockets(monkeypatch):
25
+ """各 HTTP 呼び出しに渡った socket の締切(方式・パス・秒)を並べる。"""
26
+ seen = []
27
+ original = tm._transport.open
28
+ def open_request(request, timeout=None, **kwargs):
29
+ seen.append((request.get_method(), request.full_url.split('?')[0], timeout))
30
+ return original(request, timeout=timeout, **kwargs)
31
+ monkeypatch.setattr(tm._transport, 'open', open_request)
32
+ return seen
33
+
34
+
35
+ def test_a_creation_slower_than_the_adapter_timeout_still_lands(env, wire):
36
+ # 実機の再現: 作成だけが `adapter.timeout` より長くかかる。
37
+ env[1].timeout = 0.3
38
+ wire['on_create'] = lambda: time.sleep(1.0)
39
+ result, journal, _ = invoke(env)
40
+ assert result.post_id == '100' and journal['media']['phase'] == 'published'
41
+ assert len(posts(wire, '/threads_publish')) == 1
42
+
43
+
44
+ def test_only_the_creation_gets_the_long_budget(env, wire, monkeypatch):
45
+ seen = sockets(monkeypatch)
46
+ assert invoke(env)[0].post_id == '100'
47
+ # 作成は 90 秒。状態の問い合わせも公開も adapter の 10 秒のまま。
48
+ assert [t for m, u, t in seen if m == 'POST' and u.endswith('/threads')] == [tm.CREATE_TIMEOUT_SECONDS]
49
+ assert [t for m, u, t in seen if m == 'GET'] == [env[1].timeout]
50
+ assert [t for m, u, t in seen if u.endswith('/threads_publish')] == [env[1].timeout]
51
+
52
+
53
+ @pytest.mark.parametrize('raw,seconds', [
54
+ (None, 90.0), ('120', 120.0), ('90.5', 90.5),
55
+ ('10', 10.0), ('9', 10.0), ('0', 10.0), ('-5', 10.0),
56
+ ('600', 600.0), ('601', 600.0), ('86400', 600.0),
57
+ ('', 90.0), ('abc', 90.0), ('inf', 90.0), ('nan', 90.0),
58
+ ])
59
+ def test_the_override_is_read_and_clamped(monkeypatch, raw, seconds):
60
+ monkeypatch.delenv('THTH_THREADS_CREATE_TIMEOUT_SECONDS', raising=False)
61
+ if raw is not None:
62
+ monkeypatch.setenv('THTH_THREADS_CREATE_TIMEOUT_SECONDS', raw)
63
+ assert tm.create_timeout() == seconds
64
+
65
+
66
+ def test_the_override_reaches_the_creation_socket(env, wire, monkeypatch):
67
+ monkeypatch.setenv('THTH_THREADS_CREATE_TIMEOUT_SECONDS', '900')
68
+ seen = sockets(monkeypatch)
69
+ assert invoke(env)[0].post_id == '100'
70
+ assert [t for m, u, t in seen if m == 'POST' and u.endswith('/threads')] == [600.0]
71
+
72
+
73
+ def test_the_poll_deadline_counts_the_time_the_creation_took(env, wire, monkeypatch, clock):
74
+ # 画像の持ち時間は 120 秒。作成に 40 秒使ったら、問い合わせに残るのは 80 秒。
75
+ monkeypatch.setattr(tm, 'POLL_INTERVAL', 10.0)
76
+ wire['poll'] = [(200, {'status': 'IN_PROGRESS'})] * 60
77
+ wire['on_create'] = lambda: clock.update(now=clock['now'] + 40.0)
78
+ result, journal, _ = invoke(env)
79
+ assert result.error == 'media_processing_timeout' and result.failure == 'media_held'
80
+ # 作成を数えなければ 160 秒・12 回になる。
81
+ assert clock['now'] == tm.POLL_SECONDS
82
+ assert len([c for c in wire['calls'] if c[0] == 'GET']) == 8
83
+ assert not posts(wire, '/threads_publish') and env[3]['results'] == [False]
84
+
@@ -0,0 +1,90 @@
1
+ """2.13.2 実機直し: 作成の時間切れは「出ていないこと」ではない。
2
+
3
+ Meta は `POST /{user}/threads` を開いたまま媒体を取りに行くので、こちらが
4
+ socket を諦めたあとに向こうで container ができていることがある。phase は
5
+ `unknown` のまま——そのうえで理由を `media_creating_timeout` と名指し、
6
+ `thth send` が「確かめてから消す」と言えるようにする。
7
+ """
8
+ import threading
9
+ import urllib.error
10
+ import pytest
11
+ from thth import media_delivery
12
+ from thth.adapters import threads_media as tm
13
+ from tests.test_v213_media_foundation import png
14
+ from tests.test_v213_threads_media import env, wire, invoke, posts
15
+
16
+ STEP = '次の一歩: 媒体が画像/動画を取り込むのに時間がかかっています。しばらくして inflight を確認し、投稿が無ければ消してから再実行'
17
+
18
+
19
+ @pytest.fixture
20
+ def stall():
21
+ """偽 Graph を黙らせる掛け金。試験が終わったら必ず外す(daemon を残さない)。"""
22
+ gate = threading.Event()
23
+ try:
24
+ yield lambda: gate.wait(10)
25
+ finally:
26
+ gate.set()
27
+
28
+
29
+ @pytest.mark.parametrize('error,timed_out', [
30
+ (TimeoutError('slow'), True),
31
+ (urllib.error.URLError(TimeoutError('slow')), True),
32
+ (urllib.error.URLError(ConnectionResetError('reset')), False),
33
+ (OSError('broken'), False),
34
+ (urllib.error.HTTPError('http://x.invalid', 408, 'Request Timeout', None, None), False),
35
+ ])
36
+ def test_only_a_socket_timeout_counts_as_one(error, timed_out):
37
+ assert tm.socket_timed_out(error) is timed_out
38
+
39
+
40
+ def test_a_creation_that_never_answers_is_unknown_and_named_as_a_timeout(env, wire, monkeypatch, stall):
41
+ monkeypatch.setattr(tm, 'create_timeout', lambda: 0.3)
42
+ wire['on_create'] = stall
43
+ result, journal, _ = invoke(env)
44
+ assert result.post_id is None and result.failure == 'media_ambiguous'
45
+ assert result.error == 'media_creating_timeout'
46
+ assert journal['media']['phase'] == 'unknown' and journal['media']['reason'] == 'media_creating_timeout'
47
+ # container ができたかどうかは分からない: 覚えている id は無く、公開もしない。
48
+ assert journal['media']['remote_ids'] == [] and not posts(wire, '/threads_publish')
49
+ # 付与も引き上げない(引き上げれば向こうにできた container が確実に死ぬ)。
50
+ assert env[3]['results'] == []
51
+ assert media_delivery.next_step(result.error) == STEP
52
+
53
+
54
+ def test_the_carousel_parent_timeout_is_named_the_same_way(env, wire, monkeypatch, stall):
55
+ (env[2] / 'b.png').write_bytes(png())
56
+ monkeypatch.setattr(tm, 'create_timeout', lambda: 0.3)
57
+ created = []
58
+ def on_create():
59
+ created.append(1)
60
+ if len(created) == 3:
61
+ stall()
62
+ wire['on_create'] = on_create
63
+ fm = {'media': [{'file': 'a.png', 'alt': '点'}, {'file': 'b.png', 'alt': '別の点'}]}
64
+ result, journal, _ = invoke(env, fm)
65
+ assert result.error == 'media_creating_carousel_timeout' and result.failure == 'media_ambiguous'
66
+ assert journal['media']['phase'] == 'unknown' and journal['media']['remote_ids'] == ['11', '12']
67
+ assert not posts(wire, '/threads_publish') and env[3]['results'] == []
68
+ assert media_delivery.next_step(result.error) == STEP
69
+
70
+
71
+ def test_other_creation_failures_keep_the_generic_reason(env, wire, monkeypatch):
72
+ original = tm._transport.open
73
+ def open_request(request, timeout=None, **kwargs):
74
+ if request.get_method() == 'POST' and request.full_url.endswith('/threads'):
75
+ raise urllib.error.URLError(ConnectionResetError('reset'))
76
+ return original(request, timeout=timeout, **kwargs)
77
+ monkeypatch.setattr(tm._transport, 'open', open_request)
78
+ result, journal, _ = invoke(env)
79
+ assert result.error == 'media_creating_failed' and result.failure == 'media_ambiguous'
80
+ assert journal['media']['phase'] == 'unknown'
81
+ assert media_delivery.next_step(result.error) is None
82
+
83
+
84
+ def test_a_slow_poll_is_not_a_creation_timeout(env, wire, monkeypatch, stall):
85
+ # 時間切れを名指すのは作成だけ。問い合わせの時間切れは既存の扱いのまま。
86
+ env[1].timeout = 0.3
87
+ wire['on_poll'] = stall
88
+ result, journal, _ = invoke(env)
89
+ assert result.error == 'media_processing_failed' and result.failure == 'media_held'
90
+ assert journal['media']['phase'] == 'held' and journal['media']['remote_ids'] == ['11']
@@ -41,5 +41,6 @@ def test_mastodon_still_accepts_the_same_webm(tmp_path):
41
41
  def test_policy_table_names_every_medium():
42
42
  assert set(media.MEDIUM_POLICY) == {'threads', 'bluesky', 'mastodon'}
43
43
  assert media.MEDIUM_POLICY['mastodon']['allow_webm'] is True
44
- assert media.MEDIUM_POLICY['threads']['allow_edit_lists'] is True
44
+ # An edit list is no longer a per-medium policy: every medium takes one.
45
+ assert all(set(p) <= {'allow_webm'} for p in media.MEDIUM_POLICY.values())
45
46
  assert all(p.get('allow_webm') is False for m, p in media.MEDIUM_POLICY.items() if m != 'mastodon')
@@ -463,6 +463,21 @@ def test_lint_instance_failure_never_uses_permissive_cache(env,wire,monkeypatch)
463
463
  def unavailable(*a,**k):raise OSError('synthetic connectivity failure')
464
464
  monkeypatch.setattr(mm,'_json',unavailable)
465
465
  assert media_delivery.lint_notes(cfg,{'media':[{'file':'a.png','alt':'点'}]})==['media_capability_unavailable']
466
+ # 予行は upload ではない。届かない instance は断る根拠にならず、lint と
467
+ # 同じ capability_unobserved を警告として言う(上限は upload 直前に再観測)。
468
+ warned=media_delivery.lint_notes(cfg,{'media':[{'file':'a.png','alt':'点'}]},unreachable_warns=True)
469
+ # 保存済みの上限があっても、この run では何も観測していないと言う。
470
+ assert warned==['warning: '+media_delivery.CAPABILITY_UNOBSERVED]
471
+ assert mm.cached(cfg) is not None
472
+
473
+
474
+ def test_rehearsal_still_refuses_limits_the_instance_did_answer(env,wire,monkeypatch):
475
+ cfg,adapter,repo=env
476
+ wire['caps']['configuration']['media_attachments']['image_size_limit']=1
477
+ fm={'media':[{'file':'a.png','alt':'点'}]}
478
+ assert media_delivery.lint_notes(cfg,fm,unreachable_warns=True)==['media_limit_exceeded: bytes']
479
+ del wire['caps']['configuration']['media_attachments']['image_size_limit']
480
+ assert media_delivery.lint_notes(cfg,fm,unreachable_warns=True)==['media_capability_unavailable: image_size_limit']
466
481
 
467
482
 
468
483
  @pytest.mark.parametrize('boundary', ['sleep', 'response_equal', 'response_late', 'legal'])
@@ -512,6 +527,9 @@ def test_cli_source_only_stale_is_loud_and_has_no_provider(env,wire,monkeypatch,
512
527
  argv=['send','alpha','--media','a.png','--alt','点']
513
528
  assert cli.main(argv)==0;shown=capsys.readouterr().out
514
529
  digest=next(line.split(': ',1)[1] for line in shown.splitlines() if line.startswith('digest: '))
530
+ # 予行が読むのは公開の instance 上限だけ——資格情報も POST も無い。
531
+ assert all(call[0]=='GET' and call[1]=='/api/v2/instance' and 'Authorization' not in call[3] for call in wire['calls'])
532
+ wire['calls'].clear()
515
533
  raw=(repo/'a.png').read_bytes();data=b'Note\0private fixture only';kind=b'tEXt'
516
534
  changed=raw[:-12]+struct.pack('>I',len(data))+kind+data+struct.pack('>I',zlib.crc32(kind+data))+raw[-12:]
517
535
  assert mediaformats.png(raw).public_bytes==mediaformats.png(changed).public_bytes
@@ -22,15 +22,44 @@ def prepared(tmp_path,raw,medium='threads'):
22
22
  return media.prepare(tmp_path,{'media':[{'file':'v.mp4','alt':'動画'}]},medium)
23
23
 
24
24
 
25
- def test_valid_edit_list_and_late_moov_warn_only_threads(tmp_path):
25
+ def test_valid_edit_list_and_late_moov_warn_only(tmp_path):
26
26
  raw=edit_movie(front=False)
27
27
  with prepared(tmp_path,raw) as(m,items):
28
28
  notes=tm.notes(m,items)
29
29
  assert b''.join(items[0].chunks())==raw
30
30
  assert any('edit lists' in n for n in notes) and any('moov follows' in n for n in notes)
31
31
  assert any('frame rate unobserved' in n for n in notes)
32
+ # The same file on the other media: a warning there too, never a refusal.
33
+ with prepared(tmp_path,raw,'mastodon') as(m,_):
34
+ assert m['files'][0]['metadata_notes']==['edit_list_present']
35
+
36
+
37
+ def movie_seconds(ticks,scale=1000,*,edit=True,handler=b'vide'):
38
+ """ffmpeg shape: +faststart moov before mdat and an edts/elst under trak."""
39
+ edts=box(b'edts',box(b'elst',b'\0'*4+struct.pack('>I',1)+struct.pack('>IiHH',ticks,0,1,0))) if edit else b''
40
+ mvhd=b'\0'*12+struct.pack('>II',scale,ticks)+b'\0'*80
41
+ tkhd=b'\0'*76+struct.pack('>II',720<<16,1280<<16)
42
+ hdlr=b'\0'*8+handler+b'\0'*12
43
+ return (box(b'ftyp',b'isom\0\0\0\0isom')
44
+ +box(b'moov',box(b'mvhd',mvhd)+box(b'trak',box(b'tkhd',tkhd)+edts+box(b'mdia',box(b'hdlr',hdlr))))
45
+ +box(b'mdat',b'synthetic-sample'))
46
+
47
+
48
+ @pytest.mark.parametrize('medium',['threads','bluesky','mastodon'])
49
+ def test_ffmpeg_edit_list_is_a_note_on_every_medium(tmp_path,medium):
50
+ raw=movie_seconds(5000)
51
+ with prepared(tmp_path,raw,medium) as(m,items):
52
+ row=m['files'][0]
53
+ assert (row['format'],row['kind'],row['width'],row['height'])==('mp4','video',720,1280)
54
+ assert row['duration']==5.0 and row['metadata_notes']==['edit_list_present']
55
+ assert b''.join(items[0].chunks())==raw
56
+
57
+
58
+ @pytest.mark.parametrize('medium',['threads','bluesky','mastodon'])
59
+ @pytest.mark.parametrize('ticks,scale',[(0,1000),(5000,0)])
60
+ def test_movie_header_without_seconds_stays_unverifiable(tmp_path,medium,ticks,scale):
32
61
  with pytest.raises(media.MediaError,match='duration_unverifiable'):
33
- with prepared(tmp_path,raw,'mastodon'):pass
62
+ with prepared(tmp_path,movie_seconds(ticks,scale),medium):pass
34
63
 
35
64
 
36
65
  @pytest.mark.parametrize('raw,reason',[(edit_movie(version=2),'invalid_attachment_structure'),(edit_movie(count=2),'invalid_attachment_structure'),(edit_movie(extra=box(b'uuid',b'private')),'location_metadata_unverifiable'),(edit_movie(extra=box(b'free',b'private')),'location_metadata_unverifiable'),(edit_movie(extra=box(b'ZZZZ',b'')),'location_metadata_unverifiable'),(edit_movie(extra=box(b'\xa9xyz',b'private')),'location_metadata_present')])
@@ -0,0 +1 @@
1
+ 2.13.2