@@ -3,9 +3,18 @@ import {linkedAppContext} from '../../../services/app-context.js'
33import { validateApp } from '../../../services/validate.js'
44import { testAppLinked } from '../../../models/app/app.test-data.js'
55import { describe , expect , test , vi } from 'vitest'
6+ import { AbortError } from '@shopify/cli-kit/node/error'
7+ import { outputResult } from '@shopify/cli-kit/node/output'
68
79vi . mock ( '../../../services/app-context.js' )
810vi . mock ( '../../../services/validate.js' )
11+ vi . mock ( '@shopify/cli-kit/node/output' , async ( importOriginal ) => {
12+ const actual = await importOriginal < typeof import ( '@shopify/cli-kit/node/output' ) > ( )
13+ return {
14+ ...actual ,
15+ outputResult : vi . fn ( ) ,
16+ }
17+ } )
918
1019describe ( 'app config validate command' , ( ) => {
1120 test ( 'calls validateApp with json: false by default' , async ( ) => {
@@ -46,4 +55,163 @@ describe('app config validate command', () => {
4655 // Then
4756 expect ( validateApp ) . toHaveBeenCalledWith ( app , { json : true } )
4857 } )
58+
59+ test ( 'outputs json issues when app loading aborts before validateApp runs' , async ( ) => {
60+ // Given
61+ vi . mocked ( linkedAppContext ) . mockRejectedValue (
62+ new AbortError ( 'Validation errors in /tmp/shopify.app.toml:\n\n• [name]: String is required' ) ,
63+ )
64+
65+ // When / Then
66+ await Validate . run ( [ '--json' , '--path=/tmp/app' ] , import . meta. url ) . catch ( ( ) => { } )
67+ expect ( outputResult ) . toHaveBeenCalledWith (
68+ JSON . stringify (
69+ {
70+ valid : false ,
71+ issues : [
72+ {
73+ filePath : '/tmp/shopify.app.toml' ,
74+ path : [ ] ,
75+ pathString : 'name' ,
76+ message : 'String is required' ,
77+ } ,
78+ ] ,
79+ } ,
80+ null,
81+ 2 ,
82+ ) ,
83+ )
84+ expect ( validateApp ) . not . toHaveBeenCalled( )
85+ } )
86+
87+ test ( 'outputs json issues when app loading aborts with ansi-colored structured text' , async ( ) => {
88+ // Given
89+ vi . mocked ( linkedAppContext ) . mockRejectedValue (
90+ new AbortError (
91+ '\u001b[1m\u001b[91mValidation errors\u001b[39m\u001b[22m in /tmp/shopify.app.toml:\n\n• [name]: String is required' ,
92+ ) ,
93+ )
94+
95+ // When / Then
96+ await Validate . run ( [ '--json' , '--path=/tmp/app' ] , import . meta. url ) . catch ( ( ) => { } )
97+ expect ( outputResult ) . toHaveBeenCalledWith (
98+ JSON . stringify (
99+ {
100+ valid : false ,
101+ issues : [
102+ {
103+ filePath : '/tmp/shopify.app.toml' ,
104+ path : [ ] ,
105+ pathString : 'name' ,
106+ message : 'String is required' ,
107+ } ,
108+ ] ,
109+ } ,
110+ null,
111+ 2 ,
112+ ) ,
113+ )
114+ expect ( validateApp ) . not . toHaveBeenCalled( )
115+ } )
116+
117+ test ( 'preserves a root json issue when contextual text precedes structured validation errors' , async ( ) => {
118+ // Given
119+ vi . mocked ( linkedAppContext ) . mockRejectedValue (
120+ new AbortError (
121+ 'Could not infer extension handle\n\nValidation errors in /tmp/shopify.app.toml:\n\n• [name]: String is required' ,
122+ ) ,
123+ )
124+
125+ // When / Then
126+ await Validate . run ( [ '--json' , '--path=/tmp/app' ] , import . meta. url ) . catch ( ( ) => { } )
127+ expect ( outputResult ) . toHaveBeenCalledWith (
128+ JSON . stringify (
129+ {
130+ valid : false ,
131+ issues : [
132+ {
133+ filePath : '/tmp/shopify.app.toml' ,
134+ path : [ ] ,
135+ pathString : 'name' ,
136+ message : 'String is required' ,
137+ } ,
138+ {
139+ filePath : '/tmp/shopify.app.toml' ,
140+ path : [ ] ,
141+ pathString : 'root' ,
142+ message :
143+ 'Could not infer extension handle\n\nValidation errors in /tmp/shopify.app.toml:\n\n• [name]: String is required' ,
144+ } ,
145+ ] ,
146+ } ,
147+ null ,
148+ 2 ,
149+ ) ,
150+ )
151+ expect ( validateApp ) . not . toHaveBeenCalled ( )
152+ } )
153+
154+ test ( 'parses structured validation errors for windows-style paths' , async ( ) => {
155+ // Given
156+ vi . mocked ( linkedAppContext ) . mockRejectedValue (
157+ new AbortError ( 'Validation errors in C:\\tmp\\shopify.app.toml:\n\n• [name]: String is required' ) ,
158+ )
159+
160+ // When / Then
161+ await Validate . run ( [ '--json' , '--path=/tmp/app' ] , import . meta. url ) . catch ( ( ) => { } )
162+ expect ( outputResult ) . toHaveBeenCalledWith (
163+ JSON . stringify (
164+ {
165+ valid : false ,
166+ issues : [
167+ {
168+ filePath : 'C:\\tmp\\shopify.app.toml' ,
169+ path : [ ] ,
170+ pathString : 'name' ,
171+ message : 'String is required' ,
172+ } ,
173+ ] ,
174+ } ,
175+ null,
176+ 2 ,
177+ ) ,
178+ )
179+ expect ( validateApp ) . not . toHaveBeenCalled( )
180+ } )
181+
182+ test ( 'outputs a root json issue when app loading aborts with a non-structured message' , async ( ) => {
183+ // Given
184+ vi . mocked ( linkedAppContext ) . mockRejectedValue ( new AbortError ( "Couldn't find an app toml file at /tmp/app" ) )
185+
186+ // When / Then
187+ await Validate . run ( [ '--json' , '--path=/tmp/app' ] , import . meta. url ) . catch ( ( ) => { } )
188+ expect ( outputResult ) . toHaveBeenCalledWith (
189+ JSON . stringify (
190+ {
191+ valid : false ,
192+ issues : [
193+ {
194+ filePath : '/tmp/app' ,
195+ path : [ ] ,
196+ pathString : 'root' ,
197+ message : "Couldn't find an app toml file at /tmp/app" ,
198+ } ,
199+ ] ,
200+ } ,
201+ null,
202+ 2 ,
203+ ) ,
204+ )
205+ expect ( validateApp ) . not . toHaveBeenCalled( )
206+ } )
207+
208+ test ( 'rethrows unrelated abort errors in json mode without converting them to validation json' , async ( ) => {
209+ // Given
210+ vi . mocked ( linkedAppContext ) . mockRejectedValue ( new AbortError ( 'Could not find store for domain shop.example.com' ) )
211+
212+ // When / Then
213+ await expect ( Validate . run ( [ '--json' , '--path=/tmp/app' ] , import . meta. url ) ) . rejects . toThrow ( )
214+ expect ( outputResult ) . not . toHaveBeenCalled ( )
215+ expect ( validateApp ) . not . toHaveBeenCalled( )
216+ } )
49217} )
0 commit comments