refector:@ refrence 架构重构

This commit is contained in:
sean
2025-06-12 14:18:19 +08:00
parent 5d6e678bd2
commit c46cd24fe4
12 changed files with 2790 additions and 0 deletions

View File

@ -0,0 +1,420 @@
const EnhancedResourceRegistry = require('../../../lib/core/resource/EnhancedResourceRegistry')
describe('EnhancedResourceRegistry', () => {
let registry
beforeEach(() => {
registry = new EnhancedResourceRegistry()
})
describe('constructor', () => {
test('should initialize with empty registry', () => {
expect(registry.size()).toBe(0)
expect(registry.list()).toEqual([])
})
})
describe('register', () => {
test('should register resource with metadata', () => {
const resource = {
id: 'role:test',
reference: '@package://test.md',
metadata: {
source: 'PACKAGE',
priority: 1,
timestamp: new Date()
}
}
registry.register(resource)
expect(registry.has('role:test')).toBe(true)
expect(registry.resolve('role:test')).toBe('@package://test.md')
})
test('should throw error for invalid resource', () => {
expect(() => {
registry.register({ id: 'test' }) // missing reference
}).toThrow('Resource must have id and reference')
})
test('should throw error for missing metadata', () => {
expect(() => {
registry.register({
id: 'role:test',
reference: '@package://test.md'
// missing metadata
})
}).toThrow('Resource must have metadata with source and priority')
})
})
describe('registerBatch', () => {
test('should register multiple resources at once', () => {
const resources = [
{
id: 'role:test1',
reference: '@package://test1.md',
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
},
{
id: 'role:test2',
reference: '@project://test2.md',
metadata: { source: 'PROJECT', priority: 2, timestamp: new Date() }
}
]
registry.registerBatch(resources)
expect(registry.size()).toBe(2)
expect(registry.has('role:test1')).toBe(true)
expect(registry.has('role:test2')).toBe(true)
})
test('should handle batch registration failures gracefully', () => {
const resources = [
{
id: 'role:valid',
reference: '@package://valid.md',
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
},
{
id: 'role:invalid'
// missing reference and metadata
}
]
// Should register valid resources and skip invalid ones
registry.registerBatch(resources)
expect(registry.size()).toBe(1)
expect(registry.has('role:valid')).toBe(true)
expect(registry.has('role:invalid')).toBe(false)
})
})
describe('merge', () => {
test('should merge with another registry using priority rules', () => {
// Setup first registry with package resources
const resource1 = {
id: 'role:test',
reference: '@package://test.md',
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date('2023-01-01') }
}
registry.register(resource1)
// Create second registry with project resources (higher priority)
const otherRegistry = new EnhancedResourceRegistry()
const resource2 = {
id: 'role:test', // same ID, should override
reference: '@project://test.md',
metadata: { source: 'PROJECT', priority: 2, timestamp: new Date('2023-01-02') }
}
otherRegistry.register(resource2)
// Merge - PROJECT should override PACKAGE due to higher priority
registry.merge(otherRegistry)
expect(registry.resolve('role:test')).toBe('@project://test.md')
const metadata = registry.getMetadata('role:test')
expect(metadata.source).toBe('PROJECT')
})
test('should handle same priority by timestamp (newer wins)', () => {
const older = new Date('2023-01-01')
const newer = new Date('2023-01-02')
// Setup first registry
const resource1 = {
id: 'role:test',
reference: '@package://old.md',
metadata: { source: 'PACKAGE', priority: 1, timestamp: older }
}
registry.register(resource1)
// Create second registry with same priority but newer timestamp
const otherRegistry = new EnhancedResourceRegistry()
const resource2 = {
id: 'role:test',
reference: '@package://new.md',
metadata: { source: 'PACKAGE', priority: 1, timestamp: newer }
}
otherRegistry.register(resource2)
registry.merge(otherRegistry)
expect(registry.resolve('role:test')).toBe('@package://new.md')
})
test('should handle discovery source priority correctly', () => {
// Test priority order: USER > PROJECT > PACKAGE > INTERNET
const resources = [
{
id: 'role:test',
reference: '@internet://remote.md',
metadata: { source: 'INTERNET', priority: 4, timestamp: new Date() }
},
{
id: 'role:test',
reference: '@package://builtin.md',
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
},
{
id: 'role:test',
reference: '@project://project.md',
metadata: { source: 'PROJECT', priority: 2, timestamp: new Date() }
},
{
id: 'role:test',
reference: '@user://custom.md',
metadata: { source: 'USER', priority: 3, timestamp: new Date() }
}
]
// Register in random order
registry.register(resources[0]) // INTERNET
registry.register(resources[1]) // PACKAGE (should override INTERNET)
registry.register(resources[2]) // PROJECT (should override PACKAGE)
registry.register(resources[3]) // USER (should override PROJECT)
expect(registry.resolve('role:test')).toBe('@user://custom.md')
})
})
describe('resolve', () => {
test('should resolve resource by exact ID', () => {
const resource = {
id: 'role:test',
reference: '@package://test.md',
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
}
registry.register(resource)
expect(registry.resolve('role:test')).toBe('@package://test.md')
})
test('should support backwards compatibility lookup', () => {
const resource = {
id: 'role:java-developer',
reference: '@package://java.md',
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
}
registry.register(resource)
// Should find by bare name if no exact match
expect(registry.resolve('java-developer')).toBe('@package://java.md')
})
test('should throw error if resource not found', () => {
expect(() => {
registry.resolve('non-existent')
}).toThrow('Resource \'non-existent\' not found')
})
})
describe('list', () => {
test('should list all resources', () => {
const resources = [
{
id: 'role:test1',
reference: '@package://test1.md',
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
},
{
id: 'execution:test2',
reference: '@project://test2.md',
metadata: { source: 'PROJECT', priority: 2, timestamp: new Date() }
}
]
registry.registerBatch(resources)
const list = registry.list()
expect(list).toHaveLength(2)
expect(list).toContain('role:test1')
expect(list).toContain('execution:test2')
})
test('should filter by protocol', () => {
const resources = [
{
id: 'role:test1',
reference: '@package://test1.md',
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
},
{
id: 'execution:test2',
reference: '@project://test2.md',
metadata: { source: 'PROJECT', priority: 2, timestamp: new Date() }
},
{
id: 'role:test3',
reference: '@user://test3.md',
metadata: { source: 'USER', priority: 3, timestamp: new Date() }
}
]
registry.registerBatch(resources)
const roleList = registry.list('role')
expect(roleList).toHaveLength(2)
expect(roleList).toContain('role:test1')
expect(roleList).toContain('role:test3')
const executionList = registry.list('execution')
expect(executionList).toHaveLength(1)
expect(executionList).toContain('execution:test2')
})
})
describe('getMetadata', () => {
test('should return resource metadata', () => {
const timestamp = new Date()
const resource = {
id: 'role:test',
reference: '@package://test.md',
metadata: { source: 'PACKAGE', priority: 1, timestamp: timestamp }
}
registry.register(resource)
const metadata = registry.getMetadata('role:test')
expect(metadata).toEqual({
source: 'PACKAGE',
priority: 1,
timestamp: timestamp
})
})
test('should return null for non-existent resource', () => {
const metadata = registry.getMetadata('non-existent')
expect(metadata).toBeNull()
})
})
describe('clear', () => {
test('should clear all resources', () => {
const resource = {
id: 'role:test',
reference: '@package://test.md',
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
}
registry.register(resource)
expect(registry.size()).toBe(1)
registry.clear()
expect(registry.size()).toBe(0)
expect(registry.list()).toEqual([])
})
})
describe('size', () => {
test('should return number of registered resources', () => {
expect(registry.size()).toBe(0)
const resources = [
{
id: 'role:test1',
reference: '@package://test1.md',
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
},
{
id: 'role:test2',
reference: '@project://test2.md',
metadata: { source: 'PROJECT', priority: 2, timestamp: new Date() }
}
]
registry.registerBatch(resources)
expect(registry.size()).toBe(2)
})
})
describe('has', () => {
test('should check if resource exists', () => {
const resource = {
id: 'role:test',
reference: '@package://test.md',
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
}
registry.register(resource)
expect(registry.has('role:test')).toBe(true)
expect(registry.has('non-existent')).toBe(false)
})
})
describe('remove', () => {
test('should remove resource', () => {
const resource = {
id: 'role:test',
reference: '@package://test.md',
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
}
registry.register(resource)
expect(registry.has('role:test')).toBe(true)
registry.remove('role:test')
expect(registry.has('role:test')).toBe(false)
expect(registry.size()).toBe(0)
})
test('should do nothing if resource does not exist', () => {
registry.remove('non-existent') // Should not throw
expect(registry.size()).toBe(0)
})
})
describe('loadFromDiscoveryResults', () => {
test('should load resources from discovery manager results', () => {
const discoveryResults = [
{
id: 'role:test1',
reference: '@package://test1.md',
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
},
{
id: 'role:test2',
reference: '@project://test2.md',
metadata: { source: 'PROJECT', priority: 2, timestamp: new Date() }
}
]
registry.loadFromDiscoveryResults(discoveryResults)
expect(registry.size()).toBe(2)
expect(registry.resolve('role:test1')).toBe('@package://test1.md')
expect(registry.resolve('role:test2')).toBe('@project://test2.md')
})
test('should handle empty discovery results', () => {
registry.loadFromDiscoveryResults([])
expect(registry.size()).toBe(0)
})
test('should handle invalid discovery results gracefully', () => {
const discoveryResults = [
{
id: 'role:valid',
reference: '@package://valid.md',
metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() }
},
{
id: 'role:invalid'
// missing reference and metadata
},
null,
undefined
]
registry.loadFromDiscoveryResults(discoveryResults)
expect(registry.size()).toBe(1)
expect(registry.has('role:valid')).toBe(true)
})
})
})

