99 */
1010import { spawn } from 'node:child_process' ;
1111import { existsSync , mkdtempSync , rmSync } from 'node:fs' ;
12+ import { createServer , type IncomingMessage , type Server , type ServerResponse } from 'node:http' ;
1213import { tmpdir } from 'node:os' ;
1314import { fileURLToPath } from 'node:url' ;
1415import { resolve as resolvePath , dirname , join } from 'node:path' ;
1516import { encode , renderChallengeHeader } from '@inflowpayai/mpp' ;
17+ import { encodePaymentRequiredHeader } from '@x402/core/http' ;
18+ import type { PaymentRequired } from '@x402/core/types' ;
1619import { afterAll , beforeAll , describe , expect , it , vi } from 'vitest' ;
1720
1821vi . setConfig ( { testTimeout : 15_000 } ) ;
@@ -38,6 +41,8 @@ interface RunResult {
3841 exitCode : number ;
3942}
4043
44+ type TestServerHandler = ( req : IncomingMessage , res : ServerResponse ) => void ;
45+
4146function run ( args : string [ ] , env : NodeJS . ProcessEnv = { } ) : Promise < RunResult > {
4247 return new Promise ( ( resolveResult , reject ) => {
4348 const childEnv : NodeJS . ProcessEnv = {
@@ -81,6 +86,32 @@ function parseAgentJson(out: string): unknown {
8186 }
8287}
8388
89+ async function withSeller ( handler : TestServerHandler , test : ( url : string ) => Promise < void > ) : Promise < void > {
90+ const server = createServer ( handler ) ;
91+ await new Promise < void > ( ( resolveListening ) => {
92+ server . listen ( 0 , '127.0.0.1' , resolveListening ) ;
93+ } ) ;
94+ const address = server . address ( ) ;
95+ if ( typeof address !== 'object' || address === null ) {
96+ await closeServer ( server ) ;
97+ throw new Error ( 'test server did not expose a port' ) ;
98+ }
99+ try {
100+ await test ( `http://127.0.0.1:${ address . port } /paywalled` ) ;
101+ } finally {
102+ await closeServer ( server ) ;
103+ }
104+ }
105+
106+ function closeServer ( server : Server ) : Promise < void > {
107+ return new Promise ( ( resolveClosed , reject ) => {
108+ server . close ( ( err ) => {
109+ if ( err !== undefined ) reject ( err ) ;
110+ else resolveClosed ( ) ;
111+ } ) ;
112+ } ) ;
113+ }
114+
84115describe ( 'cli smoke' , ( ) => {
85116 it ( 'the build produces an executable dist/cli.js' , ( ) => {
86117 expect ( existsSync ( cliBin ) ) . toBe ( true ) ;
@@ -129,6 +160,69 @@ describe('cli smoke', () => {
129160 expect ( `${ result . stdout } ${ result . stderr } ` ) . toContain ( 'DECODE_FAILED' ) ;
130161 } ) ;
131162
163+ it ( 'mpp pay --format json propagates a delegated generator NO_FILTERED_MATCH error' , async ( ) => {
164+ const header = renderChallengeHeader ( {
165+ id : 'chal-1' ,
166+ realm : 'mpp.test' ,
167+ method : 'inflow' ,
168+ intent : 'charge' ,
169+ request : encode ( { amount : '10' , currency : 'USDC' , methodDetails : { rail : 'balance' } } ) ,
170+ } ) ;
171+ await withSeller (
172+ ( _req , res ) => {
173+ res . writeHead ( 402 , { 'WWW-Authenticate' : header } ) ;
174+ res . end ( 'payment required' ) ;
175+ } ,
176+ async ( url ) => {
177+ const result = await run (
178+ [ 'mpp' , 'pay' , url , '--rail' , 'instrument' , '--format' , 'json' , '--interval' , '0' , '--no-show-body' ] ,
179+ { INFLOW_API_KEY : 'test-api-key' } ,
180+ ) ;
181+ expect ( result . exitCode ) . toBe ( 1 ) ;
182+ expect ( `${ result . stdout } ${ result . stderr } ` ) . toContain ( 'NO_FILTERED_MATCH' ) ;
183+ expect ( result . stdout . trim ( ) ) . not . toBe ( '[]' ) ;
184+ } ,
185+ ) ;
186+ } ) ;
187+
188+ it ( 'x402 pay --format json propagates a delegated generator NO_FILTERED_MATCH error' , async ( ) => {
189+ const header = encodePaymentRequiredHeader ( {
190+ x402Version : 2 ,
191+ resource : { url : 'https://seller.test/api' , mimeType : 'application/json' } ,
192+ accepts : [
193+ {
194+ scheme : 'balance' ,
195+ network : 'inflow:1' ,
196+ amount : '500' ,
197+ payTo : 'inflow:abc' ,
198+ maxTimeoutSeconds : 60 ,
199+ asset : 'USDC' ,
200+ extra : { } ,
201+ } ,
202+ ] ,
203+ } satisfies PaymentRequired ) ;
204+ await withSeller (
205+ ( req , res ) => {
206+ if ( req . url === '/v1/transactions/x402-supported' ) {
207+ res . writeHead ( 200 , { 'Content-Type' : 'application/json' } ) ;
208+ res . end ( JSON . stringify ( { kinds : [ { scheme : 'balance' , network : 'inflow:1' , x402Version : 2 } ] } ) ) ;
209+ return ;
210+ }
211+ res . writeHead ( 402 , { 'PAYMENT-REQUIRED' : header } ) ;
212+ res . end ( 'payment required' ) ;
213+ } ,
214+ async ( url ) => {
215+ const result = await run (
216+ [ 'x402' , 'pay' , url , '--scheme' , 'exact' , '--format' , 'json' , '--interval' , '0' , '--no-show-body' ] ,
217+ { INFLOW_API_KEY : 'test-api-key' , INFLOW_BASE_URL : new URL ( url ) . origin } ,
218+ ) ;
219+ expect ( result . exitCode ) . toBe ( 1 ) ;
220+ expect ( `${ result . stdout } ${ result . stderr } ` ) . toContain ( 'NO_FILTERED_MATCH' ) ;
221+ expect ( result . stdout . trim ( ) ) . not . toBe ( '[]' ) ;
222+ } ,
223+ ) ;
224+ } ) ;
225+
132226 it ( 'x402 decode --format json emits a DECODE_FAILED error envelope on garbage input' , async ( ) => {
133227 const result = await run ( [ 'x402' , 'decode' , 'garbage' , '--format' , 'json' ] ) ;
134228 expect ( result . exitCode ) . not . toBe ( 0 ) ;
0 commit comments