thth 3.3.0__tar.gz → 3.3.1__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 (510) hide show
  1. {thth-3.3.0 → thth-3.3.1}/PKG-INFO +2 -2
  2. {thth-3.3.0 → thth-3.3.1}/README.md +1 -1
  3. {thth-3.3.0 → thth-3.3.1}/skills/thth/SKILL.md +1 -1
  4. {thth-3.3.0 → thth-3.3.1}/tests/conftest.py +10 -0
  5. thth-3.3.1/tests/helpers/fake_threads_status.py +123 -0
  6. {thth-3.3.0 → thth-3.3.1}/tests/test_incident_notifications.py +4 -2
  7. {thth-3.3.0 → thth-3.3.1}/tests/test_v210_json_contract.py +3 -1
  8. thth-3.3.1/tests/test_v331_inflight_cli.py +156 -0
  9. thth-3.3.1/tests/test_v331_listing_resolve.py +136 -0
  10. thth-3.3.1/tests/test_v331_outbox_retention.py +135 -0
  11. thth-3.3.1/tests/test_v331_publish_status.py +213 -0
  12. thth-3.3.1/tests/test_v331_self_resolve.py +222 -0
  13. thth-3.3.1/thth/VERSION +1 -0
  14. {thth-3.3.0 → thth-3.3.1}/thth/adapters/base.py +6 -0
  15. {thth-3.3.0 → thth-3.3.1}/thth/adapters/threads.py +177 -7
  16. {thth-3.3.0 → thth-3.3.1}/thth/admin_log.py +3 -1
  17. {thth-3.3.0 → thth-3.3.1}/thth/cli.py +9 -0
  18. {thth-3.3.0 → thth-3.3.1}/thth/core.py +92 -13
  19. {thth-3.3.0 → thth-3.3.1}/thth/healthcheck.py +11 -1
  20. {thth-3.3.0 → thth-3.3.1}/thth/incident.py +77 -3
  21. {thth-3.3.0 → thth-3.3.1}/thth/inflight.py +61 -8
  22. thth-3.3.1/thth/inflight_cli.py +301 -0
  23. thth-3.3.1/thth/inflight_resolve.py +435 -0
  24. {thth-3.3.0 → thth-3.3.1}/thth/leave_gate.py +1 -1
  25. {thth-3.3.0 → thth-3.3.1}/thth/morning.py +18 -0
  26. {thth-3.3.0 → thth-3.3.1}/thth/runs.py +8 -0
  27. thth-3.3.0/thth/VERSION +0 -1
  28. {thth-3.3.0 → thth-3.3.1}/.gitignore +0 -0
  29. {thth-3.3.0 → thth-3.3.1}/LICENSE +0 -0
  30. {thth-3.3.0 → thth-3.3.1}/accounts.example/bluesky.json +0 -0
  31. {thth-3.3.0 → thth-3.3.1}/accounts.example/mastodon.json +0 -0
  32. {thth-3.3.0 → thth-3.3.1}/accounts.example/threads.json +0 -0
  33. {thth-3.3.0 → thth-3.3.1}/accounts.example/x.json +0 -0
  34. {thth-3.3.0 → thth-3.3.1}/bin/thth +0 -0
  35. {thth-3.3.0 → thth-3.3.1}/bin/thth-remote +0 -0
  36. {thth-3.3.0 → thth-3.3.1}/bin/thth-run +0 -0
  37. {thth-3.3.0 → thth-3.3.1}/mcp/server.py +0 -0
  38. {thth-3.3.0 → thth-3.3.1}/pyproject.toml +0 -0
  39. {thth-3.3.0 → thth-3.3.1}/skills/thth-admin/SKILL.md +0 -0
  40. {thth-3.3.0 → thth-3.3.1}/systemd/thth@.service +0 -0
  41. {thth-3.3.0 → thth-3.3.1}/tests/__init__.py +0 -0
  42. {thth-3.3.0 → thth-3.3.1}/tests/coordination_snapshot.py +0 -0
  43. {thth-3.3.0 → thth-3.3.1}/tests/fixtures/GraphemeBreakTest-17.0.0.txt +0 -0
  44. {thth-3.3.0 → thth-3.3.1}/tests/fixtures/build_injection_eval.py +0 -0
  45. {thth-3.3.0 → thth-3.3.1}/tests/fixtures/callback/worker_responses.json +0 -0
  46. {thth-3.3.0 → thth-3.3.1}/tests/fixtures/injection_eval.json +0 -0
  47. {thth-3.3.0 → thth-3.3.1}/tests/fixtures/umami-bile.md +0 -0
  48. {thth-3.3.0 → thth-3.3.1}/tests/helpers/__init__.py +0 -0
  49. {thth-3.3.0 → thth-3.3.1}/tests/helpers/fake_oauth_server.py +0 -0
  50. {thth-3.3.0 → thth-3.3.1}/tests/helpers/fake_push_adapter.py +0 -0
  51. {thth-3.3.0 → thth-3.3.1}/tests/helpers/fake_redirect_server.py +0 -0
  52. {thth-3.3.0 → thth-3.3.1}/tests/helpers/fake_search_adapter.py +0 -0
  53. {thth-3.3.0 → thth-3.3.1}/tests/helpers/fake_threads_reader.py +0 -0
  54. {thth-3.3.0 → thth-3.3.1}/tests/helpers/fake_threads_writes.py +0 -0
  55. {thth-3.3.0 → thth-3.3.1}/tests/helpers/fake_x.py +0 -0
  56. {thth-3.3.0 → thth-3.3.1}/tests/helpers/hold_lock.py +0 -0
  57. {thth-3.3.0 → thth-3.3.1}/tests/helpers/hold_repo_lock.py +0 -0
  58. {thth-3.3.0 → thth-3.3.1}/tests/helpers/worker_probe.mjs +0 -0
  59. {thth-3.3.0 → thth-3.3.1}/tests/test_27_acceptance_fixes.py +0 -0
  60. {thth-3.3.0 → thth-3.3.1}/tests/test_account_name_check.py +0 -0
  61. {thth-3.3.0 → thth-3.3.1}/tests/test_account_report.py +0 -0
  62. {thth-3.3.0 → thth-3.3.1}/tests/test_accounts_dir.py +0 -0
  63. {thth-3.3.0 → thth-3.3.1}/tests/test_adapter_boundary.py +0 -0
  64. {thth-3.3.0 → thth-3.3.1}/tests/test_admin_http_mcp.py +0 -0
  65. {thth-3.3.0 → thth-3.3.1}/tests/test_admin_log.py +0 -0
  66. {thth-3.3.0 → thth-3.3.1}/tests/test_admin_notifications.py +0 -0
  67. {thth-3.3.0 → thth-3.3.1}/tests/test_admin_repairs.py +0 -0
  68. {thth-3.3.0 → thth-3.3.1}/tests/test_admin_report.py +0 -0
  69. {thth-3.3.0 → thth-3.3.1}/tests/test_after_cli.py +0 -0
  70. {thth-3.3.0 → thth-3.3.1}/tests/test_analytics_comparison.py +0 -0
  71. {thth-3.3.0 → thth-3.3.1}/tests/test_analytics_report.py +0 -0
  72. {thth-3.3.0 → thth-3.3.1}/tests/test_api_diagnostic.py +0 -0
  73. {thth-3.3.0 → thth-3.3.1}/tests/test_api_error_body.py +0 -0
  74. {thth-3.3.0 → thth-3.3.1}/tests/test_app_env_cli.py +0 -0
  75. {thth-3.3.0 → thth-3.3.1}/tests/test_approval.py +0 -0
  76. {thth-3.3.0 → thth-3.3.1}/tests/test_approve_cmd.py +0 -0
  77. {thth-3.3.0 → thth-3.3.1}/tests/test_ask.py +0 -0
  78. {thth-3.3.0 → thth-3.3.1}/tests/test_atlas_fifth_review.py +0 -0
  79. {thth-3.3.0 → thth-3.3.1}/tests/test_atlas_fourth_review.py +0 -0
  80. {thth-3.3.0 → thth-3.3.1}/tests/test_atlas_review.py +0 -0
  81. {thth-3.3.0 → thth-3.3.1}/tests/test_atlas_second_review.py +0 -0
  82. {thth-3.3.0 → thth-3.3.1}/tests/test_atlas_third_review.py +0 -0
  83. {thth-3.3.0 → thth-3.3.1}/tests/test_audit1_fixes.py +0 -0
  84. {thth-3.3.0 → thth-3.3.1}/tests/test_audit2_fixes.py +0 -0
  85. {thth-3.3.0 → thth-3.3.1}/tests/test_audit_b_counterexamples.py +0 -0
  86. {thth-3.3.0 → thth-3.3.1}/tests/test_audit_b_m1_semantics.py +0 -0
  87. {thth-3.3.0 → thth-3.3.1}/tests/test_audit_b_n1_depth.py +0 -0
  88. {thth-3.3.0 → thth-3.3.1}/tests/test_audit_b_r1_closure.py +0 -0
  89. {thth-3.3.0 → thth-3.3.1}/tests/test_audit_v201.py +0 -0
  90. {thth-3.3.0 → thth-3.3.1}/tests/test_audit_v23_details.py +0 -0
  91. {thth-3.3.0 → thth-3.3.1}/tests/test_auth.py +0 -0
  92. {thth-3.3.0 → thth-3.3.1}/tests/test_auth_identity_required.py +0 -0
  93. {thth-3.3.0 → thth-3.3.1}/tests/test_behind_remote.py +0 -0
  94. {thth-3.3.0 → thth-3.3.1}/tests/test_bluesky_adapter.py +0 -0
  95. {thth-3.3.0 → thth-3.3.1}/tests/test_bluesky_secret_scrub.py +0 -0
  96. {thth-3.3.0 → thth-3.3.1}/tests/test_board_needs_review.py +0 -0
  97. {thth-3.3.0 → thth-3.3.1}/tests/test_board_running.py +0 -0
  98. {thth-3.3.0 → thth-3.3.1}/tests/test_bundle.py +0 -0
  99. {thth-3.3.0 → thth-3.3.1}/tests/test_charcount.py +0 -0
  100. {thth-3.3.0 → thth-3.3.1}/tests/test_cli_entrypoint.py +0 -0
  101. {thth-3.3.0 → thth-3.3.1}/tests/test_cli_help.py +0 -0
  102. {thth-3.3.0 → thth-3.3.1}/tests/test_cli_vm_path.py +0 -0
  103. {thth-3.3.0 → thth-3.3.1}/tests/test_collect.py +0 -0
  104. {thth-3.3.0 → thth-3.3.1}/tests/test_collect_engagements.py +0 -0
  105. {thth-3.3.0 → thth-3.3.1}/tests/test_collect_runs.py +0 -0
  106. {thth-3.3.0 → thth-3.3.1}/tests/test_collect_sent.py +0 -0
  107. {thth-3.3.0 → thth-3.3.1}/tests/test_comparability.py +0 -0
  108. {thth-3.3.0 → thth-3.3.1}/tests/test_control_chars_refused.py +0 -0
  109. {thth-3.3.0 → thth-3.3.1}/tests/test_conversation_paging.py +0 -0
  110. {thth-3.3.0 → thth-3.3.1}/tests/test_count_for.py +0 -0
  111. {thth-3.3.0 → thth-3.3.1}/tests/test_docs_links.py +0 -0
  112. {thth-3.3.0 → thth-3.3.1}/tests/test_doctor.py +0 -0
  113. {thth-3.3.0 → thth-3.3.1}/tests/test_doctor_all_permissions.py +0 -0
  114. {thth-3.3.0 → thth-3.3.1}/tests/test_doctor_next_step.py +0 -0
  115. {thth-3.3.0 → thth-3.3.1}/tests/test_duplicate_front_matter_keys.py +0 -0
  116. {thth-3.3.0 → thth-3.3.1}/tests/test_editorial_audit.py +0 -0
  117. {thth-3.3.0 → thth-3.3.1}/tests/test_editorial_n3_n4.py +0 -0
  118. {thth-3.3.0 → thth-3.3.1}/tests/test_engagements.py +0 -0
  119. {thth-3.3.0 → thth-3.3.1}/tests/test_external_acceptance.py +0 -0
  120. {thth-3.3.0 → thth-3.3.1}/tests/test_fake_api.py +0 -0
  121. {thth-3.3.0 → thth-3.3.1}/tests/test_form_spec.py +0 -0
  122. {thth-3.3.0 → thth-3.3.1}/tests/test_form_spec_cli.py +0 -0
  123. {thth-3.3.0 → thth-3.3.1}/tests/test_forms.py +0 -0
  124. {thth-3.3.0 → thth-3.3.1}/tests/test_fresh_install.py +0 -0
  125. {thth-3.3.0 → thth-3.3.1}/tests/test_front_matter_injection.py +0 -0
  126. {thth-3.3.0 → thth-3.3.1}/tests/test_healthcheck_notifications.py +0 -0
  127. {thth-3.3.0 → thth-3.3.1}/tests/test_hypotheses.py +0 -0
  128. {thth-3.3.0 → thth-3.3.1}/tests/test_hypothesis_boundaries.py +0 -0
  129. {thth-3.3.0 → thth-3.3.1}/tests/test_hypothesis_units.py +0 -0
  130. {thth-3.3.0 → thth-3.3.1}/tests/test_impact_cli.py +0 -0
  131. {thth-3.3.0 → thth-3.3.1}/tests/test_inbox_granted_scopes.py +0 -0
  132. {thth-3.3.0 → thth-3.3.1}/tests/test_independent_thread_review.py +0 -0
  133. {thth-3.3.0 → thth-3.3.1}/tests/test_independent_topic_review.py +0 -0
  134. {thth-3.3.0 → thth-3.3.1}/tests/test_independent_topic_review_r2.py +0 -0
  135. {thth-3.3.0 → thth-3.3.1}/tests/test_inflight.py +0 -0
  136. {thth-3.3.0 → thth-3.3.1}/tests/test_inflight_tag_body_hash.py +0 -0
  137. {thth-3.3.0 → thth-3.3.1}/tests/test_jst.py +0 -0
  138. {thth-3.3.0 → thth-3.3.1}/tests/test_lint.py +0 -0
  139. {thth-3.3.0 → thth-3.3.1}/tests/test_list_projections.py +0 -0
  140. {thth-3.3.0 → thth-3.3.1}/tests/test_llm_trial.py +0 -0
  141. {thth-3.3.0 → thth-3.3.1}/tests/test_llm_trial_engage.py +0 -0
  142. {thth-3.3.0 → thth-3.3.1}/tests/test_lock.py +0 -0
  143. {thth-3.3.0 → thth-3.3.1}/tests/test_m2_independent.py +0 -0
  144. {thth-3.3.0 → thth-3.3.1}/tests/test_maintain.py +0 -0
  145. {thth-3.3.0 → thth-3.3.1}/tests/test_mastodon_adapter.py +0 -0
  146. {thth-3.3.0 → thth-3.3.1}/tests/test_mastodon_visibility.py +0 -0
  147. {thth-3.3.0 → thth-3.3.1}/tests/test_max_per_run.py +0 -0
  148. {thth-3.3.0 → thth-3.3.1}/tests/test_mcp.py +0 -0
  149. {thth-3.3.0 → thth-3.3.1}/tests/test_mcp_robustness.py +0 -0
  150. {thth-3.3.0 → thth-3.3.1}/tests/test_measured.py +0 -0
  151. {thth-3.3.0 → thth-3.3.1}/tests/test_media_accounts.py +0 -0
  152. {thth-3.3.0 → thth-3.3.1}/tests/test_media_next_steps.py +0 -0
  153. {thth-3.3.0 → thth-3.3.1}/tests/test_media_roundtrip.py +0 -0
  154. {thth-3.3.0 → thth-3.3.1}/tests/test_mismatch_diagnostics.py +0 -0
  155. {thth-3.3.0 → thth-3.3.1}/tests/test_n5_reply_null_element.py +0 -0
  156. {thth-3.3.0 → thth-3.3.1}/tests/test_n6_replies_incomplete_ledger.py +0 -0
  157. {thth-3.3.0 → thth-3.3.1}/tests/test_n7_doctor_error_body.py +0 -0
  158. {thth-3.3.0 → thth-3.3.1}/tests/test_no_limits.py +0 -0
  159. {thth-3.3.0 → thth-3.3.1}/tests/test_observation_provenance.py +0 -0
  160. {thth-3.3.0 → thth-3.3.1}/tests/test_operations_handoff.py +0 -0
  161. {thth-3.3.0 → thth-3.3.1}/tests/test_packaging.py +0 -0
  162. {thth-3.3.0 → thth-3.3.1}/tests/test_preview.py +0 -0
  163. {thth-3.3.0 → thth-3.3.1}/tests/test_production_mode.py +0 -0
  164. {thth-3.3.0 → thth-3.3.1}/tests/test_pull_cli.py +0 -0
  165. {thth-3.3.0 → thth-3.3.1}/tests/test_queue.py +0 -0
  166. {thth-3.3.0 → thth-3.3.1}/tests/test_quiet_hours.py +0 -0
  167. {thth-3.3.0 → thth-3.3.1}/tests/test_r2_r4_r5.py +0 -0
  168. {thth-3.3.0 → thth-3.3.1}/tests/test_read_cli_bare_runtimeerror.py +0 -0
  169. {thth-3.3.0 → thth-3.3.1}/tests/test_reason_adoption.py +0 -0
  170. {thth-3.3.0 → thth-3.3.1}/tests/test_redact.py +0 -0
  171. {thth-3.3.0 → thth-3.3.1}/tests/test_redirect_token_leak.py +0 -0
  172. {thth-3.3.0 → thth-3.3.1}/tests/test_refresh.py +0 -0
  173. {thth-3.3.0 → thth-3.3.1}/tests/test_refresh_destination.py +0 -0
  174. {thth-3.3.0 → thth-3.3.1}/tests/test_rejudge_new.py +0 -0
  175. {thth-3.3.0 → thth-3.3.1}/tests/test_rejudge_r2_r3.py +0 -0
  176. {thth-3.3.0 → thth-3.3.1}/tests/test_replies.py +0 -0
  177. {thth-3.3.0 → thth-3.3.1}/tests/test_replies_refresh.py +0 -0
  178. {thth-3.3.0 → thth-3.3.1}/tests/test_repo_lock.py +0 -0
  179. {thth-3.3.0 → thth-3.3.1}/tests/test_repo_sync.py +0 -0
  180. {thth-3.3.0 → thth-3.3.1}/tests/test_report_checkout_import.py +0 -0
  181. {thth-3.3.0 → thth-3.3.1}/tests/test_report_http.py +0 -0
  182. {thth-3.3.0 → thth-3.3.1}/tests/test_report_isolation.py +0 -0
  183. {thth-3.3.0 → thth-3.3.1}/tests/test_report_service.py +0 -0
  184. {thth-3.3.0 → thth-3.3.1}/tests/test_reverse_audit.py +0 -0
  185. {thth-3.3.0 → thth-3.3.1}/tests/test_review_case_id_cluster_m2.py +0 -0
  186. {thth-3.3.0 → thth-3.3.1}/tests/test_review_case_id_cluster_r2.py +0 -0
  187. {thth-3.3.0 → thth-3.3.1}/tests/test_review_cli.py +0 -0
  188. {thth-3.3.0 → thth-3.3.1}/tests/test_review_evidence_account.py +0 -0
  189. {thth-3.3.0 → thth-3.3.1}/tests/test_review_form_spec_and_case_id.py +0 -0
  190. {thth-3.3.0 → thth-3.3.1}/tests/test_review_provenance.py +0 -0
  191. {thth-3.3.0 → thth-3.3.1}/tests/test_review_records.py +0 -0
  192. {thth-3.3.0 → thth-3.3.1}/tests/test_revoke.py +0 -0
  193. {thth-3.3.0 → thth-3.3.1}/tests/test_roundtrip.py +0 -0
  194. {thth-3.3.0 → thth-3.3.1}/tests/test_run_missing_env.py +0 -0
  195. {thth-3.3.0 → thth-3.3.1}/tests/test_run_record_failclosed.py +0 -0
  196. {thth-3.3.0 → thth-3.3.1}/tests/test_runs_collected.py +0 -0
  197. {thth-3.3.0 → thth-3.3.1}/tests/test_runs_read_rows.py +0 -0
  198. {thth-3.3.0 → thth-3.3.1}/tests/test_schedule_multiple.py +0 -0
  199. {thth-3.3.0 → thth-3.3.1}/tests/test_schedule_view.py +0 -0
  200. {thth-3.3.0 → thth-3.3.1}/tests/test_scopes_source_requested.py +0 -0
  201. {thth-3.3.0 → thth-3.3.1}/tests/test_search_engage_targets.py +0 -0
  202. {thth-3.3.0 → thth-3.3.1}/tests/test_search_media_routes.py +0 -0
  203. {thth-3.3.0 → thth-3.3.1}/tests/test_secret_value_redact.py +0 -0
  204. {thth-3.3.0 → thth-3.3.1}/tests/test_secrets_grep.py +0 -0
  205. {thth-3.3.0 → thth-3.3.1}/tests/test_security_small_fixes.py +0 -0
  206. {thth-3.3.0 → thth-3.3.1}/tests/test_select.py +0 -0
  207. {thth-3.3.0 → thth-3.3.1}/tests/test_selfupdate.py +0 -0
  208. {thth-3.3.0 → thth-3.3.1}/tests/test_send.py +0 -0
  209. {thth-3.3.0 → thth-3.3.1}/tests/test_send_cli.py +0 -0
  210. {thth-3.3.0 → thth-3.3.1}/tests/test_send_control_chars.py +0 -0
  211. {thth-3.3.0 → thth-3.3.1}/tests/test_send_media.py +0 -0
  212. {thth-3.3.0 → thth-3.3.1}/tests/test_send_visibility.py +0 -0
  213. {thth-3.3.0 → thth-3.3.1}/tests/test_sent_integrity.py +0 -0
  214. {thth-3.3.0 → thth-3.3.1}/tests/test_server_json.py +0 -0
  215. {thth-3.3.0 → thth-3.3.1}/tests/test_site.py +0 -0
  216. {thth-3.3.0 → thth-3.3.1}/tests/test_skill_doc.py +0 -0
  217. {thth-3.3.0 → thth-3.3.1}/tests/test_study_report.py +0 -0
  218. {thth-3.3.0 → thth-3.3.1}/tests/test_systemd.py +0 -0
  219. {thth-3.3.0 → thth-3.3.1}/tests/test_thread_publish.py +0 -0
  220. {thth-3.3.0 → thth-3.3.1}/tests/test_thread_read.py +0 -0
  221. {thth-3.3.0 → thth-3.3.1}/tests/test_threadrun.py +0 -0
  222. {thth-3.3.0 → thth-3.3.1}/tests/test_threads_read_permissions.py +0 -0
  223. {thth-3.3.0 → thth-3.3.1}/tests/test_threadshape.py +0 -0
  224. {thth-3.3.0 → thth-3.3.1}/tests/test_thth_root.py +0 -0
  225. {thth-3.3.0 → thth-3.3.1}/tests/test_token_exists.py +0 -0
  226. {thth-3.3.0 → thth-3.3.1}/tests/test_token_scopes_record.py +0 -0
  227. {thth-3.3.0 → thth-3.3.1}/tests/test_token_set.py +0 -0
  228. {thth-3.3.0 → thth-3.3.1}/tests/test_topic.py +0 -0
  229. {thth-3.3.0 → thth-3.3.1}/tests/test_topic_advice.py +0 -0
  230. {thth-3.3.0 → thth-3.3.1}/tests/test_topic_cli.py +0 -0
  231. {thth-3.3.0 → thth-3.3.1}/tests/test_topic_models.py +0 -0
  232. {thth-3.3.0 → thth-3.3.1}/tests/test_topic_review_fixes.py +0 -0
  233. {thth-3.3.0 → thth-3.3.1}/tests/test_topic_shelf_observers.py +0 -0
  234. {thth-3.3.0 → thth-3.3.1}/tests/test_topic_store.py +0 -0
  235. {thth-3.3.0 → thth-3.3.1}/tests/test_topics_record_concurrent.py +0 -0
  236. {thth-3.3.0 → thth-3.3.1}/tests/test_topics_tool.py +0 -0
  237. {thth-3.3.0 → thth-3.3.1}/tests/test_trial_frictions.py +0 -0
  238. {thth-3.3.0 → thth-3.3.1}/tests/test_v210_admin_transaction.py +0 -0
  239. {thth-3.3.0 → thth-3.3.1}/tests/test_v210_approve_account.py +0 -0
  240. {thth-3.3.0 → thth-3.3.1}/tests/test_v210_length.py +0 -0
  241. {thth-3.3.0 → thth-3.3.1}/tests/test_v210_lock_wait.py +0 -0
  242. {thth-3.3.0 → thth-3.3.1}/tests/test_v210_mastodon_scopes.py +0 -0
  243. {thth-3.3.0 → thth-3.3.1}/tests/test_v210_mentions.py +0 -0
  244. {thth-3.3.0 → thth-3.3.1}/tests/test_v210_post_urls.py +0 -0
  245. {thth-3.3.0 → thth-3.3.1}/tests/test_v210_repo_paths.py +0 -0
  246. {thth-3.3.0 → thth-3.3.1}/tests/test_v210_reposts.py +0 -0
  247. {thth-3.3.0 → thth-3.3.1}/tests/test_v210_study_add.py +0 -0
  248. {thth-3.3.0 → thth-3.3.1}/tests/test_v210_tool_version.py +0 -0
  249. {thth-3.3.0 → thth-3.3.1}/tests/test_v210_unanswered.py +0 -0
  250. {thth-3.3.0 → thth-3.3.1}/tests/test_v210_unanswered_scope.py +0 -0
  251. {thth-3.3.0 → thth-3.3.1}/tests/test_v210_where_arguments.py +0 -0
  252. {thth-3.3.0 → thth-3.3.1}/tests/test_v210_where_filters.py +0 -0
  253. {thth-3.3.0 → thth-3.3.1}/tests/test_v210_who_arguments.py +0 -0
  254. {thth-3.3.0 → thth-3.3.1}/tests/test_v211_auth_integration.py +0 -0
  255. {thth-3.3.0 → thth-3.3.1}/tests/test_v211_authflow.py +0 -0
  256. {thth-3.3.0 → thth-3.3.1}/tests/test_v211_bluesky_stdin.py +0 -0
  257. {thth-3.3.0 → thth-3.3.1}/tests/test_v211_doctor_binding.py +0 -0
  258. {thth-3.3.0 → thth-3.3.1}/tests/test_v211_mastodon_auth.py +0 -0
  259. {thth-3.3.0 → thth-3.3.1}/tests/test_v211_x_auth.py +0 -0
  260. {thth-3.3.0 → thth-3.3.1}/tests/test_v212_approval_relay.py +0 -0
  261. {thth-3.3.0 → thth-3.3.1}/tests/test_v212_ask_coordination.py +0 -0
  262. {thth-3.3.0 → thth-3.3.1}/tests/test_v212_budget.py +0 -0
  263. {thth-3.3.0 → thth-3.3.1}/tests/test_v212_budget_auth.py +0 -0
  264. {thth-3.3.0 → thth-3.3.1}/tests/test_v212_budget_mcp.py +0 -0
  265. {thth-3.3.0 → thth-3.3.1}/tests/test_v212_deletion.py +0 -0
  266. {thth-3.3.0 → thth-3.3.1}/tests/test_v212_job_completion_fault.py +0 -0
  267. {thth-3.3.0 → thth-3.3.1}/tests/test_v212_leave.py +0 -0
  268. {thth-3.3.0 → thth-3.3.1}/tests/test_v212_leave_gate.py +0 -0
  269. {thth-3.3.0 → thth-3.3.1}/tests/test_v212_leave_providers.py +0 -0
  270. {thth-3.3.0 → thth-3.3.1}/tests/test_v212_leave_token_safety.py +0 -0
  271. {thth-3.3.0 → thth-3.3.1}/tests/test_v212_mastodon_guide.py +0 -0
  272. {thth-3.3.0 → thth-3.3.1}/tests/test_v212_mcp_compat.py +0 -0
  273. {thth-3.3.0 → thth-3.3.1}/tests/test_v212_missing_token.py +0 -0
  274. {thth-3.3.0 → thth-3.3.1}/tests/test_v212_read_coordination.py +0 -0
  275. {thth-3.3.0 → thth-3.3.1}/tests/test_v212_server_writes.py +0 -0
  276. {thth-3.3.0 → thth-3.3.1}/tests/test_v212_token_parent.py +0 -0
  277. {thth-3.3.0 → thth-3.3.1}/tests/test_v212_write_storage.py +0 -0
  278. {thth-3.3.0 → thth-3.3.1}/tests/test_v2131_send_dry_run.py +0 -0
  279. {thth-3.3.0 → thth-3.3.1}/tests/test_v2131_threads_publish_wait.py +0 -0
  280. {thth-3.3.0 → thth-3.3.1}/tests/test_v2132_send_inflight_hint.py +0 -0
  281. {thth-3.3.0 → thth-3.3.1}/tests/test_v2132_threads_create_timeout.py +0 -0
  282. {thth-3.3.0 → thth-3.3.1}/tests/test_v2132_threads_create_unknown.py +0 -0
  283. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_acceptance_fixes.py +0 -0
  284. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_adts_asf.py +0 -0
  285. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_approval_attachments.py +0 -0
  286. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_attachment_kinds.py +0 -0
  287. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_bluesky_external.py +0 -0
  288. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_bluesky_media.py +0 -0
  289. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_bluesky_metadata.py +0 -0
  290. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_bluesky_metadata_uri.py +0 -0
  291. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_bluesky_metadata_video.py +0 -0
  292. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_bluesky_record.py +0 -0
  293. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_bluesky_record_video.py +0 -0
  294. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_bluesky_video.py +0 -0
  295. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_bluesky_webm_named.py +0 -0
  296. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_bmff_typed_metadata.py +0 -0
  297. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_budget_lock.py +0 -0
  298. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_capabilities.py +0 -0
  299. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_carryover_timeout.py +0 -0
  300. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_graphemes.py +0 -0
  301. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_mastodon_auth_media.py +0 -0
  302. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_mastodon_bmff_audio.py +0 -0
  303. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_mastodon_flac.py +0 -0
  304. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_mastodon_gif.py +0 -0
  305. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_mastodon_link.py +0 -0
  306. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_mastodon_media.py +0 -0
  307. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_mastodon_mp3.py +0 -0
  308. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_mastodon_ogg_opus.py +0 -0
  309. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_mastodon_ogg_vorbis.py +0 -0
  310. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_mastodon_poll.py +0 -0
  311. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_mastodon_primary_size_boundary.py +0 -0
  312. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_mastodon_quote.py +0 -0
  313. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_mastodon_thumbnail.py +0 -0
  314. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_mastodon_wav.py +0 -0
  315. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_mastodon_webm_audio.py +0 -0
  316. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_mastodon_webm_video.py +0 -0
  317. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_media_cleanup.py +0 -0
  318. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_media_formats.py +0 -0
  319. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_media_foundation.py +0 -0
  320. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_media_prepared.py +0 -0
  321. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_media_relay.py +0 -0
  322. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_media_relay_refusals.py +0 -0
  323. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_media_upload.py +0 -0
  324. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_receipt_deadline.py +0 -0
  325. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_review_followups.py +0 -0
  326. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_stage810_audit.py +0 -0
  327. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_static_diagnostics.py +0 -0
  328. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_stop_observation.py +0 -0
  329. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_systemd_worker.py +0 -0
  330. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_threads_ghost.py +0 -0
  331. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_threads_gif.py +0 -0
  332. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_threads_link.py +0 -0
  333. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_threads_media.py +0 -0
  334. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_threads_media_spoiler.py +0 -0
  335. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_threads_poll.py +0 -0
  336. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_threads_quote.py +0 -0
  337. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_threads_quote_media.py +0 -0
  338. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_threads_replies.py +0 -0
  339. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_threads_text.py +0 -0
  340. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_threads_text_spoiler.py +0 -0
  341. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_threads_video.py +0 -0
  342. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_transport_review.py +0 -0
  343. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_video_typed_metadata.py +0 -0
  344. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_vorbis_comment_boundary.py +0 -0
  345. {thth-3.3.0 → thth-3.3.1}/tests/test_v213_webm_schema_boundary.py +0 -0
  346. {thth-3.3.0 → thth-3.3.1}/tests/test_v2141_draft_lint_reason.py +0 -0
  347. {thth-3.3.0 → thth-3.3.1}/tests/test_v2141_managed_repo_umask.py +0 -0
  348. {thth-3.3.0 → thth-3.3.1}/tests/test_v214_release_signing.py +0 -0
  349. {thth-3.3.0 → thth-3.3.1}/tests/test_v214_x_adapter.py +0 -0
  350. {thth-3.3.0 → thth-3.3.1}/tests/test_v214_x_auth.py +0 -0
  351. {thth-3.3.0 → thth-3.3.1}/tests/test_v214_x_media.py +0 -0
  352. {thth-3.3.0 → thth-3.3.1}/tests/test_v214_x_posts_budget.py +0 -0
  353. {thth-3.3.0 → thth-3.3.1}/tests/test_v21b_approved_writes.py +0 -0
  354. {thth-3.3.0 → thth-3.3.1}/tests/test_v28_tags.py +0 -0
  355. {thth-3.3.0 → thth-3.3.1}/tests/test_v310_morning.py +0 -0
  356. {thth-3.3.0 → thth-3.3.1}/tests/test_v310_search_timeout.py +0 -0
  357. {thth-3.3.0 → thth-3.3.1}/tests/test_v310_watch_words.py +0 -0
  358. {thth-3.3.0 → thth-3.3.1}/tests/test_v311_leave_same_user.py +0 -0
  359. {thth-3.3.0 → thth-3.3.1}/tests/test_v311_mastodon_retract.py +0 -0
  360. {thth-3.3.0 → thth-3.3.1}/tests/test_v311_morning_overdue.py +0 -0
  361. {thth-3.3.0 → thth-3.3.1}/tests/test_v311_morning_own_exclusion.py +0 -0
  362. {thth-3.3.0 → thth-3.3.1}/tests/test_v311_morning_unanswered_window.py +0 -0
  363. {thth-3.3.0 → thth-3.3.1}/tests/test_v311_notifications_guidance.py +0 -0
  364. {thth-3.3.0 → thth-3.3.1}/tests/test_v311_replies_owned.py +0 -0
  365. {thth-3.3.0 → thth-3.3.1}/tests/test_v312_leave_shared_token.py +0 -0
  366. {thth-3.3.0 → thth-3.3.1}/tests/test_v312_own_handles_stopped.py +0 -0
  367. {thth-3.3.0 → thth-3.3.1}/tests/test_v312_report_admin.py +0 -0
  368. {thth-3.3.0 → thth-3.3.1}/tests/test_v312_report_channel.py +0 -0
  369. {thth-3.3.0 → thth-3.3.1}/tests/test_v312_report_inbox.py +0 -0
  370. {thth-3.3.0 → thth-3.3.1}/tests/test_v312_report_mcp_user.py +0 -0
  371. {thth-3.3.0 → thth-3.3.1}/tests/test_v31_analysis.py +0 -0
  372. {thth-3.3.0 → thth-3.3.1}/tests/test_v31_collection.py +0 -0
  373. {thth-3.3.0 → thth-3.3.1}/tests/test_v320_reply_to_file_display.py +0 -0
  374. {thth-3.3.0 → thth-3.3.1}/tests/test_v320_reply_to_file_lint.py +0 -0
  375. {thth-3.3.0 → thth-3.3.1}/tests/test_v320_reply_to_file_resolve.py +0 -0
  376. {thth-3.3.0 → thth-3.3.1}/tests/test_v320_report_fixes.py +0 -0
  377. {thth-3.3.0 → thth-3.3.1}/tests/test_v330_fingerprint_version.py +0 -0
  378. {thth-3.3.0 → thth-3.3.1}/tests/test_v330_held.py +0 -0
  379. {thth-3.3.0 → thth-3.3.1}/tests/test_v330_held_names.py +0 -0
  380. {thth-3.3.0 → thth-3.3.1}/tests/test_v330_last_refusals.py +0 -0
  381. {thth-3.3.0 → thth-3.3.1}/tests/test_v330_notification_route.py +0 -0
  382. {thth-3.3.0 → thth-3.3.1}/tests/test_v330_observe.py +0 -0
  383. {thth-3.3.0 → thth-3.3.1}/tests/test_v330_report_friction.py +0 -0
  384. {thth-3.3.0 → thth-3.3.1}/tests/test_v330_report_words.py +0 -0
  385. {thth-3.3.0 → thth-3.3.1}/tests/test_v330_reports_effect.py +0 -0
  386. {thth-3.3.0 → thth-3.3.1}/tests/test_value_domains_shown.py +0 -0
  387. {thth-3.3.0 → thth-3.3.1}/tests/test_version.py +0 -0
  388. {thth-3.3.0 → thth-3.3.1}/tests/test_where_cli.py +0 -0
  389. {thth-3.3.0 → thth-3.3.1}/tests/test_who_cli.py +0 -0
  390. {thth-3.3.0 → thth-3.3.1}/tests/test_writeback_rebase.py +0 -0
  391. {thth-3.3.0 → thth-3.3.1}/tests/test_writeback_validate.py +0 -0
  392. {thth-3.3.0 → thth-3.3.1}/tests/test_x_metrics.py +0 -0
  393. {thth-3.3.0 → thth-3.3.1}/thth/__init__.py +0 -0
  394. {thth-3.3.0 → thth-3.3.1}/thth/__main__.py +0 -0
  395. {thth-3.3.0 → thth-3.3.1}/thth/_grapheme_data.py +0 -0
  396. {thth-3.3.0 → thth-3.3.1}/thth/account_cli.py +0 -0
  397. {thth-3.3.0 → thth-3.3.1}/thth/account_report.py +0 -0
  398. {thth-3.3.0 → thth-3.3.1}/thth/accounts.py +0 -0
  399. {thth-3.3.0 → thth-3.3.1}/thth/adapters/__init__.py +0 -0
  400. {thth-3.3.0 → thth-3.3.1}/thth/adapters/auth_mastodon.py +0 -0
  401. {thth-3.3.0 → thth-3.3.1}/thth/adapters/auth_threads.py +0 -0
  402. {thth-3.3.0 → thth-3.3.1}/thth/adapters/auth_x.py +0 -0
  403. {thth-3.3.0 → thth-3.3.1}/thth/adapters/bluesky.py +0 -0
  404. {thth-3.3.0 → thth-3.3.1}/thth/adapters/bluesky_media.py +0 -0
  405. {thth-3.3.0 → thth-3.3.1}/thth/adapters/bluesky_video.py +0 -0
  406. {thth-3.3.0 → thth-3.3.1}/thth/adapters/mastodon.py +0 -0
  407. {thth-3.3.0 → thth-3.3.1}/thth/adapters/mastodon_media.py +0 -0
  408. {thth-3.3.0 → thth-3.3.1}/thth/adapters/threads_media.py +0 -0
  409. {thth-3.3.0 → thth-3.3.1}/thth/adapters/x.py +0 -0
  410. {thth-3.3.0 → thth-3.3.1}/thth/adapters/x_media.py +0 -0
  411. {thth-3.3.0 → thth-3.3.1}/thth/admin_notifications.py +0 -0
  412. {thth-3.3.0 → thth-3.3.1}/thth/admin_report.py +0 -0
  413. {thth-3.3.0 → thth-3.3.1}/thth/after_cli.py +0 -0
  414. {thth-3.3.0 → thth-3.3.1}/thth/analytics_comparison.py +0 -0
  415. {thth-3.3.0 → thth-3.3.1}/thth/analytics_report.py +0 -0
  416. {thth-3.3.0 → thth-3.3.1}/thth/analytics_shapes.py +0 -0
  417. {thth-3.3.0 → thth-3.3.1}/thth/analytics_threads.py +0 -0
  418. {thth-3.3.0 → thth-3.3.1}/thth/api_diagnostic.py +0 -0
  419. {thth-3.3.0 → thth-3.3.1}/thth/appconfig.py +0 -0
  420. {thth-3.3.0 → thth-3.3.1}/thth/appenv.py +0 -0
  421. {thth-3.3.0 → thth-3.3.1}/thth/approval.py +0 -0
  422. {thth-3.3.0 → thth-3.3.1}/thth/approval_jobs.py +0 -0
  423. {thth-3.3.0 → thth-3.3.1}/thth/approval_relay.py +0 -0
  424. {thth-3.3.0 → thth-3.3.1}/thth/ask.py +0 -0
  425. {thth-3.3.0 → thth-3.3.1}/thth/ask_cli.py +0 -0
  426. {thth-3.3.0 → thth-3.3.1}/thth/audioformats.py +0 -0
  427. {thth-3.3.0 → thth-3.3.1}/thth/authclients.py +0 -0
  428. {thth-3.3.0 → thth-3.3.1}/thth/authflow.py +0 -0
  429. {thth-3.3.0 → thth-3.3.1}/thth/authpassword.py +0 -0
  430. {thth-3.3.0 → thth-3.3.1}/thth/bluesky_metadata.py +0 -0
  431. {thth-3.3.0 → thth-3.3.1}/thth/budget_x.py +0 -0
  432. {thth-3.3.0 → thth-3.3.1}/thth/budget_x_posts.py +0 -0
  433. {thth-3.3.0 → thth-3.3.1}/thth/bundle.py +0 -0
  434. {thth-3.3.0 → thth-3.3.1}/thth/collect.py +0 -0
  435. {thth-3.3.0 → thth-3.3.1}/thth/collection_context.py +0 -0
  436. {thth-3.3.0 → thth-3.3.1}/thth/collection_status.py +0 -0
  437. {thth-3.3.0 → thth-3.3.1}/thth/deletion.py +0 -0
  438. {thth-3.3.0 → thth-3.3.1}/thth/doctor.py +0 -0
  439. {thth-3.3.0 → thth-3.3.1}/thth/ebmlaudio.py +0 -0
  440. {thth-3.3.0 → thth-3.3.1}/thth/ebmlvideo.py +0 -0
  441. {thth-3.3.0 → thth-3.3.1}/thth/engagements.py +0 -0
  442. {thth-3.3.0 → thth-3.3.1}/thth/fingerprint_golden.py +0 -0
  443. {thth-3.3.0 → thth-3.3.1}/thth/flacformats.py +0 -0
  444. {thth-3.3.0 → thth-3.3.1}/thth/forms.py +0 -0
  445. {thth-3.3.0 → thth-3.3.1}/thth/graphemes.py +0 -0
  446. {thth-3.3.0 → thth-3.3.1}/thth/handoff_cursor.py +0 -0
  447. {thth-3.3.0 → thth-3.3.1}/thth/httpsafe.py +0 -0
  448. {thth-3.3.0 → thth-3.3.1}/thth/jst.py +0 -0
  449. {thth-3.3.0 → thth-3.3.1}/thth/leave.py +0 -0
  450. {thth-3.3.0 → thth-3.3.1}/thth/lint.py +0 -0
  451. {thth-3.3.0 → thth-3.3.1}/thth/lock.py +0 -0
  452. {thth-3.3.0 → thth-3.3.1}/thth/maintain.py +0 -0
  453. {thth-3.3.0 → thth-3.3.1}/thth/managed_repo.py +0 -0
  454. {thth-3.3.0 → thth-3.3.1}/thth/measured.py +0 -0
  455. {thth-3.3.0 → thth-3.3.1}/thth/media.py +0 -0
  456. {thth-3.3.0 → thth-3.3.1}/thth/media_capabilities.py +0 -0
  457. {thth-3.3.0 → thth-3.3.1}/thth/media_cleanup.py +0 -0
  458. {thth-3.3.0 → thth-3.3.1}/thth/media_delivery.py +0 -0
  459. {thth-3.3.0 → thth-3.3.1}/thth/media_relay.py +0 -0
  460. {thth-3.3.0 → thth-3.3.1}/thth/media_uploads.py +0 -0
  461. {thth-3.3.0 → thth-3.3.1}/thth/mediaformats.py +0 -0
  462. {thth-3.3.0 → thth-3.3.1}/thth/mpeg_audio.py +0 -0
  463. {thth-3.3.0 → thth-3.3.1}/thth/notification_route.py +0 -0
  464. {thth-3.3.0 → thth-3.3.1}/thth/oauth.py +0 -0
  465. {thth-3.3.0 → thth-3.3.1}/thth/oggformats.py +0 -0
  466. {thth-3.3.0 → thth-3.3.1}/thth/operations_handoff.py +0 -0
  467. {thth-3.3.0 → thth-3.3.1}/thth/postid.py +0 -0
  468. {thth-3.3.0 → thth-3.3.1}/thth/queuefile.py +0 -0
  469. {thth-3.3.0 → thth-3.3.1}/thth/read_coordination.py +0 -0
  470. {thth-3.3.0 → thth-3.3.1}/thth/read_window.py +0 -0
  471. {thth-3.3.0 → thth-3.3.1}/thth/redact.py +0 -0
  472. {thth-3.3.0 → thth-3.3.1}/thth/refusals.py +0 -0
  473. {thth-3.3.0 → thth-3.3.1}/thth/replies.py +0 -0
  474. {thth-3.3.0 → thth-3.3.1}/thth/report.py +0 -0
  475. {thth-3.3.0 → thth-3.3.1}/thth/report_details.py +0 -0
  476. {thth-3.3.0 → thth-3.3.1}/thth/report_http.py +0 -0
  477. {thth-3.3.0 → thth-3.3.1}/thth/report_inbox.py +0 -0
  478. {thth-3.3.0 → thth-3.3.1}/thth/report_isolation.py +0 -0
  479. {thth-3.3.0 → thth-3.3.1}/thth/report_service.py +0 -0
  480. {thth-3.3.0 → thth-3.3.1}/thth/retract_cli.py +0 -0
  481. {thth-3.3.0 → thth-3.3.1}/thth/scopes.py +0 -0
  482. {thth-3.3.0 → thth-3.3.1}/thth/secrets_fs.py +0 -0
  483. {thth-3.3.0 → thth-3.3.1}/thth/select.py +0 -0
  484. {thth-3.3.0 → thth-3.3.1}/thth/selfupdate.py +0 -0
  485. {thth-3.3.0 → thth-3.3.1}/thth/sent.py +0 -0
  486. {thth-3.3.0 → thth-3.3.1}/thth/server_files.py +0 -0
  487. {thth-3.3.0 → thth-3.3.1}/thth/server_writes.py +0 -0
  488. {thth-3.3.0 → thth-3.3.1}/thth/stop_observation.py +0 -0
  489. {thth-3.3.0 → thth-3.3.1}/thth/study_cli.py +0 -0
  490. {thth-3.3.0 → thth-3.3.1}/thth/study_report.py +0 -0
  491. {thth-3.3.0 → thth-3.3.1}/thth/systemd_gen.py +0 -0
  492. {thth-3.3.0 → thth-3.3.1}/thth/tags.py +0 -0
  493. {thth-3.3.0 → thth-3.3.1}/thth/thread_read.py +0 -0
  494. {thth-3.3.0 → thth-3.3.1}/thth/threadrun.py +0 -0
  495. {thth-3.3.0 → thth-3.3.1}/thth/threads_read_cli.py +0 -0
  496. {thth-3.3.0 → thth-3.3.1}/thth/threadshape.py +0 -0
  497. {thth-3.3.0 → thth-3.3.1}/thth/threadthrow.py +0 -0
  498. {thth-3.3.0 → thth-3.3.1}/thth/tool_version.py +0 -0
  499. {thth-3.3.0 → thth-3.3.1}/thth/topic_advice.py +0 -0
  500. {thth-3.3.0 → thth-3.3.1}/thth/topic_cli.py +0 -0
  501. {thth-3.3.0 → thth-3.3.1}/thth/topic_models.py +0 -0
  502. {thth-3.3.0 → thth-3.3.1}/thth/topic_store.py +0 -0
  503. {thth-3.3.0 → thth-3.3.1}/thth/topics.py +0 -0
  504. {thth-3.3.0 → thth-3.3.1}/thth/unanswered.py +0 -0
  505. {thth-3.3.0 → thth-3.3.1}/thth/vorbisformats.py +0 -0
  506. {thth-3.3.0 → thth-3.3.1}/thth/watch_cli.py +0 -0
  507. {thth-3.3.0 → thth-3.3.1}/thth/where_cli.py +0 -0
  508. {thth-3.3.0 → thth-3.3.1}/thth/who_cli.py +0 -0
  509. {thth-3.3.0 → thth-3.3.1}/thth/writeback.py +0 -0
  510. {thth-3.3.0 → thth-3.3.1}/thth/x_metrics.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: thth
