mirror of https://github.com/openclaw/openclaw.git
test(context-engine): add bundle chunk isolation tests for registry (#40460)
Merged via squash.
Prepared head SHA: 44622abfbc
Co-authored-by: dsantoreis <220753637+dsantoreis@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
parent
98ea71aca5
commit
fbf5d56366
|
|
@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
|||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- Context engine/tests: add bundled-registry regression coverage for cross-chunk resolution, plugin-sdk re-exports, and concurrent chunk registration. (#40460) thanks @dsantoreis.
|
||||
|
||||
## 2026.3.8
|
||||
|
||||
|
|
|
|||
|
|
@ -348,3 +348,117 @@ describe("Initialization guard", () => {
|
|||
expect(ids).toContain("legacy");
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 7. Bundle chunk isolation (#40096)
|
||||
//
|
||||
// Published builds may split the context-engine registry across multiple
|
||||
// output chunks. The Symbol.for() keyed global ensures that a plugin
|
||||
// calling registerContextEngine() from chunk A is visible to
|
||||
// resolveContextEngine() imported from chunk B.
|
||||
//
|
||||
// These tests exercise the invariant that failed in 2026.3.7 when
|
||||
// lossless-claw registered successfully but resolution could not find it.
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe("Bundle chunk isolation (#40096)", () => {
|
||||
it("Symbol.for key is stable across independently loaded modules", async () => {
|
||||
// Simulate two distinct bundle chunks by loading the registry module
|
||||
// twice with different query strings (forces separate module instances
|
||||
// in Vite/esbuild but shares globalThis).
|
||||
const ts = Date.now().toString(36);
|
||||
const registryUrl = new URL("./registry.ts", import.meta.url).href;
|
||||
|
||||
const chunkA = await import(/* @vite-ignore */ `${registryUrl}?chunk=a-${ts}`);
|
||||
const chunkB = await import(/* @vite-ignore */ `${registryUrl}?chunk=b-${ts}`);
|
||||
|
||||
// Chunk A registers an engine
|
||||
const engineId = `cross-chunk-${ts}`;
|
||||
chunkA.registerContextEngine(engineId, () => new MockContextEngine());
|
||||
|
||||
// Chunk B must see it
|
||||
expect(chunkB.getContextEngineFactory(engineId)).toBeDefined();
|
||||
expect(chunkB.listContextEngineIds()).toContain(engineId);
|
||||
});
|
||||
|
||||
it("resolveContextEngine from chunk B finds engine registered in chunk A", async () => {
|
||||
const ts = Date.now().toString(36);
|
||||
const registryUrl = new URL("./registry.ts", import.meta.url).href;
|
||||
|
||||
const chunkA = await import(/* @vite-ignore */ `${registryUrl}?chunk=resolve-a-${ts}`);
|
||||
const chunkB = await import(/* @vite-ignore */ `${registryUrl}?chunk=resolve-b-${ts}`);
|
||||
|
||||
const engineId = `resolve-cross-${ts}`;
|
||||
chunkA.registerContextEngine(engineId, () => ({
|
||||
info: { id: engineId, name: "Cross-chunk Engine", version: "0.0.1" },
|
||||
async ingest() {
|
||||
return { ingested: true };
|
||||
},
|
||||
async assemble({ messages }: { messages: AgentMessage[] }) {
|
||||
return { messages, estimatedTokens: 0 };
|
||||
},
|
||||
async compact() {
|
||||
return { ok: true, compacted: false };
|
||||
},
|
||||
}));
|
||||
|
||||
// Resolve from chunk B using a config that points to this engine
|
||||
const engine = await chunkB.resolveContextEngine(configWithSlot(engineId));
|
||||
expect(engine.info.id).toBe(engineId);
|
||||
});
|
||||
|
||||
it("plugin-sdk export path shares the same global registry", async () => {
|
||||
// The plugin-sdk re-exports registerContextEngine. Verify the
|
||||
// re-export writes to the same global symbol as the direct import.
|
||||
const ts = Date.now().toString(36);
|
||||
const engineId = `sdk-path-${ts}`;
|
||||
|
||||
// Direct registry import
|
||||
registerContextEngine(engineId, () => new MockContextEngine());
|
||||
|
||||
// Plugin-sdk import (different chunk path in the published bundle)
|
||||
const sdkUrl = new URL("../plugin-sdk/index.ts", import.meta.url).href;
|
||||
const sdk = await import(/* @vite-ignore */ `${sdkUrl}?sdk-${ts}`);
|
||||
|
||||
// The SDK export should see the engine we just registered
|
||||
const factory = getContextEngineFactory(engineId);
|
||||
expect(factory).toBeDefined();
|
||||
|
||||
// And registering from the SDK path should be visible from the direct path
|
||||
const sdkEngineId = `sdk-registered-${ts}`;
|
||||
sdk.registerContextEngine(sdkEngineId, () => new MockContextEngine());
|
||||
expect(getContextEngineFactory(sdkEngineId)).toBeDefined();
|
||||
});
|
||||
|
||||
it("concurrent registration from multiple chunks does not lose entries", async () => {
|
||||
const ts = Date.now().toString(36);
|
||||
const registryUrl = new URL("./registry.ts", import.meta.url).href;
|
||||
let releaseRegistrations: (() => void) | undefined;
|
||||
const registrationStart = new Promise<void>((resolve) => {
|
||||
releaseRegistrations = resolve;
|
||||
});
|
||||
|
||||
// Load 5 "chunks" in parallel
|
||||
const chunks = await Promise.all(
|
||||
Array.from(
|
||||
{ length: 5 },
|
||||
(_, i) => import(/* @vite-ignore */ `${registryUrl}?concurrent-${ts}-${i}`),
|
||||
),
|
||||
);
|
||||
|
||||
const ids = chunks.map((_, i) => `concurrent-${ts}-${i}`);
|
||||
const registrationTasks = chunks.map(async (chunk, i) => {
|
||||
const id = `concurrent-${ts}-${i}`;
|
||||
await registrationStart;
|
||||
chunk.registerContextEngine(id, () => new MockContextEngine());
|
||||
});
|
||||
releaseRegistrations?.();
|
||||
await Promise.all(registrationTasks);
|
||||
|
||||
// All 5 engines must be visible from any chunk
|
||||
const allIds = chunks[0].listContextEngineIds();
|
||||
for (const id of ids) {
|
||||
expect(allIds).toContain(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue