// test/tag-matching.test.ts
import { describe, it } from 'node:test';
import assert from 'node:assert';
/**
* Test case-insensitive tag matching
*
* These tests verify that tags are normalized to lowercase and matched
* case-insensitively throughout the system.
*/
describe('Tag Matching', () => {
describe('Tag normalization on parse', () => {
it('should normalize uppercase tags to lowercase', () => {
// Test tag extraction with various cases
const testCases = [
{ input: '#Dev #Learn', expected: ['dev', 'learn'] },
{ input: '#DEV #LEARN', expected: ['dev', 'learn'] },
{ input: '#dev #learn', expected: ['dev', 'learn'] },
{ input: '#Dev #learn #TEST', expected: ['dev', 'learn', 'test'] },
];
for (const { input, expected } of testCases) {
const tags = input.match(/#\w+/g)?.map(t => t.substring(1).toLowerCase()) || [];
assert.deepStrictEqual(tags, expected,
`Tags "${input}" should normalize to ${JSON.stringify(expected)}`);
}
});
it('should handle empty tag strings', () => {
const tags = ''.match(/#\w+/g)?.map(t => t.substring(1).toLowerCase()) || [];
assert.deepStrictEqual(tags, []);
});
it('should handle mixed case tags', () => {
const input = '#MixedCase #ALLCAPS #lowercase';
const tags = input.match(/#\w+/g)?.map(t => t.substring(1).toLowerCase()) || [];
assert.deepStrictEqual(tags, ['mixedcase', 'allcaps', 'lowercase']);
});
it('should handle duplicate tags with different capitalization', () => {
const input = '#dev #Dev #DEV #test';
const tags = input.match(/#\w+/g)?.map(t => t.substring(1).toLowerCase()) || [];
// Current behavior: duplicates are NOT automatically deduplicated
assert.deepStrictEqual(tags, ['dev', 'dev', 'dev', 'test'],
'Tags are normalized but not deduplicated during parsing');
// But can be deduplicated if needed
const uniqueTags = [...new Set(tags)];
assert.deepStrictEqual(uniqueTags, ['dev', 'test'],
'Duplicates can be removed with Set');
});
});
describe('Tag mapping lookup', () => {
it('should match tag mappings case-insensitively', () => {
const tagMappings = {
'dev': 'development',
'sync': 'meeting'
};
// Simulate case-insensitive lookup
const testCases = [
{ tag: 'dev', expected: 'development' },
{ tag: 'Dev', expected: 'development' },
{ tag: 'DEV', expected: 'development' },
{ tag: 'sync', expected: 'meeting' },
{ tag: 'SYNC', expected: 'meeting' },
];
for (const { tag, expected } of testCases) {
const normalizedTag = tag.toLowerCase();
let mappedTag = normalizedTag;
// Case-insensitive lookup
for (const [key, value] of Object.entries(tagMappings)) {
if (key.toLowerCase() === normalizedTag) {
mappedTag = value;
break;
}
}
assert.strictEqual(mappedTag, expected,
`Tag "${tag}" should map to "${expected}"`);
}
});
it('should handle unmapped tags', () => {
const tagMappings = {
'dev': 'development'
};
const tag = 'unmapped';
const normalizedTag = tag.toLowerCase();
let mappedTag = normalizedTag;
for (const [key, value] of Object.entries(tagMappings)) {
if (key.toLowerCase() === normalizedTag) {
mappedTag = value;
break;
}
}
assert.strictEqual(mappedTag, 'unmapped',
'Unmapped tags should return normalized version');
});
it('should handle mixed case in config mapping keys', () => {
// Config might have mixed case keys (though lowercase is recommended)
const tagMappings = {
'Dev': 'development',
'SYNC': 'meeting'
};
const testCases = [
{ tag: 'dev', expected: 'development' },
{ tag: 'Dev', expected: 'development' },
{ tag: 'DEV', expected: 'development' },
{ tag: 'sync', expected: 'meeting' },
{ tag: 'Sync', expected: 'meeting' },
];
for (const { tag, expected } of testCases) {
const normalizedTag = tag.toLowerCase();
let mappedTag = normalizedTag;
// Case-insensitive lookup (matches implementation in summary-calculator.ts)
for (const [key, value] of Object.entries(tagMappings)) {
if (key.toLowerCase() === normalizedTag) {
mappedTag = value.toLowerCase();
break;
}
}
assert.strictEqual(mappedTag, expected,
`Tag "${tag}" should map to "${expected}" even with mixed case config keys`);
}
});
});
describe('Commitment matching', () => {
it('should match commitments case-insensitively', () => {
const commitments = {
'development': { limit: 20 },
'meeting': { limit: 5 },
'total': { limit: 25 }
};
const testCases = [
'development',
'Development',
'DEVELOPMENT',
'meeting',
'Meeting',
'MEETING'
];
for (const tag of testCases) {
const normalizedTag = tag.toLowerCase();
let found = false;
for (const commitmentKey of Object.keys(commitments)) {
if (commitmentKey.toLowerCase() === normalizedTag) {
found = true;
break;
}
}
assert.strictEqual(found, true,
`Tag "${tag}" should match a commitment`);
}
});
});
describe('Project tag matching', () => {
it('should match project tags case-insensitively', () => {
const projectTags = ['frontend', 'backend', 'testing'];
const normalizedProjectTags = projectTags.map(t => t.toLowerCase());
const testCases = [
{ tag: 'frontend', expected: true },
{ tag: 'Frontend', expected: true },
{ tag: 'FRONTEND', expected: true },
{ tag: 'backend', expected: true },
{ tag: 'Backend', expected: true },
{ tag: 'unknown', expected: false },
];
for (const { tag, expected } of testCases) {
const normalizedTag = tag.toLowerCase();
const found = normalizedProjectTags.includes(normalizedTag);
assert.strictEqual(found, expected,
`Tag "${tag}" should ${expected ? 'match' : 'not match'} project tags`);
}
});
});
});