syncade 0.7.0__tar.gz → 0.7.1__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.
- {syncade-0.7.0/src/syncade.egg-info → syncade-0.7.1}/PKG-INFO +1 -1
- {syncade-0.7.0 → syncade-0.7.1}/pyproject.toml +1 -1
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/__init__.py +1 -1
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/update_mode.py +114 -1
- {syncade-0.7.0 → syncade-0.7.1/src/syncade.egg-info}/PKG-INFO +1 -1
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade.egg-info/SOURCES.txt +1 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_update_mode.py +1 -1
- syncade-0.7.1/tests/cli/test_update_post_condition.py +398 -0
- {syncade-0.7.0 → syncade-0.7.1}/LICENSE +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/MANIFEST.in +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/README.md +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/docs/config-reference.md +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/docs/how-to-use.md +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/setup.cfg +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/__main__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/anthropic.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/base.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/fake.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/fake_common.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/fake_producer_audit_draft.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/fake_reviewer_synth.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/openai.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/openai_parsing.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/producer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/producer_anthropic.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/producer_openai.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/registry.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/auth_check.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/auth_preflight.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/base_resolution.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/billing.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/checks_config.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/auth_gate.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/config_keys.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/config_list.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/config_menu_rows.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/config_mode.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/config_overrides.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/config_tui.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/doctor_mode.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/gc_mode.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/install_skill.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/metrics_mode.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/modes.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/parser.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/parser_types.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/paths.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/preflight_paths.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/resolve.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/resume_mode.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/skill_status.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/toml_writer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/update_notice.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/validate.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/config.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/config_auth.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/config_cold.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/config_gc.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/config_loader.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/config_loop.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/config_producer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/config_retry.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/config_types.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/config_update.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/diff_filter.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/dispatcher.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/doctor.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/doctor_env.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/doctor_preview.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/doctor_types.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/exit_codes.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/findings.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/findings_json.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/gc.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/gc_execute.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/gc_protection.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/gc_types.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/gc_worktrees.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/git_object_id.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/git_preconditions.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/logging.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/metrics/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/metrics/aggregate.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/metrics/findings.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/metrics/schema.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/_runs_dir.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/branch_advance.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/branch_guard.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/budget.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/escalation_coverage.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/loop.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/loop_dispatch_check.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/loop_finalize.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/loop_preflight.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/loop_resume.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/loop_rmtree.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/loop_round_step.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/prior_round.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/producer_phase.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/results.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/resume.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/resume_load.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/resume_plan.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/resume_target.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/resume_types.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/reviewer_template_failure.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/round.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/round_checks.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/round_no_changes.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/round_predispatch.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/verdict.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/_atomic.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/_clusters.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/_findings_verdict.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/_markdown.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/_validation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/checks.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/decision_needed.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/findings_md.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/handoff.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/handoff_classify.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/last_reviewed.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/loop_manifest.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/loop_summary.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/loop_summary_text.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/producer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/reviewer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/round_manifest.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/run_init.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/run_summary.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/run_summary_next_steps.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/synth.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/test_run.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/presets.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/pricing_config.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/process.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/producer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/producer_attempt.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/producer_escalation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/producer_git.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/producer_result.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/prompts.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/prompts_loader.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/retry.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/run_inputs.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/run_status.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/selfcheck.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/skills/claude/README.md +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/skills/claude/SKILL.md +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/skills/codex/README.md +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/skills/codex/SKILL.md +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/snapshot.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/spec_audit.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/spec_audit_schema.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/spec_draft.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/spec_source.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesis.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesis_clusters.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesis_repair.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesis_schema.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesizer/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesizer/constants.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesizer/driver.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesizer/rendering.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesizer/result.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesizer/validation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesizer/workspace.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/templates/presets/balanced.toml +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/templates/presets/cheap.toml +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/templates/presets/thorough.toml +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/templates/producer.md +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/templates/reviewer.md +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/templates/reviewer_adversarial.md +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/templates/reviewer_codex.md +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/templates/spec_audit.md +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/templates/spec_draft.md +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/templates/synthesizer.md +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/test_runner.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/test_runner_classify.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/transcript.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/update_check.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/usage.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/worktree.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/worktree_env.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade/worktree_paths.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade.egg-info/dependency_links.txt +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade.egg-info/entry_points.txt +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade.egg-info/requires.txt +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/src/syncade.egg-info/top_level.txt +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/adapters/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/adapters/_anthropic_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/adapters/_openai_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/adapters/test_anthropic.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/adapters/test_anthropic_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/adapters/test_openai.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/adapters/test_openai_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/adapters/test_producer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/adapters/test_producer_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/adapters/test_prompt_transport.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/auth_check/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/auth_check/test_probes.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/auth_check/test_run_auth_check.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/_undo_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_basic_and_mutex.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_cli_library_divergence.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_config_keys.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_config_list.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_config_menu_rows.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_config_mode.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_config_overrides.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_config_tui.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_config_tui_pty.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_documented_exit_codes.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_documented_flags_exist.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_flags.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_gc.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_help_retention_contract.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_mode_exclusivity_matrix.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_mode_flag_guards.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_resume_reports_hard_kill.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_run_and_config.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_run_status_cli.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_scope_openspec_draft.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_toml_writer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_undo_auto_init.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_undo_scope.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_update_notice.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_validate_before_mutate.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/config/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_auth_enforcement.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_auth_modes.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_auth_preflight.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_auth_report_scope.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_budget_config.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_cold_actors.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_config_checks.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_config_reference_drift.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_config_schema.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_gc_config.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_global_config_layer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_presets.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_retry_config.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_reviewer_config.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_reviewer_permissions.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_worktree_base_config.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/conftest.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/dispatcher/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/dispatcher/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/dispatcher/test_dispatch_core.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/dispatcher/test_dispatch_misc.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/dispatcher/test_dispatch_usage.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/doctor/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/doctor/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/doctor/conftest.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/doctor/test_doctor.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/doctor/test_doctor_diff_size.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/doctor/test_doctor_malformed_diff.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/doctor/test_doctor_preview.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/findings/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/findings/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/findings/test_parser_and_fields.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/findings/test_parser_resilience.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/findings/test_verdict_block_selection.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/gc/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/gc/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_autoprune.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_execute.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_plan.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_plan_execution_races.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_reaping_scope.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_retention_tiers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_robustness.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_self_orphan_protection.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_two_tier_retention.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_worktree_age_bound.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/logging/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/logging/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/logging/test_logging.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/logging/test_logging_summary.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/conftest.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_actor_spend.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_aggregate.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_billing_reconciliation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_billing_surfaces_agree.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_budget_metrics.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_consensus_report.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_cost_honesty.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_findings_capture.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_metrics_cli.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_metrics_mode.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_schema.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_unknown_panel.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/_resume_fixtures.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/conftest.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_budget.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_budget_resume_e2e.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_budget_stop_message.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_budget_warning.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_checks_and_recording.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_contracts.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_cross_round.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_cross_round_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_default_branch_guard.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_diff_size_cap.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_dispatch_record.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_empty_diff.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_empty_diff_no_side_effects.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_escalation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_last_reviewed_truthful.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_loop_audit_remediation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_matrix_and_e2e.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_multiround.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_multiround_dirty.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_multiround_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_paraphrase_escalation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_pipeline.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_provider_usage_limit.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_rename_boundary.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_resume.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_resume_base_oid.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_reviewer_blindness_is_structural.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_round_input_boundaries.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_run_layout.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_run_status_wiring.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_safety_warning_survives_quiet.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_snapshot_and_base.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_synth_phase.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_synth_phase_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_synth_provenance.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_synth_validation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_template_robustness.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_termination_reason_drift.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_testleg.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_timeout_is_per_subprocess.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_usage_limit_message.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_worktree_cleanup.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_atomic_writes.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_checks_surfacing.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_diff_size_recording.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_findings_md.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_findings_md_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_findings_md_verdict.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_handoff.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_loop_manifest_checks.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_loop_summary.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_loop_summary_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_md_safety.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_next_steps.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_producer_persist.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_reviewer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_round_manifest.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_run_init.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_run_summary.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_run_summary_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_run_summary_producer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_synth_persist.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_test_run.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/producer/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/producer/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/producer/test_producer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/producer/test_producer_confinement.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/producer/test_producer_retry.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/producer/test_producer_run.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/producer/test_producer_timeout.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/producer/test_producer_usage.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/prompts/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/prompts/test_load_template.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/prompts/test_misc.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/prompts/test_producer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/prompts/test_render_synthesizer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/prompts/test_synthesizer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/resume/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/resume/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/resume/test_budget_resume.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/resume/test_checks_phase_failure.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/resume/test_cli_malformed_rehydration.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/resume/test_find_resolve_plan.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/resume/test_plan_resume.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/resume/test_round_trip.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/resume/test_tree_drift_and_escalation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/skills/test_config_section.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/skills/test_install_skill.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/skills/test_install_skill_multi_harness.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/skills/test_install_skill_ownership.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/skills/test_install_skill_refuses.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/skills/test_skill_drift.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/skills/test_skill_status.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_anthropic_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_auth_check_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_codex_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_loop_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_orchestrator_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_orchestrator_smoke_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_producer_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_resume_reap_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_selfcheck_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_synthesizer_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_test_runner_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/snapshot/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/snapshot/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/snapshot/conftest.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/snapshot/test_snapshot.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/snapshot/test_snapshot_diff_identity.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/snapshot/test_snapshot_dirty.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/snapshot/test_snapshot_warning.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/snapshot/test_three_dot_diff.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/synthesis/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/synthesis/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/synthesis/test_cluster_repair.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/synthesis/test_clusters.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/synthesis/test_models.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/synthesis/test_models_distinct_keying.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/synthesis/test_parsing.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/synthesis/test_unanimous_blocker_downgrade.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/synthesizer/test_provenance_fabrication.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/synthesizer/test_provenance_repair.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_adapter_registry.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_adapters_fake.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_auto_init_policy.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_base_resolution.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_check_loc_script.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_checks.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_config_loader.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_cross_model_claim.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_diff_filter.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_diff_filter_binary.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_diff_filter_quoted_paths.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_escalation_coverage.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_exit_codes.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_git_choke_point.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_git_config_isolation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_git_preconditions.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_last_reviewed.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_parse_repair_extra_keys.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_pricing.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_private_identifier_gate.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_process.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_process_teardown.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_producer_escalation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_regression_acme.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_retry.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_run_status.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_run_status_signals.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_runner/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_runner/test_test_runner.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_runner/test_test_runner_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_safety_disclosures_survive_quiet.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_selfcheck.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_spec_audit.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_spec_draft.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_spec_source.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_streamed_capture.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_strip_list_semantics.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_synthesizer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_synthesizer_passthrough.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_synthesizer_provenance.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_transcript.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_update_check.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_usage.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_usage_persistence.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_workflow_action_pins.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_worktree_base_isolation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_worktree_env.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/test_worktree_strip_guard.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/worktree/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/worktree/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/worktree/conftest.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/worktree/test_worktree.py +0 -0
- {syncade-0.7.0 → syncade-0.7.1}/tests/worktree/test_worktree_cleanup.py +0 -0
|
@@ -24,8 +24,12 @@ from pathlib import Path
|
|
|
24
24
|
import syncade
|
|
25
25
|
from syncade.exit_codes import SUCCESS, WORKTREE_ERROR
|
|
26
26
|
from syncade.process import SubprocessError, run_subprocess
|
|
27
|
+
from syncade.update_check import MANIFEST_URL, _fetch, _parse, _text
|
|
27
28
|
|
|
28
29
|
_UPGRADE_TIMEOUT = 300.0
|
|
30
|
+
#: Reading one version string out of a fresh interpreter. Generous, because a cold venv
|
|
31
|
+
#: import is slower than it looks; short, because we already hold the operator's terminal.
|
|
32
|
+
_VERSION_TIMEOUT = 60.0
|
|
29
33
|
#: How far up from ``syncade/__init__.py`` an install marker can sit. A venv layout is
|
|
30
34
|
#: ``<root>/lib/python3.x/site-packages/syncade/__init__.py`` — five parents to the root; the
|
|
31
35
|
#: extra headroom costs nothing and covers layouts that nest one deeper.
|
|
@@ -149,6 +153,86 @@ def _resync_skills(out) -> bool:
|
|
|
149
153
|
return all_ok
|
|
150
154
|
|
|
151
155
|
|
|
156
|
+
def _installed_version() -> str | None:
|
|
157
|
+
"""The version on disk NOW, or ``None`` when it cannot be established.
|
|
158
|
+
|
|
159
|
+
It CANNOT be ``syncade.__version__``: this process imported that before the upgrade ran, so
|
|
160
|
+
it is by definition the old number. A fresh interpreter re-reads the metadata the upgrade
|
|
161
|
+
just rewrote, and ``sys.executable`` is the tool venv's own python under both uv and pipx —
|
|
162
|
+
precisely the environment that changed.
|
|
163
|
+
|
|
164
|
+
``None`` means "could not verify", which is deliberately NOT the same as "did not change":
|
|
165
|
+
the caller must not turn an unreadable version into a failure report.
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
result = run_subprocess(
|
|
169
|
+
# `-I` (isolated) is load-bearing, not tidiness. Without it the probe inherits
|
|
170
|
+
# PYTHONPATH and the user site dir, so a `sitecustomize` on that path runs inside
|
|
171
|
+
# the answer and can emit stdout before OR after the version print.
|
|
172
|
+
# `-I` keeps the venv's own site-packages, so `importlib.metadata` still finds
|
|
173
|
+
# the install (verified) — but it does NOT suppress `sitecustomize.py` that lives
|
|
174
|
+
# in those site-packages. An atexit hook registered there (syncade ships such a
|
|
175
|
+
# shim for worktree imports) can append a version-shaped line AFTER the real one.
|
|
176
|
+
# `os._exit(0)` bypasses all atexit handlers: the process terminates immediately
|
|
177
|
+
# after `os.write`, so nothing can append to our stdout (reproduced: a hostile
|
|
178
|
+
# sitecustomize turned "0.7.0" into "0.7.0\n99.0.0" before this fix).
|
|
179
|
+
# `os.write` is used instead of `print` because `os._exit` skips stdio flushing;
|
|
180
|
+
# an unbuffered syscall guarantees the version byte reaches the pipe.
|
|
181
|
+
[
|
|
182
|
+
sys.executable,
|
|
183
|
+
"-I",
|
|
184
|
+
"-c",
|
|
185
|
+
"import importlib.metadata as m, os; "
|
|
186
|
+
"os.write(1, (m.version('syncade') + '\\n').encode()); "
|
|
187
|
+
"os._exit(0)",
|
|
188
|
+
],
|
|
189
|
+
timeout=_VERSION_TIMEOUT,
|
|
190
|
+
)
|
|
191
|
+
except SubprocessError:
|
|
192
|
+
return None
|
|
193
|
+
if result.returncode != 0:
|
|
194
|
+
return None
|
|
195
|
+
raw = result.stdout.strip()
|
|
196
|
+
if not raw:
|
|
197
|
+
return None
|
|
198
|
+
# sitecustomize from PYTHONPATH (suppressed by -I) could emit noise before -c runs;
|
|
199
|
+
# the venv's own sitecustomize can still do so. Take the LAST line — atexit is now
|
|
200
|
+
# impossible (os._exit), so the last line is what os.write put there — and validate it
|
|
201
|
+
# parses as a version. Noise that accidentally ends in a dotted string still fails _parse.
|
|
202
|
+
candidate = raw.splitlines()[-1].strip()
|
|
203
|
+
return candidate if _parse(candidate) is not None else None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _is_newest(installed: str) -> bool:
|
|
207
|
+
"""Whether ``installed`` is at or beyond the newest version the manifest advertises.
|
|
208
|
+
|
|
209
|
+
Only ever consulted on the unhappy path — the version failed to move — so the happy path
|
|
210
|
+
stays offline. Anything that leaves the question unanswered (unreachable manifest, no usable
|
|
211
|
+
``latest``, an installed version we cannot parse) returns ``False``, i.e. "cannot prove you
|
|
212
|
+
are current", which routes to the louder message. That is the safe direction: telling someone
|
|
213
|
+
to check for a pin when they were merely current is a wasted minute; telling someone they are
|
|
214
|
+
up to date when the upgrade silently failed strands them on an old version believing it is
|
|
215
|
+
the new one.
|
|
216
|
+
|
|
217
|
+
**This compares versions itself and must never delegate to ``evaluate()``.** That function
|
|
218
|
+
answers a DIFFERENT question — "should the operator see a notice?" — and the two answers
|
|
219
|
+
only usually coincide. They part company in the documented critical-no-fix state: when
|
|
220
|
+
``critical_below`` is above the installed version and no newer release exists, ``evaluate()``
|
|
221
|
+
returns a notice while the operator is genuinely current, so reading its ``None``-ness told a
|
|
222
|
+
fully up-to-date install that its upgrade had failed and it was probably pinned. Two dogfood
|
|
223
|
+
rounds landed on this function for the same underlying reason — a near-miss reuse of a
|
|
224
|
+
neighbouring answer — and the fix both times was to ask the actual question.
|
|
225
|
+
|
|
226
|
+
There is deliberately no special case for an unreachable manifest: ``{}`` has no usable
|
|
227
|
+
``latest``, so the one parse rule already answers it. An explicit branch was there and a
|
|
228
|
+
mutation run proved it dead — every input reached the same verdict without it.
|
|
229
|
+
"""
|
|
230
|
+
manifest = _fetch(MANIFEST_URL) or {}
|
|
231
|
+
latest = _parse(_text(manifest.get("latest")))
|
|
232
|
+
current = _parse(installed)
|
|
233
|
+
return latest is not None and current is not None and current >= latest
|
|
234
|
+
|
|
235
|
+
|
|
152
236
|
def run_update(*, cwd: Path | None = None, out=None) -> int:
|
|
153
237
|
"""Upgrade syncade in place, then exit asking to be re-run."""
|
|
154
238
|
out = out or sys.stderr
|
|
@@ -213,9 +297,38 @@ def run_update(*, cwd: Path | None = None, out=None) -> int:
|
|
|
213
297
|
)
|
|
214
298
|
return WORKTREE_ERROR
|
|
215
299
|
|
|
300
|
+
# THE POST-CONDITION. Exit 0 from the package manager means "the command ran", NOT "the
|
|
301
|
+
# version moved", and the two come apart routinely: `uv tool install syncade==0.6.3` records
|
|
302
|
+
# `specifier = "==0.6.3"` in its receipt, so `uv tool upgrade` honours the pin, prints
|
|
303
|
+
# "Nothing to upgrade" and exits 0. Held packages, resolver conflicts and a manifest that is
|
|
304
|
+
# ahead of PyPI all land in the same place. Without this check syncade says "updated. Re-run
|
|
305
|
+
# your command", the operator re-runs, gets the same version, and is told again next session
|
|
306
|
+
# — forever. Measured live on the 0.7.0 release, which is why item 5 of that release existed.
|
|
307
|
+
now = _installed_version()
|
|
308
|
+
if now is not None and now == syncade.__version__:
|
|
309
|
+
# The version did not move. There are TWO reasons for that and they need opposite
|
|
310
|
+
# answers, so ask the manifest which one this is rather than guessing. Guessing was the
|
|
311
|
+
# first version of this guard and it told an operator who was simply already current
|
|
312
|
+
# that their install was pinned and broken — a fresh false claim inside the fix for a
|
|
313
|
+
# false claim, caught by testing the path instead of reasoning about it.
|
|
314
|
+
if _is_newest(now):
|
|
315
|
+
skills_ok = _resync_skills(out)
|
|
316
|
+
print(f"[syncade] already up to date ({now}).", file=out)
|
|
317
|
+
return SUCCESS if skills_ok else WORKTREE_ERROR
|
|
318
|
+
print(
|
|
319
|
+
f"[syncade] the upgrade command succeeded but syncade is still "
|
|
320
|
+
f"{syncade.__version__} — nothing was installed.\n"
|
|
321
|
+
f" Most often the install pins a version: check `uv tool list` / "
|
|
322
|
+
f"`pipx list`, then\n"
|
|
323
|
+
f" reinstall unpinned (e.g. `uv tool install --force syncade`).",
|
|
324
|
+
file=out,
|
|
325
|
+
)
|
|
326
|
+
return WORKTREE_ERROR
|
|
327
|
+
|
|
216
328
|
skills_ok = _resync_skills(out)
|
|
329
|
+
moved = f" to {now}" if now else ""
|
|
217
330
|
print(
|
|
218
|
-
"[syncade] updated. Re-run your command to use the new version — this process is\n"
|
|
331
|
+
f"[syncade] updated{moved}. Re-run your command to use the new version — this process is\n"
|
|
219
332
|
" still running the old one and cannot switch to it.",
|
|
220
333
|
file=out,
|
|
221
334
|
)
|
|
@@ -275,6 +275,7 @@ tests/cli/test_undo_auto_init.py
|
|
|
275
275
|
tests/cli/test_undo_scope.py
|
|
276
276
|
tests/cli/test_update_mode.py
|
|
277
277
|
tests/cli/test_update_notice.py
|
|
278
|
+
tests/cli/test_update_post_condition.py
|
|
278
279
|
tests/cli/test_validate_before_mutate.py
|
|
279
280
|
tests/config/__init__.py
|
|
280
281
|
tests/config/test_auth_enforcement.py
|
|
@@ -12,6 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import json
|
|
14
14
|
import pathlib
|
|
15
|
+
import sys
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
|
|
17
18
|
import pytest
|
|
@@ -359,7 +360,6 @@ def test_syncade_argv_uses_current_interpreter() -> None:
|
|
|
359
360
|
|
|
360
361
|
Verified: this test would fail against the pre-fix code that called shutil.which.
|
|
361
362
|
"""
|
|
362
|
-
import sys
|
|
363
363
|
|
|
364
364
|
assert update_mode._syncade_argv() == [sys.executable, "-m", "syncade"]
|
|
365
365
|
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""PR-h-field-08 — `--update` must not report success having installed nothing.
|
|
2
|
+
|
|
3
|
+
Split out of ``test_update_mode.py`` when these tests took it to 523 of a 500 code-LOC cap.
|
|
4
|
+
The same thing happened to ``test_config_schema.py`` and `CLAUDE.md` records it; the fix is the
|
|
5
|
+
same, and it is a real separation rather than a dumping ground: everything here is about ONE
|
|
6
|
+
question — *did the version actually move, and can we prove it?* — while the original file is
|
|
7
|
+
about install-method detection and the upgrade command.
|
|
8
|
+
|
|
9
|
+
**Four revisions of this code, four dogfood blockers, all the same shape:** something was trusted
|
|
10
|
+
to mean what it nearly meant. The package manager's exit code (ran != upgraded), then
|
|
11
|
+
``evaluate()`` returning ``None`` (no notice != no newer release), then ``evaluate()`` again for
|
|
12
|
+
the comparison (critical-no-fix), then the probe's raw stdout (any text != a version). The rule
|
|
13
|
+
that finally held is to validate a value as the thing it claims to be, at the point it enters.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import pytest
|
|
22
|
+
|
|
23
|
+
import syncade
|
|
24
|
+
from syncade.cli import update_mode
|
|
25
|
+
from syncade.exit_codes import SUCCESS, WORKTREE_ERROR
|
|
26
|
+
from syncade.process import SubprocessError, SubprocessResult
|
|
27
|
+
from tests.cli.test_update_mode import _install_tree, _point_syncade_at, _Result
|
|
28
|
+
|
|
29
|
+
# --------------------------------------------------------------------- the post-condition
|
|
30
|
+
#
|
|
31
|
+
# Exit 0 from the package manager means "the command ran", NOT "the version moved". Found live
|
|
32
|
+
# on the 0.7.0 release: `uv tool install syncade==0.6.3` records `specifier = "==0.6.3"` in its
|
|
33
|
+
# receipt, so `uv tool upgrade` honours the pin, prints "Nothing to upgrade" and exits 0 — and
|
|
34
|
+
# syncade said "updated. Re-run your command" having installed nothing. The operator re-runs,
|
|
35
|
+
# sees the same version, and is told again every session, forever.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _upgrade_reporting_success(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
39
|
+
"""A proven uv install whose upgrade command exits 0."""
|
|
40
|
+
pkg = _install_tree(tmp_path, "uv-receipt.toml")
|
|
41
|
+
_point_syncade_at(monkeypatch, pkg)
|
|
42
|
+
monkeypatch.setattr(update_mode, "run_subprocess", lambda *a, **k: _Result(0))
|
|
43
|
+
monkeypatch.setattr(update_mode, "_resync_skills", lambda out: True)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_an_upgrade_that_changed_nothing_is_reported_as_failure(
|
|
47
|
+
tmp_path, monkeypatch, capsys
|
|
48
|
+
) -> None:
|
|
49
|
+
"""The defect itself: same version after a 'successful' upgrade must not read as success."""
|
|
50
|
+
_upgrade_reporting_success(monkeypatch, tmp_path)
|
|
51
|
+
monkeypatch.setattr(update_mode, "_installed_version", lambda: syncade.__version__)
|
|
52
|
+
monkeypatch.setattr(update_mode, "_fetch", lambda url: {"latest": "99.0.0"})
|
|
53
|
+
|
|
54
|
+
assert update_mode.run_update() == WORKTREE_ERROR
|
|
55
|
+
out = capsys.readouterr().err
|
|
56
|
+
assert "nothing was installed" in out
|
|
57
|
+
assert "updated." not in out, "it must not also claim success"
|
|
58
|
+
assert "pins a version" in out, "name the most likely cause, not just the symptom"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_a_real_upgrade_still_succeeds_and_names_the_new_version(
|
|
62
|
+
tmp_path, monkeypatch, capsys
|
|
63
|
+
) -> None:
|
|
64
|
+
"""The guard must not fire on the path it exists to protect."""
|
|
65
|
+
_upgrade_reporting_success(monkeypatch, tmp_path)
|
|
66
|
+
monkeypatch.setattr(update_mode, "_installed_version", lambda: "99.0.0")
|
|
67
|
+
|
|
68
|
+
assert update_mode.run_update() == SUCCESS
|
|
69
|
+
assert "updated to 99.0.0" in capsys.readouterr().err
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_an_unverifiable_version_is_not_reported_as_failure(tmp_path, monkeypatch, capsys) -> None:
|
|
73
|
+
"""`None` means "could not verify", which is NOT "did not change".
|
|
74
|
+
|
|
75
|
+
Turning an unreadable version into a failure would break every install whose metadata we
|
|
76
|
+
cannot probe, having actually upgraded them — a worse error than the one being fixed.
|
|
77
|
+
"""
|
|
78
|
+
_upgrade_reporting_success(monkeypatch, tmp_path)
|
|
79
|
+
monkeypatch.setattr(update_mode, "_installed_version", lambda: None)
|
|
80
|
+
|
|
81
|
+
assert update_mode.run_update() == SUCCESS
|
|
82
|
+
out = capsys.readouterr().err
|
|
83
|
+
assert "updated." in out
|
|
84
|
+
assert "nothing was installed" not in out
|
|
85
|
+
assert "updated to" not in out, "no version may be named when none was established"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_the_version_probe_reads_a_fresh_interpreter_not_this_one(monkeypatch) -> None:
|
|
89
|
+
"""It must not answer from `syncade.__version__`, which this process imported BEFORE the
|
|
90
|
+
upgrade and is therefore the old number by construction."""
|
|
91
|
+
seen: list[list[str]] = []
|
|
92
|
+
|
|
93
|
+
def spy(cmd, **kw):
|
|
94
|
+
seen.append(cmd)
|
|
95
|
+
return SubprocessResult(returncode=0, stdout="1.2.3\n", stderr="", duration_seconds=0.0)
|
|
96
|
+
|
|
97
|
+
monkeypatch.setattr(update_mode, "run_subprocess", spy)
|
|
98
|
+
assert update_mode._installed_version() == "1.2.3"
|
|
99
|
+
assert seen and seen[0][0] == sys.executable, "must re-invoke the interpreter, not import"
|
|
100
|
+
assert "importlib.metadata" in seen[0][-1]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_an_unreadable_probe_degrades_to_none(monkeypatch) -> None:
|
|
104
|
+
for outcome in (
|
|
105
|
+
lambda *a, **k: (_ for _ in ()).throw(SubprocessError("no python")),
|
|
106
|
+
lambda *a, **k: SubprocessResult(returncode=1, stdout="", stderr="x", duration_seconds=0.0),
|
|
107
|
+
lambda *a, **k: SubprocessResult(
|
|
108
|
+
returncode=0, stdout=" \n", stderr="", duration_seconds=0.0
|
|
109
|
+
),
|
|
110
|
+
):
|
|
111
|
+
monkeypatch.setattr(update_mode, "run_subprocess", outcome)
|
|
112
|
+
assert update_mode._installed_version() is None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_startup_noise_before_version_line_is_stripped(monkeypatch) -> None:
|
|
116
|
+
"""sitecustomize imported from PYTHONPATH can emit stdout before the version print.
|
|
117
|
+
|
|
118
|
+
The old code returned the full stdout verbatim, so "noise\\n0.7.0" != "0.7.0" and
|
|
119
|
+
run_update treated the install as having moved — a false success on an unchanged version.
|
|
120
|
+
"""
|
|
121
|
+
monkeypatch.setattr(
|
|
122
|
+
update_mode,
|
|
123
|
+
"run_subprocess",
|
|
124
|
+
lambda *a, **k: SubprocessResult(
|
|
125
|
+
returncode=0, stdout="startup noise\n1.2.3\n", stderr="", duration_seconds=0.0
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
assert update_mode._installed_version() == "1.2.3"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_non_version_stdout_degrades_to_none(monkeypatch) -> None:
|
|
132
|
+
"""stdout that doesn't end with a parseable version string means 'could not verify'."""
|
|
133
|
+
for stdout in ("not a version\n", "noise only\n", "1.2\n", "1.2.3.4\n"):
|
|
134
|
+
monkeypatch.setattr(
|
|
135
|
+
update_mode,
|
|
136
|
+
"run_subprocess",
|
|
137
|
+
lambda *a, stdout=stdout, **k: SubprocessResult(
|
|
138
|
+
returncode=0, stdout=stdout, stderr="", duration_seconds=0.0
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
assert update_mode._installed_version() is None, f"should be None for {stdout!r}"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_an_already_current_install_is_not_reported_as_a_failed_upgrade(
|
|
145
|
+
tmp_path, monkeypatch, capsys
|
|
146
|
+
) -> None:
|
|
147
|
+
"""The post-condition's own false positive, found by exercising it rather than reasoning.
|
|
148
|
+
|
|
149
|
+
The version not moving has TWO causes needing opposite answers: a pinned/held install (a
|
|
150
|
+
real failure) and simply being current already (not a failure at all). The first version of
|
|
151
|
+
the guard could not tell them apart and told an operator who ran `--update` while current
|
|
152
|
+
that their install was pinned and broken — a fresh false claim inside the fix for one.
|
|
153
|
+
"""
|
|
154
|
+
_upgrade_reporting_success(monkeypatch, tmp_path)
|
|
155
|
+
monkeypatch.setattr(update_mode, "_installed_version", lambda: syncade.__version__)
|
|
156
|
+
monkeypatch.setattr(update_mode, "_fetch", lambda url: {"latest": syncade.__version__})
|
|
157
|
+
|
|
158
|
+
assert update_mode.run_update() == SUCCESS
|
|
159
|
+
out = capsys.readouterr().err
|
|
160
|
+
assert "already up to date" in out
|
|
161
|
+
assert "pins a version" not in out
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_an_unreachable_manifest_cannot_certify_you_as_current(
|
|
165
|
+
tmp_path, monkeypatch, capsys
|
|
166
|
+
) -> None:
|
|
167
|
+
"""`_fetch` returning None must NOT read as "you are current".
|
|
168
|
+
|
|
169
|
+
`evaluate(None, v)` also returns None, so collapsing the two would tell an operator whose
|
|
170
|
+
upgrade silently failed while offline that they were up to date — stranding them on an old
|
|
171
|
+
version believing it is the new one. The louder message is the safe direction here.
|
|
172
|
+
"""
|
|
173
|
+
_upgrade_reporting_success(monkeypatch, tmp_path)
|
|
174
|
+
monkeypatch.setattr(update_mode, "_installed_version", lambda: syncade.__version__)
|
|
175
|
+
monkeypatch.setattr(update_mode, "_fetch", lambda url: None)
|
|
176
|
+
|
|
177
|
+
assert update_mode.run_update() == WORKTREE_ERROR
|
|
178
|
+
assert "already up to date" not in capsys.readouterr().err
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_the_happy_path_never_touches_the_network(tmp_path, monkeypatch) -> None:
|
|
182
|
+
"""The manifest is consulted ONLY when the version failed to move, so a normal successful
|
|
183
|
+
upgrade adds no request and no latency."""
|
|
184
|
+
_upgrade_reporting_success(monkeypatch, tmp_path)
|
|
185
|
+
monkeypatch.setattr(update_mode, "_installed_version", lambda: "99.0.0")
|
|
186
|
+
monkeypatch.setattr(
|
|
187
|
+
update_mode, "_fetch", lambda url: pytest.fail("the happy path must not fetch")
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
assert update_mode.run_update() == SUCCESS
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ---------------------------------------------------------------- malformed-manifest regression
|
|
194
|
+
#
|
|
195
|
+
# A reachable manifest that has no parseable latest field must NOT certify the operator as
|
|
196
|
+
# current. evaluate() returns None for BOTH "already current" and "no usable latest version",
|
|
197
|
+
# so trusting None alone on a dict manifest restores the false-success class for any manifest
|
|
198
|
+
# that is syntactically valid JSON but semantically unusable.
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@pytest.mark.parametrize(
|
|
202
|
+
"manifest",
|
|
203
|
+
[
|
|
204
|
+
{},
|
|
205
|
+
{"latest": None},
|
|
206
|
+
{"latest": "not-a-version"},
|
|
207
|
+
{"latest": "0.7"}, # two-part, not three-part
|
|
208
|
+
{"latest": ""},
|
|
209
|
+
],
|
|
210
|
+
ids=["empty", "null-latest", "unparseable-latest", "two-part-latest", "empty-latest"],
|
|
211
|
+
)
|
|
212
|
+
def test_is_newest_treats_malformed_manifest_as_unverifiable(manifest, monkeypatch) -> None:
|
|
213
|
+
"""A reachable manifest with no parseable latest must not certify any version as current.
|
|
214
|
+
|
|
215
|
+
Verified: this test fails against the pre-fix _is_newest that only checked
|
|
216
|
+
`evaluate(manifest, installed) is None` — all five manifests above make evaluate() return
|
|
217
|
+
None while providing no actual version information, so the pre-fix code returned True and
|
|
218
|
+
certified the operator as already-current on an unusable manifest.
|
|
219
|
+
"""
|
|
220
|
+
monkeypatch.setattr(update_mode, "_fetch", lambda url: manifest)
|
|
221
|
+
assert update_mode._is_newest("0.7.0") is False, (
|
|
222
|
+
f"manifest {manifest!r} cannot prove current status; _is_newest must return False"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_a_malformed_manifest_does_not_produce_already_up_to_date(
|
|
227
|
+
tmp_path, monkeypatch, capsys
|
|
228
|
+
) -> None:
|
|
229
|
+
"""End-to-end: an empty manifest must not report success to an operator whose version
|
|
230
|
+
did not move. Verified: fails against the pre-fix code."""
|
|
231
|
+
_upgrade_reporting_success(monkeypatch, tmp_path)
|
|
232
|
+
monkeypatch.setattr(update_mode, "_installed_version", lambda: syncade.__version__)
|
|
233
|
+
monkeypatch.setattr(update_mode, "_fetch", lambda url: {}) # reachable but malformed
|
|
234
|
+
|
|
235
|
+
assert update_mode.run_update() == WORKTREE_ERROR
|
|
236
|
+
out = capsys.readouterr().err
|
|
237
|
+
assert "already up to date" not in out
|
|
238
|
+
assert "nothing was installed" in out
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ------------------------------------------------------------ already-current resync regression
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_skills_are_resynced_on_the_already_current_path(tmp_path, monkeypatch) -> None:
|
|
245
|
+
"""--update must re-install skills even when the package is already at the newest version.
|
|
246
|
+
|
|
247
|
+
The already-current early return was added by this PR. Without explicit resync on that
|
|
248
|
+
path, a stale or missing skill is silently left behind on every invocation where the
|
|
249
|
+
package is current. Verified: fails against the pre-fix code where the already-current
|
|
250
|
+
branch returned before _resync_skills() was called.
|
|
251
|
+
"""
|
|
252
|
+
_point_syncade_at(monkeypatch, _install_tree(tmp_path, "uv-receipt.toml"))
|
|
253
|
+
monkeypatch.setattr(update_mode, "run_subprocess", lambda *a, **k: _Result(0))
|
|
254
|
+
monkeypatch.setattr(update_mode, "_installed_version", lambda: syncade.__version__)
|
|
255
|
+
monkeypatch.setattr(update_mode, "_fetch", lambda url: {"latest": syncade.__version__})
|
|
256
|
+
|
|
257
|
+
calls: list = []
|
|
258
|
+
monkeypatch.setattr(update_mode, "_resync_skills", lambda out: calls.append(1) or True)
|
|
259
|
+
|
|
260
|
+
assert update_mode.run_update(cwd=tmp_path) == SUCCESS
|
|
261
|
+
assert calls == [1], "skills must be resynced even when the package is already current"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def test_a_failed_skill_resync_on_already_current_path_returns_non_zero(
|
|
265
|
+
tmp_path, monkeypatch, capsys
|
|
266
|
+
) -> None:
|
|
267
|
+
"""A failed resync on the already-current path must propagate the failure code."""
|
|
268
|
+
_point_syncade_at(monkeypatch, _install_tree(tmp_path, "uv-receipt.toml"))
|
|
269
|
+
monkeypatch.setattr(update_mode, "run_subprocess", lambda *a, **k: _Result(0))
|
|
270
|
+
monkeypatch.setattr(update_mode, "_installed_version", lambda: syncade.__version__)
|
|
271
|
+
monkeypatch.setattr(update_mode, "_fetch", lambda url: {"latest": syncade.__version__})
|
|
272
|
+
monkeypatch.setattr(update_mode, "_resync_skills", lambda out: False)
|
|
273
|
+
|
|
274
|
+
assert update_mode.run_update(cwd=tmp_path) == WORKTREE_ERROR
|
|
275
|
+
assert "already up to date" in capsys.readouterr().err
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_a_critical_advisory_with_no_newer_release_is_still_current(
|
|
279
|
+
tmp_path, monkeypatch, capsys
|
|
280
|
+
) -> None:
|
|
281
|
+
"""`_is_newest` must compare VERSIONS, never delegate to `evaluate()`.
|
|
282
|
+
|
|
283
|
+
`evaluate()` answers "should the operator see a notice?", which is a different question and
|
|
284
|
+
only usually gives the same answer. In the documented critical-no-fix state — `critical_below`
|
|
285
|
+
above the installed version, no newer release published — it returns a notice while the
|
|
286
|
+
operator is genuinely current. Reading its None-ness therefore told a fully up-to-date install
|
|
287
|
+
that its upgrade had failed and it was probably pinned. Unanimous dogfood blocker; it survived
|
|
288
|
+
a producer round because the previous fix repaired the malformed-manifest instance while
|
|
289
|
+
keeping the same near-miss delegation.
|
|
290
|
+
"""
|
|
291
|
+
_upgrade_reporting_success(monkeypatch, tmp_path)
|
|
292
|
+
monkeypatch.setattr(update_mode, "_installed_version", lambda: syncade.__version__)
|
|
293
|
+
monkeypatch.setattr(
|
|
294
|
+
update_mode,
|
|
295
|
+
"_fetch",
|
|
296
|
+
lambda url: {
|
|
297
|
+
"latest": syncade.__version__,
|
|
298
|
+
"critical_below": "99.0.0",
|
|
299
|
+
"critical_reason": "a critical fix with no newer release",
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
assert update_mode.run_update() == SUCCESS
|
|
304
|
+
out = capsys.readouterr().err
|
|
305
|
+
assert "already up to date" in out
|
|
306
|
+
assert "pins a version" not in out
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def test_is_newest_answers_the_version_question_across_every_manifest_shape(monkeypatch) -> None:
|
|
310
|
+
"""One table, so a future edit cannot fix one shape and reopen another — which is exactly how
|
|
311
|
+
this function reached its third revision."""
|
|
312
|
+
installed = "0.7.0"
|
|
313
|
+
cases = [
|
|
314
|
+
({"latest": "0.7.0"}, True, "manifest agrees"),
|
|
315
|
+
({"latest": "0.6.3"}, True, "manifest is BEHIND the install"),
|
|
316
|
+
({"latest": "0.7.0", "critical_below": "99.0.0"}, True, "critical, no newer release"),
|
|
317
|
+
({"latest": "99.0.0"}, False, "a newer release exists"),
|
|
318
|
+
(None, False, "unreachable"),
|
|
319
|
+
({}, False, "no latest key"),
|
|
320
|
+
({"latest": None}, False, "latest is null"),
|
|
321
|
+
({"latest": ""}, False, "latest is empty"),
|
|
322
|
+
({"latest": "garbage"}, False, "latest unparseable"),
|
|
323
|
+
({"latest": 5}, False, "latest not a string"),
|
|
324
|
+
({"latest": "1.2"}, False, "latest not three components"),
|
|
325
|
+
]
|
|
326
|
+
for manifest, expected, why in cases:
|
|
327
|
+
monkeypatch.setattr(update_mode, "_fetch", lambda url, m=manifest: m)
|
|
328
|
+
assert update_mode._is_newest(installed) is expected, why
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def test_is_newest_cannot_certify_an_unparseable_installed_version(monkeypatch) -> None:
|
|
332
|
+
monkeypatch.setattr(update_mode, "_fetch", lambda url: {"latest": "0.7.0"})
|
|
333
|
+
assert update_mode._is_newest("not-a-version") is False
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def test_the_probe_is_isolated_from_ambient_interpreter_state(monkeypatch) -> None:
|
|
337
|
+
"""`-I` is load-bearing: without it the probe inherits PYTHONPATH and the user site dir.
|
|
338
|
+
|
|
339
|
+
A `sitecustomize` on that path runs INSIDE the answer — an atexit hook printing a version
|
|
340
|
+
number becomes the last stdout line, so a no-op upgrade reads as a move. Reproduced live: a
|
|
341
|
+
hostile PYTHONPATH turned the probe's `"0.7.0"` into `"0.7.0\\n99.0.0"`. Syncade ships such a
|
|
342
|
+
shim itself for worktree imports, so this is not purely adversarial.
|
|
343
|
+
|
|
344
|
+
`-I` suppresses PYTHONPATH but NOT the venv's own site-packages, where a `sitecustomize.py`
|
|
345
|
+
can still register atexit hooks. The probe must call `os._exit(0)` to bypass atexit entirely —
|
|
346
|
+
the only guarantee against a venv-local hook appending to stdout after the version line.
|
|
347
|
+
`os.write` is asserted alongside it: `os._exit` skips stdio flushing, so `print()` would lose
|
|
348
|
+
the version; the unbuffered syscall guarantees delivery before the process exits.
|
|
349
|
+
|
|
350
|
+
Pinned as flag assertions because the behavioural twin below cannot run the failure case
|
|
351
|
+
without installing a real adversarial sitecustomize.
|
|
352
|
+
"""
|
|
353
|
+
seen: list[list[str]] = []
|
|
354
|
+
monkeypatch.setattr(
|
|
355
|
+
update_mode,
|
|
356
|
+
"run_subprocess",
|
|
357
|
+
lambda cmd, **kw: (
|
|
358
|
+
seen.append(cmd)
|
|
359
|
+
or SubprocessResult(returncode=0, stdout="1.2.3\n", stderr="", duration_seconds=0.0)
|
|
360
|
+
),
|
|
361
|
+
)
|
|
362
|
+
update_mode._installed_version()
|
|
363
|
+
assert seen and "-I" in seen[0], (
|
|
364
|
+
"the version probe must run isolated; without -I a sitecustomize on PYTHONPATH can "
|
|
365
|
+
"append a version number to its stdout and fake a successful upgrade"
|
|
366
|
+
)
|
|
367
|
+
assert seen[0].index("-I") < seen[0].index("-c"), "-I must precede -c to take effect"
|
|
368
|
+
c_arg = seen[0][seen[0].index("-c") + 1]
|
|
369
|
+
assert "os._exit(0)" in c_arg, (
|
|
370
|
+
"the probe must call os._exit(0) to bypass atexit handlers; a venv-local "
|
|
371
|
+
"sitecustomize can register atexit hooks that append a fake version line after the real one"
|
|
372
|
+
)
|
|
373
|
+
assert "os.write" in c_arg, (
|
|
374
|
+
"the probe must use os.write (unbuffered) not print; os._exit skips stdio flushing "
|
|
375
|
+
"so print output would be lost before reaching the pipe"
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def test_the_isolated_probe_still_resolves_the_real_install() -> None:
|
|
380
|
+
"""The behavioural twin: `-I` must not break what the probe is FOR.
|
|
381
|
+
|
|
382
|
+
It drops PYTHONPATH and the user site dir but keeps the environment's own site-packages, so
|
|
383
|
+
`importlib.metadata` still finds the package. Runs the real subprocess — a flag assertion
|
|
384
|
+
alone would happily pin an isolation level that returns None for everyone.
|
|
385
|
+
|
|
386
|
+
We assert the result is a parseable, non-None version, NOT that it equals
|
|
387
|
+
`syncade.__version__`. In a dev worktree the source version and the installed distribution
|
|
388
|
+
version legitimately differ (e.g. 0.7.0 in source, 0.1.0 in site-packages); asserting
|
|
389
|
+
equality would make this test prove the ambient installation, not the probe's behaviour.
|
|
390
|
+
"""
|
|
391
|
+
from syncade.update_check import _parse
|
|
392
|
+
|
|
393
|
+
result = update_mode._installed_version()
|
|
394
|
+
assert result is not None, (
|
|
395
|
+
"the probe must find the installed package under -I "
|
|
396
|
+
"(drops PYTHONPATH but keeps the environment's own site-packages)"
|
|
397
|
+
)
|
|
398
|
+
assert _parse(result) is not None, f"result {result!r} must be a parseable version string"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|