Compare commits

...

4 Commits

Author SHA1 Message Date
Tankatronic 3677472efc
Merge ba7b61781d into bd1c0053d5 2026-04-19 09:56:03 +02:00
Tankatronic ba7b61781d
Merge pull request #1 from Tankatronic/fix/azure-devops-url-parsing-1
Add Azure DevOps URL parsing tests
2026-04-15 12:14:56 -07:00
Tankatronic 8d75e2808c
Add Azure DevOps URL parsing tests
This test file verifies the functionality of the CustomModuleManager's parseSource method for Azure DevOps Git URLs, including both dev.azure.com and legacy visualstudio.com formats. It includes various assertions to check the validity and expected outputs for different URL formats.
2026-04-15 12:14:33 -07:00
Tankatronic 8365a1c869
Implement Azure DevOps URL parsing for repositories
Added support for Azure DevOps URL parsing, including both modern and legacy formats, to extract relevant repository information.
2026-04-15 12:13:49 -07:00
2 changed files with 209 additions and 0 deletions

View File

@ -0,0 +1,159 @@
/**
* Azure DevOps URL Parsing Tests
*
* Verifies that CustomModuleManager.parseSource() correctly handles
* Azure DevOps Git URLs (dev.azure.com and legacy visualstudio.com).
*
* Fixes: https://github.com/bmad-code-org/BMAD-METHOD/issues/2268
* Usage: node test/test-azure-devops-url-parsing.js
*/
const { CustomModuleManager } = require('../tools/installer/modules/custom-module-manager');
// ANSI colors
const colors = {
reset: '\u001B[0m',
green: '\u001B[32m',
red: '\u001B[31m',
cyan: '\u001B[36m',
dim: '\u001B[2m',
};
let passed = 0;
let failed = 0;
function assert(condition, testName, errorMessage = '') {
if (condition) {
console.log(`${colors.green}${colors.reset} ${testName}`);
passed++;
} else {
console.log(`${colors.red}${colors.reset} ${testName}`);
if (errorMessage) {
console.log(` ${colors.dim}${errorMessage}${colors.reset}`);
}
failed++;
}
}
const manager = new CustomModuleManager();
// ─── Azure DevOps: dev.azure.com ────────────────────────────────────────────
console.log(`\n${colors.cyan}Azure DevOps URL parsing (dev.azure.com)${colors.reset}\n`);
{
const result = manager.parseSource('https://dev.azure.com/myorg/MyProject/_git/my-module');
assert(result.isValid === true, 'dev.azure.com basic URL is valid');
assert(result.type === 'url', 'dev.azure.com type is url');
assert(
result.cloneUrl === 'https://dev.azure.com/myorg/MyProject/_git/my-module',
'dev.azure.com cloneUrl preserves full _git path',
`Got: ${result.cloneUrl}`,
);
assert(result.subdir === null, 'dev.azure.com basic URL has no subdir');
assert(
result.cacheKey === 'dev.azure.com/myorg/MyProject/my-module',
'dev.azure.com cacheKey includes org/project/repo',
`Got: ${result.cacheKey}`,
);
assert(
result.displayName === 'MyProject/my-module',
'dev.azure.com displayName is project/repo',
`Got: ${result.displayName}`,
);
}
{
const result = manager.parseSource('https://dev.azure.com/myorg/MyProject/_git/my-module.git');
assert(result.isValid === true, 'dev.azure.com URL with .git suffix is valid');
assert(
result.cloneUrl === 'https://dev.azure.com/myorg/MyProject/_git/my-module',
'dev.azure.com .git suffix stripped from cloneUrl',
`Got: ${result.cloneUrl}`,
);
}
{
const result = manager.parseSource('https://dev.azure.com/myorg/MyProject/_git/my-module/path/to/subdir');
assert(result.isValid === true, 'dev.azure.com URL with subdir path is valid');
assert(
result.cloneUrl === 'https://dev.azure.com/myorg/MyProject/_git/my-module',
'dev.azure.com subdir URL cloneUrl excludes subdir',
`Got: ${result.cloneUrl}`,
);
assert(
result.subdir === 'path/to/subdir',
'dev.azure.com subdir correctly extracted',
`Got: ${result.subdir}`,
);
}
// ─── Azure DevOps: legacy visualstudio.com ──────────────────────────────────
console.log(`\n${colors.cyan}Azure DevOps URL parsing (visualstudio.com)${colors.reset}\n`);
{
const result = manager.parseSource('https://myorg.visualstudio.com/MyProject/_git/my-module');
assert(result.isValid === true, 'visualstudio.com basic URL is valid');
assert(result.type === 'url', 'visualstudio.com type is url');
assert(
result.cloneUrl === 'https://myorg.visualstudio.com/MyProject/_git/my-module',
'visualstudio.com cloneUrl preserves full _git path',
`Got: ${result.cloneUrl}`,
);
assert(result.subdir === null, 'visualstudio.com basic URL has no subdir');
assert(
result.cacheKey === 'myorg.visualstudio.com/MyProject/my-module',
'visualstudio.com cacheKey is host/project/repo',
`Got: ${result.cacheKey}`,
);
}
// ─── Non-Azure URLs still work ──────────────────────────────────────────────
console.log(`\n${colors.cyan}Non-Azure HTTPS URLs (regression check)${colors.reset}\n`);
{
const result = manager.parseSource('https://github.com/owner/repo');
assert(result.isValid === true, 'GitHub basic URL still valid');
assert(
result.cloneUrl === 'https://github.com/owner/repo',
'GitHub cloneUrl unchanged',
`Got: ${result.cloneUrl}`,
);
assert(
result.cacheKey === 'github.com/owner/repo',
'GitHub cacheKey unchanged',
`Got: ${result.cacheKey}`,
);
}
{
const result = manager.parseSource('https://github.com/owner/repo/tree/main/subdir');
assert(result.isValid === true, 'GitHub URL with tree path still valid');
assert(
result.cloneUrl === 'https://github.com/owner/repo',
'GitHub tree URL cloneUrl correct',
`Got: ${result.cloneUrl}`,
);
assert(
result.subdir === 'subdir',
'GitHub tree subdir still extracted',
`Got: ${result.subdir}`,
);
}
{
const result = manager.parseSource('git@github.com:owner/repo.git');
assert(result.isValid === true, 'SSH URL still valid');
assert(
result.cloneUrl === 'git@github.com:owner/repo.git',
'SSH cloneUrl unchanged',
`Got: ${result.cloneUrl}`,
);
}
// ─── Summary ────────────────────────────────────────────────────────────────
console.log(`\n${colors.cyan}Results: ${passed} passed, ${failed} failed${colors.reset}\n`);
process.exit(failed > 0 ? 1 : 0);