View File

@ -0,0 +1,99 @@
const BaseDiscovery = require('../../../../lib/core/resource/discovery/BaseDiscovery')
describe('BaseDiscovery', () => {
let discovery
beforeEach(() => {
discovery = new BaseDiscovery('test', 1)
})
describe('constructor', () => {
test('should initialize with source and priority', () => {
expect(discovery.source).toBe('test')
expect(discovery.priority).toBe(1)
})
test('should throw error if source is not provided', () => {
expect(() => new BaseDiscovery()).toThrow('Discovery source is required')
})
test('should use default priority if not provided', () => {
const defaultDiscovery = new BaseDiscovery('test')
expect(defaultDiscovery.priority).toBe(0)
})
})
describe('discover method', () => {
test('should throw error for abstract method', async () => {
await expect(discovery.discover()).rejects.toThrow('discover method must be implemented by subclass')
})
})
describe('getDiscoveryInfo', () => {
test('should return discovery metadata', () => {
const info = discovery.getDiscoveryInfo()
expect(info).toEqual({
source: 'test',
priority: 1,
description: expect.any(String)
})
})
})
describe('validateResource', () => {
test('should validate resource structure', () => {
const validResource = {
id: 'role:test',
reference: '@package://test.md',
metadata: {
source: 'test',
priority: 1
}
}
expect(() => discovery.validateResource(validResource)).not.toThrow()
})
test('should throw error for invalid resource', () => {
const invalidResource = { id: 'test' } // missing reference
expect(() => discovery.validateResource(invalidResource)).toThrow('Resource must have id and reference')
})
})
describe('normalizeResource', () => {
test('should add metadata to resource', () => {
const resource = {
id: 'role:test',
reference: '@package://test.md'
}
const normalized = discovery.normalizeResource(resource)
expect(normalized).toEqual({
id: 'role:test',
reference: '@package://test.md',
metadata: {
source: 'test',
priority: 1,
timestamp: expect.any(Date)
}
})
})
test('should preserve existing metadata', () => {
const resource = {
id: 'role:test',
reference: '@package://test.md',
metadata: {
customField: 'value'
}
}
const normalized = discovery.normalizeResource(resource)
expect(normalized.metadata.customField).toBe('value')
expect(normalized.metadata.source).toBe('test')
})
})
})