3
- Version: 3.3.0
3
+ Version: 3.3.1
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
@@ -125,7 +125,7 @@ timer(systemd・`thth systemd` で生成)は10分ごとに実行し、投稿
125
125
  - **英語の文書**: [README.en.md](README.en.md)・[docs/usage.en.md](docs/usage.en.md)・[llms.txt](llms.txt)。
126
126
  - **skill**: `skills/thth/SKILL.md`(wheel にも入る)。
127
127
 
128
- - **動くもの**(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`observe`・`morning`・`serve-reports`・`approval-worker`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`pull`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`who`・`retract`・`location`・`notifications`・`report`・`admin`。
128
+ - **動くもの**(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`observe`・`morning`・`serve-reports`・`approval-worker`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`pull`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`who`・`retract`・`inflight`・`location`・`notifications`・`report`・`admin`。
129
129
  - **最初の本番投稿の記録**: 2026-09-09、@aoking に疎通確認を 1 本(`17916074118445631`)。
130
130
  - **未着手**: X・Facebook ページ・Instagram の各アダプタ。トピック検索の権限(tester には降りない)。泉のサーバ(v2-5)。
131
131
  - **権限の制約**: tester に降りる scope は 5 つ。削除はできない。
@@ -101,7 +101,7 @@ timer(systemd・`thth systemd` で生成)は10分ごとに実行し、投稿
101
101
  - **英語の文書**: [README.en.md](README.en.md)・[docs/usage.en.md](docs/usage.en.md)・[llms.txt](llms.txt)。
102
102
  - **skill**: `skills/thth/SKILL.md`(wheel にも入る)。
103
103
 
104
- - **動くもの**(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`observe`・`morning`・`serve-reports`・`approval-worker`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`pull`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`who`・`retract`・`location`・`notifications`・`report`・`admin`。
104
+ - **動くもの**(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`observe`・`morning`・`serve-reports`・`approval-worker`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`pull`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`who`・`retract`・`inflight`・`location`・`notifications`・`report`・`admin`。
105
105
  - **最初の本番投稿の記録**: 2026-09-09、@aoking に疎通確認を 1 本(`17916074118445631`)。
106
106
  - **未着手**: X・Facebook ページ・Instagram の各アダプタ。トピック検索の権限(tester には降りない)。泉のサーバ(v2-5)。
107
107
  - **権限の制約**: tester に降りる scope は 5 つ。削除はできない。
@@ -168,7 +168,7 @@ MCP `after_you_posted`。**24h の刻みが無ければ `null`**(0 と混ぜ
168
168
 
169
169
  新規 account は利用者メール・管理者メール・TLS SMTP を `thth notifications config` で設定し、`status` → `test` を実行する。両宛先の受信箱で到達、account 専用 HEALTHCHECK_URL の missed-ping 通知を確認してから production/scheduled を有効にする。既存 account にも同じ設定を追加する。詳細は `docs/停止通知と運用記録.md`。
170
170
 
171
- プロジェクトの作業開始・再開時は `git fetch` 後にリモート `docs/sns/queue/` の `thth_run_state`・`thth_run_detail`・`thth_run_next` を確認する。dirty worktree へ無条件に pull しない。リモートの確認は `git show origin/main:docs/sns/queue/FILE.md` 等で行える。停止・成否不明なら inflight を消して再投稿せず、示された確認を行う。復旧状態は投稿成功の保証ではなく、公開結果は status/post_id/posted_at を見る。
171
+ プロジェクトの作業開始・再開時は `git fetch` 後にリモート `docs/sns/queue/` の `thth_run_state`・`thth_run_detail`・`thth_run_next` を確認する。dirty worktree へ無条件に pull しない。リモートの確認は `git show origin/main:docs/sns/queue/FILE.md` 等で行える。停止・成否不明なら inflight を手で消して再投稿しない(3.3.1 から道具が毎 run の最初に媒体へ 1 回だけ問い合わせ、決まれば自分で解く)。止まり続けるなら `thth inflight <account> show` で中身を見て、媒体で確かめた人が `thth inflight <account> resolve --not-published|--published <post_id> --by <名前>` で解く(二段確認・MCP には無い)。復旧状態は投稿成功の保証ではなく、公開結果は status/post_id/posted_at を見る。
172
172
 
173
173
  `mentions <account> --json` は 3 媒体の言及を同じ項目で読む。返信前に `replied` の object/false/null を区別する。`unanswered <account> --since 7d --json` は自分の根投稿の未回答候補を台帳で読む。`cannot_say` と採集の古さを確認し、`--refresh` は明示されたときだけ使う。件数は account ごとに扱い、送信は既存の承認・digest・production の門を通す。
174
174
 
@@ -67,6 +67,16 @@ def local_http_test_transport(monkeypatch):
67
67
  opener.open('http://'+host,timeout=0.01)
68
68
 
69
69
 
70
+ @pytest.fixture(autouse=True)
71
+ def no_threads_status_poll_wait(monkeypatch):
72
+ """公開の結果が分からないときの container の問い合わせ(設計 3.3.1 §2)を
73
+ 試験では待たない。本番の既定は 20 秒おき・最大 3 回。偽サーバは status の
74
+ GET に答えない(`query_failed`)ので、従前の試験の結果(inflight を残す)は
75
+ 変わらない——変わるのは待つ時間だけ。subprocess の `bin/thth` にも env で届く。
76
+ """
77
+ monkeypatch.setenv('THTH_THREADS_STATUS_POLL_SECONDS', '0')
78
+
79
+
70
80
  @pytest.fixture(autouse=True)
71
81
  def frozen_now_jst(monkeypatch):
72
82
  """`thth.jst.now_jst()` を既定で静かな時間帯の外(2026-09-09 10:00 JST)に固定する。
@@ -0,0 +1,123 @@
1
+ """container の status を答える偽 Threads(設計 3.3.1 §6・**tests の中だけ**)。
2
+
3
+ 本物の Threads API には触らない。`http.server` で次の 4 つの口だけを真似る:
4
+
5
+ - `POST /<user>/threads` → container を作る(id は `CONTAINER_ID`)
6
+ - `POST /<user>/threads_publish` → `script.publish` の先頭から 1 つずつ答える
7
+ - `GET /<container>` → `script.status` の先頭から 1 つずつ(最後の 1 つは繰り返す)
8
+ - `GET /v1.0/<user>/threads` → `script.posts`(自分の最近の投稿)
9
+
10
+ 何回呼ばれたかを数える(`publish_calls`・`status_calls`・`list_calls`)——
11
+ 「2 度出していない」を回数で確かめるため。
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import contextlib
16
+ import dataclasses
17
+ import http.server
18
+ import json
19
+ import threading
20
+ import urllib.parse
21
+
22
+ from thth.adapters import threads as threads_mod
23
+
24
+ USER_ID = "12345"
25
+ CONTAINER_ID = "900"
26
+
27
+
28
+ @dataclasses.dataclass
29
+ class Script:
30
+ # 公開の答え: ("ok", id) / (500, None) / (400, None) / ("drop", None)(接続断)
31
+ publish: list = dataclasses.field(default_factory=lambda: [("ok", "777")])
32
+ # container の status の答え: "FINISHED" などの語 / 数字(HTTP の失敗)
33
+ status: list = dataclasses.field(default_factory=lambda: ["FINISHED"])
34
+ # 自分の最近の投稿(`GET /v1.0/<user>/threads` の data)
35
+ posts: list = dataclasses.field(default_factory=list)
36
+ list_status: int = 200
37
+ publish_calls: int = 0
38
+ status_calls: int = 0
39
+ list_calls: int = 0
40
+ create_calls: int = 0
41
+ # 公開の要求に載っていた creation_id(どの container を公開したか)。
42
+ publish_ids: list = dataclasses.field(default_factory=list)
43
+
44
+
45
+ def _handler(script: Script):
46
+ class Handler(http.server.BaseHTTPRequestHandler):
47
+ def _json(self, code, payload):
48
+ body = json.dumps(payload).encode("utf-8")
49
+ self.send_response(code)
50
+ self.send_header("Content-Type", "application/json")
51
+ self.send_header("Content-Length", str(len(body)))
52
+ self.end_headers()
53
+ self.wfile.write(body)
54
+
55
+ def do_POST(self): # noqa: N802
56
+ length = int(self.headers.get("Content-Length") or 0)
57
+ raw = self.rfile.read(length) if length else b""
58
+ if self.path.endswith("/threads_publish"):
59
+ script.publish_calls += 1
60
+ params = urllib.parse.parse_qs(raw.decode("utf-8"))
61
+ script.publish_ids.append((params.get("creation_id") or [None])[0])
62
+ kind, value = (script.publish.pop(0) if script.publish else (500, None))
63
+ if kind == "ok":
64
+ self._json(200, {"id": value})
65
+ elif kind == "drop":
66
+ # 応答を返さずに切る(接続断)。
67
+ self.close_connection = True
68
+ self.connection.close()
69
+ else:
70
+ self._json(kind, {"error": {"message": "fake", "code": 1,
71
+ "is_transient": True}})
72
+ return
73
+ script.create_calls += 1
74
+ self._json(200, {"id": CONTAINER_ID})
75
+
76
+ def do_GET(self): # noqa: N802
77
+ path = urllib.parse.urlsplit(self.path).path
78
+ if path == f"/v1.0/{USER_ID}/threads":
79
+ script.list_calls += 1
80
+ if script.list_status != 200:
81
+ self._json(script.list_status, {"error": {"message": "fake"}})
82
+ return
83
+ self._json(200, {"data": list(script.posts)})
84
+ return
85
+ if path == f"/{CONTAINER_ID}":
86
+ script.status_calls += 1
87
+ answer = script.status.pop(0) if len(script.status) > 1 else script.status[0]
88
+ if isinstance(answer, int):
89
+ self._json(answer, {"error": {"message": "fake"}})
90
+ else:
91
+ self._json(200, {"status": answer, "id": CONTAINER_ID})
92
+ return
93
+ self._json(404, {"error": {"message": "not found"}})
94
+
95
+ def log_message(self, format, *args): # noqa: A002
96
+ pass
97
+
98
+ return Handler
99
+
100
+
101
+ @contextlib.contextmanager
102
+ def serve(script: Script):
103
+ server = http.server.HTTPServer(("127.0.0.1", 0), _handler(script))
104
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
105
+ thread.start()
106
+ try:
107
+ yield f"http://127.0.0.1:{server.server_port}"
108
+ finally:
109
+ server.shutdown()
110
+ thread.join(timeout=5)
111
+
112
+
113
+ def adapter(base_url: str, **kwargs) -> threads_mod.ThreadsAdapter:
114
+ kwargs.setdefault("wait_seconds", 0)
115
+ kwargs.setdefault("timeout", 2.0)
116
+ kwargs.setdefault("status_poll_seconds", 0)
117
+ return threads_mod.ThreadsAdapter(base_url=base_url, access_token="fake-token",
118
+ user_id=USER_ID, **kwargs)
119
+
120
+
121
+ def post_row(post_id: str, text: str, timestamp: str) -> dict:
122
+ return {"id": post_id, "text": text, "timestamp": timestamp,
123
+ "permalink": f"https://www.threads.net/@x/post/{post_id}"}
@@ -76,7 +76,8 @@ def test_git_roundtrip_preserves_original_and_deduplicates(setup):
76
76
  assert "thth_run_state: recovered" in s.path.read_text()
77
77
  assert len(s.captured) == 4
78
78
  assert incident._fingerprint(s.path) == before
79
- assert len(row(s)["events"]) == 1
79
+ # 3.3.1 §5: 送り終えた事象も 30 日は残す(以前は 1 件に畳んでいた)。
80
+ assert len(row(s)["events"]) == 2
80
81
 
81
82
 
82
83
  def test_initial_success_is_quiet(setup):
@@ -99,7 +100,8 @@ def test_role_failure_retries_only_unfinished_and_other_role_gets_recovery(setup
99
100
  monkeypatch.setattr(incident, "_send", lambda c, r, e, a: calls.append((r, e["state"])) or True)
100
101
  notify(s, "success", result=False)
101
102
  assert calls[-2:] == [("user@example.org", "blocked"), ("user@example.org", "recovered")]
102
- assert len(row(s)["events"]) == 1
103
+ # 3.3.1 §5: 送り終えた事象も 30 日は残す(以前は 1 件に畳んでいた)。
104
+ assert len(row(s)["events"]) == 2
103
105
 
104
106
 
105
107
  def test_same_address_single_message(setup):
@@ -30,7 +30,9 @@ COMMANDS = '''lint|preview|approve|account|revoke|posts|replies|measured|threads
30
30
  'observe',
31
31
  # 3.1.2: 報告の口(設計 3.1.2 §1)。
32
32
  'report file', 'report list', 'report show', 'report add', 'admin reports list', 'admin reports show',
33
- 'admin reports reply', 'admin reports close', 'admin reports export']
33
+ 'admin reports reply', 'admin reports close', 'admin reports export',
34
+ # 3.3.1 §4: 人の口 thth inflight(show|resolve・--json)。
35
+ 'inflight']
34
36
 
35
37
 
36
38
  def test_json_table_covers_every_parser():
@@ -0,0 +1,156 @@
1
+ """3.3.1 §4: `thth inflight <account> [show|resolve] --by <名前>`(人の口)。
2
+
3
+ 09-23 に masaru が `inflight.json` を手で消した操作の代わり。二段確認・控え
4
+ (`inflight.resolved-<日時>.json`)・変更ログ `inflight_resolved`。MCP には出さない。
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import datetime
9
+ import importlib.util
10
+ import json
11
+ import os
12
+ import pathlib
13
+
14
+ from tests.conftest import init_git_pair, make_queue_text, write_queue_file
15
+ from tests.helpers import fake_threads_status as fake
16
+ from thth import accounts as accounts_mod
17
+ from thth import admin_log
18
+ from thth import cli
19
+ from thth import core
20
+ from thth import inflight as inflight_mod
21
+ from thth import inflight_cli
22
+ from thth import queuefile
23
+ from thth import runs as runs_mod
24
+
25
+ NOW = datetime.datetime.fromisoformat("2026-09-09T10:00:00+09:00")
26
+ REPO = pathlib.Path(__file__).resolve().parent.parent
27
+
28
+
29
+ def _stuck(isolated_account_factory, tmp_path, *, git=True):
30
+ if git:
31
+ pair = init_git_pair(tmp_path, seed_content=make_queue_text(), seed_name="a.md")
32
+ account = isolated_account_factory(repo_dir=pair["work"], production=True)
33
+ else:
34
+ account = isolated_account_factory(production=True)
35
+ write_queue_file(account["queue_dir"], "a.md")
36
+ script = fake.Script(publish=[(500, None)], status=[503])
37
+ with fake.serve(script) as base_url:
38
+ result = core.throw_once(account["name"], production_flag=True,
39
+ adapter_factory=lambda c, t: fake.adapter(base_url), now=NOW)
40
+ assert result.action == "inflight"
41
+ return account, accounts_mod.state_dir_for(account["name"])
42
+
43
+
44
+ def _cli(capsys, *argv):
45
+ rc = cli.main(["inflight", *argv])
46
+ out = capsys.readouterr()
47
+ return rc, out.out, out.err
48
+
49
+
50
+ def _digest_from(out):
51
+ return next(line.split(": ", 1)[1] for line in out.splitlines() if line.startswith("digest: "))
52
+
53
+
54
+ def test_showは中身を出すが本文とtokenとcontainer_idは出さない(isolated_account_factory, tmp_path, capsys):
55
+ account, state_dir = _stuck(isolated_account_factory, tmp_path)
56
+ rc, out, _ = _cli(capsys, account["name"], "show")
57
+ assert rc == 0
58
+ assert "a.md" in out and "container_id: あり" in out and "query_failed" in out
59
+ assert "本文です" not in out and "fake-token" not in out and fake.CONTAINER_ID not in out
60
+ rc, out, _ = _cli(capsys, account["name"], "show", "--json")
61
+ info = json.loads(out)["inflight"]
62
+ assert info["container_id_present"] is True and info["remote_state"] == "query_failed"
63
+ assert info["post_id_present"] is False and info["self_resolvable"] is True
64
+
65
+
66
+ def test_showはinflightが無ければそう言う(isolated_account_factory, capsys):
67
+ account = isolated_account_factory()
68
+ rc, out, _ = _cli(capsys, account["name"])
69
+ assert rc == 0 and "inflight はありません" in out
70
+
71
+
72
+ def test_resolve_not_publishedの往復_二段確認と控えと変更ログ(isolated_account_factory, tmp_path, capsys):
73
+ account, state_dir = _stuck(isolated_account_factory, tmp_path, git=False)
74
+ before = inflight_mod.read(state_dir)
75
+ # 一段目: 何もしない。
76
+ rc, out, _ = _cli(capsys, account["name"], "resolve", "--not-published", "--by", "masaru")
77
+ assert rc == 1 and "解きません" in out
78
+ assert inflight_mod.read(state_dir) == before
79
+ code = _digest_from(out)
80
+ # 二段目。
81
+ rc, out, _ = _cli(capsys, account["name"], "resolve", "--not-published", "--by", "masaru",
82
+ "--confirm", code)
83
+ assert rc == 0, out
84
+ assert inflight_mod.read(state_dir) is None
85
+ archives = inflight_mod.archives(state_dir)
86
+ assert len(archives) == 1
87
+ saved = json.loads(open(archives[0], encoding="utf-8").read())
88
+ assert saved["resolved_by"] == "masaru" and saved["resolution"] == "human_not_published"
89
+ assert saved["inflight"]["started"] == before["started"]
90
+ assert os.stat(archives[0]).st_mode & 0o777 == 0o600
91
+ rows, broken = admin_log.read(account=account["name"], event="inflight_resolved")
92
+ assert len(rows) == 1 and rows[0]["by"] == "masaru" and not broken
93
+ runs = [r for r in runs_mod.read_runs(state_dir) if r.get("inflight_resolution")]
94
+ assert runs[-1]["inflight_resolution"] == "human_not_published"
95
+ # 原稿は approved のまま(次の run が出し直す)。
96
+ qf = queuefile.parse(os.path.join(account["queue_dir"], "a.md"))
97
+ assert qf.get("status") == "approved"
98
+
99
+
100
+ def test_resolve_publishedは書き戻して解く(isolated_account_factory, tmp_path, capsys):
101
+ account, state_dir = _stuck(isolated_account_factory, tmp_path)
102
+ rc, out, _ = _cli(capsys, account["name"], "resolve", "--published", "8001", "--by", "masaru")
103
+ assert rc == 1
104
+ rc, out, err = _cli(capsys, account["name"], "resolve", "--published", "8001", "--by", "masaru",
105
+ "--confirm", _digest_from(out))
106
+ assert rc == 0, err
107
+ assert inflight_mod.read(state_dir) is None
108
+ qf = queuefile.parse(os.path.join(account["queue_dir"], "a.md"))
109
+ assert qf.get("status") == "posted" and str(qf.get("post_id")) == "8001"
110
+ assert len(inflight_mod.archives(state_dir)) == 1
111
+ rows, _ = admin_log.read(account=account["name"], event="inflight_resolved")
112
+ assert rows[-1]["diff"]["resolution"] == [None, "human_published"]
113
+
114
+
115
+ def test_digestが違えば解かない_別のinflightには流用できない(isolated_account_factory, tmp_path, capsys):
116
+ account, state_dir = _stuck(isolated_account_factory, tmp_path, git=False)
117
+ rc, out, _ = _cli(capsys, account["name"], "resolve", "--not-published", "--by", "masaru")
118
+ code = _digest_from(out)
119
+ inflight_mod.update(state_dir, started="2026-09-09T11:00:00+09:00")
120
+ rc, _, err = _cli(capsys, account["name"], "resolve", "--not-published", "--by", "masaru",
121
+ "--confirm", code)
122
+ assert rc == 1 and "confirm_mismatch" in err
123
+ assert inflight_mod.read(state_dir) is not None and inflight_mod.archives(state_dir) == []
124
+
125
+
126
+ def test_byと決めることが無ければ断る(isolated_account_factory, tmp_path, capsys, monkeypatch):
127
+ monkeypatch.delenv("THTH_ACTOR", raising=False)
128
+ account, state_dir = _stuck(isolated_account_factory, tmp_path, git=False)
129
+ rc, _, err = _cli(capsys, account["name"], "resolve", "--not-published")
130
+ assert rc == 1 and err.startswith("by_required")
131
+ rc, _, err = _cli(capsys, account["name"], "resolve", "--by", "masaru")
132
+ assert rc == 1 and err.startswith("decision_required")
133
+ assert inflight_mod.read(state_dir) is not None
134
+
135
+
136
+ def test_出たと分かっているinflightはnot_publishedで解かない(isolated_account_factory, tmp_path, capsys):
137
+ account, state_dir = _stuck(isolated_account_factory, tmp_path, git=False)
138
+ inflight_mod.update(state_dir, post_id="8002")
139
+ rc, _, err = _cli(capsys, account["name"], "resolve", "--not-published", "--by", "masaru")
140
+ assert rc == 1 and err.startswith("published_known")
141
+ rc, _, err = _cli(capsys, account["name"], "resolve", "--published", "9999", "--by", "masaru")
142
+ assert rc == 1 and err.startswith("post_id_differs")
143
+
144
+
145
+ def test_添付のinflightはこの口では解かない(isolated_account_factory, tmp_path, capsys):
146
+ account, state_dir = _stuck(isolated_account_factory, tmp_path, git=False)
147
+ record = inflight_mod.read(state_dir)
148
+ info = inflight_cli.summary({**record, "media": {"account": account["name"]}})
149
+ assert info["media"] is True and info["self_resolvable"] is False
150
+
151
+
152
+ def test_mcpにはinflightの口が無い():
153
+ spec = importlib.util.spec_from_file_location("thth_mcp_server", REPO / "mcp" / "server.py")
154
+ server = importlib.util.module_from_spec(spec)
155
+ spec.loader.exec_module(server)
156
+ assert not any("inflight" in tool["name"] for tool in server.TOOLS)
@@ -0,0 +1,136 @@
1
+ """3.3.1 §3 の後段: container の無い媒体(Mastodon・Bluesky・X)の自己解決。
2
+
3
+ 自分の最近の投稿を公開時刻の前後 10 分で引き、本文の指紋が一致する投稿が
4
+ **ちょうど 1 件**なら書き戻して解く。**0 件なら解かない**——出ていないと
5
+ 言い切れない媒体がある(本文を変えて返す・索引が遅れる)。2 件でも決めない。
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import datetime
10
+ import os
11
+
12
+ from tests.conftest import init_git_pair, make_queue_text
13
+ from thth import accounts as accounts_mod
14
+ from thth import core
15
+ from thth import inflight as inflight_mod
16
+ from thth import queuefile
17
+ from thth import runs as runs_mod
18
+ from thth.adapters import base as adapter_base
19
+
20
+ NOW = datetime.datetime.fromisoformat("2026-09-09T10:00:00+09:00")
21
+ BODY = "## mastodon\n\n本文です。\n"
22
+ NEAR = "2026-09-09T01:02:00.000Z"
23
+ FAR = "2026-09-09T00:40:00.000Z"
24
+
25
+
26
+ class ListOnlyAdapter(adapter_base.Adapter):
27
+ """container を持たない媒体の偽物。公開は「分からない」で終わり、一覧だけ返す。"""
28
+
29
+ CAPABILITIES = frozenset({"recent_posts"})
30
+
31
+ def __init__(self, *, rows=None, fail=False):
32
+ self.rows = rows or []
33
+ self.fail = fail
34
+ self.publish_calls = 0
35
+ self.list_calls = 0
36
+
37
+ def publish(self, post, *, dry_run, on_container_created=None, before_publish=None):
38
+ self.publish_calls += 1
39
+ return adapter_base.PublishResult(None, None, "2026-09-09T10:00:00+09:00",
40
+ error="公開失敗: timed out",
41
+ failure="publish_ambiguous")
42
+
43
+ def recent_posts(self, *, limit=25):
44
+ self.list_calls += 1
45
+ if self.fail:
46
+ raise adapter_base.AdapterError("fake_list_failed")
47
+ return list(self.rows)
48
+
49
+
50
+ def _row(post_id, text, ts):
51
+ return {"post_id": post_id, "timestamp": ts, "url": None, "text": text, "topic": None}
52
+
53
+
54
+ def _stuck(isolated_account_factory, tmp_path):
55
+ seed = make_queue_text(body=BODY, media="mastodon")
56
+ pair = init_git_pair(tmp_path, seed_content=seed, seed_name="a.md")
57
+ account = isolated_account_factory(repo_dir=pair["work"], production=True,
58
+ media="mastodon", instance="https://mastodon.example")
59
+ state_dir = accounts_mod.state_dir_for(account["name"])
60
+ first = ListOnlyAdapter()
61
+ result = core.throw_once(account["name"], production_flag=True,
62
+ adapter_factory=lambda c, t: first, now=NOW)
63
+ assert result.action == "inflight"
64
+ left = inflight_mod.read(state_dir)
65
+ assert left["container_id"] is None and left["text_fingerprint"]
66
+ return account, state_dir
67
+
68
+
69
+ def _again(account, adapter):
70
+ return core.throw_once(account["name"], production_flag=True,
71
+ adapter_factory=lambda c, t: adapter, now=NOW)
72
+
73
+
74
+ def test_一覧に1件あれば書き戻して解き_出し直さない(isolated_account_factory, tmp_path):
75
+ account, state_dir = _stuck(isolated_account_factory, tmp_path)
76
+ adapter = ListOnlyAdapter(rows=[_row("m-1", "本文です。", NEAR),
77
+ _row("m-2", "本文です。", FAR),
78
+ _row("m-3", "別の本文", NEAR)])
79
+ result = _again(account, adapter)
80
+ assert adapter.publish_calls == 0 # 2 度出していない
81
+ assert inflight_mod.read(state_dir) is None
82
+ qf = queuefile.parse(os.path.join(account["queue_dir"], "a.md"))
83
+ assert qf.get("status") == "posted" and qf.get("post_id") == "m-1"
84
+ rows = runs_mod.read_runs(state_dir)
85
+ assert any(r.get("inflight_resolution") == "listing_located" and r["post_id"] == "m-1"
86
+ for r in rows)
87
+ assert len(inflight_mod.archives(state_dir)) == 1
88
+ assert result.action in ("none", "post")
89
+
90
+
91
+ def test_空白や改行を詰め直された本文も同じと見る(isolated_account_factory, tmp_path):
92
+ account, state_dir = _stuck(isolated_account_factory, tmp_path)
93
+ adapter = ListOnlyAdapter(rows=[_row("m-4", "\n本文です。 \n", NEAR)])
94
+ _again(account, adapter)
95
+ assert inflight_mod.read(state_dir) is None
96
+
97
+
98
+ def test_0件なら解かない(isolated_account_factory, tmp_path):
99
+ account, state_dir = _stuck(isolated_account_factory, tmp_path)
100
+ adapter = ListOnlyAdapter(rows=[_row("m-2", "本文です。", FAR)])
101
+ result = _again(account, adapter)
102
+ assert result.action == "inflight" and result.error == "inflight_unresolved"
103
+ assert adapter.publish_calls == 0
104
+ left = inflight_mod.read(state_dir)
105
+ assert left["remote_state"] == "listing_none"
106
+ assert inflight_mod.archives(state_dir) == []
107
+
108
+
109
+ def test_2件なら決めない(isolated_account_factory, tmp_path):
110
+ account, state_dir = _stuck(isolated_account_factory, tmp_path)
111
+ adapter = ListOnlyAdapter(rows=[_row("m-5", "本文です。", NEAR), _row("m-6", "本文です。", NEAR)])
112
+ result = _again(account, adapter)
113
+ assert result.action == "inflight" and adapter.publish_calls == 0
114
+ assert inflight_mod.read(state_dir)["remote_state"] == "listing_many"
115
+
116
+
117
+ def test_一覧が引けなければ解かない(isolated_account_factory, tmp_path):
118
+ account, state_dir = _stuck(isolated_account_factory, tmp_path)
119
+ adapter = ListOnlyAdapter(fail=True)
120
+ result = _again(account, adapter)
121
+ assert result.action == "inflight" and adapter.publish_calls == 0
122
+ assert inflight_mod.read(state_dir)["remote_state"] == "listing_failed"
123
+
124
+
125
+ def test_指紋の無い古いinflightは訊かずに従前どおり止まる(isolated_account_factory, tmp_path):
126
+ account, state_dir = _stuck(isolated_account_factory, tmp_path)
127
+ record = inflight_mod.read(state_dir)
128
+ record.pop("text_fingerprint")
129
+ inflight_mod.clear(state_dir)
130
+ inflight_mod.write(state_dir, **{k: record[k] for k in (
131
+ "file", "started", "container_id", "post_id", "body_hash", "approved_fingerprint")})
132
+ adapter = ListOnlyAdapter(rows=[_row("m-1", "本文です。", NEAR)])
133
+ result = _again(account, adapter)
134
+ assert result.action == "inflight" and result.error is None
135
+ assert adapter.list_calls == 0
136
+
@@ -0,0 +1,135 @@
1
+ """3.3.1 §5: 運用通知の事象を 30 日残し、「最後に送った運用通知」を board と observe に出す。
2
+
3
+ 09-22 の停止の通知は outbox から消えていて、送ったかどうかを翌日に言えなかった。
4
+ 届いたかどうかは道具には分からない(SMTP の受領は到達ではない)——人が受信箱と
5
+ 照合できるよう、時刻・状態・受領した役割を残す。宛先は出さない。
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+
12
+ import pytest
13
+
14
+ from tests.test_incident_notifications import notify, row, setup # noqa: F401 — fixture
15
+ from thth import accounts, cli, incident, morning
16
+
17
+
18
+ def _save_row(s, value):
19
+ incident._save(os.path.join(s.state, incident.STATE_FILE), value)
20
+
21
+
22
+ def test_送り終えた事象を残し最後に送った通知を控える(setup):
23
+ s = setup
24
+ notify(s)
25
+ notify(s, "success", result=False)
26
+ value = row(s)
27
+ assert [e["state"] for e in value["events"]] == ["blocked", "recovered"]
28
+ sent = value["last_sent"]
29
+ assert sent["state"] == "recovered" and sent["roles"] == ["admin", "user"]
30
+ assert sent["reason"] == "healthy" and sent["at"]
31
+ # 宛先は控えない。
32
+ assert "example.org" not in json.dumps(sent)
33
+ summary = incident.summary(s.cfg, s.state)["last_sent"]
34
+ assert summary["basis"] == "recorded" and summary["state"] == "recovered"
35
+
36
+
37
+ def test_30日を過ぎた送り終えた事象は畳む_最後の1件は残す(setup):
38
+ s = setup
39
+ notify(s)
40
+ notify(s, "success", result=False)
41
+ value = row(s)
42
+ for event in value["events"]:
43
+ event["at"] = "2026-07-01T10:00:00+09:00"
44
+ _save_row(s, value)
45
+ notify(s, "success", result=False) # 遷移なし・畳むだけ
46
+ assert len(row(s)["events"]) == 1
47
+ assert row(s)["last_sent"]["state"] == "recovered" # 控えは畳まれても残る
48
+
49
+
50
+ def test_送り終えていない事象は古くても畳まない(setup, monkeypatch):
51
+ s = setup
52
+ notify(s)
53
+ value = row(s)
54
+ value["events"][0]["accepted"] = {}
55
+ value["events"][0]["at"] = "2026-07-01T10:00:00+09:00"
56
+ value["events"].append(dict(value["events"][0], id="f" * 32, accepted={}))
57
+ _save_row(s, value)
58
+ # SMTP が受け付けない(両方失敗)まま流す。
59
+ monkeypatch.setattr(incident, "_send", lambda *a, **k: False)
60
+ notify(s)
61
+ assert len(row(s)["events"]) == 2
62
+
63
+
64
+ def test_30日以内でも数が溢れたら古い順に畳む(setup):
65
+ s = setup
66
+ notify(s)
67
+ value = row(s)
68
+ template = value["events"][0]
69
+ value["events"] = [dict(template, id=f"{i:032x}") for i in range(60)]
70
+ _save_row(s, value)
71
+ notify(s) # 同じ停止(遷移なし)
72
+ assert len(row(s)["events"]) == incident.RETENTION_CAP
73
+
74
+
75
+ def test_控えの無い古いoutboxは残っている事象から言う(setup):
76
+ s = setup
77
+ notify(s)
78
+ value = row(s)
79
+ value.pop("last_sent")
80
+ _save_row(s, value)
81
+ summary = incident.last_sent(row(s))
82
+ assert summary["basis"] == "event_at" and summary["state"] == "blocked"
83
+ assert "事象の時刻" in incident.last_sent_line(summary)
84
+
85
+
86
+ def test_送った記録が無ければ記録なしと言う():
87
+ assert incident.last_sent({"events": []}) is None
88
+ assert "記録なし" in incident.last_sent_line(None)
89
+
90
+
91
+ def test_壊れた控えはoutboxを壊れたものとして扱う(setup):
92
+ s = setup
93
+ notify(s)
94
+ value = row(s)
95
+ value["last_sent"] = {"at": "x", "state": "blocked", "reason": "healthy", "roles": ["user"]}
96
+ with pytest.raises(ValueError):
97
+ incident._validate(value)
98
+ value["last_sent"] = {"at": "2026-09-09T10:00:00+09:00", "state": "blocked",
99
+ "reason": "healthy", "roles": ["someone@example.org"]}
100
+ with pytest.raises(ValueError):
101
+ incident._validate(value)
102
+
103
+
104
+ def _outbox_for(account_name, sent):
105
+ state = accounts.state_dir_for(account_name)
106
+ incident._save(os.path.join(state, incident.STATE_FILE), {
107
+ "version": 1, "last": "success", "events": [], "last_sent": sent})
108
+
109
+
110
+ SENT = {"at": "2026-09-22T20:05:00+09:00", "state": "blocked", "reason": "publish_http_500",
111
+ "event_at": "2026-09-22T20:04:00+09:00", "roles": ["admin", "user"]}
112
+
113
+
114
+ def test_boardに最後に送った運用通知が出る(isolated_account, capsys):
115
+ _outbox_for(isolated_account["name"], SENT)
116
+ assert cli.main(["board"]) == 0
117
+ out = capsys.readouterr().out
118
+ line = next(l for l in out.splitlines() if l.strip().startswith("最後に送った運用通知:"))
119
+ assert "2026-09-22T20:05:00+09:00" in line and "blocked" in line and "admin・user" in line
120
+ assert cli.main(["board", "--json"]) == 0
121
+ payload = json.loads(capsys.readouterr().out)
122
+ acct = next(r for r in payload["accounts"] if r["account"] == isolated_account["name"])
123
+ assert acct["incident_notifications"]["last_sent"]["roles"] == ["admin", "user"]
124
+
125
+
126
+ def test_observeに最後に送った運用通知が出る(isolated_account):
127
+ _outbox_for(isolated_account["name"], SENT)
128
+ payload = morning.build(isolated_account["name"], mark=False)
129
+ today = next(sec for sec in payload["sections"] if sec["section"] == "today")
130
+ node = today["value"]["by_account"][isolated_account["name"]]["value"]
131
+ cell = node["last_notification"]
132
+ assert cell["cannot_say"] is None and cell["value"]["state"] == "blocked"
133
+ lines = []
134
+ morning.render(payload, out=lines.append)
135
+ assert any("最後に送った運用通知" in l and "publish_http_500" in l for l in lines)