Merge ba7b61781d into bd1c0053d5
This commit is contained in:
commit
3677472efc
|
|
@ -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);
|
||||||
|
|
@ -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]
|
// HTTPS URL: https://host/owner/repo[/tree/branch/subdir][.git]
|
||||||
const httpsMatch = trimmed.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/.]+?)(?:\.git)?(\/.*)?$/);
|
const httpsMatch = trimmed.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/.]+?)(?:\.git)?(\/.*)?$/);
|
||||||
if (httpsMatch) {
|
if (httpsMatch) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue