@@ -7,7 +7,7 @@ import * as assert from 'assert';
77import * as path from 'path' ;
88import * as semver from 'semver' ;
99import { shellExec } from '../testUtils' ;
10- import { cpLocal , readLocalFile , rmLocal } from '../../spec-utils/pfs' ;
10+ import { cpLocal , readLocalFile , rmLocal , writeLocalFile } from '../../spec-utils/pfs' ;
1111
1212const pkg = require ( '../../../package.json' ) ;
1313
@@ -279,6 +279,44 @@ describe('Lockfile', function () {
279279 }
280280 } ) ;
281281
282+ it ( 'lockfile ends with trailing newline' , async ( ) => {
283+ const workspaceFolder = path . join ( __dirname , 'configs/lockfile' ) ;
284+
285+ const lockfilePath = path . join ( workspaceFolder , '.devcontainer-lock.json' ) ;
286+ await rmLocal ( lockfilePath , { force : true } ) ;
287+
288+ const res = await shellExec ( `${ cli } build --workspace-folder ${ workspaceFolder } --experimental-lockfile` ) ;
289+ const response = JSON . parse ( res . stdout ) ;
290+ assert . equal ( response . outcome , 'success' ) ;
291+ const actual = ( await readLocalFile ( lockfilePath ) ) . toString ( ) ;
292+ assert . ok ( actual . endsWith ( '\n' ) , 'Lockfile should end with a trailing newline' ) ;
293+ } ) ;
294+
295+ it ( 'frozen lockfile matches despite formatting differences' , async ( ) => {
296+ const workspaceFolder = path . join ( __dirname , 'configs/lockfile-frozen' ) ;
297+ const lockfilePath = path . join ( workspaceFolder , '.devcontainer-lock.json' ) ;
298+
299+ // Read the existing lockfile, strip trailing newline to create a byte-different but semantically identical file
300+ const original = ( await readLocalFile ( lockfilePath ) ) . toString ( ) ;
301+ const stripped = original . replace ( / \n $ / , '' ) ;
302+ assert . notEqual ( original , stripped , 'Test setup: should have removed trailing newline' ) ;
303+ assert . deepEqual ( JSON . parse ( original ) , JSON . parse ( stripped ) , 'Test setup: JSON content should be identical' ) ;
304+
305+ try {
306+ await writeLocalFile ( lockfilePath , Buffer . from ( stripped ) ) ;
307+
308+ // Frozen lockfile should succeed because JSON content is the same
309+ const res = await shellExec ( `${ cli } build --workspace-folder ${ workspaceFolder } --experimental-lockfile --experimental-frozen-lockfile` ) ;
310+ const response = JSON . parse ( res . stdout ) ;
311+ assert . equal ( response . outcome , 'success' , 'Frozen lockfile should not fail when only formatting differs' ) ;
312+ const actual = ( await readLocalFile ( lockfilePath ) ) . toString ( ) ;
313+ assert . strictEqual ( actual , stripped , 'Frozen lockfile should remain unchanged when only formatting differs' ) ;
314+ } finally {
315+ // Restore original lockfile
316+ await writeLocalFile ( lockfilePath , Buffer . from ( original ) ) ;
317+ }
318+ } ) ;
319+
282320 it ( 'upgrade command should work with default workspace folder' , async ( ) => {
283321 const workspaceFolder = path . join ( __dirname , 'configs/lockfile-upgrade-command' ) ;
284322 const absoluteTmpPath = path . resolve ( __dirname , 'tmp' ) ;
@@ -298,4 +336,68 @@ describe('Lockfile', function () {
298336 process . chdir ( originalCwd ) ;
299337 }
300338 } ) ;
339+
340+ it ( 'frozen lockfile fails when lockfile does not exist' , async ( ) => {
341+ const workspaceFolder = path . join ( __dirname , 'configs/lockfile-frozen-no-lockfile' ) ;
342+ const lockfilePath = path . join ( workspaceFolder , '.devcontainer-lock.json' ) ;
343+ await rmLocal ( lockfilePath , { force : true } ) ;
344+
345+ try {
346+ throw await shellExec ( `${ cli } build --workspace-folder ${ workspaceFolder } --experimental-lockfile --experimental-frozen-lockfile` ) ;
347+ } catch ( res ) {
348+ const response = JSON . parse ( res . stdout ) ;
349+ assert . equal ( response . outcome , 'error' ) ;
350+ assert . equal ( response . message , 'Lockfile does not exist.' ) ;
351+ }
352+ } ) ;
353+
354+ it ( 'corrupt lockfile causes build error' , async ( ) => {
355+ const workspaceFolder = path . join ( __dirname , 'configs/lockfile' ) ;
356+ const lockfilePath = path . join ( workspaceFolder , '.devcontainer-lock.json' ) ;
357+ const expectedPath = path . join ( workspaceFolder , 'expected.devcontainer-lock.json' ) ;
358+
359+ try {
360+ // Write invalid JSON to the lockfile
361+ await writeLocalFile ( lockfilePath , Buffer . from ( 'this is not valid json{{{' ) ) ;
362+
363+ try {
364+ throw await shellExec ( `${ cli } build --workspace-folder ${ workspaceFolder } --experimental-lockfile` ) ;
365+ } catch ( res ) {
366+ const response = JSON . parse ( res . stdout ) ;
367+ assert . equal ( response . outcome , 'error' ) ;
368+ }
369+ } finally {
370+ // Restore from the known-good expected lockfile
371+ await cpLocal ( expectedPath , lockfilePath ) ;
372+ }
373+ } ) ;
374+
375+ it ( 'no lockfile flags and no existing lockfile is a no-op' , async ( ) => {
376+ const workspaceFolder = path . join ( __dirname , 'configs/lockfile' ) ;
377+ const lockfilePath = path . join ( workspaceFolder , '.devcontainer-lock.json' ) ;
378+ const expectedPath = path . join ( workspaceFolder , 'expected.devcontainer-lock.json' ) ;
379+
380+ try {
381+ await rmLocal ( lockfilePath , { force : true } ) ;
382+
383+ // Build without any lockfile flags
384+ const res = await shellExec ( `${ cli } build --workspace-folder ${ workspaceFolder } ` ) ;
385+ const response = JSON . parse ( res . stdout ) ;
386+ assert . equal ( response . outcome , 'success' ) ;
387+
388+ // Lockfile should not have been created
389+ let exists = true ;
390+ await readLocalFile ( lockfilePath ) . catch ( err => {
391+ if ( err ?. code === 'ENOENT' ) {
392+ exists = false ;
393+ } else {
394+ throw err ;
395+ }
396+ } ) ;
397+ assert . equal ( exists , false , 'Lockfile should not be created when no lockfile flags are set' ) ;
398+ } finally {
399+ // Restore from the known-good expected lockfile
400+ await cpLocal ( expectedPath , lockfilePath ) ;
401+ }
402+ } ) ;
301403} ) ;
0 commit comments