thth 2.0.1__tar.gz → 2.2.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-2.0.1 → thth-2.2.0}/PKG-INFO +4 -4
- {thth-2.0.1 → thth-2.2.0}/README.md +3 -3
- {thth-2.0.1 → thth-2.2.0}/mcp/server.py +16 -4
- {thth-2.0.1 → thth-2.2.0}/tests/fixtures/callback/worker_responses.json +15 -4
- {thth-2.0.1 → thth-2.2.0}/tests/helpers/fake_oauth_server.py +12 -1
- thth-2.2.0/tests/helpers/fake_search_adapter.py +78 -0
- thth-2.2.0/tests/helpers/fake_threads_writes.py +139 -0
- {thth-2.0.1 → thth-2.2.0}/tests/helpers/hold_lock.py +13 -2
- {thth-2.0.1 → thth-2.2.0}/tests/helpers/worker_probe.mjs +1 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_adapter_boundary.py +26 -8
- thth-2.2.0/tests/test_auth_identity_required.py +152 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_board_running.py +65 -3
- thth-2.2.0/tests/test_collect_runs.py +132 -0
- thth-2.2.0/tests/test_control_chars_refused.py +210 -0
- thth-2.2.0/tests/test_count_for.py +158 -0
- thth-2.2.0/tests/test_doctor_all_permissions.py +427 -0
- thth-2.2.0/tests/test_duplicate_front_matter_keys.py +176 -0
- thth-2.2.0/tests/test_inbox_granted_scopes.py +463 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_mastodon_adapter.py +35 -2
- thth-2.2.0/tests/test_mastodon_visibility.py +92 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_mcp_robustness.py +89 -0
- thth-2.2.0/tests/test_scopes_source_requested.py +122 -0
- thth-2.2.0/tests/test_search_engage_targets.py +259 -0
- thth-2.2.0/tests/test_secret_value_redact.py +234 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_send_visibility.py +3 -1
- {thth-2.0.1 → thth-2.2.0}/tests/test_site.py +71 -41
- thth-2.2.0/tests/test_threads_read_permissions.py +598 -0
- thth-2.2.0/tests/test_token_scopes_record.py +156 -0
- thth-2.2.0/tests/test_v21b_approved_writes.py +662 -0
- thth-2.2.0/thth/VERSION +1 -0
- {thth-2.0.1 → thth-2.2.0}/thth/account_report.py +27 -3
- {thth-2.0.1 → thth-2.2.0}/thth/adapters/__init__.py +13 -1
- {thth-2.0.1 → thth-2.2.0}/thth/adapters/base.py +133 -0
- {thth-2.0.1 → thth-2.2.0}/thth/adapters/bluesky.py +11 -2
- {thth-2.0.1 → thth-2.2.0}/thth/adapters/mastodon.py +44 -7
- thth-2.2.0/thth/adapters/threads.py +1240 -0
- {thth-2.0.1 → thth-2.2.0}/thth/approval.py +87 -5
- {thth-2.0.1 → thth-2.2.0}/thth/bundle.py +53 -11
- {thth-2.0.1 → thth-2.2.0}/thth/cli.py +92 -10
- {thth-2.0.1 → thth-2.2.0}/thth/collect.py +163 -11
- {thth-2.0.1 → thth-2.2.0}/thth/core.py +43 -8
- {thth-2.0.1 → thth-2.2.0}/thth/doctor.py +70 -2
- {thth-2.0.1 → thth-2.2.0}/thth/lint.py +67 -1
- {thth-2.0.1 → thth-2.2.0}/thth/oauth.py +86 -3
- {thth-2.0.1 → thth-2.2.0}/thth/queuefile.py +81 -5
- {thth-2.0.1 → thth-2.2.0}/thth/redact.py +46 -0
- {thth-2.0.1 → thth-2.2.0}/thth/report.py +40 -0
- thth-2.2.0/thth/retract_cli.py +350 -0
- {thth-2.0.1 → thth-2.2.0}/thth/runs.py +5 -1
- {thth-2.0.1 → thth-2.2.0}/thth/select.py +29 -2
- {thth-2.0.1 → thth-2.2.0}/thth/sent.py +31 -0
- thth-2.2.0/thth/threads_read_cli.py +581 -0
- {thth-2.0.1 → thth-2.2.0}/thth/writeback.py +42 -0
- thth-2.0.1/thth/VERSION +0 -1
- thth-2.0.1/thth/adapters/threads.py +0 -603
- {thth-2.0.1 → thth-2.2.0}/.gitignore +0 -0
- {thth-2.0.1 → thth-2.2.0}/LICENSE +0 -0
- {thth-2.0.1 → thth-2.2.0}/accounts.example/bluesky.json +0 -0
- {thth-2.0.1 → thth-2.2.0}/accounts.example/mastodon.json +0 -0
- {thth-2.0.1 → thth-2.2.0}/accounts.example/threads.json +0 -0
- {thth-2.0.1 → thth-2.2.0}/bin/thth +0 -0
- {thth-2.0.1 → thth-2.2.0}/bin/thth-remote +0 -0
- {thth-2.0.1 → thth-2.2.0}/bin/thth-run +0 -0
- {thth-2.0.1 → thth-2.2.0}/pyproject.toml +0 -0
- {thth-2.0.1 → thth-2.2.0}/skills/thth/SKILL.md +0 -0
- {thth-2.0.1 → thth-2.2.0}/systemd/thth@.service +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/__init__.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/conftest.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/fixtures/build_injection_eval.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/fixtures/injection_eval.json +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/fixtures/umami-bile.md +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/helpers/__init__.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/helpers/fake_push_adapter.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/helpers/fake_redirect_server.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/helpers/fake_threads_reader.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/helpers/hold_repo_lock.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/helpers/share_ledger.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_account_name_check.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_account_report.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_accounts_dir.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_api_error_body.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_app_env_cli.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_approval.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_approve_cmd.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_ask.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_atlas_fifth_review.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_atlas_fourth_review.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_atlas_review.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_atlas_second_review.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_atlas_third_review.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_audit1_fixes.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_audit2_fixes.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_audit_b_counterexamples.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_audit_b_m1_semantics.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_audit_b_n1_depth.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_audit_b_r1_closure.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_audit_v201.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_audit_v23_details.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_auth.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_bluesky_adapter.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_bluesky_secret_scrub.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_board_needs_review.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_bundle.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_charcount.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_cli_entrypoint.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_collect.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_collect_sent.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_comparability.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_conversation_paging.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_docs_links.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_doctor.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_doctor_next_step.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_editorial_audit.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_editorial_n3_n4.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_external_acceptance.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_fake_api.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_form_spec.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_form_spec_cli.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_forms.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_fresh_install.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_front_matter_injection.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_hypotheses.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_hypothesis_boundaries.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_hypothesis_units.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_impact_cli.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_independent_thread_review.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_independent_topic_review.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_independent_topic_review_r2.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_inflight.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_jst.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_lint.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_list_projections.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_llm_trial.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_lock.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_m2_independent.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_maintain.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_max_per_run.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_mcp.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_measured.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_media_accounts.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_media_next_steps.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_media_roundtrip.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_mismatch_diagnostics.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_n5_reply_null_element.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_n6_replies_incomplete_ledger.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_n7_doctor_error_body.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_no_limits.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_observation_provenance.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_packaging.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_preview.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_production_mode.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_queue.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_quiet_hours.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_r2_r4_r5.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_reason_adoption.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_redact.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_redirect_token_leak.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_refresh.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_refresh_destination.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_rejudge_new.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_rejudge_r2_r3.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_replies.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_replies_refresh.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_repo_lock.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_repo_sync.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_reverse_audit.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_review_case_id_cluster_m2.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_review_case_id_cluster_r2.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_review_cli.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_review_evidence_account.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_review_form_spec_and_case_id.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_review_provenance.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_review_records.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_revoke.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_roundtrip.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_run_missing_env.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_run_record_failclosed.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_runs_collected.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_schedule_multiple.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_schedule_view.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_secrets_grep.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_security_small_fixes.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_select.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_selfupdate.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_send.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_send_cli.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_send_media.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_sent_integrity.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_server_json.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_share.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_share_safety.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_share_sync_concurrent.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_systemd.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_thread_publish.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_threadrun.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_threadshape.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_thth_root.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_token_exists.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_token_set.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_topic.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_topic_advice.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_topic_cli.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_topic_models.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_topic_review_fixes.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_topic_shelf_observers.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_topic_store.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_topics_record_concurrent.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_topics_tool.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_trial_frictions.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_value_domains_shown.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_version.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_writeback_rebase.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/tests/test_writeback_validate.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/__init__.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/__main__.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/account_cli.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/accounts.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/appenv.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/ask.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/ask_cli.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/forms.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/httpsafe.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/inflight.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/jst.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/lock.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/maintain.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/measured.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/postid.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/replies.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/scopes.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/secrets_fs.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/selfupdate.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/share.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/share_cli.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/systemd_gen.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/threadrun.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/threadshape.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/threadthrow.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/topic_advice.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/topic_cli.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/topic_models.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/topic_store.py +0 -0
- {thth-2.0.1 → thth-2.2.0}/thth/topics.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: thth
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.2.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
|
|
@@ -45,7 +45,7 @@ watchtower(`~/Developer/watchtower`)の隣に同じ流儀で並べる。watc
|
|
|
45
45
|
|
|
46
46
|
## いまの状態(2026-09-13)
|
|
47
47
|
|
|
48
|
-
版 **2.0
|
|
48
|
+
版 **2.2.0**。全件テスト **2196 件**(`python -m pytest tests/ -q -n auto -p no:cacheprovider`・1 skip・rc=0)。
|
|
49
49
|
|
|
50
50
|
**媒体**: Threads(稼働)・Bluesky・Mastodon(同席用の台帳あり・未稼働)。
|
|
51
51
|
|
|
@@ -55,7 +55,7 @@ timer(systemd・`thth systemd` で生成)で毎時投稿、返信の採集
|
|
|
55
55
|
|
|
56
56
|
**入口は 4 つ**: 厚い CLI(`bin/thth`・`python -m thth`)、薄い MCP(`mcp/server.py`・読み取りと
|
|
57
57
|
同席の投稿・`before_you_post`)、timer、そして `pip install`(`thth`・`thth-mcp` の entry point・
|
|
58
|
-
**依存 0
|
|
58
|
+
**依存 0**。PyPI に公開済み・MCP registry に `io.github.aokings/thth` として登録済み)。
|
|
59
59
|
|
|
60
60
|
**トピックの棚**(`thth topics`): 観測者ごとに並ぶ・打ち消し `retract-note`・`history`。
|
|
61
61
|
|
|
@@ -87,7 +87,7 @@ timer(systemd・`thth systemd` で生成)で毎時投稿、返信の採集
|
|
|
87
87
|
- **最初の本番投稿の記録**: 2026-09-09、@aoking に疎通確認を 1 本(`17916074118445631`)。
|
|
88
88
|
- **未着手**: X・Facebook ページ・Instagram の各アダプタ。トピック検索の権限(tester には降りない)。泉のサーバ(v2-5)。
|
|
89
89
|
- **権限の制約**: tester に降りる scope は 5 つ。削除はできない。
|
|
90
|
-
- **masaru の手が要るもの**:
|
|
90
|
+
- **masaru の手が要るもの**: repo を public にする切替と `LICENSE`、tag と `release` を進める操作(PyPI と MCP registry は 2.0.1 まで公開済み)。
|
|
91
91
|
|
|
92
92
|
## MCP registry
|
|
93
93
|
|
|
@@ -21,7 +21,7 @@ watchtower(`~/Developer/watchtower`)の隣に同じ流儀で並べる。watc
|
|
|
21
21
|
|
|
22
22
|
## いまの状態(2026-09-13)
|
|
23
23
|
|
|
24
|
-
版 **2.0
|
|
24
|
+
版 **2.2.0**。全件テスト **2196 件**(`python -m pytest tests/ -q -n auto -p no:cacheprovider`・1 skip・rc=0)。
|
|
25
25
|
|
|
26
26
|
**媒体**: Threads(稼働)・Bluesky・Mastodon(同席用の台帳あり・未稼働)。
|
|
27
27
|
|
|
@@ -31,7 +31,7 @@ timer(systemd・`thth systemd` で生成)で毎時投稿、返信の採集
|
|
|
31
31
|
|
|
32
32
|
**入口は 4 つ**: 厚い CLI(`bin/thth`・`python -m thth`)、薄い MCP(`mcp/server.py`・読み取りと
|
|
33
33
|
同席の投稿・`before_you_post`)、timer、そして `pip install`(`thth`・`thth-mcp` の entry point・
|
|
34
|
-
**依存 0
|
|
34
|
+
**依存 0**。PyPI に公開済み・MCP registry に `io.github.aokings/thth` として登録済み)。
|
|
35
35
|
|
|
36
36
|
**トピックの棚**(`thth topics`): 観測者ごとに並ぶ・打ち消し `retract-note`・`history`。
|
|
37
37
|
|
|
@@ -63,7 +63,7 @@ timer(systemd・`thth systemd` で生成)で毎時投稿、返信の採集
|
|
|
63
63
|
- **最初の本番投稿の記録**: 2026-09-09、@aoking に疎通確認を 1 本(`17916074118445631`)。
|
|
64
64
|
- **未着手**: X・Facebook ページ・Instagram の各アダプタ。トピック検索の権限(tester には降りない)。泉のサーバ(v2-5)。
|
|
65
65
|
- **権限の制約**: tester に降りる scope は 5 つ。削除はできない。
|
|
66
|
-
- **masaru の手が要るもの**:
|
|
66
|
+
- **masaru の手が要るもの**: repo を public にする切替と `LICENSE`、tag と `release` を進める操作(PyPI と MCP registry は 2.0.1 まで公開済み)。
|
|
67
67
|
|
|
68
68
|
## MCP registry
|
|
69
69
|
|
|
@@ -198,7 +198,12 @@ def ledger_roots() -> list:
|
|
|
198
198
|
cfg = accounts_mod.load_account(account)
|
|
199
199
|
except Exception: # noqa: BLE001(1 本壊れていても続ける)
|
|
200
200
|
continue
|
|
201
|
-
|
|
201
|
+
# **正規の函数を通す**(監査 2026-09-16)。ここが台帳の `repo_dir` を
|
|
202
|
+
# 生のまま `os.path.realpath()` していたので、相対パスは**この
|
|
203
|
+
# プロセスの cwd** 基準で畳まれていた。`thth/accounts.py::resolved_repo_dir()`
|
|
204
|
+
# は `$THTH_ROOT` 基準——**同じ台帳で許可する場所が MCP だけ食い違って
|
|
205
|
+
# いた**(cwd は systemd 経由なら `/`、手で打てば人それぞれ)。
|
|
206
|
+
repo_dir = accounts_mod.resolved_repo_dir(cfg)
|
|
202
207
|
if repo_dir:
|
|
203
208
|
roots.append(os.path.realpath(repo_dir))
|
|
204
209
|
return roots
|
|
@@ -429,9 +434,16 @@ def main() -> None:
|
|
|
429
434
|
continue
|
|
430
435
|
try:
|
|
431
436
|
req = json.loads(line)
|
|
432
|
-
except
|
|
433
|
-
#
|
|
434
|
-
#
|
|
437
|
+
except (ValueError, RecursionError):
|
|
438
|
+
# **`json.JSONDecodeError` だけでは足りない**(監査 2026-09-16)。
|
|
439
|
+
# `json.JSONDecodeError` は `ValueError` の子だが、Python 3.12 は
|
|
440
|
+
# 4300 桁を超える整数で別の `ValueError`
|
|
441
|
+
# ("Exceeds the limit (4300 digits) for integer string conversion")
|
|
442
|
+
# を上げる——`except json.JSONDecodeError` だけではここが漏れて
|
|
443
|
+
# プロセスが死ぬ。深い入れ子(`[` の連続)の `RecursionError` も同じ
|
|
444
|
+
# 筋で、ここでしか捕まえられない。**id が判らないので返しようがない**
|
|
445
|
+
# (JSON-RPC も id:null を許すが、従来どおり黙って次の行へ・方針は
|
|
446
|
+
# 変えない)。
|
|
435
447
|
continue
|
|
436
448
|
if isinstance(req, list):
|
|
437
449
|
# **batch(配列)は受けない。** 受けたふりをして 1 件目だけ処理する
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"referrer-policy": "no-referrer",
|
|
9
9
|
"x-robots-tag": "noindex, nofollow"
|
|
10
10
|
},
|
|
11
|
-
"body": "<!doctype html>\n<html lang=\"ja\"><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<meta name=\"robots\" content=\"noindex,nofollow,noarchive\">\n<title>THTH 認可の受け口</title>\n<style>\n :root { color-scheme: light dark; }\n body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;\n font: 16px/1.7 -apple-system, BlinkMacSystemFont, \"Hiragino Sans\", \"Noto Sans JP\", sans-serif;\n background:#faf9f7; color:#1a1a1a; padding:24px; }\n @media (prefers-color-scheme: dark) { body { background:#16161a; color:#eee; } }\n main { max-width: 640px; width:100%; }\n h1 { font-size:1.15rem; font-weight:600; margin:0 0 4px; letter-spacing:.02em; }\n .sub { font-size:.85rem; opacity:.65; margin:0 0 24px; }\n .code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:.95rem;\n word-break:break-all; background:rgba(127,127,127,.12); border-radius:10px;\n padding:16px; margin:0 0 12px; user-select:all; }\n button { font:inherit; font-size:.9rem; padding:9px 18px; border-radius:8px; cursor:pointer;\n border:1px solid rgba(127,127,127,.4); background:transparent; color:inherit; }\n button:hover { background:rgba(127,127,127,.12); }\n .note { font-size:.85rem; opacity:.7; margin-top:20px; }\n .err { color:#b3261e; } @media (prefers-color-scheme: dark) { .err { color:#f2b8b5; } }\n</style></head><body><main><h1>THTH</h1>\n <p class=\"sub\">Threads の認可の受け口です。ここを直接開いても何もありません。</p>\n <p class=\"note\">ターミナルで <code>thth auth <account></code> を実行すると、\n 認可 URL
|
|
11
|
+
"body": "<!doctype html>\n<html lang=\"ja\"><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<meta name=\"robots\" content=\"noindex,nofollow,noarchive\">\n<title>THTH 認可の受け口</title>\n<style>\n :root { color-scheme: light dark; }\n body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;\n font: 16px/1.7 -apple-system, BlinkMacSystemFont, \"Hiragino Sans\", \"Noto Sans JP\", sans-serif;\n background:#faf9f7; color:#1a1a1a; padding:24px; }\n @media (prefers-color-scheme: dark) { body { background:#16161a; color:#eee; } }\n main { max-width: 640px; width:100%; }\n h1 { font-size:1.15rem; font-weight:600; margin:0 0 4px; letter-spacing:.02em; }\n .sub { font-size:.85rem; opacity:.65; margin:0 0 24px; }\n .code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:.95rem;\n word-break:break-all; background:rgba(127,127,127,.12); border-radius:10px;\n padding:16px; margin:0 0 12px; user-select:all; }\n button { font:inherit; font-size:.9rem; padding:9px 18px; border-radius:8px; cursor:pointer;\n border:1px solid rgba(127,127,127,.4); background:transparent; color:inherit; }\n button:hover { background:rgba(127,127,127,.12); }\n .note { font-size:.85rem; opacity:.7; margin-top:20px; }\n .err { color:#b3261e; } @media (prefers-color-scheme: dark) { .err { color:#f2b8b5; } }\n</style></head><body><main><h1>THTH</h1>\n <p class=\"sub\">Threads の認可の受け口です。ここを直接開いても何もありません。</p>\n <p class=\"note\">ターミナルで <code>thth auth <account></code> を実行すると、\n 認可 URL が表示されます。承認するとこのページに戻ってくるので、\n 表示された <strong>URL 全体</strong>をターミナルに貼ります。</p></main></body></html>"
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
"url": "https://thth.me/callback",
|
|
@@ -19,7 +19,18 @@
|
|
|
19
19
|
"referrer-policy": "no-referrer",
|
|
20
20
|
"x-robots-tag": "noindex, nofollow"
|
|
21
21
|
},
|
|
22
|
-
"body": "<!doctype html>\n<html lang=\"ja\"><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<meta name=\"robots\" content=\"noindex,nofollow,noarchive\">\n<title>THTH 認可の受け口</title>\n<style>\n :root { color-scheme: light dark; }\n body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;\n font: 16px/1.7 -apple-system, BlinkMacSystemFont, \"Hiragino Sans\", \"Noto Sans JP\", sans-serif;\n background:#faf9f7; color:#1a1a1a; padding:24px; }\n @media (prefers-color-scheme: dark) { body { background:#16161a; color:#eee; } }\n main { max-width: 640px; width:100%; }\n h1 { font-size:1.15rem; font-weight:600; margin:0 0 4px; letter-spacing:.02em; }\n .sub { font-size:.85rem; opacity:.65; margin:0 0 24px; }\n .code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:.95rem;\n word-break:break-all; background:rgba(127,127,127,.12); border-radius:10px;\n padding:16px; margin:0 0 12px; user-select:all; }\n button { font:inherit; font-size:.9rem; padding:9px 18px; border-radius:8px; cursor:pointer;\n border:1px solid rgba(127,127,127,.4); background:transparent; color:inherit; }\n button:hover { background:rgba(127,127,127,.12); }\n .note { font-size:.85rem; opacity:.7; margin-top:20px; }\n .err { color:#b3261e; } @media (prefers-color-scheme: dark) { .err { color:#f2b8b5; } }\n</style></head><body><main><h1>THTH</h1>\n <p class=\"sub\">Threads の認可の受け口です。ここを直接開いても何もありません。</p>\n <p class=\"note\">ターミナルで <code>thth auth <account></code> を実行すると、\n 認可 URL
|
|
22
|
+
"body": "<!doctype html>\n<html lang=\"ja\"><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<meta name=\"robots\" content=\"noindex,nofollow,noarchive\">\n<title>THTH 認可の受け口</title>\n<style>\n :root { color-scheme: light dark; }\n body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;\n font: 16px/1.7 -apple-system, BlinkMacSystemFont, \"Hiragino Sans\", \"Noto Sans JP\", sans-serif;\n background:#faf9f7; color:#1a1a1a; padding:24px; }\n @media (prefers-color-scheme: dark) { body { background:#16161a; color:#eee; } }\n main { max-width: 640px; width:100%; }\n h1 { font-size:1.15rem; font-weight:600; margin:0 0 4px; letter-spacing:.02em; }\n .sub { font-size:.85rem; opacity:.65; margin:0 0 24px; }\n .code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:.95rem;\n word-break:break-all; background:rgba(127,127,127,.12); border-radius:10px;\n padding:16px; margin:0 0 12px; user-select:all; }\n button { font:inherit; font-size:.9rem; padding:9px 18px; border-radius:8px; cursor:pointer;\n border:1px solid rgba(127,127,127,.4); background:transparent; color:inherit; }\n button:hover { background:rgba(127,127,127,.12); }\n .note { font-size:.85rem; opacity:.7; margin-top:20px; }\n .err { color:#b3261e; } @media (prefers-color-scheme: dark) { .err { color:#f2b8b5; } }\n</style></head><body><main><h1>THTH</h1>\n <p class=\"sub\">Threads の認可の受け口です。ここを直接開いても何もありません。</p>\n <p class=\"note\">ターミナルで <code>thth auth <account></code> を実行すると、\n 認可 URL が表示されます。承認するとこのページに戻ってくるので、\n 表示された <strong>URL 全体</strong>をターミナルに貼ります。</p></main></body></html>"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"url": "https://thth.me/callback/?code=FAKE-CODE-abc123&state=FAKE-STATE-xyz",
|
|
26
|
+
"status": 200,
|
|
27
|
+
"headers": {
|
|
28
|
+
"cache-control": "no-store",
|
|
29
|
+
"content-type": "text/html; charset=utf-8",
|
|
30
|
+
"referrer-policy": "no-referrer",
|
|
31
|
+
"x-robots-tag": "noindex, nofollow"
|
|
32
|
+
},
|
|
33
|
+
"body": "<!doctype html>\n<html lang=\"ja\"><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<meta name=\"robots\" content=\"noindex,nofollow,noarchive\">\n<title>THTH 認可の受け口</title>\n<style>\n :root { color-scheme: light dark; }\n body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;\n font: 16px/1.7 -apple-system, BlinkMacSystemFont, \"Hiragino Sans\", \"Noto Sans JP\", sans-serif;\n background:#faf9f7; color:#1a1a1a; padding:24px; }\n @media (prefers-color-scheme: dark) { body { background:#16161a; color:#eee; } }\n main { max-width: 640px; width:100%; }\n h1 { font-size:1.15rem; font-weight:600; margin:0 0 4px; letter-spacing:.02em; }\n .sub { font-size:.85rem; opacity:.65; margin:0 0 24px; }\n .code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:.95rem;\n word-break:break-all; background:rgba(127,127,127,.12); border-radius:10px;\n padding:16px; margin:0 0 12px; user-select:all; }\n button { font:inherit; font-size:.9rem; padding:9px 18px; border-radius:8px; cursor:pointer;\n border:1px solid rgba(127,127,127,.4); background:transparent; color:inherit; }\n button:hover { background:rgba(127,127,127,.12); }\n .note { font-size:.85rem; opacity:.7; margin-top:20px; }\n .err { color:#b3261e; } @media (prefers-color-scheme: dark) { .err { color:#f2b8b5; } }\n</style></head><body><main><h1>認可コードを受け取りました</h1>\n <p class=\"sub\">下の <strong>URL 全体</strong>をターミナルに貼ってください\n (<code>code</code> だけでは足りません)。1 時間で切れる使い捨てです。</p>\n <p class=\"code\" id=\"c\">https://thth.me/callback/?code=FAKE-CODE-abc123&state=FAKE-STATE-xyz</p>\n <button id=\"b\">コピー</button>\n \n <p class=\"note\">この画面は撮らないでください。貼り終えたら閉じて構いません。</p>\n <script>\n // アドレスバーからコードを消す(撮影・肩越しの覗き見への備え)。\n try { history.replaceState(null, \"\", location.pathname); } catch (e) {}\n document.getElementById(\"b\").onclick = async () => {\n try {\n await navigator.clipboard.writeText(document.getElementById(\"c\").textContent);\n document.getElementById(\"b\").textContent = \"コピーしました\";\n } catch (e) { document.getElementById(\"b\").textContent = \"手で選んでコピーしてください\"; }\n };\n </script></main></body></html>"
|
|
23
34
|
},
|
|
24
35
|
{
|
|
25
36
|
"url": "https://thth.me/callback/?code=FAKE-CODE-abc123",
|
|
@@ -30,7 +41,7 @@
|
|
|
30
41
|
"referrer-policy": "no-referrer",
|
|
31
42
|
"x-robots-tag": "noindex, nofollow"
|
|
32
43
|
},
|
|
33
|
-
"body": "<!doctype html>\n<html lang=\"ja\"><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<meta name=\"robots\" content=\"noindex,nofollow,noarchive\">\n<title>THTH 認可の受け口</title>\n<style>\n :root { color-scheme: light dark; }\n body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;\n font: 16px/1.7 -apple-system, BlinkMacSystemFont, \"Hiragino Sans\", \"Noto Sans JP\", sans-serif;\n background:#faf9f7; color:#1a1a1a; padding:24px; }\n @media (prefers-color-scheme: dark) { body { background:#16161a; color:#eee; } }\n main { max-width: 640px; width:100%; }\n h1 { font-size:1.15rem; font-weight:600; margin:0 0 4px; letter-spacing:.02em; }\n .sub { font-size:.85rem; opacity:.65; margin:0 0 24px; }\n .code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:.95rem;\n word-break:break-all; background:rgba(127,127,127,.12); border-radius:10px;\n padding:16px; margin:0 0 12px; user-select:all; }\n button { font:inherit; font-size:.9rem; padding:9px 18px; border-radius:8px; cursor:pointer;\n border:1px solid rgba(127,127,127,.4); background:transparent; color:inherit; }\n button:hover { background:rgba(127,127,127,.12); }\n .note { font-size:.85rem; opacity:.7; margin-top:20px; }\n .err { color:#b3261e; } @media (prefers-color-scheme: dark) { .err { color:#f2b8b5; } }\n</style></head><body><main><h1>認可コードを受け取りました</h1>\n <p class=\"sub\"
|
|
44
|
+
"body": "<!doctype html>\n<html lang=\"ja\"><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<meta name=\"robots\" content=\"noindex,nofollow,noarchive\">\n<title>THTH 認可の受け口</title>\n<style>\n :root { color-scheme: light dark; }\n body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;\n font: 16px/1.7 -apple-system, BlinkMacSystemFont, \"Hiragino Sans\", \"Noto Sans JP\", sans-serif;\n background:#faf9f7; color:#1a1a1a; padding:24px; }\n @media (prefers-color-scheme: dark) { body { background:#16161a; color:#eee; } }\n main { max-width: 640px; width:100%; }\n h1 { font-size:1.15rem; font-weight:600; margin:0 0 4px; letter-spacing:.02em; }\n .sub { font-size:.85rem; opacity:.65; margin:0 0 24px; }\n .code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:.95rem;\n word-break:break-all; background:rgba(127,127,127,.12); border-radius:10px;\n padding:16px; margin:0 0 12px; user-select:all; }\n button { font:inherit; font-size:.9rem; padding:9px 18px; border-radius:8px; cursor:pointer;\n border:1px solid rgba(127,127,127,.4); background:transparent; color:inherit; }\n button:hover { background:rgba(127,127,127,.12); }\n .note { font-size:.85rem; opacity:.7; margin-top:20px; }\n .err { color:#b3261e; } @media (prefers-color-scheme: dark) { .err { color:#f2b8b5; } }\n</style></head><body><main><h1>認可コードを受け取りました</h1>\n <p class=\"sub\">下の <strong>URL 全体</strong>をターミナルに貼ってください\n (<code>code</code> だけでは足りません)。1 時間で切れる使い捨てです。</p>\n <p class=\"code\" id=\"c\">https://thth.me/callback/?code=FAKE-CODE-abc123</p>\n <button id=\"b\">コピー</button>\n <p class=\"note err\">state が付いていません。この戻りは <code>thth auth</code> に\n 受け付けられません(もう一度 <code>thth auth</code> から始めてください)。</p>\n <p class=\"note\">この画面は撮らないでください。貼り終えたら閉じて構いません。</p>\n <script>\n // アドレスバーからコードを消す(撮影・肩越しの覗き見への備え)。\n try { history.replaceState(null, \"\", location.pathname); } catch (e) {}\n document.getElementById(\"b\").onclick = async () => {\n try {\n await navigator.clipboard.writeText(document.getElementById(\"c\").textContent);\n document.getElementById(\"b\").textContent = \"コピーしました\";\n } catch (e) { document.getElementById(\"b\").textContent = \"手で選んでコピーしてください\"; }\n };\n </script></main></body></html>"
|
|
34
45
|
},
|
|
35
46
|
{
|
|
36
47
|
"url": "https://thth.me/callback/?error=access_denied&error_description=The+user+denied+your+request",
|
|
@@ -52,7 +63,7 @@
|
|
|
52
63
|
"referrer-policy": "no-referrer",
|
|
53
64
|
"x-robots-tag": "noindex, nofollow"
|
|
54
65
|
},
|
|
55
|
-
"body": "<!doctype html>\n<html lang=\"ja\"><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<meta name=\"robots\" content=\"noindex,nofollow,noarchive\">\n<title>THTH 認可の受け口</title>\n<style>\n :root { color-scheme: light dark; }\n body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;\n font: 16px/1.7 -apple-system, BlinkMacSystemFont, \"Hiragino Sans\", \"Noto Sans JP\", sans-serif;\n background:#faf9f7; color:#1a1a1a; padding:24px; }\n @media (prefers-color-scheme: dark) { body { background:#16161a; color:#eee; } }\n main { max-width: 640px; width:100%; }\n h1 { font-size:1.15rem; font-weight:600; margin:0 0 4px; letter-spacing:.02em; }\n .sub { font-size:.85rem; opacity:.65; margin:0 0 24px; }\n .code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:.95rem;\n word-break:break-all; background:rgba(127,127,127,.12); border-radius:10px;\n padding:16px; margin:0 0 12px; user-select:all; }\n button { font:inherit; font-size:.9rem; padding:9px 18px; border-radius:8px; cursor:pointer;\n border:1px solid rgba(127,127,127,.4); background:transparent; color:inherit; }\n button:hover { background:rgba(127,127,127,.12); }\n .note { font-size:.85rem; opacity:.7; margin-top:20px; }\n .err { color:#b3261e; } @media (prefers-color-scheme: dark) { .err { color:#f2b8b5; } }\n</style></head><body><main><h1>認可コードを受け取りました</h1>\n <p class=\"sub\"
|
|
66
|
+
"body": "<!doctype html>\n<html lang=\"ja\"><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<meta name=\"robots\" content=\"noindex,nofollow,noarchive\">\n<title>THTH 認可の受け口</title>\n<style>\n :root { color-scheme: light dark; }\n body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;\n font: 16px/1.7 -apple-system, BlinkMacSystemFont, \"Hiragino Sans\", \"Noto Sans JP\", sans-serif;\n background:#faf9f7; color:#1a1a1a; padding:24px; }\n @media (prefers-color-scheme: dark) { body { background:#16161a; color:#eee; } }\n main { max-width: 640px; width:100%; }\n h1 { font-size:1.15rem; font-weight:600; margin:0 0 4px; letter-spacing:.02em; }\n .sub { font-size:.85rem; opacity:.65; margin:0 0 24px; }\n .code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:.95rem;\n word-break:break-all; background:rgba(127,127,127,.12); border-radius:10px;\n padding:16px; margin:0 0 12px; user-select:all; }\n button { font:inherit; font-size:.9rem; padding:9px 18px; border-radius:8px; cursor:pointer;\n border:1px solid rgba(127,127,127,.4); background:transparent; color:inherit; }\n button:hover { background:rgba(127,127,127,.12); }\n .note { font-size:.85rem; opacity:.7; margin-top:20px; }\n .err { color:#b3261e; } @media (prefers-color-scheme: dark) { .err { color:#f2b8b5; } }\n</style></head><body><main><h1>認可コードを受け取りました</h1>\n <p class=\"sub\">下の <strong>URL 全体</strong>をターミナルに貼ってください\n (<code>code</code> だけでは足りません)。1 時間で切れる使い捨てです。</p>\n <p class=\"code\" id=\"c\">https://thth.me/callback/?code=%3Cscript%3E</p>\n <button id=\"b\">コピー</button>\n <p class=\"note err\">state が付いていません。この戻りは <code>thth auth</code> に\n 受け付けられません(もう一度 <code>thth auth</code> から始めてください)。</p>\n <p class=\"note\">この画面は撮らないでください。貼り終えたら閉じて構いません。</p>\n <script>\n // アドレスバーからコードを消す(撮影・肩越しの覗き見への備え)。\n try { history.replaceState(null, \"\", location.pathname); } catch (e) {}\n document.getElementById(\"b\").onclick = async () => {\n try {\n await navigator.clipboard.writeText(document.getElementById(\"c\").textContent);\n document.getElementById(\"b\").textContent = \"コピーしました\";\n } catch (e) { document.getElementById(\"b\").textContent = \"手で選んでコピーしてください\"; }\n };\n </script></main></body></html>"
|
|
56
67
|
},
|
|
57
68
|
{
|
|
58
69
|
"url": "https://thth.me/deauthorize",
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
- "long"(GET /access_token)
|
|
7
7
|
- "me"(GET /v1.0/me)
|
|
8
8
|
- "refresh"(GET /refresh_access_token)
|
|
9
|
-
|
|
9
|
+
- "debug"(GET /v1.0/debug_token・既定は "absent"=404。"ok" で `data.scopes` に
|
|
10
|
+
`debug_scopes` を返す。`thth auth` が scope の記録元を切り替える試験用)
|
|
11
|
+
値は "ok" | "4xx" | "5xx" | "no_token"("debug" だけ "absent" も)。
|
|
10
12
|
"""
|
|
11
13
|
from __future__ import annotations
|
|
12
14
|
|
|
@@ -24,6 +26,7 @@ class _Handler(http.server.BaseHTTPRequestHandler):
|
|
|
24
26
|
refreshed_token = "REFRESHED-SECRET-TOKEN"
|
|
25
27
|
me_payload = {"id": "999999", "username": "nigamilab"}
|
|
26
28
|
expires_in = 5184000
|
|
29
|
+
debug_scopes = ["threads_basic", "threads_content_publish"]
|
|
27
30
|
|
|
28
31
|
def _respond_json(self, status: int, payload: dict) -> None:
|
|
29
32
|
body = json.dumps(payload).encode("utf-8")
|
|
@@ -70,6 +73,14 @@ class _Handler(http.server.BaseHTTPRequestHandler):
|
|
|
70
73
|
self._respond_for(self.behavior.get("refresh", "ok"),
|
|
71
74
|
{"access_token": self.refreshed_token, "token_type": "bearer",
|
|
72
75
|
"expires_in": self.expires_in})
|
|
76
|
+
elif parsed.path == "/v1.0/debug_token":
|
|
77
|
+
mode = self.behavior.get("debug", "absent")
|
|
78
|
+
if mode == "absent":
|
|
79
|
+
self._respond_json(404, {"error": "not found"})
|
|
80
|
+
else:
|
|
81
|
+
self._respond_for(mode, {"data": {"is_valid": True,
|
|
82
|
+
"scopes": list(self.debug_scopes),
|
|
83
|
+
"user_id": "999999"}})
|
|
73
84
|
else:
|
|
74
85
|
self._respond_json(404, {"error": "not found"})
|
|
75
86
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""**偽の検索できる媒体**(設計 v2 §4.4 の受け入れ・**tests の中だけ**)。
|
|
2
|
+
|
|
3
|
+
本物の API を 1 つも叩かずに `thth topics <account> --search <語>` の一覧を試す。
|
|
4
|
+
確かめたいのは 3 つ:
|
|
5
|
+
|
|
6
|
+
- 一覧が **post_id・permalink・投稿ごとの返信・「もう返した」印**を指せる。
|
|
7
|
+
- **返信の数を返さない媒体**(`has_replies` すら無い)で `—` / `null` になる
|
|
8
|
+
(**`n/a` を 0 と混ぜない**)。
|
|
9
|
+
- 本文は画面の先頭 60 字だけで、**ディスクに 1 バイトも落ちない**。
|
|
10
|
+
|
|
11
|
+
`REGISTRY` に 1 行足すだけで通る(境界の証明でもある・§4.2)。
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from thth.adapters import base
|
|
16
|
+
|
|
17
|
+
MEDIUM = "searchfake"
|
|
18
|
+
|
|
19
|
+
# **本文はここにしか無い**——テストはこの綴りがディスクに無いことを走査で確かめる。
|
|
20
|
+
TEXT_A = "偽の検索本文その一・WKPL3A"
|
|
21
|
+
TEXT_B = "偽の検索本文その二・WKPL3B"
|
|
22
|
+
TEXT_LONG = "長" * 80 + "・WKPL3C"
|
|
23
|
+
|
|
24
|
+
# `has_replies` を返す媒体(Threads と同じ形。**数は返らない**)。
|
|
25
|
+
ROWS_HAS_REPLIES = [
|
|
26
|
+
{"message_id": "POST7QXA1", "username": "alice", "text": TEXT_A,
|
|
27
|
+
"timestamp": "2026-09-13T10:00:00+0000", "permalink": "https://fake/POST7QXA1",
|
|
28
|
+
"medium": MEDIUM, "author_key": base.author_key(MEDIUM, "alice"),
|
|
29
|
+
"is_reply": False, "has_replies": True, "topic_tag": "お茶"},
|
|
30
|
+
{"message_id": "POST7QXA2", "username": "bob", "text": TEXT_B,
|
|
31
|
+
"timestamp": "2026-09-14T01:00:00+0000", "permalink": "https://fake/POST7QXA2",
|
|
32
|
+
"medium": MEDIUM, "author_key": base.author_key(MEDIUM, "bob"),
|
|
33
|
+
"is_reply": False, "has_replies": False, "topic_tag": None},
|
|
34
|
+
{"message_id": "POST7QXA3", "username": "carol", "text": TEXT_LONG,
|
|
35
|
+
"timestamp": "2026-09-12T00:00:00+0000", "permalink": "https://fake/POST7QXA3",
|
|
36
|
+
"medium": MEDIUM, "author_key": base.author_key(MEDIUM, "carol"),
|
|
37
|
+
"is_reply": False, "has_replies": True, "topic_tag": "お茶"},
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
# **返信のことを何も返さない媒体**(数も真偽も無い)。`—` / `null` になる。
|
|
41
|
+
ROWS_SILENT = [
|
|
42
|
+
{"message_id": "POST7QXB1", "username": "alice", "text": TEXT_A,
|
|
43
|
+
"timestamp": "2026-09-13T10:00:00+0000", "permalink": "https://fake/POST7QXB1",
|
|
44
|
+
"medium": MEDIUM, "author_key": base.author_key(MEDIUM, "alice")},
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
# **数を返す媒体**(`replies` に整数)。数があればその数を出す。
|
|
48
|
+
ROWS_WITH_COUNT = [
|
|
49
|
+
{"message_id": "POST7QXC1", "username": "alice", "text": TEXT_A,
|
|
50
|
+
"timestamp": "2026-09-13T10:00:00+0000", "permalink": "https://fake/POST7QXC1",
|
|
51
|
+
"medium": MEDIUM, "author_key": base.author_key(MEDIUM, "alice"),
|
|
52
|
+
"replies": 12, "has_replies": True},
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FakeSearchAdapter(base.Adapter):
|
|
57
|
+
"""`keyword_search` を持つ媒体。**投稿はしない**(この試験に要らない)。"""
|
|
58
|
+
|
|
59
|
+
CAPABILITIES = frozenset({"keyword_search"})
|
|
60
|
+
# `.token` を置かずに試せるようにする(在るかどうかだけを見る口なので)。
|
|
61
|
+
TOKEN_KEYS: tuple = ()
|
|
62
|
+
|
|
63
|
+
rows: list = ROWS_HAS_REPLIES
|
|
64
|
+
calls: list = []
|
|
65
|
+
|
|
66
|
+
def __init__(self, *, rows=None):
|
|
67
|
+
self.rows = list(rows if rows is not None else type(self).rows)
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_account(cls, account_cfg: dict, token: dict):
|
|
71
|
+
return cls()
|
|
72
|
+
|
|
73
|
+
def keyword_search(self, q: str, *, search_type: str = "TOP", limit: int = 25) -> list:
|
|
74
|
+
type(self).calls.append((q, search_type, limit))
|
|
75
|
+
return [dict(r) for r in self.rows]
|
|
76
|
+
|
|
77
|
+
def whoami(self) -> dict:
|
|
78
|
+
return {"user_id": "searchfake-1", "username": "searchfake"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""v2.1-B(承認を通る書き込みの口)用の偽 Threads API(`http.server`)。
|
|
2
|
+
|
|
3
|
+
本物の `graph.threads.net` には一切触れない。受けた要求を**種類ごとに数える**
|
|
4
|
+
——特に **DELETE を受けた回数**(`deletes`)が、取り下げのテストの証拠になる。
|
|
5
|
+
|
|
6
|
+
`behavior`:
|
|
7
|
+
- "create" : "ok" | "permission"(403・`error.message` に permission)| "4xx"
|
|
8
|
+
- "publish" : "ok" | "5xx"
|
|
9
|
+
- "delete" : "ok" | "permission" | "4xx" | "no_success"(200 だが success 無し)
|
|
10
|
+
- "location" : "ok" | "permission" | "empty"
|
|
11
|
+
- "recent" : "ok" | "absent"(404。URL を引けない経路の試験用)
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import contextlib
|
|
16
|
+
import http.server
|
|
17
|
+
import json
|
|
18
|
+
import threading
|
|
19
|
+
import urllib.parse
|
|
20
|
+
|
|
21
|
+
PERMISSION_BODY = {"error": {"message": "(#10) Application does not have permission "
|
|
22
|
+
"for this action", "type": "OAuthException",
|
|
23
|
+
"code": 10}}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _Handler(http.server.BaseHTTPRequestHandler):
|
|
27
|
+
behavior: dict = {}
|
|
28
|
+
counter = [1]
|
|
29
|
+
create_params: list
|
|
30
|
+
deletes: list # DELETE を受けたパス(access_token は落として記録)
|
|
31
|
+
locations: list # /location_search に届いた params
|
|
32
|
+
permalink = "https://www.threads.net/@fake/post/ABC"
|
|
33
|
+
|
|
34
|
+
def _respond_json(self, status: int, payload) -> None:
|
|
35
|
+
body = json.dumps(payload).encode("utf-8")
|
|
36
|
+
self.send_response(status)
|
|
37
|
+
self.send_header("Content-Type", "application/json")
|
|
38
|
+
self.send_header("Content-Length", str(len(body)))
|
|
39
|
+
self.end_headers()
|
|
40
|
+
self.wfile.write(body)
|
|
41
|
+
|
|
42
|
+
def do_POST(self) -> None: # noqa: N802
|
|
43
|
+
length = int(self.headers.get("Content-Length") or 0)
|
|
44
|
+
raw = self.rfile.read(length) if length else b""
|
|
45
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
46
|
+
if parsed.path.endswith("/threads_publish"):
|
|
47
|
+
mode = self.behavior.get("publish", "ok")
|
|
48
|
+
if mode == "ok":
|
|
49
|
+
_id = str(self.counter[0])
|
|
50
|
+
self.counter[0] += 1
|
|
51
|
+
self._respond_json(200, {"id": _id})
|
|
52
|
+
else:
|
|
53
|
+
self._respond_json(500, {"error": {"message": "server error (fake)"}})
|
|
54
|
+
return
|
|
55
|
+
if parsed.path.endswith("/threads"):
|
|
56
|
+
params = {k: v[0] for k, v in urllib.parse.parse_qs(raw.decode("utf-8")).items()}
|
|
57
|
+
params.pop("access_token", None)
|
|
58
|
+
self.__class__.create_params.append(params)
|
|
59
|
+
mode = self.behavior.get("create", "ok")
|
|
60
|
+
if mode == "ok":
|
|
61
|
+
_id = str(self.counter[0])
|
|
62
|
+
self.counter[0] += 1
|
|
63
|
+
self._respond_json(200, {"id": _id})
|
|
64
|
+
elif mode == "permission":
|
|
65
|
+
self._respond_json(403, PERMISSION_BODY)
|
|
66
|
+
else:
|
|
67
|
+
self._respond_json(400, {"error": {"message": "bad request (fake)"}})
|
|
68
|
+
return
|
|
69
|
+
self._respond_json(404, {"error": {"message": "not found"}})
|
|
70
|
+
|
|
71
|
+
def do_DELETE(self) -> None: # noqa: N802
|
|
72
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
73
|
+
self.__class__.deletes.append(parsed.path)
|
|
74
|
+
mode = self.behavior.get("delete", "ok")
|
|
75
|
+
media_id = parsed.path.rsplit("/", 1)[-1]
|
|
76
|
+
if mode == "ok":
|
|
77
|
+
self._respond_json(200, {"success": True, "deleted_id": media_id})
|
|
78
|
+
elif mode == "permission":
|
|
79
|
+
self._respond_json(403, PERMISSION_BODY)
|
|
80
|
+
elif mode == "no_success":
|
|
81
|
+
self._respond_json(200, {"deleted_id": media_id})
|
|
82
|
+
else:
|
|
83
|
+
self._respond_json(400, {"error": {"message": "bad request (fake)"}})
|
|
84
|
+
|
|
85
|
+
def do_GET(self) -> None: # noqa: N802
|
|
86
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
87
|
+
params = {k: v[0] for k, v in urllib.parse.parse_qs(parsed.query).items()}
|
|
88
|
+
params.pop("access_token", None)
|
|
89
|
+
if parsed.path == "/v1.0/location_search":
|
|
90
|
+
self.__class__.locations.append(params)
|
|
91
|
+
mode = self.behavior.get("location", "ok")
|
|
92
|
+
if mode == "permission":
|
|
93
|
+
self._respond_json(403, PERMISSION_BODY)
|
|
94
|
+
elif mode == "empty":
|
|
95
|
+
self._respond_json(200, {"data": []})
|
|
96
|
+
else:
|
|
97
|
+
self._respond_json(200, {"data": [
|
|
98
|
+
{"id": "101", "name": "渋谷駅", "city": "渋谷区", "country": "JP"},
|
|
99
|
+
{"id": "102", "name": "渋谷駅前", "city": "渋谷区", "country": "JP"},
|
|
100
|
+
{"id": "103", "name": "渋谷ヒカリエ", "city": "渋谷区", "country": "JP"},
|
|
101
|
+
{"id": "104", "name": "渋谷スクランブル", "city": "渋谷区", "country": "JP"},
|
|
102
|
+
{"id": "105", "name": "渋谷パルコ", "city": "渋谷区", "country": "JP"},
|
|
103
|
+
{"id": "106", "name": "渋谷 6 件目(切り詰め確認)", "city": "渋谷区",
|
|
104
|
+
"country": "JP"},
|
|
105
|
+
]})
|
|
106
|
+
return
|
|
107
|
+
if parsed.path.endswith("/threads"):
|
|
108
|
+
if self.behavior.get("recent", "ok") == "absent":
|
|
109
|
+
self._respond_json(404, {"error": {"message": "not found"}})
|
|
110
|
+
else:
|
|
111
|
+
self._respond_json(200, {"data": [
|
|
112
|
+
{"id": "777", "permalink": self.permalink,
|
|
113
|
+
"timestamp": "2026-09-14T10:00:00+0000", "text": "出した本文"}]})
|
|
114
|
+
return
|
|
115
|
+
self._respond_json(404, {"error": {"message": "not found"}})
|
|
116
|
+
|
|
117
|
+
def log_message(self, format, *args): # noqa: A002
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class _ServerURL(str):
|
|
122
|
+
handler_cls: type
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@contextlib.contextmanager
|
|
126
|
+
def fake_threads_writes_server(behavior: dict | None = None):
|
|
127
|
+
handler_cls = type("Handler", (_Handler,),
|
|
128
|
+
{"behavior": dict(behavior or {}), "counter": [1],
|
|
129
|
+
"create_params": [], "deletes": [], "locations": []})
|
|
130
|
+
server = http.server.HTTPServer(("127.0.0.1", 0), handler_cls)
|
|
131
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
132
|
+
thread.start()
|
|
133
|
+
try:
|
|
134
|
+
url = _ServerURL(f"http://127.0.0.1:{server.server_port}")
|
|
135
|
+
url.handler_cls = handler_cls
|
|
136
|
+
yield url
|
|
137
|
+
finally:
|
|
138
|
+
server.shutdown()
|
|
139
|
+
thread.join(timeout=5)
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
"""テスト専用: `thth.core` が使うのと同じロックファイルを確保して N 秒保持する。
|
|
3
3
|
`test_20260909_direct_throw_bypassed_lock`(受け入れ 9)で、別プロセスが
|
|
4
4
|
`thth throw` を直に叩いてもロックを踏むことを確認するための「保持者」役。
|
|
5
|
+
|
|
6
|
+
hold_lock.py <account> <秒> … account のロック(`thth run` が握る側)
|
|
7
|
+
hold_lock.py <account> <秒> repo … **repo のロック**(`thth collect` が握る側)
|
|
8
|
+
|
|
9
|
+
repo の側は `thth board` の「いま collect が走っています」を確かめるのに使う
|
|
10
|
+
(引継ぎ 2026-09-15 §3-D)。
|
|
5
11
|
"""
|
|
6
12
|
import os
|
|
7
13
|
import sys
|
|
@@ -16,8 +22,13 @@ from thth import lock as lock_mod # noqa: E402
|
|
|
16
22
|
def main() -> None:
|
|
17
23
|
account_name = sys.argv[1]
|
|
18
24
|
seconds = float(sys.argv[2])
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
which = sys.argv[3] if len(sys.argv) > 3 else "account"
|
|
26
|
+
if which == "repo":
|
|
27
|
+
repo_dir = accounts_mod.load_account(account_name)["repo_dir"]
|
|
28
|
+
lock_path = accounts_mod.repo_lock_path_for(repo_dir)
|
|
29
|
+
else:
|
|
30
|
+
state_dir = accounts_mod.state_dir_for(account_name)
|
|
31
|
+
lock_path = os.path.join(state_dir, "lock")
|
|
21
32
|
account_lock = lock_mod.AccountLock(lock_path)
|
|
22
33
|
account_lock.acquire()
|
|
23
34
|
sys.stdout.write("locked\n")
|
|
@@ -19,6 +19,7 @@ if (!modulePath) {
|
|
|
19
19
|
const URLS = [
|
|
20
20
|
"https://thth.me/callback/",
|
|
21
21
|
"https://thth.me/callback",
|
|
22
|
+
"https://thth.me/callback/?code=FAKE-CODE-abc123&state=FAKE-STATE-xyz",
|
|
22
23
|
"https://thth.me/callback/?code=FAKE-CODE-abc123",
|
|
23
24
|
"https://thth.me/callback/?error=access_denied&error_description=The+user+denied+your+request",
|
|
24
25
|
"https://thth.me/callback/?code=%3Cscript%3E&x=1",
|
|
@@ -92,9 +92,12 @@ def test_TB0_capabilitiesは実体を作らずに引ける():
|
|
|
92
92
|
# 呼ばない(以前は毎回 `errors` に積んでいた・T0 の残件)。
|
|
93
93
|
# `recent_posts` は F2 の配線(2026-09-13)で足した語。**3 媒体とも持つ**が、
|
|
94
94
|
# 宛先は媒体が決める(`account_report.fetch_posts()` はこの語で塞ぐだけ)。
|
|
95
|
+
# `keyword_search`・`mentions`・`profile_lookup`・`inbox` は v2.1-A(設計 v2
|
|
96
|
+
# §4.3・2026-09-14)で足した語。Threads の 11 権限を使う読み取りの口 3 つと、
|
|
97
|
+
# 言及を `inbox` に流す配管(v2-3 の芽がそのまま受け皿)。
|
|
95
98
|
assert threads_mod.ThreadsAdapter.capabilities() == {
|
|
96
99
|
"topic", "link_preview", "views", "quota", "refresh", "recent_posts",
|
|
97
|
-
"account_insights"}
|
|
100
|
+
"account_insights", "keyword_search", "mentions", "profile_lookup", "inbox"}
|
|
98
101
|
|
|
99
102
|
|
|
100
103
|
def test_F3_metrics_ofは新しい形だけを受ける():
|
|
@@ -303,10 +306,17 @@ def test_TB1_二度走らせても増えない(tmp_path, isolated_account_factor
|
|
|
303
306
|
|
|
304
307
|
|
|
305
308
|
def test_TB1_inboxを持たない媒体には聞かない(tmp_path, isolated_account_factory):
|
|
306
|
-
"""
|
|
307
|
-
|
|
309
|
+
"""`inbox` を持たない媒体には呼ばない(`capabilities()` に無い)。
|
|
310
|
+
|
|
311
|
+
**Bluesky で見る**(2026-09-14・v2.1-A)。以前は Threads で見ていたが、Threads は
|
|
312
|
+
言及(`threads_manage_mentions`)を `inbox` に流すようになった(設計 v2 §4.3)。
|
|
313
|
+
Threads のままだと `inbox()` が呼ばれて `AssertionError` が `collect` の
|
|
314
|
+
`errors` に飲まれ、**呼ばれているのに通る**——見たい性質が見えなくなる。
|
|
315
|
+
"""
|
|
316
|
+
from thth.adapters import bluesky as bluesky_mod
|
|
317
|
+
account = isolated_account_factory(media="bluesky")
|
|
308
318
|
|
|
309
|
-
class _呼ばれたら失敗(
|
|
319
|
+
class _呼ばれたら失敗(bluesky_mod.BlueskyAdapter):
|
|
310
320
|
def __init__(self):
|
|
311
321
|
pass
|
|
312
322
|
|
|
@@ -316,7 +326,9 @@ def test_TB1_inboxを持たない媒体には聞かない(tmp_path, isolated_acc
|
|
|
316
326
|
def account_insights(self, user_id, *, since, until):
|
|
317
327
|
return {}
|
|
318
328
|
|
|
319
|
-
|
|
329
|
+
assert "inbox" not in _呼ばれたら失敗.capabilities()
|
|
330
|
+
result = collect_mod.collect_once(account["name"], adapter=_呼ばれたら失敗())
|
|
331
|
+
assert not any(e.startswith("inbox") for e in result["errors"]), result["errors"]
|
|
320
332
|
assert not os.path.isdir(os.path.join(account["repo_dir"], "data", "sns", "inbox"))
|
|
321
333
|
|
|
322
334
|
|
|
@@ -589,7 +601,11 @@ def test_TB5_threadsの出力は現行と同じ(tmp_path, monkeypatch, isolated_
|
|
|
589
601
|
monkeypatch.setattr(doctor_mod, "_get", fake_get)
|
|
590
602
|
report = doctor_mod.diagnose(account["name"])
|
|
591
603
|
|
|
592
|
-
|
|
604
|
+
# **probe を移す前からある 6 行は、口も並びもそのまま**。2026-09-14 に残り
|
|
605
|
+
# 7 権限の行を**この後ろに**足した(`tests/test_doctor_all_permissions.py`)
|
|
606
|
+
# ので、ここは「先頭 6 つが現行と同じ」を固定する(増えた分は許す)。
|
|
607
|
+
既存 = 6
|
|
608
|
+
assert 叩いた[:既存] == [
|
|
593
609
|
"/v1.0/me",
|
|
594
610
|
"/v1.0/999999/threads",
|
|
595
611
|
"/v1.0/999999/threads_publishing_limit",
|
|
@@ -597,7 +613,7 @@ def test_TB5_threadsの出力は現行と同じ(tmp_path, monkeypatch, isolated_
|
|
|
597
613
|
"/v1.0/999999/threads_insights",
|
|
598
614
|
"/v1.0/POST123/conversation",
|
|
599
615
|
], 叩いた
|
|
600
|
-
assert [p["permission"] for p in report["probes"]] == [
|
|
616
|
+
assert [p["permission"] for p in report["probes"]][:既存] == [
|
|
601
617
|
"threads_basic", "threads_basic", "threads_content_publish",
|
|
602
618
|
"threads_manage_insights", "threads_manage_insights", "threads_read_replies",
|
|
603
619
|
]
|
|
@@ -605,7 +621,9 @@ def test_TB5_threadsの出力は現行と同じ(tmp_path, monkeypatch, isolated_
|
|
|
605
621
|
# 人向けの表示(`run_doctor`)が使う鍵。**足すのはよいが、消さない。**
|
|
606
622
|
assert {"label", "permission", "key", "ok", "detail"} <= set(p)
|
|
607
623
|
assert "body" not in p, "**本文を返り値に残さない**(値が漏れる)"
|
|
608
|
-
|
|
624
|
+
# 読み取りの口が無い権限(`ok=None`)が後ろに並ぶので、**叩いた行は全部○**。
|
|
625
|
+
assert all(p["ok"] is True for p in report["probes"][:既存])
|
|
626
|
+
assert all(p["ok"] is True for p in report["probes"] if p["ok"] is not None)
|
|
609
627
|
|
|
610
628
|
|
|
611
629
|
def test_TB5_投稿が無ければ返信のprobeは判定不能のまま(tmp_path, monkeypatch,
|