thth 4.2.1__tar.gz → 4.3.0__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 (767) hide show
  1. {thth-4.2.1 → thth-4.3.0}/PKG-INFO +3 -1
  2. {thth-4.2.1 → thth-4.3.0}/README.md +2 -0
  3. {thth-4.2.1 → thth-4.3.0}/tests/test_assess.py +10 -6
  4. {thth-4.2.1 → thth-4.3.0}/tests/test_limits_policy.py +2 -1
  5. {thth-4.2.1 → thth-4.3.0}/tests/test_limits_stage2.py +4 -2
  6. {thth-4.2.1 → thth-4.3.0}/tests/test_limits_stage3.py +12 -2
  7. {thth-4.2.1 → thth-4.3.0}/tests/test_v210_json_contract.py +2 -0
  8. {thth-4.2.1 → thth-4.3.0}/tests/test_v3120_activity.py +7 -4
  9. {thth-4.2.1 → thth-4.3.0}/tests/test_v3250_burst_reply_queue.py +34 -10
  10. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s4_bookmark.py +4 -0
  11. thth-4.3.0/tests/test_v422_bookmark_rejections.py +285 -0
  12. thth-4.3.0/tests/test_v422_sparse_checkout.py +148 -0
  13. thth-4.3.0/tests/test_v430_reply_batch.py +486 -0
  14. thth-4.3.0/thth/VERSION +1 -0
  15. {thth-4.2.1 → thth-4.3.0}/thth/admin_log.py +3 -1
  16. {thth-4.2.1 → thth-4.3.0}/thth/assess.py +7 -3
  17. {thth-4.2.1 → thth-4.3.0}/thth/bookmark_batch.py +72 -8
  18. {thth-4.2.1 → thth-4.3.0}/thth/bookmark_cli.py +37 -0
  19. {thth-4.2.1 → thth-4.3.0}/thth/burst_queue.py +8 -5
  20. {thth-4.2.1 → thth-4.3.0}/thth/cli.py +41 -4
  21. {thth-4.2.1 → thth-4.3.0}/thth/collect.py +11 -0
  22. {thth-4.2.1 → thth-4.3.0}/thth/core.py +374 -11
  23. {thth-4.2.1 → thth-4.3.0}/thth/guard.py +72 -22
  24. {thth-4.2.1 → thth-4.3.0}/thth/hatena_bookmark.py +90 -8
  25. {thth-4.2.1 → thth-4.3.0}/thth/hatena_reactions.py +13 -0
  26. {thth-4.2.1 → thth-4.3.0}/thth/limits_config.py +2 -0
  27. {thth-4.2.1 → thth-4.3.0}/thth/limits_policy.py +19 -7
  28. {thth-4.2.1 → thth-4.3.0}/thth/media_traits/bluesky.json +47 -4
  29. {thth-4.2.1 → thth-4.3.0}/thth/media_traits/hatenabookmark.json +13 -0
  30. {thth-4.2.1 → thth-4.3.0}/thth/media_traits/mastodon.json +47 -4
  31. {thth-4.2.1 → thth-4.3.0}/thth/media_traits/threads.json +49 -4
  32. {thth-4.2.1 → thth-4.3.0}/thth/media_traits/x.json +75 -4
  33. {thth-4.2.1 → thth-4.3.0}/thth/media_traits.py +1 -1
  34. {thth-4.2.1 → thth-4.3.0}/thth/select.py +20 -5
  35. {thth-4.2.1 → thth-4.3.0}/thth/server_writes.py +9 -3
  36. {thth-4.2.1 → thth-4.3.0}/thth/writeback.py +75 -0
  37. thth-4.2.1/thth/VERSION +0 -1
  38. {thth-4.2.1 → thth-4.3.0}/.gitignore +0 -0
  39. {thth-4.2.1 → thth-4.3.0}/LICENSE +0 -0
  40. {thth-4.2.1 → thth-4.3.0}/accounts.example/bluesky.json +0 -0
  41. {thth-4.2.1 → thth-4.3.0}/accounts.example/hatenablog.json +0 -0
  42. {thth-4.2.1 → thth-4.3.0}/accounts.example/mastodon.json +0 -0
  43. {thth-4.2.1 → thth-4.3.0}/accounts.example/principal-hatena.json +0 -0
  44. {thth-4.2.1 → thth-4.3.0}/accounts.example/threads.json +0 -0
  45. {thth-4.2.1 → thth-4.3.0}/accounts.example/x.json +0 -0
  46. {thth-4.2.1 → thth-4.3.0}/bin/thth +0 -0
  47. {thth-4.2.1 → thth-4.3.0}/bin/thth-remote +0 -0
  48. {thth-4.2.1 → thth-4.3.0}/bin/thth-run +0 -0
  49. {thth-4.2.1 → thth-4.3.0}/mcp/server.py +0 -0
  50. {thth-4.2.1 → thth-4.3.0}/pyproject.toml +0 -0
  51. {thth-4.2.1 → thth-4.3.0}/skills/thth/SKILL.md +0 -0
  52. {thth-4.2.1 → thth-4.3.0}/skills/thth-admin/SKILL.md +0 -0
  53. {thth-4.2.1 → thth-4.3.0}/systemd/thth@.service +0 -0
  54. {thth-4.2.1 → thth-4.3.0}/tests/__init__.py +0 -0
  55. {thth-4.2.1 → thth-4.3.0}/tests/conftest.py +0 -0
  56. {thth-4.2.1 → thth-4.3.0}/tests/coordination_snapshot.py +0 -0
  57. {thth-4.2.1 → thth-4.3.0}/tests/fixtures/GraphemeBreakTest-17.0.0.txt +0 -0
  58. {thth-4.2.1 → thth-4.3.0}/tests/fixtures/build_injection_eval.py +0 -0
  59. {thth-4.2.1 → thth-4.3.0}/tests/fixtures/callback/worker_responses.json +0 -0
  60. {thth-4.2.1 → thth-4.3.0}/tests/fixtures/injection_eval.json +0 -0
  61. {thth-4.2.1 → thth-4.3.0}/tests/fixtures/limits_stage3/README.txt +0 -0
  62. {thth-4.2.1 → thth-4.3.0}/tests/fixtures/limits_stage3/ledgers/vm-kopicha-hatenablog.json +0 -0
  63. {thth-4.2.1 → thth-4.3.0}/tests/fixtures/limits_stage3/ledgers/vm-kopicha-x.json +0 -0
  64. {thth-4.2.1 → thth-4.3.0}/tests/fixtures/limits_stage3/ledgers/vm-music-threads.json +0 -0
  65. {thth-4.2.1 → thth-4.3.0}/tests/fixtures/limits_stage3/principals/vm-kopicha.json +0 -0
  66. {thth-4.2.1 → thth-4.3.0}/tests/fixtures/twitter_text_v3_weighted.json +0 -0
  67. {thth-4.2.1 → thth-4.3.0}/tests/fixtures/umami-bile.md +0 -0
  68. {thth-4.2.1 → thth-4.3.0}/tests/hatena_s1_flow_helpers.py +0 -0
  69. {thth-4.2.1 → thth-4.3.0}/tests/hatena_s1_helpers.py +0 -0
  70. {thth-4.2.1 → thth-4.3.0}/tests/hatena_s2_helpers.py +0 -0
  71. {thth-4.2.1 → thth-4.3.0}/tests/hatena_s2_http.py +0 -0
  72. {thth-4.2.1 → thth-4.3.0}/tests/hatena_s3_helpers.py +0 -0
  73. {thth-4.2.1 → thth-4.3.0}/tests/hatena_s4_helpers.py +0 -0
  74. {thth-4.2.1 → thth-4.3.0}/tests/hatena_s5_helpers.py +0 -0
  75. {thth-4.2.1 → thth-4.3.0}/tests/hatena_s6_helpers.py +0 -0
  76. {thth-4.2.1 → thth-4.3.0}/tests/helpers/__init__.py +0 -0
  77. {thth-4.2.1 → thth-4.3.0}/tests/helpers/fake_oauth_server.py +0 -0
  78. {thth-4.2.1 → thth-4.3.0}/tests/helpers/fake_push_adapter.py +0 -0
  79. {thth-4.2.1 → thth-4.3.0}/tests/helpers/fake_redirect_server.py +0 -0
  80. {thth-4.2.1 → thth-4.3.0}/tests/helpers/fake_search_adapter.py +0 -0
  81. {thth-4.2.1 → thth-4.3.0}/tests/helpers/fake_threads_reader.py +0 -0
  82. {thth-4.2.1 → thth-4.3.0}/tests/helpers/fake_threads_status.py +0 -0
  83. {thth-4.2.1 → thth-4.3.0}/tests/helpers/fake_threads_writes.py +0 -0
  84. {thth-4.2.1 → thth-4.3.0}/tests/helpers/fake_x.py +0 -0
  85. {thth-4.2.1 → thth-4.3.0}/tests/helpers/hold_lock.py +0 -0
  86. {thth-4.2.1 → thth-4.3.0}/tests/helpers/hold_repo_lock.py +0 -0
  87. {thth-4.2.1 → thth-4.3.0}/tests/helpers/worker_probe.mjs +0 -0
  88. {thth-4.2.1 → thth-4.3.0}/tests/test_27_acceptance_fixes.py +0 -0
  89. {thth-4.2.1 → thth-4.3.0}/tests/test_account_name_check.py +0 -0
  90. {thth-4.2.1 → thth-4.3.0}/tests/test_account_report.py +0 -0
  91. {thth-4.2.1 → thth-4.3.0}/tests/test_accounts_dir.py +0 -0
  92. {thth-4.2.1 → thth-4.3.0}/tests/test_adapter_boundary.py +0 -0
  93. {thth-4.2.1 → thth-4.3.0}/tests/test_admin_http_mcp.py +0 -0
  94. {thth-4.2.1 → thth-4.3.0}/tests/test_admin_log.py +0 -0
  95. {thth-4.2.1 → thth-4.3.0}/tests/test_admin_notifications.py +0 -0
  96. {thth-4.2.1 → thth-4.3.0}/tests/test_admin_repairs.py +0 -0
  97. {thth-4.2.1 → thth-4.3.0}/tests/test_admin_report.py +0 -0
  98. {thth-4.2.1 → thth-4.3.0}/tests/test_after_cli.py +0 -0
  99. {thth-4.2.1 → thth-4.3.0}/tests/test_analytics_comparison.py +0 -0
  100. {thth-4.2.1 → thth-4.3.0}/tests/test_analytics_report.py +0 -0
  101. {thth-4.2.1 → thth-4.3.0}/tests/test_api_diagnostic.py +0 -0
  102. {thth-4.2.1 → thth-4.3.0}/tests/test_api_error_body.py +0 -0
  103. {thth-4.2.1 → thth-4.3.0}/tests/test_app_env_cli.py +0 -0
  104. {thth-4.2.1 → thth-4.3.0}/tests/test_approval.py +0 -0
  105. {thth-4.2.1 → thth-4.3.0}/tests/test_approve_cmd.py +0 -0
  106. {thth-4.2.1 → thth-4.3.0}/tests/test_ask.py +0 -0
  107. {thth-4.2.1 → thth-4.3.0}/tests/test_atlas_fifth_review.py +0 -0
  108. {thth-4.2.1 → thth-4.3.0}/tests/test_atlas_fourth_review.py +0 -0
  109. {thth-4.2.1 → thth-4.3.0}/tests/test_atlas_review.py +0 -0
  110. {thth-4.2.1 → thth-4.3.0}/tests/test_atlas_second_review.py +0 -0
  111. {thth-4.2.1 → thth-4.3.0}/tests/test_atlas_third_review.py +0 -0
  112. {thth-4.2.1 → thth-4.3.0}/tests/test_audit1_fixes.py +0 -0
  113. {thth-4.2.1 → thth-4.3.0}/tests/test_audit2_fixes.py +0 -0
  114. {thth-4.2.1 → thth-4.3.0}/tests/test_audit_b_counterexamples.py +0 -0
  115. {thth-4.2.1 → thth-4.3.0}/tests/test_audit_b_m1_semantics.py +0 -0
  116. {thth-4.2.1 → thth-4.3.0}/tests/test_audit_b_n1_depth.py +0 -0
  117. {thth-4.2.1 → thth-4.3.0}/tests/test_audit_b_r1_closure.py +0 -0
  118. {thth-4.2.1 → thth-4.3.0}/tests/test_audit_v201.py +0 -0
  119. {thth-4.2.1 → thth-4.3.0}/tests/test_audit_v23_details.py +0 -0
  120. {thth-4.2.1 → thth-4.3.0}/tests/test_auth.py +0 -0
  121. {thth-4.2.1 → thth-4.3.0}/tests/test_auth_identity_required.py +0 -0
  122. {thth-4.2.1 → thth-4.3.0}/tests/test_b17_remote_reads.py +0 -0
  123. {thth-4.2.1 → thth-4.3.0}/tests/test_behind_remote.py +0 -0
  124. {thth-4.2.1 → thth-4.3.0}/tests/test_bluesky_adapter.py +0 -0
  125. {thth-4.2.1 → thth-4.3.0}/tests/test_bluesky_secret_scrub.py +0 -0
  126. {thth-4.2.1 → thth-4.3.0}/tests/test_board_needs_review.py +0 -0
  127. {thth-4.2.1 → thth-4.3.0}/tests/test_board_running.py +0 -0
  128. {thth-4.2.1 → thth-4.3.0}/tests/test_branch_replies.py +0 -0
  129. {thth-4.2.1 → thth-4.3.0}/tests/test_bundle.py +0 -0
  130. {thth-4.2.1 → thth-4.3.0}/tests/test_bundle_not_type_mismatch.py +0 -0
  131. {thth-4.2.1 → thth-4.3.0}/tests/test_charcount.py +0 -0
  132. {thth-4.2.1 → thth-4.3.0}/tests/test_cli_entrypoint.py +0 -0
  133. {thth-4.2.1 → thth-4.3.0}/tests/test_cli_help.py +0 -0
  134. {thth-4.2.1 → thth-4.3.0}/tests/test_cli_vm_path.py +0 -0
  135. {thth-4.2.1 → thth-4.3.0}/tests/test_collect.py +0 -0
  136. {thth-4.2.1 → thth-4.3.0}/tests/test_collect_engagements.py +0 -0
  137. {thth-4.2.1 → thth-4.3.0}/tests/test_collect_runs.py +0 -0
  138. {thth-4.2.1 → thth-4.3.0}/tests/test_collect_sent.py +0 -0
  139. {thth-4.2.1 → thth-4.3.0}/tests/test_comparability.py +0 -0
  140. {thth-4.2.1 → thth-4.3.0}/tests/test_control_chars_refused.py +0 -0
  141. {thth-4.2.1 → thth-4.3.0}/tests/test_conversation_paging.py +0 -0
  142. {thth-4.2.1 → thth-4.3.0}/tests/test_count_for.py +0 -0
  143. {thth-4.2.1 → thth-4.3.0}/tests/test_docs_links.py +0 -0
  144. {thth-4.2.1 → thth-4.3.0}/tests/test_doctor.py +0 -0
  145. {thth-4.2.1 → thth-4.3.0}/tests/test_doctor_all_permissions.py +0 -0
  146. {thth-4.2.1 → thth-4.3.0}/tests/test_doctor_next_step.py +0 -0
  147. {thth-4.2.1 → thth-4.3.0}/tests/test_duplicate_front_matter_keys.py +0 -0
  148. {thth-4.2.1 → thth-4.3.0}/tests/test_editorial_audit.py +0 -0
  149. {thth-4.2.1 → thth-4.3.0}/tests/test_editorial_n3_n4.py +0 -0
  150. {thth-4.2.1 → thth-4.3.0}/tests/test_engagements.py +0 -0
  151. {thth-4.2.1 → thth-4.3.0}/tests/test_external_acceptance.py +0 -0
  152. {thth-4.2.1 → thth-4.3.0}/tests/test_fake_api.py +0 -0
  153. {thth-4.2.1 → thth-4.3.0}/tests/test_form_spec.py +0 -0
  154. {thth-4.2.1 → thth-4.3.0}/tests/test_form_spec_cli.py +0 -0
  155. {thth-4.2.1 → thth-4.3.0}/tests/test_forms.py +0 -0
  156. {thth-4.2.1 → thth-4.3.0}/tests/test_fresh_install.py +0 -0
  157. {thth-4.2.1 → thth-4.3.0}/tests/test_front_matter_injection.py +0 -0
  158. {thth-4.2.1 → thth-4.3.0}/tests/test_healthcheck_notifications.py +0 -0
  159. {thth-4.2.1 → thth-4.3.0}/tests/test_hypotheses.py +0 -0
  160. {thth-4.2.1 → thth-4.3.0}/tests/test_hypothesis_boundaries.py +0 -0
  161. {thth-4.2.1 → thth-4.3.0}/tests/test_hypothesis_units.py +0 -0
  162. {thth-4.2.1 → thth-4.3.0}/tests/test_impact_cli.py +0 -0
  163. {thth-4.2.1 → thth-4.3.0}/tests/test_inbox_granted_scopes.py +0 -0
  164. {thth-4.2.1 → thth-4.3.0}/tests/test_incident_notifications.py +0 -0
  165. {thth-4.2.1 → thth-4.3.0}/tests/test_independent_thread_review.py +0 -0
  166. {thth-4.2.1 → thth-4.3.0}/tests/test_independent_topic_review.py +0 -0
  167. {thth-4.2.1 → thth-4.3.0}/tests/test_independent_topic_review_r2.py +0 -0
  168. {thth-4.2.1 → thth-4.3.0}/tests/test_inflight.py +0 -0
  169. {thth-4.2.1 → thth-4.3.0}/tests/test_inflight_tag_body_hash.py +0 -0
  170. {thth-4.2.1 → thth-4.3.0}/tests/test_jst.py +0 -0
  171. {thth-4.2.1 → thth-4.3.0}/tests/test_limits_stage4.py +0 -0
  172. {thth-4.2.1 → thth-4.3.0}/tests/test_lint.py +0 -0
  173. {thth-4.2.1 → thth-4.3.0}/tests/test_list_projections.py +0 -0
  174. {thth-4.2.1 → thth-4.3.0}/tests/test_llm_trial.py +0 -0
  175. {thth-4.2.1 → thth-4.3.0}/tests/test_llm_trial_engage.py +0 -0
  176. {thth-4.2.1 → thth-4.3.0}/tests/test_lock.py +0 -0
  177. {thth-4.2.1 → thth-4.3.0}/tests/test_m2_independent.py +0 -0
  178. {thth-4.2.1 → thth-4.3.0}/tests/test_maintain.py +0 -0
  179. {thth-4.2.1 → thth-4.3.0}/tests/test_mastodon_adapter.py +0 -0
  180. {thth-4.2.1 → thth-4.3.0}/tests/test_mastodon_visibility.py +0 -0
  181. {thth-4.2.1 → thth-4.3.0}/tests/test_max_per_run.py +0 -0
  182. {thth-4.2.1 → thth-4.3.0}/tests/test_mcp.py +0 -0
  183. {thth-4.2.1 → thth-4.3.0}/tests/test_mcp_robustness.py +0 -0
  184. {thth-4.2.1 → thth-4.3.0}/tests/test_measured.py +0 -0
  185. {thth-4.2.1 → thth-4.3.0}/tests/test_media_accounts.py +0 -0
  186. {thth-4.2.1 → thth-4.3.0}/tests/test_media_next_steps.py +0 -0
  187. {thth-4.2.1 → thth-4.3.0}/tests/test_media_roundtrip.py +0 -0
  188. {thth-4.2.1 → thth-4.3.0}/tests/test_media_traits.py +0 -0
  189. {thth-4.2.1 → thth-4.3.0}/tests/test_mismatch_diagnostics.py +0 -0
  190. {thth-4.2.1 → thth-4.3.0}/tests/test_n5_reply_null_element.py +0 -0
  191. {thth-4.2.1 → thth-4.3.0}/tests/test_n6_replies_incomplete_ledger.py +0 -0
  192. {thth-4.2.1 → thth-4.3.0}/tests/test_n7_doctor_error_body.py +0 -0
  193. {thth-4.2.1 → thth-4.3.0}/tests/test_no_limits.py +0 -0
  194. {thth-4.2.1 → thth-4.3.0}/tests/test_observation_provenance.py +0 -0
  195. {thth-4.2.1 → thth-4.3.0}/tests/test_operations_handoff.py +0 -0
  196. {thth-4.2.1 → thth-4.3.0}/tests/test_packaging.py +0 -0
  197. {thth-4.2.1 → thth-4.3.0}/tests/test_preview.py +0 -0
  198. {thth-4.2.1 → thth-4.3.0}/tests/test_production_mode.py +0 -0
  199. {thth-4.2.1 → thth-4.3.0}/tests/test_pull_cli.py +0 -0
  200. {thth-4.2.1 → thth-4.3.0}/tests/test_pulse.py +0 -0
  201. {thth-4.2.1 → thth-4.3.0}/tests/test_queue.py +0 -0
  202. {thth-4.2.1 → thth-4.3.0}/tests/test_quiet_hours.py +0 -0
  203. {thth-4.2.1 → thth-4.3.0}/tests/test_r2_r4_r5.py +0 -0
  204. {thth-4.2.1 → thth-4.3.0}/tests/test_read_cli_bare_runtimeerror.py +0 -0
  205. {thth-4.2.1 → thth-4.3.0}/tests/test_reason_adoption.py +0 -0
  206. {thth-4.2.1 → thth-4.3.0}/tests/test_redact.py +0 -0
  207. {thth-4.2.1 → thth-4.3.0}/tests/test_redirect_token_leak.py +0 -0
  208. {thth-4.2.1 → thth-4.3.0}/tests/test_refresh.py +0 -0
  209. {thth-4.2.1 → thth-4.3.0}/tests/test_refresh_destination.py +0 -0
  210. {thth-4.2.1 → thth-4.3.0}/tests/test_rejudge_new.py +0 -0
  211. {thth-4.2.1 → thth-4.3.0}/tests/test_rejudge_r2_r3.py +0 -0
  212. {thth-4.2.1 → thth-4.3.0}/tests/test_replies.py +0 -0
  213. {thth-4.2.1 → thth-4.3.0}/tests/test_replies_refresh.py +0 -0
  214. {thth-4.2.1 → thth-4.3.0}/tests/test_repo_lock.py +0 -0
  215. {thth-4.2.1 → thth-4.3.0}/tests/test_repo_sync.py +0 -0
  216. {thth-4.2.1 → thth-4.3.0}/tests/test_report_checkout_import.py +0 -0
  217. {thth-4.2.1 → thth-4.3.0}/tests/test_report_http.py +0 -0
  218. {thth-4.2.1 → thth-4.3.0}/tests/test_report_isolation.py +0 -0
  219. {thth-4.2.1 → thth-4.3.0}/tests/test_report_service.py +0 -0
  220. {thth-4.2.1 → thth-4.3.0}/tests/test_retract_already_gone.py +0 -0
  221. {thth-4.2.1 → thth-4.3.0}/tests/test_reverse_audit.py +0 -0
  222. {thth-4.2.1 → thth-4.3.0}/tests/test_review_case_id_cluster_m2.py +0 -0
  223. {thth-4.2.1 → thth-4.3.0}/tests/test_review_case_id_cluster_r2.py +0 -0
  224. {thth-4.2.1 → thth-4.3.0}/tests/test_review_cli.py +0 -0
  225. {thth-4.2.1 → thth-4.3.0}/tests/test_review_evidence_account.py +0 -0
  226. {thth-4.2.1 → thth-4.3.0}/tests/test_review_form_spec_and_case_id.py +0 -0
  227. {thth-4.2.1 → thth-4.3.0}/tests/test_review_provenance.py +0 -0
  228. {thth-4.2.1 → thth-4.3.0}/tests/test_review_records.py +0 -0
  229. {thth-4.2.1 → thth-4.3.0}/tests/test_revoke.py +0 -0
  230. {thth-4.2.1 → thth-4.3.0}/tests/test_roundtrip.py +0 -0
  231. {thth-4.2.1 → thth-4.3.0}/tests/test_run_missing_env.py +0 -0
  232. {thth-4.2.1 → thth-4.3.0}/tests/test_run_record_failclosed.py +0 -0
  233. {thth-4.2.1 → thth-4.3.0}/tests/test_runs_collected.py +0 -0
  234. {thth-4.2.1 → thth-4.3.0}/tests/test_runs_read_rows.py +0 -0
  235. {thth-4.2.1 → thth-4.3.0}/tests/test_schedule_multiple.py +0 -0
  236. {thth-4.2.1 → thth-4.3.0}/tests/test_schedule_view.py +0 -0
  237. {thth-4.2.1 → thth-4.3.0}/tests/test_scopes_source_requested.py +0 -0
  238. {thth-4.2.1 → thth-4.3.0}/tests/test_search_engage_targets.py +0 -0
  239. {thth-4.2.1 → thth-4.3.0}/tests/test_search_media_routes.py +0 -0
  240. {thth-4.2.1 → thth-4.3.0}/tests/test_secret_value_redact.py +0 -0
  241. {thth-4.2.1 → thth-4.3.0}/tests/test_secrets_grep.py +0 -0
  242. {thth-4.2.1 → thth-4.3.0}/tests/test_security_small_fixes.py +0 -0
  243. {thth-4.2.1 → thth-4.3.0}/tests/test_select.py +0 -0
  244. {thth-4.2.1 → thth-4.3.0}/tests/test_selfupdate.py +0 -0
  245. {thth-4.2.1 → thth-4.3.0}/tests/test_send.py +0 -0
  246. {thth-4.2.1 → thth-4.3.0}/tests/test_send_cli.py +0 -0
  247. {thth-4.2.1 → thth-4.3.0}/tests/test_send_control_chars.py +0 -0
  248. {thth-4.2.1 → thth-4.3.0}/tests/test_send_guard_cli.py +0 -0
  249. {thth-4.2.1 → thth-4.3.0}/tests/test_send_media.py +0 -0
  250. {thth-4.2.1 → thth-4.3.0}/tests/test_send_visibility.py +0 -0
  251. {thth-4.2.1 → thth-4.3.0}/tests/test_sent_integrity.py +0 -0
  252. {thth-4.2.1 → thth-4.3.0}/tests/test_server_json.py +0 -0
  253. {thth-4.2.1 → thth-4.3.0}/tests/test_site.py +0 -0
  254. {thth-4.2.1 → thth-4.3.0}/tests/test_skill_doc.py +0 -0
  255. {thth-4.2.1 → thth-4.3.0}/tests/test_study_report.py +0 -0
  256. {thth-4.2.1 → thth-4.3.0}/tests/test_systemd.py +0 -0
  257. {thth-4.2.1 → thth-4.3.0}/tests/test_thread_publish.py +0 -0
  258. {thth-4.2.1 → thth-4.3.0}/tests/test_thread_read.py +0 -0
  259. {thth-4.2.1 → thth-4.3.0}/tests/test_threadrun.py +0 -0
  260. {thth-4.2.1 → thth-4.3.0}/tests/test_threads_read_permissions.py +0 -0
  261. {thth-4.2.1 → thth-4.3.0}/tests/test_threadshape.py +0 -0
  262. {thth-4.2.1 → thth-4.3.0}/tests/test_thth_root.py +0 -0
  263. {thth-4.2.1 → thth-4.3.0}/tests/test_token_exists.py +0 -0
  264. {thth-4.2.1 → thth-4.3.0}/tests/test_token_scopes_record.py +0 -0
  265. {thth-4.2.1 → thth-4.3.0}/tests/test_token_set.py +0 -0
  266. {thth-4.2.1 → thth-4.3.0}/tests/test_topic.py +0 -0
  267. {thth-4.2.1 → thth-4.3.0}/tests/test_topic_advice.py +0 -0
  268. {thth-4.2.1 → thth-4.3.0}/tests/test_topic_cli.py +0 -0
  269. {thth-4.2.1 → thth-4.3.0}/tests/test_topic_models.py +0 -0
  270. {thth-4.2.1 → thth-4.3.0}/tests/test_topic_review_fixes.py +0 -0
  271. {thth-4.2.1 → thth-4.3.0}/tests/test_topic_shelf_observers.py +0 -0
  272. {thth-4.2.1 → thth-4.3.0}/tests/test_topic_store.py +0 -0
  273. {thth-4.2.1 → thth-4.3.0}/tests/test_topics_record_concurrent.py +0 -0
  274. {thth-4.2.1 → thth-4.3.0}/tests/test_topics_tool.py +0 -0
  275. {thth-4.2.1 → thth-4.3.0}/tests/test_trial_frictions.py +0 -0
  276. {thth-4.2.1 → thth-4.3.0}/tests/test_v1015_path_semantics.py +0 -0
  277. {thth-4.2.1 → thth-4.3.0}/tests/test_v1015_take_what_we_can.py +0 -0
  278. {thth-4.2.1 → thth-4.3.0}/tests/test_v210_admin_transaction.py +0 -0
  279. {thth-4.2.1 → thth-4.3.0}/tests/test_v210_approve_account.py +0 -0
  280. {thth-4.2.1 → thth-4.3.0}/tests/test_v210_length.py +0 -0
  281. {thth-4.2.1 → thth-4.3.0}/tests/test_v210_lock_wait.py +0 -0
  282. {thth-4.2.1 → thth-4.3.0}/tests/test_v210_mastodon_scopes.py +0 -0
  283. {thth-4.2.1 → thth-4.3.0}/tests/test_v210_mentions.py +0 -0
  284. {thth-4.2.1 → thth-4.3.0}/tests/test_v210_post_urls.py +0 -0
  285. {thth-4.2.1 → thth-4.3.0}/tests/test_v210_repo_paths.py +0 -0
  286. {thth-4.2.1 → thth-4.3.0}/tests/test_v210_reposts.py +0 -0
  287. {thth-4.2.1 → thth-4.3.0}/tests/test_v210_study_add.py +0 -0
  288. {thth-4.2.1 → thth-4.3.0}/tests/test_v210_tool_version.py +0 -0
  289. {thth-4.2.1 → thth-4.3.0}/tests/test_v210_unanswered.py +0 -0
  290. {thth-4.2.1 → thth-4.3.0}/tests/test_v210_unanswered_scope.py +0 -0
  291. {thth-4.2.1 → thth-4.3.0}/tests/test_v210_where_arguments.py +0 -0
  292. {thth-4.2.1 → thth-4.3.0}/tests/test_v210_where_filters.py +0 -0
  293. {thth-4.2.1 → thth-4.3.0}/tests/test_v210_who_arguments.py +0 -0
  294. {thth-4.2.1 → thth-4.3.0}/tests/test_v211_auth_integration.py +0 -0
  295. {thth-4.2.1 → thth-4.3.0}/tests/test_v211_authflow.py +0 -0
  296. {thth-4.2.1 → thth-4.3.0}/tests/test_v211_bluesky_stdin.py +0 -0
  297. {thth-4.2.1 → thth-4.3.0}/tests/test_v211_doctor_binding.py +0 -0
  298. {thth-4.2.1 → thth-4.3.0}/tests/test_v211_mastodon_auth.py +0 -0
  299. {thth-4.2.1 → thth-4.3.0}/tests/test_v211_x_auth.py +0 -0
  300. {thth-4.2.1 → thth-4.3.0}/tests/test_v212_ask_coordination.py +0 -0
  301. {thth-4.2.1 → thth-4.3.0}/tests/test_v212_budget.py +0 -0
  302. {thth-4.2.1 → thth-4.3.0}/tests/test_v212_budget_auth.py +0 -0
  303. {thth-4.2.1 → thth-4.3.0}/tests/test_v212_budget_mcp.py +0 -0
  304. {thth-4.2.1 → thth-4.3.0}/tests/test_v212_deletion.py +0 -0
  305. {thth-4.2.1 → thth-4.3.0}/tests/test_v212_leave.py +0 -0
  306. {thth-4.2.1 → thth-4.3.0}/tests/test_v212_leave_gate.py +0 -0
  307. {thth-4.2.1 → thth-4.3.0}/tests/test_v212_leave_providers.py +0 -0
  308. {thth-4.2.1 → thth-4.3.0}/tests/test_v212_leave_token_safety.py +0 -0
  309. {thth-4.2.1 → thth-4.3.0}/tests/test_v212_mastodon_guide.py +0 -0
  310. {thth-4.2.1 → thth-4.3.0}/tests/test_v212_mcp_compat.py +0 -0
  311. {thth-4.2.1 → thth-4.3.0}/tests/test_v212_missing_token.py +0 -0
  312. {thth-4.2.1 → thth-4.3.0}/tests/test_v212_read_coordination.py +0 -0
  313. {thth-4.2.1 → thth-4.3.0}/tests/test_v212_relay.py +0 -0
  314. {thth-4.2.1 → thth-4.3.0}/tests/test_v212_server_writes.py +0 -0
  315. {thth-4.2.1 → thth-4.3.0}/tests/test_v212_token_parent.py +0 -0
  316. {thth-4.2.1 → thth-4.3.0}/tests/test_v212_write_storage.py +0 -0
  317. {thth-4.2.1 → thth-4.3.0}/tests/test_v2131_send_dry_run.py +0 -0
  318. {thth-4.2.1 → thth-4.3.0}/tests/test_v2131_threads_publish_wait.py +0 -0
  319. {thth-4.2.1 → thth-4.3.0}/tests/test_v2132_send_inflight_hint.py +0 -0
  320. {thth-4.2.1 → thth-4.3.0}/tests/test_v2132_threads_create_timeout.py +0 -0
  321. {thth-4.2.1 → thth-4.3.0}/tests/test_v2132_threads_create_unknown.py +0 -0
  322. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_acceptance_fixes.py +0 -0
  323. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_adts_asf.py +0 -0
  324. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_attachment_kinds.py +0 -0
  325. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_bluesky_external.py +0 -0
  326. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_bluesky_media.py +0 -0
  327. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_bluesky_metadata.py +0 -0
  328. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_bluesky_metadata_uri.py +0 -0
  329. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_bluesky_metadata_video.py +0 -0
  330. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_bluesky_record.py +0 -0
  331. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_bluesky_record_video.py +0 -0
  332. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_bluesky_video.py +0 -0
  333. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_bluesky_webm_named.py +0 -0
  334. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_bmff_typed_metadata.py +0 -0
  335. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_budget_lock.py +0 -0
  336. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_capabilities.py +0 -0
  337. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_carryover_timeout.py +0 -0
  338. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_graphemes.py +0 -0
  339. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_mastodon_auth_media.py +0 -0
  340. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_mastodon_bmff_audio.py +0 -0
  341. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_mastodon_flac.py +0 -0
  342. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_mastodon_gif.py +0 -0
  343. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_mastodon_link.py +0 -0
  344. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_mastodon_media.py +0 -0
  345. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_mastodon_mp3.py +0 -0
  346. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_mastodon_ogg_opus.py +0 -0
  347. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_mastodon_ogg_vorbis.py +0 -0
  348. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_mastodon_poll.py +0 -0
  349. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_mastodon_primary_size_boundary.py +0 -0
  350. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_mastodon_quote.py +0 -0
  351. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_mastodon_thumbnail.py +0 -0
  352. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_mastodon_wav.py +0 -0
  353. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_mastodon_webm_audio.py +0 -0
  354. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_mastodon_webm_video.py +0 -0
  355. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_media_cleanup.py +0 -0
  356. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_media_formats.py +0 -0
  357. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_media_foundation.py +0 -0
  358. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_media_prepared.py +0 -0
  359. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_media_relay.py +0 -0
  360. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_media_relay_refusals.py +0 -0
  361. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_media_upload.py +0 -0
  362. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_review_followups.py +0 -0
  363. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_stage810_audit.py +0 -0
  364. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_static_diagnostics.py +0 -0
  365. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_stop_observation.py +0 -0
  366. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_systemd_worker.py +0 -0
  367. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_threads_ghost.py +0 -0
  368. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_threads_gif.py +0 -0
  369. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_threads_link.py +0 -0
  370. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_threads_media.py +0 -0
  371. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_threads_media_spoiler.py +0 -0
  372. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_threads_poll.py +0 -0
  373. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_threads_quote.py +0 -0
  374. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_threads_quote_media.py +0 -0
  375. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_threads_replies.py +0 -0
  376. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_threads_text.py +0 -0
  377. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_threads_text_spoiler.py +0 -0
  378. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_threads_video.py +0 -0
  379. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_transport_review.py +0 -0
  380. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_video_typed_metadata.py +0 -0
  381. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_vorbis_comment_boundary.py +0 -0
  382. {thth-4.2.1 → thth-4.3.0}/tests/test_v213_webm_schema_boundary.py +0 -0
  383. {thth-4.2.1 → thth-4.3.0}/tests/test_v2141_draft_lint_reason.py +0 -0
  384. {thth-4.2.1 → thth-4.3.0}/tests/test_v2141_managed_repo_umask.py +0 -0
  385. {thth-4.2.1 → thth-4.3.0}/tests/test_v214_release_signing.py +0 -0
  386. {thth-4.2.1 → thth-4.3.0}/tests/test_v214_x_adapter.py +0 -0
  387. {thth-4.2.1 → thth-4.3.0}/tests/test_v214_x_auth.py +0 -0
  388. {thth-4.2.1 → thth-4.3.0}/tests/test_v214_x_media.py +0 -0
  389. {thth-4.2.1 → thth-4.3.0}/tests/test_v214_x_posts_budget.py +0 -0
  390. {thth-4.2.1 → thth-4.3.0}/tests/test_v21b_approved_writes.py +0 -0
  391. {thth-4.2.1 → thth-4.3.0}/tests/test_v28_tags.py +0 -0
  392. {thth-4.2.1 → thth-4.3.0}/tests/test_v3100_docs.py +0 -0
  393. {thth-4.2.1 → thth-4.3.0}/tests/test_v3100_invite_admin_approval.py +0 -0
  394. {thth-4.2.1 → thth-4.3.0}/tests/test_v3100_invite_create.py +0 -0
  395. {thth-4.2.1 → thth-4.3.0}/tests/test_v3100_invite_credential.py +0 -0
  396. {thth-4.2.1 → thth-4.3.0}/tests/test_v3100_invite_limits.py +0 -0
  397. {thth-4.2.1 → thth-4.3.0}/tests/test_v3100_invite_worker.py +0 -0
  398. {thth-4.2.1 → thth-4.3.0}/tests/test_v310_morning.py +0 -0
  399. {thth-4.2.1 → thth-4.3.0}/tests/test_v310_search_timeout.py +0 -0
  400. {thth-4.2.1 → thth-4.3.0}/tests/test_v310_watch_words.py +0 -0
  401. {thth-4.2.1 → thth-4.3.0}/tests/test_v3111_observe_counts_only.py +0 -0
  402. {thth-4.2.1 → thth-4.3.0}/tests/test_v3111_observe_no_world.py +0 -0
  403. {thth-4.2.1 → thth-4.3.0}/tests/test_v311_leave_same_user.py +0 -0
  404. {thth-4.2.1 → thth-4.3.0}/tests/test_v311_mastodon_retract.py +0 -0
  405. {thth-4.2.1 → thth-4.3.0}/tests/test_v311_morning_overdue.py +0 -0
  406. {thth-4.2.1 → thth-4.3.0}/tests/test_v311_morning_own_exclusion.py +0 -0
  407. {thth-4.2.1 → thth-4.3.0}/tests/test_v311_morning_unanswered_window.py +0 -0
  408. {thth-4.2.1 → thth-4.3.0}/tests/test_v311_notifications_guidance.py +0 -0
  409. {thth-4.2.1 → thth-4.3.0}/tests/test_v311_replies_owned.py +0 -0
  410. {thth-4.2.1 → thth-4.3.0}/tests/test_v3120_admin_write_reasons.py +0 -0
  411. {thth-4.2.1 → thth-4.3.0}/tests/test_v3120_guard.py +0 -0
  412. {thth-4.2.1 → thth-4.3.0}/tests/test_v3120_worker_version.py +0 -0
  413. {thth-4.2.1 → thth-4.3.0}/tests/test_v312_leave_shared_token.py +0 -0
  414. {thth-4.2.1 → thth-4.3.0}/tests/test_v312_own_handles_stopped.py +0 -0
  415. {thth-4.2.1 → thth-4.3.0}/tests/test_v312_report_admin.py +0 -0
  416. {thth-4.2.1 → thth-4.3.0}/tests/test_v312_report_channel.py +0 -0
  417. {thth-4.2.1 → thth-4.3.0}/tests/test_v312_report_inbox.py +0 -0
  418. {thth-4.2.1 → thth-4.3.0}/tests/test_v312_report_mcp_user.py +0 -0
  419. {thth-4.2.1 → thth-4.3.0}/tests/test_v3140_api_relay.py +0 -0
  420. {thth-4.2.1 → thth-4.3.0}/tests/test_v3140_invite_key.py +0 -0
  421. {thth-4.2.1 → thth-4.3.0}/tests/test_v3140_remote_cli.py +0 -0
  422. {thth-4.2.1 → thth-4.3.0}/tests/test_v3140_server_reads.py +0 -0
  423. {thth-4.2.1 → thth-4.3.0}/tests/test_v3142_browser_login.py +0 -0
  424. {thth-4.2.1 → thth-4.3.0}/tests/test_v3143_activity_push.py +0 -0
  425. {thth-4.2.1 → thth-4.3.0}/tests/test_v3143_cli_umask_leave.py +0 -0
  426. {thth-4.2.1 → thth-4.3.0}/tests/test_v3143_inflight_remote.py +0 -0
  427. {thth-4.2.1 → thth-4.3.0}/tests/test_v3143_managed_repo_recover.py +0 -0
  428. {thth-4.2.1 → thth-4.3.0}/tests/test_v315_reactions.py +0 -0
  429. {thth-4.2.1 → thth-4.3.0}/tests/test_v317_run_lock_wait.py +0 -0
  430. {thth-4.2.1 → thth-4.3.0}/tests/test_v3180_json_names.py +0 -0
  431. {thth-4.2.1 → thth-4.3.0}/tests/test_v3180_map_timer.py +0 -0
  432. {thth-4.2.1 → thth-4.3.0}/tests/test_v3180_where_filters.py +0 -0
  433. {thth-4.2.1 → thth-4.3.0}/tests/test_v3180_where_keep.py +0 -0
  434. {thth-4.2.1 → thth-4.3.0}/tests/test_v3192_publish_hold.py +0 -0
  435. {thth-4.2.1 → thth-4.3.0}/tests/test_v31_analysis.py +0 -0
  436. {thth-4.2.1 → thth-4.3.0}/tests/test_v31_collection.py +0 -0
  437. {thth-4.2.1 → thth-4.3.0}/tests/test_v3200_x_weighted_length.py +0 -0
  438. {thth-4.2.1 → thth-4.3.0}/tests/test_v320_reply_to_file_display.py +0 -0
  439. {thth-4.2.1 → thth-4.3.0}/tests/test_v320_reply_to_file_lint.py +0 -0
  440. {thth-4.2.1 → thth-4.3.0}/tests/test_v320_reply_to_file_resolve.py +0 -0
  441. {thth-4.2.1 → thth-4.3.0}/tests/test_v320_report_fixes.py +0 -0
  442. {thth-4.2.1 → thth-4.3.0}/tests/test_v3220_account_scheduled.py +0 -0
  443. {thth-4.2.1 → thth-4.3.0}/tests/test_v3220_bluesky_reply_control.py +0 -0
  444. {thth-4.2.1 → thth-4.3.0}/tests/test_v3220_bluesky_retract.py +0 -0
  445. {thth-4.2.1 → thth-4.3.0}/tests/test_v3220_invite_mastodon.py +0 -0
  446. {thth-4.2.1 → thth-4.3.0}/tests/test_v3220_mentions_inbox.py +0 -0
  447. {thth-4.2.1 → thth-4.3.0}/tests/test_v330_fingerprint_version.py +0 -0
  448. {thth-4.2.1 → thth-4.3.0}/tests/test_v330_held.py +0 -0
  449. {thth-4.2.1 → thth-4.3.0}/tests/test_v330_held_names.py +0 -0
  450. {thth-4.2.1 → thth-4.3.0}/tests/test_v330_last_refusals.py +0 -0
  451. {thth-4.2.1 → thth-4.3.0}/tests/test_v330_notification_route.py +0 -0
  452. {thth-4.2.1 → thth-4.3.0}/tests/test_v330_observe.py +0 -0
  453. {thth-4.2.1 → thth-4.3.0}/tests/test_v330_report_friction.py +0 -0
  454. {thth-4.2.1 → thth-4.3.0}/tests/test_v330_report_words.py +0 -0
  455. {thth-4.2.1 → thth-4.3.0}/tests/test_v330_reports_effect.py +0 -0
  456. {thth-4.2.1 → thth-4.3.0}/tests/test_v331_inflight_cli.py +0 -0
  457. {thth-4.2.1 → thth-4.3.0}/tests/test_v331_listing_resolve.py +0 -0
  458. {thth-4.2.1 → thth-4.3.0}/tests/test_v331_outbox_retention.py +0 -0
  459. {thth-4.2.1 → thth-4.3.0}/tests/test_v331_publish_status.py +0 -0
  460. {thth-4.2.1 → thth-4.3.0}/tests/test_v331_self_resolve.py +0 -0
  461. {thth-4.2.1 → thth-4.3.0}/tests/test_v340_plaza_cli.py +0 -0
  462. {thth-4.2.1 → thth-4.3.0}/tests/test_v340_plaza_docs.py +0 -0
  463. {thth-4.2.1 → thth-4.3.0}/tests/test_v340_plaza_fields.py +0 -0
  464. {thth-4.2.1 → thth-4.3.0}/tests/test_v340_plaza_mcp.py +0 -0
  465. {thth-4.2.1 → thth-4.3.0}/tests/test_v340_plaza_morning.py +0 -0
  466. {thth-4.2.1 → thth-4.3.0}/tests/test_v340_plaza_observe.py +0 -0
  467. {thth-4.2.1 → thth-4.3.0}/tests/test_v340_plaza_open.py +0 -0
  468. {thth-4.2.1 → thth-4.3.0}/tests/test_v340_plaza_open_confirm.py +0 -0
  469. {thth-4.2.1 → thth-4.3.0}/tests/test_v340_plaza_reply.py +0 -0
  470. {thth-4.2.1 → thth-4.3.0}/tests/test_v340_plaza_store.py +0 -0
  471. {thth-4.2.1 → thth-4.3.0}/tests/test_v350_map_docs.py +0 -0
  472. {thth-4.2.1 → thth-4.3.0}/tests/test_v350_map_nodes.py +0 -0
  473. {thth-4.2.1 → thth-4.3.0}/tests/test_v350_map_observe.py +0 -0
  474. {thth-4.2.1 → thth-4.3.0}/tests/test_v350_map_plaza.py +0 -0
  475. {thth-4.2.1 → thth-4.3.0}/tests/test_v350_map_retention.py +0 -0
  476. {thth-4.2.1 → thth-4.3.0}/tests/test_v350_map_self.py +0 -0
  477. {thth-4.2.1 → thth-4.3.0}/tests/test_v350_map_show.py +0 -0
  478. {thth-4.2.1 → thth-4.3.0}/tests/test_v350_map_world.py +0 -0
  479. {thth-4.2.1 → thth-4.3.0}/tests/test_v351_leave_timers.py +0 -0
  480. {thth-4.2.1 → thth-4.3.0}/tests/test_v351_plaza_open_holes.py +0 -0
  481. {thth-4.2.1 → thth-4.3.0}/tests/test_v351_who_owned.py +0 -0
  482. {thth-4.2.1 → thth-4.3.0}/tests/test_v360_goal_analytics.py +0 -0
  483. {thth-4.2.1 → thth-4.3.0}/tests/test_v360_goal_docs.py +0 -0
  484. {thth-4.2.1 → thth-4.3.0}/tests/test_v360_goal_observe_map.py +0 -0
  485. {thth-4.2.1 → thth-4.3.0}/tests/test_v360_goal_plaza.py +0 -0
  486. {thth-4.2.1 → thth-4.3.0}/tests/test_v360_goal_record.py +0 -0
  487. {thth-4.2.1 → thth-4.3.0}/tests/test_v360_goal_send.py +0 -0
  488. {thth-4.2.1 → thth-4.3.0}/tests/test_v360_report_timeline.py +0 -0
  489. {thth-4.2.1 → thth-4.3.0}/tests/test_v370_account_self_healing.py +0 -0
  490. {thth-4.2.1 → thth-4.3.0}/tests/test_v370_approve_pending.py +0 -0
  491. {thth-4.2.1 → thth-4.3.0}/tests/test_v370_click_attribution.py +0 -0
  492. {thth-4.2.1 → thth-4.3.0}/tests/test_v370_collection_stale_basis.py +0 -0
  493. {thth-4.2.1 → thth-4.3.0}/tests/test_v370_docs.py +0 -0
  494. {thth-4.2.1 → thth-4.3.0}/tests/test_v370_follow_comparison.py +0 -0
  495. {thth-4.2.1 → thth-4.3.0}/tests/test_v370_local_path.py +0 -0
  496. {thth-4.2.1 → thth-4.3.0}/tests/test_v370_weekly_goals.py +0 -0
  497. {thth-4.2.1 → thth-4.3.0}/tests/test_v370_where_aggregate.py +0 -0
  498. {thth-4.2.1 → thth-4.3.0}/tests/test_v370_where_also.py +0 -0
  499. {thth-4.2.1 → thth-4.3.0}/tests/test_v370_where_zero.py +0 -0
  500. {thth-4.2.1 → thth-4.3.0}/tests/test_v380_docs.py +0 -0
  501. {thth-4.2.1 → thth-4.3.0}/tests/test_v380_plaza_digest.py +0 -0
  502. {thth-4.2.1 → thth-4.3.0}/tests/test_v380_plaza_from.py +0 -0
  503. {thth-4.2.1 → thth-4.3.0}/tests/test_v380_plaza_mcp.py +0 -0
  504. {thth-4.2.1 → thth-4.3.0}/tests/test_v380_plaza_moments.py +0 -0
  505. {thth-4.2.1 → thth-4.3.0}/tests/test_v380_plaza_owner.py +0 -0
  506. {thth-4.2.1 → thth-4.3.0}/tests/test_v380_plaza_received.py +0 -0
  507. {thth-4.2.1 → thth-4.3.0}/tests/test_v380_plaza_triggers.py +0 -0
  508. {thth-4.2.1 → thth-4.3.0}/tests/test_v381_plaza_visibility_default.py +0 -0
  509. {thth-4.2.1 → thth-4.3.0}/tests/test_v382_confirm_file.py +0 -0
  510. {thth-4.2.1 → thth-4.3.0}/tests/test_v382_first_stage_not_refusal.py +0 -0
  511. {thth-4.2.1 → thth-4.3.0}/tests/test_v382_reader_lock_and_push_pending.py +0 -0
  512. {thth-4.2.1 → thth-4.3.0}/tests/test_v382_sync_without_fetch_head.py +0 -0
  513. {thth-4.2.1 → thth-4.3.0}/tests/test_v390_docs.py +0 -0
  514. {thth-4.2.1 → thth-4.3.0}/tests/test_v390_per_post_clicks.py +0 -0
  515. {thth-4.2.1 → thth-4.3.0}/tests/test_v390_where_rate.py +0 -0
  516. {thth-4.2.1 → thth-4.3.0}/tests/test_v390_where_suggest.py +0 -0
  517. {thth-4.2.1 → thth-4.3.0}/tests/test_v391_plaza_text_numbers.py +0 -0
  518. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_jikki_1017.py +0 -0
  519. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_jikki_1017_bookmark.py +0 -0
  520. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s1_approve.py +0 -0
  521. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s1_deliveries.py +0 -0
  522. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s1_events.py +0 -0
  523. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s1_fingerprints.py +0 -0
  524. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s1_guard.py +0 -0
  525. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s1_locks.py +0 -0
  526. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s1_principals.py +0 -0
  527. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s1_publish.py +0 -0
  528. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s1_remote.py +0 -0
  529. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s2_adapter.py +0 -0
  530. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s2_audit.py +0 -0
  531. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s2_audit_fixes.py +0 -0
  532. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s2_commit.py +0 -0
  533. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s2_convert.py +0 -0
  534. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s2_held.py +0 -0
  535. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s2_oauth1.py +0 -0
  536. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s2_principal_auth.py +0 -0
  537. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s2_syntax_readback.py +0 -0
  538. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s3_adapter.py +0 -0
  539. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s3_convert.py +0 -0
  540. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s3_flow.py +0 -0
  541. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s4_announce.py +0 -0
  542. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s4_audit_fixes.py +0 -0
  543. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s4_ledger.py +0 -0
  544. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s5_audit_fixes.py +0 -0
  545. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s5_conflict.py +0 -0
  546. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s5_detect.py +0 -0
  547. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s5_guard_remote.py +0 -0
  548. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s5_jikki.py +0 -0
  549. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s6_audit_fixes.py +0 -0
  550. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s6_bundle.py +0 -0
  551. {thth-4.2.1 → thth-4.3.0}/tests/test_v400_s6_collect.py +0 -0
  552. {thth-4.2.1 → thth-4.3.0}/tests/test_v401_board_hatena_token.py +0 -0
  553. {thth-4.2.1 → thth-4.3.0}/tests/test_v401_bookmark_burst.py +0 -0
  554. {thth-4.2.1 → thth-4.3.0}/tests/test_v40_contract_json.py +0 -0
  555. {thth-4.2.1 → thth-4.3.0}/tests/test_v410_audit_night.py +0 -0
  556. {thth-4.2.1 → thth-4.3.0}/tests/test_v410_bookmark_batch.py +0 -0
  557. {thth-4.2.1 → thth-4.3.0}/tests/test_v411_batch_cleanup.py +0 -0
  558. {thth-4.2.1 → thth-4.3.0}/tests/test_v420_bookmark_edit.py +0 -0
  559. {thth-4.2.1 → thth-4.3.0}/tests/test_v420_bookmark_tag_bytes.py +0 -0
  560. {thth-4.2.1 → thth-4.3.0}/tests/test_v421_audit_fixes.py +0 -0
  561. {thth-4.2.1 → thth-4.3.0}/tests/test_value_domains_shown.py +0 -0
  562. {thth-4.2.1 → thth-4.3.0}/tests/test_version.py +0 -0
  563. {thth-4.2.1 → thth-4.3.0}/tests/test_where_cli.py +0 -0
  564. {thth-4.2.1 → thth-4.3.0}/tests/test_where_threads_tag_author.py +0 -0
  565. {thth-4.2.1 → thth-4.3.0}/tests/test_who_cli.py +0 -0
  566. {thth-4.2.1 → thth-4.3.0}/tests/test_writeback_rebase.py +0 -0
  567. {thth-4.2.1 → thth-4.3.0}/tests/test_writeback_validate.py +0 -0
  568. {thth-4.2.1 → thth-4.3.0}/tests/test_x_community.py +0 -0
  569. {thth-4.2.1 → thth-4.3.0}/tests/test_x_list.py +0 -0
  570. {thth-4.2.1 → thth-4.3.0}/tests/test_x_metrics.py +0 -0
  571. {thth-4.2.1 → thth-4.3.0}/tests/test_x_owned_metrics.py +0 -0
  572. {thth-4.2.1 → thth-4.3.0}/tests/test_x_search_thread.py +0 -0
  573. {thth-4.2.1 → thth-4.3.0}/tests/test_x_token_autorefresh.py +0 -0
  574. {thth-4.2.1 → thth-4.3.0}/tests/test_x_trends_counts.py +0 -0
  575. {thth-4.2.1 → thth-4.3.0}/thth/__init__.py +0 -0
  576. {thth-4.2.1 → thth-4.3.0}/thth/__main__.py +0 -0
  577. {thth-4.2.1 → thth-4.3.0}/thth/_grapheme_data.py +0 -0
  578. {thth-4.2.1 → thth-4.3.0}/thth/account_cli.py +0 -0
  579. {thth-4.2.1 → thth-4.3.0}/thth/account_report.py +0 -0
  580. {thth-4.2.1 → thth-4.3.0}/thth/account_settings.py +0 -0
  581. {thth-4.2.1 → thth-4.3.0}/thth/accounts.py +0 -0
  582. {thth-4.2.1 → thth-4.3.0}/thth/activity.py +0 -0
  583. {thth-4.2.1 → thth-4.3.0}/thth/adapters/__init__.py +0 -0
  584. {thth-4.2.1 → thth-4.3.0}/thth/adapters/_x_text_data.py +0 -0
  585. {thth-4.2.1 → thth-4.3.0}/thth/adapters/auth_mastodon.py +0 -0
  586. {thth-4.2.1 → thth-4.3.0}/thth/adapters/auth_threads.py +0 -0
  587. {thth-4.2.1 → thth-4.3.0}/thth/adapters/auth_x.py +0 -0
  588. {thth-4.2.1 → thth-4.3.0}/thth/adapters/base.py +0 -0
  589. {thth-4.2.1 → thth-4.3.0}/thth/adapters/bluesky.py +0 -0
  590. {thth-4.2.1 → thth-4.3.0}/thth/adapters/bluesky_media.py +0 -0
  591. {thth-4.2.1 → thth-4.3.0}/thth/adapters/bluesky_video.py +0 -0
  592. {thth-4.2.1 → thth-4.3.0}/thth/adapters/fotolife.py +0 -0
  593. {thth-4.2.1 → thth-4.3.0}/thth/adapters/hatena_common.py +0 -0
  594. {thth-4.2.1 → thth-4.3.0}/thth/adapters/hatenablog.py +0 -0
  595. {thth-4.2.1 → thth-4.3.0}/thth/adapters/hatenabookmark.py +0 -0
  596. {thth-4.2.1 → thth-4.3.0}/thth/adapters/hatenastar.py +0 -0
  597. {thth-4.2.1 → thth-4.3.0}/thth/adapters/mastodon.py +0 -0
  598. {thth-4.2.1 → thth-4.3.0}/thth/adapters/mastodon_media.py +0 -0
  599. {thth-4.2.1 → thth-4.3.0}/thth/adapters/threads.py +0 -0
  600. {thth-4.2.1 → thth-4.3.0}/thth/adapters/threads_media.py +0 -0
  601. {thth-4.2.1 → thth-4.3.0}/thth/adapters/x.py +0 -0
  602. {thth-4.2.1 → thth-4.3.0}/thth/adapters/x_media.py +0 -0
  603. {thth-4.2.1 → thth-4.3.0}/thth/adapters/x_text.py +0 -0
  604. {thth-4.2.1 → thth-4.3.0}/thth/admin_diagnose.py +0 -0
  605. {thth-4.2.1 → thth-4.3.0}/thth/admin_notifications.py +0 -0
  606. {thth-4.2.1 → thth-4.3.0}/thth/admin_report.py +0 -0
  607. {thth-4.2.1 → thth-4.3.0}/thth/advise.py +0 -0
  608. {thth-4.2.1 → thth-4.3.0}/thth/after_cli.py +0 -0
  609. {thth-4.2.1 → thth-4.3.0}/thth/analytics_clicks.py +0 -0
  610. {thth-4.2.1 → thth-4.3.0}/thth/analytics_comparison.py +0 -0
  611. {thth-4.2.1 → thth-4.3.0}/thth/analytics_goals.py +0 -0
  612. {thth-4.2.1 → thth-4.3.0}/thth/analytics_report.py +0 -0
  613. {thth-4.2.1 → thth-4.3.0}/thth/analytics_shapes.py +0 -0
  614. {thth-4.2.1 → thth-4.3.0}/thth/analytics_threads.py +0 -0
  615. {thth-4.2.1 → thth-4.3.0}/thth/analytics_weekly.py +0 -0
  616. {thth-4.2.1 → thth-4.3.0}/thth/announce.py +0 -0
  617. {thth-4.2.1 → thth-4.3.0}/thth/api_diagnostic.py +0 -0
  618. {thth-4.2.1 → thth-4.3.0}/thth/api_requests.py +0 -0
  619. {thth-4.2.1 → thth-4.3.0}/thth/appconfig.py +0 -0
  620. {thth-4.2.1 → thth-4.3.0}/thth/appenv.py +0 -0
  621. {thth-4.2.1 → thth-4.3.0}/thth/approval.py +0 -0
  622. {thth-4.2.1 → thth-4.3.0}/thth/approve_pending.py +0 -0
  623. {thth-4.2.1 → thth-4.3.0}/thth/article_cli.py +0 -0
  624. {thth-4.2.1 → thth-4.3.0}/thth/article_reactions.py +0 -0
  625. {thth-4.2.1 → thth-4.3.0}/thth/ask.py +0 -0
  626. {thth-4.2.1 → thth-4.3.0}/thth/ask_cli.py +0 -0
  627. {thth-4.2.1 → thth-4.3.0}/thth/audioformats.py +0 -0
  628. {thth-4.2.1 → thth-4.3.0}/thth/authclients.py +0 -0
  629. {thth-4.2.1 → thth-4.3.0}/thth/authflow.py +0 -0
  630. {thth-4.2.1 → thth-4.3.0}/thth/authpassword.py +0 -0
  631. {thth-4.2.1 → thth-4.3.0}/thth/bluesky_metadata.py +0 -0
  632. {thth-4.2.1 → thth-4.3.0}/thth/budget_x.py +0 -0
  633. {thth-4.2.1 → thth-4.3.0}/thth/budget_x_posts.py +0 -0
  634. {thth-4.2.1 → thth-4.3.0}/thth/bundle.py +0 -0
  635. {thth-4.2.1 → thth-4.3.0}/thth/click_attribution.py +0 -0
  636. {thth-4.2.1 → thth-4.3.0}/thth/collection_context.py +0 -0
  637. {thth-4.2.1 → thth-4.3.0}/thth/collection_status.py +0 -0
  638. {thth-4.2.1 → thth-4.3.0}/thth/community_cli.py +0 -0
  639. {thth-4.2.1 → thth-4.3.0}/thth/conflict_cli.py +0 -0
  640. {thth-4.2.1 → thth-4.3.0}/thth/deletion.py +0 -0
  641. {thth-4.2.1 → thth-4.3.0}/thth/deliveries.py +0 -0
  642. {thth-4.2.1 → thth-4.3.0}/thth/deliveries_cli.py +0 -0
  643. {thth-4.2.1 → thth-4.3.0}/thth/doctor.py +0 -0
  644. {thth-4.2.1 → thth-4.3.0}/thth/ebmlaudio.py +0 -0
  645. {thth-4.2.1 → thth-4.3.0}/thth/ebmlvideo.py +0 -0
  646. {thth-4.2.1 → thth-4.3.0}/thth/engagements.py +0 -0
  647. {thth-4.2.1 → thth-4.3.0}/thth/events_cli.py +0 -0
  648. {thth-4.2.1 → thth-4.3.0}/thth/fingerprint_golden.py +0 -0
  649. {thth-4.2.1 → thth-4.3.0}/thth/flacformats.py +0 -0
  650. {thth-4.2.1 → thth-4.3.0}/thth/forms.py +0 -0
  651. {thth-4.2.1 → thth-4.3.0}/thth/fotolife_cli.py +0 -0
  652. {thth-4.2.1 → thth-4.3.0}/thth/goals.py +0 -0
  653. {thth-4.2.1 → thth-4.3.0}/thth/graphemes.py +0 -0
  654. {thth-4.2.1 → thth-4.3.0}/thth/handoff_cursor.py +0 -0
  655. {thth-4.2.1 → thth-4.3.0}/thth/hatena_cli.py +0 -0
  656. {thth-4.2.1 → thth-4.3.0}/thth/hatena_conflict.py +0 -0
  657. {thth-4.2.1 → thth-4.3.0}/thth/hatena_convert.py +0 -0
  658. {thth-4.2.1 → thth-4.3.0}/thth/hatena_entry.py +0 -0
  659. {thth-4.2.1 → thth-4.3.0}/thth/hatena_fotolife.py +0 -0
  660. {thth-4.2.1 → thth-4.3.0}/thth/hatena_publish.py +0 -0
  661. {thth-4.2.1 → thth-4.3.0}/thth/healthcheck.py +0 -0
  662. {thth-4.2.1 → thth-4.3.0}/thth/httpsafe.py +0 -0
  663. {thth-4.2.1 → thth-4.3.0}/thth/incident.py +0 -0
  664. {thth-4.2.1 → thth-4.3.0}/thth/inflight.py +0 -0
  665. {thth-4.2.1 → thth-4.3.0}/thth/inflight_cli.py +0 -0
  666. {thth-4.2.1 → thth-4.3.0}/thth/inflight_resolve.py +0 -0
  667. {thth-4.2.1 → thth-4.3.0}/thth/invites.py +0 -0
  668. {thth-4.2.1 → thth-4.3.0}/thth/jst.py +0 -0
  669. {thth-4.2.1 → thth-4.3.0}/thth/leave.py +0 -0
  670. {thth-4.2.1 → thth-4.3.0}/thth/leave_gate.py +0 -0
  671. {thth-4.2.1 → thth-4.3.0}/thth/limits_view.py +0 -0
  672. {thth-4.2.1 → thth-4.3.0}/thth/lint.py +0 -0
  673. {thth-4.2.1 → thth-4.3.0}/thth/list_cli.py +0 -0
  674. {thth-4.2.1 → thth-4.3.0}/thth/lock.py +0 -0
  675. {thth-4.2.1 → thth-4.3.0}/thth/maintain.py +0 -0
  676. {thth-4.2.1 → thth-4.3.0}/thth/managed_repo.py +0 -0
  677. {thth-4.2.1 → thth-4.3.0}/thth/map_cli.py +0 -0
  678. {thth-4.2.1 → thth-4.3.0}/thth/map_store.py +0 -0
  679. {thth-4.2.1 → thth-4.3.0}/thth/map_view.py +0 -0
  680. {thth-4.2.1 → thth-4.3.0}/thth/map_world.py +0 -0
  681. {thth-4.2.1 → thth-4.3.0}/thth/measured.py +0 -0
  682. {thth-4.2.1 → thth-4.3.0}/thth/media.py +0 -0
  683. {thth-4.2.1 → thth-4.3.0}/thth/media_capabilities.py +0 -0
  684. {thth-4.2.1 → thth-4.3.0}/thth/media_cleanup.py +0 -0
  685. {thth-4.2.1 → thth-4.3.0}/thth/media_cli.py +0 -0
  686. {thth-4.2.1 → thth-4.3.0}/thth/media_delivery.py +0 -0
  687. {thth-4.2.1 → thth-4.3.0}/thth/media_observations.py +0 -0
  688. {thth-4.2.1 → thth-4.3.0}/thth/media_relay.py +0 -0
  689. {thth-4.2.1 → thth-4.3.0}/thth/media_traits/fotolife.json +0 -0
  690. {thth-4.2.1 → thth-4.3.0}/thth/media_traits/hatenablog.json +0 -0
  691. {thth-4.2.1 → thth-4.3.0}/thth/media_traits/misskey.json +0 -0
  692. {thth-4.2.1 → thth-4.3.0}/thth/media_uploads.py +0 -0
  693. {thth-4.2.1 → thth-4.3.0}/thth/mediaformats.py +0 -0
  694. {thth-4.2.1 → thth-4.3.0}/thth/morning.py +0 -0
  695. {thth-4.2.1 → thth-4.3.0}/thth/mpeg_audio.py +0 -0
  696. {thth-4.2.1 → thth-4.3.0}/thth/notification_route.py +0 -0
  697. {thth-4.2.1 → thth-4.3.0}/thth/oauth.py +0 -0
  698. {thth-4.2.1 → thth-4.3.0}/thth/oauth1.py +0 -0
  699. {thth-4.2.1 → thth-4.3.0}/thth/oggformats.py +0 -0
  700. {thth-4.2.1 → thth-4.3.0}/thth/operations_handoff.py +0 -0
  701. {thth-4.2.1 → thth-4.3.0}/thth/plaza.py +0 -0
  702. {thth-4.2.1 → thth-4.3.0}/thth/plaza_cli.py +0 -0
  703. {thth-4.2.1 → thth-4.3.0}/thth/plaza_from.py +0 -0
  704. {thth-4.2.1 → thth-4.3.0}/thth/plaza_moments.py +0 -0
  705. {thth-4.2.1 → thth-4.3.0}/thth/plaza_observe.py +0 -0
  706. {thth-4.2.1 → thth-4.3.0}/thth/plaza_reads.py +0 -0
  707. {thth-4.2.1 → thth-4.3.0}/thth/plaza_redact.py +0 -0
  708. {thth-4.2.1 → thth-4.3.0}/thth/postid.py +0 -0
  709. {thth-4.2.1 → thth-4.3.0}/thth/principal_cli.py +0 -0
  710. {thth-4.2.1 → thth-4.3.0}/thth/principals.py +0 -0
  711. {thth-4.2.1 → thth-4.3.0}/thth/private_store.py +0 -0
  712. {thth-4.2.1 → thth-4.3.0}/thth/publish_hold.py +0 -0
  713. {thth-4.2.1 → thth-4.3.0}/thth/pulse.py +0 -0
  714. {thth-4.2.1 → thth-4.3.0}/thth/queuefile.py +0 -0
  715. {thth-4.2.1 → thth-4.3.0}/thth/reaction_cli.py +0 -0
  716. {thth-4.2.1 → thth-4.3.0}/thth/reactions.py +0 -0
  717. {thth-4.2.1 → thth-4.3.0}/thth/read_coordination.py +0 -0
  718. {thth-4.2.1 → thth-4.3.0}/thth/read_window.py +0 -0
  719. {thth-4.2.1 → thth-4.3.0}/thth/redact.py +0 -0
  720. {thth-4.2.1 → thth-4.3.0}/thth/refusals.py +0 -0
  721. {thth-4.2.1 → thth-4.3.0}/thth/relay.py +0 -0
  722. {thth-4.2.1 → thth-4.3.0}/thth/remote.py +0 -0
  723. {thth-4.2.1 → thth-4.3.0}/thth/replies.py +0 -0
  724. {thth-4.2.1 → thth-4.3.0}/thth/report.py +0 -0
  725. {thth-4.2.1 → thth-4.3.0}/thth/report_details.py +0 -0
  726. {thth-4.2.1 → thth-4.3.0}/thth/report_http.py +0 -0
  727. {thth-4.2.1 → thth-4.3.0}/thth/report_inbox.py +0 -0
  728. {thth-4.2.1 → thth-4.3.0}/thth/report_isolation.py +0 -0
  729. {thth-4.2.1 → thth-4.3.0}/thth/report_service.py +0 -0
  730. {thth-4.2.1 → thth-4.3.0}/thth/report_timeline.py +0 -0
  731. {thth-4.2.1 → thth-4.3.0}/thth/retract_cli.py +0 -0
  732. {thth-4.2.1 → thth-4.3.0}/thth/runs.py +0 -0
  733. {thth-4.2.1 → thth-4.3.0}/thth/scopes.py +0 -0
  734. {thth-4.2.1 → thth-4.3.0}/thth/secrets_fs.py +0 -0
  735. {thth-4.2.1 → thth-4.3.0}/thth/selfupdate.py +0 -0
  736. {thth-4.2.1 → thth-4.3.0}/thth/sent.py +0 -0
  737. {thth-4.2.1 → thth-4.3.0}/thth/server_files.py +0 -0
  738. {thth-4.2.1 → thth-4.3.0}/thth/server_reads.py +0 -0
  739. {thth-4.2.1 → thth-4.3.0}/thth/stop_observation.py +0 -0
  740. {thth-4.2.1 → thth-4.3.0}/thth/study_cli.py +0 -0
  741. {thth-4.2.1 → thth-4.3.0}/thth/study_report.py +0 -0
  742. {thth-4.2.1 → thth-4.3.0}/thth/summary_cli.py +0 -0
  743. {thth-4.2.1 → thth-4.3.0}/thth/systemd_gen.py +0 -0
  744. {thth-4.2.1 → thth-4.3.0}/thth/tags.py +0 -0
  745. {thth-4.2.1 → thth-4.3.0}/thth/thread_read.py +0 -0
  746. {thth-4.2.1 → thth-4.3.0}/thth/threadrun.py +0 -0
  747. {thth-4.2.1 → thth-4.3.0}/thth/threads_read_cli.py +0 -0
  748. {thth-4.2.1 → thth-4.3.0}/thth/threadshape.py +0 -0
  749. {thth-4.2.1 → thth-4.3.0}/thth/threadthrow.py +0 -0
  750. {thth-4.2.1 → thth-4.3.0}/thth/timer_cleanup.py +0 -0
  751. {thth-4.2.1 → thth-4.3.0}/thth/tool_version.py +0 -0
  752. {thth-4.2.1 → thth-4.3.0}/thth/topic_advice.py +0 -0
  753. {thth-4.2.1 → thth-4.3.0}/thth/topic_cli.py +0 -0
  754. {thth-4.2.1 → thth-4.3.0}/thth/topic_models.py +0 -0
  755. {thth-4.2.1 → thth-4.3.0}/thth/topic_store.py +0 -0
  756. {thth-4.2.1 → thth-4.3.0}/thth/topics.py +0 -0
  757. {thth-4.2.1 → thth-4.3.0}/thth/trends_cli.py +0 -0
  758. {thth-4.2.1 → thth-4.3.0}/thth/unanswered.py +0 -0
  759. {thth-4.2.1 → thth-4.3.0}/thth/vorbisformats.py +0 -0
  760. {thth-4.2.1 → thth-4.3.0}/thth/watch_cli.py +0 -0
  761. {thth-4.2.1 → thth-4.3.0}/thth/where_cli.py +0 -0
  762. {thth-4.2.1 → thth-4.3.0}/thth/where_keep.py +0 -0
  763. {thth-4.2.1 → thth-4.3.0}/thth/where_suggest.py +0 -0
  764. {thth-4.2.1 → thth-4.3.0}/thth/who_cli.py +0 -0
  765. {thth-4.2.1 → thth-4.3.0}/thth/worker.py +0 -0
  766. {thth-4.2.1 → thth-4.3.0}/thth/worker_version.py +0 -0
  767. {thth-4.2.1 → thth-4.3.0}/thth/x_metrics.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: thth
3
- Version: 4.2.1
3
+ Version: 4.3.0
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
@@ -44,6 +44,8 @@ English: [README.en.md](README.en.md) · サイト: https://thth.me · 文書の
44
44
  - **調べる**: `thth topics <アカウント> --search <語>` で、その語の件数・投稿者の数・最新の投稿を見ます。`thth where` は「次にどこで話すか」を、自分がすでに絡んだ相手を重ねて探します。X の行には投稿者のフォロワー数(「フォロワー 1.2 万」)と投稿数も付きます(`--json` は `author_followers`・`author_posts`。保存しません)。
45
45
  - **出す**: `thth send` で今すぐ、`thth schedule` で予約、`--reply-to` で返信です。
46
46
  - **ブックマークの連投は記事と別に数える**(4.0.1): はてなブックマークは主体の別の窓(既定 10 分に 10 件)で数え、超えても主体を止めずにその 1 件だけ断ります。1 日 30 件・同じホストへ 1 日 3 件(自分の記事は 1 日 1 件のまま)。記事の連投で止める作りは変わりません([リリースノート 4.0.1](docs/リリースノート_4.0.1_2026-10-17.md))。
47
+ - **返信はまとめて出す**(4.3.0): 時刻の過ぎた返信は承認したその場で、timer では 1 回でまとめて出します(返信の窓は返信だけで 10 分に 10 件・投稿の窓に数えない・投稿の間隔を掛けない)。使い方は「`publish_at` を今か過去にしてまとめて承認する」だけです([リリースノート 4.3.0](docs/リリースノート_4.3.0_2026-10-19.md))。
48
+ - **1 件の断りでまとめを止めない**(4.2.2): はてなに 1 件だけ断られた札は token を確かめてその札だけ外し、まとめを進めます。断られそうな札(半角の「%」・タグとコメントの長いもの)は 1 段目と lint で予告します(断りません)。止まったまとめは 60 分ごとにしか打たず、`thth bookmark batch-resume` で今すぐ見直せます。sparse-checkout の定義の外には記録を書かず、直し方を言います([リリースノート 4.2.2](docs/リリースノート_4.2.2_2026-10-18.md))。
47
49
  - **ブックマークの直しとタグの長さの柵**(4.2.0): `thth bookmark edit` で THTH が付けたブックマークのコメントとタグを 2 段の承認で直す。31 バイトを超えるタグは予告して断る。上限の設定は台帳の `limits` と別名で書け、断りに根拠と通し方が付きます([リリースノート 4.2.0](docs/リリースノート_4.2.0_2026-10-18.md))。
