44 isAudioResponse ,
55 isTranscriptionResponse ,
66 isVideoResponse ,
7+ matchesPattern ,
78} from "../helpers.js" ;
89import { matchFixture } from "../router.js" ;
910import type { Fixture , ChatCompletionRequest , FixtureResponse } from "../types.js" ;
@@ -26,6 +27,11 @@ describe("multimedia type guards", () => {
2627 expect ( isImageResponse ( r ) ) . toBe ( false ) ;
2728 } ) ;
2829
30+ test ( "isImageResponse rejects non-object image value" , ( ) => {
31+ const r = { image : "not-an-object" } as unknown as FixtureResponse ;
32+ expect ( isImageResponse ( r ) ) . toBe ( false ) ;
33+ } ) ;
34+
2935 test ( "isAudioResponse detects audio (string form)" , ( ) => {
3036 const r : FixtureResponse = { audio : "AAAA" , format : "mp3" } ;
3137 expect ( isAudioResponse ( r ) ) . toBe ( true ) ;
@@ -149,7 +155,54 @@ describe("endpoint filtering in matchFixture", () => {
149155 _endpointType : "image" ,
150156 } ;
151157
152- const first = matchFixture ( fixtures , imageReq , counts ) ;
153- expect ( first ) . toBe ( fixtures [ 0 ] ) ;
158+ // Pin the FULL sequence ordering this test claims to verify. matchFixture
159+ // gates a sequenced fixture on its match count equalling sequenceIndex but
160+ // does not itself mutate the count — the caller (journal) increments after
161+ // consuming a match, and crucially advances ALL sequenced siblings sharing
162+ // the same match criteria so the group shares one logical counter. Mimic
163+ // that here so each call advances to the next sequenceIndex, proving the
164+ // sequence resolves 0 → 1 in order and then exhausts.
165+ const advanceSequence = ( matched : Fixture ) : void => {
166+ for ( const f of fixtures ) {
167+ if ( f . match . sequenceIndex !== undefined ) {
168+ counts . set ( f , ( counts . get ( f ) ?? 0 ) + 1 ) ;
169+ }
170+ }
171+ // (matched is part of the group; the loop above already advanced it)
172+ void matched ;
173+ } ;
174+ const resolve = ( ) : Fixture | null => {
175+ const f = matchFixture ( fixtures , imageReq , counts ) ;
176+ if ( f ) advanceSequence ( f ) ;
177+ return f ;
178+ } ;
179+
180+ expect ( resolve ( ) ) . toBe ( fixtures [ 0 ] ) ;
181+ expect ( resolve ( ) ) . toBe ( fixtures [ 1 ] ) ;
182+ // The sequence is exhausted: no fixture has a sequenceIndex matching the
183+ // next shared count, so further requests no longer match.
184+ expect ( resolve ( ) ) . toBeNull ( ) ;
185+ } ) ;
186+ } ) ;
187+
188+ describe ( "matchesPattern" , ( ) => {
189+ test ( "does not mutate the caller's RegExp lastIndex" , ( ) => {
190+ // A global regex carries mutable `lastIndex` state. matchesPattern must
191+ // not leave that state mutated, or callers reusing the same regex object
192+ // (e.g. the search/rerank/moderation filter loops) get inconsistent
193+ // results on subsequent uses.
194+ const re = / g u i t a r / g;
195+ expect ( matchesPattern ( "guitar" , re ) ) . toBe ( true ) ;
196+ // After the call, the caller's own use of the same regex must behave as if
197+ // matchesPattern never touched it.
198+ expect ( re . lastIndex ) . toBe ( 0 ) ;
199+ expect ( re . test ( "guitar" ) ) . toBe ( true ) ;
200+ } ) ;
201+
202+ test ( "is consistent across repeated calls with the same global regex" , ( ) => {
203+ const re = / g / g;
204+ expect ( matchesPattern ( "guitar" , re ) ) . toBe ( true ) ;
205+ expect ( matchesPattern ( "guitar" , re ) ) . toBe ( true ) ;
206+ expect ( matchesPattern ( "guitar" , re ) ) . toBe ( true ) ;
154207 } ) ;
155208} ) ;
0 commit comments