@ngocsangairvds/vsaf 3.1.27 → 3.2.1
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.
- package/package.json +2 -2
- package/src/global.js +70 -10
- package/tools/skills/vds-scripts-skill/.openskills.json +6 -0
- package/tools/skills/vds-scripts-skill/QUALITY.md +44 -0
- package/tools/skills/vds-scripts-skill/SKILL.md +135 -0
- package/tools/skills/vds-scripts-skill/references/audit-commands.md +171 -0
- package/tools/skills/vds-scripts-skill/references/capability-index.md +34 -0
- package/tools/skills/vds-scripts-skill/references/development-commands.md +12 -0
- package/tools/skills/vds-scripts-skill/references/google-sheets.md +73 -0
- package/tools/skills/vds-scripts-skill/references/integration-commands.md +17 -0
- package/tools/skills/vds-scripts-skill/references/platform-bootstrap.md +31 -0
- package/tools/skills/vds-scripts-skill/references/specialist-routing.md +14 -0
- package/tools/skills/vds-scripts-skill/references/validation-commands.md +15 -0
- package/tools/skills/vsaf-build/SKILL.md +32 -2
- package/tools/skills/vsaf-ship/SKILL.md +41 -10
- package/tools/skills/vsaf-test/SKILL.md +8 -0
- package/tools/vds-scripts/.mcp.json +11 -0
- package/tools/vds-scripts/.secrets.baseline +133 -0
- package/tools/vds-scripts/AGENTS.md +152 -0
- package/tools/vds-scripts/CLAUDE.md +101 -0
- package/tools/vds-scripts/CLI_COMMAND_OPTIMIZATION.md +156 -0
- package/tools/vds-scripts/PACKAGE_P125B_IMPLEMENTATION_SUMMARY.md +131 -0
- package/tools/vds-scripts/PROJECT_COMPLETION_SUMMARY.md +45 -0
- package/tools/vds-scripts/README.md +97 -0
- package/tools/vds-scripts/bitbucket_manifest_mapping.toml +34 -0
- package/tools/vds-scripts/bitbucket_orchestrator/ARCHITECTURE_ANALYSIS.md +258 -0
- package/tools/vds-scripts/bitbucket_orchestrator/BITBUCKET_API_PRACTICES.md +393 -0
- package/tools/vds-scripts/bitbucket_orchestrator/EVALUATION_REPORT.md +61 -0
- package/tools/vds-scripts/bitbucket_orchestrator/FEATURES.md +908 -0
- package/tools/vds-scripts/bitbucket_orchestrator/README.md +687 -0
- package/tools/vds-scripts/bitbucket_orchestrator/pyproject.toml +40 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/__init__.py +20 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/async_client.py +657 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/cli.py +2108 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/client.py +2534 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/config.py +171 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/errors.py +67 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/factory.py +185 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/protocols.py +244 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/__init__.py +8 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/conftest.py +65 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_advanced_search.py +151 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_async_client.py +546 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_branch_permissions.py +145 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_cli.py +115 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client.py +157 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_branch_conditions.py +79 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_code_advanced.py +163 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_code_file.py +32 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_deployment_environments.py +194 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_issues.py +164 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_pipelines_advanced.py +179 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_pr_blockers.py +119 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_repository_variables.py +156 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code.py +98 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code_advanced.py +282 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code_insights.py +335 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_conditions.py +147 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_config.py +131 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_deployment_env.py +352 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_factory.py +371 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_fork_operations.py +204 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_issue_cli.py +261 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_pipeline_advanced.py +270 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_pr_blocker.py +204 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_protocols.py +334 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_repo_settings.py +343 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_repo_variables.py +270 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_webhooks.py +189 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_workspace.py +233 -0
- package/tools/vds-scripts/bitbucket_orchestrator/uv.lock +742 -0
- package/tools/vds-scripts/confluence_orchestrator/Dockerfile +19 -0
- package/tools/vds-scripts/confluence_orchestrator/README.md +412 -0
- package/tools/vds-scripts/confluence_orchestrator/SYNC_SCRIPTS.md +127 -0
- package/tools/vds-scripts/confluence_orchestrator/SYNC_STANDARDIZATION.md +108 -0
- package/tools/vds-scripts/confluence_orchestrator/pyproject.toml +48 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/__init__.py +20 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/cli.py +2532 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/config.py +175 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/content.py +290 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/content_v2.py +94 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/crawl_tree.py +1835 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/errors.py +80 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/eventing.py +109 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/http.py +1114 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/orchestration.py +165 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/reporting.py +78 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/tree.py +121 -0
- package/tools/vds-scripts/confluence_orchestrator/sync_pdfs_from_markdown.py +213 -0
- package/tools/vds-scripts/confluence_orchestrator/sync_pdfs_to_confluence.py +305 -0
- package/tools/vds-scripts/confluence_orchestrator/sync_png_attachments.py +305 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/__init__.py +0 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/conftest.py +8 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_advanced_content.py +224 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_advanced_search.py +188 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_cache_management.py +247 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_cli.py +499 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_config.py +83 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_content.py +186 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_content_flags.py +27 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_crawl_tree.py +2250 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_draft_management.py +223 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing.py +71 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_chaos.py +37 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_rate_limit.py +44 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_timeout.py +49 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_export.py +230 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_history.py +204 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_http.py +117 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_orchestration.py +91 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_reporting.py +24 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_search_cql.py +34 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_space_management.py +237 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_space_permissions.py +332 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_user_group_management.py +388 -0
- package/tools/vds-scripts/confluence_orchestrator/uv.lock +1023 -0
- package/tools/vds-scripts/git_orchestrator/ENHANCEMENT_SUMMARY.md +119 -0
- package/tools/vds-scripts/git_orchestrator/README.md +280 -0
- package/tools/vds-scripts/git_orchestrator/VERIFICATION_REPORT.md +152 -0
- package/tools/vds-scripts/git_orchestrator/pyproject.toml +35 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/__init__.py +7 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/__main__.py +4 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/cli.py +847 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/logging_config.py +63 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/manifest.py +129 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/orchestrator.py +819 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/reporting.py +53 -0
- package/tools/vds-scripts/git_orchestrator/tests/__init__.py +0 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_cli_settings.py +21 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_integration.py +74 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_manifest.py +79 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_orchestrator.py +204 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_public_api.py +236 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_resilience.py +345 -0
- package/tools/vds-scripts/git_orchestrator/uv.lock +271 -0
- package/tools/vds-scripts/jira_orchestrator/README.md +770 -0
- package/tools/vds-scripts/jira_orchestrator/pyproject.toml +39 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/__init__.py +1 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/adapter.py +1320 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/cli.py +2271 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/config.py +138 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/errors.py +67 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/reporting.py +65 -0
- package/tools/vds-scripts/jira_orchestrator/tests/__init__.py +1 -0
- package/tools/vds-scripts/jira_orchestrator/tests/conftest.py +86 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_agile_list_payloads.py +54 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_bulk_operations.py +69 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_components.py +57 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_createmeta.py +45 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_dashboard.py +117 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_issue_properties.py +54 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_permissions_compat.py +42 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_reindex.py +42 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_remote_links.py +76 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_transitions.py +91 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_user_management.py +110 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_version_management.py +133 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_watchers.py +41 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_advanced_search.py +164 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_agile.py +256 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_application_properties.py +193 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_backlog.py +91 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_bulk_operations.py +277 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_cli.py +106 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_components.py +106 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_config.py +164 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_dashboard.py +122 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_discover_fields.py +207 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_filter_management.py +333 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_issue_archiving.py +164 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_issue_links.py +257 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_issue_properties.py +171 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_link_types.py +314 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_parse_set.py +37 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_permissions.py +273 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_reindex.py +81 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_remote_links.py +254 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_security_schemes.py +170 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_transitions_changelog.py +114 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_user_management.py +226 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_version_management.py +339 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_watchers.py +101 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_worklog.py +223 -0
- package/tools/vds-scripts/jira_orchestrator/uv.lock +738 -0
- package/tools/vds-scripts/mcp_server/Dockerfile +34 -0
- package/tools/vds-scripts/mcp_server/README.md +140 -0
- package/tools/vds-scripts/mcp_server/pyproject.toml +42 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/__init__.py +4 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/config.py +36 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/server.py +66 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/__init__.py +14 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/bitbucket_tools.py +47 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/confluence_tools.py +59 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/git_tools.py +71 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/jira_tools.py +63 -0
- package/tools/vds-scripts/mcp_server/tests/__init__.py +2 -0
- package/tools/vds-scripts/mcp_server/tests/conftest.py +29 -0
- package/tools/vds-scripts/mcp_server/tests/unit/__init__.py +2 -0
- package/tools/vds-scripts/mcp_server/tests/unit/test_bitbucket_tools.py +25 -0
- package/tools/vds-scripts/mcp_server/tests/unit/test_confluence_tools.py +25 -0
- package/tools/vds-scripts/mcp_server/tests/unit/test_git_tools.py +32 -0
- package/tools/vds-scripts/mcp_server/tests/unit/test_jira_tools.py +32 -0
- package/tools/vds-scripts/mcp_server/tests/verification/__init__.py +2 -0
- package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_confluence_tools.py +40 -0
- package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_jira_tools.py +37 -0
- package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_tool_registration.py +47 -0
- package/tools/vds-scripts/mcp_server/uv.lock +1032 -0
- package/tools/vds-scripts/mypy.ini +5 -0
- package/tools/vds-scripts/pyproject.toml +29 -0
- package/tools/vds-scripts/repo-manifest.yaml +273 -0
- package/tools/vds-scripts/repo-manifest.yaml.example +25 -0
- package/tools/vds-scripts/scripts/BRD-Validation-API.postman_collection.json +706 -0
- package/tools/vds-scripts/scripts/BRD-Validation-README.md +308 -0
- package/tools/vds-scripts/scripts/README.md +162 -0
- package/tools/vds-scripts/scripts/bootstrap_uv.sh +30 -0
- package/tools/vds-scripts/scripts/brd-validation-environment.json +51 -0
- package/tools/vds-scripts/scripts/brd-validation-test-results.json +13023 -0
- package/tools/vds-scripts/scripts/brd_coverage_report.json +276 -0
- package/tools/vds-scripts/scripts/create_memory_session.py +35 -0
- package/tools/vds-scripts/scripts/deployment/load_docker_images_offline.sh +90 -0
- package/tools/vds-scripts/scripts/final_completion_report.md +139 -0
- package/tools/vds-scripts/scripts/folder_structure_report.json +321 -0
- package/tools/vds-scripts/scripts/generate_completion_report.py +125 -0
- package/tools/vds-scripts/scripts/generate_intellij_modules.py +150 -0
- package/tools/vds-scripts/scripts/link_integrity_report.json +807 -0
- package/tools/vds-scripts/scripts/move_audit_artifact_pages.py +255 -0
- package/tools/vds-scripts/scripts/move_audit_artifact_pages_rest.py +165 -0
- package/tools/vds-scripts/scripts/move_wrong_dept_pages.py +216 -0
- package/tools/vds-scripts/scripts/save_intellij_memories.py +120 -0
- package/tools/vds-scripts/scripts/save_memories_to_vds_ai.py +83 -0
- package/tools/vds-scripts/scripts/save_memories_vds_style.py +129 -0
- package/tools/vds-scripts/scripts/search_intellij_memories.py +50 -0
- package/tools/vds-scripts/scripts/setup_intellij_workspace.py +65 -0
- package/tools/vds-scripts/scripts/target-state-automation/README.md +89 -0
- package/tools/vds-scripts/scripts/target-state-automation/confluence_sync_coordinator.sh +27 -0
- package/tools/vds-scripts/scripts/target-state-automation/coordination.sh +114 -0
- package/tools/vds-scripts/scripts/target-state-automation/diagram_coordinator.sh +25 -0
- package/tools/vds-scripts/scripts/target-state-automation/docs_root.sh +22 -0
- package/tools/vds-scripts/scripts/target-state-automation/generate_diagrams.sh +22 -0
- package/tools/vds-scripts/scripts/target-state-automation/markdown_coordinator.sh +25 -0
- package/tools/vds-scripts/scripts/target-state-automation/progress_dashboard.sh +17 -0
- package/tools/vds-scripts/scripts/target-state-automation/schema_coordinator.sh +25 -0
- package/tools/vds-scripts/scripts/target-state-automation/sync_confluence.sh +30 -0
- package/tools/vds-scripts/scripts/target-state-automation/update_dependencies.sh +19 -0
- package/tools/vds-scripts/scripts/target-state-automation/validate_links.sh +86 -0
- package/tools/vds-scripts/scripts/target-state-automation/validate_markdown.sh +52 -0
- package/tools/vds-scripts/scripts/target-state-automation/validate_schemas.sh +26 -0
- package/tools/vds-scripts/scripts/target-state-automation/validate_structure.sh +98 -0
- package/tools/vds-scripts/scripts/update_modules_xml.py +190 -0
- package/tools/vds-scripts/scripts/uv-workspace-alignment-verification-2026-03-25.md +128 -0
- package/tools/vds-scripts/scripts/validate_brd_coverage.py +179 -0
- package/tools/vds-scripts/scripts/validate_folder_structure.py +240 -0
- package/tools/vds-scripts/scripts/validate_link_integrity.py +272 -0
- package/tools/vds-scripts/scripts/vds_sh_helpers.sh +180 -0
- package/tools/vds-scripts/scripts/verification/phase2_portable_paths_ubuntu_docker.sh +26 -0
- package/tools/vds-scripts/scripts/worktree_uv.sh +48 -0
- package/tools/vds-scripts/uv.lock +8 -0
- package/tools/vds-scripts/vds_cli/README.md +126 -0
- package/tools/vds-scripts/vds_cli/VERIFICATION_REPORT.md +41 -0
- package/tools/vds-scripts/vds_cli/pyproject.toml +38 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/__init__.py +3 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/cli.py +173 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/docs_sync.py +1203 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/env.py +41 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/google_sheets_orchestrator/__init__.py +3 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/google_sheets_orchestrator/google_sheets_orchestrator.py +198 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/router.py +93 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/sync_api.py +647 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/sync_service.py +266 -0
- package/tools/vds-scripts/vds_cli/tests/__init__.py +2 -0
- package/tools/vds-scripts/vds_cli/tests/conftest.py +49 -0
- package/tools/vds-scripts/vds_cli/tests/unit/__init__.py +2 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_cli.py +143 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_docs_sync.py +422 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_env.py +51 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_router.py +72 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_sync_api.py +357 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_sync_service.py +160 -0
- package/tools/vds-scripts/vds_cli/tests/verification/__init__.py +2 -0
- package/tools/vds-scripts/vds_cli/tests/verification/test_bitbucket_real.py +33 -0
- package/tools/vds-scripts/vds_cli/tests/verification/test_confluence_real.py +35 -0
- package/tools/vds-scripts/vds_cli/tests/verification/test_jira_real.py +41 -0
- package/tools/vds-scripts/vds_cli/uv.lock +524 -0
- package/tools/vds-scripts/vds_cli_common/README.md +190 -0
- package/tools/vds-scripts/vds_cli_common/pyproject.toml +92 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/__init__.py +34 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/completers.py +139 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/context.py +201 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/env.py +119 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/errors.py +318 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/output.py +284 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/paths.py +78 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/testing.py +213 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/version.py +85 -0
- package/tools/vds-scripts/vds_cli_common/tests/__init__.py +1 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_completers.py +148 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_context.py +192 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_env.py +102 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_errors.py +186 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_output.py +229 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_paths.py +61 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_testing.py +138 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_version.py +64 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"""Unit tests for multi-writer instruction sync engine."""
|
|
2
|
+
|
|
3
|
+
import errno
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
import vds_cli.docs_sync as docs_sync
|
|
9
|
+
from vds_cli.docs_sync import (
|
|
10
|
+
DocsSyncEngine,
|
|
11
|
+
HttpCentralStateBackend,
|
|
12
|
+
LocalJsonCentralStateBackend,
|
|
13
|
+
SyncConfig,
|
|
14
|
+
SyncError,
|
|
15
|
+
_atomic_write_text,
|
|
16
|
+
_build_backend,
|
|
17
|
+
)
|
|
18
|
+
from vds_cli.sync_api import (
|
|
19
|
+
ConfluencePropertySyncApiClient,
|
|
20
|
+
SyncApiDocument,
|
|
21
|
+
SyncApiTransientError,
|
|
22
|
+
VdsAiMemorySyncApiClient,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _write_targets(repo_root: Path, *, agents: str, claude: str, copilot: str) -> None:
|
|
27
|
+
(repo_root / ".github").mkdir(parents=True, exist_ok=True)
|
|
28
|
+
(repo_root / "AGENTS.md").write_text(agents, encoding="utf-8")
|
|
29
|
+
(repo_root / "CLAUDE.md").write_text(claude, encoding="utf-8")
|
|
30
|
+
(repo_root / ".github" / "copilot-instructions.md").write_text(copilot, encoding="utf-8")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _config(repo_root: Path) -> SyncConfig:
|
|
34
|
+
return SyncConfig(
|
|
35
|
+
repo_root=repo_root,
|
|
36
|
+
reconcile_interval_seconds=60,
|
|
37
|
+
debounce_ms=100,
|
|
38
|
+
log_json=False,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.parametrize(
|
|
43
|
+
("edited_relpath", "updated"),
|
|
44
|
+
[
|
|
45
|
+
("AGENTS.md", "# Shared instructions\n\n- updated from AGENTS\n"),
|
|
46
|
+
("CLAUDE.md", "# Shared instructions\n\n- updated from CLAUDE\n"),
|
|
47
|
+
(
|
|
48
|
+
".github/copilot-instructions.md",
|
|
49
|
+
"# Shared instructions\n\n- updated from copilot file\n",
|
|
50
|
+
),
|
|
51
|
+
],
|
|
52
|
+
)
|
|
53
|
+
def test_any_source_file_edit_converges_all_targets(
|
|
54
|
+
tmp_path: Path,
|
|
55
|
+
edited_relpath: str,
|
|
56
|
+
updated: str,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Editing any of the 3 source files should converge all targets."""
|
|
59
|
+
base = "# Shared instructions\n\n- baseline\n"
|
|
60
|
+
_write_targets(tmp_path, agents=base, claude=base, copilot=base)
|
|
61
|
+
|
|
62
|
+
engine = DocsSyncEngine(config=_config(tmp_path))
|
|
63
|
+
startup_result = engine.run_once(reason="startup")
|
|
64
|
+
assert startup_result in {"synced", "fanout", "noop"}
|
|
65
|
+
|
|
66
|
+
(tmp_path / edited_relpath).write_text(updated, encoding="utf-8")
|
|
67
|
+
|
|
68
|
+
result = engine.run_once(reason="event")
|
|
69
|
+
assert result == "synced"
|
|
70
|
+
|
|
71
|
+
assert (tmp_path / "AGENTS.md").read_text(encoding="utf-8") == updated
|
|
72
|
+
assert (tmp_path / "CLAUDE.md").read_text(encoding="utf-8") == updated
|
|
73
|
+
assert (tmp_path / ".github" / "copilot-instructions.md").read_text(
|
|
74
|
+
encoding="utf-8"
|
|
75
|
+
) == updated
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_divergent_local_edits_emit_conflict_artifact(tmp_path: Path) -> None:
|
|
79
|
+
"""Divergent concurrent edits should produce conflict artifact and avoid overwrite."""
|
|
80
|
+
base = "# Shared instructions\n\n- baseline\n"
|
|
81
|
+
_write_targets(tmp_path, agents=base, claude=base, copilot=base)
|
|
82
|
+
|
|
83
|
+
engine = DocsSyncEngine(config=_config(tmp_path))
|
|
84
|
+
engine.run_once(reason="startup")
|
|
85
|
+
|
|
86
|
+
agents_update = "# Shared instructions\n\n- AGENTS edit\n"
|
|
87
|
+
claude_update = "# Shared instructions\n\n- CLAUDE edit\n"
|
|
88
|
+
(tmp_path / "AGENTS.md").write_text(agents_update, encoding="utf-8")
|
|
89
|
+
(tmp_path / "CLAUDE.md").write_text(claude_update, encoding="utf-8")
|
|
90
|
+
|
|
91
|
+
result = engine.run_once(reason="event")
|
|
92
|
+
assert result == "conflict"
|
|
93
|
+
|
|
94
|
+
conflicts = list((tmp_path / ".vds-sync" / "conflicts").glob(".sync-conflict-*.md"))
|
|
95
|
+
assert conflicts, "Expected conflict artifact file"
|
|
96
|
+
|
|
97
|
+
assert (tmp_path / "AGENTS.md").read_text(encoding="utf-8") == agents_update
|
|
98
|
+
assert (tmp_path / "CLAUDE.md").read_text(encoding="utf-8") == claude_update
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_watch_backend_auto_prefers_watchfiles_when_available(
|
|
102
|
+
tmp_path: Path,
|
|
103
|
+
monkeypatch,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Auto backend should select watchfiles when module callable is available."""
|
|
106
|
+
base = "# Shared instructions\n\n- baseline\n"
|
|
107
|
+
_write_targets(tmp_path, agents=base, claude=base, copilot=base)
|
|
108
|
+
engine = DocsSyncEngine(config=_config(tmp_path))
|
|
109
|
+
|
|
110
|
+
monkeypatch.setattr("vds_cli.docs_sync._load_watchfiles_watch", lambda: lambda *a, **k: iter(()))
|
|
111
|
+
|
|
112
|
+
assert engine._resolve_watch_backend() == "watchfiles"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_revision_mismatch_non_overlapping_changes_merge_and_sync(tmp_path: Path) -> None:
|
|
116
|
+
"""Revision mismatch should auto-merge safely when edits do not overlap."""
|
|
117
|
+
base = "A\nB\nC\n"
|
|
118
|
+
_write_targets(tmp_path, agents=base, claude=base, copilot=base)
|
|
119
|
+
engine = DocsSyncEngine(config=_config(tmp_path))
|
|
120
|
+
engine.run_once(reason="startup")
|
|
121
|
+
|
|
122
|
+
backend = engine.backend
|
|
123
|
+
assert isinstance(backend, LocalJsonCentralStateBackend)
|
|
124
|
+
central = backend.pull()
|
|
125
|
+
assert central is not None
|
|
126
|
+
backend.push_if_match(
|
|
127
|
+
content="A\nB_REMOTE\nC\n",
|
|
128
|
+
source_file="AGENTS.md",
|
|
129
|
+
expected_revision=central.revision,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
(tmp_path / "CLAUDE.md").write_text("A\nB\nC_LOCAL\n", encoding="utf-8")
|
|
133
|
+
result = engine.run_once(reason="event")
|
|
134
|
+
|
|
135
|
+
assert result == "synced"
|
|
136
|
+
expected = "A\nB_REMOTE\nC_LOCAL\n"
|
|
137
|
+
assert (tmp_path / "AGENTS.md").read_text(encoding="utf-8") == expected
|
|
138
|
+
assert (tmp_path / "CLAUDE.md").read_text(encoding="utf-8") == expected
|
|
139
|
+
assert (tmp_path / ".github" / "copilot-instructions.md").read_text(encoding="utf-8") == expected
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_revision_mismatch_conflicting_changes_emits_conflict_artifact(tmp_path: Path) -> None:
|
|
143
|
+
"""Revision mismatch with overlapping edits should not auto-merge."""
|
|
144
|
+
base = "A\nB\nC\n"
|
|
145
|
+
_write_targets(tmp_path, agents=base, claude=base, copilot=base)
|
|
146
|
+
engine = DocsSyncEngine(config=_config(tmp_path))
|
|
147
|
+
engine.run_once(reason="startup")
|
|
148
|
+
|
|
149
|
+
backend = engine.backend
|
|
150
|
+
assert isinstance(backend, LocalJsonCentralStateBackend)
|
|
151
|
+
central = backend.pull()
|
|
152
|
+
assert central is not None
|
|
153
|
+
backend.push_if_match(
|
|
154
|
+
content="A\nB_REMOTE\nC\n",
|
|
155
|
+
source_file="AGENTS.md",
|
|
156
|
+
expected_revision=central.revision,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
(tmp_path / "CLAUDE.md").write_text("A\nB_LOCAL\nC\n", encoding="utf-8")
|
|
160
|
+
result = engine.run_once(reason="event")
|
|
161
|
+
assert result == "conflict"
|
|
162
|
+
|
|
163
|
+
conflicts = list((tmp_path / ".vds-sync" / "conflicts").glob(".sync-conflict-*.md"))
|
|
164
|
+
assert conflicts, "Expected conflict artifact file for unresolved merge"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_periodic_reconcile_recovers_when_central_changes_without_local_event(tmp_path: Path) -> None:
|
|
168
|
+
"""Periodic reconcile should fan out central changes even without local file-change event."""
|
|
169
|
+
base = "# Shared instructions\n\n- baseline\n"
|
|
170
|
+
_write_targets(tmp_path, agents=base, claude=base, copilot=base)
|
|
171
|
+
|
|
172
|
+
engine = DocsSyncEngine(config=_config(tmp_path))
|
|
173
|
+
engine.run_once(reason="startup")
|
|
174
|
+
|
|
175
|
+
backend = engine.backend
|
|
176
|
+
assert isinstance(backend, LocalJsonCentralStateBackend)
|
|
177
|
+
central = backend.pull()
|
|
178
|
+
assert central is not None
|
|
179
|
+
|
|
180
|
+
updated = "# Shared instructions\n\n- central update only\n"
|
|
181
|
+
backend.push_if_match(
|
|
182
|
+
content=updated,
|
|
183
|
+
source_file="AGENTS.md",
|
|
184
|
+
expected_revision=central.revision,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
result = engine.run_once(reason="periodic")
|
|
188
|
+
assert result == "fanout"
|
|
189
|
+
assert (tmp_path / "AGENTS.md").read_text(encoding="utf-8") == updated
|
|
190
|
+
assert (tmp_path / "CLAUDE.md").read_text(encoding="utf-8") == updated
|
|
191
|
+
assert (tmp_path / ".github" / "copilot-instructions.md").read_text(encoding="utf-8") == updated
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_build_backend_vds_ai_memory_contract_uses_env_defaults(
|
|
195
|
+
tmp_path: Path,
|
|
196
|
+
monkeypatch,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""HTTP backend should resolve concrete VDS AI Memory env contracts."""
|
|
199
|
+
monkeypatch.setenv("VDS_AI_MEMORY_BASE_URL", "https://memory.example")
|
|
200
|
+
monkeypatch.setenv("VDS_AI_MEMORY_API_KEY", "mem-token")
|
|
201
|
+
config = SyncConfig(
|
|
202
|
+
repo_root=tmp_path,
|
|
203
|
+
central_backend="http",
|
|
204
|
+
central_contract="vds-ai-memory",
|
|
205
|
+
central_memory_id="memory-123",
|
|
206
|
+
log_json=False,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
backend = _build_backend(config)
|
|
210
|
+
assert isinstance(backend, HttpCentralStateBackend)
|
|
211
|
+
assert isinstance(backend.client, VdsAiMemorySyncApiClient)
|
|
212
|
+
assert backend.client.base_url == "https://memory.example"
|
|
213
|
+
assert backend.client.memory_id == "memory-123"
|
|
214
|
+
assert backend.client.auth_token == "mem-token"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_build_backend_confluence_contract_uses_standard_env_credentials(
|
|
218
|
+
tmp_path: Path,
|
|
219
|
+
monkeypatch,
|
|
220
|
+
) -> None:
|
|
221
|
+
"""Confluence contract should use VDS credential policy from env."""
|
|
222
|
+
monkeypatch.setenv("CONFLUENCE_INTERNAL_URL", "https://confluence.internal")
|
|
223
|
+
monkeypatch.setenv("VDS_USERNAME", "sync-user")
|
|
224
|
+
monkeypatch.setenv("VDS_PASSWORD", "sync-pass")
|
|
225
|
+
config = SyncConfig(
|
|
226
|
+
repo_root=tmp_path,
|
|
227
|
+
central_backend="http",
|
|
228
|
+
central_contract="confluence-property",
|
|
229
|
+
central_confluence_page_id="12345",
|
|
230
|
+
central_confluence_server="internal",
|
|
231
|
+
log_json=False,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
backend = _build_backend(config)
|
|
235
|
+
assert isinstance(backend, HttpCentralStateBackend)
|
|
236
|
+
assert isinstance(backend.client, ConfluencePropertySyncApiClient)
|
|
237
|
+
assert backend.client.base_url == "https://confluence.internal"
|
|
238
|
+
assert backend.client.page_id == "12345"
|
|
239
|
+
assert backend.client.username == "sync-user"
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_build_backend_vds_ai_memory_contract_requires_memory_id(tmp_path: Path, monkeypatch) -> None:
|
|
243
|
+
"""Memory contract must reject missing memory identifiers."""
|
|
244
|
+
monkeypatch.setenv("VDS_AI_MEMORY_BASE_URL", "https://memory.example")
|
|
245
|
+
config = SyncConfig(
|
|
246
|
+
repo_root=tmp_path,
|
|
247
|
+
central_backend="http",
|
|
248
|
+
central_contract="vds-ai-memory",
|
|
249
|
+
log_json=False,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
with pytest.raises(SyncError):
|
|
253
|
+
_build_backend(config)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_http_backend_retries_transient_pull_then_succeeds(monkeypatch) -> None:
|
|
257
|
+
"""HTTP backend should retry transient pull failures with bounded backoff."""
|
|
258
|
+
|
|
259
|
+
class _TransientPullClient:
|
|
260
|
+
def __init__(self) -> None:
|
|
261
|
+
self.calls = 0
|
|
262
|
+
|
|
263
|
+
def pull_document(self):
|
|
264
|
+
self.calls += 1
|
|
265
|
+
if self.calls < 3:
|
|
266
|
+
raise SyncApiTransientError("temporary network failure")
|
|
267
|
+
return SyncApiDocument(
|
|
268
|
+
content="canonical",
|
|
269
|
+
revision="3",
|
|
270
|
+
content_hash="abc123",
|
|
271
|
+
source_file="AGENTS.md",
|
|
272
|
+
updated_at="2026-03-01T00:00:00Z",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
delays: list[float] = []
|
|
276
|
+
monkeypatch.setattr(docs_sync.time, "sleep", lambda delay: delays.append(delay))
|
|
277
|
+
|
|
278
|
+
backend = HttpCentralStateBackend(
|
|
279
|
+
client=_TransientPullClient(),
|
|
280
|
+
retry_attempts=3,
|
|
281
|
+
retry_base_delay_seconds=0.05,
|
|
282
|
+
retry_max_delay_seconds=0.1,
|
|
283
|
+
)
|
|
284
|
+
document = backend.pull()
|
|
285
|
+
assert document is not None
|
|
286
|
+
assert document.revision == "3"
|
|
287
|
+
assert delays == [0.05, 0.1]
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_http_backend_exhausts_retry_budget_on_transient_pull(monkeypatch) -> None:
|
|
291
|
+
"""HTTP backend should raise SyncError after retry budget is exhausted."""
|
|
292
|
+
|
|
293
|
+
class _AlwaysTransientClient:
|
|
294
|
+
def pull_document(self):
|
|
295
|
+
raise SyncApiTransientError("still unavailable")
|
|
296
|
+
|
|
297
|
+
monkeypatch.setattr(docs_sync.time, "sleep", lambda _delay: None)
|
|
298
|
+
backend = HttpCentralStateBackend(
|
|
299
|
+
client=_AlwaysTransientClient(),
|
|
300
|
+
retry_attempts=2,
|
|
301
|
+
retry_base_delay_seconds=0.01,
|
|
302
|
+
retry_max_delay_seconds=0.02,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
with pytest.raises(SyncError, match="Failed pulling central state via HTTP"):
|
|
306
|
+
backend.pull()
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def test_sync_logs_include_source_hash_and_write_outcomes(tmp_path: Path) -> None:
|
|
310
|
+
"""Sync log events should include source hash and fan-out write outcomes."""
|
|
311
|
+
base = "# Shared instructions\n\n- baseline\n"
|
|
312
|
+
_write_targets(tmp_path, agents=base, claude=base, copilot=base)
|
|
313
|
+
engine = DocsSyncEngine(config=_config(tmp_path))
|
|
314
|
+
engine.run_once(reason="startup")
|
|
315
|
+
|
|
316
|
+
updated = "# Shared instructions\n\n- updated from CLAUDE\n"
|
|
317
|
+
(tmp_path / "CLAUDE.md").write_text(updated, encoding="utf-8")
|
|
318
|
+
result = engine.run_once(reason="event")
|
|
319
|
+
assert result == "synced"
|
|
320
|
+
|
|
321
|
+
log_lines = (tmp_path / ".vds-sync" / "watch-agents.log.jsonl").read_text(
|
|
322
|
+
encoding="utf-8"
|
|
323
|
+
).splitlines()
|
|
324
|
+
entries = [json.loads(line) for line in log_lines if line.strip()]
|
|
325
|
+
sync_event = next(
|
|
326
|
+
entry
|
|
327
|
+
for entry in reversed(entries)
|
|
328
|
+
if entry.get("event") == "sync" and entry.get("reason") == "event"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
assert sync_event["source_file"] == "CLAUDE.md"
|
|
332
|
+
assert isinstance(sync_event.get("source_hash"), str)
|
|
333
|
+
assert len(sync_event["source_hash"]) == 64
|
|
334
|
+
assert isinstance(sync_event.get("write_outcomes"), dict)
|
|
335
|
+
assert set(sync_event["write_outcomes"].keys()) == set(docs_sync.TARGET_FILES)
|
|
336
|
+
assert "updated" in sync_event["write_outcomes"].values()
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def test_atomic_write_retries_windows_style_permission_error(tmp_path: Path, monkeypatch) -> None:
|
|
340
|
+
"""Atomic writer should retry transient PermissionError (common on Windows)."""
|
|
341
|
+
target = tmp_path / "AGENTS.md"
|
|
342
|
+
target.write_text("old\n", encoding="utf-8")
|
|
343
|
+
attempts = {"count": 0}
|
|
344
|
+
real_replace = docs_sync.os.replace
|
|
345
|
+
|
|
346
|
+
def _flaky_replace(src, dst):
|
|
347
|
+
attempts["count"] += 1
|
|
348
|
+
if attempts["count"] < 3:
|
|
349
|
+
raise PermissionError("Access is denied")
|
|
350
|
+
return real_replace(src, dst)
|
|
351
|
+
|
|
352
|
+
monkeypatch.setattr(docs_sync.os, "replace", _flaky_replace)
|
|
353
|
+
_atomic_write_text(target, "new\n")
|
|
354
|
+
|
|
355
|
+
assert target.read_text(encoding="utf-8") == "new\n"
|
|
356
|
+
assert attempts["count"] == 3
|
|
357
|
+
assert list(tmp_path.glob(".AGENTS.md.*.tmp")) == []
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def test_atomic_write_retries_posix_eacces_error(tmp_path: Path, monkeypatch) -> None:
|
|
361
|
+
"""Atomic writer should retry transient POSIX EACCES replace failures."""
|
|
362
|
+
target = tmp_path / "CLAUDE.md"
|
|
363
|
+
target.write_text("old\n", encoding="utf-8")
|
|
364
|
+
attempts = {"count": 0}
|
|
365
|
+
real_replace = docs_sync.os.replace
|
|
366
|
+
|
|
367
|
+
def _flaky_replace(src, dst):
|
|
368
|
+
attempts["count"] += 1
|
|
369
|
+
if attempts["count"] == 1:
|
|
370
|
+
raise OSError(errno.EACCES, "Permission denied")
|
|
371
|
+
return real_replace(src, dst)
|
|
372
|
+
|
|
373
|
+
monkeypatch.setattr(docs_sync.os, "replace", _flaky_replace)
|
|
374
|
+
_atomic_write_text(target, "line1\r\nline2\r\n")
|
|
375
|
+
|
|
376
|
+
assert target.read_bytes() == b"line1\r\nline2\r\n"
|
|
377
|
+
assert attempts["count"] == 2
|
|
378
|
+
assert list(tmp_path.glob(".CLAUDE.md.*.tmp")) == []
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def test_atomic_write_cleans_temp_file_after_permanent_failure(tmp_path: Path, monkeypatch) -> None:
|
|
382
|
+
"""Atomic writer should not leak temp files when replace fails repeatedly."""
|
|
383
|
+
target = tmp_path / "copilot-instructions.md"
|
|
384
|
+
target.write_text("old\n", encoding="utf-8")
|
|
385
|
+
|
|
386
|
+
def _always_fail(src, dst): # noqa: ARG001
|
|
387
|
+
raise PermissionError("still locked")
|
|
388
|
+
|
|
389
|
+
monkeypatch.setattr(docs_sync.os, "replace", _always_fail)
|
|
390
|
+
with pytest.raises(PermissionError):
|
|
391
|
+
_atomic_write_text(target, "new\n")
|
|
392
|
+
|
|
393
|
+
assert target.read_text(encoding="utf-8") == "old\n"
|
|
394
|
+
assert list(tmp_path.glob(".copilot-instructions.md.*.tmp")) == []
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def test_log_write_deadlock_does_not_crash_sync_engine(
|
|
398
|
+
tmp_path: Path,
|
|
399
|
+
monkeypatch,
|
|
400
|
+
capsys,
|
|
401
|
+
) -> None:
|
|
402
|
+
"""Sync should keep running when local JSON log sink raises EDEADLK."""
|
|
403
|
+
base = "# Shared instructions\n\n- baseline\n"
|
|
404
|
+
_write_targets(tmp_path, agents=base, claude=base, copilot=base)
|
|
405
|
+
engine = DocsSyncEngine(config=_config(tmp_path))
|
|
406
|
+
|
|
407
|
+
original_open = Path.open
|
|
408
|
+
|
|
409
|
+
def _patched_open(self: Path, *args, **kwargs): # type: ignore[no-untyped-def]
|
|
410
|
+
mode = kwargs.get("mode")
|
|
411
|
+
if mode is None and args:
|
|
412
|
+
mode = args[0]
|
|
413
|
+
if self == engine.config.log_path and mode == "a":
|
|
414
|
+
raise OSError(errno.EDEADLK, "Resource deadlock avoided")
|
|
415
|
+
return original_open(self, *args, **kwargs)
|
|
416
|
+
|
|
417
|
+
monkeypatch.setattr(Path, "open", _patched_open)
|
|
418
|
+
|
|
419
|
+
engine._log("noop", reason="test", conflict_state="none", write_outcomes={})
|
|
420
|
+
|
|
421
|
+
captured = capsys.readouterr()
|
|
422
|
+
assert '"event": "log-write-failed"' in captured.err
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Unit tests for environment management.
|
|
2
|
+
|
|
3
|
+
Based on pytest v8.3.3+ API
|
|
4
|
+
Context7: /pytest-dev/pytest
|
|
5
|
+
Key Features: Fixtures, parametrization
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from vds_cli.env import VDSSettings, load_environment
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_vds_settings_instantiation() -> None:
|
|
14
|
+
"""Test VDSSettings can be instantiated."""
|
|
15
|
+
# Note: This test verifies the class can be instantiated
|
|
16
|
+
# Actual values depend on environment and ~/.vds/.env file
|
|
17
|
+
settings = VDSSettings()
|
|
18
|
+
assert isinstance(settings, VDSSettings)
|
|
19
|
+
# Verify all expected attributes exist
|
|
20
|
+
assert hasattr(settings, "vds_username")
|
|
21
|
+
assert hasattr(settings, "vds_password")
|
|
22
|
+
assert hasattr(settings, "bitbucket_access_token")
|
|
23
|
+
assert hasattr(settings, "internal_confluence_token")
|
|
24
|
+
assert hasattr(settings, "external_confluence_token")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_vds_settings_with_env_file(mock_env_file: Path, mock_home_dir: Path) -> None:
|
|
28
|
+
"""Test VDSSettings can load from environment file."""
|
|
29
|
+
# Note: pydantic-settings reads from both env file and environment variables
|
|
30
|
+
# This test verifies the class can be instantiated with a file present
|
|
31
|
+
settings = VDSSettings()
|
|
32
|
+
assert isinstance(settings, VDSSettings)
|
|
33
|
+
# Values may come from file or environment - just verify structure
|
|
34
|
+
assert hasattr(settings, "vds_username")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_load_environment() -> None:
|
|
38
|
+
"""Test load_environment function."""
|
|
39
|
+
settings = load_environment()
|
|
40
|
+
assert isinstance(settings, VDSSettings)
|
|
41
|
+
assert hasattr(settings, "vds_username")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_load_environment_returns_settings() -> None:
|
|
45
|
+
"""Test load_environment returns VDSSettings instance."""
|
|
46
|
+
settings = load_environment()
|
|
47
|
+
assert isinstance(settings, VDSSettings)
|
|
48
|
+
# Verify it has all expected attributes
|
|
49
|
+
assert hasattr(settings, "vds_username")
|
|
50
|
+
assert hasattr(settings, "bitbucket_access_token")
|
|
51
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Unit tests for command routing.
|
|
2
|
+
|
|
3
|
+
Based on pytest v8.3.3+ API
|
|
4
|
+
Context7: /pytest-dev/pytest
|
|
5
|
+
Key Features: Fixtures, mocking
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from unittest.mock import Mock, patch
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
from vds_cli.router import ORCHESTRATORS, run_orchestrator
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_orchestrators_dict() -> None:
|
|
16
|
+
"""Test ORCHESTRATORS dictionary structure."""
|
|
17
|
+
assert "jira" in ORCHESTRATORS
|
|
18
|
+
assert "confluence" in ORCHESTRATORS
|
|
19
|
+
assert "bitbucket" in ORCHESTRATORS
|
|
20
|
+
assert "git" in ORCHESTRATORS
|
|
21
|
+
|
|
22
|
+
for _name, config in ORCHESTRATORS.items():
|
|
23
|
+
assert "module" in config
|
|
24
|
+
assert "path" in config
|
|
25
|
+
assert isinstance(config["module"], str)
|
|
26
|
+
assert isinstance(config["path"], str)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_run_orchestrator_invalid_service(mock_script_dir: Path) -> None:
|
|
30
|
+
"""Test run_orchestrator with invalid service name."""
|
|
31
|
+
with pytest.raises(ValueError, match="Unknown service"):
|
|
32
|
+
run_orchestrator("invalid", [], mock_script_dir)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_run_orchestrator_missing_directory(mock_script_dir: Path) -> None:
|
|
36
|
+
"""Test run_orchestrator when orchestrator directory doesn't exist."""
|
|
37
|
+
# Remove one orchestrator directory
|
|
38
|
+
(mock_script_dir / "jira_orchestrator").rmdir()
|
|
39
|
+
|
|
40
|
+
with pytest.raises(FileNotFoundError, match="not found"):
|
|
41
|
+
run_orchestrator("jira", [], mock_script_dir)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@patch("vds_cli.router.subprocess.run")
|
|
45
|
+
def test_run_orchestrator_success(mock_subprocess: Mock, mock_script_dir: Path) -> None:
|
|
46
|
+
"""Test successful orchestrator execution."""
|
|
47
|
+
mock_subprocess.return_value = Mock(returncode=0)
|
|
48
|
+
|
|
49
|
+
exit_code = run_orchestrator("jira", ["search", "project = NTTC"], mock_script_dir)
|
|
50
|
+
|
|
51
|
+
assert exit_code == 0
|
|
52
|
+
mock_subprocess.assert_called_once()
|
|
53
|
+
call_args = mock_subprocess.call_args[0][0]
|
|
54
|
+
assert "uv" in call_args
|
|
55
|
+
assert "--project" in call_args
|
|
56
|
+
assert "jira_orchestrator" in str(call_args)
|
|
57
|
+
assert "python" in call_args
|
|
58
|
+
assert "-m" in call_args
|
|
59
|
+
assert "vds_jira_orchestrator.cli" in call_args
|
|
60
|
+
assert "search" in call_args
|
|
61
|
+
assert "project = NTTC" in call_args
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@patch("vds_cli.router.subprocess.run")
|
|
65
|
+
def test_run_orchestrator_failure(mock_subprocess: Mock, mock_script_dir: Path) -> None:
|
|
66
|
+
"""Test orchestrator execution with non-zero exit code."""
|
|
67
|
+
mock_subprocess.return_value = Mock(returncode=1)
|
|
68
|
+
|
|
69
|
+
exit_code = run_orchestrator("confluence", ["content", "search"], mock_script_dir)
|
|
70
|
+
|
|
71
|
+
assert exit_code == 1
|
|
72
|
+
|