thth 2.13.0__tar.gz → 2.13.2__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.13.0 → thth-2.13.2}/PKG-INFO +1 -1
- thth-2.13.2/tests/test_v2131_send_dry_run.py +47 -0
- thth-2.13.2/tests/test_v2131_threads_publish_wait.py +84 -0
- thth-2.13.2/tests/test_v2132_send_inflight_hint.py +67 -0
- thth-2.13.2/tests/test_v2132_threads_create_timeout.py +84 -0
- thth-2.13.2/tests/test_v2132_threads_create_unknown.py +90 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_bluesky_webm_named.py +2 -1
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_media.py +18 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_video.py +31 -2
- thth-2.13.2/thth/VERSION +1 -0
- {thth-2.13.0 → thth-2.13.2}/thth/adapters/threads_media.py +97 -11
- {thth-2.13.0 → thth-2.13.2}/thth/cli.py +32 -4
- {thth-2.13.0 → thth-2.13.2}/thth/core.py +23 -1
- {thth-2.13.0 → thth-2.13.2}/thth/inflight.py +10 -0
- {thth-2.13.0 → thth-2.13.2}/thth/media.py +2 -2
- {thth-2.13.0 → thth-2.13.2}/thth/media_delivery.py +66 -5
- {thth-2.13.0 → thth-2.13.2}/thth/mediaformats.py +19 -10
- thth-2.13.0/thth/VERSION +0 -1
- {thth-2.13.0 → thth-2.13.2}/.gitignore +0 -0
- {thth-2.13.0 → thth-2.13.2}/LICENSE +0 -0
- {thth-2.13.0 → thth-2.13.2}/README.md +0 -0
- {thth-2.13.0 → thth-2.13.2}/accounts.example/bluesky.json +0 -0
- {thth-2.13.0 → thth-2.13.2}/accounts.example/mastodon.json +0 -0
- {thth-2.13.0 → thth-2.13.2}/accounts.example/threads.json +0 -0
- {thth-2.13.0 → thth-2.13.2}/accounts.example/x.json +0 -0
- {thth-2.13.0 → thth-2.13.2}/bin/thth +0 -0
- {thth-2.13.0 → thth-2.13.2}/bin/thth-remote +0 -0
- {thth-2.13.0 → thth-2.13.2}/bin/thth-run +0 -0
- {thth-2.13.0 → thth-2.13.2}/mcp/server.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/pyproject.toml +0 -0
- {thth-2.13.0 → thth-2.13.2}/skills/thth/SKILL.md +0 -0
- {thth-2.13.0 → thth-2.13.2}/skills/thth-admin/SKILL.md +0 -0
- {thth-2.13.0 → thth-2.13.2}/systemd/thth@.service +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/__init__.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/conftest.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/coordination_snapshot.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/fixtures/GraphemeBreakTest-17.0.0.txt +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/fixtures/build_injection_eval.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/fixtures/callback/worker_responses.json +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/fixtures/injection_eval.json +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/fixtures/umami-bile.md +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/helpers/__init__.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/helpers/fake_oauth_server.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/helpers/fake_push_adapter.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/helpers/fake_redirect_server.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/helpers/fake_search_adapter.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/helpers/fake_threads_reader.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/helpers/fake_threads_writes.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/helpers/hold_lock.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/helpers/hold_repo_lock.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/helpers/worker_probe.mjs +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_27_acceptance_fixes.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_account_name_check.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_account_report.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_accounts_dir.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_adapter_boundary.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_admin_http_mcp.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_admin_log.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_admin_notifications.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_admin_repairs.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_admin_report.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_after_cli.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_analytics_comparison.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_analytics_report.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_api_diagnostic.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_api_error_body.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_app_env_cli.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_approval.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_approve_cmd.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_ask.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_atlas_fifth_review.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_atlas_fourth_review.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_atlas_review.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_atlas_second_review.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_atlas_third_review.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_audit1_fixes.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_audit2_fixes.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_audit_b_counterexamples.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_audit_b_m1_semantics.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_audit_b_n1_depth.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_audit_b_r1_closure.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_audit_v201.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_audit_v23_details.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_auth.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_auth_identity_required.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_behind_remote.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_bluesky_adapter.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_bluesky_secret_scrub.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_board_needs_review.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_board_running.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_bundle.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_charcount.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_cli_entrypoint.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_cli_help.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_cli_vm_path.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_collect.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_collect_engagements.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_collect_runs.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_collect_sent.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_comparability.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_control_chars_refused.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_conversation_paging.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_count_for.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_docs_links.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_doctor.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_doctor_all_permissions.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_doctor_next_step.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_duplicate_front_matter_keys.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_editorial_audit.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_editorial_n3_n4.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_engagements.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_external_acceptance.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_fake_api.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_form_spec.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_form_spec_cli.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_forms.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_fresh_install.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_front_matter_injection.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_healthcheck_notifications.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_hypotheses.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_hypothesis_boundaries.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_hypothesis_units.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_impact_cli.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_inbox_granted_scopes.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_incident_notifications.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_independent_thread_review.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_independent_topic_review.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_independent_topic_review_r2.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_inflight.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_inflight_tag_body_hash.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_jst.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_lint.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_list_projections.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_llm_trial.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_llm_trial_engage.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_lock.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_m2_independent.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_maintain.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_mastodon_adapter.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_mastodon_visibility.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_max_per_run.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_mcp.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_mcp_robustness.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_measured.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_media_accounts.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_media_next_steps.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_media_roundtrip.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_mismatch_diagnostics.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_n5_reply_null_element.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_n6_replies_incomplete_ledger.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_n7_doctor_error_body.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_no_limits.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_observation_provenance.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_operations_handoff.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_packaging.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_preview.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_production_mode.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_pull_cli.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_queue.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_quiet_hours.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_r2_r4_r5.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_read_cli_bare_runtimeerror.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_reason_adoption.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_redact.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_redirect_token_leak.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_refresh.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_refresh_destination.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_rejudge_new.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_rejudge_r2_r3.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_replies.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_replies_refresh.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_repo_lock.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_repo_sync.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_report_checkout_import.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_report_http.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_report_isolation.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_report_service.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_reverse_audit.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_review_case_id_cluster_m2.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_review_case_id_cluster_r2.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_review_cli.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_review_evidence_account.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_review_form_spec_and_case_id.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_review_provenance.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_review_records.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_revoke.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_roundtrip.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_run_missing_env.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_run_record_failclosed.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_runs_collected.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_runs_read_rows.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_schedule_multiple.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_schedule_view.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_scopes_source_requested.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_search_engage_targets.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_search_media_routes.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_secret_value_redact.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_secrets_grep.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_security_small_fixes.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_select.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_selfupdate.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_send.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_send_cli.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_send_control_chars.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_send_media.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_send_visibility.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_sent_integrity.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_server_json.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_site.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_skill_doc.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_study_report.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_systemd.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_thread_publish.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_thread_read.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_threadrun.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_threads_read_permissions.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_threadshape.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_thth_root.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_token_exists.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_token_scopes_record.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_token_set.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_topic.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_topic_advice.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_topic_cli.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_topic_models.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_topic_review_fixes.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_topic_shelf_observers.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_topic_store.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_topics_record_concurrent.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_topics_tool.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_trial_frictions.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v210_admin_transaction.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v210_approve_account.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v210_json_contract.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v210_length.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v210_lock_wait.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v210_mastodon_scopes.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v210_mentions.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v210_post_urls.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v210_repo_paths.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v210_reposts.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v210_study_add.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v210_tool_version.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v210_unanswered.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v210_unanswered_scope.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v210_where_arguments.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v210_where_filters.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v210_who_arguments.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v211_auth_integration.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v211_authflow.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v211_bluesky_stdin.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v211_doctor_binding.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v211_mastodon_auth.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v211_x_auth.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v212_approval_relay.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v212_ask_coordination.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v212_budget.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v212_budget_auth.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v212_budget_mcp.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v212_deletion.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v212_job_completion_fault.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v212_leave.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v212_leave_gate.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v212_leave_providers.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v212_leave_token_safety.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v212_mastodon_guide.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v212_mcp_compat.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v212_missing_token.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v212_read_coordination.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v212_server_writes.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v212_token_parent.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v212_write_storage.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_acceptance_fixes.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_adts_asf.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_approval_attachments.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_attachment_kinds.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_bluesky_external.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_bluesky_media.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_bluesky_metadata.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_bluesky_metadata_uri.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_bluesky_metadata_video.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_bluesky_record.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_bluesky_record_video.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_bluesky_video.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_bmff_typed_metadata.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_budget_lock.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_capabilities.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_carryover_timeout.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_graphemes.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_auth_media.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_bmff_audio.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_flac.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_gif.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_link.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_mp3.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_ogg_opus.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_ogg_vorbis.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_poll.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_primary_size_boundary.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_quote.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_thumbnail.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_wav.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_webm_audio.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_mastodon_webm_video.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_media_cleanup.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_media_formats.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_media_foundation.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_media_prepared.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_media_relay.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_media_relay_refusals.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_media_upload.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_receipt_deadline.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_review_followups.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_stage810_audit.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_static_diagnostics.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_stop_observation.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_systemd_worker.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_ghost.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_gif.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_link.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_media.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_media_spoiler.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_poll.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_quote.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_quote_media.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_replies.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_text.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_threads_text_spoiler.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_transport_review.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_video_typed_metadata.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_vorbis_comment_boundary.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v213_webm_schema_boundary.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v21b_approved_writes.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v28_tags.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v31_analysis.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_v31_collection.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_value_domains_shown.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_version.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_where_cli.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_who_cli.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_writeback_rebase.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_writeback_validate.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/tests/test_x_metrics.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/__init__.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/__main__.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/_grapheme_data.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/account_cli.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/account_report.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/accounts.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/adapters/__init__.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/adapters/auth_mastodon.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/adapters/auth_threads.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/adapters/auth_x.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/adapters/base.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/adapters/bluesky.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/adapters/bluesky_media.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/adapters/bluesky_video.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/adapters/mastodon.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/adapters/mastodon_media.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/adapters/threads.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/admin_log.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/admin_notifications.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/admin_report.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/after_cli.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/analytics_comparison.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/analytics_report.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/analytics_shapes.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/analytics_threads.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/api_diagnostic.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/appconfig.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/appenv.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/approval.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/approval_jobs.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/approval_relay.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/ask.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/ask_cli.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/audioformats.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/authclients.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/authflow.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/authpassword.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/bluesky_metadata.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/budget_x.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/bundle.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/collect.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/collection_context.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/collection_status.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/deletion.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/doctor.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/ebmlaudio.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/ebmlvideo.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/engagements.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/flacformats.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/forms.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/graphemes.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/handoff_cursor.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/healthcheck.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/httpsafe.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/incident.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/jst.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/leave.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/leave_gate.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/lint.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/lock.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/maintain.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/managed_repo.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/measured.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/media_capabilities.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/media_cleanup.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/media_relay.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/media_uploads.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/mpeg_audio.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/oauth.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/oggformats.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/operations_handoff.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/postid.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/queuefile.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/read_coordination.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/read_window.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/redact.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/replies.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/report.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/report_details.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/report_http.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/report_isolation.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/report_service.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/retract_cli.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/runs.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/scopes.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/secrets_fs.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/select.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/selfupdate.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/sent.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/server_files.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/server_writes.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/stop_observation.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/study_cli.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/study_report.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/systemd_gen.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/tags.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/thread_read.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/threadrun.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/threads_read_cli.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/threadshape.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/threadthrow.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/tool_version.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/topic_advice.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/topic_cli.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/topic_models.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/topic_store.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/topics.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/unanswered.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/vorbisformats.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/where_cli.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/who_cli.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/writeback.py +0 -0
- {thth-2.13.0 → thth-2.13.2}/thth/x_metrics.py +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""2.13.1 実機直し: dry-run は理由を言い、publish が断るものをここで断る。"""
|
|
2
|
+
import re
|
|
3
|
+
import pytest
|
|
4
|
+
from thth import cli, core, read_coordination
|
|
5
|
+
from tests.test_v213_media_foundation import jpeg
|
|
6
|
+
from tests.test_v213_media_formats import box, mp4
|
|
7
|
+
from tests.test_v213_threads_media import env, wire
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def send(env, monkeypatch):
|
|
12
|
+
monkeypatch.setattr(core, '_append_run', lambda *a, **k: None)
|
|
13
|
+
monkeypatch.setattr(read_coordination, 'invoke',
|
|
14
|
+
lambda args, name, fn=None: fn() if fn else args.func(args))
|
|
15
|
+
return lambda name, alt='説明': cli.main(['send', 'alpha', '--media', name, '--alt', alt])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def sized_jpeg(total):
|
|
19
|
+
"""Pad the entropy-coded data: the public bytes are exactly `total` long."""
|
|
20
|
+
base = jpeg()
|
|
21
|
+
return base[:-3] + b'\x3f' * (1 + total - len(base)) + b'\xff\xd9'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_dry_run_states_the_media_reason_and_a_next_step(env, send, wire, capsys):
|
|
25
|
+
# fragment(moof)=本当に秒数の無い構造。ffmpeg の edit list とは違う。
|
|
26
|
+
(env[2] / 'v.mp4').write_bytes(mp4() + box(b'moof', b''))
|
|
27
|
+
assert send('v.mp4', '動画') == 2
|
|
28
|
+
shown = capsys.readouterr()
|
|
29
|
+
output = shown.out + shown.err
|
|
30
|
+
# 実機では `mode: rehearsal` の 1 行だけで 2 で終わっていた。
|
|
31
|
+
assert output.splitlines()[0] == 'mode: rehearsal'
|
|
32
|
+
assert 'duration_unverifiable' in output and '+faststart' in output
|
|
33
|
+
# 理由も次の一歩も静的: パスは 1 つも映らない。
|
|
34
|
+
assert str(env[2]) not in output and 'v.mp4' not in output
|
|
35
|
+
assert re.search(r'(^|[\s"\'(])/[A-Za-z]', output, re.M) is None
|
|
36
|
+
assert not wire['calls'] and not env[3]['upload']
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.parametrize('total,refused', [(8_000_000, False), (8_000_001, True)])
|
|
40
|
+
def test_threads_byte_cap_is_applied_in_the_dry_run(env, send, wire, capsys, total, refused):
|
|
41
|
+
(env[2] / 'big.jpg').write_bytes(sized_jpeg(total))
|
|
42
|
+
assert send('big.jpg') == (2 if refused else 0)
|
|
43
|
+
shown = capsys.readouterr()
|
|
44
|
+
output = shown.out + shown.err
|
|
45
|
+
assert ('media_limit_exceeded: bytes' in output) is refused
|
|
46
|
+
assert ('digest: ' in output) is (not refused)
|
|
47
|
+
assert not wire['calls'] and not env[3]['upload']
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""2.13.1 実機直し: FINISHED の直後に出さない。4xx は Graph の番号だけ名指す。"""
|
|
2
|
+
import json
|
|
3
|
+
import pytest
|
|
4
|
+
from thth.adapters import threads, threads_media as tm
|
|
5
|
+
from tests.test_v213_threads_media import env, wire, invoke, posts
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def clock(monkeypatch):
|
|
10
|
+
"""凍った時計。`time.sleep` だけが進める——実機の揺れを試験に入れない。"""
|
|
11
|
+
state = {'now': 0.0, 'slept': []}
|
|
12
|
+
monkeypatch.setattr(tm.time, 'monotonic', lambda: state['now'])
|
|
13
|
+
def sleep(seconds):
|
|
14
|
+
state['slept'].append(seconds)
|
|
15
|
+
state['now'] += seconds
|
|
16
|
+
monkeypatch.setattr(tm.time, 'sleep', sleep)
|
|
17
|
+
return state
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def timeline(monkeypatch, clock):
|
|
21
|
+
seen = []
|
|
22
|
+
original = tm._json
|
|
23
|
+
def record(adapter, method, path, params, **kwargs):
|
|
24
|
+
seen.append((method, path, clock['now']))
|
|
25
|
+
return original(adapter, method, path, params, **kwargs)
|
|
26
|
+
monkeypatch.setattr(tm, '_json', record)
|
|
27
|
+
return seen
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_publish_waits_the_same_wait_seconds_after_finished(env, wire, monkeypatch, clock):
|
|
31
|
+
env[1].wait_seconds = 30.0
|
|
32
|
+
seen = timeline(monkeypatch, clock)
|
|
33
|
+
result, journal, _ = invoke(env)
|
|
34
|
+
assert result.post_id == '100' and journal['media']['phase'] == 'published'
|
|
35
|
+
# FINISHED を読んだのは 0 秒。公開は 30 秒待ったあと 1 回だけ。
|
|
36
|
+
assert [row[2] for row in seen if row[0] == 'GET'] == [0.0]
|
|
37
|
+
assert [row[2] for row in seen if row[1].endswith('/threads_publish')] == [30.0]
|
|
38
|
+
assert clock['slept'] == [30.0]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_the_wait_never_outlives_the_grant(env, wire, clock):
|
|
42
|
+
# 画像の付与は 600 秒。1800 秒待てば URL が死ぬので、待たずに出す。
|
|
43
|
+
env[1].wait_seconds = 1800.0
|
|
44
|
+
result, _, _ = invoke(env)
|
|
45
|
+
assert result.post_id == '100' and clock['slept'] == []
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_the_veto_after_the_wait_is_honoured(env, wire, clock):
|
|
49
|
+
env[1].wait_seconds = 30.0
|
|
50
|
+
asked = []
|
|
51
|
+
def veto():
|
|
52
|
+
asked.append(clock['now'])
|
|
53
|
+
return 'stopped_after_wait' if clock['now'] >= 30.0 else None
|
|
54
|
+
result, journal, _ = invoke(env, veto=veto)
|
|
55
|
+
assert max(asked) >= 30.0 and clock['slept'] == [30.0]
|
|
56
|
+
assert result.error == 'stopped_after_wait' and result.failure == 'media_held'
|
|
57
|
+
assert not posts(wire, '/threads_publish')
|
|
58
|
+
assert journal['media']['phase'] == 'held' and journal['media']['reason'] == 'stopped_after_wait'
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_publish_4xx_names_the_graph_numbers_and_nothing_else(env, wire):
|
|
62
|
+
wire['publish'] = (400, {'error': {'message': 'Media ID is not available',
|
|
63
|
+
'type': 'OAuthException', 'code': 100,
|
|
64
|
+
'error_subcode': 2207032, 'fbtrace_id': 'AsEcReT'}})
|
|
65
|
+
result, journal, _ = invoke(env)
|
|
66
|
+
assert result.post_id is None and result.failure == 'media_held'
|
|
67
|
+
assert result.error == 'media_publishing_http_400_code_100_subcode_2207032'
|
|
68
|
+
assert journal['media']['phase'] == 'held' and journal['media']['reason'] == result.error
|
|
69
|
+
blob = json.dumps(journal, ensure_ascii=False) + str(result.error)
|
|
70
|
+
for secret in ('Media ID', 'OAuthException', 'AsEcReT'):
|
|
71
|
+
assert secret not in blob
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.parametrize('body', [{}, {'error': {'message': 'nope'}}, {'error': 'nope'}])
|
|
75
|
+
def test_a_4xx_without_numbers_keeps_the_plain_reason(env, wire, body):
|
|
76
|
+
wire['publish'] = (400, body)
|
|
77
|
+
result, _, _ = invoke(env)
|
|
78
|
+
assert result.error == 'media_publishing_http_400'
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_the_env_override_reaches_the_attachment_path(monkeypatch):
|
|
82
|
+
monkeypatch.setenv('THTH_THREADS_WAIT_SECONDS', '7')
|
|
83
|
+
adapter = threads.ThreadsAdapter.from_account({'account': 'alpha', 'media': 'threads'}, {})
|
|
84
|
+
assert adapter.wait_seconds == 7.0
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""2.13.2 実機直し: inflight が残っているなら、dry-run でも断って次の一歩を言う。
|
|
2
|
+
|
|
3
|
+
作成の時間切れ(`media_creating_timeout`)は inflight を `unknown` のまま
|
|
4
|
+
残す。そのあと `thth send` を打った人が最初に見るのがここ——2.13.1 までは
|
|
5
|
+
`--production` なしだと digest まで出して 0 で終わり、残っていることを一言も
|
|
6
|
+
言わなかった。`--production` を付ければ断るが、理由 1 行だけで「どうすれば
|
|
7
|
+
いいか」は無かった。
|
|
8
|
+
"""
|
|
9
|
+
import pytest
|
|
10
|
+
from thth import accounts, cli, core, inflight, media, media_delivery, read_coordination
|
|
11
|
+
from tests.test_v213_threads_media import env, wire
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def send(env, monkeypatch):
|
|
16
|
+
monkeypatch.setattr(core, '_append_run', lambda *a, **k: None)
|
|
17
|
+
monkeypatch.setattr(read_coordination, 'invoke',
|
|
18
|
+
lambda args, name, fn=None: fn() if fn else args.func(args))
|
|
19
|
+
return lambda *extra: cli.main(['send', 'alpha', '--media', 'a.png', '--alt', '説明', *extra])
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def seed_unknown(env, reason='media_creating_timeout'):
|
|
23
|
+
"""作成の時間切れが残す journal と同じ形(phase `unknown`・remote_ids 空)。"""
|
|
24
|
+
state = accounts.state_dir_for('alpha')
|
|
25
|
+
manifest = media.manifest_for({'media': [{'file': 'a.png', 'alt': '説明'}]}, env[0])
|
|
26
|
+
inflight.write(state, file='(send)', started='2030-01-01T00:00:00+09:00')
|
|
27
|
+
media_delivery._progress('alpha', manifest, 'https://graph.invalid', 'unknown',
|
|
28
|
+
remote_ids=[], reason=reason)
|
|
29
|
+
return state
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.parametrize('extra', [(), ('--production',)])
|
|
33
|
+
def test_send_refuses_and_says_what_to_do(env, send, wire, capsys, extra):
|
|
34
|
+
seed_unknown(env)
|
|
35
|
+
assert send(*extra) == 1
|
|
36
|
+
shown = capsys.readouterr()
|
|
37
|
+
output = shown.out + shown.err
|
|
38
|
+
assert 'inflight が残っています' in output
|
|
39
|
+
assert inflight.NEXT_STEP in output
|
|
40
|
+
# 断ったのだから digest も本文の下見も出ない。媒体には一度も触らない。
|
|
41
|
+
assert 'digest: ' not in output and not wire['calls'] and not env[3]['upload']
|
|
42
|
+
# 次の一歩は 1 行だけ。
|
|
43
|
+
assert len([line for line in output.splitlines() if line.startswith('次の一歩')]) == 1
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_the_refusal_stays_static(env, send, capsys):
|
|
47
|
+
seed_unknown(env)
|
|
48
|
+
assert send() == 1
|
|
49
|
+
shown = capsys.readouterr()
|
|
50
|
+
output = shown.out + shown.err
|
|
51
|
+
# repo の実パスもアカウントの state パスも映さない(既存の静的の約束)。
|
|
52
|
+
assert str(env[2]) not in output and str(accounts.state_dir_for('alpha')) not in output
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_the_inflight_is_left_exactly_as_it_was(env, send):
|
|
56
|
+
state = seed_unknown(env)
|
|
57
|
+
before = inflight.read(state)
|
|
58
|
+
assert send() == 1 and send('--production') == 1
|
|
59
|
+
assert inflight.read(state) == before
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_without_an_inflight_the_dry_run_still_shows_the_digest(env, send, capsys):
|
|
63
|
+
assert send() == 0
|
|
64
|
+
shown = capsys.readouterr()
|
|
65
|
+
output = shown.out + shown.err
|
|
66
|
+
assert 'digest: ' in output and 'inflight が残っています' not in output
|
|
67
|
+
assert inflight.NEXT_STEP not in output
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""2.13.2 実機直し: container の作成は Meta の取り込みを待つ(10 秒では足りない)。
|
|
2
|
+
|
|
3
|
+
実機 2026-09-22(masaru-threads・`--media docs/media/photo.jpg`): `POST
|
|
4
|
+
/{user}/threads` に `image_url` を渡すと、Meta はその POST を開いたまま
|
|
5
|
+
thth.me/R2 から画像を取りに行く。2.13.0 の同じ添付は通ったのに 2.13.1 は
|
|
6
|
+
`media_creating_failed`(phase `unknown`・`remote_ids=[]`)で落ちた——違いは
|
|
7
|
+
Meta の取り込みにかかった時間で、こちらの 10 秒(`adapter.timeout`)が短い。
|
|
8
|
+
"""
|
|
9
|
+
import time
|
|
10
|
+
import pytest
|
|
11
|
+
from thth.adapters import threads_media as tm
|
|
12
|
+
from tests.test_v213_threads_media import env, wire, invoke, posts
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def clock(monkeypatch):
|
|
17
|
+
"""凍った時計。`time.sleep` と偽 Graph の遅れだけが進める。"""
|
|
18
|
+
state = {'now': 0.0}
|
|
19
|
+
monkeypatch.setattr(tm.time, 'monotonic', lambda: state['now'])
|
|
20
|
+
monkeypatch.setattr(tm.time, 'sleep', lambda seconds: state.update(now=state['now'] + seconds))
|
|
21
|
+
return state
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def sockets(monkeypatch):
|
|
25
|
+
"""各 HTTP 呼び出しに渡った socket の締切(方式・パス・秒)を並べる。"""
|
|
26
|
+
seen = []
|
|
27
|
+
original = tm._transport.open
|
|
28
|
+
def open_request(request, timeout=None, **kwargs):
|
|
29
|
+
seen.append((request.get_method(), request.full_url.split('?')[0], timeout))
|
|
30
|
+
return original(request, timeout=timeout, **kwargs)
|
|
31
|
+
monkeypatch.setattr(tm._transport, 'open', open_request)
|
|
32
|
+
return seen
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_a_creation_slower_than_the_adapter_timeout_still_lands(env, wire):
|
|
36
|
+
# 実機の再現: 作成だけが `adapter.timeout` より長くかかる。
|
|
37
|
+
env[1].timeout = 0.3
|
|
38
|
+
wire['on_create'] = lambda: time.sleep(1.0)
|
|
39
|
+
result, journal, _ = invoke(env)
|
|
40
|
+
assert result.post_id == '100' and journal['media']['phase'] == 'published'
|
|
41
|
+
assert len(posts(wire, '/threads_publish')) == 1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_only_the_creation_gets_the_long_budget(env, wire, monkeypatch):
|
|
45
|
+
seen = sockets(monkeypatch)
|
|
46
|
+
assert invoke(env)[0].post_id == '100'
|
|
47
|
+
# 作成は 90 秒。状態の問い合わせも公開も adapter の 10 秒のまま。
|
|
48
|
+
assert [t for m, u, t in seen if m == 'POST' and u.endswith('/threads')] == [tm.CREATE_TIMEOUT_SECONDS]
|
|
49
|
+
assert [t for m, u, t in seen if m == 'GET'] == [env[1].timeout]
|
|
50
|
+
assert [t for m, u, t in seen if u.endswith('/threads_publish')] == [env[1].timeout]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.mark.parametrize('raw,seconds', [
|
|
54
|
+
(None, 90.0), ('120', 120.0), ('90.5', 90.5),
|
|
55
|
+
('10', 10.0), ('9', 10.0), ('0', 10.0), ('-5', 10.0),
|
|
56
|
+
('600', 600.0), ('601', 600.0), ('86400', 600.0),
|
|
57
|
+
('', 90.0), ('abc', 90.0), ('inf', 90.0), ('nan', 90.0),
|
|
58
|
+
])
|
|
59
|
+
def test_the_override_is_read_and_clamped(monkeypatch, raw, seconds):
|
|
60
|
+
monkeypatch.delenv('THTH_THREADS_CREATE_TIMEOUT_SECONDS', raising=False)
|
|
61
|
+
if raw is not None:
|
|
62
|
+
monkeypatch.setenv('THTH_THREADS_CREATE_TIMEOUT_SECONDS', raw)
|
|
63
|
+
assert tm.create_timeout() == seconds
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_the_override_reaches_the_creation_socket(env, wire, monkeypatch):
|
|
67
|
+
monkeypatch.setenv('THTH_THREADS_CREATE_TIMEOUT_SECONDS', '900')
|
|
68
|
+
seen = sockets(monkeypatch)
|
|
69
|
+
assert invoke(env)[0].post_id == '100'
|
|
70
|
+
assert [t for m, u, t in seen if m == 'POST' and u.endswith('/threads')] == [600.0]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_the_poll_deadline_counts_the_time_the_creation_took(env, wire, monkeypatch, clock):
|
|
74
|
+
# 画像の持ち時間は 120 秒。作成に 40 秒使ったら、問い合わせに残るのは 80 秒。
|
|
75
|
+
monkeypatch.setattr(tm, 'POLL_INTERVAL', 10.0)
|
|
76
|
+
wire['poll'] = [(200, {'status': 'IN_PROGRESS'})] * 60
|
|
77
|
+
wire['on_create'] = lambda: clock.update(now=clock['now'] + 40.0)
|
|
78
|
+
result, journal, _ = invoke(env)
|
|
79
|
+
assert result.error == 'media_processing_timeout' and result.failure == 'media_held'
|
|
80
|
+
# 作成を数えなければ 160 秒・12 回になる。
|
|
81
|
+
assert clock['now'] == tm.POLL_SECONDS
|
|
82
|
+
assert len([c for c in wire['calls'] if c[0] == 'GET']) == 8
|
|
83
|
+
assert not posts(wire, '/threads_publish') and env[3]['results'] == [False]
|
|
84
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""2.13.2 実機直し: 作成の時間切れは「出ていないこと」ではない。
|
|
2
|
+
|
|
3
|
+
Meta は `POST /{user}/threads` を開いたまま媒体を取りに行くので、こちらが
|
|
4
|
+
socket を諦めたあとに向こうで container ができていることがある。phase は
|
|
5
|
+
`unknown` のまま——そのうえで理由を `media_creating_timeout` と名指し、
|
|
6
|
+
`thth send` が「確かめてから消す」と言えるようにする。
|
|
7
|
+
"""
|
|
8
|
+
import threading
|
|
9
|
+
import urllib.error
|
|
10
|
+
import pytest
|
|
11
|
+
from thth import media_delivery
|
|
12
|
+
from thth.adapters import threads_media as tm
|
|
13
|
+
from tests.test_v213_media_foundation import png
|
|
14
|
+
from tests.test_v213_threads_media import env, wire, invoke, posts
|
|
15
|
+
|
|
16
|
+
STEP = '次の一歩: 媒体が画像/動画を取り込むのに時間がかかっています。しばらくして inflight を確認し、投稿が無ければ消してから再実行'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def stall():
|
|
21
|
+
"""偽 Graph を黙らせる掛け金。試験が終わったら必ず外す(daemon を残さない)。"""
|
|
22
|
+
gate = threading.Event()
|
|
23
|
+
try:
|
|
24
|
+
yield lambda: gate.wait(10)
|
|
25
|
+
finally:
|
|
26
|
+
gate.set()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.mark.parametrize('error,timed_out', [
|
|
30
|
+
(TimeoutError('slow'), True),
|
|
31
|
+
(urllib.error.URLError(TimeoutError('slow')), True),
|
|
32
|
+
(urllib.error.URLError(ConnectionResetError('reset')), False),
|
|
33
|
+
(OSError('broken'), False),
|
|
34
|
+
(urllib.error.HTTPError('http://x.invalid', 408, 'Request Timeout', None, None), False),
|
|
35
|
+
])
|
|
36
|
+
def test_only_a_socket_timeout_counts_as_one(error, timed_out):
|
|
37
|
+
assert tm.socket_timed_out(error) is timed_out
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_a_creation_that_never_answers_is_unknown_and_named_as_a_timeout(env, wire, monkeypatch, stall):
|
|
41
|
+
monkeypatch.setattr(tm, 'create_timeout', lambda: 0.3)
|
|
42
|
+
wire['on_create'] = stall
|
|
43
|
+
result, journal, _ = invoke(env)
|
|
44
|
+
assert result.post_id is None and result.failure == 'media_ambiguous'
|
|
45
|
+
assert result.error == 'media_creating_timeout'
|
|
46
|
+
assert journal['media']['phase'] == 'unknown' and journal['media']['reason'] == 'media_creating_timeout'
|
|
47
|
+
# container ができたかどうかは分からない: 覚えている id は無く、公開もしない。
|
|
48
|
+
assert journal['media']['remote_ids'] == [] and not posts(wire, '/threads_publish')
|
|
49
|
+
# 付与も引き上げない(引き上げれば向こうにできた container が確実に死ぬ)。
|
|
50
|
+
assert env[3]['results'] == []
|
|
51
|
+
assert media_delivery.next_step(result.error) == STEP
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_the_carousel_parent_timeout_is_named_the_same_way(env, wire, monkeypatch, stall):
|
|
55
|
+
(env[2] / 'b.png').write_bytes(png())
|
|
56
|
+
monkeypatch.setattr(tm, 'create_timeout', lambda: 0.3)
|
|
57
|
+
created = []
|
|
58
|
+
def on_create():
|
|
59
|
+
created.append(1)
|
|
60
|
+
if len(created) == 3:
|
|
61
|
+
stall()
|
|
62
|
+
wire['on_create'] = on_create
|
|
63
|
+
fm = {'media': [{'file': 'a.png', 'alt': '点'}, {'file': 'b.png', 'alt': '別の点'}]}
|
|
64
|
+
result, journal, _ = invoke(env, fm)
|
|
65
|
+
assert result.error == 'media_creating_carousel_timeout' and result.failure == 'media_ambiguous'
|
|
66
|
+
assert journal['media']['phase'] == 'unknown' and journal['media']['remote_ids'] == ['11', '12']
|
|
67
|
+
assert not posts(wire, '/threads_publish') and env[3]['results'] == []
|
|
68
|
+
assert media_delivery.next_step(result.error) == STEP
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_other_creation_failures_keep_the_generic_reason(env, wire, monkeypatch):
|
|
72
|
+
original = tm._transport.open
|
|
73
|
+
def open_request(request, timeout=None, **kwargs):
|
|
74
|
+
if request.get_method() == 'POST' and request.full_url.endswith('/threads'):
|
|
75
|
+
raise urllib.error.URLError(ConnectionResetError('reset'))
|
|
76
|
+
return original(request, timeout=timeout, **kwargs)
|
|
77
|
+
monkeypatch.setattr(tm._transport, 'open', open_request)
|
|
78
|
+
result, journal, _ = invoke(env)
|
|
79
|
+
assert result.error == 'media_creating_failed' and result.failure == 'media_ambiguous'
|
|
80
|
+
assert journal['media']['phase'] == 'unknown'
|
|
81
|
+
assert media_delivery.next_step(result.error) is None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_a_slow_poll_is_not_a_creation_timeout(env, wire, monkeypatch, stall):
|
|
85
|
+
# 時間切れを名指すのは作成だけ。問い合わせの時間切れは既存の扱いのまま。
|
|
86
|
+
env[1].timeout = 0.3
|
|
87
|
+
wire['on_poll'] = stall
|
|
88
|
+
result, journal, _ = invoke(env)
|
|
89
|
+
assert result.error == 'media_processing_failed' and result.failure == 'media_held'
|
|
90
|
+
assert journal['media']['phase'] == 'held' and journal['media']['remote_ids'] == ['11']
|
|
@@ -41,5 +41,6 @@ def test_mastodon_still_accepts_the_same_webm(tmp_path):
|
|
|
41
41
|
def test_policy_table_names_every_medium():
|
|
42
42
|
assert set(media.MEDIUM_POLICY) == {'threads', 'bluesky', 'mastodon'}
|
|
43
43
|
assert media.MEDIUM_POLICY['mastodon']['allow_webm'] is True
|
|
44
|
-
|
|
44
|
+
# An edit list is no longer a per-medium policy: every medium takes one.
|
|
45
|
+
assert all(set(p) <= {'allow_webm'} for p in media.MEDIUM_POLICY.values())
|
|
45
46
|
assert all(p.get('allow_webm') is False for m, p in media.MEDIUM_POLICY.items() if m != 'mastodon')
|
|
@@ -463,6 +463,21 @@ def test_lint_instance_failure_never_uses_permissive_cache(env,wire,monkeypatch)
|
|
|
463
463
|
def unavailable(*a,**k):raise OSError('synthetic connectivity failure')
|
|
464
464
|
monkeypatch.setattr(mm,'_json',unavailable)
|
|
465
465
|
assert media_delivery.lint_notes(cfg,{'media':[{'file':'a.png','alt':'点'}]})==['media_capability_unavailable']
|
|
466
|
+
# 予行は upload ではない。届かない instance は断る根拠にならず、lint と
|
|
467
|
+
# 同じ capability_unobserved を警告として言う(上限は upload 直前に再観測)。
|
|
468
|
+
warned=media_delivery.lint_notes(cfg,{'media':[{'file':'a.png','alt':'点'}]},unreachable_warns=True)
|
|
469
|
+
# 保存済みの上限があっても、この run では何も観測していないと言う。
|
|
470
|
+
assert warned==['warning: '+media_delivery.CAPABILITY_UNOBSERVED]
|
|
471
|
+
assert mm.cached(cfg) is not None
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def test_rehearsal_still_refuses_limits_the_instance_did_answer(env,wire,monkeypatch):
|
|
475
|
+
cfg,adapter,repo=env
|
|
476
|
+
wire['caps']['configuration']['media_attachments']['image_size_limit']=1
|
|
477
|
+
fm={'media':[{'file':'a.png','alt':'点'}]}
|
|
478
|
+
assert media_delivery.lint_notes(cfg,fm,unreachable_warns=True)==['media_limit_exceeded: bytes']
|
|
479
|
+
del wire['caps']['configuration']['media_attachments']['image_size_limit']
|
|
480
|
+
assert media_delivery.lint_notes(cfg,fm,unreachable_warns=True)==['media_capability_unavailable: image_size_limit']
|
|
466
481
|
|
|
467
482
|
|
|
468
483
|
@pytest.mark.parametrize('boundary', ['sleep', 'response_equal', 'response_late', 'legal'])
|
|
@@ -512,6 +527,9 @@ def test_cli_source_only_stale_is_loud_and_has_no_provider(env,wire,monkeypatch,
|
|
|
512
527
|
argv=['send','alpha','--media','a.png','--alt','点']
|
|
513
528
|
assert cli.main(argv)==0;shown=capsys.readouterr().out
|
|
514
529
|
digest=next(line.split(': ',1)[1] for line in shown.splitlines() if line.startswith('digest: '))
|
|
530
|
+
# 予行が読むのは公開の instance 上限だけ——資格情報も POST も無い。
|
|
531
|
+
assert all(call[0]=='GET' and call[1]=='/api/v2/instance' and 'Authorization' not in call[3] for call in wire['calls'])
|
|
532
|
+
wire['calls'].clear()
|
|
515
533
|
raw=(repo/'a.png').read_bytes();data=b'Note\0private fixture only';kind=b'tEXt'
|
|
516
534
|
changed=raw[:-12]+struct.pack('>I',len(data))+kind+data+struct.pack('>I',zlib.crc32(kind+data))+raw[-12:]
|
|
517
535
|
assert mediaformats.png(raw).public_bytes==mediaformats.png(changed).public_bytes
|
|
@@ -22,15 +22,44 @@ def prepared(tmp_path,raw,medium='threads'):
|
|
|
22
22
|
return media.prepare(tmp_path,{'media':[{'file':'v.mp4','alt':'動画'}]},medium)
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
def
|
|
25
|
+
def test_valid_edit_list_and_late_moov_warn_only(tmp_path):
|
|
26
26
|
raw=edit_movie(front=False)
|
|
27
27
|
with prepared(tmp_path,raw) as(m,items):
|
|
28
28
|
notes=tm.notes(m,items)
|
|
29
29
|
assert b''.join(items[0].chunks())==raw
|
|
30
30
|
assert any('edit lists' in n for n in notes) and any('moov follows' in n for n in notes)
|
|
31
31
|
assert any('frame rate unobserved' in n for n in notes)
|
|
32
|
+
# The same file on the other media: a warning there too, never a refusal.
|
|
33
|
+
with prepared(tmp_path,raw,'mastodon') as(m,_):
|
|
34
|
+
assert m['files'][0]['metadata_notes']==['edit_list_present']
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def movie_seconds(ticks,scale=1000,*,edit=True,handler=b'vide'):
|
|
38
|
+
"""ffmpeg shape: +faststart moov before mdat and an edts/elst under trak."""
|
|
39
|
+
edts=box(b'edts',box(b'elst',b'\0'*4+struct.pack('>I',1)+struct.pack('>IiHH',ticks,0,1,0))) if edit else b''
|
|
40
|
+
mvhd=b'\0'*12+struct.pack('>II',scale,ticks)+b'\0'*80
|
|
41
|
+
tkhd=b'\0'*76+struct.pack('>II',720<<16,1280<<16)
|
|
42
|
+
hdlr=b'\0'*8+handler+b'\0'*12
|
|
43
|
+
return (box(b'ftyp',b'isom\0\0\0\0isom')
|
|
44
|
+
+box(b'moov',box(b'mvhd',mvhd)+box(b'trak',box(b'tkhd',tkhd)+edts+box(b'mdia',box(b'hdlr',hdlr))))
|
|
45
|
+
+box(b'mdat',b'synthetic-sample'))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.mark.parametrize('medium',['threads','bluesky','mastodon'])
|
|
49
|
+
def test_ffmpeg_edit_list_is_a_note_on_every_medium(tmp_path,medium):
|
|
50
|
+
raw=movie_seconds(5000)
|
|
51
|
+
with prepared(tmp_path,raw,medium) as(m,items):
|
|
52
|
+
row=m['files'][0]
|
|
53
|
+
assert (row['format'],row['kind'],row['width'],row['height'])==('mp4','video',720,1280)
|
|
54
|
+
assert row['duration']==5.0 and row['metadata_notes']==['edit_list_present']
|
|
55
|
+
assert b''.join(items[0].chunks())==raw
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.parametrize('medium',['threads','bluesky','mastodon'])
|
|
59
|
+
@pytest.mark.parametrize('ticks,scale',[(0,1000),(5000,0)])
|
|
60
|
+
def test_movie_header_without_seconds_stays_unverifiable(tmp_path,medium,ticks,scale):
|
|
32
61
|
with pytest.raises(media.MediaError,match='duration_unverifiable'):
|
|
33
|
-
with prepared(tmp_path,
|
|
62
|
+
with prepared(tmp_path,movie_seconds(ticks,scale),medium):pass
|
|
34
63
|
|
|
35
64
|
|
|
36
65
|
@pytest.mark.parametrize('raw,reason',[(edit_movie(version=2),'invalid_attachment_structure'),(edit_movie(count=2),'invalid_attachment_structure'),(edit_movie(extra=box(b'uuid',b'private')),'location_metadata_unverifiable'),(edit_movie(extra=box(b'free',b'private')),'location_metadata_unverifiable'),(edit_movie(extra=box(b'ZZZZ',b'')),'location_metadata_unverifiable'),(edit_movie(extra=box(b'\xa9xyz',b'private')),'location_metadata_present')])
|
thth-2.13.2/thth/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.13.2
|