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.
- {thth-2.3.1 → thth-2.3.3}/PKG-INFO +3 -3
- {thth-2.3.1 → thth-2.3.3}/README.md +2 -2
- {thth-2.3.1 → thth-2.3.3}/skills/thth/SKILL.md +7 -3
- thth-2.3.3/tests/test_behind_remote.py +142 -0
- thth-2.3.3/tests/test_cli_vm_path.py +80 -0
- thth-2.3.3/tests/test_collect_engagements.py +175 -0
- thth-2.3.3/tests/test_pull_cli.py +96 -0
- thth-2.3.3/tests/test_read_cli_bare_runtimeerror.py +179 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_skill_doc.py +26 -0
- thth-2.3.3/thth/VERSION +1 -0
- {thth-2.3.1 → thth-2.3.3}/thth/cli.py +269 -1
- {thth-2.3.1 → thth-2.3.3}/thth/collect.py +56 -2
- {thth-2.3.1 → thth-2.3.3}/thth/thread_read.py +16 -0
- {thth-2.3.1 → thth-2.3.3}/thth/threads_read_cli.py +13 -0
- {thth-2.3.1 → thth-2.3.3}/thth/topic_cli.py +24 -0
- {thth-2.3.1 → thth-2.3.3}/thth/where_cli.py +10 -0
- {thth-2.3.1 → thth-2.3.3}/thth/who_cli.py +8 -0
- {thth-2.3.1 → thth-2.3.3}/thth/writeback.py +66 -0
- thth-2.3.1/thth/VERSION +0 -1
- {thth-2.3.1 → thth-2.3.3}/.gitignore +0 -0
- {thth-2.3.1 → thth-2.3.3}/LICENSE +0 -0
- {thth-2.3.1 → thth-2.3.3}/accounts.example/bluesky.json +0 -0
- {thth-2.3.1 → thth-2.3.3}/accounts.example/mastodon.json +0 -0
- {thth-2.3.1 → thth-2.3.3}/accounts.example/threads.json +0 -0
- {thth-2.3.1 → thth-2.3.3}/bin/thth +0 -0
- {thth-2.3.1 → thth-2.3.3}/bin/thth-remote +0 -0
- {thth-2.3.1 → thth-2.3.3}/bin/thth-run +0 -0
- {thth-2.3.1 → thth-2.3.3}/mcp/server.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/pyproject.toml +0 -0
- {thth-2.3.1 → thth-2.3.3}/systemd/thth@.service +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/__init__.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/conftest.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/fixtures/build_injection_eval.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/fixtures/callback/worker_responses.json +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/fixtures/injection_eval.json +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/fixtures/umami-bile.md +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/helpers/__init__.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/helpers/fake_oauth_server.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/helpers/fake_push_adapter.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/helpers/fake_redirect_server.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/helpers/fake_search_adapter.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/helpers/fake_threads_reader.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/helpers/fake_threads_writes.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/helpers/hold_lock.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/helpers/hold_repo_lock.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/helpers/worker_probe.mjs +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_account_name_check.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_account_report.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_accounts_dir.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_adapter_boundary.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_after_cli.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_api_error_body.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_app_env_cli.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_approval.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_approve_cmd.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_ask.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_atlas_fifth_review.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_atlas_fourth_review.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_atlas_review.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_atlas_second_review.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_atlas_third_review.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_audit1_fixes.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_audit2_fixes.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_audit_b_counterexamples.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_audit_b_m1_semantics.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_audit_b_n1_depth.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_audit_b_r1_closure.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_audit_v201.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_audit_v23_details.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_auth.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_auth_identity_required.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_bluesky_adapter.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_bluesky_secret_scrub.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_board_needs_review.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_board_running.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_bundle.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_charcount.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_cli_entrypoint.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_cli_help.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_collect.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_collect_runs.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_collect_sent.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_comparability.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_control_chars_refused.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_conversation_paging.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_count_for.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_docs_links.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_doctor.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_doctor_all_permissions.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_doctor_next_step.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_duplicate_front_matter_keys.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_editorial_audit.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_editorial_n3_n4.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_engagements.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_external_acceptance.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_fake_api.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_form_spec.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_form_spec_cli.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_forms.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_fresh_install.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_front_matter_injection.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_hypotheses.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_hypothesis_boundaries.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_hypothesis_units.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_impact_cli.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_inbox_granted_scopes.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_independent_thread_review.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_independent_topic_review.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_independent_topic_review_r2.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_inflight.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_jst.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_lint.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_list_projections.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_llm_trial.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_llm_trial_engage.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_lock.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_m2_independent.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_maintain.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_mastodon_adapter.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_mastodon_visibility.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_max_per_run.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_mcp.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_mcp_robustness.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_measured.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_media_accounts.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_media_next_steps.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_media_roundtrip.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_mismatch_diagnostics.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_n5_reply_null_element.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_n6_replies_incomplete_ledger.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_n7_doctor_error_body.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_no_limits.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_observation_provenance.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_packaging.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_preview.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_production_mode.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_queue.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_quiet_hours.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_r2_r4_r5.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_reason_adoption.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_redact.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_redirect_token_leak.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_refresh.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_refresh_destination.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_rejudge_new.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_rejudge_r2_r3.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_replies.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_replies_refresh.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_repo_lock.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_repo_sync.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_reverse_audit.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_review_case_id_cluster_m2.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_review_case_id_cluster_r2.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_review_cli.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_review_evidence_account.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_review_form_spec_and_case_id.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_review_provenance.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_review_records.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_revoke.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_roundtrip.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_run_missing_env.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_run_record_failclosed.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_runs_collected.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_runs_read_rows.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_schedule_multiple.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_schedule_view.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_scopes_source_requested.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_search_engage_targets.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_secret_value_redact.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_secrets_grep.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_security_small_fixes.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_select.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_selfupdate.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_send.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_send_cli.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_send_media.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_send_visibility.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_sent_integrity.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_server_json.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_site.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_systemd.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_thread_publish.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_thread_read.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_threadrun.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_threads_read_permissions.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_threadshape.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_thth_root.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_token_exists.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_token_scopes_record.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_token_set.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_topic.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_topic_advice.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_topic_cli.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_topic_models.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_topic_review_fixes.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_topic_shelf_observers.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_topic_store.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_topics_record_concurrent.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_topics_tool.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_trial_frictions.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_v21b_approved_writes.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_value_domains_shown.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_version.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_where_cli.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_who_cli.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_writeback_rebase.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/tests/test_writeback_validate.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/__init__.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/__main__.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/account_cli.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/account_report.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/accounts.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/adapters/__init__.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/adapters/base.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/adapters/bluesky.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/adapters/mastodon.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/adapters/threads.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/after_cli.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/appenv.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/approval.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/ask.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/ask_cli.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/bundle.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/core.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/doctor.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/engagements.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/forms.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/httpsafe.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/inflight.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/jst.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/lint.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/lock.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/maintain.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/measured.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/oauth.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/postid.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/queuefile.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/redact.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/replies.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/report.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/retract_cli.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/runs.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/scopes.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/secrets_fs.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/select.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/selfupdate.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/sent.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/systemd_gen.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/threadrun.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/threadshape.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/threadthrow.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/topic_advice.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/topic_models.py +0 -0
- {thth-2.3.1 → thth-2.3.3}/thth/topic_store.py +0 -0
- {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.
|
|
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.
|
|
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` の全
|
|
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.
|
|
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` の全
|
|
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
|