Skip to content

Commit fbf5d56

Browse files
Daniel Reisdsantoreisjalehman
authored
test(context-engine): add bundle chunk isolation tests for registry (#40460)
Merged via squash. Prepared head SHA: 44622ab Co-authored-by: dsantoreis <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
1 parent 98ea71a commit fbf5d56

File tree

2 files changed

+115
-0
lines changed

2 files changed

+115
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
1313
- macOS/LaunchAgent install: tighten LaunchAgent directory and plist permissions during install so launchd bootstrap does not fail when the target home path or generated plist inherited group/world-writable modes.
1414
- Gateway/Control UI: keep dashboard auth tokens in session-scoped browser storage so same-tab refreshes preserve remote token auth without restoring long-lived localStorage token persistence, while scoping tokens to the selected gateway URL and fragment-only bootstrap flow. (#40892) thanks @velvet-shark.
1515
- Models/Kimi Coding: send `anthropic-messages` tools in native Anthropic format again so `kimi-coding` stops degrading tool calls into XML/plain-text pseudo invocations instead of real `tool_use` blocks. (#38669, #39907, #40552) Thanks @opriz.
16+
- Context engine/tests: add bundled-registry regression coverage for cross-chunk resolution, plugin-sdk re-exports, and concurrent chunk registration. (#40460) thanks @dsantoreis.
1617

1718
## 2026.3.8
1819

src/context-engine/context-engine.test.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)