@@ -5,7 +5,9 @@ import {testAppLinked} from '../../../models/app/app.test-data.js'
55import { Project } from '../../../models/project/project.js'
66import { selectActiveConfig } from '../../../models/project/active-config.js'
77import { errorsForConfig } from '../../../models/project/config-selection.js'
8+ import metadata from '../../../metadata.js'
89import { outputResult } from '@shopify/cli-kit/node/output'
10+ import { AbortError } from '@shopify/cli-kit/node/error'
911import { TomlFile } from '@shopify/cli-kit/node/toml/toml-file'
1012import { describe , expect , test , vi } from 'vitest'
1113
@@ -14,12 +16,19 @@ vi.mock('../../../services/validate.js')
1416vi . mock ( '../../../models/project/project.js' )
1517vi . mock ( '../../../models/project/active-config.js' )
1618vi . mock ( '../../../models/project/config-selection.js' )
19+ vi . mock ( '../../../metadata.js' , ( ) => ( { default : { addPublicMetadata : vi . fn ( ) } } ) )
1720vi . mock ( '@shopify/cli-kit/node/output' , async ( importOriginal ) => {
1821 const actual = await importOriginal < typeof import ( '@shopify/cli-kit/node/output' ) > ( )
1922 return { ...actual , outputResult : vi . fn ( ) }
2023} )
2124vi . mock ( '@shopify/cli-kit/node/ui' )
2225
26+ async function expectValidationMetadataCalls ( ...expectedMetadata : Record < string , unknown > [ ] ) {
27+ const metadataCalls = vi . mocked ( metadata . addPublicMetadata ) . mock . calls . map ( ( [ getMetadata ] ) => getMetadata )
28+ expect ( metadataCalls ) . toHaveLength ( expectedMetadata . length )
29+ await expect ( Promise . all ( metadataCalls . map ( ( getMetadata ) => getMetadata ( ) ) ) ) . resolves . toEqual ( expectedMetadata )
30+ }
31+
2332function mockHealthyProject ( ) {
2433 vi . mocked ( Project . load ) . mockResolvedValue ( { errors : [ ] } as unknown as Project )
2534 vi . mocked ( selectActiveConfig ) . mockResolvedValue ( { file : new TomlFile ( 'shopify.app.toml' , { } ) } as any )
@@ -36,6 +45,7 @@ describe('app config validate command', () => {
3645 await Validate . run ( [ ] , import . meta. url )
3746
3847 expect ( validateApp ) . toHaveBeenCalledWith ( app , { json : false } )
48+ await expectValidationMetadataCalls ( { cmd_app_validate_json : false } )
3949 } )
4050
4151 test ( 'calls validateApp with json: true when --json flag is passed' , async ( ) => {
@@ -47,6 +57,7 @@ describe('app config validate command', () => {
4757 await Validate . run ( [ '--json' ] , import . meta. url )
4858
4959 expect ( validateApp ) . toHaveBeenCalledWith ( app , { json : true } )
60+ await expectValidationMetadataCalls ( { cmd_app_validate_json : true } )
5061 } )
5162
5263 test ( 'calls validateApp with json: true when -j flag is passed' , async ( ) => {
@@ -58,6 +69,7 @@ describe('app config validate command', () => {
5869 await Validate . run ( [ '-j' ] , import . meta. url )
5970
6071 expect ( validateApp ) . toHaveBeenCalledWith ( app , { json : true } )
72+ await expectValidationMetadataCalls ( { cmd_app_validate_json : true } )
6173 } )
6274
6375 test ( 'outputs JSON issues when active config has TOML parse errors' , async ( ) => {
@@ -71,5 +83,88 @@ describe('app config validate command', () => {
7183
7284 expect ( outputResult ) . toHaveBeenCalledWith ( expect . stringContaining ( '"valid": false' ) )
7385 expect ( linkedAppContext ) . not . toHaveBeenCalled ( )
86+ await expectValidationMetadataCalls (
87+ { cmd_app_validate_json : true } ,
88+ {
89+ cmd_app_validate_valid : false ,
90+ cmd_app_validate_issue_count : 1 ,
91+ cmd_app_validate_file_count : 1 ,
92+ } ,
93+ )
94+ } )
95+
96+ test ( 'records failure metadata for config errors in non-json mode' , async ( ) => {
97+ vi . mocked ( Project . load ) . mockResolvedValue ( { errors : [ ] } as unknown as Project )
98+ vi . mocked ( selectActiveConfig ) . mockResolvedValue ( { file : new TomlFile ( 'shopify.app.toml' , { } ) } as any )
99+ vi . mocked ( errorsForConfig ) . mockReturnValue ( [
100+ { path : '/app/shopify.app.toml' , message : 'Missing required field' } as any ,
101+ { path : '/app/shopify.app.toml' , message : 'Invalid value' } as any ,
102+ ] )
103+
104+ await expect ( Validate . run ( [ ] , import . meta. url ) ) . rejects . toThrow ( )
105+
106+ expect ( linkedAppContext ) . not . toHaveBeenCalled ( )
107+ await expectValidationMetadataCalls (
108+ { cmd_app_validate_json : false } ,
109+ {
110+ cmd_app_validate_valid : false ,
111+ cmd_app_validate_issue_count : 2 ,
112+ cmd_app_validate_file_count : 1 ,
113+ } ,
114+ )
115+ } )
116+
117+ test ( 'records failure metadata when Project.load fails with --json' , async ( ) => {
118+ vi . mocked ( Project . load ) . mockRejectedValue ( new AbortError ( 'Could not find app configuration' ) )
119+
120+ await expect ( Validate . run ( [ '--json' ] , import . meta. url ) ) . rejects . toThrow ( )
121+
122+ expect ( outputResult ) . toHaveBeenCalledWith ( expect . stringContaining ( '"valid": false' ) )
123+ expect ( selectActiveConfig ) . not . toHaveBeenCalled ( )
124+ await expectValidationMetadataCalls (
125+ { cmd_app_validate_json : true } ,
126+ {
127+ cmd_app_validate_valid : false ,
128+ cmd_app_validate_issue_count : 1 ,
129+ cmd_app_validate_file_count : 1 ,
130+ } ,
131+ )
132+ } )
133+
134+ test ( 'records failure metadata when selectActiveConfig fails with --json' , async ( ) => {
135+ vi . mocked ( Project . load ) . mockResolvedValue ( { errors : [ ] } as unknown as Project )
136+ vi . mocked ( selectActiveConfig ) . mockRejectedValue ( new AbortError ( 'No config found' ) )
137+
138+ await expect ( Validate . run ( [ '--json' ] , import . meta. url ) ) . rejects . toThrow ( )
139+
140+ expect ( outputResult ) . toHaveBeenCalledWith ( expect . stringContaining ( '"valid": false' ) )
141+ expect ( linkedAppContext ) . not . toHaveBeenCalled ( )
142+ await expectValidationMetadataCalls (
143+ { cmd_app_validate_json : true } ,
144+ {
145+ cmd_app_validate_valid : false ,
146+ cmd_app_validate_issue_count : 1 ,
147+ cmd_app_validate_file_count : 1 ,
148+ } ,
149+ )
150+ } )
151+
152+ test ( 'records failure metadata when linkedAppContext throws a validation error with --json' , async ( ) => {
153+ vi . mocked ( Project . load ) . mockResolvedValue ( { errors : [ ] } as unknown as Project )
154+ vi . mocked ( selectActiveConfig ) . mockResolvedValue ( { file : new TomlFile ( 'shopify.app.toml' , { } ) } as any )
155+ vi . mocked ( errorsForConfig ) . mockReturnValue ( [ ] )
156+ vi . mocked ( linkedAppContext ) . mockRejectedValue ( new AbortError ( 'Validation errors in /app/shopify.app.toml' ) )
157+
158+ await expect ( Validate . run ( [ '--json' ] , import . meta. url ) ) . rejects . toThrow ( )
159+
160+ expect ( outputResult ) . toHaveBeenCalledWith ( expect . stringContaining ( '"valid": false' ) )
161+ await expectValidationMetadataCalls (
162+ { cmd_app_validate_json : true } ,
163+ {
164+ cmd_app_validate_valid : false ,
165+ cmd_app_validate_issue_count : 1 ,
166+ cmd_app_validate_file_count : 1 ,
167+ } ,
168+ )
74169 } )
75170} )
0 commit comments