Skip to content

Commit d620bea

Browse files
committed
fix: add tests and cover the rest of requirement header places
1 parent 3c8d6e6 commit d620bea

4 files changed

Lines changed: 52 additions & 6 deletions

File tree

src/core/parsers/requirement-blocks.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function extractRequirementsSection(content: string): RequirementsSection
5858
let preambleLines: string[] = [];
5959

6060
// Collect preamble lines until first requirement header
61-
while (cursor < sectionBodyLines.length && !/^###\s+Requirement:/i.test(sectionBodyLines[cursor])) {
61+
while (cursor < sectionBodyLines.length && !REQUIREMENT_HEADER_REGEX.test(sectionBodyLines[cursor])) {
6262
preambleLines.push(sectionBodyLines[cursor]);
6363
cursor++;
6464
}
@@ -76,7 +76,7 @@ export function extractRequirementsSection(content: string): RequirementsSection
7676
cursor++;
7777
// Gather lines until next requirement header or end of section
7878
const bodyLines: string[] = [headerLineCandidate];
79-
while (cursor < sectionBodyLines.length && !/^###\s+Requirement:/i.test(sectionBodyLines[cursor]) && !/^##\s+/.test(sectionBodyLines[cursor])) {
79+
while (cursor < sectionBodyLines.length && !REQUIREMENT_HEADER_REGEX.test(sectionBodyLines[cursor]) && !/^##\s+/.test(sectionBodyLines[cursor])) {
8080
bodyLines.push(sectionBodyLines[cursor]);
8181
cursor++;
8282
}
@@ -176,15 +176,15 @@ function parseRequirementBlocksFromSection(sectionBody: string): RequirementBloc
176176
let i = 0;
177177
while (i < lines.length) {
178178
// Seek next requirement header
179-
while (i < lines.length && !/^###\s+Requirement:/i.test(lines[i])) i++;
179+
while (i < lines.length && !REQUIREMENT_HEADER_REGEX.test(lines[i])) i++;
180180
if (i >= lines.length) break;
181181
const headerLine = lines[i];
182182
const m = headerLine.match(REQUIREMENT_HEADER_REGEX);
183183
if (!m) { i++; continue; }
184184
const name = normalizeRequirementName(m[1]);
185185
const buf: string[] = [headerLine];
186186
i++;
187-
while (i < lines.length && !/^###\s+Requirement:/i.test(lines[i]) && !/^##\s+/.test(lines[i])) {
187+
while (i < lines.length && !REQUIREMENT_HEADER_REGEX.test(lines[i]) && !/^##\s+/.test(lines[i])) {
188188
buf.push(lines[i]);
189189
i++;
190190
}

src/core/parsers/spec-structure.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const REQUIREMENTS_SECTION_HEADER = /^##\s+Requirements\s*$/i;
22
const TOP_LEVEL_SECTION_HEADER = /^##\s+/;
33
const DELTA_HEADER = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\s*$/i;
4-
const REQUIREMENT_HEADER = /^###\s+Requirement:\s*(.+)\s*$/;
4+
const REQUIREMENT_HEADER = /^###\s+Requirement:\s*(.+)\s*$/i;
55

66
export interface MainSpecStructureIssue {
77
kind: 'delta-header' | 'requirement-outside-requirements';

src/core/specs-apply.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ export async function buildUpdatedSpec(
287287
throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - not found`);
288288
}
289289
// Replace block with provided raw (ensure header line matches key)
290-
const modHeaderMatch = mod.raw.split('\n')[0].match(/^###\s*Requirement:\s*(.+)\s*$/);
290+
const modHeaderMatch = mod.raw.split('\n')[0].match(/^###\s*Requirement:\s*(.+)\s*$/i);
291291
if (!modHeaderMatch || normalizeRequirementName(modHeaderMatch[1]) !== key) {
292292
throw new Error(
293293
`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - header mismatch in content`
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { extractRequirementsSection, parseDeltaSpec } from '../../../src/core/parsers/requirement-blocks.js';
3+
4+
describe('extractRequirementsSection', () => {
5+
it('parses canonical ### Requirement: headers', () => {
6+
const result = extractRequirementsSection(`## Requirements\n### Requirement: Foo\nThe system SHALL foo.\n`);
7+
expect(result.bodyBlocks.length).toBe(1);
8+
expect(result.bodyBlocks[0].name).toBe('Foo');
9+
});
10+
11+
it('regression: parses mixed-case ### requirement: headers without silently dropping them', () => {
12+
const variants = [
13+
'### requirement: Lowercase',
14+
'### REQUIREMENT: Uppercase',
15+
'### Requirement: Canonical',
16+
];
17+
for (const header of variants) {
18+
const result = extractRequirementsSection(`## Requirements\n${header}\nThe system SHALL foo.\n`);
19+
expect(result.bodyBlocks.length).toBeGreaterThan(0);
20+
expect(result.bodyBlocks[0].name).toBe(header.replace(/^###\s*requirement:\s*/i, ''));
21+
}
22+
});
23+
24+
it('regression: parses ###Requirement: header with no space after ### without silently dropping it', () => {
25+
const result = extractRequirementsSection(`## Requirements\n###Requirement: NoSpace\nThe system SHALL foo.\n`);
26+
expect(result.bodyBlocks.length).toBe(1);
27+
expect(result.bodyBlocks[0].name).toBe('NoSpace');
28+
});
29+
30+
it('regression: multiple blocks where first uses no-space header are all parsed', () => {
31+
const content = `## Requirements\n###Requirement: First\nThe system SHALL first.\n\n### Requirement: Second\nThe system SHALL second.\n`;
32+
const result = extractRequirementsSection(content);
33+
expect(result.bodyBlocks.length).toBe(2);
34+
expect(result.bodyBlocks[0].name).toBe('First');
35+
expect(result.bodyBlocks[1].name).toBe('Second');
36+
});
37+
});
38+
39+
describe('parseDeltaSpec', () => {
40+
it('regression: parses ###Requirement: header with no space in delta ADDED section', () => {
41+
const content = `## ADDED Requirements\n###Requirement: NoSpace\nThe system SHALL foo.\n`;
42+
const result = parseDeltaSpec(content);
43+
expect(result.added.length).toBe(1);
44+
expect(result.added[0].name).toBe('NoSpace');
45+
});
46+
});

0 commit comments

Comments
 (0)