@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,223 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from unittest.mock import Mock
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from confluence_orchestrator.cli import app
|
|
8
|
+
from confluence_orchestrator.config import ConfluenceSettings
|
|
9
|
+
from confluence_orchestrator.errors import ConfluenceClientError
|
|
10
|
+
from confluence_orchestrator.http import ConfluenceClient
|
|
11
|
+
from typer.testing import CliRunner
|
|
12
|
+
|
|
13
|
+
runner = CliRunner()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture(autouse=True)
|
|
17
|
+
def mock_settings(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
18
|
+
def fake_load_settings(strict: bool = False) -> ConfluenceSettings:
|
|
19
|
+
return ConfluenceSettings.model_validate(
|
|
20
|
+
{
|
|
21
|
+
"VDS_USERNAME": "user",
|
|
22
|
+
"VDS_PASSWORD": "pass",
|
|
23
|
+
"INTERNAL_CONFLUENCE_TOKEN": "token-int",
|
|
24
|
+
"EXTERNAL_CONFLUENCE_TOKEN": "token-ext",
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
monkeypatch.setattr("confluence_orchestrator.cli.load_settings", fake_load_settings)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _build_fake_client(cloud: bool = False) -> tuple[ConfluenceClient, Mock]:
|
|
32
|
+
inner = Mock()
|
|
33
|
+
client = ConfluenceClient.__new__(ConfluenceClient) # type: ignore[call-arg]
|
|
34
|
+
client._client = inner # type: ignore[attr-defined]
|
|
35
|
+
client._log = Mock()
|
|
36
|
+
client._is_cloud = cloud # type: ignore[attr-defined]
|
|
37
|
+
client._with_retry = lambda operation, _: operation() # type: ignore[assignment]
|
|
38
|
+
return client, inner
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Client method coverage
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_client_get_draft_page_by_id_server_mode_invokes_sdk() -> None:
|
|
47
|
+
client, inner = _build_fake_client(cloud=False)
|
|
48
|
+
inner.get_draft_page_by_id.return_value = {"id": "123", "title": "Draft Page"}
|
|
49
|
+
|
|
50
|
+
result = client.get_draft_page_by_id("123")
|
|
51
|
+
|
|
52
|
+
assert result["id"] == "123"
|
|
53
|
+
inner.get_draft_page_by_id.assert_called_once_with("123")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_client_get_draft_page_by_id_cloud_mode_raises_error() -> None:
|
|
57
|
+
client, inner = _build_fake_client(cloud=True)
|
|
58
|
+
|
|
59
|
+
with pytest.raises(ConfluenceClientError, match="only available in Confluence Server"):
|
|
60
|
+
client.get_draft_page_by_id("123")
|
|
61
|
+
inner.get_draft_page_by_id.assert_not_called()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_client_get_all_draft_pages_from_space_server_mode_invokes_sdk() -> None:
|
|
65
|
+
client, inner = _build_fake_client(cloud=False)
|
|
66
|
+
inner.get_all_draft_pages_from_space.return_value = [{"id": "1", "title": "Draft 1"}, {"id": "2", "title": "Draft 2"}]
|
|
67
|
+
|
|
68
|
+
result = client.get_all_draft_pages_from_space("ABC", limit=50)
|
|
69
|
+
|
|
70
|
+
assert len(result) == 2
|
|
71
|
+
inner.get_all_draft_pages_from_space.assert_called_once_with("ABC", limit=50)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_client_get_all_draft_pages_from_space_cloud_mode_raises_error() -> None:
|
|
75
|
+
client, inner = _build_fake_client(cloud=True)
|
|
76
|
+
|
|
77
|
+
with pytest.raises(ConfluenceClientError, match="only available in Confluence Server"):
|
|
78
|
+
client.get_all_draft_pages_from_space("ABC")
|
|
79
|
+
inner.get_all_draft_pages_from_space.assert_not_called()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_client_remove_page_as_draft_server_mode_invokes_sdk() -> None:
|
|
83
|
+
client, inner = _build_fake_client(cloud=False)
|
|
84
|
+
inner.remove_page_as_draft.return_value = None
|
|
85
|
+
|
|
86
|
+
result = client.remove_page_as_draft("123")
|
|
87
|
+
|
|
88
|
+
assert result is None
|
|
89
|
+
inner.remove_page_as_draft.assert_called_once_with("123")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_client_remove_page_as_draft_cloud_mode_raises_error() -> None:
|
|
93
|
+
client, inner = _build_fake_client(cloud=True)
|
|
94
|
+
|
|
95
|
+
with pytest.raises(ConfluenceClientError, match="only available in Confluence Server"):
|
|
96
|
+
client.remove_page_as_draft("123")
|
|
97
|
+
inner.remove_page_as_draft.assert_not_called()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
# CLI command coverage
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class DummyDraftClient:
|
|
106
|
+
def __init__(self, cloud: bool = False) -> None:
|
|
107
|
+
self.calls: list[tuple[str, tuple[Any, ...], dict[str, Any]]] = []
|
|
108
|
+
self._is_cloud = cloud # type: ignore[attr-defined]
|
|
109
|
+
|
|
110
|
+
def get_draft_page_by_id(self, page_id: str) -> dict[str, Any]:
|
|
111
|
+
if self._is_cloud:
|
|
112
|
+
raise ConfluenceClientError("Server-only feature")
|
|
113
|
+
self.calls.append(("get_draft_page_by_id", (page_id,), {}))
|
|
114
|
+
return {"id": page_id, "title": "Draft Page"}
|
|
115
|
+
|
|
116
|
+
def get_all_draft_pages_from_space(self, space_key: str, limit: int = 25) -> list[dict[str, Any]]:
|
|
117
|
+
if self._is_cloud:
|
|
118
|
+
raise ConfluenceClientError("Server-only feature")
|
|
119
|
+
self.calls.append(("get_all_draft_pages_from_space", (space_key,), {"limit": limit}))
|
|
120
|
+
return [{"id": "1", "title": "Draft 1"}]
|
|
121
|
+
|
|
122
|
+
def remove_page_as_draft(self, page_id: str) -> None:
|
|
123
|
+
if self._is_cloud:
|
|
124
|
+
raise ConfluenceClientError("Server-only feature")
|
|
125
|
+
self.calls.append(("remove_page_as_draft", (page_id,), {}))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_cli_draft_get_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
129
|
+
client = DummyDraftClient(cloud=False)
|
|
130
|
+
monkeypatch.setattr("confluence_orchestrator.cli._build_http_client", lambda *args: client)
|
|
131
|
+
|
|
132
|
+
result = runner.invoke(app, ["draft", "get", "--page-id", "123"])
|
|
133
|
+
|
|
134
|
+
assert result.exit_code == 0
|
|
135
|
+
assert "123" in result.stdout
|
|
136
|
+
assert len(client.calls) == 1
|
|
137
|
+
assert client.calls[0][0] == "get_draft_page_by_id"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_cli_draft_get_requires_page_id(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
141
|
+
client = DummyDraftClient(cloud=False)
|
|
142
|
+
monkeypatch.setattr("confluence_orchestrator.cli._build_http_client", lambda *args: client)
|
|
143
|
+
|
|
144
|
+
result = runner.invoke(app, ["draft", "get"])
|
|
145
|
+
|
|
146
|
+
assert result.exit_code != 0
|
|
147
|
+
assert "--page-id required" in result.stdout or "--page-id required" in result.stderr
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_cli_draft_list_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
151
|
+
client = DummyDraftClient(cloud=False)
|
|
152
|
+
monkeypatch.setattr("confluence_orchestrator.cli._build_http_client", lambda *args: client)
|
|
153
|
+
|
|
154
|
+
result = runner.invoke(app, ["draft", "list", "--space-key", "ABC", "--limit", "50"])
|
|
155
|
+
|
|
156
|
+
assert result.exit_code == 0
|
|
157
|
+
assert "ABC" in result.stdout
|
|
158
|
+
assert len(client.calls) == 1
|
|
159
|
+
assert client.calls[0][0] == "get_all_draft_pages_from_space"
|
|
160
|
+
assert client.calls[0][2]["limit"] == 50
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_cli_draft_list_requires_space_key(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
164
|
+
client = DummyDraftClient(cloud=False)
|
|
165
|
+
monkeypatch.setattr("confluence_orchestrator.cli._build_http_client", lambda *args: client)
|
|
166
|
+
|
|
167
|
+
result = runner.invoke(app, ["draft", "list"])
|
|
168
|
+
|
|
169
|
+
assert result.exit_code != 0
|
|
170
|
+
assert "--space-key required" in result.stdout or "--space-key required" in result.stderr
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_cli_draft_remove_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
174
|
+
client = DummyDraftClient(cloud=False)
|
|
175
|
+
monkeypatch.setattr("confluence_orchestrator.cli._build_http_client", lambda *args: client)
|
|
176
|
+
|
|
177
|
+
result = runner.invoke(app, ["draft", "remove", "--page-id", "123", "--yes"])
|
|
178
|
+
|
|
179
|
+
assert result.exit_code == 0
|
|
180
|
+
assert "removed" in result.stdout
|
|
181
|
+
assert len(client.calls) == 1
|
|
182
|
+
assert client.calls[0][0] == "remove_page_as_draft"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_cli_draft_remove_requires_yes(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
186
|
+
client = DummyDraftClient(cloud=False)
|
|
187
|
+
monkeypatch.setattr("confluence_orchestrator.cli._build_http_client", lambda *args: client)
|
|
188
|
+
|
|
189
|
+
result = runner.invoke(app, ["draft", "remove", "--page-id", "123"])
|
|
190
|
+
|
|
191
|
+
assert result.exit_code != 0
|
|
192
|
+
assert "--yes required" in result.stdout or "--yes required" in result.stderr
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_cli_draft_remove_requires_page_id(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
196
|
+
client = DummyDraftClient(cloud=False)
|
|
197
|
+
monkeypatch.setattr("confluence_orchestrator.cli._build_http_client", lambda *args: client)
|
|
198
|
+
|
|
199
|
+
result = runner.invoke(app, ["draft", "remove", "--yes"])
|
|
200
|
+
|
|
201
|
+
assert result.exit_code != 0
|
|
202
|
+
assert "--page-id required" in result.stdout or "--page-id required" in result.stderr
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_cli_draft_cloud_mode_error(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
206
|
+
client = DummyDraftClient(cloud=True)
|
|
207
|
+
monkeypatch.setattr("confluence_orchestrator.cli._build_http_client", lambda *args: client)
|
|
208
|
+
|
|
209
|
+
result = runner.invoke(app, ["draft", "get", "--page-id", "123"])
|
|
210
|
+
|
|
211
|
+
assert result.exit_code != 0
|
|
212
|
+
assert "Server-only" in result.stderr or "Server-only" in result.stdout
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_cli_draft_invalid_action(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
216
|
+
client = DummyDraftClient(cloud=False)
|
|
217
|
+
monkeypatch.setattr("confluence_orchestrator.cli._build_http_client", lambda *args: client)
|
|
218
|
+
|
|
219
|
+
result = runner.invoke(app, ["draft", "invalid"])
|
|
220
|
+
|
|
221
|
+
assert result.exit_code != 0
|
|
222
|
+
assert "Unknown action" in result.stdout or "Unknown action" in result.stderr
|
|
223
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from confluence_orchestrator.eventing import Poller
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FakeClient:
|
|
10
|
+
def __init__(self, responses: list[dict[str, Any]]) -> None:
|
|
11
|
+
self._responses = responses
|
|
12
|
+
self.calls = 0
|
|
13
|
+
|
|
14
|
+
def search_cql(self, *_, **kwargs) -> dict[str, Any]: # noqa: ANN401
|
|
15
|
+
limit = kwargs.get("limit")
|
|
16
|
+
if self.calls < len(self._responses):
|
|
17
|
+
resp = self._responses[self.calls]
|
|
18
|
+
payload = dict(resp)
|
|
19
|
+
if limit is not None and "results" in payload:
|
|
20
|
+
payload["results"] = payload["results"][:limit]
|
|
21
|
+
else:
|
|
22
|
+
payload = {"results": [], "start": 0, "limit": 0}
|
|
23
|
+
self.calls += 1
|
|
24
|
+
return payload
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_poller_updates_cursor(tmp_path: Path) -> None:
|
|
28
|
+
client = FakeClient(
|
|
29
|
+
[
|
|
30
|
+
{
|
|
31
|
+
"results": [
|
|
32
|
+
{"id": "10", "version": {"when": "2024-01-02T00:00:00Z"}},
|
|
33
|
+
{"id": "11", "version": {"when": "2024-01-03T00:00:00Z"}},
|
|
34
|
+
],
|
|
35
|
+
"start": 0,
|
|
36
|
+
"limit": 25,
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
poller = Poller(client) # type: ignore[arg-type]
|
|
42
|
+
state_file = tmp_path / "cursor.json"
|
|
43
|
+
count = poller.poll_once(base_cql="type=page", state_file=state_file, expand=["version"], out_jsonl=tmp_path / "out.jsonl")
|
|
44
|
+
|
|
45
|
+
assert count == 2
|
|
46
|
+
data = state_file.read_text()
|
|
47
|
+
assert "2024-01-03T00:00:00Z" in data
|
|
48
|
+
jsonl_contents = (tmp_path / "out.jsonl").read_text().strip().splitlines()
|
|
49
|
+
assert len(jsonl_contents) == 2
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_poller_limits_results(tmp_path: Path) -> None:
|
|
53
|
+
client = FakeClient(
|
|
54
|
+
[
|
|
55
|
+
{
|
|
56
|
+
"results": [
|
|
57
|
+
{"id": "10", "version": {"when": "2024-01-02T00:00:00Z"}},
|
|
58
|
+
{"id": "11", "version": {"when": "2024-01-03T00:00:00Z"}},
|
|
59
|
+
],
|
|
60
|
+
"start": 0,
|
|
61
|
+
"limit": 25,
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
poller = Poller(client) # type: ignore[arg-type]
|
|
67
|
+
state_file = tmp_path / "cursor.json"
|
|
68
|
+
count = poller.poll_once(base_cql="type=page", state_file=state_file, max_results=1)
|
|
69
|
+
|
|
70
|
+
assert count == 1
|
|
71
|
+
assert "2024-01-02T00:00:00Z" in state_file.read_text()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from confluence_orchestrator.eventing import Poller
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SingleRunClient:
|
|
10
|
+
def __init__(self, results: list[dict[str, Any]]) -> None:
|
|
11
|
+
self._results = results
|
|
12
|
+
self.calls = 0
|
|
13
|
+
|
|
14
|
+
def search_cql(self, *_, **kwargs) -> dict[str, Any]: # noqa: ANN401
|
|
15
|
+
limit = kwargs.get("limit")
|
|
16
|
+
if self.calls == 0:
|
|
17
|
+
payload = {"results": self._results, "start": 0, "limit": len(self._results)}
|
|
18
|
+
if limit is not None:
|
|
19
|
+
payload["results"] = self._results[:limit]
|
|
20
|
+
else:
|
|
21
|
+
payload = {"results": [], "start": 0, "limit": 0}
|
|
22
|
+
self.calls += 1
|
|
23
|
+
return payload
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_poller_persists_state_across_runs(tmp_path: Path) -> None:
|
|
27
|
+
state_file = tmp_path / "cursor.json"
|
|
28
|
+
|
|
29
|
+
poller = Poller(SingleRunClient([{ "id": "10", "version": {"when": "2024-01-02T00:00:00Z"}}])) # type: ignore[arg-type]
|
|
30
|
+
poller.poll_once(base_cql="type=page", state_file=state_file)
|
|
31
|
+
assert "2024-01-02T00:00:00Z" in state_file.read_text()
|
|
32
|
+
|
|
33
|
+
poller = Poller(SingleRunClient([{ "id": "11", "version": {"when": "2024-01-04T00:00:00Z"}}])) # type: ignore[arg-type]
|
|
34
|
+
poller.poll_once(base_cql="type=page", state_file=state_file)
|
|
35
|
+
contents = state_file.read_text()
|
|
36
|
+
assert "2024-01-04T00:00:00Z" in contents
|
|
37
|
+
assert "\"10\"" in contents and "\"11\"" in contents
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from confluence_orchestrator.eventing import Poller
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DuplicateClient:
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
self.responses: list[dict[str, Any]] = [
|
|
12
|
+
{
|
|
13
|
+
"results": [
|
|
14
|
+
{"id": "10", "version": {"when": "2024-01-02T00:00:00Z"}},
|
|
15
|
+
{"id": "10", "version": {"when": "2024-01-02T00:00:00Z"}},
|
|
16
|
+
{"id": "11", "version": {"when": "2024-01-03T00:00:00Z"}},
|
|
17
|
+
],
|
|
18
|
+
"start": 0,
|
|
19
|
+
"limit": 25,
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
self.calls = 0
|
|
23
|
+
|
|
24
|
+
def search_cql(self, *_, **kwargs) -> dict[str, Any]: # noqa: ANN401
|
|
25
|
+
limit = kwargs.get("limit")
|
|
26
|
+
if self.calls == 0:
|
|
27
|
+
resp = dict(self.responses[0])
|
|
28
|
+
if limit is not None:
|
|
29
|
+
resp["results"] = resp["results"][:limit]
|
|
30
|
+
else:
|
|
31
|
+
resp = {"results": [], "start": 0, "limit": 0}
|
|
32
|
+
self.calls += 1
|
|
33
|
+
return resp
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_poller_skips_duplicate_ids(tmp_path: Path) -> None:
|
|
37
|
+
client = DuplicateClient()
|
|
38
|
+
poller = Poller(client) # type: ignore[arg-type]
|
|
39
|
+
|
|
40
|
+
state_file = tmp_path / "cursor.json"
|
|
41
|
+
count = poller.poll_once(base_cql="type=page", state_file=state_file, limit=10)
|
|
42
|
+
|
|
43
|
+
assert count == 2 # one duplicate skipped
|
|
44
|
+
assert client.calls == 2 # second call returns empty page
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from confluence_orchestrator.eventing import Poller
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PagedClient:
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
self.responses: list[dict[str, Any]] = [
|
|
12
|
+
{
|
|
13
|
+
"results": [
|
|
14
|
+
{"id": "10", "version": {"when": "2024-01-02T00:00:00Z"}},
|
|
15
|
+
],
|
|
16
|
+
"start": 0,
|
|
17
|
+
"limit": 1,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"results": [
|
|
21
|
+
{"id": "11", "version": {"when": "2024-01-03T00:00:00Z"}},
|
|
22
|
+
],
|
|
23
|
+
"start": 1,
|
|
24
|
+
"limit": 1,
|
|
25
|
+
},
|
|
26
|
+
]
|
|
27
|
+
self.calls = 0
|
|
28
|
+
|
|
29
|
+
def search_cql(self, *_, **kwargs) -> dict[str, Any]: # noqa: ANN401
|
|
30
|
+
limit = kwargs.get("limit")
|
|
31
|
+
if self.calls < len(self.responses):
|
|
32
|
+
resp = dict(self.responses[self.calls])
|
|
33
|
+
if limit is not None:
|
|
34
|
+
resp["results"] = resp["results"][:limit]
|
|
35
|
+
else:
|
|
36
|
+
resp = {"results": [], "start": 0, "limit": 0}
|
|
37
|
+
self.calls += 1
|
|
38
|
+
return resp
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_poller_respects_max_results(tmp_path: Path) -> None:
|
|
42
|
+
client = PagedClient()
|
|
43
|
+
poller = Poller(client) # type: ignore[arg-type]
|
|
44
|
+
state_file = tmp_path / "cursor.json"
|
|
45
|
+
|
|
46
|
+
count = poller.poll_once(base_cql="type=page", state_file=state_file, limit=1, max_results=1)
|
|
47
|
+
|
|
48
|
+
assert count == 1
|
|
49
|
+
assert client.calls == 1
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
from unittest.mock import Mock
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from confluence_orchestrator.cli import app
|
|
9
|
+
from confluence_orchestrator.config import ConfluenceSettings
|
|
10
|
+
from confluence_orchestrator.http import ConfluenceClient
|
|
11
|
+
from typer.testing import CliRunner
|
|
12
|
+
|
|
13
|
+
runner = CliRunner()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture(autouse=True)
|
|
17
|
+
def mock_settings(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
18
|
+
def fake_load_settings(strict: bool = False) -> ConfluenceSettings:
|
|
19
|
+
return ConfluenceSettings.model_validate(
|
|
20
|
+
{
|
|
21
|
+
"VDS_USERNAME": "user",
|
|
22
|
+
"VDS_PASSWORD": "pass",
|
|
23
|
+
"INTERNAL_CONFLUENCE_TOKEN": "token-int",
|
|
24
|
+
"EXTERNAL_CONFLUENCE_TOKEN": "token-ext",
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
monkeypatch.setattr("confluence_orchestrator.cli.load_settings", fake_load_settings)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _build_fake_client() -> tuple[ConfluenceClient, Mock]:
|
|
32
|
+
inner = Mock()
|
|
33
|
+
client = ConfluenceClient.__new__(ConfluenceClient) # type: ignore[call-arg]
|
|
34
|
+
client._client = inner # type: ignore[attr-defined]
|
|
35
|
+
client._log = Mock()
|
|
36
|
+
client._is_cloud = True # type: ignore[attr-defined]
|
|
37
|
+
client._with_retry = lambda operation, _: operation() # type: ignore[assignment]
|
|
38
|
+
return client, inner
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Client method coverage
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_client_export_page_cloud_mode_invokes_sdk() -> None:
|
|
47
|
+
client, inner = _build_fake_client()
|
|
48
|
+
inner.export_page.return_value = b"%PDF-1.4 fake pdf content"
|
|
49
|
+
|
|
50
|
+
result = client.export_page("12345")
|
|
51
|
+
|
|
52
|
+
assert result == b"%PDF-1.4 fake pdf content"
|
|
53
|
+
# SDK auto-detects Cloud/Server mode, so api_version is not passed
|
|
54
|
+
inner.export_page.assert_called_once_with("12345")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_client_export_page_server_mode_invokes_sdk() -> None:
|
|
58
|
+
client, inner = _build_fake_client()
|
|
59
|
+
client._is_cloud = False # type: ignore[attr-defined]
|
|
60
|
+
inner.export_page.return_value = b"%PDF-1.4 fake pdf content"
|
|
61
|
+
|
|
62
|
+
result = client.export_page("12345")
|
|
63
|
+
|
|
64
|
+
assert result == b"%PDF-1.4 fake pdf content"
|
|
65
|
+
# SDK auto-detects Cloud/Server mode, so api_version is not passed
|
|
66
|
+
inner.export_page.assert_called_once_with("12345")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_client_export_page_with_explicit_api_version() -> None:
|
|
70
|
+
client, inner = _build_fake_client()
|
|
71
|
+
inner.export_page.return_value = b"%PDF-1.4 fake pdf content"
|
|
72
|
+
|
|
73
|
+
result = client.export_page("12345", api_version="server")
|
|
74
|
+
|
|
75
|
+
assert result == b"%PDF-1.4 fake pdf content"
|
|
76
|
+
# SDK auto-detects Cloud/Server mode, so api_version parameter is accepted but not passed to SDK
|
|
77
|
+
inner.export_page.assert_called_once_with("12345")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_client_export_page_handles_file_path() -> None:
|
|
81
|
+
client, inner = _build_fake_client()
|
|
82
|
+
# Mock file path return
|
|
83
|
+
import tempfile
|
|
84
|
+
|
|
85
|
+
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
|
86
|
+
tmp.write(b"%PDF-1.4 fake pdf content")
|
|
87
|
+
tmp_path = tmp.name
|
|
88
|
+
|
|
89
|
+
inner.export_page.return_value = tmp_path
|
|
90
|
+
|
|
91
|
+
result = client.export_page("12345")
|
|
92
|
+
|
|
93
|
+
assert result == b"%PDF-1.4 fake pdf content"
|
|
94
|
+
# Cleanup
|
|
95
|
+
Path(tmp_path).unlink()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_client_get_space_export_invokes_sdk() -> None:
|
|
99
|
+
client, inner = _build_fake_client()
|
|
100
|
+
inner.get_space_export.return_value = "https://example.com/export/space.pdf"
|
|
101
|
+
|
|
102
|
+
result = client.get_space_export("ABC", "pdf")
|
|
103
|
+
|
|
104
|
+
assert result == "https://example.com/export/space.pdf"
|
|
105
|
+
inner.get_space_export.assert_called_once_with("ABC", "pdf")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_client_get_space_export_with_different_types() -> None:
|
|
109
|
+
client, inner = _build_fake_client()
|
|
110
|
+
inner.get_space_export.return_value = "https://example.com/export/space.html"
|
|
111
|
+
|
|
112
|
+
result = client.get_space_export("ABC", "html")
|
|
113
|
+
|
|
114
|
+
assert result == "https://example.com/export/space.html"
|
|
115
|
+
inner.get_space_export.assert_called_once_with("ABC", "html")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# CLI command coverage
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class DummyExportClient:
|
|
124
|
+
def __init__(self) -> None:
|
|
125
|
+
self.calls: list[tuple[str, tuple[Any, ...], dict[str, Any]]] = []
|
|
126
|
+
|
|
127
|
+
def export_page(self, page_id: str, api_version: str | None = None) -> bytes:
|
|
128
|
+
self.calls.append(("export_page", (page_id,), {"api_version": api_version}))
|
|
129
|
+
return b"%PDF-1.4 fake pdf content"
|
|
130
|
+
|
|
131
|
+
def get_space_export(self, space_key: str, export_type: str) -> str:
|
|
132
|
+
self.calls.append(("get_space_export", (space_key, export_type), {}))
|
|
133
|
+
return f"https://example.com/export/{space_key}.{export_type}"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_cli_export_page_success(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
137
|
+
client = DummyExportClient()
|
|
138
|
+
monkeypatch.setattr("confluence_orchestrator.cli._build_http_client", lambda *args: client)
|
|
139
|
+
|
|
140
|
+
output_file = tmp_path / "test.pdf"
|
|
141
|
+
result = runner.invoke(app, ["export", "page", "--page-id", "12345", "--output", str(output_file)])
|
|
142
|
+
|
|
143
|
+
assert result.exit_code == 0
|
|
144
|
+
assert output_file.exists()
|
|
145
|
+
assert output_file.read_bytes() == b"%PDF-1.4 fake pdf content"
|
|
146
|
+
assert len(client.calls) == 1
|
|
147
|
+
assert client.calls[0][0] == "export_page"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_cli_export_page_requires_page_id(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
151
|
+
client = DummyExportClient()
|
|
152
|
+
monkeypatch.setattr("confluence_orchestrator.cli._build_http_client", lambda *args: client)
|
|
153
|
+
|
|
154
|
+
result = runner.invoke(app, ["export", "page"])
|
|
155
|
+
|
|
156
|
+
assert result.exit_code != 0
|
|
157
|
+
assert "--page-id required" in result.stdout or "--page-id required" in result.stderr
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_cli_export_page_with_api_version(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
161
|
+
client = DummyExportClient()
|
|
162
|
+
monkeypatch.setattr("confluence_orchestrator.cli._build_http_client", lambda *args: client)
|
|
163
|
+
|
|
164
|
+
output_file = tmp_path / "test.pdf"
|
|
165
|
+
result = runner.invoke(app, ["export", "page", "--page-id", "12345", "--api-version", "server", "--output", str(output_file)])
|
|
166
|
+
|
|
167
|
+
assert result.exit_code == 0
|
|
168
|
+
assert client.calls[0][2]["api_version"] == "server"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_cli_export_space_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
172
|
+
client = DummyExportClient()
|
|
173
|
+
monkeypatch.setattr("confluence_orchestrator.cli._build_http_client", lambda *args: client)
|
|
174
|
+
|
|
175
|
+
result = runner.invoke(app, ["export", "space", "--space-key", "ABC", "--export-type", "pdf"])
|
|
176
|
+
|
|
177
|
+
assert result.exit_code == 0
|
|
178
|
+
assert "ABC" in result.stdout or "download_url" in result.stdout
|
|
179
|
+
assert len(client.calls) == 1
|
|
180
|
+
assert client.calls[0][0] == "get_space_export"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_cli_export_space_requires_space_key(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
184
|
+
client = DummyExportClient()
|
|
185
|
+
monkeypatch.setattr("confluence_orchestrator.cli._build_http_client", lambda *args: client)
|
|
186
|
+
|
|
187
|
+
result = runner.invoke(app, ["export", "space", "--export-type", "pdf"])
|
|
188
|
+
|
|
189
|
+
assert result.exit_code != 0
|
|
190
|
+
assert "--space-key required" in result.stdout or "--space-key required" in result.stderr
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_cli_export_space_requires_export_type(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
194
|
+
client = DummyExportClient()
|
|
195
|
+
monkeypatch.setattr("confluence_orchestrator.cli._build_http_client", lambda *args: client)
|
|
196
|
+
|
|
197
|
+
result = runner.invoke(app, ["export", "space", "--space-key", "ABC"])
|
|
198
|
+
|
|
199
|
+
assert result.exit_code != 0
|
|
200
|
+
assert "--export-type required" in result.stdout or "--export-type required" in result.stderr
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_cli_export_space_validates_export_type(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
204
|
+
client = DummyExportClient()
|
|
205
|
+
monkeypatch.setattr("confluence_orchestrator.cli._build_http_client", lambda *args: client)
|
|
206
|
+
|
|
207
|
+
result = runner.invoke(app, ["export", "space", "--space-key", "ABC", "--export-type", "invalid"])
|
|
208
|
+
|
|
209
|
+
assert result.exit_code != 0
|
|
210
|
+
assert "must be one of" in result.stdout or "must be one of" in result.stderr
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_cli_export_space_with_different_types(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
214
|
+
client = DummyExportClient()
|
|
215
|
+
monkeypatch.setattr("confluence_orchestrator.cli._build_http_client", lambda *args: client)
|
|
216
|
+
|
|
217
|
+
for export_type in ("pdf", "html", "xml"):
|
|
218
|
+
result = runner.invoke(app, ["export", "space", "--space-key", "ABC", "--export-type", export_type])
|
|
219
|
+
assert result.exit_code == 0
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_cli_export_invalid_action(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
223
|
+
client = DummyExportClient()
|
|
224
|
+
monkeypatch.setattr("confluence_orchestrator.cli._build_http_client", lambda *args: client)
|
|
225
|
+
|
|
226
|
+
result = runner.invoke(app, ["export", "invalid"])
|
|
227
|
+
|
|
228
|
+
assert result.exit_code != 0
|
|
229
|
+
assert "Unknown action" in result.stdout or "Unknown action" in result.stderr
|
|
230
|
+
|