@@ -5,6 +5,9 @@ import os from 'node:os';
55import path from 'node:path' ;
66import { execFileSync } from 'node:child_process' ;
77
8+ import PrepareSecurityRelease from '../../lib/prepare_security.js' ;
9+ import { writeSecurityReleaseDraft } from '../../lib/security-release/draft.js' ;
10+
811import {
912 checkoutOnSecurityReleaseBranch ,
1013 commitAndPushVulnerabilitiesJSON ,
@@ -118,4 +121,81 @@ describe('security release git state', { concurrency: false }, () => {
118121 assert . match ( prompts [ 0 ] , / g i t c o m m i t / ) ;
119122 assert . strictEqual ( git ( 'write-tree' ) , index ) ;
120123 } ) ;
124+
125+ it ( 'rejects an existing draft before prompting or fetching reports' , async ( t ) => {
126+ const dir = repository ( t ) ;
127+ const file = writeSecurityReleaseDraft ( dir , { releaseDate : 'TBD' , reports : [ ] } ) ;
128+ const before = fs . readFileSync ( file ) ;
129+ const release = new PrepareSecurityRelease ( {
130+ prompt ( ) { assert . fail ( 'Existing releases must not start again' ) ; }
131+ } ) ;
132+ await assert . rejects ( release . start ( ) , / d r a f t a l r e a d y e x i s t s / ) ;
133+ assert . strictEqual ( git ( 'branch' , '--show-current' ) , 'main' ) ;
134+ assert . deepStrictEqual ( fs . readFileSync ( file ) , before ) ;
135+ } ) ;
136+
137+ it ( 'preserves Git state if draft preparation fails' , async ( t ) => {
138+ repository ( t ) ;
139+ const release = new PrepareSecurityRelease ( cli ) ;
140+ release . chooseReports = async ( ) => [ ] ;
141+ release . getDependencyUpdates = async ( ) => {
142+ throw new Error ( 'Preparation interrupted' ) ;
143+ } ;
144+ await assert . rejects (
145+ release . startVulnerabilitiesJSONCreation ( 'TBD' , 'Release' ) , / P r e p a r a t i o n i n t e r r u p t e d / ) ;
146+ assert . strictEqual ( git ( 'branch' , '--show-current' ) , 'main' ) ;
147+ assert . strictEqual ( git ( 'branch' , '--list' , 'next-security-release' ) , '' ) ;
148+ assert . strictEqual ( git ( 'status' , '--porcelain' ) , '' ) ;
149+ } ) ;
150+
151+ it ( 'preserves a draft discovered on the existing release branch' , async ( t ) => {
152+ const dir = repository ( t ) ;
153+ git ( 'checkout' , '-b' , 'next-security-release' ) ;
154+ const file = writeSecurityReleaseDraft ( dir , { releaseDate : 'TBD' , reports : [ ] } ) ;
155+ const before = fs . readFileSync ( file ) ;
156+ git ( 'add' , 'security-release' ) ;
157+ git ( 'commit' , '-m' , 'Existing release' ) ;
158+ const head = git ( 'rev-parse' , 'HEAD' ) ;
159+ git ( 'checkout' , 'main' ) ;
160+ assert . ok ( ! fs . existsSync ( file ) ) ;
161+
162+ const release = new PrepareSecurityRelease ( cli ) ;
163+ release . chooseReports = async ( ) => [ ] ;
164+ release . getDependencyUpdates = async ( ) => ( { } ) ;
165+ await assert . rejects (
166+ release . startVulnerabilitiesJSONCreation ( '2026-10-06' , 'Release' ) , / d r a f t a l r e a d y e x i s t s / ) ;
167+
168+ assert . deepStrictEqual ( fs . readFileSync ( file ) , before ) ;
169+ assert . strictEqual ( git ( 'rev-parse' , 'HEAD' ) , head ) ;
170+ assert . strictEqual ( git ( 'status' , '--porcelain' ) , '' ) ;
171+ } ) ;
172+
173+ it ( 'creates a local draft without committing when publication is declined' , async ( t ) => {
174+ const dir = repository ( t ) ;
175+ fs . writeFileSync ( 'user-notes.txt' , 'Staged user work\n' ) ;
176+ git ( 'add' , 'user-notes.txt' ) ;
177+ const index = git ( 'write-tree' ) ;
178+ const head = git ( 'rev-parse' , 'HEAD' ) ;
179+ const release = new PrepareSecurityRelease ( {
180+ ...cli , startSpinner ( ) { } , stopSpinner ( ) { }
181+ } ) ;
182+ release . chooseReports = async ( ) => [ ] ;
183+ release . getDependencyUpdates = async ( ) => ( {
184+ undici : { affectedVersions : { '24.x' : 'https://github.com/nodejs/node/pull/1' } }
185+ } ) ;
186+ release . promptReviewVulnerabilitiesJSON = async ( ) => false ;
187+ release . createPullRequest = async ( ) => assert . fail ( 'Do not publish a local draft' ) ;
188+
189+ await release . startVulnerabilitiesJSONCreation ( '2026/10/06' , 'Release' ) ;
190+
191+ const file = path . join ( dir , 'security-release/next-security-release/vulnerabilities.json' ) ;
192+ const draft = JSON . parse ( fs . readFileSync ( file , 'utf8' ) ) ;
193+ assert . strictEqual ( draft . releaseDate , '2026-10-06' ) ;
194+ assert . deepStrictEqual ( draft . reports , [ ] ) ;
195+ assert . strictEqual ( draft . dependencies . undici . affectedVersions [ '24.x' ] ,
196+ 'https://github.com/nodejs/node/pull/1' ) ;
197+ assert . strictEqual ( git ( 'write-tree' ) , index ) ;
198+ assert . strictEqual ( git ( 'rev-parse' , 'HEAD' ) , head ) ;
199+ assert . strictEqual ( git ( 'branch' , '--show-current' ) , 'next-security-release' ) ;
200+ } ) ;
121201} ) ;
0 commit comments