@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.
Files changed (183) hide show
  1. package/AGENTS.md +180 -0
  2. package/BMAD_ADAPTER_SPEC.md +489 -0
  3. package/LICENSE +14 -0
  4. package/README.md +500 -0
  5. package/dist/aps-markdown/adapter.d.ts +102 -0
  6. package/dist/aps-markdown/adapter.d.ts.map +1 -0
  7. package/dist/aps-markdown/adapter.js +351 -0
  8. package/dist/aps-markdown/index.d.ts +8 -0
  9. package/dist/aps-markdown/index.d.ts.map +1 -0
  10. package/dist/aps-markdown/index.js +7 -0
  11. package/dist/base/file-discovery.d.ts +63 -0
  12. package/dist/base/file-discovery.d.ts.map +1 -0
  13. package/dist/base/file-discovery.js +246 -0
  14. package/dist/base/index.d.ts +10 -0
  15. package/dist/base/index.d.ts.map +1 -0
  16. package/dist/base/index.js +9 -0
  17. package/dist/base/registry.d.ts +155 -0
  18. package/dist/base/registry.d.ts.map +1 -0
  19. package/dist/base/registry.js +227 -0
  20. package/dist/base/testing.d.ts +102 -0
  21. package/dist/base/testing.d.ts.map +1 -0
  22. package/dist/base/testing.js +221 -0
  23. package/dist/base/types.d.ts +255 -0
  24. package/dist/base/types.d.ts.map +1 -0
  25. package/dist/base/types.js +78 -0
  26. package/dist/base/utils.d.ts +127 -0
  27. package/dist/base/utils.d.ts.map +1 -0
  28. package/dist/base/utils.js +254 -0
  29. package/dist/bmad/format-adapter.d.ts +76 -0
  30. package/dist/bmad/format-adapter.d.ts.map +1 -0
  31. package/dist/bmad/format-adapter.js +186 -0
  32. package/dist/bmad/index.d.ts +12 -0
  33. package/dist/bmad/index.d.ts.map +1 -0
  34. package/dist/bmad/index.js +10 -0
  35. package/dist/bmad/parser.d.ts +12 -0
  36. package/dist/bmad/parser.d.ts.map +1 -0
  37. package/dist/bmad/parser.js +181 -0
  38. package/dist/bmad/serializer.d.ts +16 -0
  39. package/dist/bmad/serializer.d.ts.map +1 -0
  40. package/dist/bmad/serializer.js +170 -0
  41. package/dist/bmad/types.d.ts +127 -0
  42. package/dist/bmad/types.d.ts.map +1 -0
  43. package/dist/bmad/types.js +47 -0
  44. package/dist/bmad/utils.d.ts +120 -0
  45. package/dist/bmad/utils.d.ts.map +1 -0
  46. package/dist/bmad/utils.js +480 -0
  47. package/dist/common/index.d.ts +3 -0
  48. package/dist/common/index.d.ts.map +1 -0
  49. package/dist/common/index.js +2 -0
  50. package/dist/common/registry.d.ts +18 -0
  51. package/dist/common/registry.d.ts.map +1 -0
  52. package/dist/common/registry.js +58 -0
  53. package/dist/common/types.d.ts +68 -0
  54. package/dist/common/types.d.ts.map +1 -0
  55. package/dist/common/types.js +12 -0
  56. package/dist/generic/format-adapter.d.ts +64 -0
  57. package/dist/generic/format-adapter.d.ts.map +1 -0
  58. package/dist/generic/format-adapter.js +159 -0
  59. package/dist/generic/index.d.ts +10 -0
  60. package/dist/generic/index.d.ts.map +1 -0
  61. package/dist/generic/index.js +9 -0
  62. package/dist/generic/parser.d.ts +11 -0
  63. package/dist/generic/parser.d.ts.map +1 -0
  64. package/dist/generic/parser.js +106 -0
  65. package/dist/generic/serializer.d.ts +11 -0
  66. package/dist/generic/serializer.d.ts.map +1 -0
  67. package/dist/generic/serializer.js +118 -0
  68. package/dist/generic/types.d.ts +52 -0
  69. package/dist/generic/types.d.ts.map +1 -0
  70. package/dist/generic/types.js +6 -0
  71. package/dist/generic/utils.d.ts +51 -0
  72. package/dist/generic/utils.d.ts.map +1 -0
  73. package/dist/generic/utils.js +232 -0
  74. package/dist/index.d.ts +15 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +31 -0
  77. package/dist/speckit/export.d.ts +22 -0
  78. package/dist/speckit/export.d.ts.map +1 -0
  79. package/dist/speckit/export.js +384 -0
  80. package/dist/speckit/format-adapter.d.ts +104 -0
  81. package/dist/speckit/format-adapter.d.ts.map +1 -0
  82. package/dist/speckit/format-adapter.js +488 -0
  83. package/dist/speckit/import-v2.d.ts +33 -0
  84. package/dist/speckit/import-v2.d.ts.map +1 -0
  85. package/dist/speckit/import-v2.js +361 -0
  86. package/dist/speckit/import.d.ts +16 -0
  87. package/dist/speckit/import.d.ts.map +1 -0
  88. package/dist/speckit/import.js +247 -0
  89. package/dist/speckit/index.d.ts +5 -0
  90. package/dist/speckit/index.d.ts.map +1 -0
  91. package/dist/speckit/index.js +4 -0
  92. package/dist/speckit/parser.d.ts +28 -0
  93. package/dist/speckit/parser.d.ts.map +1 -0
  94. package/dist/speckit/parser.js +283 -0
  95. package/dist/speckit/parsers/plan-parser.d.ts +71 -0
  96. package/dist/speckit/parsers/plan-parser.d.ts.map +1 -0
  97. package/dist/speckit/parsers/plan-parser.js +216 -0
  98. package/dist/speckit/parsers/spec-parser.d.ts +67 -0
  99. package/dist/speckit/parsers/spec-parser.d.ts.map +1 -0
  100. package/dist/speckit/parsers/spec-parser.js +255 -0
  101. package/dist/speckit/parsers/tasks-parser.d.ts +57 -0
  102. package/dist/speckit/parsers/tasks-parser.d.ts.map +1 -0
  103. package/dist/speckit/parsers/tasks-parser.js +157 -0
  104. package/package.json +23 -0
  105. package/project.json +29 -0
  106. package/src/__tests__/adapter-edge-cases.test.ts +937 -0
  107. package/src/__tests__/bmad-format-adapter.test.ts +1470 -0
  108. package/src/__tests__/fixtures/aps/expected-output.json +83 -0
  109. package/src/__tests__/fixtures/bmad/invalid-malformed-yaml.md +16 -0
  110. package/src/__tests__/fixtures/bmad/invalid-no-requirements.md +23 -0
  111. package/src/__tests__/fixtures/bmad/invalid-only-yaml.md +16 -0
  112. package/src/__tests__/fixtures/bmad/invalid-too-short.md +3 -0
  113. package/src/__tests__/fixtures/bmad/invalid-wrong-format.md +40 -0
  114. package/src/__tests__/fixtures/bmad/valid-agent.md +27 -0
  115. package/src/__tests__/fixtures/bmad/valid-architecture.md +116 -0
  116. package/src/__tests__/fixtures/bmad/valid-complex-prd.md +161 -0
  117. package/src/__tests__/fixtures/bmad/valid-epic.md +73 -0
  118. package/src/__tests__/fixtures/bmad/valid-minimal-prd.md +19 -0
  119. package/src/__tests__/fixtures/bmad/valid-prd.md +107 -0
  120. package/src/__tests__/fixtures/bmad/valid-story.md +107 -0
  121. package/src/__tests__/fixtures/bmad/valid-task.md +79 -0
  122. package/src/__tests__/fixtures/bmad/valid-v6-prd.md +35 -0
  123. package/src/__tests__/fixtures/generic/plan-detailed.md +39 -0
  124. package/src/__tests__/fixtures/generic/prd-simple.md +27 -0
  125. package/src/__tests__/fixtures/generic/rfc-example.md +26 -0
  126. package/src/__tests__/fixtures/generic/todo-list.md +23 -0
  127. package/src/__tests__/fixtures/speckit/sample-plan.md +63 -0
  128. package/src/__tests__/fixtures/speckit/sample-spec-namespaced.md +50 -0
  129. package/src/__tests__/fixtures/speckit/sample-spec.md +105 -0
  130. package/src/__tests__/fixtures/speckit/sample-tasks.md +87 -0
  131. package/src/__tests__/fixtures/speckit-official/auth-feature/plan.md +272 -0
  132. package/src/__tests__/fixtures/speckit-official/auth-feature/spec.md +149 -0
  133. package/src/__tests__/fixtures/speckit-official/auth-feature/tasks.md +169 -0
  134. package/src/__tests__/generic-format-adapter.test.ts +398 -0
  135. package/src/__tests__/speckit-export.test.ts +233 -0
  136. package/src/__tests__/speckit-format-adapter.test.ts +832 -0
  137. package/src/__tests__/speckit-import-v2.test.ts +253 -0
  138. package/src/__tests__/speckit-import.test.ts +209 -0
  139. package/src/__tests__/speckit-parser.test.ts +219 -0
  140. package/src/__tests__/speckit-spec-parser.test.ts +120 -0
  141. package/src/aps-markdown/__tests__/__fixtures__/simple-leaf.aps.md +17 -0
  142. package/src/aps-markdown/__tests__/adapter.test.ts +393 -0
  143. package/src/aps-markdown/adapter.ts +455 -0
  144. package/src/aps-markdown/index.ts +8 -0
  145. package/src/base/__tests__/registry.test.ts +515 -0
  146. package/src/base/file-discovery.ts +305 -0
  147. package/src/base/index.ts +10 -0
  148. package/src/base/registry.ts +263 -0
  149. package/src/base/testing.ts +334 -0
  150. package/src/base/types.ts +342 -0
  151. package/src/base/utils.ts +306 -0
  152. package/src/bmad/format-adapter.ts +227 -0
  153. package/src/bmad/index.ts +21 -0
  154. package/src/bmad/parser.ts +224 -0
  155. package/src/bmad/serializer.ts +206 -0
  156. package/src/bmad/types.ts +135 -0
  157. package/src/bmad/utils.ts +575 -0
  158. package/src/common/index.ts +2 -0
  159. package/src/common/registry.ts +72 -0
  160. package/src/common/types.ts +84 -0
  161. package/src/generic/__tests__/serializer.test.ts +167 -0
  162. package/src/generic/format-adapter.ts +200 -0
  163. package/src/generic/index.ts +11 -0
  164. package/src/generic/parser.ts +129 -0
  165. package/src/generic/serializer.ts +134 -0
  166. package/src/generic/types.ts +53 -0
  167. package/src/generic/utils.ts +270 -0
  168. package/src/index.ts +48 -0
  169. package/src/speckit/export.ts +489 -0
  170. package/src/speckit/format-adapter.ts +595 -0
  171. package/src/speckit/import-v2.ts +445 -0
  172. package/src/speckit/import.ts +305 -0
  173. package/src/speckit/index.ts +4 -0
  174. package/src/speckit/parser.ts +351 -0
  175. package/src/speckit/parsers/plan-parser.ts +342 -0
  176. package/src/speckit/parsers/spec-parser.ts +379 -0
  177. package/src/speckit/parsers/tasks-parser.ts +246 -0
  178. package/tsconfig.json +26 -0
  179. package/tsconfig.lib.json +21 -0
  180. package/tsconfig.lib.tsbuildinfo +1 -0
  181. package/tsconfig.spec.json +9 -0
  182. package/tsconfig.tsbuildinfo +1 -0
  183. package/vitest.config.ts +14 -0