48
50
  - **ブックマークを 1 日分まとめて承認する**(4.1.0): 札を JSON Lines に並べ、`thth bookmark batch` の 1 段目で全件の一覧と digest を見て 2 段目で確定。timer が上限の中で時刻を散らして付けます(口座ごと)。あわせて、上限と連投の柵の土台(媒体の特性の台帳)を入れました([リリースノート 4.1.0](docs/リリースノート_4.1.0_2026-10-17.md))。
49
51
  - **記事をはてなへ届けて、直したら反映**(4.0.0): repo の記事を承認してはてなブログへ公開・予約し(画像はフォトライフへ)、SNS の告知(`announces`・`{article_url}`)とはてなブックマークを記事に結びます。記事を直せば `thth article diff` で差分を見て、承認して更新します。はてなの画面で手直しされていれば上書きせず `thth conflict` で止まります。反応は `thth article reactions` で記事ごとに。SNS だけの口座の動きと承認の指紋は変わりません([リリースノート 4.0.0](docs/リリースノート_4.0.0_2026-10-17.md))。
@@ -20,6 +20,8 @@ English: [README.en.md](README.en.md) · サイト: https://thth.me · 文書の
20
20
  - **調べる**: `thth topics <アカウント> --search <語>` で、その語の件数・投稿者の数・最新の投稿を見ます。`thth where` は「次にどこで話すか」を、自分がすでに絡んだ相手を重ねて探します。X の行には投稿者のフォロワー数(「フォロワー 1.2 万」)と投稿数も付きます(`--json` は `author_followers`・`author_posts`。保存しません)。
