thth 3.18.2__tar.gz → 3.19.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.
- {thth-3.18.2 → thth-3.19.0}/PKG-INFO +4 -2
- {thth-3.18.2 → thth-3.19.0}/README.md +3 -1
- {thth-3.18.2 → thth-3.19.0}/skills/thth/SKILL.md +8 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v210_json_contract.py +3 -1
- {thth-3.18.2 → thth-3.19.0}/tests/test_v214_x_adapter.py +2 -0
- thth-3.19.0/tests/test_x_community.py +398 -0
- thth-3.19.0/tests/test_x_trends_counts.py +170 -0
- thth-3.19.0/thth/VERSION +1 -0
- {thth-3.18.2 → thth-3.19.0}/thth/adapters/base.py +6 -0
- {thth-3.18.2 → thth-3.19.0}/thth/adapters/x.py +278 -1
- {thth-3.18.2 → thth-3.19.0}/thth/adapters/x_media.py +6 -0
- {thth-3.18.2 → thth-3.19.0}/thth/approval.py +31 -8
- {thth-3.18.2 → thth-3.19.0}/thth/budget_x.py +94 -6
- {thth-3.18.2 → thth-3.19.0}/thth/bundle.py +4 -0
- {thth-3.18.2 → thth-3.19.0}/thth/cli.py +17 -0
- thth-3.19.0/thth/community_cli.py +142 -0
- {thth-3.18.2 → thth-3.19.0}/thth/core.py +34 -8
- {thth-3.18.2 → thth-3.19.0}/thth/fingerprint_golden.py +5 -0
- {thth-3.18.2 → thth-3.19.0}/thth/leave_gate.py +2 -1
- {thth-3.18.2 → thth-3.19.0}/thth/lint.py +32 -0
- {thth-3.18.2 → thth-3.19.0}/thth/select.py +8 -0
- {thth-3.18.2 → thth-3.19.0}/thth/sent.py +9 -1
- thth-3.19.0/thth/trends_cli.py +186 -0
- thth-3.18.2/thth/VERSION +0 -1
- {thth-3.18.2 → thth-3.19.0}/.gitignore +0 -0
- {thth-3.18.2 → thth-3.19.0}/LICENSE +0 -0
- {thth-3.18.2 → thth-3.19.0}/accounts.example/bluesky.json +0 -0
- {thth-3.18.2 → thth-3.19.0}/accounts.example/mastodon.json +0 -0
- {thth-3.18.2 → thth-3.19.0}/accounts.example/threads.json +0 -0
- {thth-3.18.2 → thth-3.19.0}/accounts.example/x.json +0 -0
- {thth-3.18.2 → thth-3.19.0}/bin/thth +0 -0
- {thth-3.18.2 → thth-3.19.0}/bin/thth-remote +0 -0
- {thth-3.18.2 → thth-3.19.0}/bin/thth-run +0 -0
- {thth-3.18.2 → thth-3.19.0}/mcp/server.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/pyproject.toml +0 -0
- {thth-3.18.2 → thth-3.19.0}/skills/thth-admin/SKILL.md +0 -0
- {thth-3.18.2 → thth-3.19.0}/systemd/thth@.service +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/__init__.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/conftest.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/coordination_snapshot.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/fixtures/GraphemeBreakTest-17.0.0.txt +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/fixtures/build_injection_eval.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/fixtures/callback/worker_responses.json +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/fixtures/injection_eval.json +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/fixtures/umami-bile.md +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/helpers/__init__.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/helpers/fake_oauth_server.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/helpers/fake_push_adapter.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/helpers/fake_redirect_server.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/helpers/fake_search_adapter.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/helpers/fake_threads_reader.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/helpers/fake_threads_status.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/helpers/fake_threads_writes.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/helpers/fake_x.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/helpers/hold_lock.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/helpers/hold_repo_lock.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/helpers/worker_probe.mjs +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_27_acceptance_fixes.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_account_name_check.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_account_report.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_accounts_dir.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_adapter_boundary.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_admin_http_mcp.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_admin_log.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_admin_notifications.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_admin_repairs.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_admin_report.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_after_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_analytics_comparison.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_analytics_report.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_api_diagnostic.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_api_error_body.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_app_env_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_approval.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_approve_cmd.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_ask.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_atlas_fifth_review.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_atlas_fourth_review.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_atlas_review.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_atlas_second_review.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_atlas_third_review.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_audit1_fixes.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_audit2_fixes.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_audit_b_counterexamples.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_audit_b_m1_semantics.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_audit_b_n1_depth.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_audit_b_r1_closure.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_audit_v201.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_audit_v23_details.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_auth.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_auth_identity_required.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_behind_remote.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_bluesky_adapter.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_bluesky_secret_scrub.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_board_needs_review.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_board_running.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_branch_replies.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_bundle.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_bundle_not_type_mismatch.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_charcount.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_cli_entrypoint.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_cli_help.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_cli_vm_path.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_collect.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_collect_engagements.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_collect_runs.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_collect_sent.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_comparability.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_control_chars_refused.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_conversation_paging.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_count_for.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_docs_links.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_doctor.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_doctor_all_permissions.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_doctor_next_step.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_duplicate_front_matter_keys.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_editorial_audit.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_editorial_n3_n4.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_engagements.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_external_acceptance.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_fake_api.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_form_spec.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_form_spec_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_forms.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_fresh_install.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_front_matter_injection.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_healthcheck_notifications.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_hypotheses.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_hypothesis_boundaries.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_hypothesis_units.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_impact_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_inbox_granted_scopes.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_incident_notifications.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_independent_thread_review.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_independent_topic_review.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_independent_topic_review_r2.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_inflight.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_inflight_tag_body_hash.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_jst.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_lint.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_list_projections.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_llm_trial.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_llm_trial_engage.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_lock.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_m2_independent.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_maintain.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_mastodon_adapter.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_mastodon_visibility.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_max_per_run.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_mcp.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_mcp_robustness.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_measured.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_media_accounts.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_media_next_steps.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_media_roundtrip.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_mismatch_diagnostics.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_n5_reply_null_element.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_n6_replies_incomplete_ledger.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_n7_doctor_error_body.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_no_limits.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_observation_provenance.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_operations_handoff.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_packaging.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_preview.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_production_mode.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_pull_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_pulse.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_queue.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_quiet_hours.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_r2_r4_r5.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_read_cli_bare_runtimeerror.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_reason_adoption.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_redact.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_redirect_token_leak.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_refresh.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_refresh_destination.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_rejudge_new.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_rejudge_r2_r3.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_replies.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_replies_refresh.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_repo_lock.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_repo_sync.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_report_checkout_import.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_report_http.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_report_isolation.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_report_service.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_retract_already_gone.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_reverse_audit.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_review_case_id_cluster_m2.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_review_case_id_cluster_r2.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_review_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_review_evidence_account.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_review_form_spec_and_case_id.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_review_provenance.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_review_records.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_revoke.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_roundtrip.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_run_missing_env.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_run_record_failclosed.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_runs_collected.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_runs_read_rows.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_schedule_multiple.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_schedule_view.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_scopes_source_requested.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_search_engage_targets.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_search_media_routes.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_secret_value_redact.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_secrets_grep.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_security_small_fixes.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_select.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_selfupdate.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_send.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_send_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_send_control_chars.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_send_media.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_send_visibility.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_sent_integrity.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_server_json.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_site.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_skill_doc.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_study_report.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_systemd.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_thread_publish.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_thread_read.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_threadrun.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_threads_read_permissions.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_threadshape.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_thth_root.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_token_exists.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_token_scopes_record.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_token_set.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_topic.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_topic_advice.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_topic_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_topic_models.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_topic_review_fixes.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_topic_shelf_observers.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_topic_store.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_topics_record_concurrent.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_topics_tool.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_trial_frictions.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v210_admin_transaction.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v210_approve_account.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v210_length.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v210_lock_wait.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v210_mastodon_scopes.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v210_mentions.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v210_post_urls.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v210_repo_paths.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v210_reposts.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v210_study_add.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v210_tool_version.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v210_unanswered.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v210_unanswered_scope.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v210_where_arguments.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v210_where_filters.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v210_who_arguments.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v211_auth_integration.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v211_authflow.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v211_bluesky_stdin.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v211_doctor_binding.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v211_mastodon_auth.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v211_x_auth.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v212_ask_coordination.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v212_budget.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v212_budget_auth.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v212_budget_mcp.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v212_deletion.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v212_leave.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v212_leave_gate.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v212_leave_providers.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v212_leave_token_safety.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v212_mastodon_guide.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v212_mcp_compat.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v212_missing_token.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v212_read_coordination.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v212_relay.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v212_server_writes.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v212_token_parent.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v212_write_storage.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v2131_send_dry_run.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v2131_threads_publish_wait.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v2132_send_inflight_hint.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v2132_threads_create_timeout.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v2132_threads_create_unknown.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_acceptance_fixes.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_adts_asf.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_attachment_kinds.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_bluesky_external.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_bluesky_media.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_bluesky_metadata.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_bluesky_metadata_uri.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_bluesky_metadata_video.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_bluesky_record.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_bluesky_record_video.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_bluesky_video.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_bluesky_webm_named.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_bmff_typed_metadata.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_budget_lock.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_capabilities.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_carryover_timeout.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_graphemes.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_mastodon_auth_media.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_mastodon_bmff_audio.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_mastodon_flac.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_mastodon_gif.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_mastodon_link.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_mastodon_media.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_mastodon_mp3.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_mastodon_ogg_opus.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_mastodon_ogg_vorbis.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_mastodon_poll.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_mastodon_primary_size_boundary.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_mastodon_quote.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_mastodon_thumbnail.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_mastodon_wav.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_mastodon_webm_audio.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_mastodon_webm_video.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_media_cleanup.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_media_formats.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_media_foundation.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_media_prepared.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_media_relay.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_media_relay_refusals.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_media_upload.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_review_followups.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_stage810_audit.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_static_diagnostics.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_stop_observation.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_systemd_worker.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_threads_ghost.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_threads_gif.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_threads_link.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_threads_media.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_threads_media_spoiler.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_threads_poll.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_threads_quote.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_threads_quote_media.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_threads_replies.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_threads_text.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_threads_text_spoiler.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_threads_video.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_transport_review.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_video_typed_metadata.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_vorbis_comment_boundary.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v213_webm_schema_boundary.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v2141_draft_lint_reason.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v2141_managed_repo_umask.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v214_release_signing.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v214_x_auth.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v214_x_media.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v214_x_posts_budget.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v21b_approved_writes.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v28_tags.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3100_docs.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3100_invite_admin_approval.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3100_invite_create.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3100_invite_credential.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3100_invite_limits.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3100_invite_worker.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v310_morning.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v310_search_timeout.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v310_watch_words.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3111_observe_counts_only.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3111_observe_no_world.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v311_leave_same_user.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v311_mastodon_retract.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v311_morning_overdue.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v311_morning_own_exclusion.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v311_morning_unanswered_window.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v311_notifications_guidance.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v311_replies_owned.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3120_activity.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3120_admin_write_reasons.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3120_guard.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3120_worker_version.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v312_leave_shared_token.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v312_own_handles_stopped.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v312_report_admin.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v312_report_channel.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v312_report_inbox.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v312_report_mcp_user.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3140_api_relay.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3140_invite_key.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3140_remote_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3140_server_reads.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3142_browser_login.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3143_activity_push.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3143_cli_umask_leave.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3143_inflight_remote.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3143_managed_repo_recover.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v315_reactions.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v317_run_lock_wait.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3180_json_names.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3180_map_timer.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3180_where_filters.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v3180_where_keep.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v31_analysis.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v31_collection.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v320_reply_to_file_display.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v320_reply_to_file_lint.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v320_reply_to_file_resolve.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v320_report_fixes.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v330_fingerprint_version.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v330_held.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v330_held_names.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v330_last_refusals.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v330_notification_route.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v330_observe.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v330_report_friction.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v330_report_words.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v330_reports_effect.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v331_inflight_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v331_listing_resolve.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v331_outbox_retention.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v331_publish_status.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v331_self_resolve.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v340_plaza_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v340_plaza_docs.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v340_plaza_fields.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v340_plaza_mcp.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v340_plaza_morning.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v340_plaza_observe.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v340_plaza_open.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v340_plaza_open_confirm.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v340_plaza_reply.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v340_plaza_store.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v350_map_docs.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v350_map_nodes.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v350_map_observe.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v350_map_plaza.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v350_map_retention.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v350_map_self.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v350_map_show.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v350_map_world.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v351_leave_timers.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v351_plaza_open_holes.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v351_who_owned.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v360_goal_analytics.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v360_goal_docs.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v360_goal_observe_map.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v360_goal_plaza.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v360_goal_record.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v360_goal_send.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v360_report_timeline.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v370_account_self_healing.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v370_approve_pending.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v370_click_attribution.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v370_collection_stale_basis.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v370_docs.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v370_follow_comparison.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v370_local_path.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v370_weekly_goals.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v370_where_aggregate.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v370_where_also.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v370_where_zero.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v380_docs.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v380_plaza_digest.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v380_plaza_from.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v380_plaza_mcp.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v380_plaza_moments.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v380_plaza_owner.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v380_plaza_received.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v380_plaza_triggers.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v381_plaza_visibility_default.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v382_confirm_file.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v382_first_stage_not_refusal.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v382_reader_lock_and_push_pending.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v382_sync_without_fetch_head.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v390_docs.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v390_per_post_clicks.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v390_where_rate.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v390_where_suggest.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_v391_plaza_text_numbers.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_value_domains_shown.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_version.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_where_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_where_threads_tag_author.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_who_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_writeback_rebase.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_writeback_validate.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_x_list.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_x_metrics.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_x_owned_metrics.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_x_search_thread.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/tests/test_x_token_autorefresh.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/__init__.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/__main__.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/_grapheme_data.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/account_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/account_report.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/account_settings.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/accounts.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/activity.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/adapters/__init__.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/adapters/auth_mastodon.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/adapters/auth_threads.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/adapters/auth_x.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/adapters/bluesky.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/adapters/bluesky_media.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/adapters/bluesky_video.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/adapters/mastodon.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/adapters/mastodon_media.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/adapters/threads.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/adapters/threads_media.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/admin_diagnose.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/admin_log.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/admin_notifications.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/admin_report.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/after_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/analytics_clicks.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/analytics_comparison.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/analytics_goals.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/analytics_report.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/analytics_shapes.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/analytics_threads.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/analytics_weekly.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/api_diagnostic.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/api_requests.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/appconfig.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/appenv.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/approve_pending.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/ask.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/ask_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/audioformats.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/authclients.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/authflow.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/authpassword.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/bluesky_metadata.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/budget_x_posts.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/click_attribution.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/collect.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/collection_context.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/collection_status.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/deletion.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/doctor.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/ebmlaudio.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/ebmlvideo.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/engagements.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/flacformats.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/forms.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/goals.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/graphemes.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/guard.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/handoff_cursor.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/healthcheck.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/httpsafe.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/incident.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/inflight.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/inflight_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/inflight_resolve.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/invites.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/jst.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/leave.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/list_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/lock.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/maintain.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/managed_repo.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/map_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/map_store.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/map_view.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/map_world.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/measured.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/media.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/media_capabilities.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/media_cleanup.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/media_delivery.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/media_relay.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/media_uploads.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/mediaformats.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/morning.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/mpeg_audio.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/notification_route.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/oauth.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/oggformats.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/operations_handoff.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/plaza.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/plaza_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/plaza_from.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/plaza_moments.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/plaza_observe.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/plaza_reads.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/plaza_redact.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/postid.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/private_store.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/pulse.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/queuefile.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/reaction_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/reactions.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/read_coordination.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/read_window.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/redact.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/refusals.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/relay.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/remote.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/replies.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/report.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/report_details.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/report_http.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/report_inbox.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/report_isolation.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/report_service.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/report_timeline.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/retract_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/runs.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/scopes.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/secrets_fs.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/selfupdate.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/server_files.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/server_reads.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/server_writes.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/stop_observation.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/study_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/study_report.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/systemd_gen.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/tags.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/thread_read.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/threadrun.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/threads_read_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/threadshape.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/threadthrow.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/timer_cleanup.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/tool_version.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/topic_advice.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/topic_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/topic_models.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/topic_store.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/topics.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/unanswered.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/vorbisformats.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/watch_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/where_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/where_keep.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/where_suggest.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/who_cli.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/worker.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/worker_version.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/writeback.py +0 -0
- {thth-3.18.2 → thth-3.19.0}/thth/x_metrics.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: thth
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.19.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
|
|
@@ -47,6 +47,8 @@ English: [README.en.md](README.en.md) · サイト: https://thth.me · 文書の
|
|
|
47
47
|
- **再投稿といいね**: `thth repost`・`unrepost`・`like`・`unlike`(3.15.0 から)。Threads は再投稿だけ(Threads の API にいいねが無い)。Bluesky は両方。Mastodon と X のいいねは、そのアカウントを認可し直してから([リリースノート 3.15.0](docs/リリースノート_3.15.0_2026-10-09.md))。
|
|
48
48
|
- **検索の語ごとの数を残す・絞る**(3.18.0): `thth where … --also|--aggregate --keep` で語ごとの数だけを日付つきで残し(5 未満は欠測・本文や名前は残さない)、`thth where-log <project> [--budget N --medium x]` で当たり率と翌日の配り直しを見ます。`--ja`・`--question`・`--no-replies` で表示を絞ります(X の `--ja` は検索に `lang:ja` を足して読む本数を減らす)([リリースノート 3.18.0](docs/リリースノート_3.18.0_2026-10-12.md))。
|
|
49
49
|
- **X の非公開リスト**: `thth list create|add|remove|show`(3.17.0 から・X だけ)。公開のリストは作らない。足す・外すは人が選んだ少数だけ(1 回 10 人まで)。アカウントを認可し直してから([リリースノート 3.17.0](docs/リリースノート_3.17.0_2026-10-12.md))。
|
|
50
|
+
- **X のコミュニティ**: `thth community search <account> <語…>` で探し、`thth send <account> --community <id>` か queue の front-matter `community_id:` で出す(3.19.0 から・X だけ)。参加は X の画面で先に(API に参加の口は無い)。返信とは同時に使えない。各コミュニティの規則は人が読んで守る([リリースノート 3.19.0](docs/リリースノート_3.19.0_2026-10-13.md))。
|
|
51
|
+
- **X のトレンドと語ごとの投稿数**: `thth trends <account> [--woeid N] [--max N]`(既定は日本)と `thth counts <account> <語> [--granularity hour|day]`(直近 7 日)(3.19.0 から・X だけ・1 回の有償の読み取り・保存しない)。
|
|
50
52
|
|
|
51
53
|
## はじめ方
|
|
52
54
|
|
|
@@ -107,7 +109,7 @@ MCP の道具は CLI の写しで、同じ `thth` コマンドを呼ぶだけで
|
|
|
107
109
|
|
|
108
110
|
`thth --help` で各コマンドの説明が出ます。
|
|
109
111
|
|
|
110
|
-
動くもの(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`observe`・`morning`・`serve-reports`・`worker`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`pull`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`where-log`・`who`・`retract`・`repost`・`unrepost`・`like`・`unlike`・`list`・`inflight`・`location`・`notifications`・`report`・`plaza`・`map`・`admin`・`login`・`logout`。
|
|
112
|
+
動くもの(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`observe`・`morning`・`serve-reports`・`worker`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`pull`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`where-log`・`who`・`retract`・`repost`・`unrepost`・`like`・`unlike`・`list`・`community`・`trends`・`counts`・`inflight`・`location`・`notifications`・`report`・`plaza`・`map`・`admin`・`login`・`logout`。
|
|
111
113
|
|
|
112
114
|
## 版と配布
|
|
113
115
|
|
|
@@ -23,6 +23,8 @@ English: [README.en.md](README.en.md) · サイト: https://thth.me · 文書の
|
|
|
23
23
|
- **再投稿といいね**: `thth repost`・`unrepost`・`like`・`unlike`(3.15.0 から)。Threads は再投稿だけ(Threads の API にいいねが無い)。Bluesky は両方。Mastodon と X のいいねは、そのアカウントを認可し直してから([リリースノート 3.15.0](docs/リリースノート_3.15.0_2026-10-09.md))。
|
|
24
24
|
- **検索の語ごとの数を残す・絞る**(3.18.0): `thth where … --also|--aggregate --keep` で語ごとの数だけを日付つきで残し(5 未満は欠測・本文や名前は残さない)、`thth where-log <project> [--budget N --medium x]` で当たり率と翌日の配り直しを見ます。`--ja`・`--question`・`--no-replies` で表示を絞ります(X の `--ja` は検索に `lang:ja` を足して読む本数を減らす)([リリースノート 3.18.0](docs/リリースノート_3.18.0_2026-10-12.md))。
|
|
25
25
|
- **X の非公開リスト**: `thth list create|add|remove|show`(3.17.0 から・X だけ)。公開のリストは作らない。足す・外すは人が選んだ少数だけ(1 回 10 人まで)。アカウントを認可し直してから([リリースノート 3.17.0](docs/リリースノート_3.17.0_2026-10-12.md))。
|
|
26
|
+
- **X のコミュニティ**: `thth community search <account> <語…>` で探し、`thth send <account> --community <id>` か queue の front-matter `community_id:` で出す(3.19.0 から・X だけ)。参加は X の画面で先に(API に参加の口は無い)。返信とは同時に使えない。各コミュニティの規則は人が読んで守る([リリースノート 3.19.0](docs/リリースノート_3.19.0_2026-10-13.md))。
|
|
27
|
+
- **X のトレンドと語ごとの投稿数**: `thth trends <account> [--woeid N] [--max N]`(既定は日本)と `thth counts <account> <語> [--granularity hour|day]`(直近 7 日)(3.19.0 から・X だけ・1 回の有償の読み取り・保存しない)。
|
|
26
28
|
|
|
27
29
|
## はじめ方
|
|
28
30
|
|
|
@@ -83,7 +85,7 @@ MCP の道具は CLI の写しで、同じ `thth` コマンドを呼ぶだけで
|
|
|
83
85
|
|
|
84
86
|
`thth --help` で各コマンドの説明が出ます。
|
|
85
87
|
|
|
86
|
-
動くもの(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`observe`・`morning`・`serve-reports`・`worker`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`pull`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`where-log`・`who`・`retract`・`repost`・`unrepost`・`like`・`unlike`・`list`・`inflight`・`location`・`notifications`・`report`・`plaza`・`map`・`admin`・`login`・`logout`。
|
|
88
|
+
動くもの(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`observe`・`morning`・`serve-reports`・`worker`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`pull`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`where-log`・`who`・`retract`・`repost`・`unrepost`・`like`・`unlike`・`list`・`community`・`trends`・`counts`・`inflight`・`location`・`notifications`・`report`・`plaza`・`map`・`admin`・`login`・`logout`。
|
|
87
89
|
|
|
88
90
|
## 版と配布
|
|
89
91
|
|
|
@@ -62,6 +62,14 @@ THTH は CLI ツール。あなた(エージェント)が使う MCP の道
|
|
|
62
62
|
|
|
63
63
|
`thth list create <account> <名前> [--description …] --by <名前>`・`thth list add|remove <account> <list_id> <id か @handle…> --by <名前>`・`thth list show <account> <list_id> [--limit N]`(MCP には無い)。X のアカウントならどれでも(今は asmon-kanto-x と kopicha-x)・**使う前に各アカウントで `thth auth <account> --by <名前>` を打ち直す**。**非公開リストだけ**(公開は作れない)。足すのは**人が選んだ少数だけ**——1 回 10 人まで、検索結果から機械的に足さない(X の決まりで bulk・無差別の追加は不可)。足された相手に X が通知するかは未確認。書き込みは production: true の口座だけ・費用は月の X 予算(作る 0.010・足す/外す 0.005 USD、@handle の id 引き 0.010)。`show` は `where` と同じ有償の読み取り(1 本 0.005・1 日 60 本に数える・既定 10 本・最大 25)で、本文は返すだけで保存しない。断り: `scope_missing`(`thth auth <account> --by <名前>` で認可し直す)・`too_many_members`・`unsupported_on_platform`(X 以外)。
|
|
64
64
|
|
|
65
|
+
## X のコミュニティ(3.19.0)
|
|
66
|
+
|
|
67
|
+
`thth community search <account> <語…> [--limit N] [--json]`(MCP には無い)で探し、`thth send <account> --community <id>` か queue の front-matter `community_id: <id>` で出す。X だけ・認可のやり直しは不要。探すのは有償の読み取り(1 件 0.005 USD・月の X 予算・1 日 60 本には数えない・既定 10 件・最大 25・保存しない)。`community_id` は digest/承認の指紋に入る。返信(reply_to・reply_to_file)と同時には使えない・連投には書けない。**参加は X の画面で先に**(API に参加の口も「参加しているか」の欄も無い)——参加していなければ X の断りが `x_unknown_refusal(…)` で残る。**各コミュニティの規則(宣伝の禁止など)は人が読んで守る**。所有者に頼まれたコミュニティへ、人が選んだ本文を 1 本ずつ出すだけ——検索の結果へ機械的に出さない・同じ本文を複数のコミュニティへ流さない(X の自動化の決まりで bulk・無差別は不可)。断り: `unsupported_on_platform`(X 以外)・`community_id_invalid`・lint の `invalid_publish_option`。
|
|
68
|
+
|
|
69
|
+
## X のトレンドと語ごとの投稿数(3.19.0)
|
|
70
|
+
|
|
71
|
+
`thth trends <account> [--woeid N] [--max N]`(既定は日本・20 件・1 回 0.010 USD)・`thth counts <account> <語> [--granularity hour|day]`(直近 7 日・1 回 0.005 USD)(MCP には無い)。X だけ・要求は 1 回・返すだけで保存しない・月の X 予算に入り 1 日 60 本には数えない。話題選びの材料に使う——トレンドの語へ機械的に投稿しない。いまのトークンで通るかは未確認(断りは X の文面が短く残る)。
|
|
72
|
+
|
|
65
73
|
## 何を拒むか
|
|
66
74
|
|
|
67
75
|
- **手元の台帳を使う実行の、`--confirm` の無い本番。** digest の食い違う本番。承認後に本文が変わった下書き
|
|
@@ -58,7 +58,9 @@ COMMANDS = '''lint|preview|approve|account|revoke|posts|replies|measured|threads
|
|
|
58
58
|
# 3.17.0: X の非公開リスト。
|
|
59
59
|
'list create', 'list add', 'list remove', 'list show',
|
|
60
60
|
# 3.18.0: where … --keep で残した数を読む口。
|
|
61
|
-
'where-log'
|
|
61
|
+
'where-log',
|
|
62
|
+
# 3.19.0: X のコミュニティを探す。
|
|
63
|
+
'community search', 'trends', 'counts']
|
|
62
64
|
|
|
63
65
|
|
|
64
66
|
def test_json_table_covers_every_parser():
|
|
@@ -223,6 +223,8 @@ def test_after_an_unknown_the_next_send_refuses_without_touching_the_provider(
|
|
|
223
223
|
# ----- 403 の本文から静的な符丁(10/13 関東の返信の 403) --------------------
|
|
224
224
|
|
|
225
225
|
@pytest.mark.parametrize('body,expected', [
|
|
226
|
+
(b'{"type":"https://api.twitter.com/2/problems/not-authorized-for-resource","title":"Authorization Error","detail":"You can only reply to or quote posts where you are mentioned or are the author."}',
|
|
227
|
+
'x_publish_http_403: x_reply_not_engaged'),
|
|
226
228
|
(b'{"detail":"Reply to this conversation is not allowed because you have not been mentioned or otherwise engaged by the author of the post you are replying to.","title":"Forbidden","status":403}',
|
|
227
229
|
'x_publish_http_403: x_reply_not_engaged'),
|
|
228
230
|
(b'{"detail":"You are not permitted to perform this action.","title":"Forbidden","status":403}',
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""3.19.0: X のコミュニティ(`thth community search`・`send --community`・front-matter `community_id:`)。
|
|
2
|
+
|
|
3
|
+
**本物の api.x.com は叩かない**——`thth.adapters.x.request` を偽物に差し替えるか、loopback の偽の X
|
|
4
|
+
(`tests/helpers/fake_x`)に向ける。確かめること:
|
|
5
|
+
- search は `GET /2/communities/search` を 1 回・community.fields の 7 つ・下限 10 で押さえる・
|
|
6
|
+
別の台帳(Community: Read 0.005)で月の予算に入る・1 日の本数には数えない・保存しない。
|
|
7
|
+
- X 以外は unsupported_on_platform・範囲外の --limit・予算が無いときは叩かない。
|
|
8
|
+
- 投稿の本体に community_id・返信との同時指定は断る・形の違う id は断る。
|
|
9
|
+
- send の digest と queue の承認の指紋に入る(値があるときだけ・無ければ今までと同じ)。
|
|
10
|
+
- lint・select・bundle が X 以外・形違い・返信との同時指定を断る。
|
|
11
|
+
- sent の記録に community_id が残る。断り(403)は publish_refusal の流儀で文面が残る。
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import io
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import urllib.error
|
|
20
|
+
|
|
21
|
+
import pytest
|
|
22
|
+
|
|
23
|
+
from thth import accounts as accounts_mod
|
|
24
|
+
from thth import approval
|
|
25
|
+
from thth import budget_x as b
|
|
26
|
+
from thth import community_cli
|
|
27
|
+
from thth import lint as lint_mod
|
|
28
|
+
from thth.adapters import base
|
|
29
|
+
from thth.adapters import x as x_adapter
|
|
30
|
+
from tests.test_v214_x_media import env, wire # noqa: F401
|
|
31
|
+
|
|
32
|
+
ACCOUNT = "kanto-x"
|
|
33
|
+
TOKEN = "SYNTHETIC-X-TOKEN-0123456789"
|
|
34
|
+
COMMUNITY = "1493446837214187523"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _account(tmp_path, factory, *, name=ACCOUNT, media="x"):
|
|
38
|
+
data = {"access_token": TOKEN, "user_id": "123", "username": "kanto",
|
|
39
|
+
"scopes": ["tweet.read", "tweet.write", "users.read", "offline.access"],
|
|
40
|
+
"scopes_source": "response"}
|
|
41
|
+
token = tmp_path / f"{name}.token"
|
|
42
|
+
token.write_text(json.dumps(data), encoding="utf-8")
|
|
43
|
+
os.chmod(token, 0o600)
|
|
44
|
+
factory(name, media=media, handle="kanto", project="kanto", token=str(token), production=True)
|
|
45
|
+
return name
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.fixture
|
|
49
|
+
def x_account(tmp_path, isolated_account_factory):
|
|
50
|
+
_account(tmp_path, isolated_account_factory)
|
|
51
|
+
b.configure("15", by="operator")
|
|
52
|
+
return ACCOUNT
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.fixture
|
|
56
|
+
def fake(monkeypatch):
|
|
57
|
+
state = {"calls": [], "routes": {}}
|
|
58
|
+
|
|
59
|
+
def request(adapter, method, path, *, query=None, json_body=None, **kwargs):
|
|
60
|
+
state["calls"].append({"method": method, "path": path, "query": dict(query or {}),
|
|
61
|
+
"body": json_body})
|
|
62
|
+
answer = state["routes"].get((method, path))
|
|
63
|
+
if isinstance(answer, BaseException):
|
|
64
|
+
raise answer
|
|
65
|
+
if answer is None:
|
|
66
|
+
raise urllib.error.HTTPError(path, 404, "nf", {}, None)
|
|
67
|
+
return 200, answer
|
|
68
|
+
monkeypatch.setattr(x_adapter, "request", request)
|
|
69
|
+
return state
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _communities(n, *, more=False):
|
|
73
|
+
data = [{"id": str(1493446837214187523 + i), "name": f"コーヒー部{i}",
|
|
74
|
+
"description": f"説明{i}\n二行目", "member_count": 1200 + i,
|
|
75
|
+
"join_policy": "Open", "access": "Public", "created_at": "2024-01-02T03:04:05.000Z"}
|
|
76
|
+
for i in range(n)]
|
|
77
|
+
out = {"data": data, "meta": {"result_count": n}}
|
|
78
|
+
if more:
|
|
79
|
+
out["meta"]["next_token"] = "t"
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def run(capsys, *words, account=ACCOUNT, limit=None, as_json=True):
|
|
84
|
+
args = argparse.Namespace(account=account, words=list(words), limit=limit, json=as_json)
|
|
85
|
+
rc = community_cli.cmd_community_search(args)
|
|
86
|
+
out = capsys.readouterr()
|
|
87
|
+
payload = json.loads(out.out) if as_json and out.out.strip() else None
|
|
88
|
+
return rc, payload, out
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def community_rows():
|
|
92
|
+
return list(b.read_posts(b.COMMUNITIES)["reservations"].values())
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ----- 探す ---------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
def test_search_calls_once_with_all_fields_and_charges_the_returned_count(x_account, fake, capsys):
|
|
98
|
+
fake["routes"][("GET", "/2/communities/search")] = _communities(3, more=True)
|
|
99
|
+
rc, payload, _ = run(capsys, "コーヒー", "豆", limit=5)
|
|
100
|
+
assert rc == 0
|
|
101
|
+
[call] = fake["calls"]
|
|
102
|
+
assert call["path"] == "/2/communities/search"
|
|
103
|
+
assert call["query"] == {"query": "コーヒー 豆", "max_results": 10,
|
|
104
|
+
"community.fields": "access,created_at,description,id,join_policy,member_count,name"}
|
|
105
|
+
assert payload["n"] == 3 and payload["storage"] == "display_only"
|
|
106
|
+
row = payload["communities"][0]
|
|
107
|
+
assert row == {"community_id": "1493446837214187523", "name": "コーヒー部0",
|
|
108
|
+
"description": "説明0 二行目", "member_count": 1200, "join_policy": "Open",
|
|
109
|
+
"access": "Public", "created_at": "2024-01-02T03:04:05.000Z",
|
|
110
|
+
"url": "https://x.com/i/communities/1493446837214187523"}
|
|
111
|
+
assert any("続きがあります" in line for line in payload["cannot_say"])
|
|
112
|
+
assert any("参加しているかは分かりません" in line for line in payload["cannot_say"])
|
|
113
|
+
# 別の台帳(Community: Read・押さえは下限 10・確定は返ってきた 3 件)。
|
|
114
|
+
[ledger] = community_rows()
|
|
115
|
+
assert ledger["state"] == "settled" and ledger["limit"] == 10 and ledger["count"] == 3
|
|
116
|
+
assert ledger["usd"] == "0.015"
|
|
117
|
+
cost = payload["x_read_cost"]
|
|
118
|
+
assert cost["communities"] == 3 and cost["usd_estimate"] == "0.015" and cost["requests"] == 1
|
|
119
|
+
assert cost["daily_read_cap"] == "not_counted"
|
|
120
|
+
report = b.report()
|
|
121
|
+
assert report["spent_estimate_usd"] == "0.015"
|
|
122
|
+
assert report["community_read"]["month"]["communities"] == 3
|
|
123
|
+
assert any("コミュニティの検索" in line for line in b.cost_lines(report))
|
|
124
|
+
# 1 日の本数(他人の投稿の読み取り)には数えない。
|
|
125
|
+
assert b.posts_status(ACCOUNT)["today"] == 0
|
|
126
|
+
# 保存しない(runs にも名前・語を書かない)。
|
|
127
|
+
root = accounts_mod.state_dir_for(ACCOUNT)
|
|
128
|
+
for folder, _dirs, files in os.walk(root):
|
|
129
|
+
for name in files:
|
|
130
|
+
with open(os.path.join(folder, name), encoding="utf-8", errors="ignore") as fh:
|
|
131
|
+
body = fh.read()
|
|
132
|
+
assert "コーヒー部0" not in body and "コーヒー 豆" not in body
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_search_human_output_names_the_send_route(x_account, fake, capsys):
|
|
136
|
+
fake["routes"][("GET", "/2/communities/search")] = _communities(1)
|
|
137
|
+
rc, _payload, out = run(capsys, "coffee", as_json=False)
|
|
138
|
+
assert rc == 0
|
|
139
|
+
assert "1493446837214187523" in out.out and "人数=1200" in out.out and "join_policy=Open" in out.out
|
|
140
|
+
assert "--community <id>" in out.out and "community_id:" in out.out
|
|
141
|
+
assert "1 日の本数には数えない" in out.out
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_search_zero_is_not_called_absence(x_account, fake, capsys):
|
|
145
|
+
fake["routes"][("GET", "/2/communities/search")] = {"meta": {"result_count": 0}}
|
|
146
|
+
rc, payload, _ = run(capsys, "nothing")
|
|
147
|
+
assert rc == 0 and payload["n"] == 0
|
|
148
|
+
assert any("区別できません" in line for line in payload["cannot_say"])
|
|
149
|
+
[ledger] = community_rows()
|
|
150
|
+
assert ledger["state"] == "settled" and ledger["count"] == 0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_search_refuses_other_media(tmp_path, isolated_account_factory, fake, capsys):
|
|
154
|
+
_account(tmp_path, isolated_account_factory, name="kanto-threads", media="threads")
|
|
155
|
+
rc, payload, _ = run(capsys, "coffee", account="kanto-threads")
|
|
156
|
+
assert rc == 2 and payload["error"] == "unsupported_on_platform"
|
|
157
|
+
assert payload["cannot_say"] and fake["calls"] == []
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@pytest.mark.parametrize("limit", [0, 26])
|
|
161
|
+
def test_search_limit_out_of_range_never_calls(x_account, fake, capsys, limit):
|
|
162
|
+
rc, payload, _ = run(capsys, "coffee", limit=limit)
|
|
163
|
+
assert rc == 2 and payload["error"] == "invalid_request" and fake["calls"] == []
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_search_limit_above_ten_reserves_that_many(x_account, fake, capsys):
|
|
167
|
+
fake["routes"][("GET", "/2/communities/search")] = _communities(2)
|
|
168
|
+
rc, _payload, _ = run(capsys, "coffee", limit=25)
|
|
169
|
+
assert rc == 0 and fake["calls"][0]["query"]["max_results"] == 25
|
|
170
|
+
assert community_rows()[0]["limit"] == 25
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_search_without_budget_never_calls(tmp_path, isolated_account_factory, fake, capsys):
|
|
174
|
+
_account(tmp_path, isolated_account_factory)
|
|
175
|
+
rc, payload, _ = run(capsys, "coffee")
|
|
176
|
+
assert rc == 1 and payload["error"] == "budget_exhausted" and fake["calls"] == []
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_search_refusal_keeps_a_short_reason_and_charges_nothing(x_account, fake, capsys):
|
|
180
|
+
body = b'{"title":"Forbidden","detail":"You are not permitted to access this feature.","status":403}'
|
|
181
|
+
fake["routes"][("GET", "/2/communities/search")] = urllib.error.HTTPError(
|
|
182
|
+
"u", 403, "Forbidden", {}, io.BytesIO(body))
|
|
183
|
+
rc, payload, _ = run(capsys, "coffee")
|
|
184
|
+
assert rc == 1 and payload["error"] == "upstream_refused"
|
|
185
|
+
assert "x_community_search_http_403: x_not_permitted" in payload["message"]
|
|
186
|
+
[ledger] = community_rows()
|
|
187
|
+
assert ledger["state"] == "settled" and ledger["count"] == 0 and ledger["usd"] == "0.000"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_search_parser_is_registered():
|
|
191
|
+
from thth import cli
|
|
192
|
+
args = cli.build_parser().parse_args(["community", "search", ACCOUNT, "a", "b", "--limit", "3", "--json"])
|
|
193
|
+
assert args.func is community_cli.cmd_community_search and args.words == ["a", "b"] and args.limit == 3
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ----- 出す: 投稿の本体 ----------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
def test_tweet_body_carries_the_community():
|
|
199
|
+
assert x_adapter.tweet_body(base.Post("本文", community_id=COMMUNITY)) == {
|
|
200
|
+
"text": "本文", "community_id": COMMUNITY}
|
|
201
|
+
assert "community_id" not in x_adapter.tweet_body(base.Post("本文"))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_tweet_body_refuses_reply_with_community():
|
|
205
|
+
with pytest.raises(x_adapter.AdapterError):
|
|
206
|
+
x_adapter.tweet_body(base.Post("本文", reply_to="1900000000000000000", community_id=COMMUNITY))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_adapter_publish_posts_into_the_community(env, wire):
|
|
210
|
+
result = env[1].publish(base.Post("コミュニティへ", community_id=COMMUNITY), dry_run=False)
|
|
211
|
+
assert result.post_id and result.failure == "none"
|
|
212
|
+
assert wire["tweets"] == [{"text": "コミュニティへ", "community_id": COMMUNITY}]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@pytest.mark.parametrize("post", [
|
|
216
|
+
base.Post("x", community_id="12ab"),
|
|
217
|
+
base.Post("x", community_id=COMMUNITY, reply_to="1900000000000000000"),
|
|
218
|
+
])
|
|
219
|
+
def test_adapter_refuses_bad_community_before_any_request(env, wire, post):
|
|
220
|
+
result = env[1].publish(post, dry_run=False)
|
|
221
|
+
assert result.error == "x_community_id_invalid" and result.failure == "publish_vetoed"
|
|
222
|
+
assert wire["calls"] == []
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_a_community_refusal_keeps_the_short_x_text(env, wire, monkeypatch):
|
|
226
|
+
original = x_adapter.request
|
|
227
|
+
body = (b'{"type":"https://api.twitter.com/2/problems/not-authorized-for-resource",'
|
|
228
|
+
b'"title":"Forbidden","detail":"You are not a member of this community."}')
|
|
229
|
+
|
|
230
|
+
def request(adapter, method, path, **kwargs):
|
|
231
|
+
if path == "/2/tweets":
|
|
232
|
+
raise urllib.error.HTTPError(adapter.base_url + path, 403, "Forbidden", {}, io.BytesIO(body))
|
|
233
|
+
return original(adapter, method, path, **kwargs)
|
|
234
|
+
monkeypatch.setattr(x_adapter, "request", request)
|
|
235
|
+
result = env[1].publish(base.Post("本文", community_id=COMMUNITY), dry_run=False)
|
|
236
|
+
assert result.failure == "publish_definite"
|
|
237
|
+
assert result.error == ("x_publish_http_403: x_unknown_refusal(type=not-authorized-for-resource"
|
|
238
|
+
" / title=Forbidden / detail=You are not a member of this community.)")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ----- 出す: thth send --community ----------------------------------------------
|
|
242
|
+
|
|
243
|
+
def _send(env, monkeypatch, **kw):
|
|
244
|
+
from thth import core
|
|
245
|
+
monkeypatch.setattr(core, "_append_run", lambda *a, **k: None)
|
|
246
|
+
lines = []
|
|
247
|
+
result = core.send_once("alpha", log=lines.append,
|
|
248
|
+
adapter_factory=lambda cfg, token: env[1], **kw)
|
|
249
|
+
return result, lines
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_send_digest_includes_the_community_only_when_given(env, wire, monkeypatch):
|
|
253
|
+
plain, _ = _send(env, monkeypatch, text="本文")
|
|
254
|
+
into, lines = _send(env, monkeypatch, text="本文", community_id=COMMUNITY)
|
|
255
|
+
assert plain.exit_code == into.exit_code == 0
|
|
256
|
+
assert plain.digest != into.digest
|
|
257
|
+
assert plain.digest == approval.compute_send_digest(text="本文", account="alpha", reply_to=None, topic=None)
|
|
258
|
+
assert any("コミュニティ: " + COMMUNITY in line for line in lines)
|
|
259
|
+
assert wire["calls"] == []
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_send_production_posts_into_the_community_and_records_it(env, wire, monkeypatch):
|
|
263
|
+
from thth import sent as sent_mod
|
|
264
|
+
dry, _ = _send(env, monkeypatch, text="本文", community_id=COMMUNITY)
|
|
265
|
+
result, _ = _send(env, monkeypatch, text="本文", community_id=COMMUNITY,
|
|
266
|
+
production_flag=True, confirm=dry.digest)
|
|
267
|
+
assert result.exit_code == 0 and result.post_id
|
|
268
|
+
assert wire["tweets"] == [{"text": "本文", "community_id": COMMUNITY}]
|
|
269
|
+
record = json.load(open(sent_mod.path_for(accounts_mod.state_dir_for("alpha"), result.post_id),
|
|
270
|
+
encoding="utf-8"))
|
|
271
|
+
assert record["community_id"] == COMMUNITY
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def test_send_confirm_without_community_does_not_post_into_it(env, wire, monkeypatch):
|
|
275
|
+
plain, _ = _send(env, monkeypatch, text="本文")
|
|
276
|
+
result, _ = _send(env, monkeypatch, text="本文", community_id=COMMUNITY,
|
|
277
|
+
production_flag=True, confirm=plain.digest)
|
|
278
|
+
assert result.exit_code == 1 and wire["calls"] == []
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@pytest.mark.parametrize("kw,needle", [
|
|
282
|
+
({"community_id": "12ab"}, "19 桁までの数字"),
|
|
283
|
+
({"community_id": COMMUNITY, "reply_to": "1900000000000000000"}, "返信"),
|
|
284
|
+
])
|
|
285
|
+
def test_send_refuses_bad_community_even_on_dry_run(env, wire, monkeypatch, kw, needle):
|
|
286
|
+
result, lines = _send(env, monkeypatch, text="本文", **kw)
|
|
287
|
+
assert result.exit_code == 2 and result.error == "community_id_invalid" and result.digest is None
|
|
288
|
+
assert any(needle in line for line in lines) and wire["calls"] == []
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_send_refuses_community_on_other_media(env, wire, monkeypatch):
|
|
292
|
+
env[0]["media"] = "threads"
|
|
293
|
+
result, lines = _send(env, monkeypatch, text="本文", community_id=COMMUNITY)
|
|
294
|
+
assert result.exit_code == 2 and result.error == "community_id_invalid"
|
|
295
|
+
assert any("X だけ" in line for line in lines)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def test_send_cli_flag_reaches_core(monkeypatch):
|
|
299
|
+
from thth import cli, core, read_coordination
|
|
300
|
+
seen = {}
|
|
301
|
+
|
|
302
|
+
def fake_send_once(account, **kw):
|
|
303
|
+
seen.update(kw)
|
|
304
|
+
return core.ThrowResult(exit_code=0, mode="rehearsal", action="skip", message="")
|
|
305
|
+
monkeypatch.setattr(core, "send_once", fake_send_once)
|
|
306
|
+
monkeypatch.setattr(read_coordination, "invoke", lambda args, name, fn=None: fn() if fn else args.func(args))
|
|
307
|
+
assert cli.main(["send", "alpha", "--text", "本文", "--community", COMMUNITY]) == 0
|
|
308
|
+
assert seen["community_id"] == COMMUNITY
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# ----- queue の front-matter(承認の指紋・lint・select・束) ----------------------
|
|
312
|
+
|
|
313
|
+
def _sha(fm):
|
|
314
|
+
return approval.compute_approved_sha(
|
|
315
|
+
section="本文", account="alpha", reply_to=approval.reply_to_for_fingerprint(fm),
|
|
316
|
+
topic=None, publish_at="2026-10-14T08:00:00+09:00", **approval.publish_options(fm))
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_fingerprint_is_unchanged_without_community_and_changes_with_it():
|
|
320
|
+
from thth import fingerprint_golden
|
|
321
|
+
assert fingerprint_golden.drift() == []
|
|
322
|
+
assert _sha({}) == _sha({"community_id": ""})
|
|
323
|
+
assert _sha({}) != _sha({"community_id": COMMUNITY})
|
|
324
|
+
assert _sha({"community_id": COMMUNITY}) != _sha({"community_id": "1493446837214187524"})
|
|
325
|
+
parts = approval.compute_approved_components(
|
|
326
|
+
section="本文", account="alpha", reply_to=None, topic=None,
|
|
327
|
+
publish_at="2026-10-14T08:00:00+09:00", community_id=COMMUNITY)
|
|
328
|
+
assert parts["community_id"] == COMMUNITY and "community_id" in approval.COMPARED_KEYS
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@pytest.mark.parametrize("fm,cfg,expected", [
|
|
332
|
+
({"community_id": COMMUNITY}, {"media": "x"}, []),
|
|
333
|
+
({}, {"media": "threads"}, []),
|
|
334
|
+
({"community_id": COMMUNITY}, {"media": "threads"}, ["X だけ"]),
|
|
335
|
+
({"community_id": COMMUNITY}, None, ["X だけ"]),
|
|
336
|
+
({"community_id": "abc"}, {"media": "x"}, ["19 桁までの数字"]),
|
|
337
|
+
({"community_id": "12345678901234567890"}, {"media": "x"}, ["19 桁までの数字"]),
|
|
338
|
+
({"community_id": COMMUNITY, "reply_to": "1900000000000000000"}, {"media": "x"}, ["返信"]),
|
|
339
|
+
({"community_id": COMMUNITY, "reply_to_file": "q.md"}, {"media": "x"}, ["返信"]),
|
|
340
|
+
])
|
|
341
|
+
def test_lint_community_rules(fm, cfg, expected):
|
|
342
|
+
problems = lint_mod.community_id_errors(fm, cfg)
|
|
343
|
+
assert len(problems) == len(expected)
|
|
344
|
+
for problem, needle in zip(problems, expected):
|
|
345
|
+
assert problem.startswith("community_id: ") and needle in problem
|
|
346
|
+
if problems:
|
|
347
|
+
assert lint_mod.reason_code(problems) == "invalid_publish_option"
|
|
348
|
+
assert problems == [p for p in lint_mod.publish_option_errors(fm, cfg) if p.startswith("community_id")]
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def test_bundle_refuses_community():
|
|
352
|
+
from thth import bundle as bundle_mod
|
|
353
|
+
text = ("---\nthth: 2\naccount: alpha\npublish_at: 2026-10-14T08:00:00+09:00\n"
|
|
354
|
+
"continue_until: 2026-10-14T12:00:00+09:00\ncommunity_id: " + COMMUNITY + "\n---\n"
|
|
355
|
+
"## x\n一段目。\n\n---\n\n二段目。\n")
|
|
356
|
+
parsed = bundle_mod.parse_text(text, "b.md")
|
|
357
|
+
assert any(p.startswith("community_id: 連投") for p in bundle_mod.check(parsed, account_cfg={"media": "x"}))
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _queue_file(isolated_account_factory, tmp_path, *, media, community, approve=True):
|
|
361
|
+
import datetime
|
|
362
|
+
from thth import jst, queuefile, select
|
|
363
|
+
info = isolated_account_factory("alpha", media=media, production=True)
|
|
364
|
+
queue = info["queue_dir"]
|
|
365
|
+
os.makedirs(queue, exist_ok=True)
|
|
366
|
+
publish_at = (jst.now_jst() - datetime.timedelta(minutes=5)).replace(microsecond=0).isoformat()
|
|
367
|
+
cfg = accounts_mod.load_account("alpha")
|
|
368
|
+
fm = {"community_id": community} if community else {}
|
|
369
|
+
section = "コミュニティへの本文"
|
|
370
|
+
sha = approval.compute_approved_sha(
|
|
371
|
+
section=approval.effective_section(section, cfg, None), account="alpha", reply_to=None,
|
|
372
|
+
topic=None, publish_at=publish_at, **approval.publish_options(fm))
|
|
373
|
+
lines = ["---", "thth: 1", "account: alpha", f"publish_at: {publish_at}"]
|
|
374
|
+
if community:
|
|
375
|
+
lines.append(f"community_id: {community}")
|
|
376
|
+
if approve:
|
|
377
|
+
lines += ["status: approved", f"approved_sha: {sha}"]
|
|
378
|
+
lines += ["---", f"## {media}", section, ""]
|
|
379
|
+
path = os.path.join(queue, "c.md")
|
|
380
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
381
|
+
fh.write("\n".join(lines))
|
|
382
|
+
return path, cfg, select, queuefile
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def test_select_rejects_community_on_a_non_x_account(isolated_account_factory, tmp_path):
|
|
386
|
+
path, cfg, select, queuefile = _queue_file(isolated_account_factory, tmp_path, media="threads",
|
|
387
|
+
community=COMMUNITY)
|
|
388
|
+
from thth import jst
|
|
389
|
+
result = select.select_one([queuefile.parse(path)], account_name="alpha", account_cfg=cfg, now=jst.now_jst(),
|
|
390
|
+
last_post_at=None, recent_texts=set())
|
|
391
|
+
assert [r.reason for r in result.rejections if r.file == path] == ["community_id_invalid"]
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def test_lint_file_refuses_community_on_a_non_x_account(isolated_account_factory, tmp_path):
|
|
395
|
+
path, *_ = _queue_file(isolated_account_factory, tmp_path, media="threads",
|
|
396
|
+
community=COMMUNITY, approve=False)
|
|
397
|
+
problems = [p for p in lint_mod.lint_file(path) if not lint_mod.is_warning(p)]
|
|
398
|
+
assert any(p.startswith("community_id: ") for p in problems)
|