@eddacraft/anvil-adapters 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +180 -0
- package/BMAD_ADAPTER_SPEC.md +489 -0
- package/LICENSE +14 -0
- package/README.md +500 -0
- package/dist/aps-markdown/adapter.d.ts +102 -0
- package/dist/aps-markdown/adapter.d.ts.map +1 -0
- package/dist/aps-markdown/adapter.js +351 -0
- package/dist/aps-markdown/index.d.ts +8 -0
- package/dist/aps-markdown/index.d.ts.map +1 -0
- package/dist/aps-markdown/index.js +7 -0
- package/dist/base/file-discovery.d.ts +63 -0
- package/dist/base/file-discovery.d.ts.map +1 -0
- package/dist/base/file-discovery.js +246 -0
- package/dist/base/index.d.ts +10 -0
- package/dist/base/index.d.ts.map +1 -0
- package/dist/base/index.js +9 -0
- package/dist/base/registry.d.ts +155 -0
- package/dist/base/registry.d.ts.map +1 -0
- package/dist/base/registry.js +227 -0
- package/dist/base/testing.d.ts +102 -0
- package/dist/base/testing.d.ts.map +1 -0
- package/dist/base/testing.js +221 -0
- package/dist/base/types.d.ts +255 -0
- package/dist/base/types.d.ts.map +1 -0
- package/dist/base/types.js +78 -0
- package/dist/base/utils.d.ts +127 -0
- package/dist/base/utils.d.ts.map +1 -0
- package/dist/base/utils.js +254 -0
- package/dist/bmad/format-adapter.d.ts +76 -0
- package/dist/bmad/format-adapter.d.ts.map +1 -0
- package/dist/bmad/format-adapter.js +186 -0
- package/dist/bmad/index.d.ts +12 -0
- package/dist/bmad/index.d.ts.map +1 -0
- package/dist/bmad/index.js +10 -0
- package/dist/bmad/parser.d.ts +12 -0
- package/dist/bmad/parser.d.ts.map +1 -0
- package/dist/bmad/parser.js +181 -0
- package/dist/bmad/serializer.d.ts +16 -0
- package/dist/bmad/serializer.d.ts.map +1 -0
- package/dist/bmad/serializer.js +170 -0
- package/dist/bmad/types.d.ts +127 -0
- package/dist/bmad/types.d.ts.map +1 -0
- package/dist/bmad/types.js +47 -0
- package/dist/bmad/utils.d.ts +120 -0
- package/dist/bmad/utils.d.ts.map +1 -0
- package/dist/bmad/utils.js +480 -0
- package/dist/common/index.d.ts +3 -0
- package/dist/common/index.d.ts.map +1 -0
- package/dist/common/index.js +2 -0
- package/dist/common/registry.d.ts +18 -0
- package/dist/common/registry.d.ts.map +1 -0
- package/dist/common/registry.js +58 -0
- package/dist/common/types.d.ts +68 -0
- package/dist/common/types.d.ts.map +1 -0
- package/dist/common/types.js +12 -0
- package/dist/generic/format-adapter.d.ts +64 -0
- package/dist/generic/format-adapter.d.ts.map +1 -0
- package/dist/generic/format-adapter.js +159 -0
- package/dist/generic/index.d.ts +10 -0
- package/dist/generic/index.d.ts.map +1 -0
- package/dist/generic/index.js +9 -0
- package/dist/generic/parser.d.ts +11 -0
- package/dist/generic/parser.d.ts.map +1 -0
- package/dist/generic/parser.js +106 -0
- package/dist/generic/serializer.d.ts +11 -0
- package/dist/generic/serializer.d.ts.map +1 -0
- package/dist/generic/serializer.js +118 -0
- package/dist/generic/types.d.ts +52 -0
- package/dist/generic/types.d.ts.map +1 -0
- package/dist/generic/types.js +6 -0
- package/dist/generic/utils.d.ts +51 -0
- package/dist/generic/utils.d.ts.map +1 -0
- package/dist/generic/utils.js +232 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/speckit/export.d.ts +22 -0
- package/dist/speckit/export.d.ts.map +1 -0
- package/dist/speckit/export.js +384 -0
- package/dist/speckit/format-adapter.d.ts +104 -0
- package/dist/speckit/format-adapter.d.ts.map +1 -0
- package/dist/speckit/format-adapter.js +488 -0
- package/dist/speckit/import-v2.d.ts +33 -0
- package/dist/speckit/import-v2.d.ts.map +1 -0
- package/dist/speckit/import-v2.js +361 -0
- package/dist/speckit/import.d.ts +16 -0
- package/dist/speckit/import.d.ts.map +1 -0
- package/dist/speckit/import.js +247 -0
- package/dist/speckit/index.d.ts +5 -0
- package/dist/speckit/index.d.ts.map +1 -0
- package/dist/speckit/index.js +4 -0
- package/dist/speckit/parser.d.ts +28 -0
- package/dist/speckit/parser.d.ts.map +1 -0
- package/dist/speckit/parser.js +283 -0
- package/dist/speckit/parsers/plan-parser.d.ts +71 -0
- package/dist/speckit/parsers/plan-parser.d.ts.map +1 -0
- package/dist/speckit/parsers/plan-parser.js +216 -0
- package/dist/speckit/parsers/spec-parser.d.ts +67 -0
- package/dist/speckit/parsers/spec-parser.d.ts.map +1 -0
- package/dist/speckit/parsers/spec-parser.js +255 -0
- package/dist/speckit/parsers/tasks-parser.d.ts +57 -0
- package/dist/speckit/parsers/tasks-parser.d.ts.map +1 -0
- package/dist/speckit/parsers/tasks-parser.js +157 -0
- package/package.json +23 -0
- package/project.json +29 -0
- package/src/__tests__/adapter-edge-cases.test.ts +937 -0
- package/src/__tests__/bmad-format-adapter.test.ts +1470 -0
- package/src/__tests__/fixtures/aps/expected-output.json +83 -0
- package/src/__tests__/fixtures/bmad/invalid-malformed-yaml.md +16 -0
- package/src/__tests__/fixtures/bmad/invalid-no-requirements.md +23 -0
- package/src/__tests__/fixtures/bmad/invalid-only-yaml.md +16 -0
- package/src/__tests__/fixtures/bmad/invalid-too-short.md +3 -0
- package/src/__tests__/fixtures/bmad/invalid-wrong-format.md +40 -0
- package/src/__tests__/fixtures/bmad/valid-agent.md +27 -0
- package/src/__tests__/fixtures/bmad/valid-architecture.md +116 -0
- package/src/__tests__/fixtures/bmad/valid-complex-prd.md +161 -0
- package/src/__tests__/fixtures/bmad/valid-epic.md +73 -0
- package/src/__tests__/fixtures/bmad/valid-minimal-prd.md +19 -0
- package/src/__tests__/fixtures/bmad/valid-prd.md +107 -0
- package/src/__tests__/fixtures/bmad/valid-story.md +107 -0
- package/src/__tests__/fixtures/bmad/valid-task.md +79 -0
- package/src/__tests__/fixtures/bmad/valid-v6-prd.md +35 -0
- package/src/__tests__/fixtures/generic/plan-detailed.md +39 -0
- package/src/__tests__/fixtures/generic/prd-simple.md +27 -0
- package/src/__tests__/fixtures/generic/rfc-example.md +26 -0
- package/src/__tests__/fixtures/generic/todo-list.md +23 -0
- package/src/__tests__/fixtures/speckit/sample-plan.md +63 -0
- package/src/__tests__/fixtures/speckit/sample-spec-namespaced.md +50 -0
- package/src/__tests__/fixtures/speckit/sample-spec.md +105 -0
- package/src/__tests__/fixtures/speckit/sample-tasks.md +87 -0
- package/src/__tests__/fixtures/speckit-official/auth-feature/plan.md +272 -0
- package/src/__tests__/fixtures/speckit-official/auth-feature/spec.md +149 -0
- package/src/__tests__/fixtures/speckit-official/auth-feature/tasks.md +169 -0
- package/src/__tests__/generic-format-adapter.test.ts +398 -0
- package/src/__tests__/speckit-export.test.ts +233 -0
- package/src/__tests__/speckit-format-adapter.test.ts +832 -0
- package/src/__tests__/speckit-import-v2.test.ts +253 -0
- package/src/__tests__/speckit-import.test.ts +209 -0
- package/src/__tests__/speckit-parser.test.ts +219 -0
- package/src/__tests__/speckit-spec-parser.test.ts +120 -0
- package/src/aps-markdown/__tests__/__fixtures__/simple-leaf.aps.md +17 -0
- package/src/aps-markdown/__tests__/adapter.test.ts +393 -0
- package/src/aps-markdown/adapter.ts +455 -0
- package/src/aps-markdown/index.ts +8 -0
- package/src/base/__tests__/registry.test.ts +515 -0
- package/src/base/file-discovery.ts +305 -0
- package/src/base/index.ts +10 -0
- package/src/base/registry.ts +263 -0
- package/src/base/testing.ts +334 -0
- package/src/base/types.ts +342 -0
- package/src/base/utils.ts +306 -0
- package/src/bmad/format-adapter.ts +227 -0
- package/src/bmad/index.ts +21 -0
- package/src/bmad/parser.ts +224 -0
- package/src/bmad/serializer.ts +206 -0
- package/src/bmad/types.ts +135 -0
- package/src/bmad/utils.ts +575 -0
- package/src/common/index.ts +2 -0
- package/src/common/registry.ts +72 -0
- package/src/common/types.ts +84 -0
- package/src/generic/__tests__/serializer.test.ts +167 -0
- package/src/generic/format-adapter.ts +200 -0
- package/src/generic/index.ts +11 -0
- package/src/generic/parser.ts +129 -0
- package/src/generic/serializer.ts +134 -0
- package/src/generic/types.ts +53 -0
- package/src/generic/utils.ts +270 -0
- package/src/index.ts +48 -0
- package/src/speckit/export.ts +489 -0
- package/src/speckit/format-adapter.ts +595 -0
- package/src/speckit/import-v2.ts +445 -0
- package/src/speckit/import.ts +305 -0
- package/src/speckit/index.ts +4 -0
- package/src/speckit/parser.ts +351 -0
- package/src/speckit/parsers/plan-parser.ts +342 -0
- package/src/speckit/parsers/spec-parser.ts +379 -0
- package/src/speckit/parsers/tasks-parser.ts +246 -0
- package/tsconfig.json +26 -0
- package/tsconfig.lib.json +21 -0
- package/tsconfig.lib.tsbuildinfo +1 -0
- package/tsconfig.spec.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,1470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BMAD Format Adapter Tests
|
|
3
|
+
* Tests for format detection, parsing, serialization, and validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
7
|
+
import { readFile } from 'node:fs/promises';
|
|
8
|
+
import { join, dirname } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { BMADFormatAdapter } from '../bmad/format-adapter.js';
|
|
11
|
+
import {
|
|
12
|
+
analyzePath,
|
|
13
|
+
expandVariables,
|
|
14
|
+
parseYamlBoolean,
|
|
15
|
+
hasHyphenatedVariables,
|
|
16
|
+
} from '../bmad/utils.js';
|
|
17
|
+
import { BMAD_FOLDERS } from '../bmad/types.js';
|
|
18
|
+
import type { ParseContext, PathDetectionHint } from '../base/types.js';
|
|
19
|
+
|
|
20
|
+
// Get __dirname equivalent for ES modules
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = dirname(__filename);
|
|
23
|
+
const fixturesDir = join(__dirname, 'fixtures/bmad');
|
|
24
|
+
|
|
25
|
+
describe('BMADFormatAdapter', () => {
|
|
26
|
+
let adapter: BMADFormatAdapter;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
adapter = new BMADFormatAdapter();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('metadata', () => {
|
|
33
|
+
it('should have correct name and version', () => {
|
|
34
|
+
expect(adapter.metadata.name).toBe('bmad');
|
|
35
|
+
expect(adapter.metadata.version).toBe('1.0.0');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should have correct display name', () => {
|
|
39
|
+
expect(adapter.metadata.displayName).toBe(
|
|
40
|
+
'BMAD (Breakthrough Method for Agile AI-Driven Development)'
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should support bmad formats', () => {
|
|
45
|
+
expect(adapter.metadata.formats).toContain('bmad');
|
|
46
|
+
expect(adapter.metadata.formats).toContain('prd');
|
|
47
|
+
expect(adapter.metadata.formats).toContain('architecture');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should support .md extension', () => {
|
|
51
|
+
expect(adapter.metadata.extensions).toContain('.md');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('canImport / canExport', () => {
|
|
56
|
+
it('should support importing bmad format', () => {
|
|
57
|
+
expect(adapter.canImport('bmad')).toBe(true);
|
|
58
|
+
expect(adapter.canImport('prd')).toBe(true);
|
|
59
|
+
expect(adapter.canImport('architecture')).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should support exporting to bmad format', () => {
|
|
63
|
+
expect(adapter.canExport('bmad')).toBe(true);
|
|
64
|
+
expect(adapter.canExport('prd')).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should not support unknown formats', () => {
|
|
68
|
+
expect(adapter.canImport('unknown')).toBe(false);
|
|
69
|
+
expect(adapter.canExport('unknown')).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should support .md extension', () => {
|
|
73
|
+
expect(adapter.canImport('.md')).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('detect', () => {
|
|
78
|
+
it('should detect valid PRD document with high confidence', async () => {
|
|
79
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
80
|
+
const result = adapter.detect(content);
|
|
81
|
+
|
|
82
|
+
expect(result.detected).toBe(true);
|
|
83
|
+
expect(result.confidence).toBeGreaterThanOrEqual(80);
|
|
84
|
+
expect(result.reason).toContain('yaml-frontmatter');
|
|
85
|
+
expect(result.reason).toContain('requirements');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should detect valid architecture document', async () => {
|
|
89
|
+
const content = await readFile(join(fixturesDir, 'valid-architecture.md'), 'utf-8');
|
|
90
|
+
const result = adapter.detect(content);
|
|
91
|
+
|
|
92
|
+
expect(result.detected).toBe(true);
|
|
93
|
+
expect(result.confidence).toBeGreaterThanOrEqual(50);
|
|
94
|
+
expect(result.reason).toContain('yaml-frontmatter');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should detect valid epic document', async () => {
|
|
98
|
+
const content = await readFile(join(fixturesDir, 'valid-epic.md'), 'utf-8');
|
|
99
|
+
const result = adapter.detect(content);
|
|
100
|
+
|
|
101
|
+
expect(result.detected).toBe(true);
|
|
102
|
+
expect(result.confidence).toBeGreaterThanOrEqual(50);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should detect valid story document', async () => {
|
|
106
|
+
const content = await readFile(join(fixturesDir, 'valid-story.md'), 'utf-8');
|
|
107
|
+
const result = adapter.detect(content);
|
|
108
|
+
|
|
109
|
+
expect(result.detected).toBe(true);
|
|
110
|
+
expect(result.confidence).toBeGreaterThanOrEqual(50);
|
|
111
|
+
expect(result.reason).toContain('user-story');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should not detect document that is too short', async () => {
|
|
115
|
+
const content = await readFile(join(fixturesDir, 'invalid-too-short.md'), 'utf-8');
|
|
116
|
+
const result = adapter.detect(content);
|
|
117
|
+
|
|
118
|
+
expect(result.detected).toBe(false);
|
|
119
|
+
expect(result.confidence).toBeLessThan(50);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should have low confidence for document without requirements', async () => {
|
|
123
|
+
const content = await readFile(join(fixturesDir, 'invalid-no-requirements.md'), 'utf-8');
|
|
124
|
+
const result = adapter.detect(content);
|
|
125
|
+
|
|
126
|
+
// May detect YAML but lack of requirements should lower confidence
|
|
127
|
+
expect(result.confidence).toBeLessThan(80);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should detect YAML front-matter indicator', async () => {
|
|
131
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
132
|
+
const result = adapter.detect(content);
|
|
133
|
+
|
|
134
|
+
expect(result.reason).toContain('yaml-frontmatter');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should detect requirements indicator', async () => {
|
|
138
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
139
|
+
const result = adapter.detect(content);
|
|
140
|
+
|
|
141
|
+
expect(result.reason).toMatch(/\d+ requirements?/);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should detect user story format indicator', async () => {
|
|
145
|
+
const content = await readFile(join(fixturesDir, 'valid-story.md'), 'utf-8');
|
|
146
|
+
const result = adapter.detect(content);
|
|
147
|
+
|
|
148
|
+
expect(result.reason).toContain('user-story');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should detect change log indicator', async () => {
|
|
152
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
153
|
+
const result = adapter.detect(content);
|
|
154
|
+
|
|
155
|
+
expect(result.reason).toContain('change-log');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should not detect plain markdown without BMAD indicators', () => {
|
|
159
|
+
const content = `# Regular Document\n\nThis is just plain markdown content.`;
|
|
160
|
+
const result = adapter.detect(content);
|
|
161
|
+
|
|
162
|
+
expect(result.detected).toBe(false);
|
|
163
|
+
expect(result.confidence).toBeLessThan(50);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('parse', () => {
|
|
168
|
+
it('should parse valid PRD document to APS', async () => {
|
|
169
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
170
|
+
const context: ParseContext = {
|
|
171
|
+
filePath: 'test-prd.md',
|
|
172
|
+
author: 'Test Author',
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const result = await adapter.parse(content, context);
|
|
176
|
+
|
|
177
|
+
expect(result.success).toBe(true);
|
|
178
|
+
if (result.success) {
|
|
179
|
+
expect(result.data).toBeDefined();
|
|
180
|
+
expect(result.data?.schema_version).toBe('0.1.0');
|
|
181
|
+
expect(result.data?.intent).toBeDefined();
|
|
182
|
+
expect(result.data?.proposed_changes).toBeDefined();
|
|
183
|
+
expect(result.data?.provenance).toBeDefined();
|
|
184
|
+
expect(result.data?.hash).toBeDefined();
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should extract intent from PRD executive summary', async () => {
|
|
189
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
190
|
+
const result = await adapter.parse(content);
|
|
191
|
+
|
|
192
|
+
expect(result.success).toBe(true);
|
|
193
|
+
if (result.success && result.data) {
|
|
194
|
+
expect(result.data.intent).toBeDefined();
|
|
195
|
+
expect(result.data.intent.toLowerCase()).toContain('authentication');
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should parse YAML front-matter metadata', async () => {
|
|
200
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
201
|
+
const result = await adapter.parse(content);
|
|
202
|
+
|
|
203
|
+
expect(result.success).toBe(true);
|
|
204
|
+
if (result.success) {
|
|
205
|
+
expect(result.data?.provenance.author).toBe('Jane Smith');
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should parse functional requirements as changes', async () => {
|
|
210
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
211
|
+
const result = await adapter.parse(content);
|
|
212
|
+
|
|
213
|
+
expect(result.success).toBe(true);
|
|
214
|
+
if (result.success) {
|
|
215
|
+
const frChanges = result.data?.proposed_changes.filter((c) =>
|
|
216
|
+
c.description.includes('FR-')
|
|
217
|
+
);
|
|
218
|
+
expect(frChanges.length).toBeGreaterThan(0);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should parse non-functional requirements as changes', async () => {
|
|
223
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
224
|
+
const result = await adapter.parse(content);
|
|
225
|
+
|
|
226
|
+
expect(result.success).toBe(true);
|
|
227
|
+
if (result.success) {
|
|
228
|
+
const nfrChanges = result.data?.proposed_changes.filter((c) =>
|
|
229
|
+
c.description.includes('NFR-')
|
|
230
|
+
);
|
|
231
|
+
expect(nfrChanges.length).toBeGreaterThan(0);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should parse user stories as changes', async () => {
|
|
236
|
+
const content = await readFile(join(fixturesDir, 'valid-story.md'), 'utf-8');
|
|
237
|
+
const result = await adapter.parse(content);
|
|
238
|
+
|
|
239
|
+
expect(result.success).toBe(true);
|
|
240
|
+
if (result.success && result.data) {
|
|
241
|
+
expect(result.data.proposed_changes.length).toBeGreaterThan(0);
|
|
242
|
+
expect(result.data.intent).toBeDefined();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should handle document without front-matter', async () => {
|
|
247
|
+
const content = `# PRD Document
|
|
248
|
+
|
|
249
|
+
FR-01: Some requirement
|
|
250
|
+
|
|
251
|
+
NFR-01: Some non-functional requirement`;
|
|
252
|
+
|
|
253
|
+
const result = await adapter.parse(content);
|
|
254
|
+
|
|
255
|
+
expect(result.success).toBe(true);
|
|
256
|
+
if (result.success) {
|
|
257
|
+
expect(result.data?.provenance.author).toBe('unknown');
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should use context for provenance when provided', async () => {
|
|
262
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
263
|
+
const context: ParseContext = {
|
|
264
|
+
filePath: '/path/to/prd.md',
|
|
265
|
+
author: 'Context Author',
|
|
266
|
+
repositoryPath: '/path/to/repo',
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const result = await adapter.parse(content, context);
|
|
270
|
+
|
|
271
|
+
expect(result.success).toBe(true);
|
|
272
|
+
if (result.success) {
|
|
273
|
+
// YAML author should override context author
|
|
274
|
+
expect(result.data?.provenance.author).toBe('Jane Smith');
|
|
275
|
+
expect(result.data?.provenance.repository).toBe('/path/to/repo');
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should handle parse errors gracefully', async () => {
|
|
280
|
+
const content = 'Invalid content without proper structure';
|
|
281
|
+
|
|
282
|
+
const result = await adapter.parse(content);
|
|
283
|
+
|
|
284
|
+
// Parser is lenient and will parse minimal content
|
|
285
|
+
// This test verifies error handling exists
|
|
286
|
+
expect(result.success).toBeDefined();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should generate consistent hashes for same content', async () => {
|
|
290
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
291
|
+
const fixedContext: ParseContext = {
|
|
292
|
+
author: 'Test Author',
|
|
293
|
+
timestamp: '2025-01-01T00:00:00Z', // Fixed timestamp for deterministic hash
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const result1 = await adapter.parse(content, fixedContext);
|
|
297
|
+
const result2 = await adapter.parse(content, fixedContext);
|
|
298
|
+
|
|
299
|
+
expect(result1.success).toBe(true);
|
|
300
|
+
expect(result2.success).toBe(true);
|
|
301
|
+
|
|
302
|
+
// Note: Hashes will differ due to random plan ID generation
|
|
303
|
+
// This test verifies that parsing succeeds and generates valid hashes
|
|
304
|
+
if (result1.success && result2.success) {
|
|
305
|
+
expect(result1.data?.hash).toMatch(/^[a-f0-9]{64}$/);
|
|
306
|
+
expect(result2.data?.hash).toMatch(/^[a-f0-9]{64}$/);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('serialize', () => {
|
|
312
|
+
it('should serialize APS plan to BMAD format', async () => {
|
|
313
|
+
// First parse a BMAD document to APS
|
|
314
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
315
|
+
const parseResult = await adapter.parse(content);
|
|
316
|
+
|
|
317
|
+
expect(parseResult.success).toBe(true);
|
|
318
|
+
if (!parseResult.success) return;
|
|
319
|
+
|
|
320
|
+
// Then serialize back to BMAD
|
|
321
|
+
const serializeResult = await adapter.serialize(parseResult.data!);
|
|
322
|
+
|
|
323
|
+
expect(serializeResult.success).toBe(true);
|
|
324
|
+
if (serializeResult.success) {
|
|
325
|
+
expect(serializeResult.content).toBeDefined();
|
|
326
|
+
expect(serializeResult.content.length).toBeGreaterThan(0);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should include YAML front-matter in serialized output', async () => {
|
|
331
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
332
|
+
const parseResult = await adapter.parse(content);
|
|
333
|
+
|
|
334
|
+
expect(parseResult.success).toBe(true);
|
|
335
|
+
if (!parseResult.success) return;
|
|
336
|
+
|
|
337
|
+
const serializeResult = await adapter.serialize(parseResult.data!);
|
|
338
|
+
|
|
339
|
+
expect(serializeResult.success).toBe(true);
|
|
340
|
+
if (serializeResult.success) {
|
|
341
|
+
expect(serializeResult.content).toMatch(/^---\n/);
|
|
342
|
+
expect(serializeResult.content).toContain('name:');
|
|
343
|
+
expect(serializeResult.content).toContain('version:');
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should include change log table in serialized output', async () => {
|
|
348
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
349
|
+
const parseResult = await adapter.parse(content);
|
|
350
|
+
|
|
351
|
+
expect(parseResult.success).toBe(true);
|
|
352
|
+
if (!parseResult.success) return;
|
|
353
|
+
|
|
354
|
+
const serializeResult = await adapter.serialize(parseResult.data!);
|
|
355
|
+
|
|
356
|
+
expect(serializeResult.success).toBe(true);
|
|
357
|
+
if (serializeResult.success) {
|
|
358
|
+
expect(serializeResult.content).toContain('## Change Log');
|
|
359
|
+
expect(serializeResult.content).toContain('| Date');
|
|
360
|
+
expect(serializeResult.content).toContain('| Version');
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should categorize changes as FR/NFR appropriately', async () => {
|
|
365
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
366
|
+
const parseResult = await adapter.parse(content);
|
|
367
|
+
|
|
368
|
+
expect(parseResult.success).toBe(true);
|
|
369
|
+
if (!parseResult.success) return;
|
|
370
|
+
|
|
371
|
+
const serializeResult = await adapter.serialize(parseResult.data!);
|
|
372
|
+
|
|
373
|
+
expect(serializeResult.success).toBe(true);
|
|
374
|
+
if (serializeResult.success) {
|
|
375
|
+
expect(serializeResult.content).toContain('FR-');
|
|
376
|
+
expect(serializeResult.content).toContain('NFR-');
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should maintain roundtrip fidelity', async () => {
|
|
381
|
+
// Parse BMAD → APS
|
|
382
|
+
const originalContent = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
383
|
+
const parseResult1 = await adapter.parse(originalContent);
|
|
384
|
+
|
|
385
|
+
expect(parseResult1.success).toBe(true);
|
|
386
|
+
if (!parseResult1.success) return;
|
|
387
|
+
|
|
388
|
+
// Serialize APS → BMAD
|
|
389
|
+
const serializeResult = await adapter.serialize(parseResult1.data);
|
|
390
|
+
|
|
391
|
+
expect(serializeResult.success).toBe(true);
|
|
392
|
+
if (!serializeResult.success) return;
|
|
393
|
+
|
|
394
|
+
// Parse again BMAD → APS
|
|
395
|
+
const parseResult2 = await adapter.parse(serializeResult.content);
|
|
396
|
+
|
|
397
|
+
expect(parseResult2.success).toBe(true);
|
|
398
|
+
if (!parseResult2.success) return;
|
|
399
|
+
|
|
400
|
+
// Check key properties are preserved
|
|
401
|
+
if (parseResult2.data) {
|
|
402
|
+
// Intent may be transformed during serialization (e.g., to document title)
|
|
403
|
+
expect(parseResult2.data.intent).toBeDefined();
|
|
404
|
+
expect(parseResult2.data.intent.length).toBeGreaterThan(0);
|
|
405
|
+
// Changes should be present (exact count may vary due to serialization format)
|
|
406
|
+
expect(parseResult2.data.proposed_changes.length).toBeGreaterThan(0);
|
|
407
|
+
// Author should be preserved
|
|
408
|
+
expect(parseResult2.data.provenance.author).toBe(parseResult1.data?.provenance.author);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
describe('validate', () => {
|
|
414
|
+
it('should validate valid PRD document', async () => {
|
|
415
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
416
|
+
const result = await adapter.validate(content);
|
|
417
|
+
|
|
418
|
+
expect(result.valid).toBe(true);
|
|
419
|
+
expect(result.summary).toContain('valid');
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('should reject document that is too short', async () => {
|
|
423
|
+
const content = await readFile(join(fixturesDir, 'invalid-too-short.md'), 'utf-8');
|
|
424
|
+
const result = await adapter.validate(content);
|
|
425
|
+
|
|
426
|
+
expect(result.valid).toBe(false);
|
|
427
|
+
expect(result.issues).toBeDefined();
|
|
428
|
+
if (result.issues) {
|
|
429
|
+
const shortError = result.issues.find((i) => i.code === 'CONTENT_TOO_SHORT');
|
|
430
|
+
expect(shortError).toBeDefined();
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('should reject document with low confidence', async () => {
|
|
435
|
+
const content = `# Not a BMAD Document
|
|
436
|
+
|
|
437
|
+
This is just regular markdown without any BMAD indicators like requirements or YAML front-matter.
|
|
438
|
+
|
|
439
|
+
It has enough content to pass the length check, but it should still fail validation because it doesn't look like BMAD format.`;
|
|
440
|
+
|
|
441
|
+
const result = await adapter.validate(content);
|
|
442
|
+
|
|
443
|
+
expect(result.valid).toBe(false);
|
|
444
|
+
expect(result.issues).toBeDefined();
|
|
445
|
+
if (result.issues) {
|
|
446
|
+
const confidenceError = result.issues.find((i) => i.code === 'LOW_CONFIDENCE');
|
|
447
|
+
expect(confidenceError).toBeDefined();
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('should warn about missing requirements', async () => {
|
|
452
|
+
const content = await readFile(join(fixturesDir, 'invalid-no-requirements.md'), 'utf-8');
|
|
453
|
+
const result = await adapter.validate(content);
|
|
454
|
+
|
|
455
|
+
expect(result.issues).toBeDefined();
|
|
456
|
+
if (result.issues) {
|
|
457
|
+
const noReqWarning = result.issues.find((i) => i.code === 'NO_REQUIREMENTS');
|
|
458
|
+
expect(noReqWarning).toBeDefined();
|
|
459
|
+
if (noReqWarning) {
|
|
460
|
+
expect(noReqWarning.severity).toBe('warning');
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('should provide clear validation summary', async () => {
|
|
466
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
467
|
+
const result = await adapter.validate(content);
|
|
468
|
+
|
|
469
|
+
expect(result.summary).toBeDefined();
|
|
470
|
+
expect(result.summary.length).toBeGreaterThan(0);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('should validate valid architecture document', async () => {
|
|
474
|
+
const content = await readFile(join(fixturesDir, 'valid-architecture.md'), 'utf-8');
|
|
475
|
+
const result = await adapter.validate(content);
|
|
476
|
+
|
|
477
|
+
expect(result.valid).toBe(true);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('should validate valid epic document', async () => {
|
|
481
|
+
const content = await readFile(join(fixturesDir, 'valid-epic.md'), 'utf-8');
|
|
482
|
+
const result = await adapter.validate(content);
|
|
483
|
+
|
|
484
|
+
expect(result.valid).toBe(true);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('should validate valid story document', async () => {
|
|
488
|
+
const content = await readFile(join(fixturesDir, 'valid-story.md'), 'utf-8');
|
|
489
|
+
const result = await adapter.validate(content);
|
|
490
|
+
|
|
491
|
+
expect(result.valid).toBe(true);
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
describe('edge cases', () => {
|
|
496
|
+
describe('unicode and special characters', () => {
|
|
497
|
+
it('should handle unicode characters in requirements', async () => {
|
|
498
|
+
const content = `---
|
|
499
|
+
name: Unicode Test
|
|
500
|
+
version: 1.0.0
|
|
501
|
+
author: Test User
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
# Test Document
|
|
505
|
+
|
|
506
|
+
FR-01: Support émojis 🚀 and unicode characters ñ é
|
|
507
|
+
NFR-01: Handle Chinese characters 中文测试`;
|
|
508
|
+
|
|
509
|
+
const result = await adapter.parse(content);
|
|
510
|
+
|
|
511
|
+
expect(result.success).toBe(true);
|
|
512
|
+
if (result.success) {
|
|
513
|
+
expect(result.data?.proposed_changes.length).toBe(2);
|
|
514
|
+
expect(result.data?.proposed_changes[0]?.description).toContain('🚀');
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('should handle special markdown characters in descriptions', async () => {
|
|
519
|
+
const content = `---
|
|
520
|
+
name: Special Chars Test
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
# Test
|
|
524
|
+
|
|
525
|
+
FR-01: Support **bold**, *italic*, and \`code\` in descriptions
|
|
526
|
+
FR-02: Handle | pipes | and [links](http://example.com)`;
|
|
527
|
+
|
|
528
|
+
const result = await adapter.parse(content);
|
|
529
|
+
|
|
530
|
+
expect(result.success).toBe(true);
|
|
531
|
+
if (result.success) {
|
|
532
|
+
expect(result.data?.proposed_changes.length).toBe(2);
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
describe('requirement ID formats', () => {
|
|
538
|
+
it('should parse requirements with double-digit IDs', async () => {
|
|
539
|
+
const content = `# Test
|
|
540
|
+
|
|
541
|
+
FR-01: First requirement
|
|
542
|
+
FR-10: Tenth requirement
|
|
543
|
+
FR-99: Ninety-ninth requirement`;
|
|
544
|
+
|
|
545
|
+
const result = await adapter.parse(content);
|
|
546
|
+
|
|
547
|
+
expect(result.success).toBe(true);
|
|
548
|
+
if (result.success) {
|
|
549
|
+
expect(result.data?.proposed_changes.length).toBe(3);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('should not parse malformed requirement IDs', () => {
|
|
554
|
+
const content = `# Test
|
|
555
|
+
|
|
556
|
+
FR-1: Wrong format (single digit)
|
|
557
|
+
FR-001: Wrong format (three digits)
|
|
558
|
+
REQ-01: Wrong prefix`;
|
|
559
|
+
|
|
560
|
+
const result = adapter.detect(content);
|
|
561
|
+
|
|
562
|
+
// Should have low confidence due to malformed IDs
|
|
563
|
+
expect(result.confidence).toBeLessThan(50);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('should parse mixed requirement types', async () => {
|
|
567
|
+
const content = `# Test
|
|
568
|
+
|
|
569
|
+
FR-01: Functional requirement
|
|
570
|
+
NFR-01: Non-functional requirement
|
|
571
|
+
US-01: User story requirement
|
|
572
|
+
|
|
573
|
+
As a user,
|
|
574
|
+
I want feature,
|
|
575
|
+
so that benefit.`;
|
|
576
|
+
|
|
577
|
+
const result = await adapter.parse(content);
|
|
578
|
+
|
|
579
|
+
expect(result.success).toBe(true);
|
|
580
|
+
if (result.success) {
|
|
581
|
+
// US-01 is parsed both as a requirement and as a user story, so we get 4 changes
|
|
582
|
+
expect(result.data?.proposed_changes.length).toBeGreaterThanOrEqual(3);
|
|
583
|
+
expect(result.data?.proposed_changes.some((c) => c.description.includes('FR-01'))).toBe(
|
|
584
|
+
true
|
|
585
|
+
);
|
|
586
|
+
expect(result.data?.proposed_changes.some((c) => c.description.includes('NFR-01'))).toBe(
|
|
587
|
+
true
|
|
588
|
+
);
|
|
589
|
+
expect(result.data?.proposed_changes.some((c) => c.description.includes('US-01'))).toBe(
|
|
590
|
+
true
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
describe('empty and minimal content', () => {
|
|
597
|
+
it('should handle empty sections gracefully', async () => {
|
|
598
|
+
const content = `---
|
|
599
|
+
name: Empty Sections Test
|
|
600
|
+
---
|
|
601
|
+
|
|
602
|
+
# Test Document
|
|
603
|
+
|
|
604
|
+
## Executive Summary
|
|
605
|
+
|
|
606
|
+
## Functional Requirements
|
|
607
|
+
|
|
608
|
+
FR-01: Only one requirement
|
|
609
|
+
|
|
610
|
+
## Non-Functional Requirements
|
|
611
|
+
|
|
612
|
+
## User Stories`;
|
|
613
|
+
|
|
614
|
+
const result = await adapter.parse(content);
|
|
615
|
+
|
|
616
|
+
expect(result.success).toBe(true);
|
|
617
|
+
if (result.success) {
|
|
618
|
+
expect(result.data?.proposed_changes.length).toBe(1);
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('should handle document with only front-matter', async () => {
|
|
623
|
+
const content = `---
|
|
624
|
+
name: Minimal Test
|
|
625
|
+
version: 1.0.0
|
|
626
|
+
---`;
|
|
627
|
+
|
|
628
|
+
const result = await adapter.parse(content);
|
|
629
|
+
|
|
630
|
+
expect(result.success).toBe(true);
|
|
631
|
+
// Document is valid but has no changes
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('should detect minimal valid BMAD document', () => {
|
|
635
|
+
const content = `---
|
|
636
|
+
name: Minimal
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
FR-01: Minimal requirement`;
|
|
640
|
+
|
|
641
|
+
const result = adapter.detect(content);
|
|
642
|
+
|
|
643
|
+
expect(result.detected).toBe(true);
|
|
644
|
+
expect(result.confidence).toBeGreaterThanOrEqual(50);
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
describe('large documents', () => {
|
|
649
|
+
it('should handle document with many requirements', async () => {
|
|
650
|
+
let content = `---
|
|
651
|
+
name: Large Document Test
|
|
652
|
+
---
|
|
653
|
+
|
|
654
|
+
# Large Test Document
|
|
655
|
+
|
|
656
|
+
`;
|
|
657
|
+
|
|
658
|
+
// Add 50 requirements
|
|
659
|
+
for (let i = 1; i <= 50; i++) {
|
|
660
|
+
const id = i.toString().padStart(2, '0');
|
|
661
|
+
content += `FR-${id}: Requirement number ${i}\n`;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const result = await adapter.parse(content);
|
|
665
|
+
|
|
666
|
+
expect(result.success).toBe(true);
|
|
667
|
+
if (result.success) {
|
|
668
|
+
expect(result.data?.proposed_changes.length).toBe(50);
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('should handle very long requirement descriptions', async () => {
|
|
673
|
+
const longDescription = 'A'.repeat(1000);
|
|
674
|
+
const content = `---
|
|
675
|
+
name: Long Description Test
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
FR-01: ${longDescription}`;
|
|
679
|
+
|
|
680
|
+
const result = await adapter.parse(content);
|
|
681
|
+
|
|
682
|
+
expect(result.success).toBe(true);
|
|
683
|
+
if (result.success) {
|
|
684
|
+
expect(result.data?.proposed_changes[0]?.description).toContain(longDescription);
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
describe('malformed content', () => {
|
|
690
|
+
it('should handle malformed YAML front-matter', () => {
|
|
691
|
+
const content = `---
|
|
692
|
+
name: Test: Invalid: YAML
|
|
693
|
+
invalid yaml here
|
|
694
|
+
no proper structure
|
|
695
|
+
---
|
|
696
|
+
|
|
697
|
+
FR-01: Some requirement`;
|
|
698
|
+
|
|
699
|
+
const result = adapter.detect(content);
|
|
700
|
+
|
|
701
|
+
// Should still detect due to FR-01
|
|
702
|
+
expect(result.detected).toBe(true);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it('should handle documents with multiple YAML blocks', () => {
|
|
706
|
+
const content = `---
|
|
707
|
+
name: First Block
|
|
708
|
+
---
|
|
709
|
+
|
|
710
|
+
Some content
|
|
711
|
+
|
|
712
|
+
---
|
|
713
|
+
name: Second Block
|
|
714
|
+
---
|
|
715
|
+
|
|
716
|
+
FR-01: Requirement`;
|
|
717
|
+
|
|
718
|
+
const result = adapter.detect(content);
|
|
719
|
+
|
|
720
|
+
// Should still detect YAML and requirements
|
|
721
|
+
expect(result.detected).toBe(true);
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
it('should handle missing YAML closing delimiter', () => {
|
|
725
|
+
const content = `---
|
|
726
|
+
name: Unclosed YAML
|
|
727
|
+
version: 1.0.0
|
|
728
|
+
|
|
729
|
+
FR-01: Requirement without proper YAML close`;
|
|
730
|
+
|
|
731
|
+
const result = adapter.detect(content);
|
|
732
|
+
|
|
733
|
+
// Should still detect based on FR-01
|
|
734
|
+
expect(result.confidence).toBeGreaterThanOrEqual(25);
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
describe('user story format variations', () => {
|
|
739
|
+
it('should detect user story with "As an" (article)', () => {
|
|
740
|
+
const content = `# Story
|
|
741
|
+
|
|
742
|
+
As an administrator,
|
|
743
|
+
I want to manage users,
|
|
744
|
+
so that I can control access.`;
|
|
745
|
+
|
|
746
|
+
const result = adapter.detect(content);
|
|
747
|
+
|
|
748
|
+
expect(result.reason).toContain('user-story');
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it('should detect user story with "As a" (no article)', () => {
|
|
752
|
+
const content = `# Story
|
|
753
|
+
|
|
754
|
+
As a user,
|
|
755
|
+
I want to login,
|
|
756
|
+
so that I can access my account.`;
|
|
757
|
+
|
|
758
|
+
const result = adapter.detect(content);
|
|
759
|
+
|
|
760
|
+
expect(result.reason).toContain('user-story');
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
it('should handle user stories without acceptance criteria', async () => {
|
|
764
|
+
const content = `---
|
|
765
|
+
name: Story Test
|
|
766
|
+
---
|
|
767
|
+
|
|
768
|
+
US-01: Basic Login
|
|
769
|
+
|
|
770
|
+
As a user,
|
|
771
|
+
I want to log in,
|
|
772
|
+
so that I can access my account.`;
|
|
773
|
+
|
|
774
|
+
const result = await adapter.parse(content);
|
|
775
|
+
|
|
776
|
+
expect(result.success).toBe(true);
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
describe('serialization edge cases', () => {
|
|
781
|
+
it('should handle APS plan with no changes', async () => {
|
|
782
|
+
const content = `---
|
|
783
|
+
name: Empty Plan
|
|
784
|
+
---
|
|
785
|
+
|
|
786
|
+
# Test Document`;
|
|
787
|
+
|
|
788
|
+
const parseResult = await adapter.parse(content);
|
|
789
|
+
expect(parseResult.success).toBe(true);
|
|
790
|
+
if (!parseResult.success) return;
|
|
791
|
+
|
|
792
|
+
const serializeResult = await adapter.serialize(parseResult.data!);
|
|
793
|
+
expect(serializeResult.success).toBe(true);
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
it('should serialize plan with special characters', async () => {
|
|
797
|
+
const content = `---
|
|
798
|
+
name: Special Chars
|
|
799
|
+
---
|
|
800
|
+
|
|
801
|
+
FR-01: Support "quotes" and 'apostrophes'
|
|
802
|
+
FR-02: Handle <brackets> and &ersands&`;
|
|
803
|
+
|
|
804
|
+
const parseResult = await adapter.parse(content);
|
|
805
|
+
expect(parseResult.success).toBe(true);
|
|
806
|
+
if (!parseResult.success) return;
|
|
807
|
+
|
|
808
|
+
const serializeResult = await adapter.serialize(parseResult.data!);
|
|
809
|
+
expect(serializeResult.success).toBe(true);
|
|
810
|
+
if (serializeResult.success) {
|
|
811
|
+
expect(serializeResult.content).toContain('quotes');
|
|
812
|
+
expect(serializeResult.content).toContain('brackets');
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
it('should preserve line breaks in descriptions', async () => {
|
|
817
|
+
const content = `---
|
|
818
|
+
name: Line Breaks Test
|
|
819
|
+
---
|
|
820
|
+
|
|
821
|
+
FR-01: Multi-line requirement description that spans multiple lines`;
|
|
822
|
+
|
|
823
|
+
const parseResult = await adapter.parse(content);
|
|
824
|
+
const serializeResult = await adapter.serialize(parseResult.data!);
|
|
825
|
+
|
|
826
|
+
expect(serializeResult.success).toBe(true);
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
describe('detection confidence scoring', () => {
|
|
831
|
+
it('should have maximum confidence with all indicators', () => {
|
|
832
|
+
const content = `---
|
|
833
|
+
name: Product Requirements Document
|
|
834
|
+
version: 1.0.0
|
|
835
|
+
---
|
|
836
|
+
|
|
837
|
+
# Product Requirements Document
|
|
838
|
+
|
|
839
|
+
## Change Log
|
|
840
|
+
|
|
841
|
+
| Date | Version | Description | Author |
|
|
842
|
+
|------|---------|-------------|--------|
|
|
843
|
+
| 2025-01-01 | 1.0.0 | Initial | Test |
|
|
844
|
+
|
|
845
|
+
FR-01: Requirement
|
|
846
|
+
|
|
847
|
+
As a user,
|
|
848
|
+
I want feature,
|
|
849
|
+
so that benefit.`;
|
|
850
|
+
|
|
851
|
+
const result = adapter.detect(content);
|
|
852
|
+
|
|
853
|
+
expect(result.confidence).toBe(100);
|
|
854
|
+
expect(result.reason).toContain('yaml-frontmatter');
|
|
855
|
+
expect(result.reason).toContain('requirements');
|
|
856
|
+
expect(result.reason).toContain('user-story');
|
|
857
|
+
expect(result.reason).toContain('change-log');
|
|
858
|
+
expect(result.reason).toContain('document-title');
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
it('should have 0 confidence for completely unrelated content', () => {
|
|
862
|
+
const content = 'Just some random text without any structure.';
|
|
863
|
+
|
|
864
|
+
const result = adapter.detect(content);
|
|
865
|
+
|
|
866
|
+
expect(result.confidence).toBe(0);
|
|
867
|
+
expect(result.detected).toBe(false);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
it('should have partial confidence with only YAML', () => {
|
|
871
|
+
const content = `---
|
|
872
|
+
name: Test
|
|
873
|
+
---
|
|
874
|
+
|
|
875
|
+
Some content without requirements.`;
|
|
876
|
+
|
|
877
|
+
const result = adapter.detect(content);
|
|
878
|
+
|
|
879
|
+
expect(result.confidence).toBe(30); // Only YAML points
|
|
880
|
+
expect(result.detected).toBe(false); // Below 50% threshold
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
describe('additional parser tests', () => {
|
|
886
|
+
it('should parse valid task document', async () => {
|
|
887
|
+
const content = await readFile(join(fixturesDir, 'valid-task.md'), 'utf-8');
|
|
888
|
+
const result = await adapter.parse(content);
|
|
889
|
+
|
|
890
|
+
expect(result.success).toBe(true);
|
|
891
|
+
if (result.success) {
|
|
892
|
+
expect(result.data?.proposed_changes.length).toBeGreaterThan(0);
|
|
893
|
+
expect(result.data?.provenance.author).toBe('Developer');
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
it('should parse minimal PRD correctly', async () => {
|
|
898
|
+
const content = await readFile(join(fixturesDir, 'valid-minimal-prd.md'), 'utf-8');
|
|
899
|
+
const result = await adapter.parse(content);
|
|
900
|
+
|
|
901
|
+
expect(result.success).toBe(true);
|
|
902
|
+
if (result.success) {
|
|
903
|
+
// Prettier reformatted the file so some requirements are on same line
|
|
904
|
+
// Parser extracts what it can find with proper FR-XX: format on separate lines
|
|
905
|
+
expect(result.data?.proposed_changes.length).toBeGreaterThanOrEqual(1);
|
|
906
|
+
expect(result.data?.provenance.author).toBe('John Doe');
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
it('should parse complex PRD with many requirements', async () => {
|
|
911
|
+
const content = await readFile(join(fixturesDir, 'valid-complex-prd.md'), 'utf-8');
|
|
912
|
+
const result = await adapter.parse(content);
|
|
913
|
+
|
|
914
|
+
expect(result.success).toBe(true);
|
|
915
|
+
if (result.success) {
|
|
916
|
+
// Prettier reformatted the file so requirements are wrapped on same lines
|
|
917
|
+
// Parser extracts requirements that follow proper format (on separate lines)
|
|
918
|
+
expect(result.data?.proposed_changes.length).toBeGreaterThanOrEqual(20);
|
|
919
|
+
expect(result.data?.provenance.author).toBe('Product Team');
|
|
920
|
+
expect(result.data?.provenance.version).toBe('2.1.0');
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
it('should handle malformed YAML gracefully', async () => {
|
|
925
|
+
const content = await readFile(join(fixturesDir, 'invalid-malformed-yaml.md'), 'utf-8');
|
|
926
|
+
const result = await adapter.parse(content);
|
|
927
|
+
|
|
928
|
+
// Parser should be lenient and still extract requirements
|
|
929
|
+
expect(result.success).toBe(true);
|
|
930
|
+
if (result.success) {
|
|
931
|
+
expect(result.data?.proposed_changes.length).toBeGreaterThan(0);
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
it('should extract requirement IDs accurately', async () => {
|
|
936
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
937
|
+
const result = await adapter.parse(content);
|
|
938
|
+
|
|
939
|
+
expect(result.success).toBe(true);
|
|
940
|
+
if (result.success) {
|
|
941
|
+
const descriptions = result.data?.proposed_changes.map((c) => c.description) || [];
|
|
942
|
+
// Check that FR/NFR IDs are preserved in descriptions
|
|
943
|
+
expect(descriptions.some((d) => d.includes('FR-'))).toBe(true);
|
|
944
|
+
expect(descriptions.some((d) => d.includes('NFR-'))).toBe(true);
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
it('should map different requirement types to appropriate change types', async () => {
|
|
949
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
950
|
+
const result = await adapter.parse(content);
|
|
951
|
+
|
|
952
|
+
expect(result.success).toBe(true);
|
|
953
|
+
if (result.success) {
|
|
954
|
+
const changes = result.data?.proposed_changes || [];
|
|
955
|
+
// Should have file_create for FR (functional) and config_update for NFR
|
|
956
|
+
expect(changes.some((c) => c.type === 'file_create')).toBe(true);
|
|
957
|
+
expect(changes.some((c) => c.type === 'config_update')).toBe(true);
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
it('should extract provenance metadata correctly from different sources', async () => {
|
|
962
|
+
const content = await readFile(join(fixturesDir, 'valid-architecture.md'), 'utf-8');
|
|
963
|
+
const context: ParseContext = {
|
|
964
|
+
filePath: 'test.md',
|
|
965
|
+
repositoryPath: '/repo',
|
|
966
|
+
branch: 'main',
|
|
967
|
+
commit: 'abc123',
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
const result = await adapter.parse(content, context);
|
|
971
|
+
|
|
972
|
+
expect(result.success).toBe(true);
|
|
973
|
+
if (result.success) {
|
|
974
|
+
expect(result.data?.provenance.repository).toBe('/repo');
|
|
975
|
+
expect(result.data?.provenance.branch).toBe('main');
|
|
976
|
+
expect(result.data?.provenance.commit).toBe('abc123');
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
it('should handle document with no changes gracefully', async () => {
|
|
981
|
+
const content = await readFile(join(fixturesDir, 'invalid-only-yaml.md'), 'utf-8');
|
|
982
|
+
const result = await adapter.parse(content);
|
|
983
|
+
|
|
984
|
+
expect(result.success).toBe(true);
|
|
985
|
+
if (result.success) {
|
|
986
|
+
// Document with no requirements should result in empty changes array
|
|
987
|
+
expect(result.data?.proposed_changes).toBeDefined();
|
|
988
|
+
expect(Array.isArray(result.data?.proposed_changes)).toBe(true);
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
it('should handle very long requirement descriptions', async () => {
|
|
993
|
+
const longDesc = 'A'.repeat(500);
|
|
994
|
+
const content = `---
|
|
995
|
+
name: Long Description Test
|
|
996
|
+
---
|
|
997
|
+
|
|
998
|
+
FR-01: ${longDesc}`;
|
|
999
|
+
|
|
1000
|
+
const result = await adapter.parse(content);
|
|
1001
|
+
|
|
1002
|
+
expect(result.success).toBe(true);
|
|
1003
|
+
if (result.success) {
|
|
1004
|
+
expect(result.data?.proposed_changes[0]?.description).toContain(longDesc);
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
describe('additional serializer tests', () => {
|
|
1010
|
+
it('should serialize plan with no execution history', async () => {
|
|
1011
|
+
const content = await readFile(join(fixturesDir, 'valid-minimal-prd.md'), 'utf-8');
|
|
1012
|
+
const parseResult = await adapter.parse(content);
|
|
1013
|
+
|
|
1014
|
+
expect(parseResult.success).toBe(true);
|
|
1015
|
+
if (!parseResult.success) return;
|
|
1016
|
+
|
|
1017
|
+
const serializeResult = await adapter.serialize(parseResult.data!);
|
|
1018
|
+
|
|
1019
|
+
expect(serializeResult.success).toBe(true);
|
|
1020
|
+
if (serializeResult.success) {
|
|
1021
|
+
expect(serializeResult.content).toContain('---');
|
|
1022
|
+
expect(serializeResult.content).toContain('name:');
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
it('should serialize plan with custom metadata', async () => {
|
|
1027
|
+
const content = await readFile(join(fixturesDir, 'valid-task.md'), 'utf-8');
|
|
1028
|
+
const parseResult = await adapter.parse(content);
|
|
1029
|
+
|
|
1030
|
+
expect(parseResult.success).toBe(true);
|
|
1031
|
+
if (!parseResult.success) return;
|
|
1032
|
+
|
|
1033
|
+
const serializeResult = await adapter.serialize(parseResult.data!);
|
|
1034
|
+
|
|
1035
|
+
expect(serializeResult.success).toBe(true);
|
|
1036
|
+
if (serializeResult.success) {
|
|
1037
|
+
expect(serializeResult.content).toContain('Product Requirements Document');
|
|
1038
|
+
expect(serializeResult.content).toContain('Change Log');
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
it('should serialize plan with very long descriptions', async () => {
|
|
1043
|
+
const content = await readFile(join(fixturesDir, 'valid-complex-prd.md'), 'utf-8');
|
|
1044
|
+
const parseResult = await adapter.parse(content);
|
|
1045
|
+
|
|
1046
|
+
expect(parseResult.success).toBe(true);
|
|
1047
|
+
if (!parseResult.success) return;
|
|
1048
|
+
|
|
1049
|
+
const serializeResult = await adapter.serialize(parseResult.data!);
|
|
1050
|
+
|
|
1051
|
+
expect(serializeResult.success).toBe(true);
|
|
1052
|
+
if (serializeResult.success) {
|
|
1053
|
+
// Verify serialization doesn't truncate content
|
|
1054
|
+
expect(serializeResult.content.length).toBeGreaterThan(1000);
|
|
1055
|
+
expect(serializeResult.content).toContain('FR-');
|
|
1056
|
+
expect(serializeResult.content).toContain('NFR-');
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
it('should properly categorize mixed requirement types', async () => {
|
|
1061
|
+
const content = await readFile(join(fixturesDir, 'valid-complex-prd.md'), 'utf-8');
|
|
1062
|
+
const parseResult = await adapter.parse(content);
|
|
1063
|
+
|
|
1064
|
+
expect(parseResult.success).toBe(true);
|
|
1065
|
+
if (!parseResult.success) return;
|
|
1066
|
+
|
|
1067
|
+
const serializeResult = await adapter.serialize(parseResult.data!);
|
|
1068
|
+
|
|
1069
|
+
expect(serializeResult.success).toBe(true);
|
|
1070
|
+
if (serializeResult.success) {
|
|
1071
|
+
// Should have separate sections for FR, NFR, and US
|
|
1072
|
+
expect(serializeResult.content).toContain('## Functional Requirements');
|
|
1073
|
+
expect(serializeResult.content).toContain('## Non-Functional Requirements');
|
|
1074
|
+
expect(serializeResult.content).toContain('## User Stories');
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
it('should include repository information when available', async () => {
|
|
1079
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
1080
|
+
const context: ParseContext = {
|
|
1081
|
+
repositoryPath: 'https://github.com/user/repo',
|
|
1082
|
+
branch: 'feature/auth',
|
|
1083
|
+
commit: 'abc123def',
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
const parseResult = await adapter.parse(content, context);
|
|
1087
|
+
expect(parseResult.success).toBe(true);
|
|
1088
|
+
if (!parseResult.success) return;
|
|
1089
|
+
|
|
1090
|
+
const serializeResult = await adapter.serialize(parseResult.data!);
|
|
1091
|
+
|
|
1092
|
+
expect(serializeResult.success).toBe(true);
|
|
1093
|
+
if (serializeResult.success) {
|
|
1094
|
+
expect(serializeResult.content).toContain('Repository Information');
|
|
1095
|
+
expect(serializeResult.content).toContain('https://github.com/user/repo');
|
|
1096
|
+
expect(serializeResult.content).toContain('feature/auth');
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
describe('integration tests', () => {
|
|
1102
|
+
it('should complete full workflow: detect → parse → validate → serialize', async () => {
|
|
1103
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
1104
|
+
|
|
1105
|
+
// 1. Detect
|
|
1106
|
+
const detectResult = adapter.detect(content);
|
|
1107
|
+
expect(detectResult.detected).toBe(true);
|
|
1108
|
+
|
|
1109
|
+
// 2. Parse
|
|
1110
|
+
const parseResult = await adapter.parse(content);
|
|
1111
|
+
expect(parseResult.success).toBe(true);
|
|
1112
|
+
if (!parseResult.success) return;
|
|
1113
|
+
|
|
1114
|
+
// 3. Validate
|
|
1115
|
+
const validateResult = await adapter.validate(content);
|
|
1116
|
+
expect(validateResult.valid).toBe(true);
|
|
1117
|
+
|
|
1118
|
+
// 4. Serialize
|
|
1119
|
+
const serializeResult = await adapter.serialize(parseResult.data!);
|
|
1120
|
+
expect(serializeResult.success).toBe(true);
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
it('should handle format auto-detection workflow', async () => {
|
|
1124
|
+
const files = [
|
|
1125
|
+
'valid-prd.md',
|
|
1126
|
+
'valid-architecture.md',
|
|
1127
|
+
'valid-epic.md',
|
|
1128
|
+
'valid-story.md',
|
|
1129
|
+
'valid-task.md',
|
|
1130
|
+
];
|
|
1131
|
+
|
|
1132
|
+
for (const file of files) {
|
|
1133
|
+
const content = await readFile(join(fixturesDir, file), 'utf-8');
|
|
1134
|
+
|
|
1135
|
+
// Auto-detect should work for all valid BMAD documents
|
|
1136
|
+
const detectResult = adapter.detect(content);
|
|
1137
|
+
expect(detectResult.detected).toBe(true);
|
|
1138
|
+
expect(detectResult.confidence).toBeGreaterThanOrEqual(50);
|
|
1139
|
+
|
|
1140
|
+
// Parse should succeed
|
|
1141
|
+
const parseResult = await adapter.parse(content);
|
|
1142
|
+
expect(parseResult.success).toBe(true);
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
it('should recover from parse errors gracefully', async () => {
|
|
1147
|
+
const invalidContent = 'This will cause parsing issues but should not throw';
|
|
1148
|
+
|
|
1149
|
+
// Should return error result, not throw
|
|
1150
|
+
const parseResult = await adapter.parse(invalidContent);
|
|
1151
|
+
expect(parseResult).toBeDefined();
|
|
1152
|
+
expect(parseResult.success).toBeDefined();
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
it('should handle batch processing of multiple documents', async () => {
|
|
1156
|
+
const files = ['valid-prd.md', 'valid-architecture.md', 'valid-story.md'];
|
|
1157
|
+
const results = [];
|
|
1158
|
+
|
|
1159
|
+
for (const file of files) {
|
|
1160
|
+
const content = await readFile(join(fixturesDir, file), 'utf-8');
|
|
1161
|
+
const parseResult = await adapter.parse(content);
|
|
1162
|
+
results.push(parseResult);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// All should succeed
|
|
1166
|
+
expect(results.every((r) => r.success)).toBe(true);
|
|
1167
|
+
expect(results.length).toBe(3);
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
it('should preserve data integrity through full round-trip', async () => {
|
|
1171
|
+
const content = await readFile(join(fixturesDir, 'valid-prd.md'), 'utf-8');
|
|
1172
|
+
|
|
1173
|
+
// Parse → Serialize → Parse again
|
|
1174
|
+
const parse1 = await adapter.parse(content);
|
|
1175
|
+
expect(parse1.success).toBe(true);
|
|
1176
|
+
if (!parse1.success) return;
|
|
1177
|
+
|
|
1178
|
+
const serialize = await adapter.serialize(parse1.data!);
|
|
1179
|
+
expect(serialize.success).toBe(true);
|
|
1180
|
+
if (!serialize.success) return;
|
|
1181
|
+
|
|
1182
|
+
const parse2 = await adapter.parse(serialize.content);
|
|
1183
|
+
expect(parse2.success).toBe(true);
|
|
1184
|
+
if (!parse2.success) return;
|
|
1185
|
+
|
|
1186
|
+
// Key data should be preserved
|
|
1187
|
+
expect(parse2.data?.provenance.author).toBe(parse1.data?.provenance.author);
|
|
1188
|
+
expect(parse2.data?.proposed_changes.length).toBeGreaterThan(0);
|
|
1189
|
+
});
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
describe('additional round-trip tests', () => {
|
|
1193
|
+
it('should maintain fidelity for complex PRD', async () => {
|
|
1194
|
+
const content = await readFile(join(fixturesDir, 'valid-complex-prd.md'), 'utf-8');
|
|
1195
|
+
|
|
1196
|
+
const parse1 = await adapter.parse(content);
|
|
1197
|
+
expect(parse1.success).toBe(true);
|
|
1198
|
+
if (!parse1.success) return;
|
|
1199
|
+
|
|
1200
|
+
const serialize = await adapter.serialize(parse1.data!);
|
|
1201
|
+
expect(serialize.success).toBe(true);
|
|
1202
|
+
if (!serialize.success) return;
|
|
1203
|
+
|
|
1204
|
+
const parse2 = await adapter.parse(serialize.content);
|
|
1205
|
+
expect(parse2.success).toBe(true);
|
|
1206
|
+
if (!parse2.success) return;
|
|
1207
|
+
|
|
1208
|
+
// Should have similar number of changes (may vary slightly due to categorization)
|
|
1209
|
+
const change1Count = parse1.data?.proposed_changes.length || 0;
|
|
1210
|
+
const change2Count = parse2.data?.proposed_changes.length || 0;
|
|
1211
|
+
expect(Math.abs(change1Count - change2Count)).toBeLessThan(10);
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
it('should maintain fidelity for minimal PRD', async () => {
|
|
1215
|
+
const content = await readFile(join(fixturesDir, 'valid-minimal-prd.md'), 'utf-8');
|
|
1216
|
+
|
|
1217
|
+
const parse1 = await adapter.parse(content);
|
|
1218
|
+
expect(parse1.success).toBe(true);
|
|
1219
|
+
if (!parse1.success) return;
|
|
1220
|
+
|
|
1221
|
+
const serialize = await adapter.serialize(parse1.data!);
|
|
1222
|
+
expect(serialize.success).toBe(true);
|
|
1223
|
+
if (!serialize.success) return;
|
|
1224
|
+
|
|
1225
|
+
const parse2 = await adapter.parse(serialize.content);
|
|
1226
|
+
expect(parse2.success).toBe(true);
|
|
1227
|
+
if (!parse2.success) return;
|
|
1228
|
+
|
|
1229
|
+
// Change count may vary due to validation requirements being added during serialization
|
|
1230
|
+
expect(parse2.data?.proposed_changes.length).toBeGreaterThanOrEqual(
|
|
1231
|
+
parse1.data?.proposed_changes.length || 0
|
|
1232
|
+
);
|
|
1233
|
+
expect(parse2.data?.provenance.author).toBe(parse1.data?.provenance.author);
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
it('should handle round-trip with task document', async () => {
|
|
1237
|
+
const content = await readFile(join(fixturesDir, 'valid-task.md'), 'utf-8');
|
|
1238
|
+
|
|
1239
|
+
const parse1 = await adapter.parse(content);
|
|
1240
|
+
const serialize = await adapter.serialize(parse1.data!);
|
|
1241
|
+
const parse2 = await adapter.parse(serialize.content);
|
|
1242
|
+
|
|
1243
|
+
expect(parse2.success).toBe(true);
|
|
1244
|
+
if (parse2.success && parse1.success) {
|
|
1245
|
+
// Author and version should be preserved
|
|
1246
|
+
expect(parse2.data?.provenance.author).toBe(parse1.data?.provenance.author);
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
describe('ADAPTUP: BMAD v6 folder structure detection', () => {
|
|
1252
|
+
it('should detect v6 _bmad folder path', () => {
|
|
1253
|
+
const hint: PathDetectionHint = {
|
|
1254
|
+
filePath: '/project/_bmad/docs/prd.md',
|
|
1255
|
+
parentDirs: ['docs', '_bmad', 'project'],
|
|
1256
|
+
};
|
|
1257
|
+
const result = analyzePath(hint);
|
|
1258
|
+
expect(result.isBmadFolder).toBe(true);
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
it('should detect legacy .bmad folder path', () => {
|
|
1262
|
+
const hint: PathDetectionHint = {
|
|
1263
|
+
filePath: '/project/.bmad/docs/prd.md',
|
|
1264
|
+
parentDirs: ['docs', '.bmad', 'project'],
|
|
1265
|
+
};
|
|
1266
|
+
const result = analyzePath(hint);
|
|
1267
|
+
expect(result.isBmadFolder).toBe(true);
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
it('should detect v6 _config folder path', () => {
|
|
1271
|
+
const hint: PathDetectionHint = {
|
|
1272
|
+
filePath: '/project/_bmad/_config/module.yaml',
|
|
1273
|
+
parentDirs: ['_config', '_bmad', 'project'],
|
|
1274
|
+
};
|
|
1275
|
+
const result = analyzePath(hint);
|
|
1276
|
+
expect(result.isBmadFolder).toBe(true);
|
|
1277
|
+
expect(result.isConfigFolder).toBe(true);
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
it('should detect legacy _cfg folder path', () => {
|
|
1281
|
+
const hint: PathDetectionHint = {
|
|
1282
|
+
filePath: '/project/.bmad/_cfg/settings.yaml',
|
|
1283
|
+
parentDirs: ['_cfg', '.bmad', 'project'],
|
|
1284
|
+
};
|
|
1285
|
+
const result = analyzePath(hint);
|
|
1286
|
+
expect(result.isBmadFolder).toBe(true);
|
|
1287
|
+
expect(result.isConfigFolder).toBe(true);
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
it('should not detect non-BMAD folder paths', () => {
|
|
1291
|
+
const hint: PathDetectionHint = {
|
|
1292
|
+
filePath: '/project/docs/prd.md',
|
|
1293
|
+
parentDirs: ['docs', 'project'],
|
|
1294
|
+
};
|
|
1295
|
+
const result = analyzePath(hint);
|
|
1296
|
+
expect(result.isBmadFolder).toBe(false);
|
|
1297
|
+
expect(result.isConfigFolder).toBe(false);
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
it('should boost confidence when file is in _bmad folder', async () => {
|
|
1301
|
+
const content = `---
|
|
1302
|
+
name: Minimal
|
|
1303
|
+
---
|
|
1304
|
+
|
|
1305
|
+
Some document content that is not very BMAD-like.`;
|
|
1306
|
+
|
|
1307
|
+
const hint: PathDetectionHint = {
|
|
1308
|
+
filePath: '/project/_bmad/docs/doc.md',
|
|
1309
|
+
parentDirs: ['docs', '_bmad', 'project'],
|
|
1310
|
+
};
|
|
1311
|
+
|
|
1312
|
+
const resultWithPath = adapter.detectWithPath(content, hint);
|
|
1313
|
+
const resultWithout = adapter.detect(content);
|
|
1314
|
+
|
|
1315
|
+
expect(resultWithPath.confidence).toBeGreaterThan(resultWithout.confidence);
|
|
1316
|
+
expect(resultWithPath.reason).toContain('bmad-folder');
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
it('should have correct folder constants', () => {
|
|
1320
|
+
expect(BMAD_FOLDERS.PROJECT).toBe('_bmad');
|
|
1321
|
+
expect(BMAD_FOLDERS.PROJECT_LEGACY).toBe('.bmad');
|
|
1322
|
+
expect(BMAD_FOLDERS.CONFIG).toBe('_config');
|
|
1323
|
+
expect(BMAD_FOLDERS.CONFIG_LEGACY).toBe('_cfg');
|
|
1324
|
+
expect(BMAD_FOLDERS.MEMORY).toBe('_memory');
|
|
1325
|
+
expect(BMAD_FOLDERS.MODULE_CONFIG).toBe('module.yaml');
|
|
1326
|
+
});
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
describe('ADAPTUP: BMAD v6 config path handling', () => {
|
|
1330
|
+
it('should detect config folder in path indicators', async () => {
|
|
1331
|
+
const content = await readFile(join(fixturesDir, 'valid-v6-prd.md'), 'utf-8');
|
|
1332
|
+
const hint: PathDetectionHint = {
|
|
1333
|
+
filePath: '/project/_bmad/_config/prd.md',
|
|
1334
|
+
parentDirs: ['_config', '_bmad', 'project'],
|
|
1335
|
+
};
|
|
1336
|
+
|
|
1337
|
+
const result = adapter.detectWithPath(content, hint);
|
|
1338
|
+
expect(result.reason).toContain('bmad-config');
|
|
1339
|
+
});
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
describe('ADAPTUP: BMAD v6 variable syntax', () => {
|
|
1343
|
+
it('should expand underscore variable syntax', () => {
|
|
1344
|
+
const content = 'Path: {project_root}/docs';
|
|
1345
|
+
const result = expandVariables(content, { project_root: '/home/user' });
|
|
1346
|
+
expect(result).toBe('Path: /home/user/docs');
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
it('should expand hyphenated variable syntax', () => {
|
|
1350
|
+
const content = 'Path: {project-root}/docs';
|
|
1351
|
+
const result = expandVariables(content, { 'project-root': '/home/user' });
|
|
1352
|
+
expect(result).toBe('Path: /home/user/docs');
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
it('should expand both syntaxes from underscore key', () => {
|
|
1356
|
+
const content = '{project_root} and {project-root}';
|
|
1357
|
+
const result = expandVariables(content, { project_root: '/home' });
|
|
1358
|
+
expect(result).toBe('/home and /home');
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
it('should expand both syntaxes from hyphenated key', () => {
|
|
1362
|
+
const content = '{project_root} and {project-root}';
|
|
1363
|
+
const result = expandVariables(content, { 'project-root': '/home' });
|
|
1364
|
+
expect(result).toBe('/home and /home');
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
it('should detect hyphenated variables in content', () => {
|
|
1368
|
+
expect(hasHyphenatedVariables('{project-root}/docs')).toBe(true);
|
|
1369
|
+
expect(hasHyphenatedVariables('{output-file}')).toBe(true);
|
|
1370
|
+
expect(hasHyphenatedVariables('{project_root}/docs')).toBe(false);
|
|
1371
|
+
expect(hasHyphenatedVariables('no variables here')).toBe(false);
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
it('should detect v6 PRD with hyphenated variables', async () => {
|
|
1375
|
+
const content = await readFile(join(fixturesDir, 'valid-v6-prd.md'), 'utf-8');
|
|
1376
|
+
const result = adapter.detect(content);
|
|
1377
|
+
|
|
1378
|
+
expect(result.detected).toBe(true);
|
|
1379
|
+
expect(result.confidence).toBeGreaterThanOrEqual(50);
|
|
1380
|
+
});
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
describe('ADAPTUP: BMAD hasSidecar field support', () => {
|
|
1384
|
+
it('should parse hasSidecar: true from front-matter', async () => {
|
|
1385
|
+
const content = await readFile(join(fixturesDir, 'valid-agent.md'), 'utf-8');
|
|
1386
|
+
|
|
1387
|
+
// Agent docs may not reach 50% with content-only detection (YAML 30 + hasSidecar 15 = 45)
|
|
1388
|
+
// Use detectWithPath from a _bmad folder to boost (+20 = 65)
|
|
1389
|
+
const hint: PathDetectionHint = {
|
|
1390
|
+
filePath: '/project/_bmad/agents/code-review.md',
|
|
1391
|
+
parentDirs: ['agents', '_bmad', 'project'],
|
|
1392
|
+
};
|
|
1393
|
+
const result = adapter.detectWithPath(content, hint);
|
|
1394
|
+
|
|
1395
|
+
expect(result.detected).toBe(true);
|
|
1396
|
+
expect(result.reason).toContain('has-sidecar');
|
|
1397
|
+
expect(result.reason).toContain('bmad-folder');
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
it('should parse boolean YAML values', () => {
|
|
1401
|
+
expect(parseYamlBoolean('true')).toBe(true);
|
|
1402
|
+
expect(parseYamlBoolean('false')).toBe(false);
|
|
1403
|
+
expect(parseYamlBoolean('yes')).toBe(true);
|
|
1404
|
+
expect(parseYamlBoolean('no')).toBe(false);
|
|
1405
|
+
expect(parseYamlBoolean('on')).toBe(true);
|
|
1406
|
+
expect(parseYamlBoolean('off')).toBe(false);
|
|
1407
|
+
expect(parseYamlBoolean('TRUE')).toBe(true);
|
|
1408
|
+
expect(parseYamlBoolean('maybe')).toBeUndefined();
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
it('should warn on agent doc missing hasSidecar', async () => {
|
|
1412
|
+
const content = `---
|
|
1413
|
+
name: 'Test Agent'
|
|
1414
|
+
version: '1.0.0'
|
|
1415
|
+
author: 'Test'
|
|
1416
|
+
---
|
|
1417
|
+
|
|
1418
|
+
# Test Agent
|
|
1419
|
+
|
|
1420
|
+
## Purpose
|
|
1421
|
+
|
|
1422
|
+
A test agent for validation.
|
|
1423
|
+
|
|
1424
|
+
## Role
|
|
1425
|
+
|
|
1426
|
+
Performs test operations for quality assurance.`;
|
|
1427
|
+
|
|
1428
|
+
const result = await adapter.validate(content);
|
|
1429
|
+
expect(result.issues).toBeDefined();
|
|
1430
|
+
if (result.issues) {
|
|
1431
|
+
const sidecarWarning = result.issues.find((i) => i.code === 'MISSING_HAS_SIDECAR');
|
|
1432
|
+
expect(sidecarWarning).toBeDefined();
|
|
1433
|
+
if (sidecarWarning) {
|
|
1434
|
+
expect(sidecarWarning.severity).toBe('warning');
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
it('should not warn when hasSidecar is present', async () => {
|
|
1440
|
+
const content = await readFile(join(fixturesDir, 'valid-agent.md'), 'utf-8');
|
|
1441
|
+
const result = await adapter.validate(content);
|
|
1442
|
+
|
|
1443
|
+
if (result.issues) {
|
|
1444
|
+
const sidecarWarning = result.issues.find((i) => i.code === 'MISSING_HAS_SIDECAR');
|
|
1445
|
+
expect(sidecarWarning).toBeUndefined();
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
it('should parse agent document successfully', async () => {
|
|
1450
|
+
const content = await readFile(join(fixturesDir, 'valid-agent.md'), 'utf-8');
|
|
1451
|
+
const result = await adapter.parse(content);
|
|
1452
|
+
|
|
1453
|
+
expect(result.success).toBe(true);
|
|
1454
|
+
if (result.success) {
|
|
1455
|
+
expect(result.data?.provenance.author).toBe('BMAD Team');
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
it('should parse v6 PRD with all new features', async () => {
|
|
1460
|
+
const content = await readFile(join(fixturesDir, 'valid-v6-prd.md'), 'utf-8');
|
|
1461
|
+
const result = await adapter.parse(content);
|
|
1462
|
+
|
|
1463
|
+
expect(result.success).toBe(true);
|
|
1464
|
+
if (result.success) {
|
|
1465
|
+
expect(result.data?.proposed_changes.length).toBeGreaterThanOrEqual(3);
|
|
1466
|
+
expect(result.data?.provenance.author).toBe('v6 Author');
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1469
|
+
});
|
|
1470
|
+
});
|