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