@@ -3407,6 +3407,185 @@ describe('runExport / runImport', () => {
34073407 ) ,
34083408 ) . rejects . toMatchObject ( { code : 'VALIDATION_ERROR' , exitCode : 5 } ) ;
34093409 } ) ;
3410+
3411+ it ( 'import accepts a BOM-prefixed definition file (PowerShell utf8 writes one)' , async ( ) => {
3412+ const { credentialsPath } = makeCreds ( ) ;
3413+ const dir = mkdtempSync ( join ( tmpdir ( ) , 'cli-import-bom-' ) ) ;
3414+ const file = join ( dir , 'def.testsprite.json' ) ;
3415+ writeFileSync (
3416+ file ,
3417+ `${ JSON . stringify ( {
3418+ schemaVersion : 1 ,
3419+ projectId : 'project_alice' ,
3420+ type : 'backend' ,
3421+ name : 'BOM test' ,
3422+ } ) } `,
3423+ 'utf8' ,
3424+ ) ;
3425+ const fetchImpl = ( async ( ) =>
3426+ new Response ( JSON . stringify ( { testId : 'test_bom' } ) , {
3427+ status : 200 ,
3428+ headers : { 'content-type' : 'application/json' } ,
3429+ } ) ) as typeof fetch ;
3430+ const result = await runImport (
3431+ { profile : 'default' , output : 'json' , debug : false , file } ,
3432+ { credentialsPath, fetchImpl, stdout : ( ) => undefined , stderr : ( ) => undefined } ,
3433+ ) ;
3434+ expect ( result ) . toEqual ( { testId : 'test_bom' , action : 'created' } ) ;
3435+ } ) ;
3436+
3437+ it ( 'import splits missing-file from malformed-JSON errors' , async ( ) => {
3438+ const dir = mkdtempSync ( join ( tmpdir ( ) , 'cli-import-err-' ) ) ;
3439+ await expect (
3440+ runImport (
3441+ { profile : 'default' , output : 'json' , debug : false , file : join ( dir , 'nope.json' ) } ,
3442+ { stdout : ( ) => undefined } ,
3443+ ) ,
3444+ ) . rejects . toMatchObject ( {
3445+ code : 'VALIDATION_ERROR' ,
3446+ nextAction : expect . stringContaining ( 'file not found' ) ,
3447+ } ) ;
3448+ const bad = join ( dir , 'bad.json' ) ;
3449+ writeFileSync ( bad , '{not json' , 'utf8' ) ;
3450+ await expect (
3451+ runImport (
3452+ { profile : 'default' , output : 'json' , debug : false , file : bad } ,
3453+ { stdout : ( ) => undefined } ,
3454+ ) ,
3455+ ) . rejects . toMatchObject ( {
3456+ code : 'VALIDATION_ERROR' ,
3457+ nextAction : expect . stringContaining ( 'is not valid JSON' ) ,
3458+ } ) ;
3459+ } ) ;
3460+
3461+ it ( 'import with codeVersion null sends If-Match: * and warns about the unconditional overwrite' , async ( ) => {
3462+ const { credentialsPath } = makeCreds ( ) ;
3463+ const dir = mkdtempSync ( join ( tmpdir ( ) , 'cli-import-null-' ) ) ;
3464+ const file = join ( dir , 'def.testsprite.json' ) ;
3465+ writeFileSync (
3466+ file ,
3467+ JSON . stringify ( {
3468+ schemaVersion : 1 ,
3469+ testId : 'test_be' ,
3470+ projectId : 'project_alice' ,
3471+ type : 'backend' ,
3472+ name : 'Legacy row' ,
3473+ code : { language : 'python' , framework : 'pytest' , body : 'print(3)\n' , codeVersion : null } ,
3474+ } ) ,
3475+ 'utf8' ,
3476+ ) ;
3477+ const seen : Array < { url : string ; ifMatch : string | null } > = [ ] ;
3478+ const fetchImpl = ( async ( input : Parameters < typeof fetch > [ 0 ] , init : RequestInit = { } ) => {
3479+ const headers = new Headers ( init . headers ) ;
3480+ seen . push ( { url : String ( input ) , ifMatch : headers . get ( 'if-match' ) } ) ;
3481+ return new Response ( JSON . stringify ( { testId : 'test_be' } ) , {
3482+ status : 200 ,
3483+ headers : { 'content-type' : 'application/json' } ,
3484+ } ) ;
3485+ } ) as typeof fetch ;
3486+ const errs : string [ ] = [ ] ;
3487+ await runImport (
3488+ { profile : 'default' , output : 'json' , debug : false , file } ,
3489+ { credentialsPath, fetchImpl, stdout : ( ) => undefined , stderr : line => errs . push ( line ) } ,
3490+ ) ;
3491+ expect ( seen [ 1 ] ! . url ) . toContain ( '/code' ) ;
3492+ expect ( seen [ 1 ] ! . ifMatch ) . toBe ( '*' ) ;
3493+ expect ( errs . join ( '\n' ) ) . toContain ( 'If-Match: *' ) ;
3494+ } ) ;
3495+
3496+ it ( 'import mints one echoed idempotency key and derives :meta/:code from a supplied one' , async ( ) => {
3497+ const { credentialsPath } = makeCreds ( ) ;
3498+ const dir = mkdtempSync ( join ( tmpdir ( ) , 'cli-import-idem-' ) ) ;
3499+ const file = join ( dir , 'def.testsprite.json' ) ;
3500+ writeFileSync (
3501+ file ,
3502+ JSON . stringify ( {
3503+ schemaVersion : 1 ,
3504+ testId : 'test_be' ,
3505+ projectId : 'project_alice' ,
3506+ type : 'backend' ,
3507+ name : 'Idem test' ,
3508+ code : { language : 'python' , framework : 'pytest' , body : 'print(4)\n' , codeVersion : 'v3' } ,
3509+ } ) ,
3510+ 'utf8' ,
3511+ ) ;
3512+ const keys : Array < string | null > = [ ] ;
3513+ const fetchImpl = ( async ( input : Parameters < typeof fetch > [ 0 ] , init : RequestInit = { } ) => {
3514+ keys . push ( new Headers ( init . headers ) . get ( 'idempotency-key' ) ) ;
3515+ return new Response ( JSON . stringify ( { testId : 'test_be' } ) , {
3516+ status : 200 ,
3517+ headers : { 'content-type' : 'application/json' } ,
3518+ } ) ;
3519+ } ) as typeof fetch ;
3520+ const errs : string [ ] = [ ] ;
3521+ await runImport (
3522+ { profile : 'default' , output : 'json' , debug : false , file, idempotencyKey : 'ci-key-1' } ,
3523+ { credentialsPath, fetchImpl, stdout : ( ) => undefined , stderr : line => errs . push ( line ) } ,
3524+ ) ;
3525+ expect ( keys ) . toEqual ( [ 'ci-key-1:meta' , 'ci-key-1:code' ] ) ;
3526+ expect ( errs . join ( '\n' ) ) . not . toContain ( 'idempotency-key:' ) ;
3527+
3528+ keys . length = 0 ;
3529+ await runImport (
3530+ { profile : 'default' , output : 'json' , debug : false , file } ,
3531+ { credentialsPath, fetchImpl, stdout : ( ) => undefined , stderr : line => errs . push ( line ) } ,
3532+ ) ;
3533+ expect ( keys [ 0 ] ) . toMatch ( / ^ c l i - i m p o r t - .+ : m e t a $ / ) ;
3534+ expect ( keys [ 1 ] ) . toMatch ( / ^ c l i - i m p o r t - .+ : c o d e $ / ) ;
3535+ expect ( errs . join ( '\n' ) ) . toContain ( 'idempotency-key: cli-import-' ) ;
3536+ } ) ;
3537+
3538+ it ( 'export --out writes the file, refuses to overwrite without --force, overwrites with it' , async ( ) => {
3539+ const { credentialsPath } = makeCreds ( ) ;
3540+ const fetchImpl = makeFetch ( url => ( { body : url . includes ( '/code' ) ? CODE_ROW : TEST_ROW } ) ) ;
3541+ const dir = mkdtempSync ( join ( tmpdir ( ) , 'cli-export-out-' ) ) ;
3542+ const outFile = join ( dir , 'def.testsprite.json' ) ;
3543+ const errs : string [ ] = [ ] ;
3544+ await runExport (
3545+ {
3546+ profile : 'default' ,
3547+ output : 'json' ,
3548+ debug : false ,
3549+ testId : 'test_be' ,
3550+ out : outFile ,
3551+ force : false ,
3552+ } ,
3553+ { credentialsPath, fetchImpl, stdout : ( ) => undefined , stderr : line => errs . push ( line ) } ,
3554+ ) ;
3555+ const written = JSON . parse ( readFileSync ( outFile , 'utf8' ) ) as { testId ?: string } ;
3556+ expect ( written ) . toMatchObject ( { schemaVersion : 1 , testId : 'test_be' } ) ;
3557+ expect ( errs . join ( '\n' ) ) . toContain ( 'Definition written to' ) ;
3558+
3559+ await expect (
3560+ runExport (
3561+ {
3562+ profile : 'default' ,
3563+ output : 'json' ,
3564+ debug : false ,
3565+ testId : 'test_be' ,
3566+ out : outFile ,
3567+ force : false ,
3568+ } ,
3569+ { credentialsPath, fetchImpl, stdout : ( ) => undefined , stderr : ( ) => undefined } ,
3570+ ) ,
3571+ ) . rejects . toMatchObject ( {
3572+ code : 'VALIDATION_ERROR' ,
3573+ nextAction : expect . stringContaining ( 'already exists' ) ,
3574+ } ) ;
3575+
3576+ await runExport (
3577+ {
3578+ profile : 'default' ,
3579+ output : 'json' ,
3580+ debug : false ,
3581+ testId : 'test_be' ,
3582+ out : outFile ,
3583+ force : true ,
3584+ } ,
3585+ { credentialsPath, fetchImpl, stdout : ( ) => undefined , stderr : ( ) => undefined } ,
3586+ ) ;
3587+ expect ( JSON . parse ( readFileSync ( outFile , 'utf8' ) ) ) . toMatchObject ( { testId : 'test_be' } ) ;
3588+ } ) ;
34103589} ) ;
34113590
34123591describe ( 'runLint' , ( ) => {
0 commit comments