View File

@ -0,0 +1,185 @@
const DiscoveryManager = require('../../../../lib/core/resource/discovery/DiscoveryManager')
const PackageDiscovery = require('../../../../lib/core/resource/discovery/PackageDiscovery')
const ProjectDiscovery = require('../../../../lib/core/resource/discovery/ProjectDiscovery')
describe('DiscoveryManager', () => {
let manager
beforeEach(() => {
manager = new DiscoveryManager()
})
describe('constructor', () => {
test('should initialize with default discoveries', () => {
expect(manager.discoveries).toHaveLength(2)
expect(manager.discoveries[0]).toBeInstanceOf(PackageDiscovery)
expect(manager.discoveries[1]).toBeInstanceOf(ProjectDiscovery)
})
test('should allow custom discoveries in constructor', () => {
const customDiscovery = new PackageDiscovery()
const customManager = new DiscoveryManager([customDiscovery])
expect(customManager.discoveries).toHaveLength(1)
expect(customManager.discoveries[0]).toBe(customDiscovery)
})
})
describe('addDiscovery', () => {
test('should add discovery and sort by priority', () => {
const customDiscovery = { source: 'CUSTOM', priority: 0, discover: jest.fn() }
manager.addDiscovery(customDiscovery)
expect(manager.discoveries).toHaveLength(3)
expect(manager.discoveries[0].source).toBe('CUSTOM') // priority 0 comes first
expect(manager.discoveries[1].source).toBe('PACKAGE') // priority 1
expect(manager.discoveries[2].source).toBe('PROJECT') // priority 2
})
})
describe('removeDiscovery', () => {
test('should remove discovery by source', () => {
manager.removeDiscovery('PACKAGE')
expect(manager.discoveries).toHaveLength(1)
expect(manager.discoveries[0].source).toBe('PROJECT')
})
test('should do nothing if discovery not found', () => {
manager.removeDiscovery('NON_EXISTENT')
expect(manager.discoveries).toHaveLength(2)
})
})
describe('discoverAll', () => {
test('should discover resources from all discoveries in parallel', async () => {
// Mock discovery methods
const packageResources = [
{ id: 'role:java-developer', reference: '@package://test1.md', metadata: { source: 'PACKAGE', priority: 1 } }
]
const projectResources = [
{ id: 'role:custom-role', reference: '@project://test2.md', metadata: { source: 'PROJECT', priority: 2 } }
]
manager.discoveries[0].discover = jest.fn().mockResolvedValue(packageResources)
manager.discoveries[1].discover = jest.fn().mockResolvedValue(projectResources)
const allResources = await manager.discoverAll()
expect(allResources).toHaveLength(2)
expect(allResources[0].id).toBe('role:java-developer')
expect(allResources[1].id).toBe('role:custom-role')
expect(manager.discoveries[0].discover).toHaveBeenCalled()
expect(manager.discoveries[1].discover).toHaveBeenCalled()
})
test('should handle discovery failures gracefully', async () => {
const packageResources = [
{ id: 'role:java-developer', reference: '@package://test1.md', metadata: { source: 'PACKAGE', priority: 1 } }
]
manager.discoveries[0].discover = jest.fn().mockResolvedValue(packageResources)
manager.discoveries[1].discover = jest.fn().mockRejectedValue(new Error('Project discovery failed'))
const allResources = await manager.discoverAll()
expect(allResources).toHaveLength(1)
expect(allResources[0].id).toBe('role:java-developer')
})
test('should return empty array if all discoveries fail', async () => {
manager.discoveries[0].discover = jest.fn().mockRejectedValue(new Error('Package discovery failed'))
manager.discoveries[1].discover = jest.fn().mockRejectedValue(new Error('Project discovery failed'))
const allResources = await manager.discoverAll()
expect(allResources).toEqual([])
})
})
describe('discoverBySource', () => {
test('should discover resources from specific source', async () => {
const packageResources = [
{ id: 'role:java-developer', reference: '@package://test1.md', metadata: { source: 'PACKAGE', priority: 1 } }
]
manager.discoveries[0].discover = jest.fn().mockResolvedValue(packageResources)
const resources = await manager.discoverBySource('PACKAGE')
expect(resources).toEqual(packageResources)
expect(manager.discoveries[0].discover).toHaveBeenCalled()
})
test('should throw error if source not found', async () => {
await expect(manager.discoverBySource('NON_EXISTENT')).rejects.toThrow('Discovery source NON_EXISTENT not found')
})
})
describe('getDiscoveryInfo', () => {
test('should return info for all discoveries', () => {
// Mock getDiscoveryInfo methods
manager.discoveries[0].getDiscoveryInfo = jest.fn().mockReturnValue({
source: 'PACKAGE',
priority: 1,
description: 'Package discovery'
})
manager.discoveries[1].getDiscoveryInfo = jest.fn().mockReturnValue({
source: 'PROJECT',
priority: 2,
description: 'Project discovery'
})
const info = manager.getDiscoveryInfo()
expect(info).toHaveLength(2)
expect(info[0].source).toBe('PACKAGE')
expect(info[1].source).toBe('PROJECT')
})
})
describe('clearCache', () => {
test('should clear cache for all discoveries', () => {
// Mock clearCache methods
manager.discoveries[0].clearCache = jest.fn()
manager.discoveries[1].clearCache = jest.fn()
manager.clearCache()
expect(manager.discoveries[0].clearCache).toHaveBeenCalled()
expect(manager.discoveries[1].clearCache).toHaveBeenCalled()
})
})
describe('_sortDiscoveriesByPriority', () => {
test('should sort discoveries by priority ascending', () => {
const discovery1 = { source: 'A', priority: 3 }
const discovery2 = { source: 'B', priority: 1 }
const discovery3 = { source: 'C', priority: 2 }
manager.discoveries = [discovery1, discovery2, discovery3]
manager._sortDiscoveriesByPriority()
expect(manager.discoveries[0].source).toBe('B') // priority 1
expect(manager.discoveries[1].source).toBe('C') // priority 2
expect(manager.discoveries[2].source).toBe('A') // priority 3
})
})
describe('_findDiscoveryBySource', () => {
test('should find discovery by source', () => {
const packageDiscovery = manager._findDiscoveryBySource('PACKAGE')
expect(packageDiscovery).toBeDefined()
expect(packageDiscovery.source).toBe('PACKAGE')
})
test('should return undefined if source not found', () => {
const discovery = manager._findDiscoveryBySource('NON_EXISTENT')
expect(discovery).toBeUndefined()
})
})
})

View File

@ -0,0 +1,177 @@
const PackageDiscovery = require('../../../../lib/core/resource/discovery/PackageDiscovery')
const path = require('path')
describe('PackageDiscovery', () => {
let discovery
beforeEach(() => {
discovery = new PackageDiscovery()
})
describe('constructor', () => {
test('should initialize with PACKAGE source and priority 1', () => {
expect(discovery.source).toBe('PACKAGE')
expect(discovery.priority).toBe(1)
})
})
describe('discover', () => {
test('should discover package resources from static registry', async () => {
// Mock registry file content
jest.spyOn(discovery, '_loadStaticRegistry').mockResolvedValue({
protocols: {
role: {
registry: {
'java-backend-developer': '@package://prompt/domain/java-backend-developer/java-backend-developer.role.md',
'product-manager': '@package://prompt/domain/product-manager/product-manager.role.md'
}
},
thought: {
registry: {
'remember': '@package://prompt/core/thought/remember.thought.md'
}
}
}
})
// Mock scan to return empty array to isolate static registry test
jest.spyOn(discovery, '_scanPromptDirectory').mockResolvedValue([])
const resources = await discovery.discover()
expect(resources).toHaveLength(3)
expect(resources[0]).toMatchObject({
id: 'role:java-backend-developer',
reference: '@package://prompt/domain/java-backend-developer/java-backend-developer.role.md',
metadata: {
source: 'PACKAGE',
priority: 1
}
})
})
test('should discover resources from prompt directory scan', async () => {
// Mock file system operations
jest.spyOn(discovery, '_scanPromptDirectory').mockResolvedValue([
{
id: 'role:assistant',
reference: '@package://prompt/domain/assistant/assistant.role.md'
}
])
jest.spyOn(discovery, '_loadStaticRegistry').mockResolvedValue({})
const resources = await discovery.discover()
expect(resources).toHaveLength(1)
expect(resources[0].id).toBe('role:assistant')
})
test('should handle registry loading failures gracefully', async () => {
jest.spyOn(discovery, '_loadStaticRegistry').mockRejectedValue(new Error('Registry not found'))
jest.spyOn(discovery, '_scanPromptDirectory').mockResolvedValue([])
const resources = await discovery.discover()
expect(resources).toEqual([])
})
})
describe('_loadStaticRegistry', () => {
test('should load registry from default path', async () => {
// This would be mocked in real tests
expect(typeof discovery._loadStaticRegistry).toBe('function')
})
})
describe('_scanPromptDirectory', () => {
test('should scan for role, execution, thought files', async () => {
// Mock package root and prompt directory
jest.spyOn(discovery, '_findPackageRoot').mockResolvedValue('/mock/package/root')
// Mock fs.pathExists to return true for prompt directory
const mockPathExists = jest.spyOn(require('fs-extra'), 'pathExists').mockResolvedValue(true)
// Mock file scanner
const mockScanResourceFiles = jest.fn()
.mockResolvedValueOnce(['/mock/package/root/prompt/domain/test/test.role.md']) // roles
.mockResolvedValueOnce(['/mock/package/root/prompt/core/execution/test.execution.md']) // executions
.mockResolvedValueOnce(['/mock/package/root/prompt/core/thought/test.thought.md']) // thoughts
discovery.fileScanner.scanResourceFiles = mockScanResourceFiles
const resources = await discovery._scanPromptDirectory()
expect(resources).toHaveLength(3)
expect(resources[0].id).toBe('role:test')
expect(resources[1].id).toBe('execution:test')
expect(resources[2].id).toBe('thought:test')
// Cleanup
mockPathExists.mockRestore()
})
})
describe('_findPackageRoot', () => {
test('should find package root containing prompt directory', async () => {
// Mock file system check
jest.spyOn(discovery, '_findPackageJsonWithPrompt').mockResolvedValue('/mock/package/root')
const root = await discovery._findPackageRoot()
expect(root).toBe('/mock/package/root')
})
test('should throw error if package root not found', async () => {
jest.spyOn(discovery, '_findPackageJsonWithPrompt').mockResolvedValue(null)
await expect(discovery._findPackageRoot()).rejects.toThrow('Package root with prompt directory not found')
})
})
describe('_generatePackageReference', () => {
test('should generate @package:// reference', () => {
const filePath = '/mock/package/root/prompt/domain/test/test.role.md'
const packageRoot = '/mock/package/root'
// Mock the fileScanner.getRelativePath method
discovery.fileScanner.getRelativePath = jest.fn().mockReturnValue('prompt/domain/test/test.role.md')
const reference = discovery._generatePackageReference(filePath, packageRoot)
expect(reference).toBe('@package://prompt/domain/test/test.role.md')
})
test('should handle Windows paths correctly', () => {
const filePath = 'C:\\mock\\package\\root\\prompt\\domain\\test\\test.role.md'
const packageRoot = 'C:\\mock\\package\\root'
// Mock the fileScanner.getRelativePath method
discovery.fileScanner.getRelativePath = jest.fn().mockReturnValue('prompt/domain/test/test.role.md')
const reference = discovery._generatePackageReference(filePath, packageRoot)
expect(reference).toBe('@package://prompt/domain/test/test.role.md')
})
})
describe('_extractResourceId', () => {
test('should extract role id from path', () => {
const filePath = '/mock/package/root/prompt/domain/test/test.role.md'
const protocol = 'role'
const id = discovery._extractResourceId(filePath, protocol, '.role.md')
expect(id).toBe('role:test')
})
test('should extract execution id from path', () => {
const filePath = '/mock/package/root/prompt/core/execution/memory-trigger.execution.md'
const protocol = 'execution'
const id = discovery._extractResourceId(filePath, protocol, '.execution.md')
expect(id).toBe('execution:memory-trigger')
})
})
})

View File

@ -0,0 +1,227 @@
const ProjectDiscovery = require('../../../../lib/core/resource/discovery/ProjectDiscovery')
const path = require('path')
describe('ProjectDiscovery', () => {
let discovery
beforeEach(() => {
discovery = new ProjectDiscovery()
})
describe('constructor', () => {
test('should initialize with PROJECT source and priority 2', () => {
expect(discovery.source).toBe('PROJECT')
expect(discovery.priority).toBe(2)
})
})
describe('discover', () => {
test('should discover project resources from .promptx directory', async () => {
// Mock project root and .promptx directory
jest.spyOn(discovery, '_findProjectRoot').mockResolvedValue('/mock/project/root')
jest.spyOn(discovery, '_checkPrompxDirectory').mockResolvedValue(true)
jest.spyOn(discovery, '_scanProjectResources').mockResolvedValue([
{
id: 'role:custom-role',
reference: '@project://.promptx/resource/domain/custom-role/custom-role.role.md'
},
{
id: 'execution:custom-execution',
reference: '@project://.promptx/resource/execution/custom-execution.execution.md'
}
])
const resources = await discovery.discover()
expect(resources).toHaveLength(2)
expect(resources[0]).toMatchObject({
id: 'role:custom-role',
reference: '@project://.promptx/resource/domain/custom-role/custom-role.role.md',
metadata: {
source: 'PROJECT',
priority: 2
}
})
})
test('should return empty array if .promptx directory does not exist', async () => {
jest.spyOn(discovery, '_findProjectRoot').mockResolvedValue('/mock/project/root')
jest.spyOn(discovery, '_checkPrompxDirectory').mockResolvedValue(false)
const resources = await discovery.discover()
expect(resources).toEqual([])
})
test('should handle project root not found', async () => {
jest.spyOn(discovery, '_findProjectRoot').mockRejectedValue(new Error('Project root not found'))
const resources = await discovery.discover()
expect(resources).toEqual([])
})
})
describe('_findProjectRoot', () => {
test('should find project root containing package.json', async () => {
const mockFsExists = jest.fn()
.mockResolvedValueOnce(false) // /current/dir/package.json
.mockResolvedValueOnce(true) // /current/package.json
discovery._fsExists = mockFsExists
// Mock process.cwd()
const originalCwd = process.cwd
process.cwd = jest.fn().mockReturnValue('/current/dir')
const root = await discovery._findProjectRoot()
expect(root).toBe('/current')
// Restore
process.cwd = originalCwd
})
test('should return current directory if no package.json found', async () => {
const mockFsExists = jest.fn().mockResolvedValue(false)
discovery._fsExists = mockFsExists
const originalCwd = process.cwd
process.cwd = jest.fn().mockReturnValue('/current/dir')
const root = await discovery._findProjectRoot()
expect(root).toBe('/current/dir')
process.cwd = originalCwd
})
})
describe('_checkPrompxDirectory', () => {
test('should check if .promptx/resource directory exists', async () => {
const mockFsExists = jest.fn().mockResolvedValue(true)
discovery._fsExists = mockFsExists
const exists = await discovery._checkPrompxDirectory('/mock/project/root')
expect(exists).toBe(true)
expect(mockFsExists).toHaveBeenCalledWith('/mock/project/root/.promptx/resource')
})
})
describe('_scanProjectResources', () => {
test('should scan for role, execution, thought files in .promptx', async () => {
const projectRoot = '/mock/project/root'
// Mock file scanner
const mockScanResourceFiles = jest.fn()
.mockResolvedValueOnce([`${projectRoot}/.promptx/resource/domain/test/test.role.md`]) // roles
.mockResolvedValueOnce([`${projectRoot}/.promptx/resource/execution/test.execution.md`]) // executions
.mockResolvedValueOnce([`${projectRoot}/.promptx/resource/thought/test.thought.md`]) // thoughts
discovery.fileScanner.scanResourceFiles = mockScanResourceFiles
// Mock file validation
discovery._validateResourceFile = jest.fn().mockResolvedValue(true)
const resources = await discovery._scanProjectResources(projectRoot)
expect(resources).toHaveLength(3)
expect(resources[0].id).toBe('role:test')
expect(resources[1].id).toBe('execution:test')
expect(resources[2].id).toBe('thought:test')
})
test('should handle scan errors gracefully', async () => {
const projectRoot = '/mock/project/root'
const mockScanResourceFiles = jest.fn().mockRejectedValue(new Error('Scan failed'))
discovery.fileScanner.scanResourceFiles = mockScanResourceFiles
const resources = await discovery._scanProjectResources(projectRoot)
expect(resources).toEqual([])
})
})
describe('_generateProjectReference', () => {
test('should generate @project:// reference', () => {
const filePath = '/mock/project/root/.promptx/resource/domain/test/test.role.md'
const projectRoot = '/mock/project/root'
// Mock the fileScanner.getRelativePath method
discovery.fileScanner.getRelativePath = jest.fn().mockReturnValue('.promptx/resource/domain/test/test.role.md')
const reference = discovery._generateProjectReference(filePath, projectRoot)
expect(reference).toBe('@project://.promptx/resource/domain/test/test.role.md')
})
test('should handle Windows paths correctly', () => {
const filePath = 'C:\\mock\\project\\root\\.promptx\\resource\\domain\\test\\test.role.md'
const projectRoot = 'C:\\mock\\project\\root'
// Mock the fileScanner.getRelativePath method
discovery.fileScanner.getRelativePath = jest.fn().mockReturnValue('.promptx/resource/domain/test/test.role.md')
const reference = discovery._generateProjectReference(filePath, projectRoot)
expect(reference).toBe('@project://.promptx/resource/domain/test/test.role.md')
})
})
describe('_extractResourceId', () => {
test('should extract resource id with protocol prefix', () => {
const filePath = '/mock/project/root/.promptx/resource/domain/test/test.role.md'
const protocol = 'role'
const id = discovery._extractResourceId(filePath, protocol, '.role.md')
expect(id).toBe('role:test')
})
})
describe('_validateResourceFile', () => {
test('should validate role file contains required DPML tags', async () => {
const filePath = '/mock/test.role.md'
const mockContent = `
# Test Role
<role>
Test role content
</role>
`
const mockReadFile = jest.fn().mockResolvedValue(mockContent)
discovery._readFile = mockReadFile
const isValid = await discovery._validateResourceFile(filePath, 'role')
expect(isValid).toBe(true)
})
test('should return false for invalid role file', async () => {
const filePath = '/mock/test.role.md'
const mockContent = 'Invalid content without DPML tags'
const mockReadFile = jest.fn().mockResolvedValue(mockContent)
discovery._readFile = mockReadFile
const isValid = await discovery._validateResourceFile(filePath, 'role')
expect(isValid).toBe(false)
})
test('should handle file read errors', async () => {
const filePath = '/mock/test.role.md'
const mockReadFile = jest.fn().mockRejectedValue(new Error('File not found'))
discovery._readFile = mockReadFile
const isValid = await discovery._validateResourceFile(filePath, 'role')
expect(isValid).toBe(false)
})
})
})