797 lines
26 KiB
JavaScript
797 lines
26 KiB
JavaScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { DependencyResolver } from '../../../tools/cli/installers/lib/core/dependency-resolver.js';
|
|
import { createTempDir, cleanupTempDir, createTestFile } from '../../helpers/temp-dir.js';
|
|
import fs from 'fs-extra';
|
|
import path from 'node:path';
|
|
|
|
describe('DependencyResolver', () => {
|
|
let tmpDir;
|
|
let bmadDir;
|
|
|
|
beforeEach(async () => {
|
|
tmpDir = await createTempDir();
|
|
// Create structure: tmpDir/src/core and tmpDir/src/modules/
|
|
bmadDir = path.join(tmpDir, 'src');
|
|
await fs.ensureDir(path.join(bmadDir, 'core', 'agents'));
|
|
await fs.ensureDir(path.join(bmadDir, 'core', 'tasks'));
|
|
await fs.ensureDir(path.join(bmadDir, 'core', 'templates'));
|
|
await fs.ensureDir(path.join(bmadDir, 'modules', 'bmm', 'agents'));
|
|
await fs.ensureDir(path.join(bmadDir, 'modules', 'bmm', 'tasks'));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await cleanupTempDir(tmpDir);
|
|
});
|
|
|
|
describe('basic resolution', () => {
|
|
it('should resolve core agents with no dependencies', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/simple.md',
|
|
`---
|
|
name: simple
|
|
---
|
|
<agent>Simple agent</agent>`,
|
|
);
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
expect(result.primaryFiles).toHaveLength(1);
|
|
expect(result.primaryFiles[0].type).toBe('agent');
|
|
expect(result.primaryFiles[0].module).toBe('core');
|
|
expect(result.allFiles).toHaveLength(1);
|
|
});
|
|
|
|
it('should resolve multiple agents from same module', async () => {
|
|
await createTestFile(bmadDir, 'core/agents/agent1.md', '<agent>Agent 1</agent>');
|
|
await createTestFile(bmadDir, 'core/agents/agent2.md', '<agent>Agent 2</agent>');
|
|
await createTestFile(bmadDir, 'core/agents/agent3.md', '<agent>Agent 3</agent>');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
expect(result.primaryFiles).toHaveLength(3);
|
|
expect(result.allFiles).toHaveLength(3);
|
|
});
|
|
|
|
it('should always include core module', async () => {
|
|
await createTestFile(bmadDir, 'core/agents/core-agent.md', '<agent>Core</agent>');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, ['bmm']);
|
|
|
|
// Core should be included even though only 'bmm' was requested
|
|
expect(result.byModule.core).toBeDefined();
|
|
});
|
|
|
|
it('should skip agents with localskip="true"', async () => {
|
|
await createTestFile(bmadDir, 'core/agents/normal.md', '<agent>Normal agent</agent>');
|
|
await createTestFile(bmadDir, 'core/agents/webonly.md', '<agent localskip="true">Web only agent</agent>');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
expect(result.primaryFiles).toHaveLength(1);
|
|
expect(result.primaryFiles[0].name).toBe('normal');
|
|
});
|
|
});
|
|
|
|
describe('path resolution variations', () => {
|
|
it('should resolve {project-root}/bmad/core/tasks/foo.md dependencies', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/agent.md',
|
|
`---
|
|
dependencies: ["{project-root}/bmad/core/tasks/task.md"]
|
|
---
|
|
<agent>Agent with task dependency</agent>`,
|
|
);
|
|
await createTestFile(bmadDir, 'core/tasks/task.md', 'Task content');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
expect(result.allFiles).toHaveLength(2);
|
|
expect(result.dependencies.size).toBeGreaterThan(0);
|
|
expect([...result.dependencies].some((d) => d.includes('task.md'))).toBe(true);
|
|
});
|
|
|
|
it('should resolve relative path dependencies', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/agent.md',
|
|
`---
|
|
template: "../templates/template.yaml"
|
|
---
|
|
<agent>Agent with template</agent>`,
|
|
);
|
|
await createTestFile(bmadDir, 'core/templates/template.yaml', 'template: data');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
expect(result.allFiles).toHaveLength(2);
|
|
expect([...result.dependencies].some((d) => d.includes('template.yaml'))).toBe(true);
|
|
});
|
|
|
|
it('should resolve glob pattern dependencies', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/agent.md',
|
|
`---
|
|
dependencies: ["{project-root}/bmad/core/tasks/*.md"]
|
|
---
|
|
<agent>Agent with multiple tasks</agent>`,
|
|
);
|
|
await createTestFile(bmadDir, 'core/tasks/task1.md', 'Task 1');
|
|
await createTestFile(bmadDir, 'core/tasks/task2.md', 'Task 2');
|
|
await createTestFile(bmadDir, 'core/tasks/task3.md', 'Task 3');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
// Should find agent + 3 tasks
|
|
expect(result.allFiles).toHaveLength(4);
|
|
});
|
|
|
|
it('should resolve array of dependencies', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/agent.md',
|
|
`---
|
|
dependencies:
|
|
- "{project-root}/bmad/core/tasks/task1.md"
|
|
- "{project-root}/bmad/core/tasks/task2.md"
|
|
- "../templates/template.yaml"
|
|
---
|
|
<agent>Agent</agent>`,
|
|
);
|
|
await createTestFile(bmadDir, 'core/tasks/task1.md', 'Task 1');
|
|
await createTestFile(bmadDir, 'core/tasks/task2.md', 'Task 2');
|
|
await createTestFile(bmadDir, 'core/templates/template.yaml', 'template');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
expect(result.allFiles).toHaveLength(4); // agent + 2 tasks + template
|
|
});
|
|
});
|
|
|
|
describe('command reference resolution', () => {
|
|
it('should resolve @task-name references', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/agent.md',
|
|
`<agent>
|
|
Use @task-analyze for analysis
|
|
</agent>`,
|
|
);
|
|
await createTestFile(bmadDir, 'core/tasks/analyze.md', 'Analyze task');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
expect(result.allFiles.length).toBeGreaterThanOrEqual(2);
|
|
expect([...result.allFiles].some((f) => f.includes('analyze.md'))).toBe(true);
|
|
});
|
|
|
|
it('should resolve @agent-name references', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/main.md',
|
|
`<agent>
|
|
Reference @agent-helper for help
|
|
</agent>`,
|
|
);
|
|
await createTestFile(bmadDir, 'core/agents/helper.md', '<agent>Helper</agent>');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
expect(result.allFiles).toHaveLength(2);
|
|
expect([...result.allFiles].some((f) => f.includes('helper.md'))).toBe(true);
|
|
});
|
|
|
|
it('should resolve bmad/module/type/name references', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/agent.md',
|
|
`<agent>
|
|
See bmad/core/tasks/review
|
|
</agent>`,
|
|
);
|
|
await createTestFile(bmadDir, 'core/tasks/review.md', 'Review task');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
expect([...result.allFiles].some((f) => f.includes('review.md'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('exec and tmpl attribute parsing', () => {
|
|
it('should parse exec attributes from command tags', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/agent.md',
|
|
`<agent>
|
|
<command exec="{project-root}/bmad/core/tasks/task.md" />
|
|
</agent>`,
|
|
);
|
|
await createTestFile(bmadDir, 'core/tasks/task.md', 'Task');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
expect([...result.allFiles].some((f) => f.includes('task.md'))).toBe(true);
|
|
});
|
|
|
|
it('should parse tmpl attributes from command tags', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/agent.md',
|
|
`<agent>
|
|
<command tmpl="../templates/form.yaml" />
|
|
</agent>`,
|
|
);
|
|
await createTestFile(bmadDir, 'core/templates/form.yaml', 'template');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
expect([...result.allFiles].some((f) => f.includes('form.yaml'))).toBe(true);
|
|
});
|
|
|
|
it('should ignore exec="*" wildcard', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/agent.md',
|
|
`<agent>
|
|
<command exec="*" description="Dynamic" />
|
|
</agent>`,
|
|
);
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
// Should only have the agent itself
|
|
expect(result.primaryFiles).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('multi-pass dependency resolution', () => {
|
|
it('should resolve single-level dependencies (A→B)', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/agent-a.md',
|
|
`---
|
|
dependencies: ["{project-root}/bmad/core/tasks/task-b.md"]
|
|
---
|
|
<agent>Agent A</agent>`,
|
|
);
|
|
await createTestFile(bmadDir, 'core/tasks/task-b.md', 'Task B');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
expect(result.allFiles).toHaveLength(2);
|
|
// Primary files includes both agents and tasks from selected modules
|
|
expect(result.primaryFiles.length).toBeGreaterThanOrEqual(1);
|
|
expect(result.dependencies.size).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('should resolve two-level dependencies (A→B→C)', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/agent-a.md',
|
|
`---
|
|
dependencies: ["{project-root}/bmad/core/tasks/task-b.md"]
|
|
---
|
|
<agent>Agent A</agent>`,
|
|
);
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/tasks/task-b.md',
|
|
`---
|
|
template: "../templates/template-c.yaml"
|
|
---
|
|
Task B content`,
|
|
);
|
|
await createTestFile(bmadDir, 'core/templates/template-c.yaml', 'template: data');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
expect(result.allFiles).toHaveLength(3);
|
|
// Primary files includes agents and tasks
|
|
expect(result.primaryFiles.length).toBeGreaterThanOrEqual(1);
|
|
// Total dependencies (direct + transitive) should be at least 2
|
|
const totalDeps = result.dependencies.size + result.transitiveDependencies.size;
|
|
expect(totalDeps).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('should resolve three-level dependencies (A→B→C→D)', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/agent-a.md',
|
|
`---
|
|
dependencies: ["{project-root}/bmad/core/tasks/task-b.md"]
|
|
---
|
|
<agent>A</agent>`,
|
|
);
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/tasks/task-b.md',
|
|
`---
|
|
dependencies: ["{project-root}/bmad/core/tasks/task-c.md"]
|
|
---
|
|
Task B`,
|
|
);
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/tasks/task-c.md',
|
|
`---
|
|
template: "../templates/template-d.yaml"
|
|
---
|
|
Task C`,
|
|
);
|
|
await createTestFile(bmadDir, 'core/templates/template-d.yaml', 'Template D');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
expect(result.allFiles).toHaveLength(4);
|
|
});
|
|
|
|
it('should resolve multiple branches (A→B, A→C)', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/agent-a.md',
|
|
`---
|
|
dependencies:
|
|
- "{project-root}/bmad/core/tasks/task-b.md"
|
|
- "{project-root}/bmad/core/tasks/task-c.md"
|
|
---
|
|
<agent>A</agent>`,
|
|
);
|
|
await createTestFile(bmadDir, 'core/tasks/task-b.md', 'Task B');
|
|
await createTestFile(bmadDir, 'core/tasks/task-c.md', 'Task C');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
expect(result.allFiles).toHaveLength(3);
|
|
expect(result.dependencies.size).toBe(2);
|
|
});
|
|
|
|
it('should deduplicate diamond pattern (A→B,C; B,C→D)', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/agent-a.md',
|
|
`---
|
|
dependencies:
|
|
- "{project-root}/bmad/core/tasks/task-b.md"
|
|
- "{project-root}/bmad/core/tasks/task-c.md"
|
|
---
|
|
<agent>A</agent>`,
|
|
);
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/tasks/task-b.md',
|
|
`---
|
|
template: "../templates/shared.yaml"
|
|
---
|
|
Task B`,
|
|
);
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/tasks/task-c.md',
|
|
`---
|
|
template: "../templates/shared.yaml"
|
|
---
|
|
Task C`,
|
|
);
|
|
await createTestFile(bmadDir, 'core/templates/shared.yaml', 'Shared template');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
// A + B + C + shared = 4 unique files (D appears twice but should be deduped)
|
|
expect(result.allFiles).toHaveLength(4);
|
|
});
|
|
});
|
|
|
|
describe('circular dependency detection', () => {
|
|
it('should detect direct circular dependency (A→B→A)', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/agent-a.md',
|
|
`---
|
|
dependencies: ["{project-root}/bmad/core/tasks/task-b.md"]
|
|
---
|
|
<agent>A</agent>`,
|
|
);
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/tasks/task-b.md',
|
|
`---
|
|
dependencies: ["{project-root}/bmad/core/agents/agent-a.md"]
|
|
---
|
|
Task B`,
|
|
);
|
|
|
|
const resolver = new DependencyResolver();
|
|
|
|
// Should not hang or crash
|
|
const resultPromise = resolver.resolve(bmadDir, []);
|
|
await expect(resultPromise).resolves.toBeDefined();
|
|
|
|
const result = await resultPromise;
|
|
// Should process both files without infinite loop
|
|
expect(result.allFiles.length).toBeGreaterThanOrEqual(2);
|
|
}, 5000); // 5 second timeout to ensure no infinite loop
|
|
|
|
it('should detect indirect circular dependency (A→B→C→A)', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/agent-a.md',
|
|
`---
|
|
dependencies: ["{project-root}/bmad/core/tasks/task-b.md"]
|
|
---
|
|
<agent>A</agent>`,
|
|
);
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/tasks/task-b.md',
|
|
`---
|
|
dependencies: ["{project-root}/bmad/core/tasks/task-c.md"]
|
|
---
|
|
Task B`,
|
|
);
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/tasks/task-c.md',
|
|
`---
|
|
dependencies: ["{project-root}/bmad/core/agents/agent-a.md"]
|
|
---
|
|
Task C`,
|
|
);
|
|
|
|
const resolver = new DependencyResolver();
|
|
const resultPromise = resolver.resolve(bmadDir, []);
|
|
|
|
await expect(resultPromise).resolves.toBeDefined();
|
|
const result = await resultPromise;
|
|
|
|
// Should include all 3 files without duplicates
|
|
expect(result.allFiles.length).toBeGreaterThanOrEqual(3);
|
|
}, 5000);
|
|
|
|
it('should handle self-reference (A→A)', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/agent-a.md',
|
|
`---
|
|
dependencies: ["{project-root}/bmad/core/agents/agent-a.md"]
|
|
---
|
|
<agent>A</agent>`,
|
|
);
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
// Should include the file once, not infinite times
|
|
expect(result.allFiles).toHaveLength(1);
|
|
}, 5000);
|
|
});
|
|
|
|
describe('command reference parsing', () => {
|
|
describe('parseCommandReferences()', () => {
|
|
it('should extract @task- references', () => {
|
|
const resolver = new DependencyResolver();
|
|
const content = 'Use @task-analyze for analysis\nThen @task-review';
|
|
|
|
const refs = resolver.parseCommandReferences(content);
|
|
|
|
expect(refs).toContain('@task-analyze');
|
|
expect(refs).toContain('@task-review');
|
|
});
|
|
|
|
it('should extract @agent- references', () => {
|
|
const resolver = new DependencyResolver();
|
|
const content = 'Call @agent-architect then @agent-developer';
|
|
|
|
const refs = resolver.parseCommandReferences(content);
|
|
|
|
expect(refs).toContain('@agent-architect');
|
|
expect(refs).toContain('@agent-developer');
|
|
});
|
|
|
|
it('should extract bmad/ path references', () => {
|
|
const resolver = new DependencyResolver();
|
|
const content = 'See bmad/core/agents/analyst and bmad/bmm/tasks/review';
|
|
|
|
const refs = resolver.parseCommandReferences(content);
|
|
|
|
expect(refs).toContain('bmad/core/agents/analyst');
|
|
expect(refs).toContain('bmad/bmm/tasks/review');
|
|
});
|
|
|
|
it('should extract @bmad- references', () => {
|
|
const resolver = new DependencyResolver();
|
|
const content = 'Use @bmad-master command';
|
|
|
|
const refs = resolver.parseCommandReferences(content);
|
|
|
|
expect(refs).toContain('@bmad-master');
|
|
});
|
|
|
|
it('should handle multiple reference types in same content', () => {
|
|
const resolver = new DependencyResolver();
|
|
const content = `
|
|
Use @task-analyze for analysis
|
|
Then run @agent-architect
|
|
Finally check bmad/core/tasks/review
|
|
`;
|
|
|
|
const refs = resolver.parseCommandReferences(content);
|
|
|
|
expect(refs.length).toBeGreaterThanOrEqual(3);
|
|
});
|
|
});
|
|
|
|
describe('parseFileReferences()', () => {
|
|
it('should extract exec attribute paths', () => {
|
|
const resolver = new DependencyResolver();
|
|
const content = '<command exec="{project-root}/bmad/core/tasks/foo.md" />';
|
|
|
|
const refs = resolver.parseFileReferences(content);
|
|
|
|
expect(refs).toContain('/bmad/core/tasks/foo.md');
|
|
});
|
|
|
|
it('should extract tmpl attribute paths', () => {
|
|
const resolver = new DependencyResolver();
|
|
const content = '<command tmpl="../templates/bar.yaml" />';
|
|
|
|
const refs = resolver.parseFileReferences(content);
|
|
|
|
expect(refs).toContain('../templates/bar.yaml');
|
|
});
|
|
|
|
it('should extract relative file paths', () => {
|
|
const resolver = new DependencyResolver();
|
|
const content = 'Load "./data/config.json" and "../templates/form.yaml"';
|
|
|
|
const refs = resolver.parseFileReferences(content);
|
|
|
|
expect(refs).toContain('./data/config.json');
|
|
expect(refs).toContain('../templates/form.yaml');
|
|
});
|
|
|
|
it('should skip exec="*" wildcards', () => {
|
|
const resolver = new DependencyResolver();
|
|
const content = '<command exec="*" description="Dynamic" />';
|
|
|
|
const refs = resolver.parseFileReferences(content);
|
|
|
|
// Should not include "*"
|
|
expect(refs).not.toContain('*');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('module organization', () => {
|
|
it('should organize files by module correctly', async () => {
|
|
await createTestFile(bmadDir, 'core/agents/core-agent.md', '<agent>Core</agent>');
|
|
await createTestFile(bmadDir, 'bmm/agents/bmm-agent.md', '<agent>BMM</agent>');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, ['bmm']);
|
|
|
|
expect(result.byModule.core).toBeDefined();
|
|
expect(result.byModule.bmm).toBeDefined();
|
|
expect(result.byModule.core.agents).toHaveLength(1);
|
|
expect(result.byModule.bmm.agents).toHaveLength(1);
|
|
});
|
|
|
|
it('should categorize files by type', async () => {
|
|
await createTestFile(bmadDir, 'core/agents/agent.md', '<agent>Agent</agent>');
|
|
await createTestFile(bmadDir, 'core/tasks/task.md', 'Task');
|
|
await createTestFile(bmadDir, 'core/templates/template.yaml', 'template');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const files = [
|
|
path.join(bmadDir, 'core/agents/agent.md'),
|
|
path.join(bmadDir, 'core/tasks/task.md'),
|
|
path.join(bmadDir, 'core/templates/template.yaml'),
|
|
];
|
|
|
|
const organized = resolver.organizeByModule(bmadDir, new Set(files));
|
|
|
|
expect(organized.core.agents).toHaveLength(1);
|
|
expect(organized.core.tasks).toHaveLength(1);
|
|
expect(organized.core.templates).toHaveLength(1);
|
|
});
|
|
|
|
it('should treat brain-tech as data, not tasks', async () => {
|
|
await createTestFile(bmadDir, 'core/tasks/brain-tech/data.csv', 'col1,col2\nval1,val2');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const files = [path.join(bmadDir, 'core/tasks/brain-tech/data.csv')];
|
|
|
|
const organized = resolver.organizeByModule(bmadDir, new Set(files));
|
|
|
|
expect(organized.core.data).toHaveLength(1);
|
|
expect(organized.core.tasks).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('getModuleFromPath()', () => {
|
|
it('should extract module from src/core path', () => {
|
|
const resolver = new DependencyResolver();
|
|
const filePath = path.join(bmadDir, 'core/agents/agent.md');
|
|
|
|
const module = resolver.getModuleFromPath(bmadDir, filePath);
|
|
|
|
expect(module).toBe('core');
|
|
});
|
|
|
|
it('should extract module from src/bmm path', () => {
|
|
const resolver = new DependencyResolver();
|
|
const filePath = path.join(bmadDir, 'bmm/agents/pm.md');
|
|
|
|
const module = resolver.getModuleFromPath(bmadDir, filePath);
|
|
|
|
expect(module).toBe('bmm');
|
|
});
|
|
|
|
it('should handle installed directory structure', async () => {
|
|
// Create installed structure (no src/ prefix)
|
|
const installedDir = path.join(tmpDir, 'installed');
|
|
await fs.ensureDir(path.join(installedDir, 'core/agents'));
|
|
await fs.ensureDir(path.join(installedDir, 'bmm/agents'));
|
|
|
|
const resolver = new DependencyResolver();
|
|
|
|
const coreFile = path.join(installedDir, 'core/agents/agent.md');
|
|
const moduleFile = path.join(installedDir, 'bmm/agents/pm.md');
|
|
|
|
expect(resolver.getModuleFromPath(installedDir, coreFile)).toBe('core');
|
|
expect(resolver.getModuleFromPath(installedDir, moduleFile)).toBe('bmm');
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle malformed YAML frontmatter', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/bad-yaml.md',
|
|
`---
|
|
dependencies: [invalid: yaml: here
|
|
---
|
|
<agent>Agent</agent>`,
|
|
);
|
|
|
|
const resolver = new DependencyResolver();
|
|
|
|
// Should not crash, just warn and continue
|
|
await expect(resolver.resolve(bmadDir, [])).resolves.toBeDefined();
|
|
});
|
|
|
|
it('should handle backticks in YAML values', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/backticks.md',
|
|
`---
|
|
name: \`test\`
|
|
dependencies: [\`{project-root}/bmad/core/tasks/task.md\`]
|
|
---
|
|
<agent>Agent</agent>`,
|
|
);
|
|
await createTestFile(bmadDir, 'core/tasks/task.md', 'Task');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
// Backticks should be pre-processed
|
|
expect(result.allFiles.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('should handle missing dependencies gracefully', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/agent.md',
|
|
`---
|
|
dependencies: ["{project-root}/bmad/core/tasks/missing.md"]
|
|
---
|
|
<agent>Agent</agent>`,
|
|
);
|
|
// Don't create missing.md
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
expect(result.primaryFiles.length).toBeGreaterThanOrEqual(1);
|
|
// Implementation may or may not track missing dependencies
|
|
// Just verify it doesn't crash
|
|
expect(result).toBeDefined();
|
|
});
|
|
|
|
it('should handle empty dependencies array', async () => {
|
|
await createTestFile(
|
|
bmadDir,
|
|
'core/agents/agent.md',
|
|
`---
|
|
dependencies: []
|
|
---
|
|
<agent>Agent</agent>`,
|
|
);
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
expect(result.primaryFiles).toHaveLength(1);
|
|
expect(result.allFiles).toHaveLength(1);
|
|
});
|
|
|
|
it('should handle missing frontmatter', async () => {
|
|
await createTestFile(bmadDir, 'core/agents/no-frontmatter.md', '<agent>Agent</agent>');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, []);
|
|
|
|
expect(result.primaryFiles).toHaveLength(1);
|
|
expect(result.allFiles).toHaveLength(1);
|
|
});
|
|
|
|
it('should handle non-existent module directory', async () => {
|
|
// Create at least one core file so core module appears
|
|
await createTestFile(bmadDir, 'core/agents/core-agent.md', '<agent>Core</agent>');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, ['nonexistent']);
|
|
|
|
// Should include core even though nonexistent module not found
|
|
expect(result.byModule.core).toBeDefined();
|
|
expect(result.byModule.nonexistent).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('cross-module dependencies', () => {
|
|
it('should resolve dependencies across modules', async () => {
|
|
await createTestFile(bmadDir, 'core/agents/core-agent.md', '<agent>Core</agent>');
|
|
await createTestFile(
|
|
bmadDir,
|
|
'bmm/agents/bmm-agent.md',
|
|
`---
|
|
dependencies: ["{project-root}/bmad/core/tasks/shared-task.md"]
|
|
---
|
|
<agent>BMM Agent</agent>`,
|
|
);
|
|
await createTestFile(bmadDir, 'core/tasks/shared-task.md', 'Shared task');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, ['bmm']);
|
|
|
|
// Should include: core agent + bmm agent + shared task
|
|
expect(result.allFiles.length).toBeGreaterThanOrEqual(3);
|
|
expect(result.byModule.core).toBeDefined();
|
|
expect(result.byModule.bmm).toBeDefined();
|
|
});
|
|
|
|
it('should resolve module tasks', async () => {
|
|
await createTestFile(bmadDir, 'core/agents/core-agent.md', '<agent>Core</agent>');
|
|
await createTestFile(bmadDir, 'bmm/agents/pm.md', '<agent>PM</agent>');
|
|
await createTestFile(bmadDir, 'bmm/tasks/create-prd.md', 'Create PRD task');
|
|
|
|
const resolver = new DependencyResolver();
|
|
const result = await resolver.resolve(bmadDir, ['bmm']);
|
|
|
|
expect(result.byModule.bmm.agents).toHaveLength(1);
|
|
expect(result.byModule.bmm.tasks).toHaveLength(1);
|
|
});
|
|
});
|
|
});
|