21
21
  - **出す**: `thth send` で今すぐ、`thth schedule` で予約、`--reply-to` で返信です。
22
22
  - **ブックマークの連投は記事と別に数える**(4.0.1): はてなブックマークは主体の別の窓(既定 10 分に 10 件)で数え、超えても主体を止めずにその 1 件だけ断ります。1 日 30 件・同じホストへ 1 日 3 件(自分の記事は 1 日 1 件のまま)。記事の連投で止める作りは変わりません([リリースノート 4.0.1](docs/リリースノート_4.0.1_2026-10-17.md))。
23
+ - **返信はまとめて出す**(4.3.0): 時刻の過ぎた返信は承認したその場で、timer では 1 回でまとめて出します(返信の窓は返信だけで 10 分に 10 件・投稿の窓に数えない・投稿の間隔を掛けない)。使い方は「`publish_at` を今か過去にしてまとめて承認する」だけです([リリースノート 4.3.0](docs/リリースノート_4.3.0_2026-10-19.md))。
24
+ - **1 件の断りでまとめを止めない**(4.2.2): はてなに 1 件だけ断られた札は token を確かめてその札だけ外し、まとめを進めます。断られそうな札(半角の「%」・タグとコメントの長いもの)は 1 段目と lint で予告します(断りません)。止まったまとめは 60 分ごとにしか打たず、`thth bookmark batch-resume` で今すぐ見直せます。sparse-checkout の定義の外には記録を書かず、直し方を言います([リリースノート 4.2.2](docs/リリースノート_4.2.2_2026-10-18.md))。
23
25
  - **ブックマークの直しとタグの長さの柵**(4.2.0): `thth bookmark edit` で THTH が付けたブックマークのコメントとタグを 2 段の承認で直す。31 バイトを超えるタグは予告して断る。上限の設定は台帳の `limits` と別名で書け、断りに根拠と通し方が付きます([リリースノート 4.2.0](docs/リリースノート_4.2.0_2026-10-18.md))。
