cc-session-tools 2.12.2__tar.gz → 2.12.4__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.
- {cc_session_tools-2.12.2/src/cc_session_tools.egg-info → cc_session_tools-2.12.4}/PKG-INFO +1 -1
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/pyproject.toml +1 -1
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/cli/ccst.py +59 -12
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/doctor.py +10 -6
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/resolve.py +9 -1
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/verify.py +1 -1
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/bundled_jobs.py +56 -13
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/digest.py +16 -1
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/surface.py +84 -14
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4/src/cc_session_tools.egg-info}/PKG-INFO +1 -1
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_ccsched_jobs_cli.py +8 -1
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_doctor.py +28 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_pdata_sync_cli.py +54 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_scheduler_bundled_jobs.py +60 -1
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/LICENSE +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/README.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/setup.cfg +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/__init__.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/cli/__init__.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/cli/ccd.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/cli/ccmsg.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/cli/ccr.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/cli/ccs.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/cli/ccsched.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/cli/migrate_ccmsg.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/cli/migrate_ccsched.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/cli/migrate_sessions_db.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/cli/migrate_telemetry.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/config/hooks-bundle.json +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/config/session-tag-hook.json +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/hooks_install.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/__init__.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/claude_flags.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/claude_md_install.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/context_overrides.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/db.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/debug.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/doctor_mutes.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/hook_registry.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/install_sync.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/levenshtein.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/machine_identity.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/messaging/__init__.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/messaging/addressing.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/messaging/cursor.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/messaging/lock.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/messaging/message.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/messaging/move_safety.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/messaging/repository.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/messaging/retention.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/messaging/service.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/messaging/store.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/occupancy.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/paths.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/__init__.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/backup.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/classify.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/cutover.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/dump.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/formatting.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/importers.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/init_paths.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/init_service.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/manifest.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/naming.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/rehydrate.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/rename_group.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/reorganize.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/repository.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/service.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/session_output.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/store.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/sync_check.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/sync_lock.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/sync_notify.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/vector_clock.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/vector_clock_store.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/write_log.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/picker.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/proc_lock.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/prompts.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/roots.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/rules.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/__init__.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/cadence.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/cursor.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/due.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/duration.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/jobspec.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/ledger.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/lock.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/notify.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/reconcile.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/registry.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/runner.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/state.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/store.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/throttle.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/worker.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/session_gc.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/sessions.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/sessions_db.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/sessions_repair.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/shell_install.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/tasklist.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/telemetry_store.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/prompts/pdata-migration-claude-md-update.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/prompts/pdata-migration-skills-update.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/analyse-cc-usage/SKILL.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/clean-hook-sessions/SKILL.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/clean-hook-sessions/scripts/clean-hook-sessions.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/context-override/SKILL.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/delete-sessions/SKILL.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/delete-sessions/scripts/delete_sessions.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/do-executor-critic-assessor-loop/SKILL.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/find-claude-code-session/SKILL.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/generate-8digit-code/SKILL.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/generate-8digit-code/scripts/generate_8digit_code.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/list-empty-sessions/SKILL.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/list-empty-sessions/scripts/list_empty_sessions.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/manage-recurring-cc-jobs-using-ccsched/SKILL.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/move-session/SKILL.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/move-session/hooks/sessionstart-pending-rename.sh +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/move-session/scripts/cc_session_rules.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/move-session/scripts/move_session.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/pm-pdata-conflict-resolution/SKILL.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/pm-pdata-schema-design/SKILL.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/pm-project-init/SKILL.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/pm-project-layout-reference/SKILL.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/pm-update-central-files/SKILL.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/reduce-persistent-context/CALIBRATION.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/reduce-persistent-context/SKILL.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/reduce-persistent-context/scripts/analyze_context.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/reduce-persistent-context/scripts/duplicates.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/reduce-persistent-context/scripts/measure.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/reduce-persistent-context/scripts/report.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/reduce-persistent-context/scripts/tokens.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/reduce-persistent-context/scripts/usage.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/select-agent-model/SKILL.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/send-session-message/SKILL.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/update-command-cache/SKILL.md +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/skills/update-command-cache/scripts/update_command_cache.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools.egg-info/SOURCES.txt +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools.egg-info/dependency_links.txt +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools.egg-info/entry_points.txt +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools.egg-info/requires.txt +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools.egg-info/top_level.txt +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/__init__.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/after_response.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/bash_hard_deny.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/bash_security_review.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/cache.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/catchup.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/confirm_8digit.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/context_window_warning.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/last_screenshot.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/marker_allow.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/markers.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/messaging_deliver.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/model_info.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/normalise.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/pdata_sync.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/pending_migration.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/pending_rename.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/session_tag.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/stats.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/telemetry.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/telemetry_query.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/telemetry_trim.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/transcript.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cccs_hooks/worklog_guard.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/claude_code_usage/__init__.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/claude_code_usage/attribution.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/claude_code_usage/cache.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/claude_code_usage/ccusage_wrapper.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/claude_code_usage/cli.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/claude_code_usage/data/pricing.json +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/claude_code_usage/parent_inference.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/claude_code_usage/parser.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/claude_code_usage/pricing.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/claude_code_usage/query.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/claude_code_usage/report.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/claude_code_usage/schema.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/claude_code_usage/session_names.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_after_response.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_attribution.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_bash_hard_deny.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_bash_security_review.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_cache.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_cache_sqlite.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccr_orphans.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccs_emptiness.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccs_eta.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccs_list_mode.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccs_scope_flags.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccs_sentinel_sort.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccs_session_counts.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_bundle_discovery.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_cli.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_context_override_cli.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_gc_prune.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_gc_report.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_hook_dispatcher.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_hooks_install_prune.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_hooks_install_zero_arg.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_hooks_uninstall.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_install_everything.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_machine_identity_cli.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_main_gate.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_pdata_cli.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_pdata_init_cli.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_pdata_reconcile_cli.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_pdata_rename_group_cli.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_pdata_reorganize_cli.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_pdata_verify_cli.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_repair_sessions_cli.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_sessions_cli.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_shell_install.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_skills_install.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_skills_uninstall.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_telemetry_query.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccst_telemetry_trim.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_ccusage_wrapper.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_claude_flags.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_claude_md_install.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_cli.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_cli_ccd.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_cli_ccd_dry_run.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_cli_ccr.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_cli_ccs.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_cli_version.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_confirm_8digit.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_context_overrides.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_context_window_warning.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_db.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_debug.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_doctor_mutes.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_empty_session.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_hook_cache.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_hooks_install.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_install_sync.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_last_screenshot.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_levenshtein.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_machine_identity.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_marker_allow.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_migrate_ccmsg.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_migrate_ccsched.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_migrate_sessions_db.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_migrate_telemetry.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_model_info.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_normalise.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_occupancy.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_parent_inference.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_parser.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_paths.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_pending_migration_hook.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_picker.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_pricing.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_proc_lock.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_prompts.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_query.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_report.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_roots.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_rules.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_schema.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_session_names.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_session_tag.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_sessions.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_sessions_db.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_sessions_repair.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_tasklist.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_telemetry.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_telemetry_store.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_transcript.py +0 -0
- {cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/tests/test_worklog_guard.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cc-session-tools"
|
|
7
|
-
version = "2.12.
|
|
7
|
+
version = "2.12.4"
|
|
8
8
|
description = "Claude Code session management CLI tools (ccd, ccr, ccs, claude-code-usage, ccst) and hook library (cccs_hooks)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -1611,6 +1611,42 @@ def _resolve_deleted_at(payload: RecordPayload) -> int | None:
|
|
|
1611
1611
|
return payload["base"]["deleted_at"]
|
|
1612
1612
|
|
|
1613
1613
|
|
|
1614
|
+
def _close_empty_fork_if_any(project: str) -> bool:
|
|
1615
|
+
"""`resolve.diff_against_dump` found zero record/schema differences for `project` - but that
|
|
1616
|
+
alone doesn't mean there's nothing to do: the vector clock can still be a genuine fork (each
|
|
1617
|
+
machine made a local write the other hasn't seen) whose *content* happens to match on both
|
|
1618
|
+
sides, e.g. two machines independently re-deriving the same auto-reconciled index from
|
|
1619
|
+
already-synced files. In that case `dump`/`rehydrate` refuse forever with nothing for a human
|
|
1620
|
+
to pick between - `resolve.apply_resolution(project, {})` is now able to close it (see that
|
|
1621
|
+
function's own comment), so do so.
|
|
1622
|
+
|
|
1623
|
+
Deliberately narrow: only touches anything when the comparison is an actual FORK. A plain
|
|
1624
|
+
"already up to date" project (DUMP_DOMINATES, or nothing published yet) is left completely
|
|
1625
|
+
alone - the diagnose path stays side-effect-free for the overwhelmingly common case, matching
|
|
1626
|
+
`diff_against_dump`'s own "Display/diagnostic use only" contract. Returns True iff a fork was
|
|
1627
|
+
found and closed; False for every other outcome (no fork, checksum-invalid dump, lock
|
|
1628
|
+
contention) - callers fall back to the ordinary "clean" message in all of those, since no
|
|
1629
|
+
caller here treats "not closed" as an error worth surfacing on its own (a checksum-invalid
|
|
1630
|
+
dump or lock contention will already have been reported by the caller that hit it first, or
|
|
1631
|
+
will be on the very next run)."""
|
|
1632
|
+
from cc_session_tools.lib.pdata import (
|
|
1633
|
+
dump, repository, resolve, store, vector_clock, vector_clock_store,
|
|
1634
|
+
)
|
|
1635
|
+
|
|
1636
|
+
project_root = store.project_root(project)
|
|
1637
|
+
existing = dump.read_latest(project_root)
|
|
1638
|
+
if not existing.checksum_valid:
|
|
1639
|
+
return False
|
|
1640
|
+
conn = repository.connect(project)
|
|
1641
|
+
try:
|
|
1642
|
+
local_vector = vector_clock_store.read_vector(conn)
|
|
1643
|
+
finally:
|
|
1644
|
+
conn.close()
|
|
1645
|
+
if vector_clock.compare(local=local_vector, dump=existing.vector) is not vector_clock.Comparison.FORK:
|
|
1646
|
+
return False
|
|
1647
|
+
return resolve.apply_resolution(project, {}) is resolve.ApplyOutcome.APPLIED
|
|
1648
|
+
|
|
1649
|
+
|
|
1614
1650
|
def _cmd_pdata_resolve(args: argparse.Namespace) -> int:
|
|
1615
1651
|
from cc_session_tools.lib.pdata import resolve, verify
|
|
1616
1652
|
|
|
@@ -1666,18 +1702,24 @@ def _cmd_pdata_resolve(args: argparse.Namespace) -> int:
|
|
|
1666
1702
|
if has_diff:
|
|
1667
1703
|
outstanding += 1
|
|
1668
1704
|
|
|
1669
|
-
if
|
|
1670
|
-
|
|
1705
|
+
if not has_diff:
|
|
1706
|
+
closed = _close_empty_fork_if_any(project)
|
|
1707
|
+
if compact:
|
|
1708
|
+
print(f"{project}: clean (fork auto-closed)" if closed else f"{project}: clean")
|
|
1709
|
+
elif closed:
|
|
1671
1710
|
print(
|
|
1672
|
-
f"{project}:
|
|
1673
|
-
f"
|
|
1711
|
+
f"{project}: clean - content already matched the dump; closed the "
|
|
1712
|
+
f"vector-clock fork automatically (nothing to choose between)"
|
|
1674
1713
|
)
|
|
1675
1714
|
else:
|
|
1676
|
-
print(f"{project}: clean")
|
|
1715
|
+
print(f"{project}: clean - nothing to resolve")
|
|
1677
1716
|
continue
|
|
1678
1717
|
|
|
1679
|
-
if
|
|
1680
|
-
print(
|
|
1718
|
+
if compact:
|
|
1719
|
+
print(
|
|
1720
|
+
f"{project}: {len(diff.records)} record(s) + "
|
|
1721
|
+
f"{len(diff.schema_fields)} schema field(s) outstanding"
|
|
1722
|
+
)
|
|
1681
1723
|
continue
|
|
1682
1724
|
|
|
1683
1725
|
print(
|
|
@@ -2132,12 +2174,17 @@ def _cmd_ccsched_jobs_install(args: argparse.Namespace) -> int:
|
|
|
2132
2174
|
for job in bundled_jobs.BUNDLED_CCSCHED_JOBS:
|
|
2133
2175
|
spec = existing.get(job.job_id)
|
|
2134
2176
|
if spec is not None:
|
|
2135
|
-
|
|
2136
|
-
if
|
|
2177
|
+
changed_detail = bundled_jobs.diff_from_bundled_detail(spec, job)
|
|
2178
|
+
if changed_detail:
|
|
2179
|
+
field_word = "field differs" if len(changed_detail) == 1 else "fields differ"
|
|
2180
|
+
print(
|
|
2181
|
+
f" changed (not touched): {job.job_id} - {len(changed_detail)} "
|
|
2182
|
+
f"{field_word} from the bundled definition:"
|
|
2183
|
+
)
|
|
2184
|
+
print(bundled_jobs.render_field_diffs(changed_detail))
|
|
2137
2185
|
print(
|
|
2138
|
-
f"
|
|
2139
|
-
f"
|
|
2140
|
-
f"leave as your intentional customization"
|
|
2186
|
+
f" run 'ccsched edit {job.job_id}' to realign, or leave as your "
|
|
2187
|
+
f"intentional customization"
|
|
2141
2188
|
)
|
|
2142
2189
|
elif not spec.enabled:
|
|
2143
2190
|
print(f" disabled (not touched): {job.job_id} - run 'ccsched enable {job.job_id}' to re-enable")
|
|
@@ -256,16 +256,20 @@ def check_ccsched_job_registered(
|
|
|
256
256
|
for spec in specs:
|
|
257
257
|
if spec.job_id == job_id:
|
|
258
258
|
if expected is not None:
|
|
259
|
-
from cc_session_tools.lib.scheduler.bundled_jobs import
|
|
259
|
+
from cc_session_tools.lib.scheduler.bundled_jobs import (
|
|
260
|
+
diff_from_bundled_detail, render_field_diffs,
|
|
261
|
+
)
|
|
260
262
|
|
|
261
|
-
|
|
262
|
-
if
|
|
263
|
+
changed_detail = diff_from_bundled_detail(spec, expected)
|
|
264
|
+
if changed_detail:
|
|
265
|
+
field_word = "field differs" if len(changed_detail) == 1 else "fields differ"
|
|
263
266
|
return CheckResult(
|
|
264
267
|
name=name, status=Status.WARN,
|
|
265
268
|
reason=(
|
|
266
|
-
f"registered but {
|
|
267
|
-
f"
|
|
268
|
-
f"
|
|
269
|
+
f"registered but {len(changed_detail)} {field_word} from "
|
|
270
|
+
f"the bundled definition:\n{render_field_diffs(changed_detail)}\n"
|
|
271
|
+
f"run 'ccsched edit {job_id}' to realign, or leave as your "
|
|
272
|
+
f"intentional customization"
|
|
269
273
|
),
|
|
270
274
|
)
|
|
271
275
|
if spec.enabled:
|
{cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/resolve.py
RENAMED
|
@@ -316,7 +316,15 @@ def _apply_resolution(
|
|
|
316
316
|
f"machine is missing a field, matching the other's definition, then retry."
|
|
317
317
|
)
|
|
318
318
|
|
|
319
|
-
|
|
319
|
+
# Scoped to `diff.records` non-empty, not unconditional: a genuine vector-clock fork whose
|
|
320
|
+
# records happen to be byte-identical on both sides (diff.records == []) has nothing for a
|
|
321
|
+
# human to pick between, and `choices={}` correctly names "every record_id in the diff" for
|
|
322
|
+
# that case (there are zero). Rejecting it unconditionally left that case with no way through
|
|
323
|
+
# `apply_resolution` at all — `dump`/`rehydrate` would refuse forever with only `--force` (a
|
|
324
|
+
# blunt overwrite that risks discarding a real edit neither side actually has to lose here)
|
|
325
|
+
# to unblock it. Found 2026-08-31 working the `home` project's fork-testing walkthrough; see
|
|
326
|
+
# CHANGELOG.
|
|
327
|
+
if not choices and diff.records:
|
|
320
328
|
raise ValueError("apply_resolution requires at least one record_id in choices")
|
|
321
329
|
|
|
322
330
|
by_id = {record_diff.record_id: record_diff for record_diff in diff.records}
|
{cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/pdata/verify.py
RENAMED
|
@@ -41,7 +41,7 @@ CREATE TABLE IF NOT EXISTS pdata_verify_runs (
|
|
|
41
41
|
CREATE INDEX IF NOT EXISTS idx_pdata_verify_runs_run_at ON pdata_verify_runs(run_at);
|
|
42
42
|
"""
|
|
43
43
|
|
|
44
|
-
_MAX_RETAINED_RUNS =
|
|
44
|
+
_MAX_RETAINED_RUNS = 100
|
|
45
45
|
_DOUBLE_UPDATE_WINDOW_SECONDS = 6 * 60 * 60 # see plan Decision 4 — not a CLI flag
|
|
46
46
|
|
|
47
47
|
_SEVERITY_ORDER = {"OK": 0, "WARN": 1, "FAIL": 2}
|
|
@@ -10,6 +10,7 @@ deliberately excludes `enabled`: that is per-machine operational state a human t
|
|
|
10
10
|
`_cmd_ccsched_jobs_install` and `doctor.check_ccsched_job_registered`, the two callers."""
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import shlex
|
|
13
14
|
from dataclasses import dataclass
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from typing import TYPE_CHECKING
|
|
@@ -183,23 +184,65 @@ BUNDLED_CCSCHED_JOBS: tuple[BundledJob, ...] = (
|
|
|
183
184
|
)
|
|
184
185
|
|
|
185
186
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
@dataclass(frozen=True, slots=True)
|
|
188
|
+
class FieldDiff:
|
|
189
|
+
"""One field where a registered JobSpec no longer matches its bundled definition,
|
|
190
|
+
carrying both the bundled and the current (live) value, rendered as display-ready
|
|
191
|
+
strings so a caller never has to know a field's underlying type - see
|
|
192
|
+
`diff_from_bundled_detail`."""
|
|
193
|
+
field: str
|
|
194
|
+
bundled: str
|
|
195
|
+
current: str
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def diff_from_bundled_detail(spec: "JobSpec", job: BundledJob) -> tuple[FieldDiff, ...]:
|
|
199
|
+
"""Same comparison as `diff_from_bundled`, but carries the before/after values for
|
|
200
|
+
display, in declaration order; empty if the spec matches its bundled definition
|
|
201
|
+
exactly. Deliberately ignores `enabled` — see module docstring. This is the one place
|
|
202
|
+
that decides both *whether* a field changed and *how* its value is rendered, so
|
|
203
|
+
`_cmd_ccsched_jobs_install` and `check_ccsched_job_registered` — the two callers —
|
|
204
|
+
can never disagree on either question."""
|
|
205
|
+
diffs: list[FieldDiff] = []
|
|
191
206
|
if spec.cadence != job.cadence:
|
|
192
|
-
|
|
207
|
+
diffs.append(FieldDiff("cadence", job.cadence, spec.cadence))
|
|
193
208
|
if spec.coalesce.value != job.coalesce:
|
|
194
|
-
|
|
209
|
+
diffs.append(FieldDiff("coalesce", job.coalesce, spec.coalesce.value))
|
|
195
210
|
if spec.catchup_window != job.catchup_window:
|
|
196
|
-
|
|
211
|
+
diffs.append(FieldDiff("catchup_window", job.catchup_window, spec.catchup_window))
|
|
197
212
|
if spec.timeout != job.timeout:
|
|
198
|
-
|
|
213
|
+
diffs.append(FieldDiff("timeout", job.timeout, spec.timeout))
|
|
199
214
|
if spec.surface != job.surface:
|
|
200
|
-
|
|
215
|
+
diffs.append(FieldDiff("surface", str(job.surface), str(spec.surface)))
|
|
201
216
|
if spec.command != job.command:
|
|
202
|
-
|
|
217
|
+
# shlex.join renders as an actual shell line, not a Python tuple repr - readable
|
|
218
|
+
# for a multi-arg command.
|
|
219
|
+
diffs.append(FieldDiff("command", shlex.join(job.command), shlex.join(spec.command)))
|
|
203
220
|
if spec.success_exit_codes != job.success_exit_codes:
|
|
204
|
-
|
|
205
|
-
|
|
221
|
+
diffs.append(FieldDiff(
|
|
222
|
+
"success_exit_codes",
|
|
223
|
+
", ".join(str(c) for c in job.success_exit_codes),
|
|
224
|
+
", ".join(str(c) for c in spec.success_exit_codes),
|
|
225
|
+
))
|
|
226
|
+
return tuple(diffs)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def diff_from_bundled(spec: "JobSpec", job: BundledJob) -> tuple[str, ...]:
|
|
230
|
+
"""Names of the fields where a registered JobSpec no longer matches its bundled
|
|
231
|
+
definition, in declaration order; empty if it matches exactly. A thin wrapper over
|
|
232
|
+
`diff_from_bundled_detail` so both callers stay guaranteed-identical in what counts
|
|
233
|
+
as "changed" — see module docstring."""
|
|
234
|
+
return tuple(fd.field for fd in diff_from_bundled_detail(spec, job))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def render_field_diffs(diffs: tuple[FieldDiff, ...], *, indent: str = " ") -> str:
|
|
238
|
+
"""Shared before/after rendering for a non-empty `diff_from_bundled_detail` result -
|
|
239
|
+
one `field:` sub-block per entry, in the same order, `-`/`+` prefixed so it reads as a
|
|
240
|
+
diff without pulling in `difflib`. Both `_cmd_ccsched_jobs_install` and
|
|
241
|
+
`check_ccsched_job_registered` call this so they render byte-identical bodies and only
|
|
242
|
+
differ in the header/hint text wrapped around it."""
|
|
243
|
+
lines: list[str] = []
|
|
244
|
+
for d in diffs:
|
|
245
|
+
lines.append(f"{indent}{d.field}:")
|
|
246
|
+
lines.append(f"{indent} - bundled: {d.bundled}")
|
|
247
|
+
lines.append(f"{indent} + current: {d.current}")
|
|
248
|
+
return "\n".join(lines)
|
{cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/digest.py
RENAMED
|
@@ -40,6 +40,11 @@ class JobReport:
|
|
|
40
40
|
# exclusive with `findings`: a given run is either a clean 0-exit or a
|
|
41
41
|
# findings-bearing nonzero exit, never both.
|
|
42
42
|
output: str | None = None
|
|
43
|
+
# RAN with output and count > 1 only: the span from the oldest coalesced
|
|
44
|
+
# run to now (e.g. "11h", "2d"), formatted without an "ago" suffix so it
|
|
45
|
+
# reads as a window ("... in last {window}:") rather than a point in time
|
|
46
|
+
# - see surface.py's per-job clean-output coalescing.
|
|
47
|
+
window: str | None = None
|
|
43
48
|
|
|
44
49
|
|
|
45
50
|
def _ordinal(n: int) -> str:
|
|
@@ -90,7 +95,17 @@ def _line(report: JobReport) -> str | None:
|
|
|
90
95
|
# distinct from the findings/warning case above so a passing check
|
|
91
96
|
# is never mistaken for one that found something. One line-group:
|
|
92
97
|
# never falls through to the bare "✓ ran" case below.
|
|
93
|
-
|
|
98
|
+
if report.count > 1:
|
|
99
|
+
# Several clean-output runs of the same job coalesced within
|
|
100
|
+
# one sweep (§ output-coalesce fix, mirrors the bare-run
|
|
101
|
+
# coalesce below) - one line carrying a count and the window
|
|
102
|
+
# they span, body is the most recent run's output only.
|
|
103
|
+
header = (
|
|
104
|
+
f"✓ ran {report.job_id}{age_suffix} "
|
|
105
|
+
f"({report.count} times in last {report.window}):"
|
|
106
|
+
)
|
|
107
|
+
else:
|
|
108
|
+
header = f"✓ ran {report.job_id}{overdue}{age_suffix}:"
|
|
94
109
|
body = "\n".join(f" {line}" for line in report.output.splitlines())
|
|
95
110
|
return f"{header}\n{body}"
|
|
96
111
|
if report.count > 1:
|
{cc_session_tools-2.12.2 → cc_session_tools-2.12.4}/src/cc_session_tools/lib/scheduler/surface.py
RENAMED
|
@@ -17,6 +17,13 @@ unconditionally rather than the per-job flag, since digest.py never consults
|
|
|
17
17
|
`surface` for Outcome.RAN — the per-job flag is only meaningful for LAUNCH,
|
|
18
18
|
where it is still looked up via `_surface_flag`.
|
|
19
19
|
|
|
20
|
+
A clean (0-exit) run's captured stdout coalesces per job_id the same way bare
|
|
21
|
+
routine runs do: several such runs for the same job within one digest render
|
|
22
|
+
as one line carrying a count, the span they cover, and only the MOST RECENT
|
|
23
|
+
run's output (§ output-coalesce fix) — see `_output_reports`. Findings
|
|
24
|
+
(nonzero-exit captured stdout) are exempt, same as FAILED/SUSPENDED: they are
|
|
25
|
+
signal, never folded, always replayed individually with their own age suffix.
|
|
26
|
+
|
|
20
27
|
`surface(..., lookback=...)` optionally widens the read below the session's
|
|
21
28
|
own cursor to also pick up ledger activity within `lookback` of `now`, for the
|
|
22
29
|
SessionStart 24h rolling lookback (§ widen fix) — see catchup.py, the only
|
|
@@ -81,6 +88,19 @@ def _format_age(sent: datetime | None, now: datetime) -> str:
|
|
|
81
88
|
return f"{hours // 24}d ago"
|
|
82
89
|
|
|
83
90
|
|
|
91
|
+
def _format_span(oldest: datetime, now: datetime) -> str:
|
|
92
|
+
"""Same buckets as `_format_age`, but as a window ("11h") rather than a
|
|
93
|
+
point in time ("11h ago") - for the "N times in last {span}" wording on
|
|
94
|
+
coalesced clean-output runs."""
|
|
95
|
+
minutes = max(1, int((now - oldest).total_seconds() // 60))
|
|
96
|
+
if minutes < 60:
|
|
97
|
+
return f"{minutes}m"
|
|
98
|
+
hours = minutes // 60
|
|
99
|
+
if hours < 24:
|
|
100
|
+
return f"{hours}h"
|
|
101
|
+
return f"{hours // 24}d"
|
|
102
|
+
|
|
103
|
+
|
|
84
104
|
# Sort key for an entry whose ts is missing/unparseable - sorts first, same
|
|
85
105
|
# convention as `_format_age`'s "unknown time ago" treating it as maximally old.
|
|
86
106
|
_UNKNOWN_TS = datetime.min.replace(tzinfo=timezone.utc)
|
|
@@ -161,6 +181,51 @@ def _routine_reports(
|
|
|
161
181
|
return out
|
|
162
182
|
|
|
163
183
|
|
|
184
|
+
def _output_reports(
|
|
185
|
+
entries: list[dict[str, object]], *, now: datetime,
|
|
186
|
+
) -> list[tuple[datetime, JobReport]]:
|
|
187
|
+
"""Clean-output (0-exit, captured stdout) RUN/BACKFILL entries, grouped by
|
|
188
|
+
job_id. A job_id with only one entry renders exactly as before (no count
|
|
189
|
+
suffix). A job_id with several entries within one digest coalesces to one
|
|
190
|
+
line carrying a count, the span the group covers, and only the MOST
|
|
191
|
+
RECENT entry's output - not N near-identical output blocks for the same
|
|
192
|
+
job (§ output-coalesce fix). Findings-bearing (nonzero-exit) entries never
|
|
193
|
+
reach this function - they are signal, not routine noise, and always
|
|
194
|
+
replay individually (see the caller)."""
|
|
195
|
+
groups: dict[str, list[dict[str, object]]] = {}
|
|
196
|
+
for e in entries:
|
|
197
|
+
groups.setdefault(str(e.get("job_id", "")), []).append(e)
|
|
198
|
+
|
|
199
|
+
out: list[tuple[datetime, JobReport]] = []
|
|
200
|
+
for job_id, group in groups.items():
|
|
201
|
+
timestamps = [_parse_ts(g.get("ts")) for g in group]
|
|
202
|
+
latest = max((t for t in timestamps if t is not None), default=None)
|
|
203
|
+
oldest = min((t for t in timestamps if t is not None), default=None)
|
|
204
|
+
sort_ts = latest if latest is not None else _UNKNOWN_TS
|
|
205
|
+
if len(group) == 1:
|
|
206
|
+
e = group[0]
|
|
207
|
+
out.append((sort_ts, JobReport(
|
|
208
|
+
job_id=job_id, outcome=Outcome.RAN, surface=True, overdue="",
|
|
209
|
+
ran=int(cast(int, e.get("ran", 0)) or 0), deferred=0, expired=0,
|
|
210
|
+
consecutive_failures=0, age=_format_age(latest, now),
|
|
211
|
+
output=str(e.get("error")),
|
|
212
|
+
)))
|
|
213
|
+
continue
|
|
214
|
+
# Most recent entry by ts (fall back to list order for unparseable ts).
|
|
215
|
+
latest_entry = max(
|
|
216
|
+
group, key=lambda g: (_parse_ts(g.get("ts")) or _UNKNOWN_TS),
|
|
217
|
+
)
|
|
218
|
+
window = _format_span(oldest, now) if oldest is not None else "unknown time"
|
|
219
|
+
out.append((sort_ts, JobReport(
|
|
220
|
+
job_id=job_id, outcome=Outcome.RAN, surface=True, overdue="",
|
|
221
|
+
ran=sum(int(cast(int, g.get("ran", 0)) or 0) for g in group),
|
|
222
|
+
deferred=0, expired=0, consecutive_failures=0,
|
|
223
|
+
age=_format_age(latest, now), count=len(group),
|
|
224
|
+
output=str(latest_entry.get("error")), window=window,
|
|
225
|
+
)))
|
|
226
|
+
return out
|
|
227
|
+
|
|
228
|
+
|
|
164
229
|
def surface(
|
|
165
230
|
*, session_uuid: str, now: datetime, lookback: timedelta | None = None,
|
|
166
231
|
) -> SurfaceResult:
|
|
@@ -185,6 +250,7 @@ def surface(
|
|
|
185
250
|
# them.
|
|
186
251
|
pairs: list[tuple[datetime, JobReport]] = []
|
|
187
252
|
routine: list[dict[str, object]] = []
|
|
253
|
+
clean_output: list[dict[str, object]] = []
|
|
188
254
|
for e in entries:
|
|
189
255
|
event = str(e.get("event", ""))
|
|
190
256
|
job_id = str(e.get("job_id", ""))
|
|
@@ -218,20 +284,23 @@ def surface(
|
|
|
218
284
|
captured = e.get("error")
|
|
219
285
|
if captured:
|
|
220
286
|
is_warning = raw_exit not in (0, None)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
287
|
+
if is_warning:
|
|
288
|
+
# Findings are signal, not routine noise - never coalesced,
|
|
289
|
+
# always replayed individually (see module docstring).
|
|
290
|
+
# surface=True unconditionally: a completed run is always
|
|
291
|
+
# visible, so the per-job flag is not load-bearing here.
|
|
292
|
+
pairs.append((sort_ts, JobReport(
|
|
293
|
+
job_id=job_id, outcome=Outcome.RAN, surface=True, overdue="",
|
|
294
|
+
ran=int(cast(int, e.get("ran", 0)) or 0), deferred=0, expired=0,
|
|
295
|
+
consecutive_failures=0, findings=str(captured),
|
|
296
|
+
age=_format_age(sent, now),
|
|
297
|
+
)))
|
|
298
|
+
else:
|
|
299
|
+
# A clean (0-exit) run's captured stdout - eligible for
|
|
300
|
+
# per-job_id coalescing (§ output-coalesce fix), same
|
|
301
|
+
# spirit as the bare-run routine fold below but keeping
|
|
302
|
+
# the most recent output rather than dropping it.
|
|
303
|
+
clean_output.append(e)
|
|
235
304
|
else:
|
|
236
305
|
# A bare clean run with no captured stdout - still eligible
|
|
237
306
|
# for the routine backlog fold (spam control, not the
|
|
@@ -249,6 +318,7 @@ def surface(
|
|
|
249
318
|
# skip_expired and defer events are not surfaced as standalone lines.
|
|
250
319
|
|
|
251
320
|
pairs.extend(_routine_reports(routine, now=now))
|
|
321
|
+
pairs.extend(_output_reports(clean_output, now=now))
|
|
252
322
|
pairs.sort(key=lambda p: p[0])
|
|
253
323
|
|
|
254
324
|
cursor.write_cursor(session_uuid, new_offset)
|
|
@@ -93,7 +93,14 @@ def test_changed_job_is_reported_not_touched_and_not_overwritten(base_env):
|
|
|
93
93
|
|
|
94
94
|
r = _run(base_env, "ccsched-jobs", "install", "--apply")
|
|
95
95
|
assert r.returncode == 0, r.stderr
|
|
96
|
-
assert
|
|
96
|
+
assert (
|
|
97
|
+
"changed (not touched): telemetry-trim-weekly - 1 field differs "
|
|
98
|
+
"from the bundled definition:" in r.stdout
|
|
99
|
+
)
|
|
100
|
+
assert "timeout:" in r.stdout
|
|
101
|
+
assert "- bundled: 60s" in r.stdout
|
|
102
|
+
assert "+ current: 9s" in r.stdout
|
|
103
|
+
assert "run 'ccsched edit telemetry-trim-weekly' to realign" in r.stdout
|
|
97
104
|
assert "already registered: telemetry-trim-weekly" not in r.stdout
|
|
98
105
|
|
|
99
106
|
show = subprocess.run(
|
|
@@ -1207,6 +1207,34 @@ def test_check_ccsched_job_registered_warns_on_drift_from_bundled_definition(
|
|
|
1207
1207
|
assert "timeout" in result.reason
|
|
1208
1208
|
|
|
1209
1209
|
|
|
1210
|
+
def test_check_ccsched_job_registered_shows_before_and_after_values_not_just_field_name(
|
|
1211
|
+
monkeypatch, tmp_path
|
|
1212
|
+
) -> None:
|
|
1213
|
+
"""§ diff-detail fix: the reason must carry the actual bundled/current values, not just
|
|
1214
|
+
name the field that differs — no more 'run install --apply to see the diff' indirection."""
|
|
1215
|
+
monkeypatch.setenv("CCST_DATA_HOME", str(tmp_path))
|
|
1216
|
+
from cc_session_tools.lib.scheduler import bundled_jobs, registry
|
|
1217
|
+
from cc_session_tools.lib.scheduler.jobspec import validate_job_fields
|
|
1218
|
+
|
|
1219
|
+
job = next(
|
|
1220
|
+
j for j in bundled_jobs.BUNDLED_CCSCHED_JOBS if j.job_id == "telemetry-trim-weekly"
|
|
1221
|
+
)
|
|
1222
|
+
spec = validate_job_fields(
|
|
1223
|
+
job_id=job.job_id, cadence=job.cadence, coalesce=job.coalesce,
|
|
1224
|
+
command=list(job.command), surface=job.surface, enabled=True,
|
|
1225
|
+
catchup_window=job.catchup_window, timeout="9s",
|
|
1226
|
+
success_exit_codes=job.success_exit_codes,
|
|
1227
|
+
)
|
|
1228
|
+
registry.add_job(spec)
|
|
1229
|
+
|
|
1230
|
+
result = check_ccsched_job_registered(job.job_id, expected=job)
|
|
1231
|
+
assert "1 field differ" in result.reason
|
|
1232
|
+
assert "- bundled: 60s" in result.reason
|
|
1233
|
+
assert "+ current: 9s" in result.reason
|
|
1234
|
+
assert "run 'ccsched edit telemetry-trim-weekly' to realign" in result.reason
|
|
1235
|
+
assert "install --apply' to see the diff" not in result.reason
|
|
1236
|
+
|
|
1237
|
+
|
|
1210
1238
|
def test_check_ccsched_job_registered_ok_when_matching_bundled_definition(
|
|
1211
1239
|
monkeypatch, tmp_path
|
|
1212
1240
|
) -> None:
|
|
@@ -390,11 +390,65 @@ def test_resolve_diagnostic_mode_clean_when_no_diff(base_env):
|
|
|
390
390
|
_build_local("proj", vector={"ltxy": 1})
|
|
391
391
|
dump_pub = _run(base_env, "pdata", "dump", "--project", "proj")
|
|
392
392
|
assert dump_pub.returncode == 0, dump_pub.stderr
|
|
393
|
+
published = dump.read_latest(store.project_root("proj"))
|
|
393
394
|
|
|
394
395
|
r = _run(base_env, "pdata", "resolve", "--project", "proj")
|
|
395
396
|
|
|
396
397
|
assert r.returncode == 0, r.stdout
|
|
397
398
|
assert "clean" in r.stdout
|
|
399
|
+
assert "auto-closed" not in r.stdout
|
|
400
|
+
# No fork exists here (local published its own dump, nothing to converge) - diagnose mode
|
|
401
|
+
# must stay side-effect-free in this, the overwhelmingly common case: same vector, same
|
|
402
|
+
# dumped_at, i.e. nothing was silently re-published underneath the "clean" message.
|
|
403
|
+
still_published = dump.read_latest(store.project_root("proj"))
|
|
404
|
+
assert still_published.vector == published.vector
|
|
405
|
+
assert still_published.dumped_at == published.dumped_at
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def test_resolve_diagnostic_mode_closes_a_fork_automatically_when_content_already_matches(
|
|
409
|
+
base_env,
|
|
410
|
+
):
|
|
411
|
+
"""Reproduces the real dead end found 2026-08-31 (`home` project's fork-testing walkthrough):
|
|
412
|
+
two machines each made a local write since they last synced (a genuine fork - neither vector
|
|
413
|
+
dominates), but the resulting content is identical on both sides, so there is nothing for a
|
|
414
|
+
human to choose between. `ccst pdata resolve` must close it automatically rather than report
|
|
415
|
+
"clean" while leaving `dump`/`rehydrate` refusing forever with only `--force` to unblock it."""
|
|
416
|
+
_build_local("proj", content="same-content", vector={"ltxy": 1})
|
|
417
|
+
project_root = store.project_root("proj")
|
|
418
|
+
_publish_remote_dump(
|
|
419
|
+
project_root, remote_project="proj-remote", content="same-content",
|
|
420
|
+
vector={"mbp": 1}, machine_id="mbp",
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
# Confirm the fork is real before resolving it - `dump` refuses.
|
|
424
|
+
precheck = _run(base_env, "pdata", "dump", "--project", "proj")
|
|
425
|
+
assert precheck.returncode == 1
|
|
426
|
+
|
|
427
|
+
r = _run(base_env, "pdata", "resolve", "--project", "proj")
|
|
428
|
+
|
|
429
|
+
assert r.returncode == 0, r.stdout
|
|
430
|
+
assert "clean" in r.stdout
|
|
431
|
+
assert "auto-closed" in r.stdout or "closed the vector-clock fork automatically" in r.stdout
|
|
432
|
+
|
|
433
|
+
# The fork is genuinely gone now, not just hidden - both directions work without --force.
|
|
434
|
+
postcheck = _run(base_env, "pdata", "dump", "--project", "proj")
|
|
435
|
+
assert postcheck.returncode == 0, postcheck.stderr
|
|
436
|
+
info = dump.read_latest(project_root)
|
|
437
|
+
assert info.vector == {"ltxy": 2, "mbp": 1}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def test_resolve_diagnostic_all_projects_reports_auto_closed_fork(base_env):
|
|
441
|
+
_build_local("proj", content="same-content", vector={"ltxy": 1})
|
|
442
|
+
project_root = store.project_root("proj")
|
|
443
|
+
_publish_remote_dump(
|
|
444
|
+
project_root, remote_project="proj-remote", content="same-content",
|
|
445
|
+
vector={"mbp": 1}, machine_id="mbp",
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
r = _run(base_env, "pdata", "resolve", "--all-projects")
|
|
449
|
+
|
|
450
|
+
assert r.returncode == 0, r.stdout
|
|
451
|
+
assert "proj: clean (fork auto-closed)" in r.stdout
|
|
398
452
|
|
|
399
453
|
|
|
400
454
|
def test_resolve_diagnostic_mode_reports_outstanding_records_and_schema_fields(base_env):
|
|
@@ -3,7 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
from cc_session_tools.lib.scheduler import bundled_jobs
|
|
6
|
-
from cc_session_tools.lib.scheduler.bundled_jobs import
|
|
6
|
+
from cc_session_tools.lib.scheduler.bundled_jobs import (
|
|
7
|
+
diff_from_bundled, diff_from_bundled_detail, render_field_diffs,
|
|
8
|
+
)
|
|
7
9
|
from cc_session_tools.lib.scheduler.jobspec import validate_job_fields
|
|
8
10
|
|
|
9
11
|
|
|
@@ -209,3 +211,60 @@ def test_diff_from_bundled_detects_a_changed_command():
|
|
|
209
211
|
job = _job("pdata-verify-all")
|
|
210
212
|
spec = _spec_for(job, command=["ccst", "pdata", "verify", "--project", "x"])
|
|
211
213
|
assert diff_from_bundled(spec, job) == ("command",)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_diff_from_bundled_detail_is_empty_when_nothing_differs():
|
|
217
|
+
job = _job("pdata-verify-all")
|
|
218
|
+
assert diff_from_bundled_detail(_spec_for(job), job) == ()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_diff_from_bundled_detail_carries_before_and_after_for_one_field():
|
|
222
|
+
job = _job("pdata-verify-all")
|
|
223
|
+
spec = _spec_for(job, timeout="30s")
|
|
224
|
+
diffs = diff_from_bundled_detail(spec, job)
|
|
225
|
+
assert len(diffs) == 1
|
|
226
|
+
assert diffs[0].field == "timeout"
|
|
227
|
+
assert diffs[0].bundled == job.timeout
|
|
228
|
+
assert diffs[0].current == "30s"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_diff_from_bundled_detail_carries_every_differing_field_in_order():
|
|
232
|
+
job = _job("pdata-verify-all")
|
|
233
|
+
spec = _spec_for(job, cadence="every:3d", timeout="30s")
|
|
234
|
+
diffs = diff_from_bundled_detail(spec, job)
|
|
235
|
+
assert [d.field for d in diffs] == ["cadence", "timeout"]
|
|
236
|
+
assert diffs[0].bundled == job.cadence
|
|
237
|
+
assert diffs[0].current == "every:3d"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_diff_from_bundled_detail_renders_command_as_a_shell_line_not_a_tuple_repr():
|
|
241
|
+
job = _job("pdata-verify-all")
|
|
242
|
+
spec = _spec_for(job, command=["ccst", "pdata", "verify", "--project", "x y"])
|
|
243
|
+
diffs = diff_from_bundled_detail(spec, job)
|
|
244
|
+
assert len(diffs) == 1
|
|
245
|
+
assert diffs[0].field == "command"
|
|
246
|
+
assert diffs[0].bundled == "ccst pdata verify --all-projects"
|
|
247
|
+
# shlex.join quotes the argument containing a space, unlike a bare join(" ")
|
|
248
|
+
assert diffs[0].current == "ccst pdata verify --project 'x y'"
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_diff_from_bundled_detail_is_the_single_source_diff_from_bundled_wraps():
|
|
252
|
+
"""diff_from_bundled must return exactly the field names diff_from_bundled_detail
|
|
253
|
+
reports, in the same order — the two can never disagree since one wraps the other."""
|
|
254
|
+
job = _job("pdata-verify-all")
|
|
255
|
+
spec = _spec_for(job, cadence="every:3d", timeout="30s")
|
|
256
|
+
assert diff_from_bundled(spec, job) == tuple(
|
|
257
|
+
d.field for d in diff_from_bundled_detail(spec, job)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def test_render_field_diffs_shows_dash_bundled_plus_current_per_field():
|
|
262
|
+
job = _job("pdata-verify-all")
|
|
263
|
+
spec = _spec_for(job, cadence="every:3d", timeout="30s")
|
|
264
|
+
out = render_field_diffs(diff_from_bundled_detail(spec, job))
|
|
265
|
+
assert "cadence:" in out
|
|
266
|
+
assert "- bundled: daily@03:00" in out
|
|
267
|
+
assert "+ current: every:3d" in out
|
|
268
|
+
assert "timeout:" in out
|
|
269
|
+
assert "- bundled: 300s" in out
|
|
270
|
+
assert "+ current: 30s" in out
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|