@@ -348,3 +348,117 @@ describe("Initialization guard", () => {
348348 expect ( ids ) . toContain ( "legacy" ) ;
349349 } ) ;
350350} ) ;
351+
352+ // ═══════════════════════════════════════════════════════════════════════════
353+ // 7. Bundle chunk isolation (#40096)
354+ //
355+ // Published builds may split the context-engine registry across multiple
356+ // output chunks. The Symbol.for() keyed global ensures that a plugin
357+ // calling registerContextEngine() from chunk A is visible to
358+ // resolveContextEngine() imported from chunk B.
359+ //
360+ // These tests exercise the invariant that failed in 2026.3.7 when
361+ // lossless-claw registered successfully but resolution could not find it.
362+ // ═══════════════════════════════════════════════════════════════════════════
363+
364+ describe ( "Bundle chunk isolation (#40096)" , ( ) => {
365+ it ( "Symbol.for key is stable across independently loaded modules" , async ( ) => {
366+ // Simulate two distinct bundle chunks by loading the registry module
367+ // twice with different query strings (forces separate module instances
368+ // in Vite/esbuild but shares globalThis).
369+ const ts = Date . now ( ) . toString ( 36 ) ;
370+ const registryUrl = new URL ( "./registry.ts" , import . meta. url ) . href ;
371+
372+ const chunkA = await import ( /* @vite -ignore */ `${ registryUrl } ?chunk=a-${ ts } ` ) ;
373+ const chunkB = await import ( /* @vite -ignore */ `${ registryUrl } ?chunk=b-${ ts } ` ) ;
374+
375+ // Chunk A registers an engine
376+ const engineId = `cross-chunk-${ ts } ` ;
377+ chunkA . registerContextEngine ( engineId , ( ) => new MockContextEngine ( ) ) ;
378+
379+ // Chunk B must see it
380+ expect ( chunkB . getContextEngineFactory ( engineId ) ) . toBeDefined ( ) ;
381+ expect ( chunkB . listContextEngineIds ( ) ) . toContain ( engineId ) ;
382+ } ) ;
383+
384+ it ( "resolveContextEngine from chunk B finds engine registered in chunk A" , async ( ) => {
385+ const ts = Date . now ( ) . toString ( 36 ) ;
386+ const registryUrl = new URL ( "./registry.ts" , import . meta. url ) . href ;
387+
388+ const chunkA = await import ( /* @vite -ignore */ `${ registryUrl } ?chunk=resolve-a-${ ts } ` ) ;
389+ const chunkB = await import ( /* @vite -ignore */ `${ registryUrl } ?chunk=resolve-b-${ ts } ` ) ;
390+
391+ const engineId = `resolve-cross-${ ts } ` ;
392+ chunkA . registerContextEngine ( engineId , ( ) => ( {
393+ info : { id : engineId , name : "Cross-chunk Engine" , version : "0.0.1" } ,
394+ async ingest ( ) {
395+ return { ingested : true } ;
396+ } ,
397+ async assemble ( { messages } : { messages : AgentMessage [ ] } ) {
398+ return { messages, estimatedTokens : 0 } ;
399+ } ,
400+ async compact ( ) {
401+ return { ok : true , compacted : false } ;
402+ } ,
403+ } ) ) ;
404+
405+ // Resolve from chunk B using a config that points to this engine
406+ const engine = await chunkB . resolveContextEngine ( configWithSlot ( engineId ) ) ;
407+ expect ( engine . info . id ) . toBe ( engineId ) ;
408+ } ) ;
409+
410+ it ( "plugin-sdk export path shares the same global registry" , async ( ) => {
411+ // The plugin-sdk re-exports registerContextEngine. Verify the
412+ // re-export writes to the same global symbol as the direct import.
413+ const ts = Date . now ( ) . toString ( 36 ) ;
414+ const engineId = `sdk-path-${ ts } ` ;
415+
416+ // Direct registry import
417+ registerContextEngine ( engineId , ( ) => new MockContextEngine ( ) ) ;
418+
419+ // Plugin-sdk import (different chunk path in the published bundle)
420+ const sdkUrl = new URL ( "../plugin-sdk/index.ts" , import . meta. url ) . href ;
421+ const sdk = await import ( /* @vite -ignore */ `${ sdkUrl } ?sdk-${ ts } ` ) ;
422+
423+ // The SDK export should see the engine we just registered
424+ const factory = getContextEngineFactory ( engineId ) ;
425+ expect ( factory ) . toBeDefined ( ) ;
426+
427+ // And registering from the SDK path should be visible from the direct path
428+ const sdkEngineId = `sdk-registered-${ ts } ` ;
429+ sdk . registerContextEngine ( sdkEngineId , ( ) => new MockContextEngine ( ) ) ;
430+ expect ( getContextEngineFactory ( sdkEngineId ) ) . toBeDefined ( ) ;
431+ } ) ;
432+
433+ it ( "concurrent registration from multiple chunks does not lose entries" , async ( ) => {
434+ const ts = Date . now ( ) . toString ( 36 ) ;
435+ const registryUrl = new URL ( "./registry.ts" , import . meta. url ) . href ;
436+ let releaseRegistrations : ( ( ) => void ) | undefined ;
437+ const registrationStart = new Promise < void > ( ( resolve ) => {
438+ releaseRegistrations = resolve ;
439+ } ) ;
440+
441+ // Load 5 "chunks" in parallel
442+ const chunks = await Promise . all (
443+ Array . from (
444+ { length : 5 } ,
445+ ( _ , i ) => import ( /* @vite -ignore */ `${ registryUrl } ?concurrent-${ ts } -${ i } ` ) ,
446+ ) ,
447+ ) ;
448+
449+ const ids = chunks . map ( ( _ , i ) => `concurrent-${ ts } -${ i } ` ) ;
450+ const registrationTasks = chunks . map ( async ( chunk , i ) => {
451+ const id = `concurrent-${ ts } -${ i } ` ;
452+ await registrationStart ;
453+ chunk . registerContextEngine ( id , ( ) => new MockContextEngine ( ) ) ;
454+ } ) ;
455+ releaseRegistrations ?.( ) ;
456+ await Promise . all ( registrationTasks ) ;
457+
458+ // All 5 engines must be visible from any chunk
459+ const allIds = chunks [ 0 ] . listContextEngineIds ( ) ;
460+ for ( const id of ids ) {
461+ expect ( allIds ) . toContain ( id ) ;
462+ }
463+ } ) ;
464+ } ) ;
0 commit comments