24
26
  - **ブックマークを 1 日分まとめて承認する**(4.1.0): 札を JSON Lines に並べ、`thth bookmark batch` の 1 段目で全件の一覧と digest を見て 2 段目で確定。timer が上限の中で時刻を散らして付けます(口座ごと)。あわせて、上限と連投の柵の土台(媒体の特性の台帳)を入れました([リリースノート 4.1.0](docs/リリースノート_4.1.0_2026-10-17.md))。
25
27
  - **記事をはてなへ届けて、直したら反映**(4.0.0): repo の記事を承認してはてなブログへ公開・予約し(画像はフォトライフへ)、SNS の告知(`announces`・`{article_url}`)とはてなブックマークを記事に結びます。記事を直せば `thth article diff` で差分を見て、承認して更新します。はてなの画面で手直しされていれば上書きせず `thth conflict` で止まります。反応は `thth article reactions` で記事ごとに。SNS だけの口座の動きと承認の指紋は変わりません([リリースノート 4.0.0](docs/リリースノート_4.0.0_2026-10-17.md))。
@@ -59,10 +59,10 @@ def hatena_loose(world):
59
59
  daily_max_self_bookmarks=100, bookmark_same_host_daily=100)
60
60
 
61
61
 
62
- def sns_post(world, account, at, *, retract=False):
62
+ def sns_post(world, account, at, *, retract=False, reply_to=None):
63
63
  state = world.state(account)
