thth 2.3.1__tar.gz → 2.3.3__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 (255) hide show
  1. {thth-2.3.1 → thth-2.3.3}/PKG-INFO +3 -3
  2. {thth-2.3.1 → thth-2.3.3}/README.md +2 -2
  3. {thth-2.3.1 → thth-2.3.3}/skills/thth/SKILL.md +7 -3
  4. thth-2.3.3/tests/test_behind_remote.py +142 -0
  5. thth-2.3.3/tests/test_cli_vm_path.py +80 -0
  6. thth-2.3.3/tests/test_collect_engagements.py +175 -0
  7. thth-2.3.3/tests/test_pull_cli.py +96 -0
  8. thth-2.3.3/tests/test_read_cli_bare_runtimeerror.py +179 -0
  9. {thth-2.3.1 → thth-2.3.3}/tests/test_skill_doc.py +26 -0
  10. thth-2.3.3/thth/VERSION +1 -0
  11. {thth-2.3.1 → thth-2.3.3}/thth/cli.py +269 -1
  12. {thth-2.3.1 → thth-2.3.3}/thth/collect.py +56 -2
  13. {thth-2.3.1 → thth-2.3.3}/thth/thread_read.py +16 -0
  14. {thth-2.3.1 → thth-2.3.3}/thth/threads_read_cli.py +13 -0
  15. {thth-2.3.1 → thth-2.3.3}/thth/topic_cli.py +24 -0
  16. {thth-2.3.1 → thth-2.3.3}/thth/where_cli.py +10 -0
  17. {thth-2.3.1 → thth-2.3.3}/thth/who_cli.py +8 -0
  18. {thth-2.3.1 → thth-2.3.3}/thth/writeback.py +66 -0
  19. thth-2.3.1/thth/VERSION +0 -1
  20. {thth-2.3.1 → thth-2.3.3}/.gitignore +0 -0
  21. {thth-2.3.1 → thth-2.3.3}/LICENSE +0 -0
  22. {thth-2.3.1 → thth-2.3.3}/accounts.example/bluesky.json +0 -0
  23. {thth-2.3.1 → thth-2.3.3}/accounts.example/mastodon.json +0 -0
  24. {thth-2.3.1 → thth-2.3.3}/accounts.example/threads.json +0 -0
  25. {thth-2.3.1 → thth-2.3.3}/bin/thth +0 -0
  26. {thth-2.3.1 → thth-2.3.3}/bin/thth-remote +0 -0
  27. {thth-2.3.1 → thth-2.3.3}/bin/thth-run +0 -0
  28. {thth-2.3.1 → thth-2.3.3}/mcp/server.py +0 -0
  29. {thth-2.3.1 → thth-2.3.3}/pyproject.toml +0 -0
  30. {thth-2.3.1 → thth-2.3.3}/systemd/thth@.service +0 -0
  31. {thth-2.3.1 → thth-2.3.3}/tests/__init__.py +0 -0
  32. {thth-2.3.1 → thth-2.3.3}/tests/conftest.py +0 -0
  33. {thth-2.3.1 → thth-2.3.3}/tests/fixtures/build_injection_eval.py +0 -0
  34. {thth-2.3.1 → thth-2.3.3}/tests/fixtures/callback/worker_responses.json +0 -0
  35. {thth-2.3.1 → thth-2.3.3}/tests/fixtures/injection_eval.json +0 -0
  36. {thth-2.3.1 → thth-2.3.3}/tests/fixtures/umami-bile.md +0 -0
  37. {thth-2.3.1 → thth-2.3.3}/tests/helpers/__init__.py +0 -0
  38. {thth-2.3.1 → thth-2.3.3}/tests/helpers/fake_oauth_server.py +0 -0
  39. {thth-2.3.1 → thth-2.3.3}/tests/helpers/fake_push_adapter.py +0 -0
  40. {thth-2.3.1 → thth-2.3.3}/tests/helpers/fake_redirect_server.py +0 -0
  41. {thth-2.3.1 → thth-2.3.3}/tests/helpers/fake_search_adapter.py +0 -0
  42. {thth-2.3.1 → thth-2.3.3}/tests/helpers/fake_threads_reader.py +0 -0
  43. {thth-2.3.1 → thth-2.3.3}/tests/helpers/fake_threads_writes.py +0 -0
  44. {thth-2.3.1 → thth-2.3.3}/tests/helpers/hold_lock.py +0 -0
  45. {thth-2.3.1 → thth-2.3.3}/tests/helpers/hold_repo_lock.py +0 -0
  46. {thth-2.3.1 → thth-2.3.3}/tests/helpers/worker_probe.mjs +0 -0
  47. {thth-2.3.1 → thth-2.3.3}/tests/test_account_name_check.py +0 -0
  48. {thth-2.3.1 → thth-2.3.3}/tests/test_account_report.py +0 -0
  49. {thth-2.3.1 → thth-2.3.3}/tests/test_accounts_dir.py +0 -0
  50. {thth-2.3.1 → thth-2.3.3}/tests/test_adapter_boundary.py +0 -0
  51. {thth-2.3.1 → thth-2.3.3}/tests/test_after_cli.py +0 -0
  52. {thth-2.3.1 → thth-2.3.3}/tests/test_api_error_body.py +0 -0
  53. {thth-2.3.1 → thth-2.3.3}/tests/test_app_env_cli.py +0 -0
  54. {thth-2.3.1 → thth-2.3.3}/tests/test_approval.py +0 -0
  55. {thth-2.3.1 → thth-2.3.3}/tests/test_approve_cmd.py +0 -0
  56. {thth-2.3.1 → thth-2.3.3}/tests/test_ask.py +0 -0
  57. {thth-2.3.1 → thth-2.3.3}/tests/test_atlas_fifth_review.py +0 -0
  58. {thth-2.3.1 → thth-2.3.3}/tests/test_atlas_fourth_review.py +0 -0
  59. {thth-2.3.1 → thth-2.3.3}/tests/test_atlas_review.py +0 -0
  60. {thth-2.3.1 → thth-2.3.3}/tests/test_atlas_second_review.py +0 -0
  61. {thth-2.3.1 → thth-2.3.3}/tests/test_atlas_third_review.py +0 -0
  62. {thth-2.3.1 → thth-2.3.3}/tests/test_audit1_fixes.py +0 -0
  63. {thth-2.3.1 → thth-2.3.3}/tests/test_audit2_fixes.py +0 -0
  64. {thth-2.3.1 → thth-2.3.3}/tests/test_audit_b_counterexamples.py +0 -0
  65. {thth-2.3.1 → thth-2.3.3}/tests/test_audit_b_m1_semantics.py +0 -0
  66. {thth-2.3.1 → thth-2.3.3}/tests/test_audit_b_n1_depth.py +0 -0
  67. {thth-2.3.1 → thth-2.3.3}/tests/test_audit_b_r1_closure.py +0 -0
  68. {thth-2.3.1 → thth-2.3.3}/tests/test_audit_v201.py +0 -0
  69. {thth-2.3.1 → thth-2.3.3}/tests/test_audit_v23_details.py +0 -0
  70. {thth-2.3.1 → thth-2.3.3}/tests/test_auth.py +0 -0
  71. {thth-2.3.1 → thth-2.3.3}/tests/test_auth_identity_required.py +0 -0
  72. {thth-2.3.1 → thth-2.3.3}/tests/test_bluesky_adapter.py +0 -0
  73. {thth-2.3.1 → thth-2.3.3}/tests/test_bluesky_secret_scrub.py +0 -0
  74. {thth-2.3.1 → thth-2.3.3}/tests/test_board_needs_review.py +0 -0
  75. {thth-2.3.1 → thth-2.3.3}/tests/test_board_running.py +0 -0
  76. {thth-2.3.1 → thth-2.3.3}/tests/test_bundle.py +0 -0
  77. {thth-2.3.1 → thth-2.3.3}/tests/test_charcount.py +0 -0
  78. {thth-2.3.1 → thth-2.3.3}/tests/test_cli_entrypoint.py +0 -0
  79. {thth-2.3.1 → thth-2.3.3}/tests/test_cli_help.py +0 -0
  80. {thth-2.3.1 → thth-2.3.3}/tests/test_collect.py +0 -0
  81. {thth-2.3.1 → thth-2.3.3}/tests/test_collect_runs.py +0 -0
  82. {thth-2.3.1 → thth-2.3.3}/tests/test_collect_sent.py +0 -0
  83. {thth-2.3.1 → thth-2.3.3}/tests/test_comparability.py +0 -0
  84. {thth-2.3.1 → thth-2.3.3}/tests/test_control_chars_refused.py +0 -0
  85. {thth-2.3.1 → thth-2.3.3}/tests/test_conversation_paging.py +0 -0
  86. {thth-2.3.1 → thth-2.3.3}/tests/test_count_for.py +0 -0
  87. {thth-2.3.1 → thth-2.3.3}/tests/test_docs_links.py +0 -0
  88. {thth-2.3.1 → thth-2.3.3}/tests/test_doctor.py +0 -0
  89. {thth-2.3.1 → thth-2.3.3}/tests/test_doctor_all_permissions.py +0 -0
  90. {thth-2.3.1 → thth-2.3.3}/tests/test_doctor_next_step.py +0 -0
  91. {thth-2.3.1 → thth-2.3.3}/tests/test_duplicate_front_matter_keys.py +0 -0
  92. {thth-2.3.1 → thth-2.3.3}/tests/test_editorial_audit.py +0 -0
  93. {thth-2.3.1 → thth-2.3.3}/tests/test_editorial_n3_n4.py +0 -0
  94. {thth-2.3.1 → thth-2.3.3}/tests/test_engagements.py +0 -0
  95. {thth-2.3.1 → thth-2.3.3}/tests/test_external_acceptance.py +0 -0
  96. {thth-2.3.1 → thth-2.3.3}/tests/test_fake_api.py +0 -0
  97. {thth-2.3.1 → thth-2.3.3}/tests/test_form_spec.py +0 -0
  98. {thth-2.3.1 → thth-2.3.3}/tests/test_form_spec_cli.py +0 -0
  99. {thth-2.3.1 → thth-2.3.3}/tests/test_forms.py +0 -0
  100. {thth-2.3.1 → thth-2.3.3}/tests/test_fresh_install.py +0 -0
  101. {thth-2.3.1 → thth-2.3.3}/tests/test_front_matter_injection.py +0 -0
  102. {thth-2.3.1 → thth-2.3.3}/tests/test_hypotheses.py +0 -0
  103. {thth-2.3.1 → thth-2.3.3}/tests/test_hypothesis_boundaries.py +0 -0
  104. {thth-2.3.1 → thth-2.3.3}/tests/test_hypothesis_units.py +0 -0
  105. {thth-2.3.1 → thth-2.3.3}/tests/test_impact_cli.py +0 -0
  106. {thth-2.3.1 → thth-2.3.3}/tests/test_inbox_granted_scopes.py +0 -0
  107. {thth-2.3.1 → thth-2.3.3}/tests/test_independent_thread_review.py +0 -0
  108. {thth-2.3.1 → thth-2.3.3}/tests/test_independent_topic_review.py +0 -0
  109. {thth-2.3.1 → thth-2.3.3}/tests/test_independent_topic_review_r2.py +0 -0
  110. {thth-2.3.1 → thth-2.3.3}/tests/test_inflight.py +0 -0
  111. {thth-2.3.1 → thth-2.3.3}/tests/test_jst.py +0 -0
  112. {thth-2.3.1 → thth-2.3.3}/tests/test_lint.py +0 -0
  113. {thth-2.3.1 → thth-2.3.3}/tests/test_list_projections.py +0 -0
  114. {thth-2.3.1 → thth-2.3.3}/tests/test_llm_trial.py +0 -0
  115. {thth-2.3.1 → thth-2.3.3}/tests/test_llm_trial_engage.py +0 -0
  116. {thth-2.3.1 → thth-2.3.3}/tests/test_lock.py +0 -0
  117. {thth-2.3.1 → thth-2.3.3}/tests/test_m2_independent.py +0 -0
  118. {thth-2.3.1 → thth-2.3.3}/tests/test_maintain.py +0 -0
  119. {thth-2.3.1 → thth-2.3.3}/tests/test_mastodon_adapter.py +0 -0
  120. {thth-2.3.1 → thth-2.3.3}/tests/test_mastodon_visibility.py +0 -0
  121. {thth-2.3.1 → thth-2.3.3}/tests/test_max_per_run.py +0 -0
  122. {thth-2.3.1 → thth-2.3.3}/tests/test_mcp.py +0 -0
  123. {thth-2.3.1 → thth-2.3.3}/tests/test_mcp_robustness.py +0 -0
  124. {thth-2.3.1 → thth-2.3.3}/tests/test_measured.py +0 -0
  125. {thth-2.3.1 → thth-2.3.3}/tests/test_media_accounts.py +0 -0
  126. {thth-2.3.1 → thth-2.3.3}/tests/test_media_next_steps.py +0 -0
  127. {thth-2.3.1 → thth-2.3.3}/tests/test_media_roundtrip.py +0 -0
  128. {thth-2.3.1 → thth-2.3.3}/tests/test_mismatch_diagnostics.py +0 -0
  129. {thth-2.3.1 → thth-2.3.3}/tests/test_n5_reply_null_element.py +0 -0
  130. {thth-2.3.1 → thth-2.3.3}/tests/test_n6_replies_incomplete_ledger.py +0 -0
  131. {thth-2.3.1 → thth-2.3.3}/tests/test_n7_doctor_error_body.py +0 -0
  132. {thth-2.3.1 → thth-2.3.3}/tests/test_no_limits.py +0 -0
  133. {thth-2.3.1 → thth-2.3.3}/tests/test_observation_provenance.py +0 -0
  134. {thth-2.3.1 → thth-2.3.3}/tests/test_packaging.py +0 -0
  135. {thth-2.3.1 → thth-2.3.3}/tests/test_preview.py +0 -0
  136. {thth-2.3.1 → thth-2.3.3}/tests/test_production_mode.py +0 -0
  137. {thth-2.3.1 → thth-2.3.3}/tests/test_queue.py +0 -0
  138. {thth-2.3.1 → thth-2.3.3}/tests/test_quiet_hours.py +0 -0
  139. {thth-2.3.1 → thth-2.3.3}/tests/test_r2_r4_r5.py +0 -0
  140. {thth-2.3.1 → thth-2.3.3}/tests/test_reason_adoption.py +0 -0
  141. {thth-2.3.1 → thth-2.3.3}/tests/test_redact.py +0 -0
  142. {thth-2.3.1 → thth-2.3.3}/tests/test_redirect_token_leak.py +0 -0
  143. {thth-2.3.1 → thth-2.3.3}/tests/test_refresh.py +0 -0
  144. {thth-2.3.1 → thth-2.3.3}/tests/test_refresh_destination.py +0 -0
  145. {thth-2.3.1 → thth-2.3.3}/tests/test_rejudge_new.py +0 -0
  146. {thth-2.3.1 → thth-2.3.3}/tests/test_rejudge_r2_r3.py +0 -0
  147. {thth-2.3.1 → thth-2.3.3}/tests/test_replies.py +0 -0
  148. {thth-2.3.1 → thth-2.3.3}/tests/test_replies_refresh.py +0 -0
  149. {thth-2.3.1 → thth-2.3.3}/tests/test_repo_lock.py +0 -0
  150. {thth-2.3.1 → thth-2.3.3}/tests/test_repo_sync.py +0 -0
  151. {thth-2.3.1 → thth-2.3.3}/tests/test_reverse_audit.py +0 -0
  152. {thth-2.3.1 → thth-2.3.3}/tests/test_review_case_id_cluster_m2.py +0 -0
  153. {thth-2.3.1 → thth-2.3.3}/tests/test_review_case_id_cluster_r2.py +0 -0
  154. {thth-2.3.1 → thth-2.3.3}/tests/test_review_cli.py +0 -0
  155. {thth-2.3.1 → thth-2.3.3}/tests/test_review_evidence_account.py +0 -0
  156. {thth-2.3.1 → thth-2.3.3}/tests/test_review_form_spec_and_case_id.py +0 -0
  157. {thth-2.3.1 → thth-2.3.3}/tests/test_review_provenance.py +0 -0
  158. {thth-2.3.1 → thth-2.3.3}/tests/test_review_records.py +0 -0
  159. {thth-2.3.1 → thth-2.3.3}/tests/test_revoke.py +0 -0
  160. {thth-2.3.1 → thth-2.3.3}/tests/test_roundtrip.py +0 -0
  161. {thth-2.3.1 → thth-2.3.3}/tests/test_run_missing_env.py +0 -0
  162. {thth-2.3.1 → thth-2.3.3}/tests/test_run_record_failclosed.py +0 -0
  163. {thth-2.3.1 → thth-2.3.3}/tests/test_runs_collected.py +0 -0
  164. {thth-2.3.1 → thth-2.3.3}/tests/test_runs_read_rows.py +0 -0
  165. {thth-2.3.1 → thth-2.3.3}/tests/test_schedule_multiple.py +0 -0
  166. {thth-2.3.1 → thth-2.3.3}/tests/test_schedule_view.py +0 -0
  167. {thth-2.3.1 → thth-2.3.3}/tests/test_scopes_source_requested.py +0 -0
  168. {thth-2.3.1 → thth-2.3.3}/tests/test_search_engage_targets.py +0 -0
  169. {thth-2.3.1 → thth-2.3.3}/tests/test_secret_value_redact.py +0 -0
  170. {thth-2.3.1 → thth-2.3.3}/tests/test_secrets_grep.py +0 -0
  171. {thth-2.3.1 → thth-2.3.3}/tests/test_security_small_fixes.py +0 -0
  172. {thth-2.3.1 → thth-2.3.3}/tests/test_select.py +0 -0
  173. {thth-2.3.1 → thth-2.3.3}/tests/test_selfupdate.py +0 -0
  174. {thth-2.3.1 → thth-2.3.3}/tests/test_send.py +0 -0
  175. {thth-2.3.1 → thth-2.3.3}/tests/test_send_cli.py +0 -0
  176. {thth-2.3.1 → thth-2.3.3}/tests/test_send_media.py +0 -0
  177. {thth-2.3.1 → thth-2.3.3}/tests/test_send_visibility.py +0 -0
  178. {thth-2.3.1 → thth-2.3.3}/tests/test_sent_integrity.py +0 -0
  179. {thth-2.3.1 → thth-2.3.3}/tests/test_server_json.py +0 -0
  180. {thth-2.3.1 → thth-2.3.3}/tests/test_site.py +0 -0
  181. {thth-2.3.1 → thth-2.3.3}/tests/test_systemd.py +0 -0
  182. {thth-2.3.1 → thth-2.3.3}/tests/test_thread_publish.py +0 -0
  183. {thth-2.3.1 → thth-2.3.3}/tests/test_thread_read.py +0 -0
  184. {thth-2.3.1 → thth-2.3.3}/tests/test_threadrun.py +0 -0
  185. {thth-2.3.1 → thth-2.3.3}/tests/test_threads_read_permissions.py +0 -0
  186. {thth-2.3.1 → thth-2.3.3}/tests/test_threadshape.py +0 -0
  187. {thth-2.3.1 → thth-2.3.3}/tests/test_thth_root.py +0 -0
  188. {thth-2.3.1 → thth-2.3.3}/tests/test_token_exists.py +0 -0
  189. {thth-2.3.1 → thth-2.3.3}/tests/test_token_scopes_record.py +0 -0
  190. {thth-2.3.1 → thth-2.3.3}/tests/test_token_set.py +0 -0
  191. {thth-2.3.1 → thth-2.3.3}/tests/test_topic.py +0 -0
  192. {thth-2.3.1 → thth-2.3.3}/tests/test_topic_advice.py +0 -0
  193. {thth-2.3.1 → thth-2.3.3}/tests/test_topic_cli.py +0 -0
  194. {thth-2.3.1 → thth-2.3.3}/tests/test_topic_models.py +0 -0
  195. {thth-2.3.1 → thth-2.3.3}/tests/test_topic_review_fixes.py +0 -0
  196. {thth-2.3.1 → thth-2.3.3}/tests/test_topic_shelf_observers.py +0 -0
  197. {thth-2.3.1 → thth-2.3.3}/tests/test_topic_store.py +0 -0
  198. {thth-2.3.1 → thth-2.3.3}/tests/test_topics_record_concurrent.py +0 -0
  199. {thth-2.3.1 → thth-2.3.3}/tests/test_topics_tool.py +0 -0
  200. {thth-2.3.1 → thth-2.3.3}/tests/test_trial_frictions.py +0 -0
  201. {thth-2.3.1 → thth-2.3.3}/tests/test_v21b_approved_writes.py +0 -0
  202. {thth-2.3.1 → thth-2.3.3}/tests/test_value_domains_shown.py +0 -0
  203. {thth-2.3.1 → thth-2.3.3}/tests/test_version.py +0 -0
  204. {thth-2.3.1 → thth-2.3.3}/tests/test_where_cli.py +0 -0
  205. {thth-2.3.1 → thth-2.3.3}/tests/test_who_cli.py +0 -0
  206. {thth-2.3.1 → thth-2.3.3}/tests/test_writeback_rebase.py +0 -0
  207. {thth-2.3.1 → thth-2.3.3}/tests/test_writeback_validate.py +0 -0
  208. {thth-2.3.1 → thth-2.3.3}/thth/__init__.py +0 -0
  209. {thth-2.3.1 → thth-2.3.3}/thth/__main__.py +0 -0
  210. {thth-2.3.1 → thth-2.3.3}/thth/account_cli.py +0 -0
  211. {thth-2.3.1 → thth-2.3.3}/thth/account_report.py +0 -0
  212. {thth-2.3.1 → thth-2.3.3}/thth/accounts.py +0 -0
  213. {thth-2.3.1 → thth-2.3.3}/thth/adapters/__init__.py +0 -0
  214. {thth-2.3.1 → thth-2.3.3}/thth/adapters/base.py +0 -0
  215. {thth-2.3.1 → thth-2.3.3}/thth/adapters/bluesky.py +0 -0
  216. {thth-2.3.1 → thth-2.3.3}/thth/adapters/mastodon.py +0 -0
  217. {thth-2.3.1 → thth-2.3.3}/thth/adapters/threads.py +0 -0
  218. {thth-2.3.1 → thth-2.3.3}/thth/after_cli.py +0 -0
  219. {thth-2.3.1 → thth-2.3.3}/thth/appenv.py +0 -0
  220. {thth-2.3.1 → thth-2.3.3}/thth/approval.py +0 -0
  221. {thth-2.3.1 → thth-2.3.3}/thth/ask.py +0 -0
  222. {thth-2.3.1 → thth-2.3.3}/thth/ask_cli.py +0 -0
  223. {thth-2.3.1 → thth-2.3.3}/thth/bundle.py +0 -0
  224. {thth-2.3.1 → thth-2.3.3}/thth/core.py +0 -0
  225. {thth-2.3.1 → thth-2.3.3}/thth/doctor.py +0 -0
  226. {thth-2.3.1 → thth-2.3.3}/thth/engagements.py +0 -0
  227. {thth-2.3.1 → thth-2.3.3}/thth/forms.py +0 -0
  228. {thth-2.3.1 → thth-2.3.3}/thth/httpsafe.py +0 -0
  229. {thth-2.3.1 → thth-2.3.3}/thth/inflight.py +0 -0
  230. {thth-2.3.1 → thth-2.3.3}/thth/jst.py +0 -0
  231. {thth-2.3.1 → thth-2.3.3}/thth/lint.py +0 -0
  232. {thth-2.3.1 → thth-2.3.3}/thth/lock.py +0 -0
  233. {thth-2.3.1 → thth-2.3.3}/thth/maintain.py +0 -0
  234. {thth-2.3.1 → thth-2.3.3}/thth/measured.py +0 -0
  235. {thth-2.3.1 → thth-2.3.3}/thth/oauth.py +0 -0
  236. {thth-2.3.1 → thth-2.3.3}/thth/postid.py +0 -0
  237. {thth-2.3.1 → thth-2.3.3}/thth/queuefile.py +0 -0
  238. {thth-2.3.1 → thth-2.3.3}/thth/redact.py +0 -0
  239. {thth-2.3.1 → thth-2.3.3}/thth/replies.py +0 -0
  240. {thth-2.3.1 → thth-2.3.3}/thth/report.py +0 -0
  241. {thth-2.3.1 → thth-2.3.3}/thth/retract_cli.py +0 -0
  242. {thth-2.3.1 → thth-2.3.3}/thth/runs.py +0 -0
  243. {thth-2.3.1 → thth-2.3.3}/thth/scopes.py +0 -0
  244. {thth-2.3.1 → thth-2.3.3}/thth/secrets_fs.py +0 -0
  245. {thth-2.3.1 → thth-2.3.3}/thth/select.py +0 -0
  246. {thth-2.3.1 → thth-2.3.3}/thth/selfupdate.py +0 -0
  247. {thth-2.3.1 → thth-2.3.3}/thth/sent.py +0 -0
  248. {thth-2.3.1 → thth-2.3.3}/thth/systemd_gen.py +0 -0
  249. {thth-2.3.1 → thth-2.3.3}/thth/threadrun.py +0 -0
  250. {thth-2.3.1 → thth-2.3.3}/thth/threadshape.py +0 -0
  251. {thth-2.3.1 → thth-2.3.3}/thth/threadthrow.py +0 -0
  252. {thth-2.3.1 → thth-2.3.3}/thth/topic_advice.py +0 -0
  253. {thth-2.3.1 → thth-2.3.3}/thth/topic_models.py +0 -0
  254. {thth-2.3.1 → thth-2.3.3}/thth/topic_store.py +0 -0
  255. {thth-2.3.1 → thth-2.3.3}/thth/topics.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: thth
