Updated product group parser to include tech leads and Security & compliance group. (#33849)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #33848

Verified by running the GitHub Action:

```
***"timestamp":"2025-10-05T21:10:43.408Z","level":"info","message":"Parsing product groups from /home/runner/work/fleet/fleet/handbook/company/product-groups.md"***
***"timestamp":"2025-10-05T21:10:43.409Z","level":"info","message":"Found 1 tech lead(s) in mdm group: JordanMontgomery"***
***"timestamp":"2025-10-05T21:10:43.409Z","level":"info","message":"Found 3 developer(s) in mdm group: gillespi314, ghernandez345, MagnusHJensen"***
***"timestamp":"2025-10-05T21:10:43.409Z","level":"info","message":"Found 1 tech lead(s) in orchestration group: lucasmrod"***
***"timestamp":"2025-10-05T21:10:43.410Z","level":"info","message":"Found 4 developer(s) in orchestration group: sgress454, juan-fdz-hawa, iansltx, ksykulev"***
***"timestamp":"2025-10-05T21:10:43.410Z","level":"info","message":"Found 1 tech lead(s) in software group: cdcme"***
***"timestamp":"2025-10-05T21:10:43.410Z","level":"info","message":"Found 4 developer(s) in software group: rachelelysia, jahzielv, jkatz01, mna"***
***"timestamp":"2025-10-05T21:10:43.410Z","level":"info","message":"Found 1 tech lead(s) in security-compliance group: getvictor"***
***"timestamp":"2025-10-05T21:10:43.410Z","level":"info","message":"Found 3 developer(s) in security-compliance group: dantecatalfamo, jacobshandling, mostlikelee"***
```

# Checklist for submitter

This is not a product change. Only an update to gathering engineering
metrics.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- New Features
  - Adds support for the Security & compliance group.
- Extracts and reports Tech Lead memberships, including inclusion in
overall engineering stats.
- Bug Fixes
- More reliable username parsing (handles hyphens, numbers, multi-line
cells).
- Stricter validation halts on missing sections or rows to prevent
partial/inaccurate results, with clearer error messages.
- Tests
- Expanded coverage for new groups, tech leads, error paths, and
edge-case username formats.
- Chores
  - Updated dependencies for stability and maintenance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Victor Lyuboslavsky
2025-10-06 13:44:18 -05:00
committed by GitHub
parent 6eefc8ecb5
commit 91c15314b7
5 changed files with 898 additions and 4051 deletions
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -29,14 +29,14 @@
"analytics"
],
"dependencies": {
"@google-cloud/bigquery": "^8.1.0",
"dotenv": "^17.0.0",
"@google-cloud/bigquery": "^8.1.1",
"dotenv": "^17.2.3",
"octokit": "^5.0.3"
},
"devDependencies": {
"@eslint/js": "^9.30.0",
"eslint": "^9.30.0",
"jest": "^30.0.3"
"@eslint/js": "^9.37.0",
"eslint": "^9.37.0",
"jest": "^30.2.0"
},
"engines": {
"node": "20.18.1"
@@ -34,6 +34,7 @@ export const parseProductGroups = (filePath) => {
* Extracts usernames from markdown content
* @param {string} content - Markdown content
* @returns {Array<{group: string, username: string}>} Array of user group mappings
* @throws {Error} If required sections are missing or validation fails
*/
const extractUsernamesFromMarkdown = (content) => {
const userGroups = [];
@@ -43,6 +44,7 @@ const extractUsernamesFromMarkdown = (content) => {
'MDM group': 'mdm',
'Orchestration group': 'orchestration',
'Software group': 'software',
'Security & compliance group': 'security-compliance',
};
// For each group, find its section and extract usernames
@@ -56,10 +58,17 @@ const extractUsernamesFromMarkdown = (content) => {
if (sectionMatch) {
const sectionContent = sectionMatch[1];
const usernames = extractUsernamesFromSection(sectionContent, groupName);
userGroups.push(...usernames);
try {
const usernames = extractUsernamesFromSection(sectionContent, groupName);
userGroups.push(...usernames);
} catch (err) {
logger.error(`Error extracting usernames from ${sectionName}`, {}, err);
throw err;
}
} else {
logger.warn(`Section not found: ${sectionName}`);
const error = new Error(`Section not found: ${sectionName}`);
logger.error(error.message);
throw error;
}
}
@@ -72,12 +81,38 @@ const extractUsernamesFromMarkdown = (content) => {
/**
* Extracts usernames from a specific section
* @param {string} sectionContent - Content of the section
* @param {string} groupName - Name of the group (mdm, orchestration, software)
* @param {string} groupName - Name of the group (mdm, orchestration, software, security-compliance)
* @returns {Array<{group: string, username: string}>} Array of user group mappings
* @throws {Error} If Tech Lead or Developer requirements are not met
*/
const extractUsernamesFromSection = (sectionContent, groupName) => {
const userGroups = [];
// Look for the Tech Lead row in the table
const techLeadRowMatch = sectionContent.match(
/\|\s*Tech Lead\s*\|\s*([\s\S]*?)(?=\n\||\n\n|$)/
);
if (!techLeadRowMatch) {
throw new Error(`No Tech Lead row found in ${groupName} group section`);
}
const techLeadCell = techLeadRowMatch[1];
const techLeadUsernames = extractUsernamesFromCell(techLeadCell);
if (techLeadUsernames.length === 0) {
throw new Error(`No Tech Lead found in ${groupName} group`);
}
logger.info(
`Found ${techLeadUsernames.length} tech lead(s) in ${groupName} group: ${techLeadUsernames.join(', ')}`
);
for (const username of techLeadUsernames) {
userGroups.push({ group: groupName, username });
userGroups.push({ group: 'engineering', username });
}
// Look for the Developer row in the table
// The pattern needs to handle multi-line content in the cell
const developerRowMatch = sectionContent.match(
@@ -85,40 +120,25 @@ const extractUsernamesFromSection = (sectionContent, groupName) => {
);
if (!developerRowMatch) {
logger.warn(`No Developer row found in ${groupName} group section`);
return userGroups;
throw new Error(`No Developer row found in ${groupName} group section`);
}
const developerCell = developerRowMatch[1];
const developerUsernames = extractUsernamesFromCell(developerCell);
// Extract GitHub usernames from the developer cell
// Look for patterns like [@username](https://github.com/username)
// Note: This match could fail with slight variations in formatting (extra spaces, different brackets, etc.).
const usernameMatches = developerCell.match(/\[@([a-zA-Z0-9-]+)]\([^)]+\)/g);
if (!usernameMatches) {
logger.warn(
`No GitHub usernames found in ${groupName} group Developer row`
if (developerUsernames.length === 0) {
throw new Error(
`No developers found in ${groupName} group Developer row`
);
return userGroups;
}
const usernames = usernameMatches
.map((match) => {
// Extract username from _([@username](url))_ format
const usernameMatch = match.match(/\[@([a-zA-Z0-9-]+)]/);
return usernameMatch ? usernameMatch[1] : null;
})
.filter(Boolean);
logger.info(
`Found ${usernames.length
} developers in ${groupName} group: ${usernames.join(', ')}`
`Found ${developerUsernames.length} developer(s) in ${groupName} group: ${developerUsernames.join(', ')}`
);
// Create user group mappings for both the specific group and engineering
for (const username of usernames) {
// Add to specific group (mdm, orchestration, software)
for (const username of developerUsernames) {
// Add to specific group (mdm, orchestration, software, security-compliance)
userGroups.push({ group: groupName, username });
// Add to engineering group (all developers are in engineering)
@@ -128,6 +148,30 @@ const extractUsernamesFromSection = (sectionContent, groupName) => {
return userGroups;
};
/**
* Extracts GitHub usernames from a table cell
* @param {string} cellContent - Content of the table cell
* @returns {Array<string>} Array of GitHub usernames
*/
const extractUsernamesFromCell = (cellContent) => {
// Extract GitHub usernames from the cell
// Look for patterns like [@username](https://github.com/username)
// Note: This match could fail with slight variations in formatting (extra spaces, different brackets, etc.).
const usernameMatches = cellContent.match(/\[@([a-zA-Z0-9-]+)]\([^)]+\)/g);
if (!usernameMatches) {
return [];
}
return usernameMatches
.map((match) => {
// Extract username from _([@username](url))_ format
const usernameMatch = match.match(/\[@([a-zA-Z0-9-]+)]/);
return usernameMatch ? usernameMatch[1] : null;
})
.filter(Boolean);
};
/**
* Validates the structure of the markdown content
* @param {string} content - Markdown content to validate
@@ -138,6 +182,7 @@ export const validateMarkdownStructure = (content) => {
'MDM group',
'Orchestration group',
'Software group',
'Security & compliance group',
];
for (const section of requiredSections) {
@@ -60,6 +60,7 @@ describe('MarkdownParser', () => {
| Role | Contributor |
|------|-------------|
| Product Manager | _([@testpm1](https://github.com/testpm1))_ |
| Tech Lead | _([@techlead1](https://github.com/techlead1))_ |
| Developer | _([@testdev1](https://github.com/testdev1))_, _([@testdev2](https://github.com/testdev2))_, _([@testdev3](https://github.com/testdev3))_ |
| Quality Assurance | _([@testqa1](https://github.com/testqa1))_ |
@@ -68,14 +69,23 @@ describe('MarkdownParser', () => {
| Role | Contributor |
|------|-------------|
| Product Manager | _([@testpm2](https://github.com/testpm2))_ |
| Tech Lead | _([@techlead2](https://github.com/techlead2))_ |
| Developer | _([@orchdev1](https://github.com/orchdev1))_, _([@orchdev2](https://github.com/orchdev2))_, _([@orchdev3](https://github.com/orchdev3))_ |
### Software group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead3](https://github.com/techlead3))_ |
| Developer | _([@softdev1](https://github.com/softdev1))_, _([@softdev2](https://github.com/softdev2))_ |
| Quality Assurance | _([@testqa2](https://github.com/testqa2))_ |
### Security & compliance group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead4](https://github.com/techlead4))_ |
| Developer | _([@secdev1](https://github.com/secdev1))_, _([@secdev2](https://github.com/secdev2))_ |
`;
mockFs.existsSync.mockReturnValue(true);
@@ -88,35 +98,53 @@ describe('MarkdownParser', () => {
// Should extract usernames and create dual group membership
expect(result).toEqual([
// MDM group users
// MDM group users (tech lead + developers)
{ group: 'mdm', username: 'techlead1' },
{ group: 'engineering', username: 'techlead1' },
{ group: 'mdm', username: 'testdev1' },
{ group: 'engineering', username: 'testdev1' },
{ group: 'mdm', username: 'testdev2' },
{ group: 'engineering', username: 'testdev2' },
{ group: 'mdm', username: 'testdev3' },
{ group: 'engineering', username: 'testdev3' },
// Orchestration group users
// Orchestration group users (tech lead + developers)
{ group: 'orchestration', username: 'techlead2' },
{ group: 'engineering', username: 'techlead2' },
{ group: 'orchestration', username: 'orchdev1' },
{ group: 'engineering', username: 'orchdev1' },
{ group: 'orchestration', username: 'orchdev2' },
{ group: 'engineering', username: 'orchdev2' },
{ group: 'orchestration', username: 'orchdev3' },
{ group: 'engineering', username: 'orchdev3' },
// Software group users
// Software group users (tech lead + developers)
{ group: 'software', username: 'techlead3' },
{ group: 'engineering', username: 'techlead3' },
{ group: 'software', username: 'softdev1' },
{ group: 'engineering', username: 'softdev1' },
{ group: 'software', username: 'softdev2' },
{ group: 'engineering', username: 'softdev2' }
{ group: 'engineering', username: 'softdev2' },
// Security & compliance group users (tech lead + developers)
{ group: 'security-compliance', username: 'techlead4' },
{ group: 'engineering', username: 'techlead4' },
{ group: 'security-compliance', username: 'secdev1' },
{ group: 'engineering', username: 'secdev1' },
{ group: 'security-compliance', username: 'secdev2' },
{ group: 'engineering', username: 'secdev2' }
]);
expect(mockLogger.info).toHaveBeenCalledWith('Parsing product groups from /resolved/test-file.md');
expect(mockLogger.info).toHaveBeenCalledWith('Found 3 developers in mdm group: testdev1, testdev2, testdev3');
expect(mockLogger.info).toHaveBeenCalledWith('Found 3 developers in orchestration group: orchdev1, orchdev2, orchdev3');
expect(mockLogger.info).toHaveBeenCalledWith('Found 2 developers in software group: softdev1, softdev2');
expect(mockLogger.info).toHaveBeenCalledWith('Extracted 16 user-group mappings from markdown');
expect(mockLogger.info).toHaveBeenCalledWith('Found 1 tech lead(s) in mdm group: techlead1');
expect(mockLogger.info).toHaveBeenCalledWith('Found 3 developer(s) in mdm group: testdev1, testdev2, testdev3');
expect(mockLogger.info).toHaveBeenCalledWith('Found 1 tech lead(s) in orchestration group: techlead2');
expect(mockLogger.info).toHaveBeenCalledWith('Found 3 developer(s) in orchestration group: orchdev1, orchdev2, orchdev3');
expect(mockLogger.info).toHaveBeenCalledWith('Found 1 tech lead(s) in software group: techlead3');
expect(mockLogger.info).toHaveBeenCalledWith('Found 2 developer(s) in software group: softdev1, softdev2');
expect(mockLogger.info).toHaveBeenCalledWith('Found 1 tech lead(s) in security-compliance group: techlead4');
expect(mockLogger.info).toHaveBeenCalledWith('Found 2 developer(s) in security-compliance group: secdev1, secdev2');
expect(mockLogger.info).toHaveBeenCalledWith('Extracted 28 user-group mappings from markdown');
});
it('should handle missing sections gracefully', () => {
it('should throw error when sections are missing', () => {
const mockMarkdown = `
# Product Groups
@@ -124,12 +152,14 @@ describe('MarkdownParser', () => {
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead1](https://github.com/techlead1))_ |
| Developer | _([@testdev1](https://github.com/testdev1))_ |
### Software group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead2](https://github.com/techlead2))_ |
| Developer | _([@softdev1](https://github.com/softdev1))_ |
`;
@@ -138,17 +168,11 @@ describe('MarkdownParser', () => {
const result = parseProductGroups('test-file.md');
expect(result).toEqual([
{ group: 'mdm', username: 'testdev1' },
{ group: 'engineering', username: 'testdev1' },
{ group: 'software', username: 'softdev1' },
{ group: 'engineering', username: 'softdev1' }
]);
expect(mockLogger.warn).toHaveBeenCalledWith('Section not found: Orchestration group');
expect(result).toEqual([]);
expect(mockLogger.error).toHaveBeenCalledWith('Section not found: Orchestration group');
});
it('should handle missing Developer rows', () => {
it('should throw error when Tech Lead row is missing', () => {
const mockMarkdown = `
# Product Groups
@@ -157,13 +181,29 @@ describe('MarkdownParser', () => {
| Role | Contributor |
|------|-------------|
| Product Manager | _([@testpm1](https://github.com/testpm1))_ |
| Developer | _([@testdev1](https://github.com/testdev1))_ |
| Quality Assurance | _([@testqa1](https://github.com/testqa1))_ |
### Orchestration group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead1](https://github.com/techlead1))_ |
| Developer | _([@orchdev1](https://github.com/orchdev1))_ |
### Software group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead2](https://github.com/techlead2))_ |
| Developer | _([@softdev1](https://github.com/softdev1))_ |
### Security & compliance group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead3](https://github.com/techlead3))_ |
| Developer | _([@secdev1](https://github.com/secdev1))_ |
`;
mockFs.existsSync.mockReturnValue(true);
@@ -171,15 +211,11 @@ describe('MarkdownParser', () => {
const result = parseProductGroups('test-file.md');
expect(result).toEqual([
{ group: 'orchestration', username: 'orchdev1' },
{ group: 'engineering', username: 'orchdev1' }
]);
expect(mockLogger.warn).toHaveBeenCalledWith('No Developer row found in mdm group section');
expect(result).toEqual([]);
expect(mockLogger.error).toHaveBeenCalledWith('Error extracting usernames from MDM group', {}, expect.any(Error));
});
it('should handle malformed GitHub username patterns', () => {
it('should throw error when Developer row is missing', () => {
const mockMarkdown = `
# Product Groups
@@ -187,13 +223,30 @@ describe('MarkdownParser', () => {
| Role | Contributor |
|------|-------------|
| Developer | Some text without proper format, _([@testvaliduser](https://github.com/testvaliduser))_, invalid format here |
| Product Manager | _([@testpm1](https://github.com/testpm1))_ |
| Tech Lead | _([@techlead1](https://github.com/techlead1))_ |
| Quality Assurance | _([@testqa1](https://github.com/testqa1))_ |
### Orchestration group
| Role | Contributor |
|------|-------------|
| Developer | No valid usernames here at all |
| Tech Lead | _([@techlead2](https://github.com/techlead2))_ |
| Developer | _([@orchdev1](https://github.com/orchdev1))_ |
### Software group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead3](https://github.com/techlead3))_ |
| Developer | _([@softdev1](https://github.com/softdev1))_ |
### Security & compliance group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead4](https://github.com/techlead4))_ |
| Developer | _([@secdev1](https://github.com/secdev1))_ |
`;
mockFs.existsSync.mockReturnValue(true);
@@ -201,12 +254,92 @@ describe('MarkdownParser', () => {
const result = parseProductGroups('test-file.md');
expect(result).toEqual([
{ group: 'mdm', username: 'testvaliduser' },
{ group: 'engineering', username: 'testvaliduser' }
]);
expect(result).toEqual([]);
expect(mockLogger.error).toHaveBeenCalledWith('Error extracting usernames from MDM group', {}, expect.any(Error));
});
expect(mockLogger.warn).toHaveBeenCalledWith('No GitHub usernames found in orchestration group Developer row');
it('should throw error when no valid developer usernames found', () => {
const mockMarkdown = `
# Product Groups
### MDM group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead1](https://github.com/techlead1))_ |
| Developer | Some text without proper format, _([@testvaliduser](https://github.com/testvaliduser))_, invalid format here |
### Orchestration group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead2](https://github.com/techlead2))_ |
| Developer | No valid usernames here at all |
### Software group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead3](https://github.com/techlead3))_ |
| Developer | _([@softdev1](https://github.com/softdev1))_ |
### Security & compliance group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead4](https://github.com/techlead4))_ |
| Developer | _([@secdev1](https://github.com/secdev1))_ |
`;
mockFs.existsSync.mockReturnValue(true);
mockFs.readFileSync.mockReturnValue(mockMarkdown);
const result = parseProductGroups('test-file.md');
expect(result).toEqual([]);
expect(mockLogger.error).toHaveBeenCalledWith('Error extracting usernames from Orchestration group', {}, expect.any(Error));
});
it('should throw error when no Tech Lead usernames found', () => {
const mockMarkdown = `
# Product Groups
### MDM group
| Role | Contributor |
|------|-------------|
| Tech Lead | No valid usernames here |
| Developer | _([@testdev1](https://github.com/testdev1))_ |
### Orchestration group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead2](https://github.com/techlead2))_ |
| Developer | _([@orchdev1](https://github.com/orchdev1))_ |
### Software group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead3](https://github.com/techlead3))_ |
| Developer | _([@softdev1](https://github.com/softdev1))_ |
### Security & compliance group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead4](https://github.com/techlead4))_ |
| Developer | _([@secdev1](https://github.com/secdev1))_ |
`;
mockFs.existsSync.mockReturnValue(true);
mockFs.readFileSync.mockReturnValue(mockMarkdown);
const result = parseProductGroups('test-file.md');
expect(result).toEqual([]);
expect(mockLogger.error).toHaveBeenCalledWith('Error extracting usernames from MDM group', {}, expect.any(Error));
});
it('should handle multi-line developer cells', () => {
@@ -217,9 +350,31 @@ describe('MarkdownParser', () => {
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead1](https://github.com/techlead1))_ |
| Developer | _([@testuser1](https://github.com/testuser1))_,
_([@testuser2](https://github.com/testuser2))_,
_([@testuser3](https://github.com/testuser3))_ |
### Orchestration group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead2](https://github.com/techlead2))_ |
| Developer | _([@orchdev1](https://github.com/orchdev1))_ |
### Software group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead3](https://github.com/techlead3))_ |
| Developer | _([@softdev1](https://github.com/softdev1))_ |
### Security & compliance group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead4](https://github.com/techlead4))_ |
| Developer | _([@secdev1](https://github.com/secdev1))_ |
`;
mockFs.existsSync.mockReturnValue(true);
@@ -227,14 +382,14 @@ _([@testuser3](https://github.com/testuser3))_ |
const result = parseProductGroups('test-file.md');
expect(result).toEqual([
{ group: 'mdm', username: 'testuser1' },
{ group: 'engineering', username: 'testuser1' },
{ group: 'mdm', username: 'testuser2' },
{ group: 'engineering', username: 'testuser2' },
{ group: 'mdm', username: 'testuser3' },
{ group: 'engineering', username: 'testuser3' }
]);
expect(result).toContainEqual({ group: 'mdm', username: 'techlead1' });
expect(result).toContainEqual({ group: 'engineering', username: 'techlead1' });
expect(result).toContainEqual({ group: 'mdm', username: 'testuser1' });
expect(result).toContainEqual({ group: 'engineering', username: 'testuser1' });
expect(result).toContainEqual({ group: 'mdm', username: 'testuser2' });
expect(result).toContainEqual({ group: 'engineering', username: 'testuser2' });
expect(result).toContainEqual({ group: 'mdm', username: 'testuser3' });
expect(result).toContainEqual({ group: 'engineering', username: 'testuser3' });
});
it('should handle file not found', () => {
@@ -270,7 +425,29 @@ _([@testuser3](https://github.com/testuser3))_ |
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead-123](https://github.com/techlead-123))_ |
| Developer | _([@testuser-123](https://github.com/testuser-123))_, _([@fakeuser-456](https://github.com/fakeuser-456))_ |
### Orchestration group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead2](https://github.com/techlead2))_ |
| Developer | _([@orchdev1](https://github.com/orchdev1))_ |
### Software group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead3](https://github.com/techlead3))_ |
| Developer | _([@softdev1](https://github.com/softdev1))_ |
### Security & compliance group
| Role | Contributor |
|------|-------------|
| Tech Lead | _([@techlead4](https://github.com/techlead4))_ |
| Developer | _([@secdev1](https://github.com/secdev1))_ |
`;
mockFs.existsSync.mockReturnValue(true);
@@ -278,12 +455,12 @@ _([@testuser3](https://github.com/testuser3))_ |
const result = parseProductGroups('test-file.md');
expect(result).toEqual([
{ group: 'mdm', username: 'testuser-123' },
{ group: 'engineering', username: 'testuser-123' },
{ group: 'mdm', username: 'fakeuser-456' },
{ group: 'engineering', username: 'fakeuser-456' }
]);
expect(result).toContainEqual({ group: 'mdm', username: 'techlead-123' });
expect(result).toContainEqual({ group: 'engineering', username: 'techlead-123' });
expect(result).toContainEqual({ group: 'mdm', username: 'testuser-123' });
expect(result).toContainEqual({ group: 'engineering', username: 'testuser-123' });
expect(result).toContainEqual({ group: 'mdm', username: 'fakeuser-456' });
expect(result).toContainEqual({ group: 'engineering', username: 'fakeuser-456' });
});
});
@@ -300,6 +477,9 @@ Some content
### Software group
Some content
### Security & compliance group
Some content
`;
const result = validateMarkdownStructure(validMarkdown);
@@ -342,6 +522,9 @@ Some content
### software group
Some content
### security & compliance group
Some content
`;
const result = validateMarkdownStructure(invalidMarkdown);
File diff suppressed because it is too large Load Diff