thth 2.3.2__tar.gz → 2.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {thth-2.3.2 → thth-2.3.3}/PKG-INFO +2 -2
- {thth-2.3.2 → thth-2.3.3}/README.md +1 -1
- {thth-2.3.2 → thth-2.3.3}/skills/thth/SKILL.md +7 -3
- {thth-2.3.2 → thth-2.3.3}/tests/test_behind_remote.py +5 -0
- thth-2.3.3/tests/test_collect_engagements.py +175 -0
- thth-2.3.3/tests/test_read_cli_bare_runtimeerror.py +179 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_skill_doc.py +26 -0
- thth-2.3.3/thth/VERSION +1 -0
- {thth-2.3.2 → thth-2.3.3}/thth/collect.py +56 -2
- {thth-2.3.2 → thth-2.3.3}/thth/thread_read.py +16 -0
- {thth-2.3.2 → thth-2.3.3}/thth/threads_read_cli.py +13 -0
- {thth-2.3.2 → thth-2.3.3}/thth/where_cli.py +10 -0
- {thth-2.3.2 → thth-2.3.3}/thth/who_cli.py +8 -0
- thth-2.3.2/thth/VERSION +0 -1
- {thth-2.3.2 → thth-2.3.3}/.gitignore +0 -0
- {thth-2.3.2 → thth-2.3.3}/LICENSE +0 -0
- {thth-2.3.2 → thth-2.3.3}/accounts.example/bluesky.json +0 -0
- {thth-2.3.2 → thth-2.3.3}/accounts.example/mastodon.json +0 -0
- {thth-2.3.2 → thth-2.3.3}/accounts.example/threads.json +0 -0
- {thth-2.3.2 → thth-2.3.3}/bin/thth +0 -0
- {thth-2.3.2 → thth-2.3.3}/bin/thth-remote +0 -0
- {thth-2.3.2 → thth-2.3.3}/bin/thth-run +0 -0
- {thth-2.3.2 → thth-2.3.3}/mcp/server.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/pyproject.toml +0 -0
- {thth-2.3.2 → thth-2.3.3}/systemd/thth@.service +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/__init__.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/conftest.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/fixtures/build_injection_eval.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/fixtures/callback/worker_responses.json +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/fixtures/injection_eval.json +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/fixtures/umami-bile.md +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/helpers/__init__.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/helpers/fake_oauth_server.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/helpers/fake_push_adapter.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/helpers/fake_redirect_server.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/helpers/fake_search_adapter.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/helpers/fake_threads_reader.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/helpers/fake_threads_writes.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/helpers/hold_lock.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/helpers/hold_repo_lock.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/helpers/worker_probe.mjs +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_account_name_check.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_account_report.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_accounts_dir.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_adapter_boundary.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_after_cli.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_api_error_body.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_app_env_cli.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_approval.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_approve_cmd.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_ask.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_atlas_fifth_review.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_atlas_fourth_review.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_atlas_review.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_atlas_second_review.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_atlas_third_review.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_audit1_fixes.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_audit2_fixes.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_audit_b_counterexamples.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_audit_b_m1_semantics.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_audit_b_n1_depth.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_audit_b_r1_closure.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_audit_v201.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_audit_v23_details.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_auth.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_auth_identity_required.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_bluesky_adapter.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_bluesky_secret_scrub.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_board_needs_review.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_board_running.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_bundle.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_charcount.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_cli_entrypoint.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_cli_help.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_cli_vm_path.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_collect.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_collect_runs.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_collect_sent.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_comparability.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_control_chars_refused.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_conversation_paging.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_count_for.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_docs_links.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_doctor.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_doctor_all_permissions.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_doctor_next_step.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_duplicate_front_matter_keys.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_editorial_audit.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_editorial_n3_n4.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_engagements.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_external_acceptance.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_fake_api.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_form_spec.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_form_spec_cli.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_forms.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_fresh_install.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_front_matter_injection.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_hypotheses.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_hypothesis_boundaries.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_hypothesis_units.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_impact_cli.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_inbox_granted_scopes.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_independent_thread_review.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_independent_topic_review.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_independent_topic_review_r2.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_inflight.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_jst.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_lint.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_list_projections.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_llm_trial.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_llm_trial_engage.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_lock.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_m2_independent.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_maintain.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_mastodon_adapter.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_mastodon_visibility.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_max_per_run.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_mcp.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_mcp_robustness.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_measured.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_media_accounts.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_media_next_steps.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_media_roundtrip.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_mismatch_diagnostics.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_n5_reply_null_element.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_n6_replies_incomplete_ledger.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_n7_doctor_error_body.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_no_limits.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_observation_provenance.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_packaging.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_preview.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_production_mode.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_pull_cli.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_queue.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_quiet_hours.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_r2_r4_r5.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_reason_adoption.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_redact.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_redirect_token_leak.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_refresh.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_refresh_destination.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_rejudge_new.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_rejudge_r2_r3.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_replies.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_replies_refresh.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_repo_lock.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_repo_sync.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_reverse_audit.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_review_case_id_cluster_m2.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_review_case_id_cluster_r2.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_review_cli.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_review_evidence_account.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_review_form_spec_and_case_id.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_review_provenance.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_review_records.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_revoke.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_roundtrip.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_run_missing_env.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_run_record_failclosed.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_runs_collected.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_runs_read_rows.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_schedule_multiple.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_schedule_view.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_scopes_source_requested.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_search_engage_targets.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_secret_value_redact.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_secrets_grep.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_security_small_fixes.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_select.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_selfupdate.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_send.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_send_cli.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_send_media.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_send_visibility.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_sent_integrity.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_server_json.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_site.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_systemd.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_thread_publish.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_thread_read.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_threadrun.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_threads_read_permissions.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_threadshape.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_thth_root.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_token_exists.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_token_scopes_record.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_token_set.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_topic.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_topic_advice.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_topic_cli.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_topic_models.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_topic_review_fixes.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_topic_shelf_observers.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_topic_store.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_topics_record_concurrent.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_topics_tool.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_trial_frictions.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_v21b_approved_writes.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_value_domains_shown.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_version.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_where_cli.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_who_cli.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_writeback_rebase.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/tests/test_writeback_validate.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/__init__.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/__main__.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/account_cli.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/account_report.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/accounts.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/adapters/__init__.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/adapters/base.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/adapters/bluesky.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/adapters/mastodon.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/adapters/threads.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/after_cli.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/appenv.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/approval.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/ask.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/ask_cli.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/bundle.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/cli.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/core.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/doctor.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/engagements.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/forms.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/httpsafe.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/inflight.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/jst.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/lint.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/lock.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/maintain.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/measured.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/oauth.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/postid.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/queuefile.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/redact.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/replies.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/report.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/retract_cli.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/runs.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/scopes.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/secrets_fs.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/select.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/selfupdate.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/sent.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/systemd_gen.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/threadrun.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/threadshape.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/threadthrow.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/topic_advice.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/topic_cli.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/topic_models.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/topic_store.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/topics.py +0 -0
- {thth-2.3.2 → thth-2.3.3}/thth/writeback.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: thth
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.3
|
|
4
4
|
Summary: 承認済みの投稿だけを投げる門(Threads・Bluesky・Mastodon)。人が承認し、記録は利用者の git に残る。
|
|
5
5
|
Project-URL: Homepage, https://thth.me
|
|
6
6
|
Project-URL: Source, https://github.com/aokings/thth
|
|
@@ -45,7 +45,7 @@ watchtower(`~/Developer/watchtower`)の隣に同じ流儀で並べる。watc
|
|
|
45
45
|
|
|
46
46
|
## いまの状態(2026-09-13)
|
|
47
47
|
|
|
48
|
-
版 **2.3.
|
|
48
|
+
版 **2.3.3**。全件テスト **2320 件**(`python -m pytest tests/ -q -n auto -p no:cacheprovider`・1 skip・rc=0)。
|
|
49
49
|
|
|
50
50
|
**媒体**: Threads(稼働)・Bluesky・Mastodon(同席用の台帳あり・未稼働)。
|
|
51
51
|
|
|
@@ -21,7 +21,7 @@ watchtower(`~/Developer/watchtower`)の隣に同じ流儀で並べる。watc
|
|
|
21
21
|
|
|
22
22
|
## いまの状態(2026-09-13)
|
|
23
23
|
|
|
24
|
-
版 **2.3.
|
|
24
|
+
版 **2.3.3**。全件テスト **2320 件**(`python -m pytest tests/ -q -n auto -p no:cacheprovider`・1 skip・rc=0)。
|
|
25
25
|
|
|
26
26
|
**媒体**: Threads(稼働)・Bluesky・Mastodon(同席用の台帳あり・未稼働)。
|
|
27
27
|
|
|
@@ -12,7 +12,8 @@ description: 投稿する前・返信を書く前・出したあと・絡みに
|
|
|
12
12
|
- **`--production` を付けるまで出ない。** 付けない実行は、投げるはずの本文と digest を
|
|
13
13
|
表示して終わる。
|
|
14
14
|
- **記録は利用者の git に残る。** post_id・承認者・承認時刻・返信・実測は、あなたの repo の
|
|
15
|
-
commit として残る。THTH
|
|
15
|
+
commit として残る。THTH のサーバには何も無い。絡みの台帳(`data/sns/engagements/`)は
|
|
16
|
+
公開の commit とは別で、**次の採取(`thth collect`・最大 10 分の遅れ)**で commit される。
|
|
16
17
|
- **数値には分母が付く。** 件数・期間・揃えた条件が無い数字は返さない。取れていない刻みは
|
|
17
18
|
`null` であって `0` ではない。
|
|
18
19
|
- **黙って間違えない。** 拒むときは理由と次の一手を言って非ゼロで終わる(loud reject)。
|
|
@@ -30,8 +31,11 @@ description: 投稿する前・返信を書く前・出したあと・絡みに
|
|
|
30
31
|
- **何を書くかを決めない。** 文体・頻度・時刻・語の選び方はあなたのプロジェクトのもの。
|
|
31
32
|
渡された本文を**整形も切り詰めもせず**そのまま出す。
|
|
32
33
|
- **読む口は何も保存しない。** `thread_read`・`where_to_appear`・`who_is_this` はどれも、
|
|
33
|
-
読んで見せるだけで**台帳に 1
|
|
34
|
-
|
|
34
|
+
読んで見せるだけで**台帳に 1 バイトも書かない**。この 3 つと絡みの台帳
|
|
35
|
+
(`data/sns/engagements/`)だけを見れば、残るのは**自分の行為と反応**だけで、
|
|
36
|
+
**相手の本文・相手の名前・あなたの判断は入らない**。**別の台帳**——自分の投稿への
|
|
37
|
+
返信の台帳(`thth collect` が採り、`thth replies` で読む)——は、この規律の外で
|
|
38
|
+
相手の `username` と本文をそのまま残す(読む口でもなく、絡みの台帳でもない)。
|
|
35
39
|
- **人の代わりに承認しない。** あなた(エージェント)は下書きと digest を人に見せ、
|
|
36
40
|
人が `--confirm` を打つ。
|
|
37
41
|
|
|
@@ -46,6 +46,11 @@ def test_behind_remoteはfetchできないとNoneとreason(tmp_path):
|
|
|
46
46
|
["git", "init", "-b", "main", str(broken)], capture_output=True, text=True)
|
|
47
47
|
assert run_git_init.returncode == 0
|
|
48
48
|
run_git(str(broken), ["remote", "add", "origin", "/does/not/exist/repo.git"])
|
|
49
|
+
# **git の名前とメールを repo に持たせる**(`conftest.init_git_pair()` と同じ)。
|
|
50
|
+
# 開発機(macOS)は全体の設定があるので通っていたが、GitHub Actions の
|
|
51
|
+
# Linux には無く、`commit` が "Author identity unknown" で落ちた(2026-09-16)。
|
|
52
|
+
run_git(str(broken), ["config", "user.email", "thth-test@example.invalid"])
|
|
53
|
+
run_git(str(broken), ["config", "user.name", "thth-test"])
|
|
49
54
|
run_git(str(broken), ["commit", "--allow-empty", "-m", "x"])
|
|
50
55
|
|
|
51
56
|
info = writeback.behind_remote(str(broken))
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""絡みの台帳(`data/sns/engagements/`)を採取の commit に含める(T9-1・
|
|
2
|
+
照合「X Developer Agreement と自分の泉」検収 1)。
|
|
3
|
+
|
|
4
|
+
**事実**: 投稿の commit(`core.py` の `writeback.commit_and_push(rel_path=原稿)`)は
|
|
5
|
+
原稿ファイルだけ、採取の commit(`collect.py` の `result["touched"]`)は採取で
|
|
6
|
+
触ったファイルだけを add する。同席送信(`core._send_locked`)は commit しない。
|
|
7
|
+
`thth/engagements.py::append()` が書く `data/sns/engagements/<YYYY-MM>.ndjson` は
|
|
8
|
+
どこからも add されず、VM の repo に未追跡のまま溜まっていた。
|
|
9
|
+
|
|
10
|
+
固定するのは 4 つ(発注 T9-1 の (a)〜(d)):
|
|
11
|
+
|
|
12
|
+
(a) `engagements.append` で 1 行書いた repo で `collect` を 1 回 → origin に
|
|
13
|
+
`data/sns/engagements/<月>.ndjson` が push されている。
|
|
14
|
+
(b) 採取で触るものが 0 でも (a) と同じ。
|
|
15
|
+
(c) repo が無い account では git を呼ばない。
|
|
16
|
+
(d) 何も変わっていなければ空の commit を作らない。
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import datetime
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import subprocess
|
|
24
|
+
|
|
25
|
+
from tests.conftest import init_git_pair, make_queue_text
|
|
26
|
+
from tests.test_collect import FakeAdapter
|
|
27
|
+
from thth import accounts as accounts_mod
|
|
28
|
+
from thth import collect as collect_mod
|
|
29
|
+
from thth import engagements as engagements_mod
|
|
30
|
+
from thth import jst
|
|
31
|
+
|
|
32
|
+
NOW = datetime.datetime(2026, 9, 16, 12, 0, tzinfo=jst.JST)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class NoOpAdapter:
|
|
36
|
+
"""能力を何も名乗らない偽アダプタ(`collect_once()` に何も触らせない)。
|
|
37
|
+
|
|
38
|
+
`tests/test_collect_sent.py` の `FakeAdapter` と違い `views` すら持たない
|
|
39
|
+
——**採取が触るものを 0 にする**ためだけの道具。
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
CAPABILITIES = frozenset()
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def capabilities(cls):
|
|
46
|
+
return set(cls.CAPABILITIES)
|
|
47
|
+
|
|
48
|
+
def insights(self, post_id):
|
|
49
|
+
raise AssertionError("能力を名乗っていないのに insights を呼んだ")
|
|
50
|
+
|
|
51
|
+
def conversation(self, post_id, *, since=None):
|
|
52
|
+
raise AssertionError("能力を名乗っていないのに conversation を呼んだ")
|
|
53
|
+
|
|
54
|
+
def account_insights(self, user_id, *, since, until):
|
|
55
|
+
raise AssertionError("能力を名乗っていないのに account_insights を呼んだ")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _engagement_row(**over):
|
|
59
|
+
row = {
|
|
60
|
+
"schema": engagements_mod.SCHEMA,
|
|
61
|
+
"post_id": "111",
|
|
62
|
+
"reply_to": "222",
|
|
63
|
+
"root_post": None,
|
|
64
|
+
"author_key": None,
|
|
65
|
+
"account": None, # append() 側の既定に任せる
|
|
66
|
+
"medium": "threads",
|
|
67
|
+
"topic": None,
|
|
68
|
+
"form": None,
|
|
69
|
+
"hour_band": "朝",
|
|
70
|
+
"posted_at": "2026-09-16T08:00:00+09:00",
|
|
71
|
+
"found_by": None,
|
|
72
|
+
}
|
|
73
|
+
row.update(over)
|
|
74
|
+
return row
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _in_origin(bare: str) -> str:
|
|
78
|
+
return subprocess.run(
|
|
79
|
+
["git", "-C", bare, "ls-tree", "-r", "--name-only", "main"],
|
|
80
|
+
capture_output=True, text=True,
|
|
81
|
+
).stdout
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_a_絡みの台帳が採取のcommitでpushされる(tmp_path, isolated_account_factory):
|
|
85
|
+
"""採取が投稿本体も触る、ふつうの回。絡みの台帳も同じ commit に乗って
|
|
86
|
+
origin まで届く。"""
|
|
87
|
+
pair = init_git_pair(tmp_path, seed_content=make_queue_text({
|
|
88
|
+
"status": "posted", "post_id": "POST1",
|
|
89
|
+
"posted_at": "2026-09-16T10:00:00+09:00"})) # 2 時間前・1h の刻みを跨ぐ
|
|
90
|
+
account = isolated_account_factory(repo_dir=pair["work"], production=True)
|
|
91
|
+
|
|
92
|
+
cfg = accounts_mod.load_account(account["name"])
|
|
93
|
+
engagements_mod.append(cfg, account["name"],
|
|
94
|
+
_engagement_row(account=account["name"]), now=NOW)
|
|
95
|
+
|
|
96
|
+
rc = collect_mod.run_collect(account["name"], adapter=FakeAdapter(), now=NOW,
|
|
97
|
+
log=lambda _l: None)
|
|
98
|
+
|
|
99
|
+
assert rc == 0
|
|
100
|
+
in_origin = _in_origin(pair["bare"])
|
|
101
|
+
assert "data/sns/engagements/2026-09.ndjson" in in_origin, in_origin
|
|
102
|
+
# ついでに、投稿の実測も同じ commit の対象として届いていること
|
|
103
|
+
# (足した処理が既存の touched を壊していないこと)。
|
|
104
|
+
assert "data/sns/insights/posts/POST1.ndjson" in in_origin, in_origin
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_b_採取で触るものが0でも絡みの台帳はpushされる(tmp_path, isolated_account_factory):
|
|
108
|
+
"""`collect_once()` の `touched` が空でも、絡みの台帳の未追跡・変更だけで
|
|
109
|
+
commit する。"""
|
|
110
|
+
pair = init_git_pair(tmp_path, seed_content=make_queue_text({
|
|
111
|
+
"status": "posted", "post_id": "POST1",
|
|
112
|
+
# collect_days(既定 14 日)をとうに過ぎている → posts_seen は 0、
|
|
113
|
+
# touched も空のまま(NoOpAdapter は何も能力を名乗らないので
|
|
114
|
+
# アカウント日次も呼ばれない)。
|
|
115
|
+
"posted_at": "2026-08-01T10:00:00+09:00"}))
|
|
116
|
+
account = isolated_account_factory(repo_dir=pair["work"], production=True)
|
|
117
|
+
|
|
118
|
+
cfg = accounts_mod.load_account(account["name"])
|
|
119
|
+
engagements_mod.append(cfg, account["name"],
|
|
120
|
+
_engagement_row(account=account["name"]), now=NOW)
|
|
121
|
+
|
|
122
|
+
rc = collect_mod.run_collect(account["name"], adapter=NoOpAdapter(), now=NOW,
|
|
123
|
+
log=lambda _l: None)
|
|
124
|
+
|
|
125
|
+
assert rc == 0
|
|
126
|
+
in_origin = _in_origin(pair["bare"])
|
|
127
|
+
assert "data/sns/engagements/2026-09.ndjson" in in_origin, in_origin
|
|
128
|
+
# 投稿側は本当に何も触っていないこと(この test が (a) の焼き直しに
|
|
129
|
+
# なっていないことの確認)。
|
|
130
|
+
assert "data/sns/insights/posts/POST1.ndjson" not in in_origin, in_origin
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_c_repoが無いaccountでは絡みの台帳も含めてgitを呼ばない(
|
|
134
|
+
tmp_path, isolated_account_factory, monkeypatch):
|
|
135
|
+
"""`repo_dir` が実在しない台帳(`repos/_none` 相当)では、絡みの台帳の
|
|
136
|
+
判定そのもの(`_pending_engagement_paths`)を呼ばない——呼べば
|
|
137
|
+
`git status` を repo の外で走らせる危険がある。"""
|
|
138
|
+
account = isolated_account_factory(
|
|
139
|
+
repo_dir=str(tmp_path / "repos" / "_none"), production=True, scheduled=False)
|
|
140
|
+
|
|
141
|
+
def 呼ぶな(*a, **k):
|
|
142
|
+
raise AssertionError("repo が無いのに絡みの台帳の git 判定を呼んだ")
|
|
143
|
+
|
|
144
|
+
monkeypatch.setattr(collect_mod, "_pending_engagement_paths", 呼ぶな)
|
|
145
|
+
monkeypatch.setattr(collect_mod, "_git", 呼ぶな)
|
|
146
|
+
monkeypatch.setattr(collect_mod.writeback, "sync_repo", 呼ぶな)
|
|
147
|
+
monkeypatch.setattr(collect_mod.writeback, "commit_and_push", 呼ぶな)
|
|
148
|
+
|
|
149
|
+
rc = collect_mod.run_collect(account["name"], adapter=NoOpAdapter(), now=NOW,
|
|
150
|
+
log=lambda _l: None)
|
|
151
|
+
assert rc == 0
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_d_変わっていなければ空のcommitを作らない(tmp_path, isolated_account_factory,
|
|
155
|
+
monkeypatch):
|
|
156
|
+
"""絡みの台帳に変更が無い回は、`commit_and_push` を一切呼ばない。"""
|
|
157
|
+
pair = init_git_pair(tmp_path, seed_content=make_queue_text({
|
|
158
|
+
"status": "posted", "post_id": "POST1",
|
|
159
|
+
"posted_at": "2026-08-01T10:00:00+09:00"})) # collect_days を過ぎている
|
|
160
|
+
account = isolated_account_factory(repo_dir=pair["work"], production=True)
|
|
161
|
+
|
|
162
|
+
calls = []
|
|
163
|
+
real_commit_and_push = collect_mod.writeback.commit_and_push
|
|
164
|
+
|
|
165
|
+
def 数える(*a, **k):
|
|
166
|
+
calls.append((a, k))
|
|
167
|
+
return real_commit_and_push(*a, **k)
|
|
168
|
+
|
|
169
|
+
monkeypatch.setattr(collect_mod.writeback, "commit_and_push", 数える)
|
|
170
|
+
|
|
171
|
+
rc = collect_mod.run_collect(account["name"], adapter=NoOpAdapter(), now=NOW,
|
|
172
|
+
log=lambda _l: None)
|
|
173
|
+
|
|
174
|
+
assert rc == 0
|
|
175
|
+
assert calls == [], "変更が無いのに commit_and_push を呼んだ"
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""読む口 4 つ(`thread`・`where`・`who`・`threads_read_cli` の口)が、adapter の
|
|
2
|
+
素の `RuntimeError` で traceback にならない(T9-2・照合「X Developer Agreement
|
|
3
|
+
と自分の泉」検収 5)。
|
|
4
|
+
|
|
5
|
+
**事実**: `thth/adapters/base.py` の `AdapterError` は `RuntimeError` の子。
|
|
6
|
+
Bluesky(`bluesky.py` の `_request` 周り・`session()` の 367・374 行、`_xrpc()` の
|
|
7
|
+
260・262 行)は素の `RuntimeError` を投げる箇所がある。`thth/thread_read.py`・
|
|
8
|
+
`thth/where_cli.py`・`thth/who_cli.py`・`thth/threads_read_cli.py::_run` はどれも
|
|
9
|
+
`AdapterError` と `PermissionMissing` だけを捕まえていたので、素の `RuntimeError`
|
|
10
|
+
は traceback になっていた。
|
|
11
|
+
|
|
12
|
+
ここでは実際の Bluesky・Threads の通信は起こさず、`adapters.make_adapter()` を
|
|
13
|
+
差し替えた偽アダプタで「adapter が素の `RuntimeError` を投げたら」を作る——
|
|
14
|
+
4 つの口それぞれの捕捉の**位置**(`thread_read.cmd_thread`・
|
|
15
|
+
`where_cli._account_node` の語ごとの try・`who_cli._profile_for`・
|
|
16
|
+
`threads_read_cli._run`)を直に確かめる。
|
|
17
|
+
|
|
18
|
+
固定するのは:
|
|
19
|
+
- `thread`・`threads_read_cli`(`topics --search`): rc=1・`--json` は
|
|
20
|
+
`{"error", "account"}`・**adapter の例外の型は変えていない**ので
|
|
21
|
+
`AdapterError` の枝はそのまま通る。
|
|
22
|
+
- `where`・`who`: その account(語・プロフィール)の `cannot_say` に入り、
|
|
23
|
+
他の語・他の account は続く(`AdapterError` と同じ既存の筋のまま)。
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
|
|
30
|
+
import pytest
|
|
31
|
+
|
|
32
|
+
from thth import accounts as accounts_mod
|
|
33
|
+
from thth import adapters as adapters_mod
|
|
34
|
+
from thth import thread_read as thread_read_mod
|
|
35
|
+
from thth import threads_read_cli as threads_read_cli_mod
|
|
36
|
+
from thth import where_cli as where_cli_mod
|
|
37
|
+
from thth import who_cli as who_cli_mod
|
|
38
|
+
|
|
39
|
+
BARE_MESSAGE = "素の RuntimeError(fake・T9-2・bluesky._request 相当)"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _BareRuntimeErrorAdapter:
|
|
43
|
+
"""能力だけ答え、実際に叩かれたら**`AdapterError` ではない**素の
|
|
44
|
+
`RuntimeError` を投げる偽アダプタ。Bluesky の `_request`/`session()` が
|
|
45
|
+
本番で投げる形を模す。"""
|
|
46
|
+
|
|
47
|
+
def capabilities(self):
|
|
48
|
+
return {"thread_read", "keyword_search", "profile_lookup", "mentions"}
|
|
49
|
+
|
|
50
|
+
def fetch_post(self, post_id):
|
|
51
|
+
raise RuntimeError(BARE_MESSAGE)
|
|
52
|
+
|
|
53
|
+
def conversation(self, post_id, *, since=None):
|
|
54
|
+
raise RuntimeError(BARE_MESSAGE)
|
|
55
|
+
|
|
56
|
+
def keyword_search(self, word, *, search_type, limit):
|
|
57
|
+
raise RuntimeError(BARE_MESSAGE)
|
|
58
|
+
|
|
59
|
+
def profile_lookup(self, username):
|
|
60
|
+
raise RuntimeError(BARE_MESSAGE)
|
|
61
|
+
|
|
62
|
+
def mentions(self, *, since=None):
|
|
63
|
+
raise RuntimeError(BARE_MESSAGE)
|
|
64
|
+
|
|
65
|
+
def granted_scopes(self):
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _write_token(path):
|
|
70
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
71
|
+
json.dump({"access_token": "FAKE-SECRET", "user_id": "999999",
|
|
72
|
+
"username": "nigamilab", "scopes": None,
|
|
73
|
+
"obtained_at": "2026-09-16T09:00:00+09:00"}, f)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.fixture
|
|
77
|
+
def bare_runtimeerror_account(tmp_path, isolated_account_factory, monkeypatch):
|
|
78
|
+
"""Threads 台帳(4 つの capability を全部持つ実クラスの検査は通す)だが、
|
|
79
|
+
`adapters.make_adapter()` を差し替えて、実際に返す adapter は素の
|
|
80
|
+
`RuntimeError` しか投げない偽物にする。"""
|
|
81
|
+
token_path = str(tmp_path / "a.token")
|
|
82
|
+
_write_token(token_path)
|
|
83
|
+
account = isolated_account_factory(token=token_path, handle="nigamilab")
|
|
84
|
+
monkeypatch.setattr(adapters_mod, "make_adapter",
|
|
85
|
+
lambda cfg, token: _BareRuntimeErrorAdapter())
|
|
86
|
+
return account
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------------- thread_read
|
|
90
|
+
|
|
91
|
+
def test_thread読む口はrc1でtracebackにならない(bare_runtimeerror_account, capsys):
|
|
92
|
+
args = argparse.Namespace(account=bare_runtimeerror_account["name"], post_id="POST1",
|
|
93
|
+
since=None, max_messages=thread_read_mod.DEFAULT_MAX_MESSAGES,
|
|
94
|
+
json=True)
|
|
95
|
+
rc = thread_read_mod.cmd_thread(args)
|
|
96
|
+
out = capsys.readouterr()
|
|
97
|
+
|
|
98
|
+
assert rc == 1
|
|
99
|
+
assert "Traceback" not in out.err
|
|
100
|
+
payload = json.loads(out.out)
|
|
101
|
+
assert set(payload) == {"error", "account"}
|
|
102
|
+
assert payload["account"] == bare_runtimeerror_account["name"]
|
|
103
|
+
assert BARE_MESSAGE in payload["error"]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ------------------------------------------------------------ threads_read_cli
|
|
107
|
+
|
|
108
|
+
def test_threads_read_cliの口はrc1でtracebackにならない(bare_runtimeerror_account, capsys):
|
|
109
|
+
args = argparse.Namespace(account=bare_runtimeerror_account["name"], search="語",
|
|
110
|
+
recent=False, limit=25, json=True)
|
|
111
|
+
rc = threads_read_cli_mod.cmd_topics_search(args)
|
|
112
|
+
out = capsys.readouterr()
|
|
113
|
+
|
|
114
|
+
assert rc == 1
|
|
115
|
+
assert "Traceback" not in out.err
|
|
116
|
+
payload = json.loads(out.out)
|
|
117
|
+
assert set(payload) == {"error", "account"}
|
|
118
|
+
assert payload["account"] == bare_runtimeerror_account["name"]
|
|
119
|
+
assert BARE_MESSAGE in payload["error"]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# --------------------------------------------------------------------- where
|
|
123
|
+
|
|
124
|
+
def test_where読む口はcannot_sayに入り他の語は続く(bare_runtimeerror_account, capsys):
|
|
125
|
+
args = argparse.Namespace(project=None,
|
|
126
|
+
targets=[bare_runtimeerror_account["name"], "苦い", "コーヒー"],
|
|
127
|
+
recent=False, limit=where_cli_mod.DEFAULT_LIMIT, json=True)
|
|
128
|
+
rc = where_cli_mod.cmd_where(args)
|
|
129
|
+
out = capsys.readouterr()
|
|
130
|
+
|
|
131
|
+
assert rc == 0, out.err # `AdapterError` と同じ筋(account 単位の失敗にしない)
|
|
132
|
+
assert "Traceback" not in out.err
|
|
133
|
+
result = json.loads(out.out)
|
|
134
|
+
node = result["by_account"][bare_runtimeerror_account["name"]]
|
|
135
|
+
# 2 語とも by_word には入らず、node の cannot_say に落ちる(PermissionMissing/
|
|
136
|
+
# AdapterError と同じ扱い・T9-2 で RuntimeError も同じ列に並んだ)。
|
|
137
|
+
assert node["by_word"] == {}
|
|
138
|
+
assert any("苦い" in line and BARE_MESSAGE in line for line in node["cannot_say"])
|
|
139
|
+
assert any("コーヒー" in line and BARE_MESSAGE in line for line in node["cannot_say"])
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------- who
|
|
143
|
+
|
|
144
|
+
def test_whoの読む口はcannot_sayに入る(bare_runtimeerror_account, capsys):
|
|
145
|
+
args = argparse.Namespace(project=None,
|
|
146
|
+
targets=[bare_runtimeerror_account["name"], "@bob"],
|
|
147
|
+
profile=True, json=True)
|
|
148
|
+
rc = who_cli_mod.cmd_who(args)
|
|
149
|
+
out = capsys.readouterr()
|
|
150
|
+
|
|
151
|
+
assert rc == 0, out.err
|
|
152
|
+
assert "Traceback" not in out.err
|
|
153
|
+
result = json.loads(out.out)
|
|
154
|
+
assert result["profile"] is None
|
|
155
|
+
assert any(BARE_MESSAGE in line for line in result["cannot_say"])
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ----------------------------------------------------- adapter の例外の型は不変
|
|
159
|
+
|
|
160
|
+
def test_AdapterErrorの枝は素のRuntimeErrorに食われない():
|
|
161
|
+
"""`except AdapterError` が先・`except RuntimeError` が後——`AdapterError`
|
|
162
|
+
は `RuntimeError` の子なので、順序を間違えると `AdapterError` の枝が
|
|
163
|
+
死んでいてもテストは気づかない。ここは 4 箇所のソースを直に読んで、
|
|
164
|
+
`AdapterError` の except が `RuntimeError` の except より**前**にあることを
|
|
165
|
+
確かめる(読み違いで壊す変異にも強くする)。"""
|
|
166
|
+
import inspect
|
|
167
|
+
|
|
168
|
+
for mod, func_name in (
|
|
169
|
+
(thread_read_mod, "cmd_thread"),
|
|
170
|
+
(threads_read_cli_mod, "_run"),
|
|
171
|
+
(where_cli_mod, "_account_node"),
|
|
172
|
+
(who_cli_mod, "_profile_for"),
|
|
173
|
+
):
|
|
174
|
+
src = inspect.getsource(getattr(mod, func_name))
|
|
175
|
+
adapter_error_at = src.index("except adapter_base.AdapterError")
|
|
176
|
+
runtime_error_at = src.index("except RuntimeError")
|
|
177
|
+
assert adapter_error_at < runtime_error_at, (
|
|
178
|
+
f"{mod.__name__}.{func_name}: AdapterError の except が "
|
|
179
|
+
f"RuntimeError より後にある——AdapterError の枝が死ぬ")
|
|
@@ -99,3 +99,29 @@ def test_泉に落とすかどうかの節が削られている():
|
|
|
99
99
|
"""T4-1「『泉に落とすかどうか(§4)』の節は削る」。"""
|
|
100
100
|
text = _read()
|
|
101
101
|
assert "泉に落とすかどうか" not in text
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _bullets(text: str) -> list:
|
|
105
|
+
"""トップレベルの `- ` 箇条書きを 1 項目 1 要素に割る。
|
|
106
|
+
|
|
107
|
+
続きの行(インデントされた行)は直前の項目にくっつく——1 つの箇条書き
|
|
108
|
+
項目が複数行にまたがっていても 1 ブロックとして見られるように。
|
|
109
|
+
"""
|
|
110
|
+
return re.split(r"\n(?=- )", text)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_replies台帳が相手のusernameと本文を残すと書かれている():
|
|
114
|
+
"""T9-3: 自分の投稿への返信の台帳(`thth collect` が採り、`thth replies` で
|
|
115
|
+
読む)は相手の `username` と本文を残す——「何を絶対にしないか」の読む口の
|
|
116
|
+
項(`thread_read`・`where_to_appear`・`who_is_this` は何も保存しない・
|
|
117
|
+
絡みの台帳は自分の行為と反応だけ)と混同されないよう、この事実を書く
|
|
118
|
+
(照合「X Developer Agreement と自分の泉」検収 2)。
|
|
119
|
+
|
|
120
|
+
**言い方を固定しすぎない検査**: 特定の文言を assert するのではなく、
|
|
121
|
+
`replies` と `username` が**同じ箇条書き項目**に出てくることだけを見る。
|
|
122
|
+
"""
|
|
123
|
+
text = _read()
|
|
124
|
+
bullets = _bullets(text)
|
|
125
|
+
hit = [b for b in bullets if "replies" in b and "username" in b]
|
|
126
|
+
assert hit, ("「replies」と「username」が同じ箇条書き項目に出てくる"
|
|
127
|
+
"ブロックがありません")
|
thth-2.3.3/thth/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.3.3
|
|
@@ -132,6 +132,54 @@ def pending_paths(repo_dir: str, account_cfg: dict) -> list:
|
|
|
132
132
|
return sorted(out)
|
|
133
133
|
|
|
134
134
|
|
|
135
|
+
def _pending_engagement_paths(repo_dir: str, account_cfg: dict, account_name: str) -> list:
|
|
136
|
+
"""絡みの台帳(`data/sns/engagements/*.ndjson`)のうち、未追跡か変更が
|
|
137
|
+
あるものを絶対パスで返す(T9-1・照合「X Developer Agreement と自分の泉」
|
|
138
|
+
検収 1)。
|
|
139
|
+
|
|
140
|
+
`collect_once()` の `touched` は「今回の採取で触ったファイル」だけを
|
|
141
|
+
数える。絡みの台帳(`thth/engagements.py::append()`)は**公開が確定した
|
|
142
|
+
直後**に書かれ、それは `thth core.py` の queue 経由の投稿だけでなく
|
|
143
|
+
`_send_locked`(同席送信)でも起きる——だが `_send_locked` は commit しない
|
|
144
|
+
(`core.py:707,759-780`)。結果、絡みの行は VM の repo に未追跡のまま
|
|
145
|
+
溜まり、**同期(`sync_repo`)は未追跡を見ないので止まらないが、git には
|
|
146
|
+
一度も入らない**。
|
|
147
|
+
|
|
148
|
+
ここで `git status --porcelain` を見て、採取自身が何も触らなくても
|
|
149
|
+
絡みの台帳の変更を拾う(`writeback.pending_paths()` と同じ流儀だが、
|
|
150
|
+
見る先を `data/sns` 全体ではなく **engagements だけ**に絞る——他の
|
|
151
|
+
`data/sns` 配下の変更を二重に拾わないため)。
|
|
152
|
+
"""
|
|
153
|
+
if not repo_dir or not os.path.isdir(repo_dir):
|
|
154
|
+
return []
|
|
155
|
+
dirs = accounts_mod.data_dirs(account_cfg, account_name)
|
|
156
|
+
engagements_dir = dirs["engagements"]
|
|
157
|
+
if not os.path.isdir(engagements_dir):
|
|
158
|
+
return []
|
|
159
|
+
rel_dir = os.path.relpath(os.path.realpath(engagements_dir), os.path.realpath(repo_dir))
|
|
160
|
+
if rel_dir == os.pardir or rel_dir.startswith(os.pardir + os.sep):
|
|
161
|
+
# engagements が repo の外を指す異常系(state 側の置き場と repo_dir が
|
|
162
|
+
# 食い違っている)。git には触らない。
|
|
163
|
+
return []
|
|
164
|
+
# **`--untracked-files=all` が要る。** 既定の `git status --porcelain` は
|
|
165
|
+
# 中身が丸ごと未追跡なディレクトリを `?? data/sns/engagements/`(末尾
|
|
166
|
+
# スラッシュ付きの 1 行)にまとめて返し、個々のファイル名を出さない。
|
|
167
|
+
# `engagements/` は採取が repo に初めて作る側なので、**最初の 1 回は
|
|
168
|
+
# ほぼ必ずこの「ディレクトリごと未追跡」の形**になる——`.ndjson` で
|
|
169
|
+
# 終わらない行として弾かれ、絶対に拾えなかった(この関数を書いた本人が
|
|
170
|
+
# 変異テストで踏んだ)。
|
|
171
|
+
status = _git(repo_dir, ["status", "--porcelain", "--untracked-files=all",
|
|
172
|
+
"--", rel_dir])
|
|
173
|
+
if status.returncode != 0:
|
|
174
|
+
return []
|
|
175
|
+
out = []
|
|
176
|
+
for line in status.stdout.splitlines():
|
|
177
|
+
path = line[3:].strip()
|
|
178
|
+
if path and path.endswith(".ndjson"):
|
|
179
|
+
out.append(os.path.join(repo_dir, path))
|
|
180
|
+
return sorted(out)
|
|
181
|
+
|
|
182
|
+
|
|
135
183
|
def _read_ndjson_strict(path: str) -> tuple:
|
|
136
184
|
"""`(行, 壊れているか)`。**壊れた行を黙って飛ばさない。**
|
|
137
185
|
|
|
@@ -1004,9 +1052,15 @@ def _collect_entry(account_name: str, *, adapter, now, log, info: dict) -> int:
|
|
|
1004
1052
|
result = collect_once(account_name, adapter=adapter, now=now, log=log)
|
|
1005
1053
|
_note(info, result)
|
|
1006
1054
|
|
|
1007
|
-
|
|
1055
|
+
# T9-1: 絡みの台帳(`_send_locked` が書いた行も含む)は `collect_once()`
|
|
1056
|
+
# の `touched` に一度も現れない。採取で他に何も触らなくても、絡みの
|
|
1057
|
+
# 台帳に未追跡・変更があれば同じ commit に含める。
|
|
1058
|
+
engagement_paths = _pending_engagement_paths(repo_dir, account_cfg, account_name)
|
|
1059
|
+
all_touched = sorted(set(result["touched"]) | set(engagement_paths))
|
|
1060
|
+
|
|
1061
|
+
if all_touched:
|
|
1008
1062
|
rel = [os.path.relpath(os.path.realpath(p), os.path.realpath(repo_dir))
|
|
1009
|
-
for p in
|
|
1063
|
+
for p in all_touched]
|
|
1010
1064
|
pushed, push_err = writeback.commit_and_push(
|
|
1011
1065
|
repo_dir, rel_path=rel,
|
|
1012
1066
|
message=f"収集: {len(rel)} ファイル({account_name})")
|
|
@@ -474,6 +474,22 @@ def cmd_thread(args) -> int:
|
|
|
474
474
|
else:
|
|
475
475
|
print(message, file=sys.stderr)
|
|
476
476
|
return 1
|
|
477
|
+
except RuntimeError as e:
|
|
478
|
+
# **`AdapterError` ではない素の `RuntimeError`**(Bluesky の `_request`
|
|
479
|
+
# 周りなど)を、`AdapterError` と同じ出し方で受ける(T9-2・照合
|
|
480
|
+
# 「X Developer Agreement と自分の泉」検収 5)。`AdapterError` は
|
|
481
|
+
# `RuntimeError` の子なので、この節は上の `except AdapterError` を
|
|
482
|
+
# すり抜けたものだけを拾う。**adapter が投げる例外の型は変えない**
|
|
483
|
+
# ——ここは受け口を増やすだけ。
|
|
484
|
+
message = f"{args.account}: {redact_mod.redact(str(e))}"
|
|
485
|
+
_record_run(args.account, media=media, post_id=args.post_id, messages=None,
|
|
486
|
+
truncated=False, status="error", error=message)
|
|
487
|
+
if as_json:
|
|
488
|
+
print(json.dumps({"error": message, "account": args.account},
|
|
489
|
+
ensure_ascii=False, indent=2))
|
|
490
|
+
else:
|
|
491
|
+
print(message, file=sys.stderr)
|
|
492
|
+
return 1
|
|
477
493
|
|
|
478
494
|
_record_run(args.account, media=media, post_id=args.post_id,
|
|
479
495
|
messages=result["counts"]["messages"],
|
|
@@ -246,6 +246,19 @@ def _run(account: str, *, capability: str, call, as_json: bool, render,
|
|
|
246
246
|
else:
|
|
247
247
|
print(message, file=sys.stderr)
|
|
248
248
|
return 1
|
|
249
|
+
except RuntimeError as e:
|
|
250
|
+
# **`AdapterError` ではない素の `RuntimeError`**(Bluesky の `_request`
|
|
251
|
+
# 周りなど)を、`AdapterError` と同じ出し方で受ける(T9-2)。
|
|
252
|
+
# `AdapterError` は `RuntimeError` の子なので、上の
|
|
253
|
+
# `except AdapterError` をすり抜けたものだけがここに来る。**adapter が
|
|
254
|
+
# 投げる例外の型は変えない**——ここは受け口を増やすだけ。
|
|
255
|
+
message = f"{account}: {redact_mod.redact(str(e))}"
|
|
256
|
+
if as_json:
|
|
257
|
+
print(json.dumps({"error": message, "account": account},
|
|
258
|
+
ensure_ascii=False, indent=2))
|
|
259
|
+
else:
|
|
260
|
+
print(message, file=sys.stderr)
|
|
261
|
+
return 1
|
|
249
262
|
return render(result)
|
|
250
263
|
|
|
251
264
|
|
|
@@ -163,6 +163,16 @@ def _account_node(account_name: str, words: list, *, search_type: str,
|
|
|
163
163
|
except adapter_base.AdapterError as e:
|
|
164
164
|
node_cannot_say.append(f"{word}: {redact_mod.redact(str(e))}")
|
|
165
165
|
continue
|
|
166
|
+
except RuntimeError as e:
|
|
167
|
+
# **`AdapterError` ではない素の `RuntimeError`**(Bluesky の
|
|
168
|
+
# `_request` 周りなど)を、`AdapterError` と同じ出し方で受ける
|
|
169
|
+
# (T9-2)。`AdapterError` は `RuntimeError` の子なので、上の
|
|
170
|
+
# `except AdapterError` をすり抜けたものだけがここに来る。この
|
|
171
|
+
# account 全体を落とさず、その語だけ `cannot_say` に流して他の
|
|
172
|
+
# 語・他の account を続ける(`--project` で 1 account だけ落ちて
|
|
173
|
+
# も他が出る、という既存の筋と同じ)。
|
|
174
|
+
node_cannot_say.append(f"{word}: {redact_mod.redact(str(e))}")
|
|
175
|
+
continue
|
|
166
176
|
|
|
167
177
|
material = threads_read_cli_mod.search_material(
|
|
168
178
|
rows, q=word, search_type=search_type, limit=limit)
|
|
@@ -95,6 +95,14 @@ def _profile_for(account_cfg: dict, account_name: str, *, username: str | None):
|
|
|
95
95
|
return None, _permission_message(account_name, adapter, e)
|
|
96
96
|
except adapter_base.AdapterError as e:
|
|
97
97
|
return None, redact_mod.redact(str(e))
|
|
98
|
+
except RuntimeError as e:
|
|
99
|
+
# **`AdapterError` ではない素の `RuntimeError`**(Bluesky の
|
|
100
|
+
# `_request` 周りなど)を、`AdapterError` と同じ出し方で受ける
|
|
101
|
+
# (T9-2)。`AdapterError` は `RuntimeError` の子なので、上の
|
|
102
|
+
# `except AdapterError` をすり抜けたものだけがここに来る。呼び出し側
|
|
103
|
+
# (`_account_node`)はこれを `cannot_say` に足して account 全体は
|
|
104
|
+
# 落とさない。
|
|
105
|
+
return None, redact_mod.redact(str(e))
|
|
98
106
|
return profile, None
|
|
99
107
|
|
|
100
108
|
|
thth-2.3.2/thth/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2.3.2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|