64
64
  post_id = str(1000000 + seq())
65
- sent.write(state, post_id=post_id, text="本文", body_hash="0" * 64, sent_at=jst.iso(at))
65
+ sent.write(state, post_id=post_id, text="本文", body_hash="0" * 64, sent_at=jst.iso(at), reply_to=reply_to)
66
66
  if retract:
67
67
  sent.mark_retracted(state, post_id, retracted_at=jst.iso(at), retracted_by="t", retract_reason="t")
68
68
 
@@ -105,16 +105,19 @@ def scenario(rid, world):
105
105
  """行ごとの試し方(`count` 2 に締め、ほかの行は緩める)。"""
106
106
  hatena_loose(world)
107
107
  hcheck = lambda kind, **kw: guard.check(ACCOUNT, accounts.load_account(ACCOUNT), kind, now=NOW, **kw)
108
- if rid in ("post.daily", "retract.daily", "reply.burst", "post.burst"):
108
+ if rid in ("post.daily", "retract.daily", "reply.burst", "post.burst", "reply.daily"):
109
109
  values = {"post.daily": {"daily_max_posts": 2}, "retract.daily": {"daily_max_retracts": 2},
110
110
  "reply.burst": {"burst": {"count": 2, "minutes": 10}},
111
+ "reply.daily": {"limits": {"reply.daily": 2}},
111
112
  "post.burst": {"burst": {"count": 2, "minutes": 10}}}[rid]
112
113
  cfg = sns_cfg(**values)
113
114
  when = minutes_ago if rid.endswith("burst") else hours_ago
114
115
  kind = "retract" if rid == "retract.daily" else "publish"
115
116
  return Scenario(
116
- place=lambda i, inside: sns_post(world, SNS if inside else SNS_OTHER, when(i), retract=rid == "retract.daily"),
117
- ask=lambda: guard.check(SNS, cfg, kind, now=NOW, reply=rid == "reply.burst"),
117
+ # 4.3: 返信の窓(reply.burst)は返信だけを数える → 返信の行は返信の記録を置く。
118
+ place=lambda i, inside: sns_post(world, SNS if inside else SNS_OTHER, when(i), retract=rid == "retract.daily",
119
+ reply_to="1799" if rid.startswith("reply.") else None),
120
+ ask=lambda: guard.check(SNS, cfg, kind, now=NOW, reply=rid.startswith("reply.")),
118
121
  next_at=(lambda: jst.iso(minutes_ago(0) + datetime.timedelta(minutes=11))) if rid == "reply.burst"
119
122
  else (None if rid == "post.burst" else lambda: MIDNIGHT),
120
123
  stop_target=("account", SNS) if rid == "post.burst" else None)
@@ -183,7 +186,8 @@ def scenario(rid, world):
183
186
 
184
187
 
185
188
  # 数のある行(`count` 2 に締めて試す)。数の無い行は下の個別の試験。