3
- Version: 2.3.1
3
+ Version: 2.3.3
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
@@ -45,7 +45,7 @@ watchtower(`~/Developer/watchtower`)の隣に同じ流儀で並べる。watc
45
45
 
46
46
  ## いまの状態(2026-09-13)
47
47
 
48
- 版 **2.3.1**。全件テスト **2287 件**(`python -m pytest tests/ -q -n auto -p no:cacheprovider`・1 skip・rc=0)。
48
+ 版 **2.3.3**。全件テスト **2320 件**(`python -m pytest tests/ -q -n auto -p no:cacheprovider`・1 skip・rc=0)。
49
49
 
50
50
  **媒体**: Threads(稼働)・Bluesky・Mastodon(同席用の台帳あり・未稼働)。
51
51
 
@@ -79,7 +79,7 @@ timer(systemd・`thth systemd` で生成)で毎時投稿、返信の採集
79
79
  - **英語の文書**: [README.en.md](README.en.md)・[docs/usage.en.md](docs/usage.en.md)・[llms.txt](llms.txt)。
80
80
  - **skill**: `skills/thth/SKILL.md`(wheel にも入る)。
81
81
 
82
- - **動くもの**(`thth --help` の全 34 サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`who`・`retract`・`location`。
82
+ - **動くもの**(`thth --help` の全 35 サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`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`。
83
83
  - **最初の本番投稿の記録**: 2026-09-09、@aoking に疎通確認を 1 本(`17916074118445631`)。
84
84
  - **未着手**: X・Facebook ページ・Instagram の各アダプタ。トピック検索の権限(tester には降りない)。泉のサーバ(v2-5)。
85
85
  - **権限の制約**: tester に降りる scope は 5 つ。削除はできない。
@@ -21,7 +21,7 @@ watchtower(`~/Developer/watchtower`)の隣に同じ流儀で並べる。watc
21
21
 
22
22
  ## いまの状態(2026-09-13)
23
23
 
24
- 版 **2.3.1**。全件テスト **2287 件**(`python -m pytest tests/ -q -n auto -p no:cacheprovider`・1 skip・rc=0)。
24
+ 版 **2.3.3**。全件テスト **2320 件**(`python -m pytest tests/ -q -n auto -p no:cacheprovider`・1 skip・rc=0)。
25
25
 
26
26
  **媒体**: Threads(稼働)・Bluesky・Mastodon(同席用の台帳あり・未稼働)。
27
27
 
@@ -55,7 +55,7 @@ timer(systemd・`thth systemd` で生成)で毎時投稿、返信の採集
55
55
  - **英語の文書**: [README.en.md](README.en.md)・[docs/usage.en.md](docs/usage.en.md)・[llms.txt](llms.txt)。
56
56
  - **skill**: `skills/thth/SKILL.md`(wheel にも入る)。
57
57
 
58
- - **動くもの**(`thth --help` の全 34 サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`who`・`retract`・`location`。
58
+ - **動くもの**(`thth --help` の全 35 サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`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`。
59
59
  - **最初の本番投稿の記録**: 2026-09-09、@aoking に疎通確認を 1 本(`17916074118445631`)。
60
60
  - **未着手**: X・Facebook ページ・Instagram の各アダプタ。トピック検索の権限(tester には降りない)。泉のサーバ(v2-5)。
61
61
  - **権限の制約**: tester に降りる scope は 5 つ。削除はできない。
@@ -12,7 +12,8 @@ description: 投稿する前・返信を書く前・出したあと・絡みに
12
12
  - **`--production` を付けるまで出ない。** 付けない実行は、投げるはずの本文と digest を
13
13
  表示して終わる。
14
14
  - **記録は利用者の git に残る。** post_id・承認者・承認時刻・返信・実測は、あなたの repo の
15
- commit として残る。THTH のサーバには何も無い。
15
+ commit として残る。THTH のサーバには何も無い。絡みの台帳(`data/sns/engagements/`)は
16
+ 公開の commit とは別で、**次の採取(`thth collect`・最大 10 分の遅れ)**で commit される。
16
17
  - **数値には分母が付く。** 件数・期間・揃えた条件が無い数字は返さない。取れていない刻みは
17
18
  `null` であって `0` ではない。
18
19
  - **黙って間違えない。** 拒むときは理由と次の一手を言って非ゼロで終わる(loud reject)。
@@ -30,8 +31,11 @@ description: 投稿する前・返信を書く前・出したあと・絡みに
30
31
  - **何を書くかを決めない。** 文体・頻度・時刻・語の選び方はあなたのプロジェクトのもの。
31
32
  渡された本文を**整形も切り詰めもせず**そのまま出す。
32
33
  - **読む口は何も保存しない。** `thread_read`・`where_to_appear`・`who_is_this` はどれも、
33
- 読んで見せるだけで**台帳に 1 バイトも書かない**。残るのは**自分の行為と反応**
34
- (絡みの台帳)だけで、**相手の本文・相手の名前・あなたの判断は台帳に入らない**。
34
+ 読んで見せるだけで**台帳に 1 バイトも書かない**。この 3 つと絡みの台帳
35
+ (`data/sns/engagements/`)だけを見れば、残るのは**自分の行為と反応**だけで、
36
+ **相手の本文・相手の名前・あなたの判断は入らない**。**別の台帳**——自分の投稿への
37
+ 返信の台帳(`thth collect` が採り、`thth replies` で読む)——は、この規律の外で
38
+ 相手の `username` と本文をそのまま残す(読む口でもなく、絡みの台帳でもない)。
35
39
  - **人の代わりに承認しない。** あなた(エージェント)は下書きと digest を人に見せ、
36
40
  人が `--confirm` を打つ。
37
41
 
@@ -0,0 +1,142 @@
1
+ """`writeback.behind_remote()` と `thth queue`/`schedule`/`board` の遅れ表示
2
+ (発注 T8-2・kopicha 続報)。
3
+
4
+ `thth approve`(と `revoke`・`throw`・`collect`・`retract`)は既に
5
+ `writeback.sync_repo()` を通しているが、**読むだけの口**(`queue`・
6
+ `schedule`・`board`)は通していない。push した直後に読むだけの口を見ると、
7
+ timer の `collect` が pull するまでの間、まだ取り込み前の一覧が出る
8
+ ——**読むだけの口が勝手に pull するのは避ける**のが設計の芯なので、
9
+ ここでは「遅れていることを言う」だけを確かめる(取り込みは T8-3 の
10
+ `thth pull` か `thth approve` が行う)。
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from tests.conftest import init_git_pair, run_git, run_thth, write_queue_file
15
+ from thth import writeback
16
+
17
+
18
+ def test_behind_remoteは追いついていれば0(tmp_path):
19
+ pair = init_git_pair(tmp_path / "a", seed_content="", seed_name=".gitkeep")
20
+ info = writeback.behind_remote(pair["work"])
21
+ assert info == {"behind": 0, "ahead": 0, "head": info["head"],
22
+ "fetched_at": info["fetched_at"], "reason": None}
23
+ assert info["head"]
24
+ assert info["reason"] is None
25
+
26
+
27
+ def test_behind_remoteはfetchだけでpullしない(tmp_path):
28
+ """**pull はしない**——fetch のあと、work の作業ツリー(HEAD)は動かない。"""
29
+ pair = init_git_pair(tmp_path / "a", seed_content="旧", seed_name="a.md")
30
+ head_before = run_git(pair["work"], ["rev-parse", "HEAD"]).stdout.strip()
31
+
32
+ run_git(pair["seed"], ["commit", "--allow-empty", "-m", "second"])
33
+ run_git(pair["seed"], ["push"])
34
+
35
+ info = writeback.behind_remote(pair["work"])
36
+ assert info["behind"] == 1
37
+ assert info["ahead"] == 0
38
+
39
+ head_after = run_git(pair["work"], ["rev-parse", "HEAD"]).stdout.strip()
40
+ assert head_after == head_before, "fetch だけのはずが HEAD が動いた(pull してしまった)"
41
+
42
+
43
+ def test_behind_remoteはfetchできないとNoneとreason(tmp_path):
44
+ broken = tmp_path / "broken"
45
+ run_git_init = __import__("subprocess").run(
46
+ ["git", "init", "-b", "main", str(broken)], capture_output=True, text=True)
47
+ assert run_git_init.returncode == 0
48
+ run_git(str(broken), ["remote", "add", "origin", "/does/not/exist/repo.git"])
49
+ # **git の名前とメールを repo に持たせる**(`conftest.init_git_pair()` と同じ)。
50
+ # 開発機(macOS)は全体の設定があるので通っていたが、GitHub Actions の
51
+ # Linux には無く、`commit` が "Author identity unknown" で落ちた(2026-09-16)。
52
+ run_git(str(broken), ["config", "user.email", "thth-test@example.invalid"])
53
+ run_git(str(broken), ["config", "user.name", "thth-test"])
54
+ run_git(str(broken), ["commit", "--allow-empty", "-m", "x"])
55
+
56
+ info = writeback.behind_remote(str(broken))
57
+ assert info["behind"] is None
58
+ assert info["ahead"] is None
59
+ assert info["reason"]
60
+ assert "0" != info["reason"] # **0 と混ぜない**: reason があるのに behind: 0 ではない
61
+
62
+
63
+ def test_queueは遅れているときに先頭に1行(isolated_account_factory, tmp_path):
64
+ pair = init_git_pair(tmp_path / "acct", seed_content="", seed_name=".gitkeep")
65
+ account = isolated_account_factory(repo_dir=pair["work"])
66
+ write_queue_file(account["queue_dir"], "a.md",
67
+ fm_overrides={"status": "draft", "account": account["name"]})
68
+
69
+ # **`seed` を origin の最新に合わせてから**新しい commit を積む(`work` の
70
+ # `write_queue_file()` push で origin が進んでいるため、合わせないと
71
+ # `seed` の push 自体が non-fast-forward で失敗する)。
72
+ run_git(pair["seed"], ["pull"])
73
+ run_git(pair["seed"], ["commit", "--allow-empty", "-m", "second"])
74
+ run_git(pair["seed"], ["push"])
75
+
76
+ result = run_thth(["queue", account["name"]])
77
+ assert result.returncode == 0, result.stderr
78
+ assert "remote に 1 commit 分の新しいものがあります" in result.stdout
79
+ assert f"thth pull {account['name']}" in result.stdout
80
+
81
+
82
+ def test_queueは遅れていなければ何も出さない(isolated_account_factory, tmp_path):
83
+ pair = init_git_pair(tmp_path / "acct", seed_content="", seed_name=".gitkeep")
84
+ account = isolated_account_factory(repo_dir=pair["work"])
85
+ write_queue_file(account["queue_dir"], "a.md",
86
+ fm_overrides={"status": "draft", "account": account["name"]})
87
+
88
+ result = run_thth(["queue", account["name"]])
89
+ assert result.returncode == 0, result.stderr
90
+ assert "remote に" not in result.stdout
91
+ assert "thth pull" not in result.stdout
92
+
93
+
94
+ def test_queueのjsonにrepoが乗る(isolated_account_factory, tmp_path):
95
+ pair = init_git_pair(tmp_path / "acct", seed_content="", seed_name=".gitkeep")
96
+ account = isolated_account_factory(repo_dir=pair["work"])
97
+ write_queue_file(account["queue_dir"], "a.md",
98
+ fm_overrides={"status": "draft", "account": account["name"]})
99
+
100
+ result = run_thth(["queue", account["name"], "--json"])
101
+ assert result.returncode == 0, result.stderr
102
+ import json
103
+ out = json.loads(result.stdout)
104
+ assert out[account["name"]]["repo"]["behind"] == 0
105
+
106
+
107
+ def test_scheduleは遅れているときに先頭に1行(isolated_account_factory, tmp_path):
108
+ pair = init_git_pair(tmp_path / "acct", seed_content="", seed_name=".gitkeep")
109
+ account = isolated_account_factory(repo_dir=pair["work"])
110
+ write_queue_file(account["queue_dir"], "a.md",
111
+ fm_overrides={"status": "draft", "account": account["name"],
112
+ "publish_at": "2026-09-20T08:00:00+09:00"})
113
+
114
+ run_git(pair["seed"], ["pull"])
115
+ run_git(pair["seed"], ["commit", "--allow-empty", "-m", "second"])
116
+ run_git(pair["seed"], ["push"])
117
+
118
+ result = run_thth(["schedule", account["name"]])
119
+ assert result.returncode == 0, result.stderr
120
+ assert "remote に 1 commit 分の新しいものがあります" in result.stdout
121
+
122
+
123
+ def test_boardは遅れているaccountの行の下に1行(isolated_account_factory, tmp_path):
124
+ pair = init_git_pair(tmp_path / "acct", seed_content="", seed_name=".gitkeep")
125
+ account = isolated_account_factory(repo_dir=pair["work"])
126
+
127
+ run_git(pair["seed"], ["commit", "--allow-empty", "-m", "second"])
128
+ run_git(pair["seed"], ["push"])
129
+
130
+ result = run_thth(["board"])
131
+ assert result.returncode == 0, result.stderr
132
+ assert "repo が 1 commit 遅れています" in result.stdout
133
+ assert f"thth pull {account['name']}" in result.stdout
134
+
135
+
136
+ def test_boardは遅れていなければ何も出さない(isolated_account_factory, tmp_path):
137
+ pair = init_git_pair(tmp_path / "acct", seed_content="", seed_name=".gitkeep")
138
+ isolated_account_factory(repo_dir=pair["work"])
139
+
140
+ result = run_thth(["board"])
141
+ assert result.returncode == 0, result.stderr
142
+ assert "repo が" not in result.stdout
@@ -0,0 +1,80 @@
1
+ """VM に無いパスを渡したときの断り方(発注 T8-1・kopicha 続報)。
2
+
3
+ `~/.local/bin/thth` は Mac から VM へ ssh する薄いラッパで、引数はそのまま
4
+ VM 側の `thth` に渡る。**Mac 側の相対パスも絶対パスも VM には無い**ので、
5
+ いままでは `FileNotFoundError` の素の traceback が出ていた(`--article
6
+ /tmp/x.json` で踏んだのと同じ根)。
7
+
8
+ ファイルを受け取る全ての引数(`lint`・`approve`・`revoke`・`preview`・
9
+ `send --text-file`・`topics suggest` の queue ファイル・`--article`・
10
+ `--proposal`・`--input`・`--draft`・`--revised-draft`)が、共通の
11
+ `cli._require_vm_path()` を通って rc=2・案内つきで断ることを確かめる。
12
+ 存在するパスは従来どおり動くことも確かめる(回帰)。
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import os
17
+
18
+ from tests.conftest import run_thth, write_queue_file
19
+
20
+ MISSING = "/tmp/thth-t8-does-not-exist-まさかここには無い.md"
21
+ MISSING_JSON = "/tmp/thth-t8-does-not-exist-まさかここには無い.json"
22
+
23
+
24
+ def _assert_vm_refusal(result, *, rc=2):
25
+ combined = result.stdout + result.stderr
26
+ assert result.returncode == rc, f"rc={result.returncode}: {combined}"
27
+ assert "VM" in combined, combined
28
+ assert "thth queue" in combined, combined
29
+ assert "Traceback" not in result.stderr, result.stderr
30
+
31
+
32
+ def test_lintにVMに無いパスを渡すとrc2で案内する():
33
+ result = run_thth(["lint", MISSING])
34
+ _assert_vm_refusal(result)
35
+
36
+
37
+ def test_approveにVMに無いパスを渡すとrc2で案内する():
38
+ result = run_thth(["approve", MISSING])
39
+ _assert_vm_refusal(result)
40
+
41
+
42
+ def test_sendのtext_fileにVMに無いパスを渡すとrc2で案内する(isolated_account):
43
+ result = run_thth(["send", isolated_account["name"], "--text-file", MISSING])
44
+ _assert_vm_refusal(result)
45
+
46
+
47
+ def test_topics_suggestのarticleにVMに無いパスを渡すとrc2で案内する(isolated_account):
48
+ queue_path = write_queue_file(
49
+ isolated_account["queue_dir"], "q.md",
50
+ body="## threads\n\nhello https://example.test/x\n",
51
+ fm_overrides={"status": "draft", "account": isolated_account["name"]})
52
+ result = run_thth(["topics", "suggest", queue_path, "--article", MISSING_JSON])
53
+ _assert_vm_refusal(result)
54
+
55
+
56
+ def test_revokeにVMに無いパスを渡すとrc2で案内する():
57
+ result = run_thth(["revoke", MISSING, "--by", "test"])
58
+ _assert_vm_refusal(result)
59
+
60
+
61
+ def test_previewにVMに無いパスを渡すとrc2で案内する():
62
+ result = run_thth(["preview", MISSING])
63
+ _assert_vm_refusal(result)
64
+
65
+
66
+ def test_存在するパスは従来どおり動く(isolated_account):
67
+ """回帰: 存在するパスには案内を出さない(発注書「存在するパスは従来どおり」)。"""
68
+ queue_path = write_queue_file(
69
+ isolated_account["queue_dir"], "ok.md",
70
+ body="## threads\n\nこんにちは https://example.test/x\n",
71
+ fm_overrides={"status": "draft", "account": isolated_account["name"]})
72
+ assert os.path.exists(queue_path)
73
+
74
+ result = run_thth(["lint", queue_path])
75
+ assert "VM" not in (result.stdout + result.stderr)
76
+ assert "thth queue" not in (result.stdout + result.stderr)
77
+
78
+ result = run_thth(["preview", queue_path])
79
+ assert result.returncode == 0, result.stderr
80
+ assert "こんにちは" in result.stdout
@@ -0,0 +1,175 @@
1
+ """絡みの台帳(`data/sns/engagements/`)を採取の commit に含める(T9-1・
2
+ 照合「X Developer Agreement と自分の泉」検収 1)。
3
+
4
+ **事実**: 投稿の commit(`core.py` の `writeback.commit_and_push(rel_path=原稿)`)は
5
+ 原稿ファイルだけ、採取の commit(`collect.py` の `result["touched"]`)は採取で
6
+ 触ったファイルだけを add する。同席送信(`core._send_locked`)は commit しない。
7
+ `thth/engagements.py::append()` が書く `data/sns/engagements/<YYYY-MM>.ndjson` は
8
+ どこからも add されず、VM の repo に未追跡のまま溜まっていた。
9
+
10
+ 固定するのは 4 つ(発注 T9-1 の (a)〜(d)):
11
+
12
+ (a) `engagements.append` で 1 行書いた repo で `collect` を 1 回 → origin に
13
+ `data/sns/engagements/<月>.ndjson` が push されている。
14
+ (b) 採取で触るものが 0 でも (a) と同じ。
15
+ (c) repo が無い account では git を呼ばない。
16
+ (d) 何も変わっていなければ空の commit を作らない。
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import datetime
21
+ import json
22
+ import os
23
+ import subprocess
24
+
25
+ from tests.conftest import init_git_pair, make_queue_text
26
+ from tests.test_collect import FakeAdapter
27
+ from thth import accounts as accounts_mod
28
+ from thth import collect as collect_mod
29
+ from thth import engagements as engagements_mod
30
+ from thth import jst
31
+
32
+ NOW = datetime.datetime(2026, 9, 16, 12, 0, tzinfo=jst.JST)
33
+
34
+
35
+ class NoOpAdapter:
36
+ """能力を何も名乗らない偽アダプタ(`collect_once()` に何も触らせない)。
37
+
38
+ `tests/test_collect_sent.py` の `FakeAdapter` と違い `views` すら持たない
39
+ ——**採取が触るものを 0 にする**ためだけの道具。
40
+ """
41
+
42
+ CAPABILITIES = frozenset()
43
+
44
+ @classmethod
45
+ def capabilities(cls):
46
+ return set(cls.CAPABILITIES)
47
+
48
+ def insights(self, post_id):
49
+ raise AssertionError("能力を名乗っていないのに insights を呼んだ")
50
+
51
+ def conversation(self, post_id, *, since=None):
52
+ raise AssertionError("能力を名乗っていないのに conversation を呼んだ")
53
+
54
+ def account_insights(self, user_id, *, since, until):
55
+ raise AssertionError("能力を名乗っていないのに account_insights を呼んだ")
56
+
57
+
58
+ def _engagement_row(**over):
59
+ row = {
60
+ "schema": engagements_mod.SCHEMA,
61
+ "post_id": "111",
62
+ "reply_to": "222",
63
+ "root_post": None,
64
+ "author_key": None,
65
+ "account": None, # append() 側の既定に任せる
66
+ "medium": "threads",
67
+ "topic": None,
68
+ "form": None,
69
+ "hour_band": "朝",
70
+ "posted_at": "2026-09-16T08:00:00+09:00",
71
+ "found_by": None,
72
+ }
73
+ row.update(over)
74
+ return row
75
+
76
+
77
+ def _in_origin(bare: str) -> str:
78
+ return subprocess.run(
79
+ ["git", "-C", bare, "ls-tree", "-r", "--name-only", "main"],
80
+ capture_output=True, text=True,
81
+ ).stdout
82
+
83
+
84
+ def test_a_絡みの台帳が採取のcommitでpushされる(tmp_path, isolated_account_factory):
85
+ """採取が投稿本体も触る、ふつうの回。絡みの台帳も同じ commit に乗って
86
+ origin まで届く。"""
87
+ pair = init_git_pair(tmp_path, seed_content=make_queue_text({
88
+ "status": "posted", "post_id": "POST1",
89
+ "posted_at": "2026-09-16T10:00:00+09:00"})) # 2 時間前・1h の刻みを跨ぐ
90
+ account = isolated_account_factory(repo_dir=pair["work"], production=True)
91
+
92
+ cfg = accounts_mod.load_account(account["name"])
93
+ engagements_mod.append(cfg, account["name"],
94
+ _engagement_row(account=account["name"]), now=NOW)
95
+
96
+ rc = collect_mod.run_collect(account["name"], adapter=FakeAdapter(), now=NOW,
97
+ log=lambda _l: None)
98
+
99
+ assert rc == 0
100
+ in_origin = _in_origin(pair["bare"])
101
+ assert "data/sns/engagements/2026-09.ndjson" in in_origin, in_origin
102
+ # ついでに、投稿の実測も同じ commit の対象として届いていること
103
+ # (足した処理が既存の touched を壊していないこと)。
104
+ assert "data/sns/insights/posts/POST1.ndjson" in in_origin, in_origin
105
+
106
+
107
+ def test_b_採取で触るものが0でも絡みの台帳はpushされる(tmp_path, isolated_account_factory):
108
+ """`collect_once()` の `touched` が空でも、絡みの台帳の未追跡・変更だけで
109
+ commit する。"""
110
+ pair = init_git_pair(tmp_path, seed_content=make_queue_text({
111
+ "status": "posted", "post_id": "POST1",
112
+ # collect_days(既定 14 日)をとうに過ぎている → posts_seen は 0、
113
+ # touched も空のまま(NoOpAdapter は何も能力を名乗らないので
114
+ # アカウント日次も呼ばれない)。
115
+ "posted_at": "2026-08-01T10:00:00+09:00"}))
116
+ account = isolated_account_factory(repo_dir=pair["work"], production=True)
117
+
118
+ cfg = accounts_mod.load_account(account["name"])
119
+ engagements_mod.append(cfg, account["name"],
120
+ _engagement_row(account=account["name"]), now=NOW)
121
+
122
+ rc = collect_mod.run_collect(account["name"], adapter=NoOpAdapter(), now=NOW,
123
+ log=lambda _l: None)
124
+
125
+ assert rc == 0
126
+ in_origin = _in_origin(pair["bare"])
127
+ assert "data/sns/engagements/2026-09.ndjson" in in_origin, in_origin
128
+ # 投稿側は本当に何も触っていないこと(この test が (a) の焼き直しに
129
+ # なっていないことの確認)。
130
+ assert "data/sns/insights/posts/POST1.ndjson" not in in_origin, in_origin
131
+
132
+
133
+ def test_c_repoが無いaccountでは絡みの台帳も含めてgitを呼ばない(
134
+ tmp_path, isolated_account_factory, monkeypatch):
135
+ """`repo_dir` が実在しない台帳(`repos/_none` 相当)では、絡みの台帳の
136
+ 判定そのもの(`_pending_engagement_paths`)を呼ばない——呼べば
137
+ `git status` を repo の外で走らせる危険がある。"""
138
+ account = isolated_account_factory(
139
+ repo_dir=str(tmp_path / "repos" / "_none"), production=True, scheduled=False)
140
+
141
+ def 呼ぶな(*a, **k):
142
+ raise AssertionError("repo が無いのに絡みの台帳の git 判定を呼んだ")
143
+
144
+ monkeypatch.setattr(collect_mod, "_pending_engagement_paths", 呼ぶな)
145
+ monkeypatch.setattr(collect_mod, "_git", 呼ぶな)
146
+ monkeypatch.setattr(collect_mod.writeback, "sync_repo", 呼ぶな)
147
+ monkeypatch.setattr(collect_mod.writeback, "commit_and_push", 呼ぶな)
148
+
149
+ rc = collect_mod.run_collect(account["name"], adapter=NoOpAdapter(), now=NOW,
150
+ log=lambda _l: None)
151
+ assert rc == 0
152
+
153
+
154
+ def test_d_変わっていなければ空のcommitを作らない(tmp_path, isolated_account_factory,
155
+ monkeypatch):
156
+ """絡みの台帳に変更が無い回は、`commit_and_push` を一切呼ばない。"""
157
+ pair = init_git_pair(tmp_path, seed_content=make_queue_text({
158
+ "status": "posted", "post_id": "POST1",
159
+ "posted_at": "2026-08-01T10:00:00+09:00"})) # collect_days を過ぎている
160
+ account = isolated_account_factory(repo_dir=pair["work"], production=True)
161
+
162
+ calls = []
163
+ real_commit_and_push = collect_mod.writeback.commit_and_push
164
+
165
+ def 数える(*a, **k):
166
+ calls.append((a, k))
167
+ return real_commit_and_push(*a, **k)
168
+
169
+ monkeypatch.setattr(collect_mod.writeback, "commit_and_push", 数える)
170
+
171
+ rc = collect_mod.run_collect(account["name"], adapter=NoOpAdapter(), now=NOW,
172
+ log=lambda _l: None)
173
+
174
+ assert rc == 0
175
+ assert calls == [], "変更が無いのに commit_and_push を呼んだ"
@@ -0,0 +1,96 @@
1
+ """`thth pull <account|--project P>`(発注 T8-3・kopicha 続報)。
2
+
3
+ 読むだけの口(`queue`・`schedule`・`board`。T8-2)は遅れを**言うだけ**で
4
+ pull しない。この道具は、その案内から誘導される**明示の**取り込み口——
5
+ `writeback.sync_repo()` を呼ぶだけで、`thth approve` が既に通しているのと
6
+ 同じ関数・同じ repo ロックを使う。
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+
12
+ from tests.conftest import init_git_pair, run_git, run_thth
13
+
14
+
15
+ def test_pullはffして取り込む(isolated_account_factory, tmp_path):
16
+ pair = init_git_pair(tmp_path / "acct", seed_content="旧", seed_name="a.md")
17
+ account = isolated_account_factory(repo_dir=pair["work"])
18
+ head_before = run_git(pair["work"], ["rev-parse", "--short", "HEAD"]).stdout.strip()
19
+
20
+ run_git(pair["seed"], ["commit", "--allow-empty", "-m", "second"])
21
+ run_git(pair["seed"], ["push"])
22
+ head_after_remote = run_git(pair["seed"], ["rev-parse", "--short", "HEAD"]).stdout.strip()
23
+
24
+ result = run_thth(["pull", account["name"]])
25
+ assert result.returncode == 0, result.stderr
26
+ assert f"取り込みました: {head_before} → {head_after_remote}(1 commit)" in result.stdout
27
+
28
+ # 本当に取り込まれている(work の HEAD が動いた)。
29
+ head_now = run_git(pair["work"], ["rev-parse", "--short", "HEAD"]).stdout.strip()
30
+ assert head_now == head_after_remote
31
+
32
+
33
+ def test_pullはすでに最新なら何も変えない(isolated_account_factory, tmp_path):
34
+ pair = init_git_pair(tmp_path / "acct", seed_content="", seed_name=".gitkeep")
35
+ account = isolated_account_factory(repo_dir=pair["work"])
36
+ head_before = run_git(pair["work"], ["rev-parse", "--short", "HEAD"]).stdout.strip()
37
+
38
+ result = run_thth(["pull", account["name"]])
39
+ assert result.returncode == 0, result.stderr
40
+ assert "すでに最新です" in result.stdout
41
+
42
+ head_after = run_git(pair["work"], ["rev-parse", "--short", "HEAD"]).stdout.strip()
43
+ assert head_after == head_before
44
+
45
+
46
+ def test_pullのjsonはbehind_remoteの形(isolated_account_factory, tmp_path):
47
+ pair = init_git_pair(tmp_path / "acct", seed_content="", seed_name=".gitkeep")
48
+ account = isolated_account_factory(repo_dir=pair["work"])
49
+
50
+ result = run_thth(["pull", account["name"], "--json"])
51
+ assert result.returncode == 0, result.stderr
52
+ out = json.loads(result.stdout)
53
+ assert len(out) == 1
54
+ row = out[0]
55
+ assert row["account"] == account["name"]
56
+ assert row["ok"] is True
57
+ assert set(row["repo"].keys()) == {"behind", "ahead", "head", "fetched_at", "reason"}
58
+ assert row["repo"]["behind"] == 0
59
+
60
+
61
+ def test_pullのprojectは同じrepoを共有する2accountで1回だけ引く(
62
+ isolated_account_factory, tmp_path):
63
+ pair = init_git_pair(tmp_path / "acct", seed_content="旧", seed_name="a.md")
64
+ account_a = isolated_account_factory("a-threads", repo_dir=pair["work"], project="shared")
65
+ account_b = isolated_account_factory("b-threads", repo_dir=pair["work"], project="shared")
66
+ assert account_a["repo_dir"] == account_b["repo_dir"]
67
+
68
+ run_git(pair["seed"], ["commit", "--allow-empty", "-m", "second"])
69
+ run_git(pair["seed"], ["push"])
70
+
71
+ result = run_thth(["pull", "--project", "shared", "--json"])
72
+ assert result.returncode == 0, result.stderr
73
+ out = json.loads(result.stdout)
74
+ # **重複なく 1 回だけ**——2 account だが repo は 1 つ。
75
+ assert len(out) == 1
76
+ assert out[0]["repo"]["behind"] == 0
77
+
78
+
79
+ def test_pullは失敗をsync_repoの理由のままrc1(isolated_account_factory, tmp_path):
80
+ account = isolated_account_factory(repo_dir=str(tmp_path / "no-such-repo-dir"))
81
+
82
+ result = run_thth(["pull", account["name"]])
83
+ assert result.returncode == 1, result.stdout
84
+ assert "取り込めませんでした" in result.stderr or "見当たりません" in result.stderr
85
+
86
+
87
+ def test_pullはaccountもprojectも無ければ断る(isolated_account_factory):
88
+ result = run_thth(["pull"])
89
+ assert result.returncode == 2
90
+ assert "account" in result.stderr
91
+
92
+
93
+ def test_pullは未知のprojectを断る(isolated_account_factory):
94
+ result = run_thth(["pull", "--project", "no-such-project"])
95
+ assert result.returncode == 2
96
+ assert "no-such-project" in result.stderr