mirror of
https://github.com/yamadashy/repomix.git
synced 2026-02-03 11:33:39 +01:00
- Fix extractRepoName to handle trailing slashes, query strings, and fragments in URLs (e.g., https://github.com/user/repo/ now works) - Extract toTitleCase helper function to reduce code duplication - Use projectName instead of sourceUrl for link text in SKILL.md footer (displays as "from [Vite](https://...)" instead of full URL twice) - Add tests for URL edge cases (trailing slash, query, fragment)
175 lines
6.6 KiB
TypeScript
175 lines
6.6 KiB
TypeScript
import { describe, expect, test } from 'vitest';
|
|
import {
|
|
generateProjectName,
|
|
generateProjectNameFromUrl,
|
|
generateSkillDescription,
|
|
toKebabCase,
|
|
validateSkillName,
|
|
} from '../../../src/core/skill/skillUtils.js';
|
|
|
|
describe('skillUtils', () => {
|
|
describe('toKebabCase', () => {
|
|
test('should convert PascalCase to kebab-case', () => {
|
|
expect(toKebabCase('MyProjectName')).toBe('my-project-name');
|
|
});
|
|
|
|
test('should convert camelCase to kebab-case', () => {
|
|
expect(toKebabCase('myProjectName')).toBe('my-project-name');
|
|
});
|
|
|
|
test('should convert snake_case to kebab-case', () => {
|
|
expect(toKebabCase('my_project_name')).toBe('my-project-name');
|
|
});
|
|
|
|
test('should convert spaces to hyphens', () => {
|
|
expect(toKebabCase('my project name')).toBe('my-project-name');
|
|
});
|
|
|
|
test('should remove invalid characters', () => {
|
|
expect(toKebabCase('my@project#name!')).toBe('myprojectname');
|
|
});
|
|
|
|
test('should collapse multiple hyphens', () => {
|
|
expect(toKebabCase('my--project--name')).toBe('my-project-name');
|
|
});
|
|
|
|
test('should trim leading and trailing hyphens', () => {
|
|
expect(toKebabCase('-my-project-name-')).toBe('my-project-name');
|
|
});
|
|
|
|
test('should handle already kebab-case strings', () => {
|
|
expect(toKebabCase('my-project-name')).toBe('my-project-name');
|
|
});
|
|
|
|
test('should handle empty string', () => {
|
|
expect(toKebabCase('')).toBe('');
|
|
});
|
|
|
|
test('should handle mixed case with numbers', () => {
|
|
// Numbers don't trigger hyphen insertion (only lowercase-to-uppercase transitions do)
|
|
expect(toKebabCase('MyProject123Name')).toBe('my-project123name');
|
|
});
|
|
});
|
|
|
|
describe('validateSkillName', () => {
|
|
test('should return kebab-case name', () => {
|
|
expect(validateSkillName('MyProject')).toBe('my-project');
|
|
});
|
|
|
|
test('should truncate to 64 characters', () => {
|
|
const longName = 'a'.repeat(100);
|
|
expect(validateSkillName(longName).length).toBe(64);
|
|
});
|
|
|
|
test('should throw error for empty name after normalization', () => {
|
|
expect(() => validateSkillName('!@#$%')).toThrow('Skill name cannot be empty after normalization');
|
|
});
|
|
|
|
test('should handle valid kebab-case names', () => {
|
|
expect(validateSkillName('my-valid-skill-name')).toBe('my-valid-skill-name');
|
|
});
|
|
|
|
// Security tests for path traversal prevention
|
|
test('should reject path traversal attempts with forward slashes', () => {
|
|
expect(() => validateSkillName('../../../etc/passwd')).toThrow('Skill name cannot contain path separators');
|
|
expect(() => validateSkillName('foo/bar')).toThrow('Skill name cannot contain path separators');
|
|
});
|
|
|
|
test('should reject path traversal attempts with backslashes', () => {
|
|
expect(() => validateSkillName('..\\..\\etc\\passwd')).toThrow('Skill name cannot contain path separators');
|
|
expect(() => validateSkillName('foo\\bar')).toThrow('Skill name cannot contain path separators');
|
|
});
|
|
|
|
test('should reject null bytes', () => {
|
|
expect(() => validateSkillName('foo\0bar')).toThrow('Skill name cannot contain path separators or null bytes');
|
|
});
|
|
|
|
test('should reject dot-only names', () => {
|
|
expect(() => validateSkillName('.')).toThrow('Skill name cannot consist only of dots');
|
|
expect(() => validateSkillName('..')).toThrow('Skill name cannot consist only of dots');
|
|
expect(() => validateSkillName('...')).toThrow('Skill name cannot consist only of dots');
|
|
});
|
|
});
|
|
|
|
describe('generateProjectNameFromUrl', () => {
|
|
test('should extract and convert GitHub URL to title case', () => {
|
|
expect(generateProjectNameFromUrl('https://github.com/vitejs/vite')).toBe('Vite');
|
|
});
|
|
|
|
test('should handle kebab-case repo names', () => {
|
|
expect(generateProjectNameFromUrl('https://github.com/yamadashy/repomix')).toBe('Repomix');
|
|
});
|
|
|
|
test('should handle snake_case repo names', () => {
|
|
expect(generateProjectNameFromUrl('https://github.com/user/my_project_name')).toBe('My Project Name');
|
|
});
|
|
|
|
test('should handle shorthand format', () => {
|
|
expect(generateProjectNameFromUrl('yamadashy/repomix')).toBe('Repomix');
|
|
});
|
|
|
|
test('should handle .git suffix', () => {
|
|
expect(generateProjectNameFromUrl('https://github.com/vitejs/vite.git')).toBe('Vite');
|
|
});
|
|
|
|
test('should handle trailing slash', () => {
|
|
expect(generateProjectNameFromUrl('https://github.com/vitejs/vite/')).toBe('Vite');
|
|
});
|
|
|
|
test('should handle multiple trailing slashes', () => {
|
|
expect(generateProjectNameFromUrl('https://github.com/vitejs/vite///')).toBe('Vite');
|
|
});
|
|
|
|
test('should handle query string', () => {
|
|
expect(generateProjectNameFromUrl('https://github.com/vitejs/vite?tab=readme')).toBe('Vite');
|
|
});
|
|
|
|
test('should handle fragment', () => {
|
|
expect(generateProjectNameFromUrl('https://github.com/vitejs/vite#installation')).toBe('Vite');
|
|
});
|
|
|
|
test('should handle trailing slash with .git suffix', () => {
|
|
expect(generateProjectNameFromUrl('https://github.com/vitejs/vite.git/')).toBe('Vite');
|
|
});
|
|
});
|
|
|
|
describe('generateProjectName', () => {
|
|
test('should convert directory name to title case', () => {
|
|
expect(generateProjectName(['my-project'])).toBe('My Project');
|
|
});
|
|
|
|
test('should handle snake_case directory names', () => {
|
|
expect(generateProjectName(['my_project_name'])).toBe('My Project Name');
|
|
});
|
|
|
|
test('should use first directory when multiple provided', () => {
|
|
expect(generateProjectName(['first-project', 'second-project'])).toBe('First Project');
|
|
});
|
|
|
|
test('should handle current directory', () => {
|
|
// This depends on the actual directory name, so we just check it returns something
|
|
const result = generateProjectName(['.']);
|
|
expect(result).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe('generateSkillDescription', () => {
|
|
test('should generate description with skill and project names', () => {
|
|
const description = generateSkillDescription('my-skill', 'My Project');
|
|
expect(description).toContain('My Project');
|
|
expect(description).toContain('Reference codebase');
|
|
});
|
|
|
|
test('should truncate to 1024 characters', () => {
|
|
const longProjectName = 'A'.repeat(1000);
|
|
const description = generateSkillDescription('my-skill', longProjectName);
|
|
expect(description.length).toBeLessThanOrEqual(1024);
|
|
});
|
|
|
|
test('should include usage guidance', () => {
|
|
const description = generateSkillDescription('my-skill', 'My Project');
|
|
expect(description).toContain('Use this skill when');
|
|
});
|
|
});
|
|
});
|