-
Notifications
You must be signed in to change notification settings - Fork 4.6k
fix(core): canonicalize rebuilt spec EOF #1528
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+100
−1
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@fission-ai/openspec": patch | ||
| --- | ||
|
|
||
| Canonicalize rebuilt specs to end with exactly one final LF. Previously a spec whose `## Requirements` section was last was rebuilt with a trailing blank line (`\n\n`), which failed Markdown whitespace checks after sync or archive. Internal spacing and content after the Requirements section are unchanged. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import { afterEach, beforeEach, describe, expect, it } from 'vitest'; | ||
| import { promises as fs } from 'node:fs'; | ||
| import os from 'node:os'; | ||
| import path from 'node:path'; | ||
| import { buildUpdatedSpec, findSpecUpdates } from '../../src/core/specs-apply.js'; | ||
|
|
||
| const ORIGINAL_REQUIREMENT = [ | ||
| '### Requirement: Existing behavior', | ||
| 'The project SHALL expose the original behavior.', | ||
| '', | ||
| '#### Scenario: Existing path', | ||
| '- **WHEN** the behavior is exercised', | ||
| '- **THEN** it SHALL remain available', | ||
| ].join('\n'); | ||
|
|
||
| const UPDATED_REQUIREMENT = [ | ||
| '### Requirement: Existing behavior', | ||
| 'The project SHALL expose the updated behavior.', | ||
| '', | ||
| 'It SHALL preserve this meaningful internal paragraph break.', | ||
| '', | ||
| '#### Scenario: Existing path', | ||
| '- **WHEN** the behavior is exercised', | ||
| '- **THEN** it SHALL remain available', | ||
| ].join('\n'); | ||
|
|
||
| const BASE_SPEC = [ | ||
| '# demo Specification', | ||
| '', | ||
| '## Purpose', | ||
| 'Demonstrates deterministic serializer output.', | ||
| '', | ||
| '## Requirements', | ||
| ORIGINAL_REQUIREMENT, | ||
| ].join('\n'); | ||
|
|
||
| const DELTA_SPEC = ['## MODIFIED Requirements', '', UPDATED_REQUIREMENT, ''].join('\n'); | ||
|
|
||
| function expectOneFinalLf(content: string): void { | ||
| expect(content.endsWith('\n')).toBe(true); | ||
| expect(content.endsWith('\n\n')).toBe(false); | ||
| expect(content.match(/\n+$/)?.[0]).toBe('\n'); | ||
| } | ||
|
|
||
| describe('spec serialization', () => { | ||
| let tempDir: string; | ||
| let changeDir: string; | ||
| let mainSpecsDir: string; | ||
| let source: string; | ||
| let target: string; | ||
|
|
||
| beforeEach(async () => { | ||
| tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'openspec-spec-eof-')); | ||
| changeDir = path.join(tempDir, 'openspec', 'changes', 'serializer-test'); | ||
| mainSpecsDir = path.join(tempDir, 'openspec', 'specs'); | ||
| source = path.join(changeDir, 'specs', 'demo', 'spec.md'); | ||
| target = path.join(mainSpecsDir, 'demo', 'spec.md'); | ||
| await fs.mkdir(path.dirname(source), { recursive: true }); | ||
| await fs.mkdir(path.dirname(target), { recursive: true }); | ||
| await fs.writeFile(source, DELTA_SPEC); | ||
| }); | ||
|
|
||
| afterEach(async () => { | ||
| await fs.rm(tempDir, { recursive: true, force: true }); | ||
| }); | ||
|
|
||
| async function build(targetContent: string) { | ||
| await fs.writeFile(target, targetContent); | ||
| const [update] = await findSpecUpdates(changeDir, mainSpecsDir); | ||
| return buildUpdatedSpec(update, 'serializer-test', { silent: true }); | ||
| } | ||
|
|
||
| it.each([ | ||
| ['no terminal newline', BASE_SPEC], | ||
| ['one terminal LF', `${BASE_SPEC}\n`], | ||
| ['multiple terminal LF', `${BASE_SPEC}\n\n\n`], | ||
| ['multiple terminal CRLF', `${BASE_SPEC.replaceAll('\n', '\r\n')}\r\n\r\n`], | ||
| ])('emits one final LF from %s', async (_name, targetContent) => { | ||
| const result = await build(targetContent); | ||
|
|
||
| expectOneFinalLf(result.rebuilt); | ||
| expect(result.rebuilt).toContain( | ||
| 'updated behavior.\n\nIt SHALL preserve this meaningful internal paragraph break.' | ||
| ); | ||
| }); | ||
|
|
||
| it('preserves content after Requirements', async () => { | ||
| const result = await build(`${BASE_SPEC}\n\n## Notes\n\nKeep this later section.\n\n`); | ||
|
|
||
| expectOneFinalLf(result.rebuilt); | ||
| expect(result.rebuilt).toContain('## Notes\n\nKeep this later section.\n'); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.