View File

@ -73,6 +73,56 @@ class CustomModuleManager {
};
}
// Azure DevOps URL: https://dev.azure.com/{org}/{project}/_git/{repo}
// Also supports legacy: https://{org}.visualstudio.com/{project}/_git/{repo}
const adoModernMatch = trimmed.match(
/^https?:\/\/(dev\.azure\.com)\/([^/]+)\/([^/]+)\/_git\/([^/.]+?)(?:\.git)?(\/.*)?$/,
);
const adoLegacyMatch =
!adoModernMatch &&
trimmed.match(
/^https?:\/\/([^/]+\.visualstudio\.com)\/([^/]+)\/_git\/([^/.]+?)(?:\.git)?(\/.*)?$/,
);
const adoMatch = adoModernMatch || adoLegacyMatch;
if (adoMatch) {
let host, org, project, repo, remainder;
if (adoModernMatch) {
[, host, org, project, repo, remainder] = adoModernMatch;
} else {
// Legacy: org is in the hostname, path is /{project}/_git/{repo}
[, host, project, repo, remainder] = adoLegacyMatch;
org = null;
}
const cloneUrl = adoModernMatch
? `https://${host}/${org}/${project}/_git/${repo}`
: `https://${host}/${project}/_git/${repo}`;
let subdir = null;
if (remainder) {
// Azure DevOps uses ?path=/subdir or /path/subdir patterns
const subdirMatch = remainder.match(/^\/(.+)$/);
if (subdirMatch) {
subdir = subdirMatch[1].replace(/\/$/, '');
}
}
const cacheKey = adoModernMatch
? `${host}/${org}/${project}/${repo}`
: `${host}/${project}/${repo}`;
return {
type: 'url',
cloneUrl,
subdir,
localPath: null,
cacheKey,
displayName: `${project}/${repo}`,
isValid: true,
error: null,
};
}
// HTTPS URL: https://host/owner/repo[/tree/branch/subdir][.git]
const httpsMatch = trimmed.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/.]+?)(?:\.git)?(\/.*)?$/);
if (httpsMatch) {