186
- COUNTED = ("post.daily", "retract.daily", "reply.burst", "post.burst", "reaction.daily",
189
+ # 4.3(監査 中-1): 返信だけの日の上限 `reply.daily` を足した。
190
+ COUNTED = ("post.daily", "retract.daily", "reply.burst", "reply.daily", "post.burst", "reaction.daily",
187
191
  "entry.daily", "entry.blog_daily", "entry.principal_daily",
188
192
  "entry_delete.daily", "entry_delete.blog_daily", "entry_delete.principal_daily",
189
193
  "entry.burst", "entry.principal_burst",
@@ -59,7 +59,8 @@ def test_信号は7つ():
59
59
  @pytest.mark.parametrize("family,action,expected", [
60
60
  # 段 2: 各道の最後に共通の最低線 `write.floor`(予約と反応には当てない)。
61
61
  ("sns", "post", ["post.quiet", "post.interval", "post.daily", "post.burst", "write.floor"]),
62
- ("sns", "reply", ["post.quiet", "post.daily", "reply.burst"]), # 夜の監査 低-1: 最低線は返信で止めない
62
+ # 夜の監査 低-1: 最低線は返信で止めない。4.3(監査 中-1): 返信は投稿の日の上限でなく `reply.daily`。
63
+ ("sns", "reply", ["post.quiet", "reply.daily", "reply.burst"]),
63
64
  ("sns", "retract", ["retract.daily", "post.burst", "write.floor"]),
64
65
  ("sns", "schedule", ["post.daily"]),
65
66
  ("hatenablog", "entry_create", ["entry.platform_refusal", "entry.quiet", "entry.interval", "entry.daily",
@@ -364,8 +364,10 @@ def test_T_P_10_同じ配信の2回目はtargetの行で1件_opの行で2件():
364
364
 
365
365
 
366
366
  def test_分の窓は行の動作だけを数える():
367
- """記録の出来事のうち行の `actions` に入るものだけ(SNS の記録の `post` は投稿・返信・記事の行に入る)。"""
368
- assert assess.counts_as("post", ("reply",)) and assess.counts_as("post", ("entry_create",))
367
+ """記録の出来事のうち行の `actions` に入るものだけ(SNS の記録の `post` は投稿・記事の行に入る。4.3 から返信の
368
+ 記録は `reply` の出来事で、`post` は返信の行に入らない)。"""
369
+ assert not assess.counts_as("post", ("reply",)) and assess.counts_as("post", ("entry_create",))
370
+ assert assess.counts_as("reply", ("reply",)) and not assess.counts_as("reply", ("post", "retract"))
369
371
  assert assess.counts_as("retract", ("entry_delete",))
370
372
  assert not assess.counts_as("bookmark_add", ("entry_create", "entry_update", "entry_delete"))
371
373
  assert not assess.counts_as("post", ("bookmark_add",))
@@ -228,7 +228,8 @@ def test_返信の窓はreply_burstで別に締められる(world):
228
228
  cfg = {"account": SNS, "media": "threads", "daily_max_posts": 100, "daily_max_retracts": 100,
229
229
  "burst": {"count": 1, "minutes": 10}, "limits": {"reply.burst": {"count": 5, "minutes": 10}}}
230
230
  for i in range(3):
231
- sns_post(world, SNS, minutes_before(5 - i))
231
+ # 4.3: 返信の窓は返信だけを数える → 返信の記録を置く。
232
+ sns_post(world, SNS, minutes_before(5 - i), reply_to="1799")
232
233
  guard.check(SNS, cfg, "publish", now=NOW, reply=True) # 3 件のあとの返信は通る(5 まで)
233
234
  cfg["limits"] = {"reply.burst": {"count": 3, "minutes": 10}}
234
235
  e = refused(lambda: guard.check(SNS, cfg, "publish", now=NOW, reply=True))
@@ -366,6 +367,14 @@ def test_T_P_6_account_setの新しい名前と媒体(world):
366
367
  assert raw["burst"] == {"count": 4, "minutes": 10} and raw["limits"] == {"post.daily": 12}
367
368
 
368
369
 
370
+ def test_返信の日の上限はaccount_setで書ける(world):
371
+ """4.3.0(監査 中-1): 断りの文が案内する `thth account set <口座> reply.daily <数>` が通る。"""
372
+ name = sns_account(world)
373
+ rc, out, err = call(["account", "set", name, "reply.daily", "20", "--by", "masaru", "--json"])
374
+ assert rc == 0, err
375
+ assert json.loads(out)["after"] == 20 and world.account(name)["limits"] == {"reply.daily": 20}
376
+
377
+
369
378
  def test_T_P_6_締める向きだけの口(world):
370
379
  """柵: `tightens` の向きを逆にすると赤(LLM の口から緩められる)。"""
371
380
  world.edit_principal("hatena-main", limits={"bookmark.burst": {"count": 10, "minutes": 10}})
@@ -505,8 +514,9 @@ EXPECTED_4_1_0 = {
505
514
  "bookmark.burst": (10, 10), "bookmark.stop": (30, 10), "upload.daily": 30},
506
515
  ("account", "vm-kopicha-hatenablog"): {"entry.daily": 3, "entry_delete.daily": 2, "entry.burst": (3, 10),
507
516
  "write.floor": (20, 10)},
517
+ # 4.3: 返信の窓の既定は媒体の台帳の 10 分に 10(`burst` を書いていない口座)。
508
518
  ("account", "vm-music-threads"): {"post.daily": 8, "retract.daily": 5, "post.burst": (3, 10),
509
- "reply.burst": (3, 10), "reaction.daily": 50, "write.floor": (20, 10)},
519
+ "reply.burst": (10, 10), "reaction.daily": 50, "write.floor": (20, 10)},
510
520
  ("account", "vm-kopicha-x"): {"post.daily": 4, "retract.daily": 5, "post.burst": (2, 30),
511
521
  "reply.burst": (2, 30), "reaction.daily": 20, "write.floor": (20, 10)},
512
522
  }
@@ -85,6 +85,8 @@ COMMANDS = '''lint|preview|approve|account|revoke|posts|replies|measured|threads
85
85
  COMMANDS += ['bookmark edit']
86
86
  # 上限と連投の柵 段 4(§9.2): 媒体の観測を数える・人が足す。
87
87
  COMMANDS += ['media observations', 'media observe']
88
+ # 4.2.2(S4 §24): 止まったまとめを今すぐ見直させる(1 段)。
89
+ COMMANDS += ['bookmark batch-resume']
88
90
 
89
91
 
90
92
  def test_json_table_covers_every_parser():
@@ -191,7 +191,7 @@ def test_止まった口座はmcpから戻せず_成功の答えにも理由が
191
191
  # 裁定 (a)・(b)
192
192
  # --------------------------------------------------------------------------
193
193
 
194
- def test_返信には最短間隔を掛けない_上限には数える(env, publisher, monkeypatch):
194
+ def test_返信には最短間隔を掛けない_返信の日の上限に数える(env, publisher, monkeypatch):
195
195
  from thth import adapters
196
196
  made = adapters.make_adapter
197
197
 
@@ -200,13 +200,16 @@ def test_返信には最短間隔を掛けない_上限には数える(env, publ
200
200
  type(adapter).fetch_post = lambda self, post_id: {"id": post_id}
201
201
  return adapter
202
202
  monkeypatch.setattr(adapters, "make_adapter", with_fetch)
203
- ledger(env, min_interval_hours=6, daily_max_posts=2)
203
+ # 4.3.0(監査 中-1): 返信は投稿の日の上限(`daily_max_posts`)でなく、返信だけの日の上限
204
+ # (`reply.daily`)に数える。投稿の上限を 1 にしても返信は通り、返信の上限 1 で 2 本目を断る。
205
+ ledger(env, min_interval_hours=6, daily_max_posts=1, limits={"reply.daily": 1})
204
206
  call(env, operation="send_request", body="一本目")
205
207
  assert str(refused(env, operation="send_request", body="二本目")) == "too_soon"
206
208
  replied = call(env, operation="send_request", body="返信の本文", reply_to="123456")
207
209
  assert replied["status"] == "published"
208
- # 1 日の上限(2)には返信も数える。
209
- assert str(refused(env, operation="send_request", body="三本目", reply_to="123456")) == "daily_limit"
210
+ gone = refused(env, operation="send_request", body="三本目", reply_to="123456")
211
+ assert str(gone) == "daily_limit"
212
+ assert "reply_daily" in json.dumps(gone.__dict__, ensure_ascii=False, default=str)
210
213
 
211
214
 
212
215
  @pytest.mark.parametrize("mode", [None, "publish", "all"])
@@ -93,8 +93,9 @@ def test_続く5件目は4件目より後ろに並ぶ(isolated_account_factory):
93
93
  def test_返信でない投稿は今までどおり口座を止める(isolated_account_factory):
94
94
  account = isolated_account_factory(production=True, min_interval_hours=0, burst=BURST)
95
95
  spy = _Spy()
96
+ # 4.3: 投稿の窓は返信を数えない → 投稿 3 本のあとの投稿で止まる(返信 3 本のあとでは止まらない)。
96
97
  for i in range(3):
97
- assert _send(account, spy, f"お返事{i}", minute=i, reply_to=f"1799{i}").exit_code == 0
98
+ assert _send(account, spy, f"ふつうの投稿{i}", minute=i).exit_code == 0
98
99
  r = _send(account, spy, "ふつうの投稿", minute=3)
99
100
  assert r.action == "guard" and r.error == "account_stopped"
100
101
  assert guard.stopped(account["name"])["reason"] == "burst"
@@ -102,7 +103,8 @@ def test_返信でない投稿は今までどおり口座を止める(isolated_a
102
103
 
103
104
 
104
105
  def test_1日の上限は返信でも並べずに断る(isolated_account_factory):
105
- account = isolated_account_factory(production=True, min_interval_hours=0, daily_max_posts=3,
106
+ # 4.3(監査 中-1): 返信の日の上限は投稿の `daily_max_posts` でなく返信だけの `reply.daily`。
107
+ account = isolated_account_factory(production=True, min_interval_hours=0, limits={"reply.daily": 3},
106
108
  burst={"count": 2, "minutes": 10})
107
109
  spy = _Spy()
108
110
  for i in range(2):
@@ -166,13 +168,18 @@ def test_guardの柵_返信のburstは止めずに窓が空く時刻を添える
166
168
  cfg = accounts.load_account(account["name"])
167
169
  sdir = accounts.state_dir_for(account["name"])
168
170
  for i in range(3):
171
+ # 4.3: 返信の窓は返信だけを数える → 前の 3 件は返信の記録。
169
172
  sent.write(sdir, post_id=f"90{i}", text=f"前{i}", body_hash="x",
170
- sent_at=jst.iso(NOW + datetime.timedelta(minutes=i)))
173
+ sent_at=jst.iso(NOW + datetime.timedelta(minutes=i)), reply_to=f"1799{i}")
171
174
  with pytest.raises(guard.GuardRefused) as caught:
172
175
  guard.check(account["name"], cfg, "publish", now=NOW + datetime.timedelta(minutes=3), reply=True)
173
176
  assert str(caught.value) == "burst_limit" and caught.value.reason == "burst"
174
177
  assert caught.value.next_at == "2026-10-15T17:41:00+09:00"
175
178
  assert guard.stopped(account["name"]) is None
179
+ # 4.3: 投稿の窓は返信を数えない → 投稿の記録 3 件を足してから、投稿で止まる。
180
+ for i in range(3):
181
+ sent.write(sdir, post_id=f"91{i}", text=f"投稿{i}", body_hash="x",
182
+ sent_at=jst.iso(NOW + datetime.timedelta(minutes=i)))
176
183
  with pytest.raises(guard.GuardRefused) as stopped:
177
184
  guard.check(account["name"], cfg, "publish", now=NOW + datetime.timedelta(minutes=3))
178
185
  assert str(stopped.value) == "account_stopped" and guard.stopped(account["name"])["reason"] == "burst"
@@ -183,18 +190,19 @@ def test_burst_slotの計算():
183
190
  cfg = {"burst": {"count": 3, "minutes": 10}}
184
191
  at = lambda m: NOW + datetime.timedelta(minutes=m) # noqa: E731
185
192
  import thth.guard as g
186
- orig = g._events
193
+ # 4.3: 材料は返信の時刻だけ(`reply_times`)。
194
+ orig = g.reply_times
187
195
  try:
188
- g._events = lambda account: ([at(0), at(1), at(2)], [])
196
+ g.reply_times = lambda account: [at(0), at(1), at(2)]
189
197
  assert g.burst_slot("x", cfg, now=at(3)) == at(11)
190
198
  assert g.burst_slot("x", cfg, now=at(3), pending=[at(11)]) == at(12)
191
199
  assert g.burst_slot("x", cfg, now=at(3), pending=[at(11), at(12)]) == at(13)
192
200
  # 3 件目で窓が詰まる(11・12・13)→ 次は 11 + 11 分。
193
201
  assert g.burst_slot("x", cfg, now=at(3), pending=[at(11), at(12), at(13)]) == at(22)
194
- g._events = lambda account: ([], [])
202
+ g.reply_times = lambda account: []
195
203
  assert g.burst_slot("x", cfg, now=at(3)) == at(3)
196
204
  finally:
197
- g._events = orig
205
+ g.reply_times = orig
198
206
 
199
207
 
200
208
  # --------------------------------------------------------------------------
@@ -223,10 +231,11 @@ def _call(env, **request):
223
231
  return writes.execute(env['context'], dict(account='alpha', **request), via='mcp')
224
232
 
225
233
 
226
- def _write_recent(env, n):
234
+ def _write_recent(env, n, reply_to=None):
227
235
  state = env['root'] / 'state/alpha'
228
236
  for i in range(n):
229
- sent.write(str(state), post_id=f'90{i}', text=f'前の返信{i}', body_hash='x', sent_at=jst.iso())
237
+ sent.write(str(state), post_id=f'90{i}', text=f'前の返信{i}', body_hash='x', sent_at=jst.iso(),
238
+ reply_to=reply_to)
230
239
 
231
240
 
232
241
  def test_thth_meの道でも返信は順番待ちに並び_口座は止まらない(env, remote, publisher):
@@ -259,7 +268,7 @@ def test_thth_meの道でも返信でない投稿は止める(env, remote, publi
259
268
 
260
269
  def test_thth_meの道でscheduled_falseは止めずにburst_limit(env, remote, publisher):
261
270
  ledger(env, burst={'count': 2, 'minutes': 10}, scheduled=False)
262
- _write_recent(env, 2)
271
+ _write_recent(env, 2, reply_to='17980') # 4.3: 返信の窓は返信だけを数える
263
272
  with pytest.raises(ReportServiceError) as caught:
264
273
  _call(env, operation='send_request', body='お返事', reply_to='17990')
265
274
  assert str(caught.value) == 'burst_limit' and caught.value.reason == 'burst' and caught.value.next_at
@@ -267,6 +276,21 @@ def test_thth_meの道でscheduled_falseは止めずにburst_limit(env, remote,
267
276
  assert 'burst_limit' in writes.SAFE_ERRORS
268
277
 
269
278
 
279
+ def test_thth_meの道で並べる返信は返信の日の上限で数え_投稿の上限は使わない(env, remote, publisher):
280
+ # 4.3.0(監査 中-1): 窓に当たった返信を並べる道(`burst_queued_send` → `schedule`)も、投稿の日の上限
281
+ # (`daily_max_posts`)でなく返信だけの日の上限(`reply.daily`)で数える。投稿の上限は今日の投稿 1 本で
282
+ # 埋まっているが、返信は並ぶ。返信の上限 4 = 出した 2 + 並べた 2 で、5 本目は並べずに断る。
283
+ ledger(env, burst={'count': 2, 'minutes': 10}, min_interval_hours=0, daily_max_posts=1,
284
+ limits={'reply.daily': 4})
285
+ _write_recent(env, 1)
286
+ results = [_call(env, operation='send_request', body=f'お返事{i}', reply_to=f'1799{i}') for i in range(4)]
287
+ assert [r['status'] for r in results] == ['published'] * 2 + ['queued'] * 2
288
+ with pytest.raises(ReportServiceError) as caught:
289
+ _call(env, operation='send_request', body='お返事4', reply_to='17994')
290
+ assert str(caught.value) == 'daily_limit' and caught.value.reason == 'reply_daily'
291
+ assert guard.stopped('alpha') is None
292
+
293
+
270
294
  def test_順番待ちの印はqueued_forだけで指紋は変えない():
271
295
  assert burst_queue.is_queued({'queued_for': 'burst'})
272
296
  assert not burst_queue.is_queued({'held_minutes': '5'}) and not burst_queue.is_queued(None)
@@ -317,6 +317,10 @@ def test_T_S4_7a_書くもの(bm, answer, result, error):
317
317
  (429, "provider_rate_limited"), (409, "hatena_http_409")])
318
318
  def test_T_S4_7a_書かないもの(bm, status, error):
319
319
  bm.bookmarks.post = _post((status, b"no"))
320
+ if status in (401, 403):
321
+ # 4.2.2(S4 §24): 401/403 は `/rest/1/my` で token を確かめる。ここは token も断られる場合
322
+ # (GET が通る=その札だけの断りは tests/test_v422_bookmark_rejections.py)。
323
+ bm.bookmarks.me = (status, b"no")
320
324
  assert failed(ACCOUNT, url=EXTERNAL, comment="c") == error
321
325
  assert rows(bm, ACCOUNT) == []
322
326
  assert principals.stopped("hatena-main") is None # 429 でも主体は止めない
@@ -0,0 +1,285 @@
1
+ """4.2.2(設計 S4 §24・kopicha の本番 10/18): はてなブックマークの 1 件の断りでまとめ全体を止めない。
2
+
3
+ 見るもの:
4
+ B(主). POST が 401/403 なら同じ run の中で `/rest/1/my` を GET して token を確かめる。通ればその札だけ
5
+ `hatena_rejected_item`(詳細に HTTP の番号)でまとめは進む。GET も断られたら `hatena_auth_rejected`。
6
+ 1 件ずつの `add`・`edit` でも同じ見分けで、人向けの文を分ける。原因が何であれ、打ち直さない。
7
+ A. 本文の予告(**断らない**・原因が未確定): 半角「%」(`bookmark_percent_maybe_rejected`)と、
8
+ `[tag]…comment` の形で 135 字か 343 バイトを超える長さ(`bookmark_length_maybe_rejected`)を、
9
+ `add`・`edit`・`batch` の 1 段目と `thth lint <札のファイル>` で予告する(lint は `warning:`)。
10
+ 承認の指紋は変えない。台帳に観測の項目(確かさ low・数は台帳とコードで同じ)。
11
+ C. はてなに打って止まった(blocked)ら `BLOCKED_HOLD_MINUTES`(60 分)は次の run も HTTP を打たない。
12
+ `thth bookmark batch-resume <口座> --by` で今すぐ見直させる。打たずに止まる止め(主体の止めなど)は今どおり。
13
+
14
+ HTTP は偽物(`hatena_s4_helpers.Bookmarks`)。はてなには出ない。秘密の値は偽物だけ。
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import datetime
19
+ import json
20
+
21
+ from tests.hatena_s1_flow_helpers import call, lane # noqa: F401
22
+ from tests.hatena_s4_helpers import ACCOUNT, EXTERNAL, EXTERNAL_2, bm, bookmark, calls, cfg, fake_http, world # noqa: F401
23
+ from tests.test_v410_bookmark_batch import (URL_A1, URL_B1, approve, batch_file, bb, item, later, run, # noqa: F401
24
+ states, write)
25
+ from thth import bookmark_batch, hatena_bookmark, lint, media_traits, sent
26
+
27
+ ME = "https://bookmark.hatenaapis.com/rest/1/my"
28
+ # `[tag]…comment` の形で 138 字・398 バイト(予告の長さを超える)。コメントは 100 字以内・タグは 31 バイト以内。
29
+ LONG_TAGS = ["一次資料いろはにほへ", "二次資料いろはにほへ", "三次資料いろはにほへ", "四次資料いろはにほへ"]
30
+ LONG_COMMENT = "あ" * 90
31
+
32
+
33
+ def hatena_bookmark_api(target):
34
+ import urllib.parse
35
+ return "https://bookmark.hatenaapis.com/rest/1/my/bookmark?" + urllib.parse.urlencode([("url", target)])
36
+
37
+
38
+ def me_calls(world):
39
+ return [u for m, u in calls(world, "GET") if u == ME]
40
+
41
+
42
+ def codes(warnings):
43
+ return [w["code"] for w in warnings]
44
+
45
+
46
+ # ================================================================ A. 本文の予告(断らない)
47
+
48
+ def test_A_予告の境目は135字と343バイト_半角パーセントは全角なら出ない():
49
+ assert hatena_bookmark.body_form("c", ["a", "b"]) == "[a][b]c"
50
+ assert codes(hatena_bookmark.body_warnings("x" * 135, [])) == []
51
+ assert codes(hatena_bookmark.body_warnings("x" * 136, [])) == ["bookmark_length_maybe_rejected"]
52
+ assert codes(hatena_bookmark.body_warnings("あ" * 114 + "x", [])) == [] # 115 字・343 バイト
53
+ assert codes(hatena_bookmark.body_warnings("あ" * 114 + "xy", [])) == ["bookmark_length_maybe_rejected"] # 344 バイト
54
+ assert codes(hatena_bookmark.body_warnings("約 85%", [])) == []
55
+ got = hatena_bookmark.body_warnings("約 85%", ["割合%"])
56
+ assert codes(got) == ["bookmark_percent_maybe_rejected"]
57
+ assert "コメント 1 個" in got[0]["text"] and "タグ「割合%」 1 個" in got[0]["text"]
58
+ assert len(hatena_bookmark.body_form(LONG_COMMENT, LONG_TAGS)) == 138
59
+
60
+
61
+ def test_A_addの1段目は予告して断らず_2段目も打つ_指紋は変わらない(bm):
62
+ """柵: `body_warnings` を 1 段目に出さないと赤。断る形に戻すと赤(rc 2)。"""
63
+ args = ["bookmark", "add", ACCOUNT, "--url", EXTERNAL, "--comment", "約 85% が一次資料", "--tags", "割合%,統計"]
64
+ rc, out, _ = call(args + ["--json"])
65
+ body = json.loads(out)
66
+ assert rc == 1 and body["stage"] == 1
67
+ assert codes(body["preview"]["warnings"]) == ["bookmark_percent_maybe_rejected"]
68
+ rc, text, _ = call(args)
69
+ assert "予告(断りません): bookmark_percent_maybe_rejected" in text and "原因は未確定" in text
70
+ p = hatena_bookmark.prepare(ACCOUNT, cfg(ACCOUNT), url=EXTERNAL, comment="約 85% が一次資料", tags="割合%,統計",
71
+ check_remote=False)
72
+ from thth import approval
73
+ assert p["approved_sha"] == approval.compute_hatena_bookmark_sha(
74
+ account=ACCOUNT, principal="hatena-main", op="add", url=EXTERNAL, delivery="", comment="約 85% が一次資料",
75
+ tags=["割合%", "統計"])
76
+ rc, out, err = call(args + ["--confirm", body["digest"], "--by", "masaru", "--production", "--json"])
77
+ assert rc == 0, (out, err)
78
+ assert len(calls(bm, "POST")) == 1
79
+
80
+
81
+ def test_A_長い札も予告する_editは直したあとの値(bm):
82
+ rc, out, _ = call(["bookmark", "add", ACCOUNT, "--url", EXTERNAL, "--comment", LONG_COMMENT, "--tags",
83
+ ",".join(LONG_TAGS), "--json"])
84
+ body = json.loads(out)
85
+ assert rc == 1 and codes(body["preview"]["warnings"]) == ["bookmark_length_maybe_rejected"]
86
+ assert "138 字・398 バイト" in body["preview"]["warnings"][0]["text"]
87
+ assert "135 字・343 バイト" in body["preview"]["warnings"][0]["text"]
88
+ bookmark(ACCOUNT, url=EXTERNAL_2, comment="旧", tags=["旧名"])
89
+ rc, out, _ = call(["bookmark", "edit", ACCOUNT, "--url", EXTERNAL_2, "--comment", "100%", "--json"])
90
+ assert rc == 1 and codes(json.loads(out)["preview"]["warnings"]) == ["bookmark_percent_maybe_rejected"]
91
+ rc, out, _ = call(["bookmark", "edit", ACCOUNT, "--url", EXTERNAL_2, "--comment", "100%", "--json"])
92
+ assert rc == 1 and json.loads(out)["preview"]["warnings"] == []
93
+
94
+
95
+ def test_A_batchは札を除かず予告を添える_指紋は今と同じ(bb):
96
+ path = write(bb, [item(URL_A1, comment="約 85%"), {"url": URL_B1, "comment": LONG_COMMENT, "tags": LONG_TAGS},
97
+ item("https://c.example.net/y", comment="約 85%")])
98
+ first = json.loads(call(["bookmark", "batch", ACCOUNT, path, "--json"])[1])
99
+ items = first["preview"]["items"]
100
+ assert [r["include"] for r in items] == [True, True, True]
101
+ assert [r.get("warnings") for r in items] == [["bookmark_percent_maybe_rejected"],
102
+ ["bookmark_length_maybe_rejected"], None]
103
+ shas = [hatena_bookmark.prepare(ACCOUNT, cfg(ACCOUNT), url=r["url"], comment=r["comment"], tags=r["tags"],
104
+ check_remote=False)["approved_sha"] for r in items]
105
+ assert first["preview"]["approved_sha"] == bookmark_batch.compute_sha(ACCOUNT, "hatena-main", shas)
106
+ rc, out, _ = call(["bookmark", "batch", ACCOUNT, path])
107
+ assert "予告(断りません): bookmark_percent_maybe_rejected" in out
108
+ approve(path)
109
+ assert all("warnings" not in it for it in batch_file(bb)["items"]) # 札の形は変えない
110
+
111
+
112
+ def test_A_lintは札のファイルの予告をwarningで言いrcは0(tmp_path):
113
+ path = tmp_path / "day.jsonl"
114
+ path.write_text("\n".join([
115
+ json.dumps({"url": EXTERNAL, "comment": "約 85%", "tags": "a%b"}, ensure_ascii=False),
116
+ json.dumps({"url": EXTERNAL_2, "comment": LONG_COMMENT, "tags": LONG_TAGS}, ensure_ascii=False),
117
+ json.dumps({"url": "https://c.example.net/y", "comment": "約 85%"}, ensure_ascii=False)]) + "\n",
118
+ encoding="utf-8")
119
+ problems = lint.lint_file(str(path))
120
+ assert [p.split(": 札")[0] for p in problems] == ["warning: bookmark_percent_maybe_rejected",
121
+ "warning: bookmark_length_maybe_rejected"]
122
+ assert "札 1(1 行目)" in problems[0] and "タグ「a%b」 1 個" in problems[0]
123
+ assert all(lint.is_warning(p) for p in problems)
124
+ rc, out, err = call(["lint", str(path)])
125
+ assert rc == 0 and "bookmark_length_maybe_rejected" in out + err
126
+
127
+
128
+ def test_A_予告の札が断られてもその札だけ外してまとめは進む(bb):
129
+ """本番の形: 14 件目「約 85%」・26 件目(長い札)が 401。token は生きている → その札だけ refused。"""
130
+ bb.bookmarks.post = lambda target, form: ((401, b"no") if target in (URL_A1, URL_B1)
131
+ else _place_default(bb, target, form))
132
+ approve(write(bb, [item(URL_A1, comment="約 85%"), {"url": URL_B1, "comment": LONG_COMMENT, "tags": LONG_TAGS},
133
+ item("https://c.example.net/y")]))
134
+ out, _ = run(bb)
135
+ b = batch_file(bb)
136
+ assert [(i["state"], i["reason"]) for i in b["items"]] == [
137
+ ("refused", "hatena_rejected_item"), ("refused", "hatena_rejected_item"), ("placed", None)]
138
+ assert out["status"] == "placed" and not out.get("blocked")
139
+
140
+
141
+ def test_A_媒体の台帳に観測の項目があり_数はコードと同じ():
142
+ doc = media_traits.load("hatenabookmark")
143
+ found = next(t for t in doc["traits"] if t["id"] == "hatenabookmark.body_rejected_observed")
144
+ assert found["kind"] == "observation" and found["confidence"] == "low" and found["signals"] == []
145
+ assert found["evidence"]["type"] == "observed" and found["evidence"]["observed_at"] == "2026-10-18"
146
+ assert "kopicha" in found["evidence"]["record"]
147
+ assert "半角 %" in found["statement"] and "未確定" in found["statement"] and "401" in found["statement"]
148
+ assert f"{hatena_bookmark.BODY_OK_MAX_CHARS} 字・{hatena_bookmark.BODY_OK_MAX_BYTES} バイト" in found["statement"]
149
+ assert not any(t["id"] == "hatenabookmark.percent_in_body_rejected_observed" for t in doc["traits"])
150
+
151
+
152
+ # ================================================================ B. 1 件だけの 401/403
153
+
154
+ def test_B_addでPOSTが401でもtokenが生きていればその札だけの断り(bm):
155
+ """柵: `_rejection` の GET を外すと赤(token の生きている 401 を認可の問題と言う)。"""
156
+ bm.bookmarks.post = (401, b"Unauthorized")
157
+ rc, out, _ = call(["bookmark", "add", ACCOUNT, "--url", EXTERNAL, "--comment", "c", "--json"])
158
+ digest = json.loads(out)["digest"]
159
+ rc, out, err = call(["bookmark", "add", ACCOUNT, "--url", EXTERNAL, "--comment", "c", "--confirm", digest,
160
+ "--by", "masaru", "--production", "--json"])
161
+ body = json.loads(out)
162
+ assert rc == 2 and body["error"] == "hatena_rejected_item", (out, err)
163
+ assert "この札だけはてなに断られました" in body["message"] and "token は生きています" in body["message"]
164
+ assert "HTTP 401" in body["message"] and "principal auth" not in body["message"]
165
+ assert len(me_calls(bm)) == 1 # POST のあとに 1 回だけ確かめる
166
+ assert [r for r in sent.records(bm.state(ACCOUNT)) if r.get("kind") == "bookmark_add"] == []
167
+
168
+
169
+ def test_B_addでtokenも断られたら認可の問題(bm):
170
+ bm.bookmarks.post = (403, b"Forbidden")
171
+ bm.bookmarks.me = (401, b"Unauthorized")
172
+ rc, out, _ = call(["bookmark", "add", ACCOUNT, "--url", EXTERNAL, "--comment", "c", "--json"])
173
+ digest = json.loads(out)["digest"]
174
+ rc, out, _ = call(["bookmark", "add", ACCOUNT, "--url", EXTERNAL, "--comment", "c", "--confirm", digest,
175
+ "--by", "masaru", "--production", "--json"])
176
+ body = json.loads(out)
177
+ assert rc == 2 and body["error"] == "hatena_auth_rejected"
178
+ assert "thth principal auth" in body["message"] and "--force" in body["message"]
179
+
180
+
181
+ def test_B_editでも同じ見分け(bm):
182
+ bookmark(ACCOUNT, url=EXTERNAL, comment="旧", tags=["旧名"])
183
+ bm.bookmarks.post = (401, b"Unauthorized")
184
+ rc, out, _ = call(["bookmark", "edit", ACCOUNT, "--url", EXTERNAL, "--comment", "新", "--json"])
185
+ digest = json.loads(out)["digest"]
186
+ args = ["bookmark", "edit", ACCOUNT, "--url", EXTERNAL, "--comment", "新", "--confirm", digest, "--by", "masaru",
187
+ "--production", "--json"]
188
+ rc, out, _ = call(args)
189
+ assert rc == 2 and json.loads(out)["error"] == "hatena_rejected_item"
190
+ bm.bookmarks.me = (401, b"Unauthorized")
191
+ rc, out, _ = call(args)
192
+ assert rc == 2 and json.loads(out)["error"] == "hatena_auth_rejected"
193
+ row = sent.read(bm.state(ACCOUNT), sent.bookmark_post_id(EXTERNAL))
194
+ assert row["text"] == "旧" and not row.get("attempts")
195
+
196
+
197
+ def test_B_まとめは断られた札だけrefusedにして進む(bb):
198
+ """柵: `hatena_rejected_item` を `_FINAL_BOOKMARK` から外すと赤(まとめ全体が止まる)。"""
199
+ bb.bookmarks.post = lambda target, form: (401, b"no") if target == URL_A1 else _place_default(bb, target, form)
200
+ approve(write(bb, [item(URL_A1), item(URL_B1)]))
201
+ out, _ = run(bb)
202
+ b = batch_file(bb)
203
+ assert [(i["state"], i["reason"], i["detail"]) for i in b["items"]] == [
204
+ ("refused", "hatena_rejected_item", "HTTP 401"), ("placed", None, None)]
205
+ assert out["status"] == "placed" and not out.get("blocked")
206
+ assert bookmark_batch.status(ACCOUNT)["blocked"] is None
207
+
208
+
209
+ def test_B_まとめでtokenも断られたら止まる(bb):
210
+ bb.bookmarks.post = (401, b"no")
211
+ bb.bookmarks.me = (401, b"no")
212
+ approve(write(bb, [item(URL_A1), item(URL_B1)]))
213
+ out, _ = run(bb)
214
+ assert out["status"] == "blocked" and out["blocked"]["code"] == "hatena_auth_rejected"
215
+ assert states(bb) == ["pending", "pending"]
216
+
217
+
218
+ def _place_default(world, target, form):
219
+ """偽物のはてなの既定の POST(付ける)。"""
220
+ from tests.hatena_s4_helpers import bookmark_object
221
+ mark = bookmark_object(target, comment=form.get("comment", [""])[0], tags=form.get("tags", []))
222
+ world.bookmarks.marks[("lane-owner", target)] = mark
223
+ return 200, json.dumps(mark).encode()
224
+
225
+
226
+ # ================================================================ C. 止まったら打ち直し続けない
227
+
228
+ def _blocked(bb):
229
+ bb.bookmarks.post = (401, b"no")
230
+ bb.bookmarks.me = (401, b"no")
231
+ approve(write(bb, [item(URL_A1), item(URL_B1)]))
232
+ now = later(0)
233
+ out, _ = run(bb, now=now)
234
+ assert out["status"] == "blocked" and out["blocked"]["code"] == "hatena_auth_rejected"
235
+ return now
236
+
237
+
238
+ def test_C_はてなに打って止まったら次のrunはHTTPを打たない_60分あとに試す(bb):
239
+ """柵: blocked に `next_at` を置かないと赤(10 分ごとに同じ札を打ち直す・kopicha 10/18 に 57 回)。"""
240
+ now = _blocked(bb)
241
+ before = len(bb.http.calls)
242
+ for minutes in (10, 20, 30, 59):
243
+ out, _ = run(bb, now=now + datetime.timedelta(minutes=minutes))
244
+ assert out["status"] == "waiting" and out["blocked"]["code"] == "hatena_auth_rejected"
245
+ assert len(bb.http.calls) == before
246
+ s = bookmark_batch.status(ACCOUNT)
247
+ assert s["blocked"]["until"] == s["next_at"]
248
+ assert bookmark_batch.jst.parse(s["next_at"]) - now == datetime.timedelta(
249
+ minutes=bookmark_batch.BLOCKED_HOLD_MINUTES)
250
+ rc, text, _ = call(["bookmark", "batch-status", ACCOUNT])
251
+ assert "まではてなに打ちません" in text and f"thth bookmark batch-resume {ACCOUNT} --by" in text
252
+ # 60 分あとは今の柵で見直す(直っていなければまた止まる)
253
+ run(bb, now=now + datetime.timedelta(minutes=61))
254
+ assert len(bb.http.calls) > before
255
+
256
+
257
+ def test_C_batch_resumeで今すぐ見直させ_札と指紋は変えない(bb):
258
+ now = _blocked(bb)
259
+ before_items = batch_file(bb)["items"]
260
+ rc, out, _ = call(["bookmark", "batch-resume", ACCOUNT, "--json"])
261
+ assert rc == 1 and json.loads(out)["error"] == "by_required"
262
+ bb.bookmarks.post = None
263
+ bb.bookmarks.me = None
264
+ rc, out, err = call(["bookmark", "batch-resume", ACCOUNT, "--by", "masaru", "--json"])
265
+ body = json.loads(out)
266
+ assert rc == 0 and body["was_blocked"]["code"] == "hatena_auth_rejected" and body["pending"] == 2, (out, err)
267
+ s = bookmark_batch.status(ACCOUNT)
268
+ assert s["blocked"] is None and s["next_at"] is None
269
+ assert [i["approved_sha"] for i in batch_file(bb)["items"]] == [i["approved_sha"] for i in before_items]
270
+ out, _ = run(bb, now=now + datetime.timedelta(minutes=5))
271
+ assert out["status"] == "placed" and states(bb)[0] == "placed"
272
+ from thth import admin_log
273
+ assert any(r["event"] == "bookmark_batch_resumed" and r["by"] == "masaru" for r in admin_log.read()[0])
274
+
275
+
276
+ def test_C_打たずに止まる止めは今どおり次の刻みで見直す(bb):
277
+ """主体の止め(HTTP を打たない)は 60 分待たせない(止めを解けば次の刻みで付く)。"""
278
+ from thth import principals
279
+ approve(write(bb, [item(URL_A1)]))
280
+ principals.stop("hatena-main", "owner", actor="tester")
281
+ out, _ = run(bb)
282
+ assert out["blocked"]["code"] == "principal_stopped" and "until" not in out["blocked"]
283
+ principals.resume("hatena-main", by="tester")
284
+ run(bb, now=later(10))
285
+ assert states(bb) == ["placed"]