@@ -0,0 +1,937 @@
1
+ /**
2
+ * Comprehensive Edge Case Tests for Adapter Framework
3
+ *
4
+ * Tests edge cases that are critical for production robustness but may not be
5
+ * covered by standard functional tests. Organized by category.
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach, afterAll } from 'vitest';
9
+ import { SpecKitFormatAdapter } from '../speckit/format-adapter.js';
10
+ import { BMADFormatAdapter } from '../bmad/format-adapter.js';
11
+ import { GenericMarkdownAdapter } from '../generic/format-adapter.js';
12
+ import { AdapterRegistry } from '../base/registry.js';
13
+ import type { ParseContext } from '../base/types.js';
14
+
15
+ describe('Adapter Edge Cases', () => {
16
+ let speckitAdapter: SpecKitFormatAdapter;
17
+ let bmadAdapter: BMADFormatAdapter;
18
+ let genericAdapter: GenericMarkdownAdapter;
19
+
20
+ beforeEach(() => {
21
+ speckitAdapter = new SpecKitFormatAdapter();
22
+ bmadAdapter = new BMADFormatAdapter();
23
+ genericAdapter = new GenericMarkdownAdapter();
24
+ });
25
+
26
+ describe('Binary and Non-Text Content', () => {
27
+ it('should detect binary content and handle gracefully - SpecKit', async () => {
28
+ // Simulate binary content with null bytes and non-UTF8 sequences
29
+ const binaryContent = '\x00\xFF\xFE\x89PNG\r\n\x1a\n\x00\x00\x00';
30
+
31
+ const result = await speckitAdapter.parse(binaryContent);
32
+
33
+ // Adapters may succeed but produce empty/invalid results for binary content
34
+ // The test validates that they don't crash
35
+ expect(result).toBeDefined();
36
+ if (result.success && result.data) {
37
+ // Binary content should result in minimal/empty data
38
+ expect(result.data.proposed_changes.length).toBe(0);
39
+ }
40
+ });
41
+
42
+ it('should detect binary content and handle gracefully - BMAD', async () => {
43
+ const binaryContent = '\x00\xFF\xFE\x89PNG\r\n\x1a\n\x00\x00\x00';
44
+
45
+ const result = await bmadAdapter.parse(binaryContent);
46
+
47
+ // Adapters may succeed but produce empty/invalid results for binary content
48
+ // The test validates that they don't crash
49
+ expect(result).toBeDefined();
50
+ if (result.success && result.data) {
51
+ // Binary content should result in minimal/empty data
52
+ expect(result.data.proposed_changes.length).toBe(0);
53
+ }
54
+ });
55
+
56
+ it('should handle mixed binary/text content - SpecKit', async () => {
57
+ // Content with embedded null bytes
58
+ const mixedContent = '# Specification\n\n## Intent\n\x00\x00\nSome text\n\xFF\xFE';
59
+
60
+ const result = await speckitAdapter.parse(mixedContent);
61
+
62
+ // Should not crash, validates robustness
63
+ expect(result).toBeDefined();
64
+ // Adapters may succeed or fail, both are acceptable for binary content
65
+ if (result.success) {
66
+ expect(result.data).toBeDefined();
67
+ } else {
68
+ expect(result.errors).toBeDefined();
69
+ }
70
+ });
71
+
72
+ it('should detect binary content in detection phase', () => {
73
+ const binaryContent = '\x00\xFF\xFE\x89PNG\r\n\x1a\n';
74
+
75
+ const result = speckitAdapter.detect(binaryContent);
76
+
77
+ expect(result.detected).toBe(false);
78
+ expect(result.confidence).toBe(0);
79
+ });
80
+ });
81
+
82
+ describe('Unicode and Encoding Edge Cases', () => {
83
+ it('should handle RTL (right-to-left) text', async () => {
84
+ const rtlContent = `# Specification
85
+
86
+ ## Intent
87
+
88
+ تطوير ميزة جديدة (Arabic RTL text)
89
+ יצירת קובץ חדש (Hebrew RTL text)
90
+
91
+ ## Changes
92
+
93
+ - file_create: src/feature.ts - Create feature with RTL support`;
94
+
95
+ const result = await speckitAdapter.parse(rtlContent);
96
+
97
+ expect(result.success).toBe(true);
98
+ if (result.success && result.data) {
99
+ // Intent section should contain both Arabic and Hebrew text
100
+ expect(result.data.intent).toContain('تطوير');
101
+ expect(result.data.intent).toContain('יצירת');
102
+ }
103
+ });
104
+
105
+ it('should handle zero-width characters in content', async () => {
106
+ // Zero-width space (U+200B), zero-width joiner (U+200D)
107
+ const content = `---
108
+ name: Zero\u200BWidth\u200DTest
109
+ ---
110
+
111
+ FR-01: Test\u200Bwith\u200Dzero\u200Bwidth\u200Dcharacters`;
112
+
113
+ const result = await bmadAdapter.parse(content);
114
+
115
+ expect(result.success).toBe(true);
116
+ if (result.success && result.data) {
117
+ // Zero-width characters may be preserved or stripped
118
+ // Test validates that parsing doesn't crash
119
+ expect(result.data.proposed_changes.length).toBeGreaterThan(0);
120
+ }
121
+ });
122
+
123
+ it('should handle combining characters in descriptions', async () => {
124
+ // Combining diacritics: "e" + combining acute accent
125
+ const content = `# Specification
126
+
127
+ ## Intent
128
+
129
+ Test café (with combining character: cafe\u0301)
130
+
131
+ ## Changes
132
+
133
+ - file_create: src/résumé.ts - Description`;
134
+
135
+ const result = await speckitAdapter.parse(content);
136
+
137
+ expect(result.success).toBe(true);
138
+ });
139
+
140
+ it('should handle BOM (Byte Order Mark) at file start', async () => {
141
+ // UTF-8 BOM: EF BB BF
142
+ const bomContent =
143
+ '\uFEFF# Specification\n\n## Intent\n\nTest with BOM\n\n## Changes\n\n- file_create: test.ts';
144
+
145
+ const result = await speckitAdapter.parse(bomContent);
146
+
147
+ expect(result.success).toBe(true);
148
+ if (result.success && result.data) {
149
+ // BOM should be stripped, not included in intent
150
+ expect(result.data.intent).not.toContain('\uFEFF');
151
+ expect(result.data.intent).toContain('Test with BOM');
152
+ }
153
+ });
154
+
155
+ it('should handle emoji and special Unicode symbols', async () => {
156
+ const content = `---
157
+ name: Emoji Test 🚀
158
+ ---
159
+
160
+ FR-01: Add support for emojis 🎉 💻 🔥
161
+ FR-02: Mathematical symbols ∑ ∫ √ ∞
162
+ FR-03: Currency symbols ¥ € £ ₹
163
+ FR-04: Box drawing characters ┌─┐ │ └─┘`;
164
+
165
+ const result = await bmadAdapter.parse(content);
166
+
167
+ expect(result.success).toBe(true);
168
+ if (result.success && result.data) {
169
+ expect(result.data.proposed_changes.length).toBeGreaterThanOrEqual(4);
170
+ }
171
+ });
172
+
173
+ it('should handle normalization forms (NFC vs NFD)', async () => {
174
+ // Same character in different normalization forms
175
+ // "é" as single character (NFC) vs "e" + combining acute (NFD)
176
+ const nfcContent = `# Specification\n\n## Intent\n\nCafé (NFC)\n\n## Changes\n\n- file_create: test.ts`;
177
+ const nfdContent = `# Specification\n\n## Intent\n\nCafe\u0301 (NFD)\n\n## Changes\n\n- file_create: test.ts`;
178
+
179
+ const nfcResult = await speckitAdapter.parse(nfcContent);
180
+ const nfdResult = await speckitAdapter.parse(nfdContent);
181
+
182
+ expect(nfcResult.success).toBe(true);
183
+ expect(nfdResult.success).toBe(true);
184
+ });
185
+ });
186
+
187
+ describe('Empty and Null Content Edge Cases', () => {
188
+ it('should handle completely empty string - SpecKit', async () => {
189
+ const result = await speckitAdapter.parse('');
190
+
191
+ // Adapters may succeed with empty data or fail
192
+ // Test validates consistent behavior without crashing
193
+ expect(result).toBeDefined();
194
+ if (result.success && result.data) {
195
+ // Empty content should result in empty changes
196
+ expect(result.data.proposed_changes.length).toBe(0);
197
+ }
198
+ });
199
+
200
+ it('should handle completely empty string - BMAD', async () => {
201
+ const result = await bmadAdapter.parse('');
202
+
203
+ expect(result).toBeDefined();
204
+ if (result.success && result.data) {
205
+ // Empty content should result in empty changes
206
+ expect(result.data.proposed_changes.length).toBe(0);
207
+ }
208
+ });
209
+
210
+ it('should handle completely empty string - Generic', async () => {
211
+ const result = await genericAdapter.parse('');
212
+
213
+ expect(result).toBeDefined();
214
+ if (result.success && result.data) {
215
+ // Empty content should result in empty changes
216
+ expect(result.data.proposed_changes.length).toBe(0);
217
+ }
218
+ });
219
+
220
+ it('should handle only whitespace content', async () => {
221
+ const whitespaceContent = ' \n\n\t\t\n \n';
222
+
223
+ const result = await speckitAdapter.parse(whitespaceContent);
224
+
225
+ // Test validates no crash on whitespace-only content
226
+ expect(result).toBeDefined();
227
+ if (result.success && result.data) {
228
+ expect(result.data.proposed_changes.length).toBe(0);
229
+ }
230
+ });
231
+
232
+ it('should handle content with only comments', async () => {
233
+ const commentContent = `<!-- This is a comment -->
234
+ <!-- Another comment -->
235
+ <!-- And another -->`;
236
+
237
+ const result = await speckitAdapter.parse(commentContent);
238
+
239
+ // Test validates no crash on comment-only content
240
+ expect(result).toBeDefined();
241
+ if (result.success && result.data) {
242
+ expect(result.data.proposed_changes.length).toBe(0);
243
+ }
244
+ });
245
+
246
+ it('should detect empty content consistently', () => {
247
+ const emptyDetection = speckitAdapter.detect('');
248
+ const whitespaceDetection = speckitAdapter.detect(' \n\t\n ');
249
+
250
+ expect(emptyDetection.detected).toBe(false);
251
+ expect(whitespaceDetection.detected).toBe(false);
252
+ });
253
+ });
254
+
255
+ describe('Corrupted and Malformed Content', () => {
256
+ it('should handle corrupted YAML frontmatter - BMAD', async () => {
257
+ const corruptedContent = `---
258
+ name: Test
259
+ version: 1.0.0
260
+ author: Test User
261
+ this is not valid yaml: [unclosed bracket
262
+ more invalid: {unclosed brace
263
+ ---
264
+
265
+ FR-01: Valid requirement`;
266
+
267
+ const result = await bmadAdapter.parse(corruptedContent);
268
+
269
+ // Should either recover or fail gracefully with clear error
270
+ if (!result.success) {
271
+ expect(result.errors).toBeDefined();
272
+ expect(result.errors?.[0]?.message).toMatch(/yaml|frontmatter|metadata/i);
273
+ }
274
+ });
275
+
276
+ it('should handle missing closing YAML delimiter', async () => {
277
+ const content = `---
278
+ name: Unclosed YAML
279
+ version: 1.0.0
280
+
281
+ FR-01: This is after unclosed YAML`;
282
+
283
+ const result = await bmadAdapter.parse(content);
284
+
285
+ // Should handle gracefully without crashing
286
+ expect(result).toBeDefined();
287
+ // May succeed or fail depending on YAML parser leniency
288
+ if (result.success) {
289
+ expect(result.data).toBeDefined();
290
+ } else {
291
+ expect(result.errors).toBeDefined();
292
+ }
293
+ });
294
+
295
+ it('should handle deeply nested markdown structures', async () => {
296
+ let content = '# Specification\n\n## Intent\n\nNested test\n\n## Changes\n\n';
297
+
298
+ // Create deeply nested list structure
299
+ for (let i = 0; i < 50; i++) {
300
+ content += ' '.repeat(i) + `- Level ${i} item\n`;
301
+ }
302
+
303
+ const result = await speckitAdapter.parse(content);
304
+
305
+ // Should not crash, either succeed or fail gracefully
306
+ expect(result).toBeDefined();
307
+ });
308
+
309
+ it('should handle extremely long lines', async () => {
310
+ const veryLongLine = 'A'.repeat(100000);
311
+ const content = `# Specification\n\n## Intent\n\n${veryLongLine}\n\n## Changes\n\n- file_create: test.ts`;
312
+
313
+ const result = await speckitAdapter.parse(content);
314
+
315
+ expect(result).toBeDefined();
316
+ if (result.success && result.data) {
317
+ expect(result.data.intent.length).toBeGreaterThan(50000);
318
+ }
319
+ });
320
+ });
321
+
322
+ describe('Concurrent Operations', () => {
323
+ it('should handle concurrent parse operations on same adapter', async () => {
324
+ const content1 = `# Specification\n\n## Intent\n\nTest 1\n\n## Changes\n\n- file_create: test1.ts`;
325
+ const content2 = `# Specification\n\n## Intent\n\nTest 2\n\n## Changes\n\n- file_create: test2.ts`;
326
+ const content3 = `# Specification\n\n## Intent\n\nTest 3\n\n## Changes\n\n- file_create: test3.ts`;
327
+
328
+ // Parse all three concurrently
329
+ const results = await Promise.all([
330
+ speckitAdapter.parse(content1),
331
+ speckitAdapter.parse(content2),
332
+ speckitAdapter.parse(content3),
333
+ ]);
334
+
335
+ // All should succeed independently
336
+ expect(results[0]?.success).toBe(true);
337
+ expect(results[1]?.success).toBe(true);
338
+ expect(results[2]?.success).toBe(true);
339
+
340
+ // Should have correct data for each
341
+ if (results[0]?.success && results[0].data) {
342
+ expect(results[0].data.intent).toContain('Test 1');
343
+ }
344
+ if (results[1]?.success && results[1].data) {
345
+ expect(results[1].data.intent).toContain('Test 2');
346
+ }
347
+ if (results[2]?.success && results[2].data) {
348
+ expect(results[2].data.intent).toContain('Test 3');
349
+ }
350
+ });
351
+
352
+ it('should handle concurrent detection operations', () => {
353
+ const contents = Array.from(
354
+ { length: 10 },
355
+ (_v, i) =>
356
+ `# Specification\n\n## Intent\n\nTest ${i}\n\n## Changes\n\n- file_create: test${i}.ts`
357
+ );
358
+
359
+ // Detection is synchronous, but we test multiple concurrent calls
360
+ const detections = contents.map((content) => speckitAdapter.detect(content));
361
+
362
+ // All detections should succeed
363
+ detections.forEach((detection) => {
364
+ expect(detection.detected).toBe(true);
365
+ expect(detection.confidence).toBeGreaterThan(50);
366
+ });
367
+ });
368
+ });
369
+
370
+ describe('Registry Edge Cases', () => {
371
+ let registry: AdapterRegistry;
372
+
373
+ beforeEach(() => {
374
+ AdapterRegistry.resetInstance();
375
+ registry = AdapterRegistry.getInstance();
376
+ });
377
+
378
+ afterAll(() => {
379
+ // Restore registry to clean state after all tests in this block
380
+ AdapterRegistry.resetInstance();
381
+ });
382
+
383
+ it('should detect format conflicts when multiple adapters support same format', () => {
384
+ const speckit = new SpecKitFormatAdapter();
385
+ const generic = new GenericMarkdownAdapter();
386
+
387
+ registry.register(speckit);
388
+ registry.register(generic);
389
+
390
+ // Both support .md extension
391
+ const mdAdapters = registry.getImportAdapters('.md');
392
+
393
+ // Should return multiple adapters
394
+ expect(mdAdapters.length).toBeGreaterThan(1);
395
+ });
396
+
397
+ it('should handle adapter detection with conflicting confidence scores', () => {
398
+ const speckit = new SpecKitFormatAdapter();
399
+ const generic = new GenericMarkdownAdapter();
400
+
401
+ registry.register(speckit);
402
+ registry.register(generic);
403
+
404
+ // Content that could match multiple formats
405
+ const ambiguousContent = `# Some Document\n\nThis is a test with some content`;
406
+
407
+ const result = registry.detectAdapter(ambiguousContent, 10); // Lower threshold
408
+
409
+ // Should return an adapter if any match above threshold
410
+ if (result) {
411
+ expect(result.detection.confidence).toBeGreaterThanOrEqual(10);
412
+ } else {
413
+ // If no adapter matches even low threshold, that's also valid
414
+ expect(result).toBeUndefined();
415
+ }
416
+ });
417
+
418
+ it('should handle rapid sequential adapter registration', async () => {
419
+ const adapters = [
420
+ new SpecKitFormatAdapter(),
421
+ new BMADFormatAdapter(),
422
+ new GenericMarkdownAdapter(),
423
+ ];
424
+
425
+ // Register all adapters in rapid succession via microtasks
426
+ // Note: This tests sequential registration order, not true concurrency,
427
+ // as JavaScript microtasks execute sequentially within a single event loop tick
428
+ const registrations = adapters.map((adapter) =>
429
+ Promise.resolve().then(() => registry.register(adapter))
430
+ );
431
+
432
+ await Promise.all(registrations);
433
+
434
+ expect(registry.size).toBe(3);
435
+ });
436
+
437
+ it('should prevent duplicate adapter registration with same name', () => {
438
+ const adapter1 = new SpecKitFormatAdapter();
439
+ const adapter2 = new SpecKitFormatAdapter();
440
+
441
+ registry.register(adapter1);
442
+
443
+ expect(() => registry.register(adapter2)).toThrow(/already registered/i);
444
+ });
445
+
446
+ it('should handle registry operations after clear', () => {
447
+ registry.register(new SpecKitFormatAdapter());
448
+ registry.register(new BMADFormatAdapter());
449
+
450
+ expect(registry.size).toBe(2);
451
+
452
+ registry.clear();
453
+
454
+ expect(registry.size).toBe(0);
455
+
456
+ // Should be able to register again after clear
457
+ registry.register(new SpecKitFormatAdapter());
458
+ expect(registry.size).toBe(1);
459
+ });
460
+ });
461
+
462
+ describe('Error Recovery and Partial Parsing', () => {
463
+ it('should continue parsing after encountering invalid change - SpecKit', async () => {
464
+ const content = `# Specification
465
+
466
+ ## Intent
467
+
468
+ Test partial parsing
469
+
470
+ ## Changes
471
+
472
+ - file_create: valid1.ts - Valid change
473
+ - invalid_change_format_here
474
+ - file_update: valid2.ts - Another valid change
475
+ - more garbage data
476
+ - file_delete: valid3.ts - Final valid change`;
477
+
478
+ const result = await speckitAdapter.parse(content);
479
+
480
+ // Test validates that parser doesn't crash on invalid changes
481
+ expect(result).toBeDefined();
482
+ if (result.success && result.data) {
483
+ // Should capture at least some valid changes
484
+ expect(result.data.proposed_changes.length).toBeGreaterThan(0);
485
+ }
486
+ });
487
+
488
+ it('should provide detailed error information for multiple failures', async () => {
489
+ const content = `# Specification
490
+
491
+ ## Intent
492
+
493
+ ## Changes
494
+
495
+ - file_create: - Missing path
496
+ - file_update: test.ts
497
+ - file_delete:`;
498
+
499
+ const result = await speckitAdapter.parse(content);
500
+
501
+ // Should collect all errors
502
+ if (!result.success || result.errors) {
503
+ expect(result.errors).toBeDefined();
504
+ // Could have multiple errors for different invalid changes
505
+ }
506
+ });
507
+
508
+ it('should handle invalid requirement IDs gracefully - BMAD', async () => {
509
+ const content = `---
510
+ name: Invalid IDs Test
511
+ ---
512
+
513
+ FR-01: Valid requirement
514
+ INVALID-ID: Bad ID format
515
+ FR-02: Another valid requirement
516
+ : No ID at all
517
+ FR-03: Final valid requirement`;
518
+
519
+ const result = await bmadAdapter.parse(content);
520
+
521
+ if (result.success && result.data) {
522
+ // Should parse valid requirements even if some are invalid
523
+ expect(result.data.proposed_changes.length).toBeGreaterThan(0);
524
+ }
525
+ });
526
+ });
527
+
528
+ describe('Metadata Edge Cases', () => {
529
+ it('should handle circular reference-like structures in metadata', async () => {
530
+ // Can't have true circular references in JSON, but can test deep nesting
531
+ const content = `---
532
+ name: Deep Metadata
533
+ nested:
534
+ level1:
535
+ level2:
536
+ level3:
537
+ level4:
538
+ level5:
539
+ level6:
540
+ value: deep
541
+ ---
542
+
543
+ FR-01: Test deep metadata`;
544
+
545
+ const result = await bmadAdapter.parse(content);
546
+
547
+ expect(result.success).toBe(true);
548
+ });
549
+
550
+ it('should handle metadata with special characters in keys', async () => {
551
+ const content = `---
552
+ name: Special Keys
553
+ "key-with-dashes": value1
554
+ "key.with.dots": value2
555
+ "key with spaces": value3
556
+ "key:with:colons": value4
557
+ ---
558
+
559
+ FR-01: Test requirement`;
560
+
561
+ const result = await bmadAdapter.parse(content);
562
+
563
+ expect(result.success).toBe(true);
564
+ });
565
+
566
+ it('should handle very large metadata objects', async () => {
567
+ let yamlContent = '---\nname: Large Metadata\n';
568
+
569
+ // Add 100 metadata fields
570
+ for (let i = 0; i < 100; i++) {
571
+ yamlContent += `field${i}: value${i}\n`;
572
+ }
573
+
574
+ yamlContent += '---\n\nFR-01: Test requirement';
575
+
576
+ const result = await bmadAdapter.parse(yamlContent);
577
+
578
+ expect(result.success).toBe(true);
579
+ });
580
+
581
+ it('should handle null and undefined values in metadata', async () => {
582
+ const content = `---
583
+ name: Null Values Test
584
+ nullField: null
585
+ undefinedField:
586
+ emptyString: ""
587
+ ---
588
+
589
+ FR-01: Test requirement`;
590
+
591
+ const result = await bmadAdapter.parse(content);
592
+
593
+ expect(result.success).toBe(true);
594
+ });
595
+ });
596
+
597
+ describe('Performance and Large Content', () => {
598
+ it('should handle very large documents efficiently', async () => {
599
+ // Create a document with 200 changes
600
+ let content = '# Specification\n\n## Intent\n\nLarge document test\n\n## Changes\n\n';
601
+
602
+ for (let i = 0; i < 200; i++) {
603
+ content += `- file_create: src/file${i}.ts - Create file number ${i}\n`;
604
+ }
605
+
606
+ const startTime = Date.now();
607
+ const result = await speckitAdapter.parse(content);
608
+ const endTime = Date.now();
609
+
610
+ expect(result.success).toBe(true);
611
+
612
+ // Sanity check: should complete in reasonable time (< 30 seconds for 200 items)
613
+ // Using a generous threshold to avoid flaky tests on slow CI runners
614
+ expect(endTime - startTime).toBeLessThan(30000);
615
+
616
+ if (result.success && result.data) {
617
+ expect(result.data.proposed_changes.length).toBe(200);
618
+ }
619
+ });
620
+
621
+ it('should handle documents approaching 1MB in size', async () => {
622
+ // Create a document around 1MB
623
+ const largeDescription = 'A'.repeat(10000); // 10KB
624
+ let content = `---
625
+ name: Large Document
626
+ ---
627
+
628
+ `;
629
+
630
+ // Add 50 requirements with 10KB descriptions each = ~500KB
631
+ for (let i = 0; i < 50; i++) {
632
+ content += `FR-${i.toString().padStart(2, '0')}: ${largeDescription}\n\n`;
633
+ }
634
+
635
+ const sizeInBytes = Buffer.byteLength(content, 'utf8');
636
+ expect(sizeInBytes).toBeGreaterThan(500000); // > 500KB
637
+
638
+ const result = await bmadAdapter.parse(content);
639
+
640
+ expect(result).toBeDefined();
641
+ // Should either succeed or fail gracefully, not crash
642
+ });
643
+ });
644
+
645
+ describe('Round-trip Fidelity', () => {
646
+ it('should preserve information through parse-serialize cycle - SpecKit', async () => {
647
+ const originalContent = `# Specification
648
+
649
+ ## Intent
650
+
651
+ Test round-trip fidelity for SpecKit adapter with specific formatting.
652
+
653
+ ## Changes
654
+
655
+ - file_create: src/test.ts - Create test file
656
+ - file_update: src/app.ts - Update application
657
+ - file_delete: src/old.ts - Remove old file`;
658
+
659
+ const parseResult = await speckitAdapter.parse(originalContent);
660
+ expect(parseResult.success).toBe(true);
661
+
662
+ if (parseResult.success && parseResult.data) {
663
+ const serializeResult = await speckitAdapter.serialize(parseResult.data);
664
+ expect(serializeResult.success).toBe(true);
665
+
666
+ if (serializeResult.success && serializeResult.content) {
667
+ // Re-parse the serialized content
668
+ const reparseResult = await speckitAdapter.parse(serializeResult.content);
669
+
670
+ expect(reparseResult.success).toBe(true);
671
+
672
+ if (reparseResult.success && reparseResult.data) {
673
+ // Should have same number of changes
674
+ expect(reparseResult.data.proposed_changes.length).toBe(
675
+ parseResult.data.proposed_changes.length
676
+ );
677
+
678
+ // Should preserve intent
679
+ expect(reparseResult.data.intent).toContain('round-trip');
680
+ }
681
+ }
682
+ }
683
+ });
684
+
685
+ it('should handle conversion of unstructured content', async () => {
686
+ // Generic markdown might lose some structure when converting to APS
687
+ const genericContent = `# My Project Plan
688
+
689
+ ## Overview
690
+
691
+ This is a generic project document.
692
+
693
+ ## Tasks
694
+
695
+ - [ ] Task 1
696
+ - [ ] Task 2
697
+ - [x] Task 3 (completed)
698
+
699
+ ## Notes
700
+
701
+ Some additional notes here.`;
702
+
703
+ const result = await genericAdapter.parse(genericContent);
704
+
705
+ // Should parse successfully
706
+ expect(result).toBeDefined();
707
+ if (result.success) {
708
+ // Generic adapter should extract task-like content
709
+ expect(result.data).toBeDefined();
710
+ }
711
+ });
712
+ });
713
+
714
+ describe('Context Handling', () => {
715
+ it('should handle parse context with all fields', async () => {
716
+ const content = `# Specification
717
+
718
+ ## Intent
719
+
720
+ Test context handling
721
+
722
+ ## Changes
723
+
724
+ - file_create: test.ts`;
725
+
726
+ const context: ParseContext = {
727
+ repositoryPath: 'https://github.com/test/repo',
728
+ branch: 'feature/test',
729
+ commit: 'abc123def456',
730
+ author: 'Test User <test@example.com>',
731
+ timestamp: new Date().toISOString(),
732
+ metadata: {
733
+ customField: 'customValue',
734
+ nested: {
735
+ data: 'value',
736
+ },
737
+ },
738
+ };
739
+
740
+ const result = await speckitAdapter.parse(content, context);
741
+
742
+ expect(result.success).toBe(true);
743
+ });
744
+
745
+ it('should handle parse context with minimal fields', async () => {
746
+ const content = `# Specification
747
+
748
+ ## Intent
749
+
750
+ Minimal context
751
+
752
+ ## Changes
753
+
754
+ - file_create: test.ts`;
755
+
756
+ const context: ParseContext = {};
757
+
758
+ const result = await speckitAdapter.parse(content, context);
759
+
760
+ expect(result.success).toBe(true);
761
+ });
762
+ });
763
+
764
+ describe('Path Traversal Prevention', () => {
765
+ it('should strip traversal paths from SpecKit backtick extraction', async () => {
766
+ const content = `# Specification
767
+
768
+ ## Intent
769
+
770
+ Path traversal test
771
+
772
+ ## Changes
773
+
774
+ ### Create malicious file
775
+
776
+ \`../../../etc/passwd\`
777
+
778
+ ### Update safe file
779
+
780
+ \`src/app.ts\`
781
+
782
+ ### Absolute path attempt
783
+
784
+ \`/etc/shadow\``;
785
+
786
+ const result = await speckitAdapter.parse(content);
787
+
788
+ expect(result.success).toBe(true);
789
+ if (result.success && result.data) {
790
+ const paths = result.data.proposed_changes.map((c) => c.path);
791
+ // Traversal and absolute paths should be dropped from changes
792
+ expect(paths).not.toContain('../../../etc/passwd');
793
+ expect(paths).not.toContain('/etc/shadow');
794
+ // Safe path should be preserved
795
+ expect(paths).toContain('src/app.ts');
796
+ }
797
+ });
798
+
799
+ it('should strip traversal paths from SpecKit list items', async () => {
800
+ const content = `# Specification
801
+
802
+ ## Intent
803
+
804
+ List item path traversal test
805
+
806
+ ## Changes
807
+
808
+ - Create file \`../../secrets/key.pem\` for credentials
809
+ - Update \`src/config.ts\` with new settings
810
+ - Delete \`/absolute/path/file.ts\` permanently`;
811
+
812
+ const result = await speckitAdapter.parse(content);
813
+
814
+ expect(result.success).toBe(true);
815
+ if (result.success && result.data) {
816
+ const paths = result.data.proposed_changes.map((c) => c.path);
817
+ expect(paths).not.toContain('../../secrets/key.pem');
818
+ expect(paths).not.toContain('/absolute/path/file.ts');
819
+ expect(paths).toContain('src/config.ts');
820
+ }
821
+ });
822
+
823
+ it('should handle null bytes in paths - SpecKit', async () => {
824
+ const content = `# Specification
825
+
826
+ ## Intent
827
+
828
+ Null byte test
829
+
830
+ ## Changes
831
+
832
+ ### Create file
833
+
834
+ \`src/app\x00.ts\``;
835
+
836
+ const result = await speckitAdapter.parse(content);
837
+
838
+ expect(result.success).toBe(true);
839
+ if (result.success && result.data) {
840
+ const paths = result.data.proposed_changes.map((c) => c.path);
841
+ expect(paths).not.toContain('src/app\x00.ts');
842
+ }
843
+ });
844
+
845
+ it('should sanitise BMAD requirement ID-based paths', async () => {
846
+ const content = `---
847
+ name: Path Traversal Test
848
+ ---
849
+
850
+ FR-01: Valid requirement
851
+ FR-../../etc/passwd: Malicious requirement`;
852
+
853
+ const result = await bmadAdapter.parse(content);
854
+
855
+ expect(result.success).toBe(true);
856
+ if (result.success && result.data) {
857
+ for (const change of result.data.proposed_changes) {
858
+ if (change.path) {
859
+ expect(change.path).not.toContain('..');
860
+ expect(change.path.startsWith('/')).toBe(false);
861
+ }
862
+ }
863
+ }
864
+ });
865
+
866
+ it('should sanitise Generic parser generated paths', async () => {
867
+ const content = `# Project Plan
868
+
869
+ ## Tasks
870
+
871
+ - ../../../etc/passwd
872
+ - Normal task description
873
+ - /absolute/path/attack`;
874
+
875
+ const result = await genericAdapter.parse(content);
876
+
877
+ expect(result.success).toBe(true);
878
+ if (result.success && result.data) {
879
+ for (const change of result.data.proposed_changes) {
880
+ if (change.path) {
881
+ expect(change.path).not.toContain('..');
882
+ expect(change.path.startsWith('/')).toBe(false);
883
+ }
884
+ }
885
+ }
886
+ });
887
+ });
888
+
889
+ describe('File Extension and Format Detection', () => {
890
+ it('should detect format from content when extension is ambiguous', () => {
891
+ const speckitContent = `# Specification
892
+
893
+ ## Intent
894
+
895
+ This is clearly a SpecKit document
896
+
897
+ ## Changes
898
+
899
+ - file_create: test.ts`;
900
+
901
+ const bmadContent = `---
902
+ name: Test
903
+ ---
904
+
905
+ FR-01: This is clearly a BMAD document`;
906
+
907
+ const speckitDetection = speckitAdapter.detect(speckitContent);
908
+ const bmadDetection = bmadAdapter.detect(bmadContent);
909
+
910
+ // Both should be detected
911
+ expect(speckitDetection.detected).toBe(true);
912
+ expect(bmadDetection.detected).toBe(true);
913
+
914
+ // Each adapter should be most confident about its own format
915
+ expect(speckitDetection.confidence).toBeGreaterThanOrEqual(50);
916
+ expect(bmadDetection.confidence).toBeGreaterThanOrEqual(50);
917
+ });
918
+
919
+ it('should handle files with unusual extensions', () => {
920
+ // Adapters should rely on content, not just extensions
921
+ const content = `# Specification
922
+
923
+ ## Intent
924
+
925
+ Test content detection
926
+
927
+ ## Changes
928
+
929
+ - file_create: test.ts`;
930
+
931
+ const result = speckitAdapter.detect(content);
932
+
933
+ expect(result.detected).toBe(true);
934
+ expect(result.confidence).toBeGreaterThan(50);
935
+ });
936
+ });
937
+ });