Files
vibn-api/src/test/skill-service.test.ts

454 lines
14 KiB
TypeScript

// SkillService unit tests
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'bun:test';
import { eq } from 'drizzle-orm';
import { db } from '../db/client';
import {
categories,
marketplaceSkills,
skillCategories,
skillTags,
skillVersions,
tags,
users,
} from '../db/schema';
import { skillService } from '../services/skill-service';
import { clearDatabase, seedTestDatabase } from './fixtures';
let testUserId: string;
let testCategoryId: string;
beforeAll(async () => {
console.log('\n🔧 Setting up SkillService tests...\n');
const _testData = await seedTestDatabase();
// Get a test user
const usersResult = await db.select().from(users).limit(1);
if (usersResult.length === 0) {
throw new Error('No test users found');
}
testUserId = usersResult[0].id;
// Get a test category
const categoriesResult = await db.select().from(categories).limit(1);
if (categoriesResult.length === 0) {
throw new Error('No test categories found');
}
testCategoryId = categoriesResult[0].slug;
console.log('✅ SkillService test setup complete\n');
});
afterAll(async () => {
console.log('\n🧹 Cleaning up SkillService tests...\n');
await clearDatabase();
console.log('✅ Cleanup complete\n');
});
describe('SkillService - Create Skill', () => {
it('should create a skill successfully', async () => {
const skillData = {
name: `Test Skill ${Date.now()}`,
description: 'A test skill for unit testing',
longDescription: 'This is a detailed description of the test skill',
systemPromptFragment: 'You are a test assistant',
workflowRules: 'Follow test workflow',
documentation: [
{
type: 'inline' as const,
title: 'Getting Started',
content: 'This is how to use the test skill',
},
],
iconUrl: 'https://example.com/icon.png',
categories: [testCategoryId],
tags: ['test', 'unit-test', 'automation'],
};
const skill = await skillService.createSkill(testUserId, skillData);
expect(skill).toBeDefined();
expect(skill.id).toBeDefined();
expect(skill.name).toBe(skillData.name);
expect(skill.description).toBe(skillData.description);
expect(skill.systemPromptFragment).toBe(skillData.systemPromptFragment);
expect(skill.workflowRules).toBe(skillData.workflowRules);
expect(skill.latestVersion).toBe('1.0.0');
expect(skill.isPublished).toBe(false);
expect(skill.authorId).toBe(testUserId);
// Verify initial version was created
const versions = await db
.select()
.from(skillVersions)
.where(eq(skillVersions.skillId, skill.id));
expect(versions.length).toBe(1);
expect(versions[0].version).toBe('1.0.0');
expect(versions[0].changeLog).toBe('Initial release');
// Verify categories were linked
const linkedCategories = await db
.select()
.from(skillCategories)
.where(eq(skillCategories.skillId, skill.id));
expect(linkedCategories.length).toBeGreaterThan(0);
// Verify tags were created and linked
const linkedTags = await db.select().from(skillTags).where(eq(skillTags.skillId, skill.id));
expect(linkedTags.length).toBe(3);
});
it('should fail when creating skill with duplicate name', async () => {
const skillData = {
name: `Duplicate Skill ${Date.now()}`,
description: 'First skill',
documentation: [],
categories: [testCategoryId],
tags: [],
};
// Create first skill
await skillService.createSkill(testUserId, skillData);
// Try to create duplicate - should fail
try {
await skillService.createSkill(testUserId, skillData);
expect(true).toBe(false); // Should not reach here
} catch (error) {
expect(error).toBeDefined();
expect((error as Error).message).toContain('already exists');
}
});
it('should fail when creating skill without valid categories', async () => {
const skillData = {
name: `No Category Skill ${Date.now()}`,
description: 'Skill without categories',
documentation: [],
categories: ['non-existent-category'],
tags: [],
};
try {
await skillService.createSkill(testUserId, skillData);
expect(true).toBe(false); // Should not reach here
} catch (error) {
expect(error).toBeDefined();
expect((error as Error).message).toContain('No valid categories');
}
});
});
describe('SkillService - Update Skill', () => {
let testSkillId: string;
beforeEach(async () => {
// Create a test skill
const skillData = {
name: `Skill to Update ${Date.now()}`,
description: 'Original description',
documentation: [],
categories: [testCategoryId],
tags: ['original'],
};
const skill = await skillService.createSkill(testUserId, skillData);
testSkillId = skill.id;
});
afterEach(async () => {
// Clean up the test skill
if (testSkillId) {
try {
await skillService.deleteSkill(testUserId, testSkillId);
} catch (_e) {
// Ignore errors if already deleted
}
}
});
it('should update skill successfully', async () => {
const updates = {
name: `Updated Skill Name ${Date.now()}`,
description: 'Updated description',
systemPromptFragment: 'Updated prompt',
tags: ['updated', 'modified'],
};
const updatedSkill = await skillService.updateSkill(testUserId, testSkillId, updates);
expect(updatedSkill).toBeDefined();
expect(updatedSkill?.name).toBe(updates.name);
expect(updatedSkill?.description).toBe(updates.description);
expect(updatedSkill?.systemPromptFragment).toBe(updates.systemPromptFragment);
});
it('should fail to update non-existent skill', async () => {
try {
await skillService.updateSkill(testUserId, 'non-existent-id', {
name: 'New Name',
});
expect(true).toBe(false); // Should not reach here
} catch (error) {
expect(error).toBeDefined();
expect((error as Error).message).toContain('not found');
}
});
it('should fail to update skill without ownership', async () => {
const otherUserId = 'different-user-id';
try {
await skillService.updateSkill(otherUserId, testSkillId, {
name: 'New Name',
});
expect(true).toBe(false); // Should not reach here
} catch (error) {
expect(error).toBeDefined();
expect((error as Error).message).toContain('unauthorized');
}
});
});
describe('SkillService - Publish/Unpublish Skill', () => {
let testSkillId: string;
beforeEach(async () => {
const skillData = {
name: `Skill to Publish ${Date.now()}`,
description: 'Test publishing',
documentation: [],
categories: [testCategoryId],
tags: [],
};
const skill = await skillService.createSkill(testUserId, skillData);
testSkillId = skill.id;
});
afterEach(async () => {
// Clean up the test skill
if (testSkillId) {
try {
await skillService.deleteSkill(testUserId, testSkillId);
} catch (_e) {
// Ignore errors if already deleted
}
}
});
it('should publish skill successfully', async () => {
const publishedSkill = await skillService.publishSkill(testUserId, testSkillId);
expect(publishedSkill).toBeDefined();
expect(publishedSkill?.isPublished).toBe(true);
expect(publishedSkill?.publishedAt).toBeDefined();
expect(publishedSkill?.publishedAt).toBeGreaterThan(0);
});
it('should unpublish skill successfully', async () => {
// First publish
await skillService.publishSkill(testUserId, testSkillId);
// Then unpublish
const unpublishedSkill = await skillService.unpublishSkill(testUserId, testSkillId);
expect(unpublishedSkill).toBeDefined();
expect(unpublishedSkill?.isPublished).toBe(false);
});
it('should fail to publish non-existent skill', async () => {
try {
await skillService.publishSkill(testUserId, 'non-existent-id');
expect(true).toBe(false);
} catch (error) {
expect(error).toBeDefined();
expect((error as Error).message).toContain('not found');
}
});
});
describe('SkillService - Delete Skill', () => {
it('should delete skill successfully', async () => {
const skillData = {
name: `Skill to Delete ${Date.now()}`,
description: 'Will be deleted',
documentation: [],
categories: [testCategoryId],
tags: ['deletable'],
};
const skill = await skillService.createSkill(testUserId, skillData);
const result = await skillService.deleteSkill(testUserId, skill.id);
expect(result).toBe(true);
// Verify skill is deleted
const deletedSkill = await db
.select()
.from(marketplaceSkills)
.where(eq(marketplaceSkills.id, skill.id))
.limit(1);
expect(deletedSkill.length).toBe(0);
});
it('should fail to delete non-existent skill', async () => {
try {
await skillService.deleteSkill(testUserId, 'non-existent-id');
expect(true).toBe(false);
} catch (error) {
expect(error).toBeDefined();
expect((error as Error).message).toContain('not found');
}
});
});
describe('SkillService - Create Version', () => {
let testSkillId: string;
beforeEach(async () => {
const skillData = {
name: `Versioned Skill ${Date.now()}`,
description: 'Test versioning',
documentation: [],
categories: [testCategoryId],
tags: [],
};
const skill = await skillService.createSkill(testUserId, skillData);
testSkillId = skill.id;
});
afterEach(async () => {
// Clean up the test skill
if (testSkillId) {
try {
await skillService.deleteSkill(testUserId, testSkillId);
} catch (_e) {
// Ignore errors if already deleted
}
}
});
it('should create new version successfully', async () => {
const versionData = {
version: '1.1.0',
systemPromptFragment: 'Updated prompt for v1.1',
changeLog: 'Added new features',
};
const version = await skillService.createVersion(testUserId, testSkillId, versionData);
expect(version).toBeDefined();
expect(version.version).toBe('1.1.0');
expect(version.systemPromptFragment).toBe(versionData.systemPromptFragment);
expect(version.changeLog).toBe(versionData.changeLog);
// Verify latest version was updated
const skill = await db
.select()
.from(marketplaceSkills)
.where(eq(marketplaceSkills.id, testSkillId))
.limit(1);
expect(skill[0].latestVersion).toBe('1.1.0');
});
it('should fail to create duplicate version', async () => {
const versionData = {
version: '1.0.0', // Already exists from initial creation
changeLog: 'Duplicate version',
};
try {
await skillService.createVersion(testUserId, testSkillId, versionData);
expect(true).toBe(false);
} catch (error) {
expect(error).toBeDefined();
expect((error as Error).message).toContain('already exists');
}
});
});
describe('SkillService - Tag Management', () => {
it('should create new tags when they do not exist', async () => {
const skillData = {
name: `Skill with New Tags ${Date.now()}`,
description: 'Testing tag creation',
documentation: [],
categories: [testCategoryId],
tags: [`brand-new-tag-${Date.now()}`, `another-new-tag-${Date.now()}`],
};
const _skill = await skillService.createSkill(testUserId, skillData);
// Verify tags were created
for (const tagName of skillData.tags) {
const tagSlug = tagName.toLowerCase().replace(/\s+/g, '-');
const foundTag = await db.select().from(tags).where(eq(tags.slug, tagSlug)).limit(1);
expect(foundTag.length).toBe(1);
expect(foundTag[0].name).toBe(tagName);
}
});
it('should increment usage count for existing tags', async () => {
const tagName = `reusable-tag-${Date.now()}`;
const timestamp = Date.now();
// Create first skill with tag
const skill1Data = {
name: `First Skill ${timestamp}`,
description: 'First skill',
documentation: [],
categories: [testCategoryId],
tags: [tagName],
};
await skillService.createSkill(testUserId, skill1Data);
const tagSlug = tagName.toLowerCase().replace(/\s+/g, '-');
const tag1 = await db.select().from(tags).where(eq(tags.slug, tagSlug)).limit(1);
const initialCount = tag1[0].usageCount;
// Create second skill with same tag
const skill2Data = {
name: `Second Skill ${timestamp}`,
description: 'Second skill',
documentation: [],
categories: [testCategoryId],
tags: [tagName],
};
await skillService.createSkill(testUserId, skill2Data);
const tag2 = await db.select().from(tags).where(eq(tags.slug, tagSlug)).limit(1);
expect(tag2[0].usageCount).toBe(initialCount + 1);
});
});
describe('SkillService - Slug Generation', () => {
it('should generate valid slugs from skill names', async () => {
const testCases = [
{ name: 'My Awesome Skill', expectedSlug: 'my-awesome-skill' },
{ name: 'Skill with CAPS', expectedSlug: 'skill-with-caps' },
{ name: 'Skill!!!???', expectedSlug: 'skill' },
{ name: ' Spaced Out ', expectedSlug: 'spaced-out' },
];
for (const testCase of testCases) {
const skillData = {
name: testCase.name,
description: 'Test slug generation',
documentation: [],
categories: [testCategoryId],
tags: [],
};
const skill = await skillService.createSkill(testUserId, skillData);
expect(skill.slug).toBe(testCase.expectedSlug);
// Clean up
await skillService.deleteSkill(testUserId, skill.id);
}
});
});