@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,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
|
+
});
|